diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEvent.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEvent.java index 44e4de3e90e6..49c4355ae3a8 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEvent.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEvent.java @@ -71,6 +71,8 @@ public final class JfrEvent { public static final JfrEvent AllocationRequiringGC = create("jdk.AllocationRequiringGC"); public static final JfrEvent OldObjectSample = create("jdk.OldObjectSample"); public static final JfrEvent ObjectAllocationSample = create("jdk.ObjectAllocationSample", JfrEventFlags.SupportsThrottling); + public static final JfrEvent NativeMemoryUsage = create("jdk.NativeMemoryUsage"); + public static final JfrEvent NativeMemoryUsageTotal = create("jdk.NativeMemoryUsageTotal"); private final long id; private final String name; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java index 3a7e70b4729c..0715c7c7f45c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java @@ -27,7 +27,6 @@ import java.util.Collections; import java.util.List; -import com.oracle.svm.core.sampler.SamplerStatistics; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.hosted.Feature; @@ -43,6 +42,7 @@ import com.oracle.svm.core.sampler.SamplerJfrStackTraceSerializer; import com.oracle.svm.core.sampler.SamplerStackTraceSerializer; import com.oracle.svm.core.sampler.SamplerStackWalkVisitor; +import com.oracle.svm.core.sampler.SamplerStatistics; import com.oracle.svm.core.thread.ThreadListenerSupport; import com.oracle.svm.core.thread.ThreadListenerSupportFeature; import com.oracle.svm.core.util.UserError; @@ -172,6 +172,9 @@ public void afterRegistration(AfterRegistrationAccess access) { JfrSerializerSupport.get().register(new JfrGCNameSerializer()); JfrSerializerSupport.get().register(new JfrVMOperationNameSerializer()); JfrSerializerSupport.get().register(new JfrGCWhenSerializer()); + if (VMInspectionOptions.hasNativeMemoryTrackingSupport()) { + JfrSerializerSupport.get().register(new JfrNmtCategorySerializer()); + } ThreadListenerSupport.get().register(SubstrateJVM.getThreadLocal()); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtMallocMemorySnapshot.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java old mode 100644 new mode 100755 similarity index 52% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtMallocMemorySnapshot.java rename to substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java index d5a8bca94a31..06b01b3a988d --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtMallocMemorySnapshot.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2023, 2023, Red Hat Inc. All rights reserved. + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,39 +24,27 @@ * questions. */ -package com.oracle.svm.core.nmt; +package com.oracle.svm.core.jfr; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; -import com.oracle.svm.core.Uninterruptible; - -class NmtMallocMemorySnapshot { - private final NmtMallocMemoryInfo[] categories; - private final NmtMallocMemoryInfo total; +import com.oracle.svm.core.nmt.NmtCategory; +public class JfrNmtCategorySerializer implements JfrSerializer { @Platforms(Platform.HOSTED_ONLY.class) - NmtMallocMemorySnapshot() { - total = new NmtMallocMemoryInfo(); - categories = new NmtMallocMemoryInfo[NmtCategory.values().length]; - for (int i = 0; i < categories.length; i++) { - categories[i] = new NmtMallocMemoryInfo(); - } - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - NmtMallocMemoryInfo getInfoByCategory(NmtCategory category) { - return getInfoByCategory(category.ordinal()); + public JfrNmtCategorySerializer() { } - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - NmtMallocMemoryInfo getInfoByCategory(int category) { - assert category < categories.length; - return categories[category]; - } + @Override + public void write(JfrChunkWriter writer) { + writer.writeCompressedLong(JfrType.NMTType.getId()); - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - NmtMallocMemoryInfo getTotalInfo() { - return total; + NmtCategory[] nmtCategories = NmtCategory.values(); + writer.writeCompressedLong(nmtCategories.length); + for (NmtCategory nmtCategory : nmtCategories) { + writer.writeCompressedInt(nmtCategory.ordinal()); + writer.writeString(nmtCategory.getName()); + } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrType.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrType.java index 4650fda41b87..b3c8603704e6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrType.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrType.java @@ -47,7 +47,8 @@ public enum JfrType { GCWhen("jdk.types.GCWhen"), VMOperation("jdk.types.VMOperationType"), MonitorInflationCause("jdk.types.InflateCause"), - OldObject("jdk.types.OldObject"); + OldObject("jdk.types.OldObject"), + NMTType("jdk.types.NMTType"); private final long id; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/EveryChunkNativePeriodicEvents.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/EveryChunkNativePeriodicEvents.java index db0a82d9c853..07a2f37357a9 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/EveryChunkNativePeriodicEvents.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/EveryChunkNativePeriodicEvents.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,6 +30,7 @@ import org.graalvm.nativeimage.StackValue; import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.heap.Heap; import com.oracle.svm.core.heap.PhysicalMemory; import com.oracle.svm.core.heap.VMOperationInfos; @@ -38,6 +40,8 @@ import com.oracle.svm.core.jfr.JfrNativeEventWriterData; import com.oracle.svm.core.jfr.JfrNativeEventWriterDataAccess; import com.oracle.svm.core.jfr.JfrTicks; +import com.oracle.svm.core.nmt.NativeMemoryTracking; +import com.oracle.svm.core.nmt.NmtCategory; import com.oracle.svm.core.thread.JavaVMOperation; import com.oracle.svm.core.thread.VMThreads; @@ -54,6 +58,7 @@ public static void emit() { emitPhysicalMemory(); emitClassLoadingStatistics(); emitPerThreadEvents(); + emitNativeMemoryTrackingEvents(); } @Uninterruptible(reason = "Accesses a JFR buffer.") @@ -102,6 +107,71 @@ private static void emitClassLoadingStatistics() { } } + /** + * Emit events for NativeMemoryUsage and NativeMemoryUsageTotal. We do not guarantee consistent + * measurements across NMT categories and the total. Each individual NMT category uses atomic + * counters which may change while we are in this method. Similar to OpenJDK, it is only a + * best-effort approach. + */ + private static void emitNativeMemoryTrackingEvents() { + if (VMInspectionOptions.hasNativeMemoryTrackingSupport()) { + emitJdkNmtEvents(NmtCategory.values()); + emitNmtPeakEvents(); + } + } + + /** Emit Native Image-specific events that report the peak memory usage. */ + private static void emitNmtPeakEvents() { + NativeMemoryUsageTotalPeakEvent nmtTotalPeakEvent = new NativeMemoryUsageTotalPeakEvent(); + + long totalPeakUsed = NativeMemoryTracking.singleton().getPeakTotalUsedMemory(); + nmtTotalPeakEvent.peakCommitted = totalPeakUsed; + nmtTotalPeakEvent.peakReserved = totalPeakUsed; + nmtTotalPeakEvent.countAtPeak = NativeMemoryTracking.singleton().getCountAtTotalPeakUsage(); + nmtTotalPeakEvent.commit(); + + for (NmtCategory nmtCategory : NmtCategory.values()) { + NativeMemoryUsagePeakEvent nmtPeakEvent = new NativeMemoryUsagePeakEvent(); + nmtPeakEvent.type = nmtCategory.getName(); + + long peakUsed = NativeMemoryTracking.singleton().getPeakUsedMemory(nmtCategory); + nmtPeakEvent.peakCommitted = peakUsed; + nmtPeakEvent.peakReserved = peakUsed; + nmtPeakEvent.countAtPeak = NativeMemoryTracking.singleton().getCountAtPeakUsage(nmtCategory); + nmtPeakEvent.commit(); + } + } + + @Uninterruptible(reason = "Accesses a JFR buffer.") + private static void emitJdkNmtEvents(NmtCategory[] nmtCategories) { + long timestamp = JfrTicks.elapsedTicks(); + JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); + JfrNativeEventWriterDataAccess.initializeThreadLocalNativeBuffer(data); + + if (JfrEvent.NativeMemoryUsage.shouldEmit()) { + for (NmtCategory nmtCategory : nmtCategories) { + long usedMemory = NativeMemoryTracking.singleton().getUsedMemory(nmtCategory); + + JfrNativeEventWriter.beginSmallEvent(data, JfrEvent.NativeMemoryUsage); + JfrNativeEventWriter.putLong(data, timestamp); + JfrNativeEventWriter.putLong(data, nmtCategory.ordinal()); + JfrNativeEventWriter.putLong(data, usedMemory); // reserved + JfrNativeEventWriter.putLong(data, usedMemory); // committed + JfrNativeEventWriter.endSmallEvent(data); + } + } + + if (JfrEvent.NativeMemoryUsageTotal.shouldEmit()) { + long totalUsedMemory = NativeMemoryTracking.singleton().getTotalUsedMemory(); + + JfrNativeEventWriter.beginSmallEvent(data, JfrEvent.NativeMemoryUsageTotal); + JfrNativeEventWriter.putLong(data, timestamp); + JfrNativeEventWriter.putLong(data, totalUsedMemory); // reserved + JfrNativeEventWriter.putLong(data, totalUsedMemory); // committed + JfrNativeEventWriter.endSmallEvent(data); + } + } + private static void emitPerThreadEvents() { if (needsVMOperation()) { EmitPeriodicPerThreadEventsOperation vmOp = new EmitPeriodicPerThreadEventsOperation(); @@ -111,7 +181,7 @@ private static void emitPerThreadEvents() { @Uninterruptible(reason = "Used to avoid the VM operation if it is not absolutely needed.") private static boolean needsVMOperation() { - /* The returned value is racy. */ + /* The returned value is racy but this is fine because we recheck in the VM operation. */ return JfrEvent.ThreadCPULoad.shouldEmit() || JfrEvent.ThreadAllocationStatistics.shouldEmit(); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/NativeMemoryUsagePeakEvent.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/NativeMemoryUsagePeakEvent.java new file mode 100644 index 000000000000..a3762683e565 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/NativeMemoryUsagePeakEvent.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jfr.events; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Experimental; +import jdk.jfr.Label; +import jdk.jfr.Name; +import jdk.jfr.StackTrace; + +/** Similar to the JFR event jdk.NativeMemoryUsage, except that it tracks the peak usage. */ +@Experimental +@Name("jdk.NativeMemoryUsagePeak") +@Label("Native Memory Usage Peak") +@Description("Native memory peak usage for a given memory type in the JVM (GraalVM Native Image only).") +@Category({"Java Virtual Machine", "Memory"}) +@StackTrace(false) +public class NativeMemoryUsagePeakEvent extends Event { + @Label("Memory Type") public String type; + @Label("Peak Reserved") public long peakReserved; + @Label("Peak Committed") public long peakCommitted; + @Label("Count At Peak") public long countAtPeak; +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/NativeMemoryUsageTotalPeakEvent.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/NativeMemoryUsageTotalPeakEvent.java new file mode 100644 index 000000000000..3e615a41c1e3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/NativeMemoryUsageTotalPeakEvent.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jfr.events; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Experimental; +import jdk.jfr.Label; +import jdk.jfr.Name; +import jdk.jfr.StackTrace; + +/** Similar to the JFR event jdk.NativeMemoryUsageTotal, except that it tracks the peak usage. */ +@Experimental +@Name("jdk.NativeMemoryUsageTotalPeak") +@Label("Native Memory Usage Total Peak") +@Description("Total native memory peak usage for the JVM (GraalVM Native Image only). Might not be the exact sum of the NativeMemoryUsagePeak events due to timing.") +@Category({"Java Virtual Machine", "Memory"}) +@StackTrace(false) +public class NativeMemoryUsageTotalPeakEvent extends Event { + @Label("Peak Reserved") public long peakReserved; + @Label("Peak Committed") public long peakCommitted; + @Label("Count At Peak") public long countAtPeak; +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java index 616cbe1944d0..149d897ede1a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java @@ -57,10 +57,17 @@ public class NativeMemoryTracking { private static final UnsignedWord ALIGNMENT = WordFactory.unsigned(16); private static final int MAGIC = 0xF0F1F2F3; - private final NmtMallocMemorySnapshot mallocMemorySnapshot = new NmtMallocMemorySnapshot(); + + private final NmtMallocMemoryInfo[] categories; + private final NmtMallocMemoryInfo total; @Platforms(Platform.HOSTED_ONLY.class) public NativeMemoryTracking() { + total = new NmtMallocMemoryInfo(); + categories = new NmtMallocMemoryInfo[NmtCategory.values().length]; + for (int i = 0; i < categories.length; i++) { + categories[i] = new NmtMallocMemoryInfo(); + } } @Fold @@ -71,7 +78,7 @@ public static NativeMemoryTracking singleton() { @Fold public static UnsignedWord sizeOfNmtHeader() { /* - * Align the header to 16 bytes to preserve platform-specific malloc alignments up to 16 + * Align the header to 16 bytes to preserve platform-specific malloc alignment up to 16 * bytes (i.e., the allocation payload is aligned to 16 bytes if the platform-specific * malloc implementation returns a pointer that is aligned to at least 16 bytes). */ @@ -109,9 +116,9 @@ public void track(PointerBase innerPtr) { UnsignedWord allocationSize = header.getAllocationSize(); UnsignedWord totalSize = allocationSize.add(nmtHeaderSize); - mallocMemorySnapshot.getInfoByCategory(header.getCategory()).track(allocationSize); - mallocMemorySnapshot.getInfoByCategory(NmtCategory.NMT).track(nmtHeaderSize); - mallocMemorySnapshot.getTotalInfo().track(totalSize); + getInfo(header.getCategory()).track(allocationSize); + getInfo(NmtCategory.NMT).track(nmtHeaderSize); + total.track(totalSize); } @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) @@ -127,9 +134,9 @@ public PointerBase untrack(PointerBase innerPtr) { @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) public void untrack(UnsignedWord size, int category) { - mallocMemorySnapshot.getInfoByCategory(category).untrack(size); - mallocMemorySnapshot.getInfoByCategory(NmtCategory.NMT).untrack(sizeOfNmtHeader()); - mallocMemorySnapshot.getTotalInfo().untrack(size.add(sizeOfNmtHeader())); + getInfo(category).untrack(size); + getInfo(NmtCategory.NMT).untrack(sizeOfNmtHeader()); + total.untrack(size.add(sizeOfNmtHeader())); } @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) @@ -144,27 +151,73 @@ private static Pointer getInnerPointer(NmtMallocHeader mallocHeader) { return ((Pointer) mallocHeader).add(sizeOfNmtHeader()); } + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) public long getUsedMemory(NmtCategory category) { - return mallocMemorySnapshot.getInfoByCategory(category).getUsed(); + return getInfo(category).getUsed(); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public long getPeakUsedMemory(NmtCategory category) { + return getInfo(category).getPeakUsed(); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public long getCountAtPeakUsage(NmtCategory category) { + return getInfo(category).getCountAtPeakUsage(); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public long getTotalCount() { + return total.getCount(); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public long getTotalUsedMemory() { + return total.getUsed(); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public long getPeakTotalUsedMemory() { + return total.getPeakUsed(); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public long getCountAtTotalPeakUsage() { + return total.getCountAtPeakUsage(); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private NmtMallocMemoryInfo getInfo(NmtCategory category) { + return getInfo(category.ordinal()); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private NmtMallocMemoryInfo getInfo(int category) { + assert category < categories.length; + return categories[category]; } public static RuntimeSupport.Hook shutdownHook() { return isFirstIsolate -> NativeMemoryTracking.singleton().printStatistics(); } - public void printStatistics() { + private void printStatistics() { if (VMInspectionOptions.PrintNMTStatistics.getValue()) { System.out.println(); System.out.println("Native memory tracking"); - System.out.println(" Total used memory: " + mallocMemorySnapshot.getTotalInfo().getUsed() + " bytes"); - System.out.println(" Total alive allocations: " + mallocMemorySnapshot.getTotalInfo().getCount()); + System.out.println(" Peak total used memory: " + getPeakTotalUsedMemory() + " bytes"); + System.out.println(" Total alive allocations at peak usage: " + getCountAtTotalPeakUsage()); + System.out.println(" Total used memory: " + getTotalUsedMemory() + " bytes"); + System.out.println(" Total alive allocations: " + getTotalCount()); for (int i = 0; i < NmtCategory.values().length; i++) { String name = NmtCategory.values()[i].getName(); - NmtMallocMemoryInfo info = mallocMemorySnapshot.getInfoByCategory(i); + NmtMallocMemoryInfo info = getInfo(i); - System.out.println(" " + name + " used memory: " + info.getUsed() + " bytes"); - System.out.println(" " + name + " alive allocations: " + info.getCount()); + System.out.println(" " + name + " peak used memory: " + info.getPeakUsed() + " bytes"); + System.out.println(" " + name + " alive allocations at peak: " + info.getCountAtPeakUsage()); + System.out.println(" " + name + " currently used memory: " + info.getUsed() + " bytes"); + System.out.println(" " + name + " currently alive allocations: " + info.getCount()); } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtMallocMemoryInfo.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtMallocMemoryInfo.java index 7a54c3b491ca..bdd192413aae 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtMallocMemoryInfo.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtMallocMemoryInfo.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2023, 2023, Red Hat Inc. All rights reserved. + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,6 +26,8 @@ package com.oracle.svm.core.nmt; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import org.graalvm.word.UnsignedWord; @@ -36,29 +38,57 @@ class NmtMallocMemoryInfo { private final AtomicLong count = new AtomicLong(0); private final AtomicLong used = new AtomicLong(0); + private final AtomicLong countAtPeakUsage = new AtomicLong(0); + private final AtomicLong peakUsed = new AtomicLong(0); @Platforms(Platform.HOSTED_ONLY.class) NmtMallocMemoryInfo() { } - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) void track(UnsignedWord allocationSize) { - count.incrementAndGet(); - used.addAndGet(allocationSize.rawValue()); + long newUsed = used.addAndGet(allocationSize.rawValue()); + long newCount = count.incrementAndGet(); + updatePeak(newUsed, newCount); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private void updatePeak(long newUsed, long newCount) { + long oldUsed = peakUsed.get(); + while (oldUsed < newUsed) { + if (peakUsed.compareAndSet(oldUsed, newUsed)) { + /* Recording the count at peak usage is racy (similar to Hotspot). */ + countAtPeakUsage.set(newCount); + return; + } + oldUsed = peakUsed.get(); + } } - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) void untrack(UnsignedWord allocationSize) { long lastCount = count.decrementAndGet(); long lastSize = used.addAndGet(-allocationSize.rawValue()); assert lastSize >= 0 && lastCount >= 0; } + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) long getUsed() { return used.get(); } + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) long getCount() { return count.get(); } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + long getPeakUsed() { + return peakUsed.get(); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + long getCountAtPeakUsage() { + return countAtPeakUsage.get(); + } } diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestNmtEvents.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestNmtEvents.java new file mode 100644 index 000000000000..45c40797b492 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestNmtEvents.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.test.jfr; + +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.graalvm.word.Pointer; +import org.graalvm.word.WordFactory; +import org.junit.Test; + +import com.oracle.svm.core.jfr.JfrEvent; +import com.oracle.svm.core.memory.NativeMemory; +import com.oracle.svm.core.nmt.NmtCategory; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; + +public class TestNmtEvents extends JfrRecordingTest { + private static final int ALLOCATION_SIZE = 1024 * 16; + + @Test + public void test() throws Throwable { + String[] events = new String[]{ + JfrEvent.NativeMemoryUsage.getName(), + JfrEvent.NativeMemoryUsageTotal.getName(), + "jdk.NativeMemoryUsagePeak", + "jdk.NativeMemoryUsageTotalPeak" + }; + Recording recording = startRecording(events); + + Pointer ptr = NativeMemory.malloc(WordFactory.unsigned(ALLOCATION_SIZE), NmtCategory.Code); + + /* Force a chunk rotation to trigger periodic event emission. */ + recording.dump(createTempJfrFile()); + + NativeMemory.free(ptr); + + stopRecording(recording, TestNmtEvents::validateEvents); + } + + private static void validateEvents(List events) { + assertTrue(events.size() >= 4); + + boolean foundNativeMemoryUsage = false; + boolean foundNativeMemoryUsagePeak = false; + boolean foundNativeMemoryUsageTotal = false; + boolean foundNativeMemoryUsageTotalPeak = false; + + for (RecordedEvent e : events) { + String eventName = e.getEventType().getName(); + if (eventName.equals(JfrEvent.NativeMemoryUsage.getName()) && e.getString("type").equals(NmtCategory.Code.getName())) { + foundNativeMemoryUsage = true; + } + + if (eventName.equals("jdk.NativeMemoryUsageTotalPeak")) { + foundNativeMemoryUsageTotalPeak = true; + } + + if (eventName.equals("jdk.NativeMemoryUsagePeak") && e.getString("type").equals(NmtCategory.Code.getName())) { + foundNativeMemoryUsagePeak = true; + } + + if (eventName.equals(JfrEvent.NativeMemoryUsageTotal.getName())) { + foundNativeMemoryUsageTotal = true; + } + } + + assertTrue(foundNativeMemoryUsage); + assertTrue(foundNativeMemoryUsagePeak); + assertTrue(foundNativeMemoryUsageTotal); + assertTrue(foundNativeMemoryUsageTotalPeak); + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java index 4898f751a865..461ff4020a29 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java @@ -38,6 +38,7 @@ import java.util.Collection; import java.util.HashMap; +import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.jfr.JfrCheckpointType; import com.oracle.svm.core.jfr.JfrChunkFileWriter; import com.oracle.svm.core.jfr.JfrReservedEvent; @@ -54,6 +55,7 @@ import com.oracle.svm.test.jfr.utils.poolparsers.MethodConstantPoolParser; import com.oracle.svm.test.jfr.utils.poolparsers.ModuleConstantPoolParser; import com.oracle.svm.test.jfr.utils.poolparsers.MonitorInflationCauseConstantPoolParser; +import com.oracle.svm.test.jfr.utils.poolparsers.NmtCategoryConstantPoolParser; import com.oracle.svm.test.jfr.utils.poolparsers.OldObjectConstantPoolParser; import com.oracle.svm.test.jfr.utils.poolparsers.PackageConstantPoolParser; import com.oracle.svm.test.jfr.utils.poolparsers.StacktraceConstantPoolParser; @@ -92,6 +94,10 @@ public JfrFileParser(Path path) { addParser(JfrType.MonitorInflationCause, new MonitorInflationCauseConstantPoolParser(this)); addParser(JfrType.GCWhen, new GCWhenConstantPoolParser(this)); addParser(JfrType.OldObject, new OldObjectConstantPoolParser(this)); + + if (VMInspectionOptions.hasNativeMemoryTrackingSupport()) { + addParser(JfrType.NMTType, new NmtCategoryConstantPoolParser(this)); + } } private void addParser(JfrType type, ConstantPoolParser parser) { diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/NmtCategoryConstantPoolParser.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/NmtCategoryConstantPoolParser.java new file mode 100644 index 000000000000..52db8ee8b6c7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/NmtCategoryConstantPoolParser.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.test.jfr.utils.poolparsers; + +import java.io.IOException; + +import org.junit.Assert; + +import com.oracle.svm.test.jfr.utils.JfrFileParser; +import com.oracle.svm.test.jfr.utils.RecordingInput; + +public class NmtCategoryConstantPoolParser extends AbstractSerializerParser { + public NmtCategoryConstantPoolParser(JfrFileParser parser) { + super(parser); + } + + @Override + public void parse(RecordingInput input) throws IOException { + int count = input.readInt(); + Assert.assertTrue(count > 0); + for (int i = 0; i < count; i++) { + addFoundId(input.readInt()); + Assert.assertFalse("NMT category name is empty!", input.readUTF().isBlank()); + } + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/nmt/NativeMemoryTrackingTests.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/nmt/NativeMemoryTrackingTests.java index 7c0a5ce10e37..6f633651ae44 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/nmt/NativeMemoryTrackingTests.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/nmt/NativeMemoryTrackingTests.java @@ -38,15 +38,15 @@ import com.oracle.svm.core.nmt.NmtCategory; public class NativeMemoryTrackingTests { - private static final int ALLOCATION_SIZE = 1024 * 16; - private static final int REALLOC_SIZE = ALLOCATION_SIZE / 2; + private static final int K = 1024; + private static final int M = 1024 * 1024; @Test public void testMalloc() { assertEquals(0, getUsedMemory()); - Pointer ptr = NativeMemory.malloc(WordFactory.unsigned(ALLOCATION_SIZE), NmtCategory.Code); - assertEquals(ALLOCATION_SIZE, getUsedMemory()); + Pointer ptr = NativeMemory.malloc(16 * K, NmtCategory.Code); + assertEquals(16 * K, getUsedMemory()); assertTrue(getUsedMemory() > 0); NativeMemory.free(ptr); @@ -57,9 +57,9 @@ public void testMalloc() { @Test public void testCalloc() { assertEquals(0, getUsedMemory()); - Pointer ptr = NativeMemory.calloc(WordFactory.unsigned(ALLOCATION_SIZE), NmtCategory.Code); + Pointer ptr = NativeMemory.calloc(16 * K, NmtCategory.Code); - assertEquals(ALLOCATION_SIZE, getUsedMemory()); + assertEquals(16 * K, getUsedMemory()); assertTrue(getUsedMemory() > 0); NativeMemory.free(ptr); @@ -70,19 +70,50 @@ public void testCalloc() { @Test public void testRealloc() { assertEquals(0, getUsedMemory()); - Pointer ptr = NativeMemory.malloc(WordFactory.unsigned(ALLOCATION_SIZE), NmtCategory.Code); + Pointer ptr = NativeMemory.malloc(16 * K, NmtCategory.Code); - assertEquals(getUsedMemory(), ALLOCATION_SIZE); + assertEquals(getUsedMemory(), 16 * K); assertTrue(getUsedMemory() > 0); - Pointer reallocPtr = NativeMemory.realloc(ptr, WordFactory.unsigned(REALLOC_SIZE), NmtCategory.Code); - - assertEquals(REALLOC_SIZE, getUsedMemory()); + Pointer reallocPtr = NativeMemory.realloc(ptr, WordFactory.unsigned(8 * K), NmtCategory.Code); + assertEquals(8 * K, getUsedMemory()); NativeMemory.free(reallocPtr); assertEquals(0, getUsedMemory()); } + @Test + public void testPeakTracking() { + assertEquals(0, getUsedMemory()); + + Pointer ptr1 = NativeMemory.malloc(M, NmtCategory.Code); + long peakUsed = NativeMemoryTracking.singleton().getPeakUsedMemory(NmtCategory.Code); + assertEquals(M, peakUsed); + + Pointer ptr2 = NativeMemory.malloc(M, NmtCategory.Code); + peakUsed = NativeMemoryTracking.singleton().getPeakUsedMemory(NmtCategory.Code); + assertEquals(2 * M, peakUsed); + + NativeMemory.free(ptr1); + ptr1 = WordFactory.nullPointer(); + + NativeMemory.free(ptr2); + ptr2 = WordFactory.nullPointer(); + + assertEquals(0, getUsedMemory()); + assertEquals(2 * M, NativeMemoryTracking.singleton().getPeakUsedMemory(NmtCategory.Code)); + + Pointer ptr3 = NativeMemory.malloc(3 * M, NmtCategory.Code); + peakUsed = NativeMemoryTracking.singleton().getPeakUsedMemory(NmtCategory.Code); + assertEquals(3 * M, peakUsed); + + NativeMemory.free(ptr3); + ptr3 = WordFactory.nullPointer(); + + assertEquals(0, getUsedMemory()); + assertEquals(3 * M, NativeMemoryTracking.singleton().getPeakUsedMemory(NmtCategory.Code)); + } + private static long getUsedMemory() { return NativeMemoryTracking.singleton().getUsedMemory(NmtCategory.Code); }