From 81617ee63123b005c3f65349cd2f50c0537979df Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Thu, 20 Feb 2025 16:35:45 -0500 Subject: [PATCH 01/16] allocation free --- .../oracle/svm/core/genscavenge/HeapImpl.java | 19 +- .../core/posix/PosixPlatformTimeUtils.java | 5 +- .../jdk/Target_jdk_internal_misc_VM.java | 9 +- .../windows/WindowsPlatformTimeUtils.java | 6 +- .../AbstractUninterruptibleHashtable.java | 7 + .../src/com/oracle/svm/core/heap/Heap.java | 11 +- .../com/oracle/svm/core/hub/DynamicHub.java | 2 +- .../svm/core/jfr/JfrChunkFileWriter.java | 21 +- .../svm/core/jfr/JfrFrameTypeSerializer.java | 14 +- .../svm/core/jfr/JfrGCCauseSerializer.java | 13 +- .../svm/core/jfr/JfrGCNameSerializer.java | 6 +- .../svm/core/jfr/JfrGCWhenSerializer.java | 9 +- .../JfrMonitorInflationCauseSerializer.java | 4 +- .../core/jfr/JfrNmtCategorySerializer.java | 9 +- .../svm/core/jfr/JfrSymbolRepository.java | 60 +- .../core/jfr/JfrThreadStateSerializer.java | 3 +- .../svm/core/jfr/JfrTypeRepository.java | 689 +++++++++++++++--- .../com/oracle/svm/core/jfr/SubstrateJVM.java | 4 + .../jfr/oldobject/JfrOldObjectRepository.java | 2 +- .../svm/core/util/PlatformTimeUtils.java | 39 +- 20 files changed, 734 insertions(+), 198 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java b/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java index 987fc9e56321..0e984906a193 100644 --- a/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java +++ b/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java @@ -121,7 +121,7 @@ public final class HeapImpl extends Heap { private volatile long refListWaiterWakeUpCounter; /** A cached list of all the classes, if someone asks for it. */ - private List> classList; + private Class[] classList; @Platforms(Platform.HOSTED_ONLY.class) public HeapImpl() { @@ -324,17 +324,28 @@ public int getClassCount() { } @Override - protected List> getAllClasses() { + protected Class[] getAllClasses() { /* Two threads might race to set classList, but they compute the same result. */ - if (classList == null) { + if (getCachedClasses() == null) { ArrayList> list = findAllDynamicHubs(); /* Ensure that other threads see consistent values once the list is published. */ MembarNode.memoryBarrier(MembarNode.FenceKind.STORE_STORE); - classList = list; + setCachedClasses(list); } + return getCachedClasses(); + } + + @Override + public Class[] getCachedClasses() { return classList; } + + private void setCachedClasses(List> list) { + classList = new Class[list.size()]; + list.toArray(classList); + } + private ArrayList> findAllDynamicHubs() { int dynamicHubCount = getClassCount(); diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixPlatformTimeUtils.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixPlatformTimeUtils.java index 13e2a7f7e906..d72d786a98ac 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixPlatformTimeUtils.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixPlatformTimeUtils.java @@ -38,10 +38,11 @@ public final class PosixPlatformTimeUtils extends PlatformTimeUtils { @Override @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+3/src/hotspot/os/posix/os_posix.cpp#L1409-L1415") @Uninterruptible(reason = "Must not migrate platform threads when executing on a virtual thread.") - public SecondsNanos javaTimeSystemUTC() { + public void javaTimeSystemUTC(SecondsNanos secondsNanos) { Time.timespec ts = StackValue.get(Time.timespec.class); int status = PosixUtils.clock_gettime(Time.CLOCK_REALTIME(), ts); PosixUtils.checkStatusIs0(status, "javaTimeSystemUTC: clock_gettime(CLOCK_REALTIME) failed."); - return allocateSecondsNanosInterruptibly(ts.tv_sec(), ts.tv_nsec()); + secondsNanos.setNanos(ts.tv_nsec()); + secondsNanos.setSeconds(ts.tv_sec()); } } diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jdk/Target_jdk_internal_misc_VM.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jdk/Target_jdk_internal_misc_VM.java index 3e98471c29b7..c60eeafca907 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jdk/Target_jdk_internal_misc_VM.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jdk/Target_jdk_internal_misc_VM.java @@ -29,6 +29,7 @@ import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.graal.stackvalue.UnsafeStackValue; import com.oracle.svm.core.util.BasedOnJDKFile; import com.oracle.svm.core.util.PlatformTimeUtils; import com.oracle.svm.core.util.PlatformTimeUtils.SecondsNanos; @@ -41,13 +42,13 @@ final class Target_jdk_internal_misc_VM { public static long getNanoTimeAdjustment(long offsetInSeconds) { long maxDiffSecs = 0x0100000000L; long minDiffSecs = -maxDiffSecs; + SecondsNanos secondsNanos = UnsafeStackValue.get(SecondsNanos.class); + PlatformTimeUtils.singleton().javaTimeSystemUTC(secondsNanos); - SecondsNanos time = PlatformTimeUtils.singleton().javaTimeSystemUTC(); - - long diff = time.seconds() - offsetInSeconds; + long diff = secondsNanos.getSeconds() - offsetInSeconds; if (diff >= maxDiffSecs || diff <= minDiffSecs) { return -1; } - return (diff * 1000000000) + time.nanos(); + return (diff * 1000000000) + secondsNanos.getNanos(); } } diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsPlatformTimeUtils.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsPlatformTimeUtils.java index ab88c1c91942..8d2d3b0440ad 100644 --- a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsPlatformTimeUtils.java +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsPlatformTimeUtils.java @@ -60,12 +60,14 @@ private static long windowsToTimeTicks(FILETIME wt) { @Override @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+3/src/hotspot/os/windows/os_windows.cpp#L1198-L1205") @Uninterruptible(reason = "Must not migrate platform threads when executing on a virtual thread.") - public SecondsNanos javaTimeSystemUTC() { + public void javaTimeSystemUTC(SecondsNanos secondsNanos) { FILETIME wt = StackValue.get(FILETIME.class); GetSystemTimeAsFileTime(wt); long ticks = windowsToTimeTicks(wt); // 10th of micros long secs = ticks / 10000000L; // 10000 * 1000 long nanos = (ticks - (secs * 10000000L)) * 100L; - return allocateSecondsNanosInterruptibly(secs, nanos); + + secondsNanos.setNanos(nanos); + secondsNanos.setSeconds(secs); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/collections/AbstractUninterruptibleHashtable.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/collections/AbstractUninterruptibleHashtable.java index f056da7f97b6..f26eb9a66287 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/collections/AbstractUninterruptibleHashtable.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/collections/AbstractUninterruptibleHashtable.java @@ -171,6 +171,13 @@ public boolean putIfAbsent(UninterruptibleEntry valueOnStack) { } } + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean contains(UninterruptibleEntry valueOnStack) { + assert valueOnStack.isNonNull(); + UninterruptibleEntry existingEntry = get(valueOnStack); + return existingEntry.isNonNull(); + } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public UninterruptibleEntry putNew(UninterruptibleEntry valueOnStack) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/Heap.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/Heap.java index b911f1919ad0..693bbfbb01a8 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/Heap.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/Heap.java @@ -112,9 +112,10 @@ protected Heap() { /** Visits all loaded classes in the heap (see {@link PredefinedClassesSupport}). */ public void visitLoadedClasses(Consumer> visitor) { - for (Class clazz : getAllClasses()) { - if (DynamicHub.fromClass(clazz).isLoaded()) { - visitor.accept(clazz); + Class[] classes = getAllClasses(); + for (int i = 0; i < classes.length; i++) { + if (DynamicHub.fromClass(classes[i]).isLoaded()) { + visitor.accept(classes[i]); } } } @@ -123,7 +124,9 @@ public void visitLoadedClasses(Consumer> visitor) { * Get all known classes. Intentionally protected to prevent access to classes that have not * been "loaded" yet, see {@link PredefinedClassesSupport}. */ - protected abstract List> getAllClasses(); + protected abstract Class[] getAllClasses(); + + public abstract Class[] getCachedClasses(); /** * Get the ObjectHeader implementation that this Heap uses. diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java index d8bad63d0146..64e3b368fb50 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java @@ -1033,7 +1033,7 @@ private int getClassAccessFlags(ReflectionMetadata reflectionMetadata) { } @Substitute - private DynamicHub getComponentType() { + public DynamicHub getComponentType() { return componentType; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java index 85e807666fed..1a6cfd0f021f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java @@ -32,11 +32,16 @@ import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.word.Pointer; import org.graalvm.word.UnsignedWord; +import org.graalvm.word.WordFactory; import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.graal.stackvalue.UnsafeStackValue; +import com.oracle.svm.core.heap.RestrictHeapAccess; import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository; +import com.oracle.svm.core.jdk.UninterruptibleUtils; import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler; import com.oracle.svm.core.jfr.sampler.JfrRecurringCallbackExecutionSampler; import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; @@ -192,6 +197,7 @@ public void write(JfrBuffer buffer) { } } +// @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") @Override public void flush() { assert lock.isOwner(); @@ -355,8 +361,8 @@ private long getDeltaToLastCheckpoint(long startOfNewCheckpoint) { private int writeSerializers() { JfrSerializer[] serializers = JfrSerializerSupport.get().getSerializers(); - for (JfrSerializer serializer : serializers) { - serializer.write(this); + for (int i =0; i causes = GCCause.getGCCauses(); int nonNullItems = 0; - for (GCCause cause : causes) { - if (cause != null) { + + for (int i = 0; i < causes.size(); i++) { + if (causes.get(i) != null) { nonNullItems++; } } @@ -51,10 +52,10 @@ public void write(JfrChunkWriter writer) { writer.writeCompressedLong(JfrType.GCCause.getId()); writer.writeCompressedLong(nonNullItems); - for (GCCause cause : causes) { - if (cause != null) { - writer.writeCompressedLong(cause.getId()); - writer.writeString(cause.getName()); + for (int i = 0; i < causes.size(); i++) { + if (causes.get(i) != null) { + writer.writeCompressedLong(causes.get(i).getId()); + writer.writeString(causes.get(i).getName()); } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCNameSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCNameSerializer.java index 2aee2f3e1dd0..3e4bd76e2c3a 100755 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCNameSerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCNameSerializer.java @@ -37,9 +37,9 @@ public void write(JfrChunkWriter writer) { JfrGCName[] gcNames = JfrGCNames.singleton().getNames(); writer.writeCompressedLong(JfrType.GCName.getId()); writer.writeCompressedLong(gcNames.length); - for (JfrGCName name : gcNames) { - writer.writeCompressedLong(name.getId()); - writer.writeString(name.getName()); + for (int i = 0; i < gcNames.length; i++) { + writer.writeCompressedLong(gcNames[i].getId()); + writer.writeString(gcNames[i].getName()); } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCWhenSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCWhenSerializer.java index 6570c116edaa..ca20af0a657f 100755 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCWhenSerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCWhenSerializer.java @@ -28,18 +28,19 @@ import org.graalvm.nativeimage.Platforms; public class JfrGCWhenSerializer implements JfrSerializer { + private JfrGCWhen[] values; @Platforms(Platform.HOSTED_ONLY.class) public JfrGCWhenSerializer() { + values = JfrGCWhen.values(); } @Override public void write(JfrChunkWriter writer) { - JfrGCWhen[] values = JfrGCWhen.values(); writer.writeCompressedLong(JfrType.GCWhen.getId()); writer.writeCompressedLong(values.length); - for (JfrGCWhen value : values) { - writer.writeCompressedLong(value.getId()); - writer.writeString(value.getText()); + for (int i = 0; i < values.length; i++) { + writer.writeCompressedLong(values[i].getId()); + writer.writeString(values[i].getText()); } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrMonitorInflationCauseSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrMonitorInflationCauseSerializer.java index 4d71137a41ff..5a4ee87d5987 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrMonitorInflationCauseSerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrMonitorInflationCauseSerializer.java @@ -32,15 +32,17 @@ import com.oracle.svm.core.monitor.MonitorInflationCause; public class JfrMonitorInflationCauseSerializer implements JfrSerializer { + private MonitorInflationCause[] inflationCauses; + @Platforms(Platform.HOSTED_ONLY.class) public JfrMonitorInflationCauseSerializer() { + inflationCauses = MonitorInflationCause.values(); } @Override public void write(JfrChunkWriter writer) { writer.writeCompressedLong(JfrType.MonitorInflationCause.getId()); - MonitorInflationCause[] inflationCauses = MonitorInflationCause.values(); writer.writeCompressedLong(inflationCauses.length); for (int i = 0; i < inflationCauses.length; i++) { writer.writeCompressedInt(i); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java index 06b01b3a988d..cea4e1f0738d 100755 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java @@ -32,19 +32,20 @@ import com.oracle.svm.core.nmt.NmtCategory; public class JfrNmtCategorySerializer implements JfrSerializer { + private NmtCategory[] nmtCategories; @Platforms(Platform.HOSTED_ONLY.class) public JfrNmtCategorySerializer() { + nmtCategories = NmtCategory.values(); } @Override public void write(JfrChunkWriter writer) { writer.writeCompressedLong(JfrType.NMTType.getId()); - NmtCategory[] nmtCategories = NmtCategory.values(); writer.writeCompressedLong(nmtCategories.length); - for (NmtCategory nmtCategory : nmtCategories) { - writer.writeCompressedInt(nmtCategory.ordinal()); - writer.writeString(nmtCategory.getName()); + for (int i = 0; i < nmtCategories.length; i++) { + writer.writeCompressedInt(nmtCategories[i].ordinal()); + writer.writeString(nmtCategories[i].getName()); } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java index bba166c80118..40f565c6d2c4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java @@ -24,12 +24,17 @@ */ package com.oracle.svm.core.jfr; +import com.oracle.svm.core.headers.LibC; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.word.Pointer; +import org.graalvm.word.PointerBase; import org.graalvm.nativeimage.StackValue; import org.graalvm.nativeimage.c.struct.RawField; import org.graalvm.nativeimage.c.struct.RawStructure; import org.graalvm.nativeimage.c.struct.SizeOf; +import org.graalvm.word.UnsignedWord; +import org.graalvm.word.WordFactory; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.c.struct.PinnedObjectField; @@ -41,6 +46,7 @@ import com.oracle.svm.core.jdk.UninterruptibleUtils.ReplaceDotWithSlash; import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.locks.VMMutex; +import com.oracle.svm.core.memory.NullableNativeMemory; import com.oracle.svm.core.nmt.NmtCategory; import jdk.graal.compiler.core.common.SuppressFBWarnings; @@ -75,18 +81,30 @@ public long getSymbolId(String imageHeapString, boolean previousEpoch) { @Uninterruptible(reason = "Locking without transition and result is only valid until epoch changes.", callerMustBe = true) public long getSymbolId(String imageHeapString, boolean previousEpoch, boolean replaceDotWithSlash) { - if (imageHeapString == null) { + if (imageHeapString == null || imageHeapString.isEmpty()) { return 0; } + assert Heap.getHeap().isInImageHeap(""); assert Heap.getHeap().isInImageHeap(imageHeapString); - - JfrSymbol symbol = StackValue.get(JfrSymbol.class); - symbol.setValue(imageHeapString); - symbol.setReplaceDotWithSlash(replaceDotWithSlash); + int length = 0; + length = UninterruptibleUtils.String.modifiedUTF8Length(imageHeapString, false); + Pointer buffer = NullableNativeMemory.malloc(length, NmtCategory.JFR); + UninterruptibleUtils.String.toModifiedUTF8(imageHeapString, imageHeapString.length(), buffer, buffer.add(length), false, replaceDotWithSlash ? dotWithSlash : null); long rawPointerValue = Word.objectToUntrackedPointer(imageHeapString).rawValue(); - symbol.setHash(UninterruptibleUtils.Long.hashCode(rawPointerValue)); + int hash = UninterruptibleUtils.Long.hashCode(rawPointerValue); + return getSymbolId(buffer, WordFactory.unsigned(length), hash, previousEpoch); + } + + + @Uninterruptible(reason = "Locking without transition and result is only valid until epoch changes.", callerMustBe = true) + public long getSymbolId(PointerBase buffer, UnsignedWord length, int hash, boolean previousEpoch) { + com.oracle.svm.core.util.VMError.guarantee(buffer.isNonNull()); + JfrSymbol symbol = StackValue.get(JfrSymbol.class); + symbol.setModifiedUTF8(buffer); // *** symbol in native memory + symbol.setLength(length); + symbol.setHash(hash); /* * Get an existing entry from the hashtable or insert a new entry. This needs to be atomic @@ -97,7 +115,8 @@ public long getSymbolId(String imageHeapString, boolean previousEpoch, boolean r try { JfrSymbolEpochData epochData = getEpochData(previousEpoch); JfrSymbol existingEntry = (JfrSymbol) epochData.table.get(symbol); - if (existingEntry.isNonNull()) { + if (existingEntry.isNonNull() && symbol.getModifiedUTF8().isNonNull()) { + NullableNativeMemory.free(symbol.getModifiedUTF8()); return existingEntry.getId(); } @@ -111,12 +130,11 @@ public long getSymbolId(String imageHeapString, boolean previousEpoch, boolean r epochData.buffer = JfrBufferAccess.allocate(JfrBufferType.C_HEAP); } - CharReplacer charReplacer = newEntry.getReplaceDotWithSlash() ? dotWithSlash : null; JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); JfrNativeEventWriterDataAccess.initialize(data, epochData.buffer); JfrNativeEventWriter.putLong(data, newEntry.getId()); - JfrNativeEventWriter.putString(data, newEntry.getValue(), charReplacer); + JfrNativeEventWriter.putString(data, (org.graalvm.word.Pointer) newEntry.getModifiedUTF8(), (int) newEntry.getLength().rawValue()); if (!JfrNativeEventWriter.commit(data)) { return 0L; } @@ -163,19 +181,14 @@ private interface JfrSymbol extends UninterruptibleEntry { @RawField void setId(long value); - @PinnedObjectField @RawField - String getValue(); - - @PinnedObjectField + void setLength(UnsignedWord value); @RawField - void setValue(String value); - + UnsignedWord getLength(); @RawField - boolean getReplaceDotWithSlash(); - + void setModifiedUTF8(PointerBase value); @RawField - void setReplaceDotWithSlash(boolean value); + PointerBase getModifiedUTF8(); } private static class JfrSymbolHashtable extends AbstractUninterruptibleHashtable { @@ -204,7 +217,7 @@ public JfrSymbol[] getTable() { protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { JfrSymbol a = (JfrSymbol) v0; JfrSymbol b = (JfrSymbol) v1; - return a.getValue() == b.getValue() && a.getReplaceDotWithSlash() == b.getReplaceDotWithSlash(); + return a.getLength().equal(b.getLength()) && LibC.memcmp(a.getModifiedUTF8(), b.getModifiedUTF8(), a.getLength()) == 0; } @Override @@ -216,6 +229,15 @@ protected UninterruptibleEntry copyToHeap(UninterruptibleEntry symbolOnStack) { } return result; } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected void free(UninterruptibleEntry entry) { + JfrSymbol symbol = (JfrSymbol) entry; + /* The base method will free only the entry itself, not actual utf8 data. */ + NullableNativeMemory.free(symbol.getModifiedUTF8()); + super.free(entry); + } } private static class JfrSymbolEpochData { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadStateSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadStateSerializer.java index ba05a2f7cc7c..8d0b247f20e3 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadStateSerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadStateSerializer.java @@ -31,16 +31,17 @@ * Used to serialize all possible thread states into the chunk. */ public class JfrThreadStateSerializer implements JfrSerializer { + private JfrThreadState[] threadStates; @Platforms(Platform.HOSTED_ONLY.class) public JfrThreadStateSerializer() { + threadStates = JfrThreadState.values(); } @Override public void write(JfrChunkWriter writer) { writer.writeCompressedLong(JfrType.ThreadState.getId()); - JfrThreadState[] threadStates = JfrThreadState.values(); writer.writeCompressedLong(threadStates.length); for (int i = 0; i < threadStates.length; i++) { writer.writeCompressedInt(i); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index 68d90e4ab366..309592722641 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -24,26 +24,43 @@ */ package com.oracle.svm.core.jfr; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import com.oracle.svm.core.UnmanagedMemoryUtil; +import com.oracle.svm.core.headers.LibC; +import com.oracle.svm.core.jdk.UninterruptibleUtils; +import org.graalvm.nativeimage.c.struct.RawField; +import org.graalvm.nativeimage.c.struct.RawStructure; +import com.oracle.svm.core.c.struct.PinnedObjectField; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; - +import org.graalvm.word.PointerBase; +import org.graalvm.word.Pointer; +import org.graalvm.nativeimage.c.struct.SizeOf; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.word.UnsignedWord; +import jdk.graal.compiler.word.Word; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.collections.AbstractUninterruptibleHashtable; +import com.oracle.svm.core.collections.UninterruptibleEntry; +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.hub.LayoutEncoding; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.heap.Heap; import com.oracle.svm.core.jfr.traceid.JfrTraceId; +import com.oracle.svm.core.nmt.NmtCategory; +import com.oracle.svm.core.memory.NullableNativeMemory; /** * Repository that collects and writes used classes, packages, modules, and classloaders. */ public class JfrTypeRepository implements JfrRepository { - private final Set> flushedClasses = new HashSet<>(); - private final Map flushedPackages = new HashMap<>(); - private final Map flushedModules = new HashMap<>(); - private final Map flushedClassLoaders = new HashMap<>(); + private final JfrClassInfoTable flushedClasses = new JfrClassInfoTable(); // *** Ordinary objects, so can be in image heap. + private final JfrPackageInfoTable flushedPackages = new JfrPackageInfoTable(); + private final JfrModuleInfoTable flushedModules = new JfrModuleInfoTable(); + private final JfrClassLoaderInfoTable flushedClassLoaders = new JfrClassLoaderInfoTable(); + private final TypeInfo typeInfo = new TypeInfo(); + private final UninterruptibleUtils.CharReplacer dotWithSlash = new ReplaceDotWithSlash(); private long currentPackageId = 0; private long currentModuleId = 0; private long currentClassLoaderId = 0; @@ -54,6 +71,7 @@ public JfrTypeRepository() { public void teardown() { clearEpochData(); + typeInfo.teardown(); } @Uninterruptible(reason = "Result is only valid until epoch changes.", callerMustBe = true) @@ -63,21 +81,20 @@ public long getClassId(Class clazz) { @Override public int write(JfrChunkWriter writer, boolean flushpoint) { - TypeInfo typeInfo = collectTypeInfo(flushpoint); + typeInfo.reset(); + collectTypeInfo(flushpoint); int count = writeClasses(writer, typeInfo, flushpoint); count += writePackages(writer, typeInfo, flushpoint); count += writeModules(writer, typeInfo, flushpoint); count += writeClassLoaders(writer, typeInfo, flushpoint); - if (flushpoint) { - flushedClasses.addAll(typeInfo.classes); + flushedClasses.putAll(typeInfo.classes); flushedPackages.putAll(typeInfo.packages); flushedModules.putAll(typeInfo.modules); flushedClassLoaders.putAll(typeInfo.classLoaders); } else { clearEpochData(); } - return count; } @@ -85,38 +102,45 @@ public int write(JfrChunkWriter writer, boolean flushpoint) { * Visit all used classes, and collect their packages, modules, classloaders and possibly * referenced classes. */ - private TypeInfo collectTypeInfo(boolean flushpoint) { - TypeInfo typeInfo = new TypeInfo(); - Heap.getHeap().visitLoadedClasses((clazz) -> { - if (flushpoint) { - if (JfrTraceId.isUsedCurrentEpoch(clazz)) { - visitClass(typeInfo, clazz); - } - } else { - if (JfrTraceId.isUsedPreviousEpoch(clazz)) { - JfrTraceId.clearUsedPreviousEpoch(clazz); - visitClass(typeInfo, clazz); + private void collectTypeInfo(boolean flushpoint) { + Class[] classes = Heap.getHeap().getCachedClasses(); + if (classes == null) { + return; + } + for (int i = 0; i < classes.length; i++) { + Class clazz = classes[i]; + if (DynamicHub.fromClass(clazz).isLoaded()) { + if (flushpoint) { + if (JfrTraceId.isUsedCurrentEpoch(clazz)) { + visitClass(typeInfo, clazz); + } + } else { + if (JfrTraceId.isUsedPreviousEpoch(clazz)) { + JfrTraceId.clearUsedPreviousEpoch(clazz); + visitClass(typeInfo, clazz); + } } } - }); - return typeInfo; + } + } private void visitClass(TypeInfo typeInfo, Class clazz) { if (clazz != null && addClass(typeInfo, clazz)) { visitClassLoader(typeInfo, clazz.getClassLoader()); - visitPackage(typeInfo, clazz.getPackage(), clazz.getModule()); + visitPackage(typeInfo, clazz); visitClass(typeInfo, clazz.getSuperclass()); } } - private void visitPackage(TypeInfo typeInfo, Package pkg, Module module) { - if (pkg != null && addPackage(typeInfo, pkg, module)) { - visitModule(typeInfo, module); + private void visitPackage(TypeInfo typeInfo, Class clazz) { + if (addPackage(typeInfo, clazz)) { + visitModule(typeInfo, clazz); } } - private void visitModule(TypeInfo typeInfo, Module module) { + private void visitModule(TypeInfo typeInfo, Class clazz) { + Module module = clazz.getModule(); if (module != null && addModule(typeInfo, module)) { visitClassLoader(typeInfo, module.getClassLoader()); } @@ -130,25 +154,34 @@ private void visitClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { } private int writeClasses(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { - if (typeInfo.classes.isEmpty()) { + int size = typeInfo.classes.getSize(); + ClassInfoRaw[] table = (ClassInfoRaw[]) typeInfo.classes.getTable(); + if (size == 0) { return EMPTY; } writer.writeCompressedLong(JfrType.Class.getId()); - writer.writeCompressedInt(typeInfo.classes.size()); - - for (Class clazz : typeInfo.classes) { - writeClass(typeInfo, writer, clazz, flushpoint); + writer.writeCompressedInt(size); + + // *** we can't use visitor pattern, but maybe there's a better way than duplicating this over and over again. + for (int i = 0; i < table.length; i++) { + ClassInfoRaw entry = table[i]; + while (entry.isNonNull()) { + writeClass(typeInfo, writer, entry, flushpoint); + entry = entry.getNext(); + } } return NON_EMPTY; } - private void writeClass(TypeInfo typeInfo, JfrChunkWriter writer, Class clazz, boolean flushpoint) { - writer.writeCompressedLong(JfrTraceId.getTraceId(clazz)); - writer.writeCompressedLong(getClassLoaderId(typeInfo, clazz.getClassLoader())); - writer.writeCompressedLong(getSymbolId(writer, clazz.getName(), flushpoint, true)); - writer.writeCompressedLong(getPackageId(typeInfo, clazz.getPackage())); - writer.writeCompressedLong(clazz.getModifiers()); - writer.writeBoolean(clazz.isHidden()); + private void writeClass(TypeInfo typeInfo, JfrChunkWriter writer, ClassInfoRaw classInfoRaw, boolean flushpoint) { + assert classInfoRaw.getHash() != 0; + + writer.writeCompressedLong(classInfoRaw.getId()); + writer.writeCompressedLong(getClassLoaderId(typeInfo, classInfoRaw.getClassLoaderName(), classInfoRaw.getHasClassLoader())); + writer.writeCompressedLong(getSymbolId(writer, classInfoRaw.getName(), flushpoint, true)); + writer.writeCompressedLong(getPackageId(typeInfo, classInfoRaw.getModifiedUTF8PackageName(), classInfoRaw.getNameLength(), getHash(classInfoRaw.getName()))); + writer.writeCompressedLong(classInfoRaw.getModifiers()); + writer.writeBoolean(classInfoRaw.getIsHidden()); } @Uninterruptible(reason = "Needed for JfrSymbolRepository.getSymbolId().") @@ -161,159 +194,241 @@ private static long getSymbolId(JfrChunkWriter writer, String symbol, boolean fl return SubstrateJVM.getSymbolRepository().getSymbolId(symbol, !flushpoint, replaceDotWithSlash); } + // Copy to a new buffer so each table has its own copy of the data. This simplifies cleanup and mitigates double frees. + @Uninterruptible(reason = "Needed for JfrSymbolRepository.getSymbolId().") + private static long getSymbolId(JfrChunkWriter writer, PointerBase source, UnsignedWord length, int hash, boolean flushpoint) { + Pointer destination = NullableNativeMemory.malloc(length, NmtCategory.JFR); + if (destination.isNull()) { + return 0L; + } + UnmanagedMemoryUtil.copy((Pointer) source, destination, length); + + assert writer.isLockedByCurrentThread(); + return SubstrateJVM.getSymbolRepository().getSymbolId(destination, length, hash, !flushpoint); + } + private int writePackages(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { - if (typeInfo.packages.isEmpty()) { + int size = typeInfo.packages.getSize(); + PackageInfoRaw[] table = (PackageInfoRaw[]) typeInfo.packages.getTable(); + if (size == 0) { return EMPTY; } writer.writeCompressedLong(JfrType.Package.getId()); - writer.writeCompressedInt(typeInfo.packages.size()); + writer.writeCompressedInt(size); - for (Map.Entry pkgInfo : typeInfo.packages.entrySet()) { - writePackage(typeInfo, writer, pkgInfo.getKey(), pkgInfo.getValue(), flushpoint); + for (int i = 0; i < table.length; i++) { + PackageInfoRaw packageInfoRaw = table[i]; + while (packageInfoRaw.isNonNull()) { + writePackage(typeInfo, writer, packageInfoRaw, flushpoint); + packageInfoRaw = packageInfoRaw.getNext(); + } } return NON_EMPTY; } - private void writePackage(TypeInfo typeInfo, JfrChunkWriter writer, String pkgName, PackageInfo pkgInfo, boolean flushpoint) { - writer.writeCompressedLong(pkgInfo.id); // id - writer.writeCompressedLong(getSymbolId(writer, pkgName, flushpoint, true)); - writer.writeCompressedLong(getModuleId(typeInfo, pkgInfo.module)); + private void writePackage(TypeInfo typeInfo, JfrChunkWriter writer, PackageInfoRaw packageInfoRaw, boolean flushpoint) { + assert packageInfoRaw.getHash() != 0; + writer.writeCompressedLong(packageInfoRaw.getId()); // id + // Packages with the same name use the same buffer for the whole epoch so it's fine to use that address as the symbol repo hash. No further need to deduplicate. + writer.writeCompressedLong(getSymbolId(writer, packageInfoRaw.getModifiedUTF8Name(), packageInfoRaw.getNameLength(), UninterruptibleUtils.Long.hashCode(packageInfoRaw.getModifiedUTF8Name().rawValue()), flushpoint)); + writer.writeCompressedLong(getModuleId(typeInfo, packageInfoRaw.getModuleName(), packageInfoRaw.getHasModule())); writer.writeBoolean(false); // exported } private int writeModules(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { - if (typeInfo.modules.isEmpty()) { + int size = typeInfo.modules.getSize(); + ModuleInfoRaw[] table = (ModuleInfoRaw[]) typeInfo.modules.getTable(); + if (size == 0) { return EMPTY; } writer.writeCompressedLong(JfrType.Module.getId()); - writer.writeCompressedInt(typeInfo.modules.size()); + writer.writeCompressedInt(size); - for (Map.Entry modInfo : typeInfo.modules.entrySet()) { - writeModule(typeInfo, writer, modInfo.getKey(), modInfo.getValue(), flushpoint); + for (int i = 0; i < table.length; i++) { + ModuleInfoRaw entry = table[i]; + while (entry.isNonNull()) { + writeModule(typeInfo, writer, entry, flushpoint); + entry = entry.getNext(); + } } return NON_EMPTY; } - private void writeModule(TypeInfo typeInfo, JfrChunkWriter writer, Module module, long id, boolean flushpoint) { - writer.writeCompressedLong(id); - writer.writeCompressedLong(getSymbolId(writer, module.getName(), flushpoint, false)); + private void writeModule(TypeInfo typeInfo, JfrChunkWriter writer, ModuleInfoRaw moduleInfoRaw, boolean flushpoint) { + writer.writeCompressedLong(moduleInfoRaw.getId()); + writer.writeCompressedLong(getSymbolId(writer, moduleInfoRaw.getName(), flushpoint, false)); writer.writeCompressedLong(0); // Version, e.g. "11.0.10-internal" writer.writeCompressedLong(0); // Location, e.g. "jrt:/java.base" - writer.writeCompressedLong(getClassLoaderId(typeInfo, module.getClassLoader())); + writer.writeCompressedLong(getClassLoaderId(typeInfo, moduleInfoRaw.getClassLoaderName(), moduleInfoRaw.getHasClassLoader())); } private static int writeClassLoaders(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { - if (typeInfo.classLoaders.isEmpty()) { + if (typeInfo.classLoaders.getSize() == 0) { return EMPTY; } writer.writeCompressedLong(JfrType.ClassLoader.getId()); - writer.writeCompressedInt(typeInfo.classLoaders.size()); + writer.writeCompressedInt(typeInfo.classLoaders.getSize()); - for (Map.Entry clInfo : typeInfo.classLoaders.entrySet()) { - writeClassLoader(writer, clInfo.getKey(), clInfo.getValue(), flushpoint); + for (int i = 0; i < typeInfo.classLoaders.getTable().length; i++) { + ClassLoaderInfoRaw entry = (ClassLoaderInfoRaw) typeInfo.classLoaders.getTable()[i]; + while (entry.isNonNull()) { + writeClassLoader(writer, entry, flushpoint); + entry = entry.getNext(); + } } return NON_EMPTY; } - private static void writeClassLoader(JfrChunkWriter writer, ClassLoader cl, long id, boolean flushpoint) { - writer.writeCompressedLong(id); - if (cl == null) { + private static void writeClassLoader(JfrChunkWriter writer, ClassLoaderInfoRaw classLoaderInfoRaw, boolean flushpoint) { + writer.writeCompressedLong(classLoaderInfoRaw.getId()); + if (classLoaderInfoRaw.getName() == null) { // TODO is this branch reachable now? I don't think it was reachable before my changes either. weird!! writer.writeCompressedLong(0); writer.writeCompressedLong(getSymbolId(writer, "bootstrap", flushpoint, false)); } else { - writer.writeCompressedLong(JfrTraceId.getTraceId(cl.getClass())); - writer.writeCompressedLong(getSymbolId(writer, cl.getName(), flushpoint, false)); + writer.writeCompressedLong(classLoaderInfoRaw.getClassTraceId()); + writer.writeCompressedLong(getSymbolId(writer, classLoaderInfoRaw.getName(), flushpoint, false)); } } - private static class PackageInfo { - private final long id; - private final Module module; + private boolean addClass(TypeInfo typeInfo, Class clazz) { + boolean hasPackage = false; + ClassInfoRaw classInfoRaw = StackValue.get(ClassInfoRaw.class); + classInfoRaw.setId(JfrTraceId.getTraceId(clazz)); + classInfoRaw.setHash(getHash(clazz.getName())); - PackageInfo(long id, Module module) { - this.id = id; - this.module = module; + // Once the traceID is set, we can do a look-up. + if (isClassVisited(typeInfo, classInfoRaw)) { + return false; } - } - private boolean addClass(TypeInfo typeInfo, Class clazz) { - if (isClassVisited(typeInfo, clazz)) { - return false; + // Class hasn't yet been visited so set the package info. Package name buffer is malloc'ed. + PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); + if (setPackageNameAndLength(clazz, packageInfoRaw)) { + hasPackage = true; } - return typeInfo.classes.add(clazz); + + classInfoRaw.setModifiedUTF8PackageName(hasPackage ? packageInfoRaw.getModifiedUTF8Name() : WordFactory.nullPointer()); + classInfoRaw.setNameLength(hasPackage ? packageInfoRaw.getNameLength() : WordFactory.unsigned(0)); + classInfoRaw.setName(clazz.getName()); + classInfoRaw.setIsHidden(clazz.isHidden()); + classInfoRaw.setModifiers(clazz.getModifiers()); + // TODO the orig uses the class's CL but should we use the module's CL? Thats the only place we addClassloader + classInfoRaw.setHasClassLoader(clazz.getClassLoader() != null); + classInfoRaw.setClassLoaderName(classInfoRaw.getHasClassLoader() ? clazz.getClassLoader().getName() : null); // *** if you forget to set something accessing it will segfault. + assert !typeInfo.classes.contains(classInfoRaw); + typeInfo.classes.putNew(classInfoRaw); // *** must be value on stack. Hopefully it shallow copies the malloc'ed buffer. It does since it just uses memcpy on the header + assert typeInfo.classes.contains(classInfoRaw); + return hasPackage; } - private boolean isClassVisited(TypeInfo typeInfo, Class clazz) { - return typeInfo.classes.contains(clazz) || flushedClasses.contains(clazz); + private boolean isClassVisited(TypeInfo typeInfo, ClassInfoRaw classInfoRaw) { + return typeInfo.classes.contains(classInfoRaw) || flushedClasses.contains(classInfoRaw); } - private boolean addPackage(TypeInfo typeInfo, Package pkg, Module module) { - if (isPackageVisited(typeInfo, pkg)) { - assert module == (flushedPackages.containsKey(pkg.getName()) ? flushedPackages.get(pkg.getName()).module : typeInfo.packages.get(pkg.getName()).module); + private boolean addPackage(TypeInfo typeInfo, Class clazz) { + if (clazz.isPrimitive() || clazz.isArray()) { + return false; + } + // *** if we've made it this far, we know the package is not null. Although the name may be empty + boolean hasModule = clazz.getModule() != null; + String moduleName = hasModule ? clazz.getModule().getName() : null; + + PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); + setPackageNameAndLength(clazz, packageInfoRaw); + packageInfoRaw.setHash(getHash(clazz.getName())); + if (isPackageVisited(typeInfo, packageInfoRaw)) { + assert moduleName == (flushedPackages.contains(packageInfoRaw) ? ((PackageInfoRaw)flushedPackages.get(packageInfoRaw)).getModuleName() : ((PackageInfoRaw)typeInfo.packages.get(packageInfoRaw)).getModuleName()); + NullableNativeMemory.free(packageInfoRaw.getModifiedUTF8Name()); return false; } // The empty package represented by "" is always traced with id 0 - long id = pkg.getName().isEmpty() ? 0 : ++currentPackageId; - typeInfo.packages.put(pkg.getName(), new PackageInfo(id, module)); + long id = packageInfoRaw.getNameLength().belowOrEqual(0) ? 0 : ++currentPackageId; + packageInfoRaw.setId(id); + packageInfoRaw.setHasModule(hasModule); + packageInfoRaw.setModuleName(moduleName); + assert !typeInfo.packages.contains(packageInfoRaw); // *** remove later + typeInfo.packages.putNew(packageInfoRaw); + assert typeInfo.packages.contains(packageInfoRaw); return true; } - private boolean isPackageVisited(TypeInfo typeInfo, Package pkg) { - return flushedPackages.containsKey(pkg.getName()) || typeInfo.packages.containsKey(pkg.getName()); + private boolean isPackageVisited(TypeInfo typeInfo, PackageInfoRaw packageInfoRaw) { + return flushedPackages.contains(packageInfoRaw) || typeInfo.packages.contains(packageInfoRaw); } - private long getPackageId(TypeInfo typeInfo, Package pkg) { - if (pkg != null) { - if (flushedPackages.containsKey(pkg.getName())) { - return flushedPackages.get(pkg.getName()).id; + private long getPackageId(TypeInfo typeInfo, PointerBase modifiedUTF8PackageName, UnsignedWord length, int hash) { + if (modifiedUTF8PackageName.isNonNull() && length.aboveOrEqual(1)) { + PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); + packageInfoRaw.setModifiedUTF8Name(modifiedUTF8PackageName); + packageInfoRaw.setNameLength(length); + packageInfoRaw.setHash(hash); // Using the associated class' name for the hash + if (flushedPackages.contains(packageInfoRaw)) { + return ((PackageInfoRaw) flushedPackages.get(packageInfoRaw)).getId(); } - return typeInfo.packages.get(pkg.getName()).id; + return ((PackageInfoRaw) typeInfo.packages.get(packageInfoRaw)).getId(); } else { return 0; } } private boolean addModule(TypeInfo typeInfo, Module module) { - if (isModuleVisited(typeInfo, module)) { + ModuleInfoRaw moduleInfoRaw = StackValue.get(ModuleInfoRaw.class); + moduleInfoRaw.setName(module.getName()); + moduleInfoRaw.setHash(getHash(module.getName())); + if (isModuleVisited(typeInfo, moduleInfoRaw)) { return false; } - typeInfo.modules.put(module, ++currentModuleId); + moduleInfoRaw.setId(++currentModuleId); + moduleInfoRaw.setHasClassLoader(module.getClassLoader() != null); + moduleInfoRaw.setClassLoaderName(moduleInfoRaw.getHasClassLoader() ? module.getClassLoader().getName() : null); + typeInfo.modules.putNew(moduleInfoRaw); return true; } - private boolean isModuleVisited(TypeInfo typeInfo, Module module) { - return typeInfo.modules.containsKey(module) || flushedModules.containsKey(module); + private boolean isModuleVisited(TypeInfo typeInfo, ModuleInfoRaw moduleInfoRaw) { + return typeInfo.modules.contains(moduleInfoRaw) || flushedModules.contains(moduleInfoRaw); } - private long getModuleId(TypeInfo typeInfo, Module module) { - if (module != null) { - if (flushedModules.containsKey(module)) { - return flushedModules.get(module); + private long getModuleId(TypeInfo typeInfo, String moduleName, boolean hasModule) { + if (hasModule) { + ModuleInfoRaw moduleInfoRaw = StackValue.get(ModuleInfoRaw.class); + moduleInfoRaw.setName(moduleName); + moduleInfoRaw.setHash(getHash(moduleName)); + if (flushedModules.contains(moduleInfoRaw)) { + return ((ModuleInfoRaw) flushedModules.get(moduleInfoRaw)).getId(); } - return typeInfo.modules.get(module); + return ((ModuleInfoRaw) typeInfo.modules.get(moduleInfoRaw)).getId(); } else { return 0; } } private boolean addClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { - if (isClassLoaderVisited(typeInfo, classLoader)) { + ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); + classLoaderInfoRaw.setName(classLoader.getName()); + classLoaderInfoRaw.setHash(getHash(classLoader.getName())); + if (isClassLoaderVisited(typeInfo, classLoaderInfoRaw)) { return false; } - typeInfo.classLoaders.put(classLoader, ++currentClassLoaderId); + classLoaderInfoRaw.setId(++currentClassLoaderId); + classLoaderInfoRaw.setClassTraceId(JfrTraceId.getTraceId(classLoader.getClass())); + typeInfo.classLoaders.putNew(classLoaderInfoRaw); return true; } - private boolean isClassLoaderVisited(TypeInfo typeInfo, ClassLoader classLoader) { - return flushedClassLoaders.containsKey(classLoader) || typeInfo.classLoaders.containsKey(classLoader); + private boolean isClassLoaderVisited(TypeInfo typeInfo, ClassLoaderInfoRaw classLoaderInfoRaw) { + return flushedClassLoaders.contains(classLoaderInfoRaw) || typeInfo.classLoaders.contains(classLoaderInfoRaw); } - private long getClassLoaderId(TypeInfo typeInfo, ClassLoader classLoader) { - if (classLoader != null) { - if (flushedClassLoaders.containsKey(classLoader)) { - return flushedClassLoaders.get(classLoader); + private long getClassLoaderId(TypeInfo typeInfo, String classLoaderName, boolean hasClassLoader) { + if (hasClassLoader) { + ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); + classLoaderInfoRaw.setName(classLoaderName); + classLoaderInfoRaw.setHash(getHash(classLoaderName)); + if (flushedClassLoaders.contains(classLoaderInfoRaw)) { + return ((ClassLoaderInfoRaw) flushedClassLoaders.get(classLoaderInfoRaw)).getId(); } - return typeInfo.classLoaders.get(classLoader); + return ((ClassLoaderInfoRaw) typeInfo.classLoaders.get(classLoaderInfoRaw)).getId(); } return 0; } @@ -328,10 +443,352 @@ private void clearEpochData() { currentClassLoaderId = 0; } - private static final class TypeInfo { - final Set> classes = new HashSet<>(); - final Map packages = new HashMap<>(); - final Map modules = new HashMap<>(); - final Map classLoaders = new HashMap<>(); + private final class TypeInfo { + final JfrClassInfoTable classes = new JfrClassInfoTable(); + final JfrPackageInfoTable packages = new JfrPackageInfoTable(); + final JfrModuleInfoTable modules = new JfrModuleInfoTable(); + final JfrClassLoaderInfoTable classLoaders = new JfrClassLoaderInfoTable(); + void reset() { + classes.clear(); + packages.clear(); + modules.clear(); + classLoaders.clear(); + } + + void teardown() { + classes.teardown(); + packages.teardown(); + modules.teardown(); + classLoaders.teardown(); + } + } + + // *** we shouldn't preemtively compute ALL package names, only the ones used in JFR events. This current approach is ok, but since we don't stash, we must recompute every time. + // *** Maybe its not a big deal since we call getPackage() on every class we visit anyway. And we only do this for classes that are in an event. + /** This method sets the package name and length. packageInfoRaw may be on the stack or native memory.*/ + private boolean setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoRaw) { + if (clazz.isPrimitive() || clazz.isArray()) { + return false; + } + + DynamicHub hub = DynamicHub.fromClass(clazz); + if (!LayoutEncoding.isHybrid(hub.getLayoutEncoding())) { + while (hub.hubIsArray()) { + hub = hub.getComponentType(); + } + } + + String str = hub.getName(); + int dot = str.lastIndexOf('.'); + if (dot == -1) { + dot = 0; + } + + int utf8Length = UninterruptibleUtils.String.modifiedUTF8Length(str, false); + Pointer buffer = NullableNativeMemory.malloc(utf8Length, NmtCategory.JFR); + Pointer bufferEnd = buffer.add(utf8Length); + + // If malloc fails, we'll just set a blank package name. + if (buffer.isNull()) { + return false; + } + + assert buffer.add(dot).belowOrEqual(bufferEnd); + + Pointer packageNameEnd = UninterruptibleUtils.String.toModifiedUTF8(str, dot, buffer, bufferEnd, false, dotWithSlash); // *** we need to replace dots w slashes in here now. + packageInfoRaw.setModifiedUTF8Name(buffer); + + UnsignedWord packageNameLength = packageNameEnd.subtract(buffer); // end - start + packageInfoRaw.setNameLength(packageNameLength); + if (dot == 0) { + assert packageNameLength.equal(0); + } + return true; + } + + private static int getHash(String imageHeapString) { + // It's possible the type exists, but has no name. + if (imageHeapString == null){ + return 0; + } + long rawPointerValue = Word.objectToUntrackedPointer(imageHeapString).rawValue(); + return UninterruptibleUtils.Long.hashCode(rawPointerValue); + } + + @RawStructure + public interface JfrTypeInfo extends UninterruptibleEntry { + @PinnedObjectField + @RawField + void setName(String value); + @PinnedObjectField + @RawField + String getName(); + @RawField + void setId(long value); + @RawField + long getId(); + } + + + @RawStructure + public interface ClassInfoRaw extends JfrTypeInfo { + @PinnedObjectField + @RawField + void setClassLoaderName(String value); + @PinnedObjectField + @RawField + String getClassLoaderName(); + @RawField + void setHasClassLoader(boolean value); + @RawField + boolean getHasClassLoader(); + @RawField + void setNameLength(UnsignedWord value); + @RawField + UnsignedWord getNameLength(); + @RawField + void setModifiedUTF8PackageName(PointerBase value); + @RawField + PointerBase getModifiedUTF8PackageName(); + @RawField + void setModifiers(long value); + @RawField + long getModifiers(); + @RawField + void setIsHidden(boolean value); + @RawField + boolean getIsHidden(); + } + + @RawStructure + public interface PackageInfoRaw extends JfrTypeInfo { + @PinnedObjectField + @RawField + void setModuleName(String value); + @PinnedObjectField + @RawField + String getModuleName(); + @RawField + void setHasModule(boolean value); + @RawField + boolean getHasModule(); + @RawField + void setNameLength(UnsignedWord value); + @RawField + UnsignedWord getNameLength(); + @RawField + void setModifiedUTF8Name(PointerBase value); + @RawField + PointerBase getModifiedUTF8Name(); + } + + @RawStructure + public interface ModuleInfoRaw extends JfrTypeInfo { + @PinnedObjectField + @RawField + void setClassLoaderName(String value); + @PinnedObjectField + @RawField + String getClassLoaderName(); + @RawField + void setHasClassLoader(boolean value); + @RawField + boolean getHasClassLoader(); + } + + @RawStructure + public interface ClassLoaderInfoRaw extends JfrTypeInfo { + @RawField + void setClassTraceId(long value); + @RawField + long getClassTraceId(); + } + + private abstract class JfrTypeInfoTable extends AbstractUninterruptibleHashtable { + public JfrTypeInfoTable(NmtCategory nmtCategory) { + super(nmtCategory); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { + JfrTypeInfo a = (JfrTypeInfo) v0; + JfrTypeInfo b = (JfrTypeInfo) v1; + return a.getName() == b.getName(); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void putAll(JfrTypeInfoTable sourceTable) { + for (int i = 0; i < sourceTable.getTable().length; i++) { + JfrTypeInfo entry = (JfrTypeInfo) sourceTable.getTable()[i]; + while (entry.isNonNull()) { + putNew(entry); + entry = entry.getNext(); + } + } + } + } + + private final class JfrClassInfoTable extends JfrTypeInfoTable { + @Platforms(Platform.HOSTED_ONLY.class) + public JfrClassInfoTable() { + super(NmtCategory.JFR); + } + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected ClassInfoRaw[] createTable(int size) { + return new ClassInfoRaw[size]; + } + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { + JfrTypeInfo a = (JfrTypeInfo) v0; + JfrTypeInfo b = (JfrTypeInfo) v1; + return a.getId() == b.getId(); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected UninterruptibleEntry copyToHeap(UninterruptibleEntry visitedOnStack) { + return copyToHeap(visitedOnStack, SizeOf.unsigned(ClassInfoRaw.class)); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected void free(UninterruptibleEntry entry) { + ClassInfoRaw classInfoRaw = (ClassInfoRaw) entry; + /* The base method will free only the entry itself, not th utf8 data. */ + NullableNativeMemory.free(classInfoRaw.getModifiedUTF8PackageName()); + classInfoRaw.setModifiedUTF8PackageName(WordFactory.nullPointer()); + super.free(entry); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void putAll(JfrClassInfoTable sourceTable) { + for (int i = 0; i < sourceTable.getTable().length; i++) { + ClassInfoRaw sourceInfo = (ClassInfoRaw) sourceTable.getTable()[i]; + while (sourceInfo.isNonNull()) { + if (!contains(sourceInfo)) { + // Put if not already there. + ClassInfoRaw destinationInfo = (ClassInfoRaw) putNew(sourceInfo); // *** does a shallow copy... BAD it can overwrites ptrs that were malloc'ed!!! ... but there shouldnt be overlap anyway... + // allocate a new buffer + PointerBase newUtf8Name = NullableNativeMemory.malloc(sourceInfo.getNameLength(), NmtCategory.JFR); + // set the buffer ptr + destinationInfo.setModifiedUTF8PackageName(newUtf8Name); + // Copy source buffer contents over to new buffer + if (newUtf8Name.isNonNull()) { + UnmanagedMemoryUtil.copy((Pointer) sourceInfo.getModifiedUTF8PackageName(), (Pointer) newUtf8Name, sourceInfo.getNameLength()); + } + } + sourceInfo = sourceInfo.getNext(); + } + } + } + } + + private final class JfrPackageInfoTable extends JfrTypeInfoTable { + @Platforms(Platform.HOSTED_ONLY.class) + public JfrPackageInfoTable() { + super(NmtCategory.JFR); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected PackageInfoRaw[] createTable(int size) { + return new PackageInfoRaw[size]; + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected UninterruptibleEntry copyToHeap(UninterruptibleEntry visitedOnStack) { + return copyToHeap(visitedOnStack, SizeOf.unsigned(PackageInfoRaw.class)); + } + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { + // *** We can't compare IDs bc that's something we assign after we do the check. + PackageInfoRaw entry1 = (PackageInfoRaw) v0; + PackageInfoRaw entry2 = (PackageInfoRaw) v1; + return entry1.getNameLength().equal(entry2.getNameLength()) && LibC.memcmp(entry1.getModifiedUTF8Name(), entry2.getModifiedUTF8Name(), entry1.getNameLength()) == 0; + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected void free(UninterruptibleEntry entry) { + PackageInfoRaw packageInfoRaw = (PackageInfoRaw) entry; + /* The base method will free only the entry itself, not th utf8 data. */ + NullableNativeMemory.free(packageInfoRaw.getModifiedUTF8Name()); + packageInfoRaw.setModifiedUTF8Name(WordFactory.nullPointer()); + super.free(entry); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void putAll(JfrPackageInfoTable sourceTable) { + for (int i = 0; i < sourceTable.getTable().length; i++) { + PackageInfoRaw sourceInfo = (PackageInfoRaw) sourceTable.getTable()[i]; + while (sourceInfo.isNonNull()) { + if (!contains(sourceInfo)) { + // Put if not already there. + PackageInfoRaw destinationInfo = (PackageInfoRaw) putNew(sourceInfo); + // allocate a new buffer + PointerBase newUtf8Name = NullableNativeMemory.malloc(sourceInfo.getNameLength(), NmtCategory.JFR); + // set the buffer ptr + destinationInfo.setModifiedUTF8Name(newUtf8Name); + // Copy source buffer contents over to new buffer + if (newUtf8Name.isNonNull()) { + UnmanagedMemoryUtil.copy((Pointer) sourceInfo.getModifiedUTF8Name(), (Pointer) newUtf8Name, sourceInfo.getNameLength()); + } + } + sourceInfo = sourceInfo.getNext(); + } + } + } + } + + private final class JfrModuleInfoTable extends JfrTypeInfoTable { + @Platforms(Platform.HOSTED_ONLY.class) + public JfrModuleInfoTable() { + super(NmtCategory.JFR); + } + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected ModuleInfoRaw[] createTable(int size) { + return new ModuleInfoRaw[size]; + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected UninterruptibleEntry copyToHeap(UninterruptibleEntry visitedOnStack) { + return copyToHeap(visitedOnStack, SizeOf.unsigned(ModuleInfoRaw.class)); + } + } + + private final class JfrClassLoaderInfoTable extends JfrTypeInfoTable { + @Platforms(Platform.HOSTED_ONLY.class) + public JfrClassLoaderInfoTable() { + super(NmtCategory.JFR); + } + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected ClassLoaderInfoRaw[] createTable(int size) { + return new ClassLoaderInfoRaw[size]; + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected UninterruptibleEntry copyToHeap(UninterruptibleEntry visitedOnStack) { + return copyToHeap(visitedOnStack, SizeOf.unsigned(ClassLoaderInfoRaw.class)); + } + } + + private static final class ReplaceDotWithSlash implements UninterruptibleUtils.CharReplacer { + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public char replace(char ch) { + if (ch == '.') { + return '/'; + } + return ch; + } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index 5fe55113dc2c..018447d510ae 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -33,6 +33,7 @@ import org.graalvm.word.Pointer; import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.heap.Heap; import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.jfr.events.JfrAllocationEvents; @@ -336,6 +337,9 @@ public void beginRecording() { return; } + // Cache all classes in preparation for TypeRepository TODO maybe there is a better way + Heap.getHeap().visitLoadedClasses(clazz -> {}); + JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); try { // It is possible that setOutput was called with a filename earlier. In that case, we diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/oldobject/JfrOldObjectRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/oldobject/JfrOldObjectRepository.java index 18441411bbe0..0c7b0871e470 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/oldobject/JfrOldObjectRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/oldobject/JfrOldObjectRepository.java @@ -85,7 +85,7 @@ public long serializeOldObject(Object obj) { JfrNativeEventWriterDataAccess.initialize(data, epochData.buffer); JfrNativeEventWriter.putLong(data, id); JfrNativeEventWriter.putLong(data, pointer.rawValue()); - JfrNativeEventWriter.putLong(data, SubstrateJVM.getTypeRepository().getClassId(obj.getClass())); + JfrNativeEventWriter.putClass(data, obj.getClass()); writeDescription(obj, data); JfrNativeEventWriter.putLong(data, 0L); // GC root if (!JfrNativeEventWriter.commit(data)) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java index 9dea252eedc1..6f81c9222aac 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java @@ -24,9 +24,14 @@ */ package com.oracle.svm.core.util; +import com.oracle.svm.core.graal.stackvalue.UnsafeStackValue; + +import org.graalvm.nativeimage.c.struct.RawField; +import org.graalvm.nativeimage.c.struct.RawStructure; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.word.PointerBase; import com.oracle.svm.core.Uninterruptible; @@ -48,26 +53,16 @@ protected PlatformTimeUtils() { private long last = 0; - public record SecondsNanos(long seconds, long nanos) { - } - - @Uninterruptible(reason = "Wrap the now safe call to interruptibly allocate a SecondsNanos object.", calleeMustBe = false) - protected static SecondsNanos allocateSecondsNanosInterruptibly(long seconds, long nanos) { - return allocateSecondsNanos0(seconds, nanos); - } - - private static SecondsNanos allocateSecondsNanos0(long seconds, long nanos) { - return new SecondsNanos(seconds, nanos); - } - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+5/src/hotspot/share/jfr/recorder/repository/jfrChunk.cpp#L38-L52") public long nanosNow() { // Use same clock source as Instant.now() to ensure // that Recording::getStopTime() returns an Instant that // is in sync. - var t = javaTimeSystemUTC(); - long seconds = t.seconds; - long nanos = t.nanos; + SecondsNanos t = UnsafeStackValue.get(SecondsNanos.class); + PlatformTimeUtils.singleton().javaTimeSystemUTC(t); + javaTimeSystemUTC(t); + long seconds = t.getSeconds(); + long nanos = t.getNanos(); long now = seconds * 1000000000 + nanos; if (now > last) { last = now; @@ -75,5 +70,17 @@ public long nanosNow() { return last; } - public abstract SecondsNanos javaTimeSystemUTC(); + public abstract void javaTimeSystemUTC(SecondsNanos secondsNanos); + + @RawStructure + public interface SecondsNanos extends PointerBase { + @RawField + void setNanos(long value); + @RawField + long getNanos(); + @RawField + void setSeconds(long value); + @RawField + long getSeconds(); + } } From 46e2d11454c4a1fc6b1c98387a99a2d06a4a7e76 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Thu, 13 Mar 2025 11:12:33 -0400 Subject: [PATCH 02/16] Compiling dump file is working --- .../jfr/PosixJfrEmergencyDumpSupport.java | 302 ++++++++++++++++++ .../oracle/svm/core/heap/OutOfMemoryUtil.java | 2 +- .../svm/core/jfr/JfrChunkFileWriter.java | 87 +++-- .../oracle/svm/core/jfr/JfrChunkNoWriter.java | 5 + .../oracle/svm/core/jfr/JfrChunkWriter.java | 1 + .../svm/core/jfr/JfrEmergencyDumpFeature.java | 48 +++ .../svm/core/jfr/JfrEmergencyDumpSupport.java | 22 ++ .../com/oracle/svm/core/jfr/JfrFeature.java | 3 +- .../svm/core/jfr/JfrRecorderThread.java | 1 + .../svm/core/jfr/JfrTypeRepository.java | 10 +- .../com/oracle/svm/core/jfr/SubstrateJVM.java | 30 +- 11 files changed, 467 insertions(+), 44 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java new file mode 100644 index 000000000000..00b69039d12a --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -0,0 +1,302 @@ +package com.oracle.svm.core.posix.jfr; +import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.headers.LibC; +import com.oracle.svm.core.nmt.NmtCategory; +import com.oracle.svm.core.posix.headers.Dirent; +import jdk.graal.compiler.word.Word; +//import jdk.jfr.internal.LogLevel; +//import jdk.jfr.internal.LogTag; +//import jdk.jfr.internal.Logger; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.handles.PrimitiveArrayView; +import com.oracle.svm.core.os.RawFileOperationSupport; +import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; +import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; +import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; + + +import jdk.graal.compiler.api.replacements.Fold; + +import java.nio.charset.StandardCharsets; + +import static com.oracle.svm.core.posix.headers.Fcntl.O_NOFOLLOW; +import static com.oracle.svm.core.posix.headers.Fcntl.O_RDONLY; +public class PosixJfrEmergencyDumpSupport implements com.oracle.svm.core.jfr.JfrEmergencyDumpSupport { + private static final int CHUNK_FILE_HEADER_SIZE = 68;// TODO based on jdk file + private static final int JVM_MAXPATHLEN = 4096;// TODO based on jdk file + private static final byte FILE_SEPARATOR = "/".getBytes(StandardCharsets.UTF_8)[0]; + private static final byte[] DUMP_FILE_PREFIX = "hs_oom_pid_".getBytes(StandardCharsets.UTF_8); + private static final byte[] CHUNKFILE_EXTENSION_BYTES = ".jfr".getBytes(StandardCharsets.UTF_8); // TODO double check its utf8 you want. + private Dirent.DIR directory; + private byte[] pidBytes; + private byte[] dumpPathBytes; + private byte[] repositoryLocationBytes; + private RawFileDescriptor emergencyFd; + private PrimitiveArrayView pathBuffer; + + @Platforms(Platform.HOSTED_ONLY.class) + public PosixJfrEmergencyDumpSupport() { + } + + public void initialize() { + pidBytes = String.valueOf(ProcessHandle.current().pid()).getBytes(StandardCharsets.UTF_8); + pathBuffer = PrimitiveArrayView.createForReadingAndWriting(new byte[JVM_MAXPATHLEN]); + directory = org.graalvm.word.WordFactory.nullPointer(); // *** maybe not necessary. + //TODO need to terminate the string with 0. otherwise length function will not work. + } + + public void setRepositoryLocation(String dirText) { + repositoryLocationBytes = dirText.getBytes(StandardCharsets.UTF_8); + } + + private CCharPointer getRepositoryLocation() { + clearPathBuffer(); + for (int i = 0; i < repositoryLocationBytes.length; i++) { + getPathBuffer().write(i, repositoryLocationBytes[i]); + } + return getPathBuffer(); + } + + public void setDumpPath(String dumpPathText) { + pathBuffer = PrimitiveArrayView.createForReadingAndWriting(new byte[JVM_MAXPATHLEN]); + } + + public String getDumpPath() { + if (dumpPathBytes != null) { + return new String(dumpPathBytes,StandardCharsets.UTF_8); + } + return ""; + } + + /** See JfrEmergencyDump::on_vm_error*/ + public void onVmError(){ + if (openEmergencyDumpFile()) { + writeEmergencyDumpFile(); + getFileSupport().close(emergencyFd); + emergencyFd = Word.nullPointer(); + } + } + + /** See open_emergency_dump_file */ + private boolean openEmergencyDumpFile(){ + if (getFileSupport().isValid(emergencyFd)){ + return true; + } + emergencyFd = getFileSupport().create(createEmergencyDumpPath(), FileCreationMode.CREATE, FileAccessMode.READ_WRITE); //gives us O_CREAT | O_RDWR for creation mode and S_IREAD | S_IWRITE permissions + if (!getFileSupport().isValid(emergencyFd)) { + // Fallback. Try to create it in the current directory. + dumpPathBytes = null; + emergencyFd = getFileSupport().create(createEmergencyDumpPath(), FileCreationMode.CREATE, FileAccessMode.READ_WRITE); + } + System.out.println("openEmergencyDumpFile pathBuffer: "+ CTypeConversion.toJavaString(getPathBuffer())); + return getFileSupport().isValid(emergencyFd); + } + + /** See create_emergency_dump_path */ + private CCharPointer createEmergencyDumpPath() { + int idx = 0; + + clearPathBuffer(); + + if(dumpPathBytes != null) { + for (int i = 0; i < dumpPathBytes.length; i++) { + getPathBuffer().write(idx++, dumpPathBytes[i]); + } + // Add delimiter + getPathBuffer().write(idx++, FILE_SEPARATOR); + } + + for (int i = 0; i < DUMP_FILE_PREFIX.length; i++) { + getPathBuffer().write(idx++, DUMP_FILE_PREFIX[i]); + } + + for (int i = 0; i < pidBytes.length; i++) { + getPathBuffer().write(idx++, pidBytes[i]); + } + + for (int i = 0; i < CHUNKFILE_EXTENSION_BYTES.length; i++) { + getPathBuffer().write(idx++, CHUNKFILE_EXTENSION_BYTES[i]); + } + + return getPathBuffer(); + } + + private void writeEmergencyDumpFile() { + if (openDirectorySecure()) { + if (directory.isNull()) { + return; + } + int blockSize = 1024 * 1024; + org.graalvm.word.Pointer copyBlock = com.oracle.svm.core.memory.NullableNativeMemory.malloc(blockSize, NmtCategory.JFR); + if (copyBlock.isNull()) { + // TODO trouble importing these internal methods +// Logger.log(LogTag.JFR_SYSTEM, LogLevel.ERROR, "Emergency dump failed. Could not allocate copy block."); + return; + } + + Dirent.dirent entry; + while ((entry = Dirent.readdir(directory)).isNonNull()) { + RawFileDescriptor chunkFd = filter(entry.d_name()); + if (getFileSupport().isValid(chunkFd)){ + String name = CTypeConversion.toJavaString(entry.d_name()); + System.out.println("Filter checks passed. Chunk file name: "+ name); + + // Read it's size + long chunkFileSize = getFileSupport().size(chunkFd); + long bytesRead = 0; + long bytesWritten = 0; + while (bytesRead < chunkFileSize){ + // Start at beginning + getFileSupport().seek(chunkFd, 0); // seems unneeded. idk why??? + // Read from chunk file to copy block + long readResult = getFileSupport().read(chunkFd, copyBlock, org.graalvm.word.WordFactory.unsigned(blockSize));// *** i think this already retries until fully read[no just retries until gets a sucessful read] + if (readResult < 0){ // -1 if read failed + System.out.println("Log ERROR. Read failed."); + break; + } + bytesRead += readResult; + assert bytesRead - bytesWritten <= blockSize; + // Write from copy block to dump file + if (!getFileSupport().write(emergencyFd, copyBlock, org.graalvm.word.WordFactory.unsigned(bytesRead - bytesWritten))) { // *** may iterate until fully writtem + System.out.println("Log ERROR. Write failed."); + break; + } + bytesWritten = bytesRead; + System.out.println("bytesWritten " + bytesWritten); + } + getFileSupport().close(chunkFd); + } + } + com.oracle.svm.core.memory.NullableNativeMemory.free(copyBlock); + } + } + + // *** copied from PosixPerfMemoryProvider + private boolean openDirectorySecure() { + int fd = restartableOpen(getRepositoryLocation(), O_RDONLY() | O_NOFOLLOW(), 0); + if (fd == -1) { + return false; + } + +// if (!isDirFdSecure(fd)) { //TODO do we need this? +// com.oracle.svm.core.posix.headers.Unistd.NoTransitions.close(fd); +// return null; +// } + + this.directory = Dirent.fdopendir(fd); + if (directory.isNull()) { + com.oracle.svm.core.posix.headers.Unistd.NoTransitions.close(fd); + return false; + } + return true; + } + + // *** copied from PosixPerfMemoryProvider + @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") + private static int restartableOpen(CCharPointer directory, int flags, int mode) { + int result; + do { + result = com.oracle.svm.core.posix.headers.Fcntl.NoTransitions.open(directory, flags, mode); + } while (result == -1 && LibC.errno() == com.oracle.svm.core.posix.headers.Errno.EINTR()); + + return result; + } + + /** See RepositoryIterator::filter */ + private RawFileDescriptor filter(CCharPointer fn){ + + // check filename extension + int filenameLength = (int) SubstrateUtil.strlen(fn).rawValue(); + if (filenameLength <= CHUNKFILE_EXTENSION_BYTES.length){ + return org.graalvm.word.WordFactory.nullPointer(); + } + + // Verify file extension + for (int i = 0; i < CHUNKFILE_EXTENSION_BYTES.length; i++) { + int idx1 = CHUNKFILE_EXTENSION_BYTES.length - i - 1; + int idx2 = filenameLength - i - 1; + //System.out.println("expected byte " + chunkfileExtensionBytes[idx1] + " actual byte " + ((org.graalvm.word.Pointer) fn).readByte(idx2)); + if (CHUNKFILE_EXTENSION_BYTES[idx1] != ((org.graalvm.word.Pointer) fn).readByte(idx2)) { + System.out.println("failed extension check"); + return org.graalvm.word.WordFactory.nullPointer(); + } + } + + String name = CTypeConversion.toJavaString(fullyQualified(fn)); + System.out.println("fully qualified: "+ name); + + // Verify if you can open it and receive a valid file descriptor + RawFileDescriptor chunkFd = getFileSupport().open(fullyQualified(fn) ,FileAccessMode.READ_WRITE); + if (!getFileSupport().isValid(chunkFd)) { + System.out.println("failed open check"); + return org.graalvm.word.WordFactory.nullPointer(); + } + + // Verify file size + long chunkFileSize = getFileSupport().size(chunkFd); + if (chunkFileSize < CHUNK_FILE_HEADER_SIZE) { + System.out.println("failed size check"); + return org.graalvm.word.WordFactory.nullPointer(); + } + + return chunkFd; + } + + /** Given a chunk file name, it returns the fully qualified filename. See RepositoryIterator::fully_qualified */ + private CCharPointer fullyQualified(CCharPointer fn){ + long fnLength = SubstrateUtil.strlen(fn).rawValue() ; + int idx = 0; + + clearPathBuffer(); + + // TODO HS uses _path_buffer_file_name_offset to avoid building this part of th path each time. + // Cached in RepositoryIterator::RepositoryIterator and used in fully_qualified + for (int i = 0; i < repositoryLocationBytes.length; i++) { + getPathBuffer().write(idx++, repositoryLocationBytes[i]); + } + + // Add delimiter + getPathBuffer().write(idx++, FILE_SEPARATOR); + + + for (int i = 0; i < fnLength; i++) { + getPathBuffer().write(idx++, fn.read(i)); + } + return getPathBuffer(); + } + + private CCharPointer getPathBuffer(){ + return pathBuffer.addressOfArrayElement(0); + } + + private void clearPathBuffer(){ + LibC.memset(getPathBuffer(), Word.signed(0), Word.unsigned(JVM_MAXPATHLEN)); + } + + @Fold + static RawFileOperationSupport getFileSupport() { + return RawFileOperationSupport.bigEndian(); + } + + public void teardown() { + // *** this must survive as long as we need the dump path c pointer. + Dirent.closedir(directory); + pathBuffer.close(); + } +} + +@com.oracle.svm.core.feature.AutomaticallyRegisteredFeature +class PosixJfrEmergencyDumpFeature extends com.oracle.svm.core.jfr.JfrEmergencyDumpFeature { + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + PosixJfrEmergencyDumpSupport support = new PosixJfrEmergencyDumpSupport(); + org.graalvm.nativeimage.ImageSingletons.add(com.oracle.svm.core.jfr.JfrEmergencyDumpSupport.class, support); + org.graalvm.nativeimage.ImageSingletons.add(PosixJfrEmergencyDumpSupport.class, support); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/OutOfMemoryUtil.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/OutOfMemoryUtil.java index 7536f4a2fed4..9891620da34b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/OutOfMemoryUtil.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/OutOfMemoryUtil.java @@ -62,7 +62,7 @@ public static OutOfMemoryError reportOutOfMemoryError(OutOfMemoryError error) { @Uninterruptible(reason = "Not uninterruptible but it doesn't matter for the callers.", calleeMustBe = false) private static void reportOutOfMemoryError0(OutOfMemoryError error) { - if (VMInspectionOptions.hasHeapDumpSupport() && SubstrateOptions.HeapDumpOnOutOfMemoryError.getValue()) { + if (VMInspectionOptions.hasHeapDumpSupport() && SubstrateOptions.HeapDumpOnOutOfMemoryError.getValue()) { // TODO add jfr emergency dump here HeapDumping.singleton().dumpHeapOnOutOfMemoryError(); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java index 1a6cfd0f021f..2fb526866dc8 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java @@ -29,6 +29,10 @@ import java.nio.charset.StandardCharsets; +import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository; +import com.oracle.svm.core.nmt.NmtCategory; +import jdk.graal.compiler.word.Word; + import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; @@ -197,7 +201,7 @@ public void write(JfrBuffer buffer) { } } -// @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") @Override public void flush() { assert lock.isOwner(); @@ -247,6 +251,25 @@ public void closeFile() { fd = Word.nullPointer(); } + /** Similar to a regular chunk rotation but we do not safepoint, start a new epoch, or re-register threads. + * Similar to a flushpoint but we close the file and also process sampler buffers.*/ + @Override + public void closeFileForEmergencyDump() { + assert lock.isOwner(); + + processSamplerBuffers(); + flushStorage(true); + + writeThreadCheckpoint(true); + writeFlushCheckpoint(true); + writeMetadataEvent(); + patchFileHeader(true); + + getFileSupport().close(fd); + filename = null; + fd = Word.nullPointer(); + } + private void writeFileHeader() { /* Write the header - some data gets patched later on. */ getFileSupport().write(fd, FILE_MAGIC); @@ -533,12 +556,14 @@ public void writeString(String str) { getFileSupport().writeByte(fd, StringEncoding.UTF8_BYTE_ARRAY.getValue()); int length = UninterruptibleUtils.String.modifiedUTF8Length(str, false); + Pointer buffer = com.oracle.svm.core.memory.NullableNativeMemory.malloc(length, NmtCategory.JFR); + if (buffer.isNull()){ + return; + } writeCompressedInt(length); - int bufferSize = 512; //must be a compile time constant. Or use malloc. - Pointer buffer = UnsafeStackValue.get(bufferSize); - Pointer bufferEnd = buffer.add(bufferSize); - UninterruptibleUtils.String.toModifiedUTF8(str, buffer, bufferEnd, false); + UninterruptibleUtils.String.toModifiedUTF8(str, buffer, buffer.add(length), false); getFileSupport().write(fd, buffer, WordFactory.unsigned(length)); + com.oracle.svm.core.memory.NullableNativeMemory.free(buffer); } } @@ -671,33 +696,33 @@ private void changeEpoch() { // Now that the epoch changed, re-register all running threads for the new epoch. SubstrateJVM.getThreadRepo().registerRunningThreads(); } - - /** - * The VM is at a safepoint, so all other threads have a native state. However, execution - * sampling could still be executed. For the {@link JfrRecurringCallbackExecutionSampler}, - * it is sufficient to mark this method as uninterruptible to prevent execution of the - * recurring callbacks. If the SIGPROF-based sampler is used, the signal handler may still - * be executed at any time for any thread (including the current thread). To prevent races, - * we need to ensure that there are no threads that execute the SIGPROF handler while we are - * accessing the currently active buffers of other threads. - */ - @Uninterruptible(reason = "Prevent JFR recording.") - private static void processSamplerBuffers() { - assert VMOperation.isInProgressAtSafepoint(); - assert RecurringCallbackSupport.isCallbackUnsupportedOrTimerSuspended(); - - JfrExecutionSampler.singleton().disallowThreadsInSamplerCode(); - try { - processSamplerBuffers0(); - } finally { - JfrExecutionSampler.singleton().allowThreadsInSamplerCode(); - } + } + + /** + * The VM is at a safepoint, so all other threads have a native state. However, execution + * sampling could still be executed. For the {@link JfrRecurringCallbackExecutionSampler}, + * it is sufficient to mark this method as uninterruptible to prevent execution of the + * recurring callbacks. If the SIGPROF-based sampler is used, the signal handler may still + * be executed at any time for any thread (including the current thread). To prevent races, + * we need to ensure that there are no threads that execute the SIGPROF handler while we are + * accessing the currently active buffers of other threads. + */ + @Uninterruptible(reason = "Prevent JFR recording.") + private static void processSamplerBuffers() { + assert VMOperation.isInProgressAtSafepoint(); + assert RecurringCallbackSupport.isCallbackUnsupportedOrTimerSuspended(); + + JfrExecutionSampler.singleton().disallowThreadsInSamplerCode(); + try { + processSamplerBuffers0(); + } finally { + JfrExecutionSampler.singleton().allowThreadsInSamplerCode(); } + } - @Uninterruptible(reason = "Prevent JFR recording.") - private static void processSamplerBuffers0() { - SamplerBuffersAccess.processActiveBuffers(); - SamplerBuffersAccess.processFullBuffers(false); - } + @Uninterruptible(reason = "Prevent JFR recording.") + private static void processSamplerBuffers0() { + SamplerBuffersAccess.processActiveBuffers(); + SamplerBuffersAccess.processFullBuffers(false); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkNoWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkNoWriter.java index 89b0de50c3b4..68ec7f0362ba 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkNoWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkNoWriter.java @@ -115,6 +115,11 @@ public void closeFile() { VMError.shouldNotReachHere(ERROR_MESSAGE); } + @Override + public void closeFileForEmergencyDump() { + /* Nothing to do. */ + } + @Override public void setMetadata(byte[] bytes) { /* Nothing to do. */ diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java index 3412f99eac85..fa7da55ba6b0 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java @@ -43,6 +43,7 @@ public interface JfrChunkWriter extends JfrUnlockedChunkWriter { void markChunkFinal(); void closeFile(); + void closeFileForEmergencyDump(); void setMetadata(byte[] bytes); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java new file mode 100644 index 000000000000..50b337550b8b --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2025, 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; + +import com.oracle.svm.core.SigQuitFeature; +import com.oracle.svm.core.VMInspectionOptions; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.jdk.RuntimeSupport; +import com.oracle.svm.core.jdk.RuntimeSupportFeature; +import org.graalvm.nativeimage.hosted.Feature; + +import java.util.Collections; +import java.util.List; + +/** + * The JFR emergency dump mechanism uses platform-specific implementations (see {@link JfrEmergencyDumpSupport}). + */ +@AutomaticallyRegisteredFeature +public class JfrEmergencyDumpFeature implements InternalFeature { + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return VMInspectionOptions.hasJfrSupport(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java new file mode 100644 index 000000000000..2607ae60ef0e --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java @@ -0,0 +1,22 @@ +package com.oracle.svm.core.jfr; +import org.graalvm.nativeimage.ImageSingletons; + +import jdk.graal.compiler.api.replacements.Fold; +public interface JfrEmergencyDumpSupport { + @Fold + static boolean isPresent() { + return ImageSingletons.contains(JfrEmergencyDumpSupport.class); + } + + @Fold + static JfrEmergencyDumpSupport singleton() { + return ImageSingletons.lookup(JfrEmergencyDumpSupport.class); + } + + void initialize(); + void setRepositoryLocation(String dirText); + void setDumpPath(String dumpPathText); + String getDumpPath(); + void onVmError(); + void teardown(); +} 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 08ad4dfd9046..ed06660f6f53 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 @@ -24,6 +24,7 @@ */ package com.oracle.svm.core.jfr; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -154,7 +155,7 @@ private static HotSpotDiagnosticMXBean getDiagnosticBean() { @Override public List> getRequiredFeatures() { - return Collections.singletonList(ThreadListenerSupportFeature.class); + return Arrays.asList(ThreadListenerSupportFeature.class, com.oracle.svm.core.jfr.JfrEmergencyDumpFeature.class); } @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java index 6e28690d99b6..08cf5a177f9f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java @@ -111,6 +111,7 @@ private void work() { } void endRecording() { + com.oracle.svm.core.jfr.SubstrateJVM.get().vmErrorRotation();// TODO move to where it can handle an actual OOME lock.lock(); try { SubstrateJVM.JfrEndRecordingOperation vmOp = new SubstrateJVM.JfrEndRecordingOperation(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index 309592722641..703422f37351 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -516,7 +516,7 @@ private static int getHash(String imageHeapString) { } @RawStructure - public interface JfrTypeInfo extends UninterruptibleEntry { + private interface JfrTypeInfo extends UninterruptibleEntry { @PinnedObjectField @RawField void setName(String value); @@ -531,7 +531,7 @@ public interface JfrTypeInfo extends UninterruptibleEntry { @RawStructure - public interface ClassInfoRaw extends JfrTypeInfo { + private interface ClassInfoRaw extends JfrTypeInfo { @PinnedObjectField @RawField void setClassLoaderName(String value); @@ -561,7 +561,7 @@ public interface ClassInfoRaw extends JfrTypeInfo { } @RawStructure - public interface PackageInfoRaw extends JfrTypeInfo { + private interface PackageInfoRaw extends JfrTypeInfo { @PinnedObjectField @RawField void setModuleName(String value); @@ -583,7 +583,7 @@ public interface PackageInfoRaw extends JfrTypeInfo { } @RawStructure - public interface ModuleInfoRaw extends JfrTypeInfo { + private interface ModuleInfoRaw extends JfrTypeInfo { @PinnedObjectField @RawField void setClassLoaderName(String value); @@ -597,7 +597,7 @@ public interface ModuleInfoRaw extends JfrTypeInfo { } @RawStructure - public interface ClassLoaderInfoRaw extends JfrTypeInfo { + private interface ClassLoaderInfoRaw extends JfrTypeInfo { @RawField void setClassTraceId(long value); @RawField diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index 018447d510ae..1b835df806ac 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -34,6 +34,7 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.heap.Heap; +import com.oracle.svm.core.heap.RestrictHeapAccess; import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.jfr.events.JfrAllocationEvents; @@ -102,7 +103,6 @@ public class SubstrateJVM { * in). */ private volatile boolean recording; - private String dumpPath; @Platforms(Platform.HOSTED_ONLY.class) public SubstrateJVM(List configurations, boolean writeFile) { @@ -339,6 +339,7 @@ public void beginRecording() { // Cache all classes in preparation for TypeRepository TODO maybe there is a better way Heap.getHeap().visitLoadedClasses(clazz -> {}); + JfrEmergencyDumpSupport.singleton().initialize(); JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); try { @@ -585,24 +586,24 @@ public void markChunkFinal() { * See {@link JVM#setRepositoryLocation}. */ public void setRepositoryLocation(@SuppressWarnings("unused") String dirText) { - // Would only be used in case of an emergency dump, which is not supported at the moment. + JfrEmergencyDumpSupport.singleton().setRepositoryLocation(dirText); } /** * See {@code JfrEmergencyDump::set_dump_path}. */ public void setDumpPath(String dumpPathText) { - dumpPath = dumpPathText; + JfrEmergencyDumpSupport.singleton().setDumpPath(dumpPathText); } /** * See {@code JVM#getDumpPath()}. */ public String getDumpPath() { - if (dumpPath == null) { - dumpPath = Target_jdk_jfr_internal_util_Utils.getPathInProperty("user.home", null).toString(); + if (JfrEmergencyDumpSupport.singleton().getDumpPath() == null) { + JfrEmergencyDumpSupport.singleton().setDumpPath(Target_jdk_jfr_internal_util_Utils.getPathInProperty("user.home", null).toString()); } - return dumpPath; + return JfrEmergencyDumpSupport.singleton().getDumpPath(); } /** @@ -741,6 +742,22 @@ public Object getConfiguration(Class eventClass) { return DynamicHub.fromClass(eventClass).getJfrEventConfiguration(); } + /** See JfrRecorderService::vm_error_rotation */ +// @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") + public void vmErrorRotation() { + JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); + try { + boolean existingFile = chunkWriter.hasOpenFile(); + if (existingFile) { + chunkWriter.closeFileForEmergencyDump(); + } + JfrEmergencyDumpSupport.singleton().onVmError(); + } finally { + chunkWriter.unlock(); + } + + } + private static class JfrBeginRecordingOperation extends JavaVMOperation { JfrBeginRecordingOperation() { super(VMOperationInfos.get(JfrBeginRecordingOperation.class, "JFR begin recording", SystemEffect.SAFEPOINT)); @@ -833,6 +850,7 @@ protected void operate() { methodRepo.teardown(); typeRepo.teardown(); oldObjectRepo.teardown(); + JfrEmergencyDumpSupport.singleton().teardown(); initialized = false; } From be0cd0ad8ae9d2aae69cdce7dc4d86be2a7ed55c Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Wed, 30 Apr 2025 09:42:16 -0400 Subject: [PATCH 03/16] fix classloader issue --- .../svm/core/jfr/JfrTypeRepository.java | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index 703422f37351..bd8cb7d1ef69 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -148,8 +148,10 @@ private void visitModule(TypeInfo typeInfo, Class clazz) { private void visitClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { // The null class-loader is serialized as the "bootstrap" class-loader. - if (classLoader != null && addClassLoader(typeInfo, classLoader)) { - visitClass(typeInfo, classLoader.getClass()); + if (addClassLoader(typeInfo, classLoader)) { + if (classLoader != null) { + visitClass(typeInfo, classLoader.getClass()); + } } } @@ -281,13 +283,8 @@ private static int writeClassLoaders(JfrChunkWriter writer, TypeInfo typeInfo, b private static void writeClassLoader(JfrChunkWriter writer, ClassLoaderInfoRaw classLoaderInfoRaw, boolean flushpoint) { writer.writeCompressedLong(classLoaderInfoRaw.getId()); - if (classLoaderInfoRaw.getName() == null) { // TODO is this branch reachable now? I don't think it was reachable before my changes either. weird!! - writer.writeCompressedLong(0); - writer.writeCompressedLong(getSymbolId(writer, "bootstrap", flushpoint, false)); - } else { - writer.writeCompressedLong(classLoaderInfoRaw.getClassTraceId()); - writer.writeCompressedLong(getSymbolId(writer, classLoaderInfoRaw.getName(), flushpoint, false)); - } + writer.writeCompressedLong(classLoaderInfoRaw.getClassTraceId()); + writer.writeCompressedLong(getSymbolId(writer, classLoaderInfoRaw.getName(), flushpoint, false)); } private boolean addClass(TypeInfo typeInfo, Class clazz) { @@ -405,13 +402,24 @@ private long getModuleId(TypeInfo typeInfo, String moduleName, boolean hasModule private boolean addClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); - classLoaderInfoRaw.setName(classLoader.getName()); - classLoaderInfoRaw.setHash(getHash(classLoader.getName())); + if (classLoader == null) { + // Bootstrap loader has reserved ID of 0 (getClassLoaderId returns 0) + classLoaderInfoRaw.setName("bootstrap"); + classLoaderInfoRaw.setHash(0); + classLoaderInfoRaw.setId(0); + classLoaderInfoRaw.setClassTraceId(0); + } else { + classLoaderInfoRaw.setName(classLoader.getName()); + classLoaderInfoRaw.setHash(getHash(classLoader.getName())); + } + if (isClassLoaderVisited(typeInfo, classLoaderInfoRaw)) { return false; } - classLoaderInfoRaw.setId(++currentClassLoaderId); - classLoaderInfoRaw.setClassTraceId(JfrTraceId.getTraceId(classLoader.getClass())); + if (classLoader != null) { + classLoaderInfoRaw.setId(++currentClassLoaderId); + classLoaderInfoRaw.setClassTraceId(JfrTraceId.getTraceId(classLoader.getClass())); + } typeInfo.classLoaders.putNew(classLoaderInfoRaw); return true; } From b062580db17b86a8364a859302b713d568d5901f Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Tue, 6 May 2025 10:58:42 -0400 Subject: [PATCH 04/16] type repo refactor --- .../oracle/svm/core/genscavenge/HeapImpl.java | 1 + .../svm/core/jfr/JfrChunkFileWriter.java | 2 +- .../svm/core/jfr/JfrTypeRepository.java | 162 ++++++------------ .../com/oracle/svm/core/jfr/SubstrateJVM.java | 3 + .../poolparsers/ClassConstantPoolParser.java | 2 +- .../ClassLoaderConstantPoolParser.java | 4 +- .../utils/poolparsers/ConstantPoolParser.java | 2 +- 7 files changed, 66 insertions(+), 110 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java b/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java index 0e984906a193..ce0f0e7a1e5b 100644 --- a/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java +++ b/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java @@ -325,6 +325,7 @@ public int getClassCount() { @Override protected Class[] getAllClasses() { + System.out.println(" ----- caching classes!!!!!"); /* Two threads might race to set classList, but they compute the same result. */ if (getCachedClasses() == null) { ArrayList> list = findAllDynamicHubs(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java index 2fb526866dc8..9737815a4515 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java @@ -201,7 +201,7 @@ public void write(JfrBuffer buffer) { } } - @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") +// @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") @Override public void flush() { assert lock.isOwner(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index bd8cb7d1ef69..19f9f7ea0469 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -55,6 +55,7 @@ * Repository that collects and writes used classes, packages, modules, and classloaders. */ public class JfrTypeRepository implements JfrRepository { + private static final String BOOTSTRAP_NAME = "bootstrap"; private final JfrClassInfoTable flushedClasses = new JfrClassInfoTable(); // *** Ordinary objects, so can be in image heap. private final JfrPackageInfoTable flushedPackages = new JfrPackageInfoTable(); private final JfrModuleInfoTable flushedModules = new JfrModuleInfoTable(); @@ -177,13 +178,23 @@ private int writeClasses(JfrChunkWriter writer, TypeInfo typeInfo, boolean flush private void writeClass(TypeInfo typeInfo, JfrChunkWriter writer, ClassInfoRaw classInfoRaw, boolean flushpoint) { assert classInfoRaw.getHash() != 0; + Class clazz = classInfoRaw.getInstance(); + PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); + setPackageNameAndLength(clazz, packageInfoRaw); + packageInfoRaw.setHash(getHash("test")); + boolean hasClassLoader = clazz.getClassLoader() != null; writer.writeCompressedLong(classInfoRaw.getId()); - writer.writeCompressedLong(getClassLoaderId(typeInfo, classInfoRaw.getClassLoaderName(), classInfoRaw.getHasClassLoader())); - writer.writeCompressedLong(getSymbolId(writer, classInfoRaw.getName(), flushpoint, true)); - writer.writeCompressedLong(getPackageId(typeInfo, classInfoRaw.getModifiedUTF8PackageName(), classInfoRaw.getNameLength(), getHash(classInfoRaw.getName()))); - writer.writeCompressedLong(classInfoRaw.getModifiers()); - writer.writeBoolean(classInfoRaw.getIsHidden()); + writer.writeCompressedLong(getClassLoaderId(typeInfo, hasClassLoader ? clazz.getClassLoader().getName() : null, hasClassLoader)); + writer.writeCompressedLong(getSymbolId(writer, clazz.getName(), flushpoint, true)); + writer.writeCompressedLong(getPackageId(typeInfo, packageInfoRaw)); + writer.writeCompressedLong(clazz.getModifiers()); + writer.writeBoolean(clazz.isHidden()); + + // We no longer need the buffer. + if(packageInfoRaw.getModifiedUTF8Name().isNonNull()){ + NullableNativeMemory.free(packageInfoRaw.getModifiedUTF8Name()); + } } @Uninterruptible(reason = "Needed for JfrSymbolRepository.getSymbolId().") @@ -288,7 +299,6 @@ private static void writeClassLoader(JfrChunkWriter writer, ClassLoaderInfoRaw c } private boolean addClass(TypeInfo typeInfo, Class clazz) { - boolean hasPackage = false; ClassInfoRaw classInfoRaw = StackValue.get(ClassInfoRaw.class); classInfoRaw.setId(JfrTraceId.getTraceId(clazz)); classInfoRaw.setHash(getHash(clazz.getName())); @@ -298,24 +308,12 @@ private boolean addClass(TypeInfo typeInfo, Class clazz) { return false; } - // Class hasn't yet been visited so set the package info. Package name buffer is malloc'ed. - PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); - if (setPackageNameAndLength(clazz, packageInfoRaw)) { - hasPackage = true; - } - - classInfoRaw.setModifiedUTF8PackageName(hasPackage ? packageInfoRaw.getModifiedUTF8Name() : WordFactory.nullPointer()); - classInfoRaw.setNameLength(hasPackage ? packageInfoRaw.getNameLength() : WordFactory.unsigned(0)); classInfoRaw.setName(clazz.getName()); - classInfoRaw.setIsHidden(clazz.isHidden()); - classInfoRaw.setModifiers(clazz.getModifiers()); - // TODO the orig uses the class's CL but should we use the module's CL? Thats the only place we addClassloader - classInfoRaw.setHasClassLoader(clazz.getClassLoader() != null); - classInfoRaw.setClassLoaderName(classInfoRaw.getHasClassLoader() ? clazz.getClassLoader().getName() : null); // *** if you forget to set something accessing it will segfault. + classInfoRaw.setInstance(clazz); assert !typeInfo.classes.contains(classInfoRaw); - typeInfo.classes.putNew(classInfoRaw); // *** must be value on stack. Hopefully it shallow copies the malloc'ed buffer. It does since it just uses memcpy on the header + typeInfo.classes.putNew(classInfoRaw); assert typeInfo.classes.contains(classInfoRaw); - return hasPackage; + return true; } private boolean isClassVisited(TypeInfo typeInfo, ClassInfoRaw classInfoRaw) { @@ -323,28 +321,31 @@ private boolean isClassVisited(TypeInfo typeInfo, ClassInfoRaw classInfoRaw) { } private boolean addPackage(TypeInfo typeInfo, Class clazz) { - if (clazz.isPrimitive() || clazz.isArray()) { - return false; - } // *** if we've made it this far, we know the package is not null. Although the name may be empty boolean hasModule = clazz.getModule() != null; String moduleName = hasModule ? clazz.getModule().getName() : null; PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); setPackageNameAndLength(clazz, packageInfoRaw); - packageInfoRaw.setHash(getHash(clazz.getName())); + + /* The empty/null package represented by "" is always traced with id 0. + The id 0 is reserved and does not need to be serialized. */ + if (packageInfoRaw.getNameLength().equal(0)) { + return false; + } + + packageInfoRaw.setHash(getHash("test")); // TODO there must be a better way if (isPackageVisited(typeInfo, packageInfoRaw)) { assert moduleName == (flushedPackages.contains(packageInfoRaw) ? ((PackageInfoRaw)flushedPackages.get(packageInfoRaw)).getModuleName() : ((PackageInfoRaw)typeInfo.packages.get(packageInfoRaw)).getModuleName()); NullableNativeMemory.free(packageInfoRaw.getModifiedUTF8Name()); return false; } - // The empty package represented by "" is always traced with id 0 - long id = packageInfoRaw.getNameLength().belowOrEqual(0) ? 0 : ++currentPackageId; - packageInfoRaw.setId(id); + + packageInfoRaw.setId(++currentPackageId); packageInfoRaw.setHasModule(hasModule); packageInfoRaw.setModuleName(moduleName); assert !typeInfo.packages.contains(packageInfoRaw); // *** remove later - typeInfo.packages.putNew(packageInfoRaw); + typeInfo.packages.putNew(packageInfoRaw); // *** Do not free the buffer. A pointer to it is shallow copied into the hash map. assert typeInfo.packages.contains(packageInfoRaw); return true; } @@ -353,17 +354,14 @@ private boolean isPackageVisited(TypeInfo typeInfo, PackageInfoRaw packageInfoRa return flushedPackages.contains(packageInfoRaw) || typeInfo.packages.contains(packageInfoRaw); } - private long getPackageId(TypeInfo typeInfo, PointerBase modifiedUTF8PackageName, UnsignedWord length, int hash) { - if (modifiedUTF8PackageName.isNonNull() && length.aboveOrEqual(1)) { - PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); - packageInfoRaw.setModifiedUTF8Name(modifiedUTF8PackageName); - packageInfoRaw.setNameLength(length); - packageInfoRaw.setHash(hash); // Using the associated class' name for the hash + private long getPackageId(TypeInfo typeInfo, PackageInfoRaw packageInfoRaw) { + if (packageInfoRaw.getModifiedUTF8Name().isNonNull() && packageInfoRaw.getNameLength().aboveOrEqual(1)) { if (flushedPackages.contains(packageInfoRaw)) { return ((PackageInfoRaw) flushedPackages.get(packageInfoRaw)).getId(); } return ((PackageInfoRaw) typeInfo.packages.get(packageInfoRaw)).getId(); } else { + // Empty package has reserved ID 0 return 0; } } @@ -403,15 +401,11 @@ private long getModuleId(TypeInfo typeInfo, String moduleName, boolean hasModule private boolean addClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); if (classLoader == null) { - // Bootstrap loader has reserved ID of 0 (getClassLoaderId returns 0) - classLoaderInfoRaw.setName("bootstrap"); - classLoaderInfoRaw.setHash(0); - classLoaderInfoRaw.setId(0); - classLoaderInfoRaw.setClassTraceId(0); + classLoaderInfoRaw.setName(BOOTSTRAP_NAME); } else { classLoaderInfoRaw.setName(classLoader.getName()); - classLoaderInfoRaw.setHash(getHash(classLoader.getName())); } + classLoaderInfoRaw.setHash(getHash(classLoaderInfoRaw.getName())); if (isClassLoaderVisited(typeInfo, classLoaderInfoRaw)) { return false; @@ -419,6 +413,10 @@ private boolean addClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { if (classLoader != null) { classLoaderInfoRaw.setId(++currentClassLoaderId); classLoaderInfoRaw.setClassTraceId(JfrTraceId.getTraceId(classLoader.getClass())); + } else { + // Bootstrap loader has reserved ID of 0 + classLoaderInfoRaw.setId(0); + classLoaderInfoRaw.setClassTraceId(0); } typeInfo.classLoaders.putNew(classLoaderInfoRaw); return true; @@ -438,6 +436,7 @@ private long getClassLoaderId(TypeInfo typeInfo, String classLoaderName, boolean } return ((ClassLoaderInfoRaw) typeInfo.classLoaders.get(classLoaderInfoRaw)).getId(); } + // Bootstrap classloader return 0; } @@ -474,11 +473,7 @@ void teardown() { // *** we shouldn't preemtively compute ALL package names, only the ones used in JFR events. This current approach is ok, but since we don't stash, we must recompute every time. // *** Maybe its not a big deal since we call getPackage() on every class we visit anyway. And we only do this for classes that are in an event. /** This method sets the package name and length. packageInfoRaw may be on the stack or native memory.*/ - private boolean setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoRaw) { - if (clazz.isPrimitive() || clazz.isArray()) { - return false; - } - + private void setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoRaw) { DynamicHub hub = DynamicHub.fromClass(clazz); if (!LayoutEncoding.isHybrid(hub.getLayoutEncoding())) { while (hub.hubIsArray()) { @@ -486,19 +481,29 @@ private boolean setPackageNameAndLength(Class clazz, PackageInfoRaw packageIn } } + /* Primitives have the null package, which technically has the name "java.lang", + but JFR still assigns these the reserved 0 id. */ + if (hub.isPrimitive()) { + packageInfoRaw.setModifiedUTF8Name(WordFactory.nullPointer()); + packageInfoRaw.setNameLength(WordFactory.unsigned(0)); + return; + } + String str = hub.getName(); int dot = str.lastIndexOf('.'); if (dot == -1) { dot = 0; } - int utf8Length = UninterruptibleUtils.String.modifiedUTF8Length(str, false); + int utf8Length = UninterruptibleUtils.String.modifiedUTF8Length(str, false, dotWithSlash); Pointer buffer = NullableNativeMemory.malloc(utf8Length, NmtCategory.JFR); Pointer bufferEnd = buffer.add(utf8Length); - // If malloc fails, we'll just set a blank package name. + // If malloc fails, set a blank package name. if (buffer.isNull()) { - return false; + packageInfoRaw.setModifiedUTF8Name(WordFactory.nullPointer()); + packageInfoRaw.setNameLength(WordFactory.unsigned(0)); + return; } assert buffer.add(dot).belowOrEqual(bufferEnd); @@ -511,7 +516,6 @@ private boolean setPackageNameAndLength(Class clazz, PackageInfoRaw packageIn if (dot == 0) { assert packageNameLength.equal(0); } - return true; } private static int getHash(String imageHeapString) { @@ -542,30 +546,10 @@ private interface JfrTypeInfo extends UninterruptibleEntry { private interface ClassInfoRaw extends JfrTypeInfo { @PinnedObjectField @RawField - void setClassLoaderName(String value); + void setInstance(Class value); @PinnedObjectField @RawField - String getClassLoaderName(); - @RawField - void setHasClassLoader(boolean value); - @RawField - boolean getHasClassLoader(); - @RawField - void setNameLength(UnsignedWord value); - @RawField - UnsignedWord getNameLength(); - @RawField - void setModifiedUTF8PackageName(PointerBase value); - @RawField - PointerBase getModifiedUTF8PackageName(); - @RawField - void setModifiers(long value); - @RawField - long getModifiers(); - @RawField - void setIsHidden(boolean value); - @RawField - boolean getIsHidden(); + Class getInstance(); } @RawStructure @@ -599,7 +583,7 @@ private interface ModuleInfoRaw extends JfrTypeInfo { @RawField String getClassLoaderName(); @RawField - void setHasClassLoader(boolean value); + void setHasClassLoader(boolean value); // *** name may be empty or null, but CL may be non null @RawField boolean getHasClassLoader(); } @@ -660,38 +644,6 @@ protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { protected UninterruptibleEntry copyToHeap(UninterruptibleEntry visitedOnStack) { return copyToHeap(visitedOnStack, SizeOf.unsigned(ClassInfoRaw.class)); } - - @Override - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - protected void free(UninterruptibleEntry entry) { - ClassInfoRaw classInfoRaw = (ClassInfoRaw) entry; - /* The base method will free only the entry itself, not th utf8 data. */ - NullableNativeMemory.free(classInfoRaw.getModifiedUTF8PackageName()); - classInfoRaw.setModifiedUTF8PackageName(WordFactory.nullPointer()); - super.free(entry); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public void putAll(JfrClassInfoTable sourceTable) { - for (int i = 0; i < sourceTable.getTable().length; i++) { - ClassInfoRaw sourceInfo = (ClassInfoRaw) sourceTable.getTable()[i]; - while (sourceInfo.isNonNull()) { - if (!contains(sourceInfo)) { - // Put if not already there. - ClassInfoRaw destinationInfo = (ClassInfoRaw) putNew(sourceInfo); // *** does a shallow copy... BAD it can overwrites ptrs that were malloc'ed!!! ... but there shouldnt be overlap anyway... - // allocate a new buffer - PointerBase newUtf8Name = NullableNativeMemory.malloc(sourceInfo.getNameLength(), NmtCategory.JFR); - // set the buffer ptr - destinationInfo.setModifiedUTF8PackageName(newUtf8Name); - // Copy source buffer contents over to new buffer - if (newUtf8Name.isNonNull()) { - UnmanagedMemoryUtil.copy((Pointer) sourceInfo.getModifiedUTF8PackageName(), (Pointer) newUtf8Name, sourceInfo.getNameLength()); - } - } - sourceInfo = sourceInfo.getNext(); - } - } - } } private final class JfrPackageInfoTable extends JfrTypeInfoTable { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index 1b835df806ac..03f17476812e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -24,6 +24,7 @@ */ package com.oracle.svm.core.jfr; +import java.util.Arrays; import java.util.List; import org.graalvm.nativeimage.ImageSingletons; @@ -339,6 +340,8 @@ public void beginRecording() { // Cache all classes in preparation for TypeRepository TODO maybe there is a better way Heap.getHeap().visitLoadedClasses(clazz -> {}); + System.out.println("Cached classes count: " + Heap.getHeap().getCachedClasses().length); + System.out.println("Cached classes count: " + Arrays.stream(Heap.getHeap().getCachedClasses()).count()); JfrEmergencyDumpSupport.singleton().initialize(); JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ClassConstantPoolParser.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ClassConstantPoolParser.java index daeb20e4064c..605d775668a0 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ClassConstantPoolParser.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ClassConstantPoolParser.java @@ -36,7 +36,7 @@ public class ClassConstantPoolParser extends AbstractRepositoryParser { public ClassConstantPoolParser(JfrFileParser parser) { - super(parser); + super(parser, 0L); } @Override diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ClassLoaderConstantPoolParser.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ClassLoaderConstantPoolParser.java index 0ae4f2d5c35c..ba7fa40c8a77 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ClassLoaderConstantPoolParser.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ClassLoaderConstantPoolParser.java @@ -34,8 +34,8 @@ public class ClassLoaderConstantPoolParser extends AbstractRepositoryParser { public ClassLoaderConstantPoolParser(JfrFileParser parser) { - /* 0 is the null class loader. */ - super(parser, 0L); + /* 0 is the null class loader, but still should be serialized every epoch if tagged. */ + super(parser); } @Override diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ConstantPoolParser.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ConstantPoolParser.java index f05314deadae..4395966ead01 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ConstantPoolParser.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ConstantPoolParser.java @@ -71,7 +71,7 @@ public void compareFoundAndExpectedIds() { missingIds.removeAll(foundIds); if (!missingIds.isEmpty()) { - Assert.fail("Error during parsing " + this + " constant pool! Missing IDs: " + missingIds + ". Expected IDs: " + expectedIds + ". Found IDs: " + foundIds); + Assert.fail("Error during parsing " + this.getClass().getName() + " constant pool! Missing IDs: " + missingIds + ". Expected IDs: " + expectedIds + ". Found IDs: " + foundIds); } } From 20065217d4432678b8f801e3422a24b2f437de27 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Tue, 6 May 2025 13:20:14 -0400 Subject: [PATCH 05/16] improve hashing for packages --- .../jfr/PosixJfrEmergencyDumpSupport.java | 2 +- .../svm/core/jfr/JfrTypeRepository.java | 20 ++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java index 00b69039d12a..ec9c15571df8 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -63,7 +63,7 @@ private CCharPointer getRepositoryLocation() { } public void setDumpPath(String dumpPathText) { - pathBuffer = PrimitiveArrayView.createForReadingAndWriting(new byte[JVM_MAXPATHLEN]); + dumpPathBytes = dumpPathText.getBytes(StandardCharsets.UTF_8); } public String getDumpPath() { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index 19f9f7ea0469..0e3009d65976 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -181,7 +181,7 @@ private void writeClass(TypeInfo typeInfo, JfrChunkWriter writer, ClassInfoRaw c Class clazz = classInfoRaw.getInstance(); PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); setPackageNameAndLength(clazz, packageInfoRaw); - packageInfoRaw.setHash(getHash("test")); + packageInfoRaw.setHash(getHash(packageInfoRaw)); boolean hasClassLoader = clazz.getClassLoader() != null; writer.writeCompressedLong(classInfoRaw.getId()); @@ -326,6 +326,7 @@ private boolean addPackage(TypeInfo typeInfo, Class clazz) { String moduleName = hasModule ? clazz.getModule().getName() : null; PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); + packageInfoRaw.setName(null); // No allocation free way to get the name String. setPackageNameAndLength(clazz, packageInfoRaw); /* The empty/null package represented by "" is always traced with id 0. @@ -333,8 +334,8 @@ private boolean addPackage(TypeInfo typeInfo, Class clazz) { if (packageInfoRaw.getNameLength().equal(0)) { return false; } - - packageInfoRaw.setHash(getHash("test")); // TODO there must be a better way + System.out.println(clazz.getPackageName()); + packageInfoRaw.setHash(getHash(packageInfoRaw)); if (isPackageVisited(typeInfo, packageInfoRaw)) { assert moduleName == (flushedPackages.contains(packageInfoRaw) ? ((PackageInfoRaw)flushedPackages.get(packageInfoRaw)).getModuleName() : ((PackageInfoRaw)typeInfo.packages.get(packageInfoRaw)).getModuleName()); NullableNativeMemory.free(packageInfoRaw.getModifiedUTF8Name()); @@ -527,6 +528,19 @@ private static int getHash(String imageHeapString) { return UninterruptibleUtils.Long.hashCode(rawPointerValue); } + /** We do not have access to an image heap String for packages. + * Instead, sum the value of the serialized String and use that for the hash.*/ + private static int getHash(PackageInfoRaw packageInfoRaw) { + if (packageInfoRaw.getModifiedUTF8Name().isNull() || packageInfoRaw.getNameLength().belowOrEqual(0)) { + return 0; + } + long sum = 0; + for (int i = 0; packageInfoRaw.getNameLength().aboveThan(i); i++){ + sum += ((Pointer) packageInfoRaw.getModifiedUTF8Name()).readByte(i); + } + return UninterruptibleUtils.Long.hashCode(sum); + } + @RawStructure private interface JfrTypeInfo extends UninterruptibleEntry { @PinnedObjectField From 7e79ee6dcbdf9318a4216fb130db6bb7eb497acc Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Wed, 4 Jun 2025 16:11:45 -0400 Subject: [PATCH 06/16] make pathBuffer allocation free. Clean up --- .../jfr/PosixJfrEmergencyDumpSupport.java | 119 +++++++++++------- .../svm/core/jfr/JfrTypeRepository.java | 1 - .../com/oracle/svm/core/jfr/SubstrateJVM.java | 2 +- 3 files changed, 77 insertions(+), 45 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java index ec9c15571df8..502f85c20c6a 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -1,31 +1,65 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2025, 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.posix.jfr; + import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.headers.LibC; import com.oracle.svm.core.nmt.NmtCategory; import com.oracle.svm.core.posix.headers.Dirent; +import com.oracle.svm.core.posix.headers.Errno; +import com.oracle.svm.core.posix.headers.Fcntl; +import com.oracle.svm.core.posix.headers.Unistd; +import org.graalvm.word.Pointer; import jdk.graal.compiler.word.Word; +import org.graalvm.word.WordFactory; //import jdk.jfr.internal.LogLevel; //import jdk.jfr.internal.LogTag; //import jdk.jfr.internal.Logger; import org.graalvm.nativeimage.c.type.CTypeConversion; import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.Uninterruptible; -import com.oracle.svm.core.handles.PrimitiveArrayView; +import com.oracle.svm.core.memory.NativeMemory; +import com.oracle.svm.core.memory.NullableNativeMemory; import com.oracle.svm.core.os.RawFileOperationSupport; import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; - import jdk.graal.compiler.api.replacements.Fold; import java.nio.charset.StandardCharsets; import static com.oracle.svm.core.posix.headers.Fcntl.O_NOFOLLOW; import static com.oracle.svm.core.posix.headers.Fcntl.O_RDONLY; + public class PosixJfrEmergencyDumpSupport implements com.oracle.svm.core.jfr.JfrEmergencyDumpSupport { private static final int CHUNK_FILE_HEADER_SIZE = 68;// TODO based on jdk file private static final int JVM_MAXPATHLEN = 4096;// TODO based on jdk file @@ -37,7 +71,7 @@ public class PosixJfrEmergencyDumpSupport implements com.oracle.svm.core.jfr.Jfr private byte[] dumpPathBytes; private byte[] repositoryLocationBytes; private RawFileDescriptor emergencyFd; - private PrimitiveArrayView pathBuffer; + private CCharPointer pathBuffer; @Platforms(Platform.HOSTED_ONLY.class) public PosixJfrEmergencyDumpSupport() { @@ -45,8 +79,8 @@ public PosixJfrEmergencyDumpSupport() { public void initialize() { pidBytes = String.valueOf(ProcessHandle.current().pid()).getBytes(StandardCharsets.UTF_8); - pathBuffer = PrimitiveArrayView.createForReadingAndWriting(new byte[JVM_MAXPATHLEN]); - directory = org.graalvm.word.WordFactory.nullPointer(); // *** maybe not necessary. + pathBuffer = NativeMemory.calloc(JVM_MAXPATHLEN, NmtCategory.JFR); + directory = WordFactory.nullPointer(); // *** maybe not necessary. //TODO need to terminate the string with 0. otherwise length function will not work. } @@ -54,14 +88,6 @@ public void setRepositoryLocation(String dirText) { repositoryLocationBytes = dirText.getBytes(StandardCharsets.UTF_8); } - private CCharPointer getRepositoryLocation() { - clearPathBuffer(); - for (int i = 0; i < repositoryLocationBytes.length; i++) { - getPathBuffer().write(i, repositoryLocationBytes[i]); - } - return getPathBuffer(); - } - public void setDumpPath(String dumpPathText) { dumpPathBytes = dumpPathText.getBytes(StandardCharsets.UTF_8); } @@ -78,7 +104,7 @@ public void onVmError(){ if (openEmergencyDumpFile()) { writeEmergencyDumpFile(); getFileSupport().close(emergencyFd); - emergencyFd = Word.nullPointer(); + emergencyFd = WordFactory.nullPointer(); } } @@ -93,7 +119,7 @@ private boolean openEmergencyDumpFile(){ dumpPathBytes = null; emergencyFd = getFileSupport().create(createEmergencyDumpPath(), FileCreationMode.CREATE, FileAccessMode.READ_WRITE); } - System.out.println("openEmergencyDumpFile pathBuffer: "+ CTypeConversion.toJavaString(getPathBuffer())); +// System.out.println("openEmergencyDumpFile pathBuffer: "+ CTypeConversion.toJavaString(getPathBuffer())); return getFileSupport().isValid(emergencyFd); } @@ -132,7 +158,7 @@ private void writeEmergencyDumpFile() { return; } int blockSize = 1024 * 1024; - org.graalvm.word.Pointer copyBlock = com.oracle.svm.core.memory.NullableNativeMemory.malloc(blockSize, NmtCategory.JFR); + Pointer copyBlock = NullableNativeMemory.malloc(blockSize, NmtCategory.JFR); if (copyBlock.isNull()) { // TODO trouble importing these internal methods // Logger.log(LogTag.JFR_SYSTEM, LogLevel.ERROR, "Emergency dump failed. Could not allocate copy block."); @@ -143,8 +169,8 @@ private void writeEmergencyDumpFile() { while ((entry = Dirent.readdir(directory)).isNonNull()) { RawFileDescriptor chunkFd = filter(entry.d_name()); if (getFileSupport().isValid(chunkFd)){ - String name = CTypeConversion.toJavaString(entry.d_name()); - System.out.println("Filter checks passed. Chunk file name: "+ name); +// String name = CTypeConversion.toJavaString(entry.d_name()); +// System.out.println("Filter checks passed. Chunk file name: "+ name); // Read it's size long chunkFileSize = getFileSupport().size(chunkFd); @@ -154,25 +180,25 @@ private void writeEmergencyDumpFile() { // Start at beginning getFileSupport().seek(chunkFd, 0); // seems unneeded. idk why??? // Read from chunk file to copy block - long readResult = getFileSupport().read(chunkFd, copyBlock, org.graalvm.word.WordFactory.unsigned(blockSize));// *** i think this already retries until fully read[no just retries until gets a sucessful read] + long readResult = getFileSupport().read(chunkFd, copyBlock, WordFactory.unsigned(blockSize));// *** i think this already retries until fully read[no just retries until gets a sucessful read] if (readResult < 0){ // -1 if read failed - System.out.println("Log ERROR. Read failed."); +// System.out.println("Log ERROR. Read failed."); break; } bytesRead += readResult; assert bytesRead - bytesWritten <= blockSize; // Write from copy block to dump file - if (!getFileSupport().write(emergencyFd, copyBlock, org.graalvm.word.WordFactory.unsigned(bytesRead - bytesWritten))) { // *** may iterate until fully writtem - System.out.println("Log ERROR. Write failed."); + if (!getFileSupport().write(emergencyFd, copyBlock, WordFactory.unsigned(bytesRead - bytesWritten))) { // *** may iterate until fully writtem +// System.out.println("Log ERROR. Write failed."); break; } bytesWritten = bytesRead; - System.out.println("bytesWritten " + bytesWritten); +// System.out.println("bytesWritten " + bytesWritten); } getFileSupport().close(chunkFd); } } - com.oracle.svm.core.memory.NullableNativeMemory.free(copyBlock); + NullableNativeMemory.free(copyBlock); } } @@ -190,19 +216,27 @@ private boolean openDirectorySecure() { this.directory = Dirent.fdopendir(fd); if (directory.isNull()) { - com.oracle.svm.core.posix.headers.Unistd.NoTransitions.close(fd); + Unistd.NoTransitions.close(fd); return false; } return true; } + private CCharPointer getRepositoryLocation() { + clearPathBuffer(); + for (int i = 0; i < repositoryLocationBytes.length; i++) { + getPathBuffer().write(i, repositoryLocationBytes[i]); + } + return getPathBuffer(); + } + // *** copied from PosixPerfMemoryProvider @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") private static int restartableOpen(CCharPointer directory, int flags, int mode) { int result; do { - result = com.oracle.svm.core.posix.headers.Fcntl.NoTransitions.open(directory, flags, mode); - } while (result == -1 && LibC.errno() == com.oracle.svm.core.posix.headers.Errno.EINTR()); + result = Fcntl.NoTransitions.open(directory, flags, mode); + } while (result == -1 && LibC.errno() == Errno.EINTR()); return result; } @@ -213,35 +247,34 @@ private RawFileDescriptor filter(CCharPointer fn){ // check filename extension int filenameLength = (int) SubstrateUtil.strlen(fn).rawValue(); if (filenameLength <= CHUNKFILE_EXTENSION_BYTES.length){ - return org.graalvm.word.WordFactory.nullPointer(); + return WordFactory.nullPointer(); } // Verify file extension for (int i = 0; i < CHUNKFILE_EXTENSION_BYTES.length; i++) { int idx1 = CHUNKFILE_EXTENSION_BYTES.length - i - 1; int idx2 = filenameLength - i - 1; - //System.out.println("expected byte " + chunkfileExtensionBytes[idx1] + " actual byte " + ((org.graalvm.word.Pointer) fn).readByte(idx2)); - if (CHUNKFILE_EXTENSION_BYTES[idx1] != ((org.graalvm.word.Pointer) fn).readByte(idx2)) { - System.out.println("failed extension check"); - return org.graalvm.word.WordFactory.nullPointer(); + if (CHUNKFILE_EXTENSION_BYTES[idx1] != ((Pointer) fn).readByte(idx2)) { +// System.out.println("failed extension check"); + return WordFactory.nullPointer(); } } - String name = CTypeConversion.toJavaString(fullyQualified(fn)); - System.out.println("fully qualified: "+ name); +// String name = CTypeConversion.toJavaString(fullyQualified(fn)); +// System.out.println("fully qualified: "+ name); // Verify if you can open it and receive a valid file descriptor - RawFileDescriptor chunkFd = getFileSupport().open(fullyQualified(fn) ,FileAccessMode.READ_WRITE); + RawFileDescriptor chunkFd = getFileSupport().open(fullyQualified(fn), FileAccessMode.READ_WRITE); if (!getFileSupport().isValid(chunkFd)) { - System.out.println("failed open check"); - return org.graalvm.word.WordFactory.nullPointer(); +// System.out.println("failed open check"); + return WordFactory.nullPointer(); } // Verify file size long chunkFileSize = getFileSupport().size(chunkFd); if (chunkFileSize < CHUNK_FILE_HEADER_SIZE) { - System.out.println("failed size check"); - return org.graalvm.word.WordFactory.nullPointer(); +// System.out.println("failed size check"); + return WordFactory.nullPointer(); } return chunkFd; @@ -271,7 +304,7 @@ private CCharPointer fullyQualified(CCharPointer fn){ } private CCharPointer getPathBuffer(){ - return pathBuffer.addressOfArrayElement(0); + return pathBuffer; } private void clearPathBuffer(){ @@ -286,7 +319,7 @@ static RawFileOperationSupport getFileSupport() { public void teardown() { // *** this must survive as long as we need the dump path c pointer. Dirent.closedir(directory); - pathBuffer.close(); + NativeMemory.free(pathBuffer); } } @@ -296,7 +329,7 @@ class PosixJfrEmergencyDumpFeature extends com.oracle.svm.core.jfr.JfrEmergencyD @Override public void afterRegistration(AfterRegistrationAccess access) { PosixJfrEmergencyDumpSupport support = new PosixJfrEmergencyDumpSupport(); - org.graalvm.nativeimage.ImageSingletons.add(com.oracle.svm.core.jfr.JfrEmergencyDumpSupport.class, support); - org.graalvm.nativeimage.ImageSingletons.add(PosixJfrEmergencyDumpSupport.class, support); + ImageSingletons.add(com.oracle.svm.core.jfr.JfrEmergencyDumpSupport.class, support); + ImageSingletons.add(PosixJfrEmergencyDumpSupport.class, support); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index 0e3009d65976..d8c08e4a8d76 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -334,7 +334,6 @@ private boolean addPackage(TypeInfo typeInfo, Class clazz) { if (packageInfoRaw.getNameLength().equal(0)) { return false; } - System.out.println(clazz.getPackageName()); packageInfoRaw.setHash(getHash(packageInfoRaw)); if (isPackageVisited(typeInfo, packageInfoRaw)) { assert moduleName == (flushedPackages.contains(packageInfoRaw) ? ((PackageInfoRaw)flushedPackages.get(packageInfoRaw)).getModuleName() : ((PackageInfoRaw)typeInfo.packages.get(packageInfoRaw)).getModuleName()); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index 03f17476812e..9bf1dcc5f11d 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -746,7 +746,7 @@ public Object getConfiguration(Class eventClass) { } /** See JfrRecorderService::vm_error_rotation */ -// @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void vmErrorRotation() { JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); try { From bfe91e7140d5a97ddbb2a6e77759c982cf52bee6 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Thu, 5 Jun 2025 16:16:26 -0400 Subject: [PATCH 07/16] JfrTypeRepository record classes upon event emission. Remove preallocated class list. --- .../oracle/svm/core/genscavenge/HeapImpl.java | 20 +---- .../src/com/oracle/svm/core/heap/Heap.java | 11 +-- .../svm/core/jfr/JfrTypeRepository.java | 85 ++++++++++++++----- .../com/oracle/svm/core/jfr/SubstrateJVM.java | 4 - 4 files changed, 72 insertions(+), 48 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java b/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java index ce0f0e7a1e5b..987fc9e56321 100644 --- a/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java +++ b/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/HeapImpl.java @@ -121,7 +121,7 @@ public final class HeapImpl extends Heap { private volatile long refListWaiterWakeUpCounter; /** A cached list of all the classes, if someone asks for it. */ - private Class[] classList; + private List> classList; @Platforms(Platform.HOSTED_ONLY.class) public HeapImpl() { @@ -324,29 +324,17 @@ public int getClassCount() { } @Override - protected Class[] getAllClasses() { - System.out.println(" ----- caching classes!!!!!"); + protected List> getAllClasses() { /* Two threads might race to set classList, but they compute the same result. */ - if (getCachedClasses() == null) { + if (classList == null) { ArrayList> list = findAllDynamicHubs(); /* Ensure that other threads see consistent values once the list is published. */ MembarNode.memoryBarrier(MembarNode.FenceKind.STORE_STORE); - setCachedClasses(list); + classList = list; } - return getCachedClasses(); - } - - @Override - public Class[] getCachedClasses() { return classList; } - - private void setCachedClasses(List> list) { - classList = new Class[list.size()]; - list.toArray(classList); - } - private ArrayList> findAllDynamicHubs() { int dynamicHubCount = getClassCount(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/Heap.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/Heap.java index 693bbfbb01a8..b911f1919ad0 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/Heap.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/Heap.java @@ -112,10 +112,9 @@ protected Heap() { /** Visits all loaded classes in the heap (see {@link PredefinedClassesSupport}). */ public void visitLoadedClasses(Consumer> visitor) { - Class[] classes = getAllClasses(); - for (int i = 0; i < classes.length; i++) { - if (DynamicHub.fromClass(classes[i]).isLoaded()) { - visitor.accept(classes[i]); + for (Class clazz : getAllClasses()) { + if (DynamicHub.fromClass(clazz).isLoaded()) { + visitor.accept(clazz); } } } @@ -124,9 +123,7 @@ public void visitLoadedClasses(Consumer> visitor) { * Get all known classes. Intentionally protected to prevent access to classes that have not * been "loaded" yet, see {@link PredefinedClassesSupport}. */ - protected abstract Class[] getAllClasses(); - - public abstract Class[] getCachedClasses(); + protected abstract List> getAllClasses(); /** * Get the ObjectHeader implementation that this Heap uses. diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index d8c08e4a8d76..9de043862db7 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -46,21 +46,29 @@ import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.hub.LayoutEncoding; import com.oracle.svm.core.Uninterruptible; -import com.oracle.svm.core.heap.Heap; import com.oracle.svm.core.jfr.traceid.JfrTraceId; +import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.nmt.NmtCategory; import com.oracle.svm.core.memory.NullableNativeMemory; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + /** * Repository that collects and writes used classes, packages, modules, and classloaders. */ public class JfrTypeRepository implements JfrRepository { private static final String BOOTSTRAP_NAME = "bootstrap"; - private final JfrClassInfoTable flushedClasses = new JfrClassInfoTable(); // *** Ordinary objects, so can be in image heap. - private final JfrPackageInfoTable flushedPackages = new JfrPackageInfoTable(); - private final JfrModuleInfoTable flushedModules = new JfrModuleInfoTable(); - private final JfrClassLoaderInfoTable flushedClassLoaders = new JfrClassLoaderInfoTable(); - private final TypeInfo typeInfo = new TypeInfo(); + // The following tables are only used by the flushing/rotating thread + private final JfrClassInfoTable flushedClasses; + private final JfrPackageInfoTable flushedPackages; + private final JfrModuleInfoTable flushedModules; + private final JfrClassLoaderInfoTable flushedClassLoaders; + private final TypeInfo typeInfo; + + // epochTypeData tables are written to from threads emitting events and read from the flushing/rotating thread + private final JfrClassInfoTable epochTypeData0; + private final JfrClassInfoTable epochTypeData1; + private final UninterruptibleUtils.CharReplacer dotWithSlash = new ReplaceDotWithSlash(); private long currentPackageId = 0; private long currentModuleId = 0; @@ -68,6 +76,13 @@ public class JfrTypeRepository implements JfrRepository { @Platforms(Platform.HOSTED_ONLY.class) public JfrTypeRepository() { + flushedClasses = new JfrClassInfoTable(); + flushedPackages = new JfrPackageInfoTable(); + flushedModules = new JfrModuleInfoTable(); + flushedClassLoaders = new JfrClassLoaderInfoTable(); + typeInfo = new TypeInfo(); + epochTypeData0 = new JfrClassInfoTable(); + epochTypeData1 = new JfrClassInfoTable(); } public void teardown() { @@ -75,8 +90,16 @@ public void teardown() { typeInfo.teardown(); } + @Uninterruptible(reason = "Result is only valid until epoch changes.") + private JfrClassInfoTable getEpochData(boolean previousEpoch) { + boolean epoch = previousEpoch ? JfrTraceIdEpoch.getInstance().previousEpoch() : JfrTraceIdEpoch.getInstance().currentEpoch(); + return epoch ? epochTypeData0 : epochTypeData1; + } + @Uninterruptible(reason = "Result is only valid until epoch changes.", callerMustBe = true) public long getClassId(Class clazz) { + addClass(clazz); + /* Tagging the traceID epoch bits is not required for this class to serialize types. But we must still do it to support JVM#getTypeId(Class)*/ return JfrTraceId.load(clazz); } @@ -99,31 +122,49 @@ public int write(JfrChunkWriter writer, boolean flushpoint) { return count; } + /** Called upon event emission. */ + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public void addClass(Class clazz) { + JfrClassInfoTable classInfoTable = getEpochData(false); + ClassInfoRaw classInfoRaw = StackValue.get(ClassInfoRaw.class); + classInfoRaw.setId(JfrTraceId.getTraceId(clazz)); + classInfoRaw.setHash(getHash(clazz.getName())); + classInfoRaw.setName(clazz.getName()); + classInfoRaw.setInstance(clazz); + classInfoTable.putIfAbsent(classInfoRaw); + } + /** * Visit all used classes, and collect their packages, modules, classloaders and possibly * referenced classes. + * + * This method does not need to be marked uninterruptible since the epoch cannot change while the chunkwriter + * lock is held. + * Unlike other JFR repositories, locking is not needed to protect a data buffer. Writes to the JfrClassInfoTable + * and the read here are allowed to race. There is no risk of separating events from constant data due to + * the write order (constants before events during emission, and events before constants during flush). */ private void collectTypeInfo(boolean flushpoint) { - Class[] classes = Heap.getHeap().getCachedClasses(); - if (classes == null) { - return; - } - for (int i = 0; i < classes.length; i++) { - Class clazz = classes[i]; - if (DynamicHub.fromClass(clazz).isLoaded()) { + JfrClassInfoTable classInfoTable = getEpochData(!flushpoint); + ClassInfoRaw[] table = (ClassInfoRaw[]) classInfoTable.getTable(); + + for (int i = 0; i < table.length; i++) { + ClassInfoRaw entry = table[i]; + while (entry.isNonNull()) { + Class clazz = entry.getInstance(); + assert DynamicHub.fromClass(clazz).isLoaded(); if (flushpoint) { - if (JfrTraceId.isUsedCurrentEpoch(clazz)) { - visitClass(typeInfo, clazz); - } + assert JfrTraceId.isUsedCurrentEpoch(clazz); + visitClass(typeInfo, clazz); + } else { - if (JfrTraceId.isUsedPreviousEpoch(clazz)) { - JfrTraceId.clearUsedPreviousEpoch(clazz); - visitClass(typeInfo, clazz); - } + assert JfrTraceId.isUsedPreviousEpoch(clazz); + JfrTraceId.clearUsedPreviousEpoch(clazz); + visitClass(typeInfo, clazz); } + entry = entry.getNext(); } } - } private void visitClass(TypeInfo typeInfo, Class clazz) { @@ -448,6 +489,7 @@ private void clearEpochData() { currentPackageId = 0; currentModuleId = 0; currentClassLoaderId = 0; + getEpochData(true).clear(); } private final class TypeInfo { @@ -518,6 +560,7 @@ private void setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoR } } + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) private static int getHash(String imageHeapString) { // It's possible the type exists, but has no name. if (imageHeapString == null){ diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index 9bf1dcc5f11d..167f1b5ca97d 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -338,10 +338,6 @@ public void beginRecording() { return; } - // Cache all classes in preparation for TypeRepository TODO maybe there is a better way - Heap.getHeap().visitLoadedClasses(clazz -> {}); - System.out.println("Cached classes count: " + Heap.getHeap().getCachedClasses().length); - System.out.println("Cached classes count: " + Arrays.stream(Heap.getHeap().getCachedClasses()).count()); JfrEmergencyDumpSupport.singleton().initialize(); JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); From e912a0b663baa946a1f6d690e7cbce855dfe5aeb Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Tue, 10 Jun 2025 15:33:21 -0400 Subject: [PATCH 08/16] add hook next to heap dump code. Only process full sampler buffers. --- .../oracle/svm/core/heap/OutOfMemoryUtil.java | 6 +- .../svm/core/jfr/JfrChunkFileWriter.java | 63 ++++++++-------- .../svm/core/jfr/JfrRecorderThread.java | 1 - .../svm/core/jfr/JfrTypeRepository.java | 71 ++++++++++--------- .../com/oracle/svm/core/jfr/SubstrateJVM.java | 3 + 5 files changed, 76 insertions(+), 68 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/OutOfMemoryUtil.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/OutOfMemoryUtil.java index 9891620da34b..89f796bd91ab 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/OutOfMemoryUtil.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/OutOfMemoryUtil.java @@ -31,6 +31,7 @@ import com.oracle.svm.core.headers.LibC; import com.oracle.svm.core.heap.dump.HeapDumping; import com.oracle.svm.core.jdk.JDKUtils; +import com.oracle.svm.core.jfr.SubstrateJVM; import com.oracle.svm.core.log.Log; import com.oracle.svm.core.stack.StackOverflowCheck; import com.oracle.svm.core.util.VMError; @@ -62,9 +63,12 @@ public static OutOfMemoryError reportOutOfMemoryError(OutOfMemoryError error) { @Uninterruptible(reason = "Not uninterruptible but it doesn't matter for the callers.", calleeMustBe = false) private static void reportOutOfMemoryError0(OutOfMemoryError error) { - if (VMInspectionOptions.hasHeapDumpSupport() && SubstrateOptions.HeapDumpOnOutOfMemoryError.getValue()) { // TODO add jfr emergency dump here + if (VMInspectionOptions.hasHeapDumpSupport() && SubstrateOptions.HeapDumpOnOutOfMemoryError.getValue()) { HeapDumping.singleton().dumpHeapOnOutOfMemoryError(); } + if (VMInspectionOptions.hasJfrSupport()) { + SubstrateJVM.get().vmErrorRotation(); + } if (SubstrateGCOptions.ExitOnOutOfMemoryError.getValue()) { if (LibC.isSupported()) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java index 9737815a4515..ba27641366c5 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java @@ -50,6 +50,7 @@ import com.oracle.svm.core.jfr.sampler.JfrRecurringCallbackExecutionSampler; import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.locks.VMMutex; +import com.oracle.svm.core.memory.NullableNativeMemory; import com.oracle.svm.core.os.RawFileOperationSupport; import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; @@ -201,7 +202,6 @@ public void write(JfrBuffer buffer) { } } -// @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") @Override public void flush() { assert lock.isOwner(); @@ -252,12 +252,13 @@ public void closeFile() { } /** Similar to a regular chunk rotation but we do not safepoint, start a new epoch, or re-register threads. - * Similar to a flushpoint but we close the file and also process sampler buffers.*/ + * Similar to a flushpoint but we close the file and also process full sampler buffers. + * Unfortunately, it's not possible to process the active buffers since we are not stopping for a safepoint.*/ @Override public void closeFileForEmergencyDump() { assert lock.isOwner(); - processSamplerBuffers(); + SamplerBuffersAccess.processFullBuffers(false); flushStorage(true); writeThreadCheckpoint(true); @@ -556,14 +557,14 @@ public void writeString(String str) { getFileSupport().writeByte(fd, StringEncoding.UTF8_BYTE_ARRAY.getValue()); int length = UninterruptibleUtils.String.modifiedUTF8Length(str, false); - Pointer buffer = com.oracle.svm.core.memory.NullableNativeMemory.malloc(length, NmtCategory.JFR); + Pointer buffer = NullableNativeMemory.malloc(length, NmtCategory.JFR); if (buffer.isNull()){ return; } writeCompressedInt(length); UninterruptibleUtils.String.toModifiedUTF8(str, buffer, buffer.add(length), false); getFileSupport().write(fd, buffer, WordFactory.unsigned(length)); - com.oracle.svm.core.memory.NullableNativeMemory.free(buffer); + NullableNativeMemory.free(buffer); } } @@ -696,33 +697,33 @@ private void changeEpoch() { // Now that the epoch changed, re-register all running threads for the new epoch. SubstrateJVM.getThreadRepo().registerRunningThreads(); } - } - - /** - * The VM is at a safepoint, so all other threads have a native state. However, execution - * sampling could still be executed. For the {@link JfrRecurringCallbackExecutionSampler}, - * it is sufficient to mark this method as uninterruptible to prevent execution of the - * recurring callbacks. If the SIGPROF-based sampler is used, the signal handler may still - * be executed at any time for any thread (including the current thread). To prevent races, - * we need to ensure that there are no threads that execute the SIGPROF handler while we are - * accessing the currently active buffers of other threads. - */ - @Uninterruptible(reason = "Prevent JFR recording.") - private static void processSamplerBuffers() { - assert VMOperation.isInProgressAtSafepoint(); - assert RecurringCallbackSupport.isCallbackUnsupportedOrTimerSuspended(); - - JfrExecutionSampler.singleton().disallowThreadsInSamplerCode(); - try { - processSamplerBuffers0(); - } finally { - JfrExecutionSampler.singleton().allowThreadsInSamplerCode(); + + /** + * The VM is at a safepoint, so all other threads have a native state. However, execution + * sampling could still be executed. For the {@link JfrRecurringCallbackExecutionSampler}, + * it is sufficient to mark this method as uninterruptible to prevent execution of the + * recurring callbacks. If the SIGPROF-based sampler is used, the signal handler may still + * be executed at any time for any thread (including the current thread). To prevent races, + * we need to ensure that there are no threads that execute the SIGPROF handler while we are + * accessing the currently active buffers of other threads. + */ + @Uninterruptible(reason = "Prevent JFR recording.") + private static void processSamplerBuffers() { + assert VMOperation.isInProgressAtSafepoint(); + assert RecurringCallbackSupport.isCallbackUnsupportedOrTimerSuspended(); + + JfrExecutionSampler.singleton().disallowThreadsInSamplerCode(); + try { + processSamplerBuffers0(); + } finally { + JfrExecutionSampler.singleton().allowThreadsInSamplerCode(); + } } - } - @Uninterruptible(reason = "Prevent JFR recording.") - private static void processSamplerBuffers0() { - SamplerBuffersAccess.processActiveBuffers(); - SamplerBuffersAccess.processFullBuffers(false); + @Uninterruptible(reason = "Prevent JFR recording.") + private static void processSamplerBuffers0() { + SamplerBuffersAccess.processActiveBuffers(); + SamplerBuffersAccess.processFullBuffers(false); + } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java index 08cf5a177f9f..6e28690d99b6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java @@ -111,7 +111,6 @@ private void work() { } void endRecording() { - com.oracle.svm.core.jfr.SubstrateJVM.get().vmErrorRotation();// TODO move to where it can handle an actual OOME lock.lock(); try { SubstrateJVM.JfrEndRecordingOperation vmOp = new SubstrateJVM.JfrEndRecordingOperation(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index 9de043862db7..c1329d9bad22 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -69,7 +69,7 @@ public class JfrTypeRepository implements JfrRepository { private final JfrClassInfoTable epochTypeData0; private final JfrClassInfoTable epochTypeData1; - private final UninterruptibleUtils.CharReplacer dotWithSlash = new ReplaceDotWithSlash(); + private final UninterruptibleUtils.CharReplacer dotWithSlash; private long currentPackageId = 0; private long currentModuleId = 0; private long currentClassLoaderId = 0; @@ -80,13 +80,17 @@ public JfrTypeRepository() { flushedPackages = new JfrPackageInfoTable(); flushedModules = new JfrModuleInfoTable(); flushedClassLoaders = new JfrClassLoaderInfoTable(); + typeInfo = new TypeInfo(); + dotWithSlash = new ReplaceDotWithSlash(); + epochTypeData0 = new JfrClassInfoTable(); epochTypeData1 = new JfrClassInfoTable(); } public void teardown() { clearEpochData(); + getEpochData(false).clear(); typeInfo.teardown(); } @@ -98,8 +102,15 @@ private JfrClassInfoTable getEpochData(boolean previousEpoch) { @Uninterruptible(reason = "Result is only valid until epoch changes.", callerMustBe = true) public long getClassId(Class clazz) { - addClass(clazz); - /* Tagging the traceID epoch bits is not required for this class to serialize types. But we must still do it to support JVM#getTypeId(Class)*/ + JfrClassInfoTable classInfoTable = getEpochData(false); + ClassInfoRaw classInfoRaw = StackValue.get(ClassInfoRaw.class); + classInfoRaw.setId(JfrTraceId.getTraceId(clazz)); + classInfoRaw.setHash(getHash(clazz.getName())); + classInfoRaw.setName(clazz.getName()); + classInfoRaw.setInstance(clazz); + classInfoTable.putIfAbsent(classInfoRaw); + + /* Tagging the traceID epoch bits is not actually required for this class to serialize types.*/ return JfrTraceId.load(clazz); } @@ -122,27 +133,16 @@ public int write(JfrChunkWriter writer, boolean flushpoint) { return count; } - /** Called upon event emission. */ - @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) - public void addClass(Class clazz) { - JfrClassInfoTable classInfoTable = getEpochData(false); - ClassInfoRaw classInfoRaw = StackValue.get(ClassInfoRaw.class); - classInfoRaw.setId(JfrTraceId.getTraceId(clazz)); - classInfoRaw.setHash(getHash(clazz.getName())); - classInfoRaw.setName(clazz.getName()); - classInfoRaw.setInstance(clazz); - classInfoTable.putIfAbsent(classInfoRaw); - } - /** * Visit all used classes, and collect their packages, modules, classloaders and possibly * referenced classes. * * This method does not need to be marked uninterruptible since the epoch cannot change while the chunkwriter * lock is held. - * Unlike other JFR repositories, locking is not needed to protect a data buffer. Writes to the JfrClassInfoTable - * and the read here are allowed to race. There is no risk of separating events from constant data due to - * the write order (constants before events during emission, and events before constants during flush). + * Unlike other JFR repositories, locking is not needed to protect a data buffer. Similar to other constant repositories, + * writes/reads to the current epochData are allowed to race at flushpoints. + * There is no risk of separating events from constant data due to the write order + * (constants before events during emission, and events before constants during flush). */ private void collectTypeInfo(boolean flushpoint) { JfrClassInfoTable classInfoTable = getEpochData(!flushpoint); @@ -206,7 +206,7 @@ private int writeClasses(JfrChunkWriter writer, TypeInfo typeInfo, boolean flush writer.writeCompressedLong(JfrType.Class.getId()); writer.writeCompressedInt(size); - // *** we can't use visitor pattern, but maybe there's a better way than duplicating this over and over again. + // Nested loops since the visitor pattern may allocate. for (int i = 0; i < table.length; i++) { ClassInfoRaw entry = table[i]; while (entry.isNonNull()) { @@ -361,8 +361,8 @@ private boolean isClassVisited(TypeInfo typeInfo, ClassInfoRaw classInfoRaw) { return typeInfo.classes.contains(classInfoRaw) || flushedClasses.contains(classInfoRaw); } + /** We cannot directly call getPackage() or getPackageName() since that may allocate. */ private boolean addPackage(TypeInfo typeInfo, Class clazz) { - // *** if we've made it this far, we know the package is not null. Although the name may be empty boolean hasModule = clazz.getModule() != null; String moduleName = hasModule ? clazz.getModule().getName() : null; @@ -371,7 +371,7 @@ private boolean addPackage(TypeInfo typeInfo, Class clazz) { setPackageNameAndLength(clazz, packageInfoRaw); /* The empty/null package represented by "" is always traced with id 0. - The id 0 is reserved and does not need to be serialized. */ + The id 0 is reserved and does not need to be serialized. */ if (packageInfoRaw.getNameLength().equal(0)) { return false; } @@ -446,19 +446,21 @@ private boolean addClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { } else { classLoaderInfoRaw.setName(classLoader.getName()); } - classLoaderInfoRaw.setHash(getHash(classLoaderInfoRaw.getName())); + classLoaderInfoRaw.setHash(getHash(classLoaderInfoRaw.getName())); if (isClassLoaderVisited(typeInfo, classLoaderInfoRaw)) { return false; } - if (classLoader != null) { - classLoaderInfoRaw.setId(++currentClassLoaderId); - classLoaderInfoRaw.setClassTraceId(JfrTraceId.getTraceId(classLoader.getClass())); - } else { + + if (classLoader == null) { // Bootstrap loader has reserved ID of 0 classLoaderInfoRaw.setId(0); classLoaderInfoRaw.setClassTraceId(0); + } else { + classLoaderInfoRaw.setId(++currentClassLoaderId); + classLoaderInfoRaw.setClassTraceId(JfrTraceId.getTraceId(classLoader.getClass())); } + typeInfo.classLoaders.putNew(classLoaderInfoRaw); return true; } @@ -512,8 +514,6 @@ void teardown() { } } - // *** we shouldn't preemtively compute ALL package names, only the ones used in JFR events. This current approach is ok, but since we don't stash, we must recompute every time. - // *** Maybe its not a big deal since we call getPackage() on every class we visit anyway. And we only do this for classes that are in an event. /** This method sets the package name and length. packageInfoRaw may be on the stack or native memory.*/ private void setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoRaw) { DynamicHub hub = DynamicHub.fromClass(clazz); @@ -550,7 +550,8 @@ private void setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoR assert buffer.add(dot).belowOrEqual(bufferEnd); - Pointer packageNameEnd = UninterruptibleUtils.String.toModifiedUTF8(str, dot, buffer, bufferEnd, false, dotWithSlash); // *** we need to replace dots w slashes in here now. + // Since we're serializing now, we must do replacements here, instead of the symbol repository. + Pointer packageNameEnd = UninterruptibleUtils.String.toModifiedUTF8(str, dot, buffer, bufferEnd, false, dotWithSlash); packageInfoRaw.setModifiedUTF8Name(buffer); UnsignedWord packageNameLength = packageNameEnd.subtract(buffer); // end - start @@ -639,7 +640,7 @@ private interface ModuleInfoRaw extends JfrTypeInfo { @RawField String getClassLoaderName(); @RawField - void setHasClassLoader(boolean value); // *** name may be empty or null, but CL may be non null + void setHasClassLoader(boolean value); // Needed because CL name may be empty or null, even if CL is non-null @RawField boolean getHasClassLoader(); } @@ -722,7 +723,7 @@ protected UninterruptibleEntry copyToHeap(UninterruptibleEntry visitedOnStack) { @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { - // *** We can't compare IDs bc that's something we assign after we do the check. + // IDs cannot be compared since they are only assigned after checking the table. PackageInfoRaw entry1 = (PackageInfoRaw) v0; PackageInfoRaw entry2 = (PackageInfoRaw) v1; return entry1.getNameLength().equal(entry2.getNameLength()) && LibC.memcmp(entry1.getModifiedUTF8Name(), entry2.getModifiedUTF8Name(), entry1.getNameLength()) == 0; @@ -732,7 +733,7 @@ protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) protected void free(UninterruptibleEntry entry) { PackageInfoRaw packageInfoRaw = (PackageInfoRaw) entry; - /* The base method will free only the entry itself, not th utf8 data. */ + /* The base method will free only the entry itself, not the utf8 data. */ NullableNativeMemory.free(packageInfoRaw.getModifiedUTF8Name()); packageInfoRaw.setModifiedUTF8Name(WordFactory.nullPointer()); super.free(entry); @@ -746,11 +747,11 @@ public void putAll(JfrPackageInfoTable sourceTable) { if (!contains(sourceInfo)) { // Put if not already there. PackageInfoRaw destinationInfo = (PackageInfoRaw) putNew(sourceInfo); - // allocate a new buffer + // allocate a new buffer. PointerBase newUtf8Name = NullableNativeMemory.malloc(sourceInfo.getNameLength(), NmtCategory.JFR); - // set the buffer ptr + // set the buffer ptr. destinationInfo.setModifiedUTF8Name(newUtf8Name); - // Copy source buffer contents over to new buffer + // Copy source buffer contents over to new buffer. if (newUtf8Name.isNonNull()) { UnmanagedMemoryUtil.copy((Pointer) sourceInfo.getModifiedUTF8Name(), (Pointer) newUtf8Name, sourceInfo.getNameLength()); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index 167f1b5ca97d..d94a3e5cf971 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -744,6 +744,9 @@ public Object getConfiguration(Class eventClass) { /** See JfrRecorderService::vm_error_rotation */ @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void vmErrorRotation() { + if (!recording) { + return; + } JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); try { boolean existingFile = chunkWriter.hasOpenFile(); From 01b11b9e30335e0d0b25314d3e93ac639eb89950 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Mon, 16 Jun 2025 17:18:58 -0400 Subject: [PATCH 09/16] qsort chunk files. Bug fix traceID check. --- .../svm/core/posix/PosixLibCSupport.java | 6 + .../svm/core/posix/headers/PosixLibC.java | 3 + .../jfr/PosixJfrEmergencyDumpSupport.java | 301 ++++++++++++------ .../svm/core/windows/WindowsLibCSupport.java | 6 + .../svm/core/windows/headers/WindowsLibC.java | 3 + .../collections/GrowableWordArrayAccess.java | 42 +++ .../src/com/oracle/svm/core/headers/LibC.java | 5 + .../oracle/svm/core/headers/LibCSupport.java | 3 + .../svm/core/jfr/JfrChunkFileWriter.java | 29 +- .../oracle/svm/core/jfr/JfrChunkNoWriter.java | 6 + .../oracle/svm/core/jfr/JfrChunkWriter.java | 4 + .../svm/core/jfr/JfrEmergencyDumpFeature.java | 7 - .../svm/core/jfr/JfrEmergencyDumpSupport.java | 2 + .../svm/core/jfr/JfrRecorderThread.java | 1 + .../svm/core/jfr/JfrTypeRepository.java | 126 +++++--- .../com/oracle/svm/core/jfr/SubstrateJVM.java | 11 +- .../jfr/oldobject/JfrOldObjectRepository.java | 1 - .../jfr/TestGrowableWordArrayQuickSort.java | 145 +++++++++ 18 files changed, 539 insertions(+), 162 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestGrowableWordArrayQuickSort.java diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixLibCSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixLibCSupport.java index 528c09c8f9fa..27b1b819208b 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixLibCSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixLibCSupport.java @@ -107,6 +107,12 @@ public int strcmp(CCharPointer s1, CCharPointer s2) { return PosixLibC.strcmp(s1, s2); } + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public int strncmp(CCharPointer s1, CCharPointer s2, UnsignedWord n) { + return PosixLibC.strncmp(s1, s2, n); + } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public int isdigit(int c) { diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/PosixLibC.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/PosixLibC.java index 36bfdcfeac15..838cf9d02bca 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/PosixLibC.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/PosixLibC.java @@ -77,6 +77,9 @@ public class PosixLibC { @CFunction(transition = CFunction.Transition.NO_TRANSITION) public static native int strcmp(PointerBase s1, PointerBase s2); + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int strncmp(PointerBase s1, PointerBase s2, UnsignedWord n); + @CFunction(transition = CFunction.Transition.NO_TRANSITION) public static native CCharPointer strcpy(CCharPointer dst, CCharPointer src); diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java index 502f85c20c6a..c3f24856f500 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -36,22 +36,26 @@ import org.graalvm.word.Pointer; import jdk.graal.compiler.word.Word; import org.graalvm.word.WordFactory; -//import jdk.jfr.internal.LogLevel; -//import jdk.jfr.internal.LogTag; -//import jdk.jfr.internal.Logger; -import org.graalvm.nativeimage.c.type.CTypeConversion; import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.StackValue; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.jfr.JfrEmergencyDumpFeature; +import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; +import com.oracle.svm.core.log.Log; import com.oracle.svm.core.memory.NativeMemory; import com.oracle.svm.core.memory.NullableNativeMemory; import com.oracle.svm.core.os.RawFileOperationSupport; import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; +import com.oracle.svm.core.util.BasedOnJDKFile; +import com.oracle.svm.core.collections.GrowableWordArray; +import com.oracle.svm.core.collections.GrowableWordArrayAccess; import jdk.graal.compiler.api.replacements.Fold; @@ -61,11 +65,16 @@ import static com.oracle.svm.core.posix.headers.Fcntl.O_RDONLY; public class PosixJfrEmergencyDumpSupport implements com.oracle.svm.core.jfr.JfrEmergencyDumpSupport { - private static final int CHUNK_FILE_HEADER_SIZE = 68;// TODO based on jdk file - private static final int JVM_MAXPATHLEN = 4096;// TODO based on jdk file + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L49") // + private static final int CHUNK_FILE_HEADER_SIZE = 68; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/os/posix/include/jvm_md.h#L57") // + private static final int JVM_MAXPATHLEN = 4096; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L47") // + private static int ISO8601_LEN = 19; private static final byte FILE_SEPARATOR = "/".getBytes(StandardCharsets.UTF_8)[0]; + private static final byte DOT = ".".getBytes(StandardCharsets.UTF_8)[0]; private static final byte[] DUMP_FILE_PREFIX = "hs_oom_pid_".getBytes(StandardCharsets.UTF_8); - private static final byte[] CHUNKFILE_EXTENSION_BYTES = ".jfr".getBytes(StandardCharsets.UTF_8); // TODO double check its utf8 you want. + private static final byte[] CHUNKFILE_EXTENSION_BYTES = ".jfr".getBytes(StandardCharsets.UTF_8); private Dirent.DIR directory; private byte[] pidBytes; private byte[] dumpPathBytes; @@ -80,8 +89,7 @@ public PosixJfrEmergencyDumpSupport() { public void initialize() { pidBytes = String.valueOf(ProcessHandle.current().pid()).getBytes(StandardCharsets.UTF_8); pathBuffer = NativeMemory.calloc(JVM_MAXPATHLEN, NmtCategory.JFR); - directory = WordFactory.nullPointer(); // *** maybe not necessary. - //TODO need to terminate the string with 0. otherwise length function will not work. + directory = WordFactory.nullPointer(); } public void setRepositoryLocation(String dirText) { @@ -94,42 +102,79 @@ public void setDumpPath(String dumpPathText) { public String getDumpPath() { if (dumpPathBytes != null) { - return new String(dumpPathBytes,StandardCharsets.UTF_8); + return new String(dumpPathBytes, StandardCharsets.UTF_8); } return ""; } - /** See JfrEmergencyDump::on_vm_error*/ - public void onVmError(){ + // Either use create and use the dumpfile itself, or create a new file in the repository + // location. + public RawFileDescriptor chunkPath() { + if (repositoryLocationBytes == null) { + if (!openEmergencyDumpFile()) { + return WordFactory.nullPointer(); + } + // We can directly use the emergency dump file name as the chunk. + return emergencyFd; + } + return createEmergencyChunkPath(); + } + + private RawFileDescriptor createEmergencyChunkPath() { + int idx = 0; + clearPathBuffer(); + for (int i = 0; i < repositoryLocationBytes.length; i++) { + getPathBuffer().write(idx++, repositoryLocationBytes[i]); + } + getPathBuffer().write(idx++, FILE_SEPARATOR); + + for (int i = 0; i < CHUNKFILE_EXTENSION_BYTES.length; i++) { + getPathBuffer().write(idx++, CHUNKFILE_EXTENSION_BYTES[i]); + } + // TODO what about date time? Need that for sorting. + // repository path + file separator + date time + extension + return getFileSupport().create(getPathBuffer(), FileCreationMode.CREATE, FileAccessMode.READ_WRITE); + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L409-L416") + public void onVmError() { + Log.log().string("Attempting JFR Emergency Dump").newline(); if (openEmergencyDumpFile()) { - writeEmergencyDumpFile(); - getFileSupport().close(emergencyFd); - emergencyFd = WordFactory.nullPointer(); + GrowableWordArray sortedChunkFilenames = StackValue.get(GrowableWordArray.class); + GrowableWordArrayAccess.initialize(sortedChunkFilenames); + try { + iterateRepository(sortedChunkFilenames); + writeEmergencyDumpFile(sortedChunkFilenames); + closeEmergencyDumpFile(); + } finally { + GrowableWordArrayAccess.freeData(sortedChunkFilenames); + sortedChunkFilenames = Word.nullPointer(); + } } } - /** See open_emergency_dump_file */ - private boolean openEmergencyDumpFile(){ - if (getFileSupport().isValid(emergencyFd)){ + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L131-L146") + private boolean openEmergencyDumpFile() { + if (getFileSupport().isValid(emergencyFd)) { return true; } - emergencyFd = getFileSupport().create(createEmergencyDumpPath(), FileCreationMode.CREATE, FileAccessMode.READ_WRITE); //gives us O_CREAT | O_RDWR for creation mode and S_IREAD | S_IWRITE permissions + // O_CREAT | O_RDWR and S_IREAD | S_IWRITE permissions + emergencyFd = getFileSupport().create(createEmergencyDumpPath(), FileCreationMode.CREATE, FileAccessMode.READ_WRITE); if (!getFileSupport().isValid(emergencyFd)) { // Fallback. Try to create it in the current directory. dumpPathBytes = null; emergencyFd = getFileSupport().create(createEmergencyDumpPath(), FileCreationMode.CREATE, FileAccessMode.READ_WRITE); } -// System.out.println("openEmergencyDumpFile pathBuffer: "+ CTypeConversion.toJavaString(getPathBuffer())); return getFileSupport().isValid(emergencyFd); } - /** See create_emergency_dump_path */ + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L110-L129") private CCharPointer createEmergencyDumpPath() { int idx = 0; clearPathBuffer(); - if(dumpPathBytes != null) { + if (dumpPathBytes != null) { for (int i = 0; i < dumpPathBytes.length; i++) { getPathBuffer().write(idx++, dumpPathBytes[i]); } @@ -152,69 +197,107 @@ private CCharPointer createEmergencyDumpPath() { return getPathBuffer(); } - private void writeEmergencyDumpFile() { - if (openDirectorySecure()) { + private GrowableWordArray iterateRepository(GrowableWordArray gwa) { + int count = 0; + // Open directory + if (openDirectory()) { if (directory.isNull()) { - return; - } - int blockSize = 1024 * 1024; - Pointer copyBlock = NullableNativeMemory.malloc(blockSize, NmtCategory.JFR); - if (copyBlock.isNull()) { - // TODO trouble importing these internal methods -// Logger.log(LogTag.JFR_SYSTEM, LogLevel.ERROR, "Emergency dump failed. Could not allocate copy block."); - return; + return WordFactory.nullPointer(); } - + // Iterate files in the repository and append filtered file names to the files array Dirent.dirent entry; while ((entry = Dirent.readdir(directory)).isNonNull()) { - RawFileDescriptor chunkFd = filter(entry.d_name()); - if (getFileSupport().isValid(chunkFd)){ -// String name = CTypeConversion.toJavaString(entry.d_name()); -// System.out.println("Filter checks passed. Chunk file name: "+ name); - - // Read it's size - long chunkFileSize = getFileSupport().size(chunkFd); - long bytesRead = 0; - long bytesWritten = 0; - while (bytesRead < chunkFileSize){ - // Start at beginning - getFileSupport().seek(chunkFd, 0); // seems unneeded. idk why??? - // Read from chunk file to copy block - long readResult = getFileSupport().read(chunkFd, copyBlock, WordFactory.unsigned(blockSize));// *** i think this already retries until fully read[no just retries until gets a sucessful read] - if (readResult < 0){ // -1 if read failed -// System.out.println("Log ERROR. Read failed."); - break; - } - bytesRead += readResult; - assert bytesRead - bytesWritten <= blockSize; - // Write from copy block to dump file - if (!getFileSupport().write(emergencyFd, copyBlock, WordFactory.unsigned(bytesRead - bytesWritten))) { // *** may iterate until fully writtem -// System.out.println("Log ERROR. Write failed."); - break; - } - bytesWritten = bytesRead; -// System.out.println("bytesWritten " + bytesWritten); + // Filter files + CCharPointer fn = entry.d_name(); + if (filter(fn)) { + // Append filtered files to list + GrowableWordArrayAccess.add(gwa, (Word) fn, NmtCategory.JFR); + count++; + } + } + closeDirectory(); + if (count > 0) { + GrowableWordArrayAccess.qsort(gwa, 0, count - 1, PosixJfrEmergencyDumpSupport::compare); + } +// for (int i=0; i < count; i ++){ // todo remove +// String name = +// org.graalvm.nativeimage.c.type.CTypeConversion.toJavaString(GrowableWordArrayAccess.read(gwa, +// i)); +// System.out.println("chunk file: "+ name); +// } + } + return WordFactory.nullPointer(); + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L191-L212") + static int compare(Word a, Word b) { + CCharPointer filenameA = (CCharPointer) a; + CCharPointer filenameB = (CCharPointer) b; + int cmp = LibC.strncmp(filenameA, filenameB, WordFactory.unsigned(ISO8601_LEN)); + if (cmp == 0) { + CCharPointer aDot = SubstrateUtil.strchr(filenameA, DOT); + CCharPointer bDot = SubstrateUtil.strchr(filenameB, DOT); + long aLen = aDot.rawValue() - a.rawValue(); + long bLen = bDot.rawValue() - b.rawValue(); + if (aLen < bLen) { + return -1; + } + if (aLen > bLen) { + return 1; + } + cmp = LibC.strncmp(filenameA, filenameB, WordFactory.unsigned(aLen)); + } + return cmp; + } + + private void writeEmergencyDumpFile(GrowableWordArray sortedChunkFilenames) { + int blockSize = 1024 * 1024; + Pointer copyBlock = NullableNativeMemory.malloc(blockSize, NmtCategory.JFR); + if (copyBlock.isNull()) { + Log.log().string("Emergency dump failed. Could not allocate copy block."); + return; + } + + for (int i = 0; i < sortedChunkFilenames.getSize(); i++) { + CCharPointer fn = GrowableWordArrayAccess.read(sortedChunkFilenames, i); + RawFileDescriptor chunkFd = getFileSupport().open(fullyQualified(fn), FileAccessMode.READ_WRITE); + if (getFileSupport().isValid(chunkFd)) { + + // Read it's size + long chunkFileSize = getFileSupport().size(chunkFd); + long bytesRead = 0; + long bytesWritten = 0; + while (bytesRead < chunkFileSize) { + // Start at beginning + getFileSupport().seek(chunkFd, 0); // seems unneeded. idk why??? + // Read from chunk file to copy block + long readResult = getFileSupport().read(chunkFd, copyBlock, WordFactory.unsigned(blockSize)); + if (readResult < 0) { // -1 if read failed + Log.log().string("Emergency dump failed. Chunk file read failed."); + break; } - getFileSupport().close(chunkFd); + bytesRead += readResult; + assert bytesRead - bytesWritten <= blockSize; + // Write from copy block to dump file + if (!getFileSupport().write(emergencyFd, copyBlock, WordFactory.unsigned(bytesRead - bytesWritten))) { + Log.log().string("Emergency dump failed. Dump file write failed."); + break; + } + bytesWritten = bytesRead; } + getFileSupport().close(chunkFd); } - NullableNativeMemory.free(copyBlock); } + NullableNativeMemory.free(copyBlock); } - // *** copied from PosixPerfMemoryProvider - private boolean openDirectorySecure() { + private boolean openDirectory() { int fd = restartableOpen(getRepositoryLocation(), O_RDONLY() | O_NOFOLLOW(), 0); if (fd == -1) { return false; } -// if (!isDirFdSecure(fd)) { //TODO do we need this? -// com.oracle.svm.core.posix.headers.Unistd.NoTransitions.close(fd); -// return null; -// } - - this.directory = Dirent.fdopendir(fd); + directory = Dirent.fdopendir(fd); if (directory.isNull()) { Unistd.NoTransitions.close(fd); return false; @@ -230,7 +313,10 @@ private CCharPointer getRepositoryLocation() { return getPathBuffer(); } - // *** copied from PosixPerfMemoryProvider + /** + * See + * {@link com.oracle.svm.core.posix.jvmstat.PosixPerfMemoryProvider#restartableOpen(CCharPointer, int, int)} + */ @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") private static int restartableOpen(CCharPointer directory, int flags, int mode) { int result; @@ -241,13 +327,13 @@ private static int restartableOpen(CCharPointer directory, int flags, int mode) return result; } - /** See RepositoryIterator::filter */ - private RawFileDescriptor filter(CCharPointer fn){ + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L276-L308") + private boolean filter(CCharPointer fn) { - // check filename extension + // Check filename length int filenameLength = (int) SubstrateUtil.strlen(fn).rawValue(); - if (filenameLength <= CHUNKFILE_EXTENSION_BYTES.length){ - return WordFactory.nullPointer(); + if (filenameLength <= CHUNKFILE_EXTENSION_BYTES.length) { + return false; } // Verify file extension @@ -255,40 +341,39 @@ private RawFileDescriptor filter(CCharPointer fn){ int idx1 = CHUNKFILE_EXTENSION_BYTES.length - i - 1; int idx2 = filenameLength - i - 1; if (CHUNKFILE_EXTENSION_BYTES[idx1] != ((Pointer) fn).readByte(idx2)) { -// System.out.println("failed extension check"); - return WordFactory.nullPointer(); + return false; } } -// String name = CTypeConversion.toJavaString(fullyQualified(fn)); -// System.out.println("fully qualified: "+ name); - // Verify if you can open it and receive a valid file descriptor RawFileDescriptor chunkFd = getFileSupport().open(fullyQualified(fn), FileAccessMode.READ_WRITE); if (!getFileSupport().isValid(chunkFd)) { -// System.out.println("failed open check"); - return WordFactory.nullPointer(); + return false; } // Verify file size long chunkFileSize = getFileSupport().size(chunkFd); if (chunkFileSize < CHUNK_FILE_HEADER_SIZE) { -// System.out.println("failed size check"); - return WordFactory.nullPointer(); + return false; } - - return chunkFd; + getFileSupport().close(chunkFd); + return true; } - /** Given a chunk file name, it returns the fully qualified filename. See RepositoryIterator::fully_qualified */ - private CCharPointer fullyQualified(CCharPointer fn){ - long fnLength = SubstrateUtil.strlen(fn).rawValue() ; + /** + * Given a chunk file name, it returns the fully qualified filename. See + * RepositoryIterator::fully_qualified + */ + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L263-L273") + private CCharPointer fullyQualified(CCharPointer fn) { + long fnLength = SubstrateUtil.strlen(fn).rawValue(); int idx = 0; clearPathBuffer(); - // TODO HS uses _path_buffer_file_name_offset to avoid building this part of th path each time. - // Cached in RepositoryIterator::RepositoryIterator and used in fully_qualified + // TODO HS uses _path_buffer_file_name_offset to avoid building this part of th path each + // time. + // Cached in RepositoryIterator::RepositoryIterator and used in fully_qualified for (int i = 0; i < repositoryLocationBytes.length; i++) { getPathBuffer().write(idx++, repositoryLocationBytes[i]); } @@ -296,18 +381,18 @@ private CCharPointer fullyQualified(CCharPointer fn){ // Add delimiter getPathBuffer().write(idx++, FILE_SEPARATOR); - for (int i = 0; i < fnLength; i++) { getPathBuffer().write(idx++, fn.read(i)); } return getPathBuffer(); } - private CCharPointer getPathBuffer(){ - return pathBuffer; + private CCharPointer getPathBuffer() { + return pathBuffer; } - private void clearPathBuffer(){ + private void clearPathBuffer() { + // Terminate the string with 0. LibC.memset(getPathBuffer(), Word.signed(0), Word.unsigned(JVM_MAXPATHLEN)); } @@ -316,20 +401,34 @@ static RawFileOperationSupport getFileSupport() { return RawFileOperationSupport.bigEndian(); } + private void closeEmergencyDumpFile() { + if (getFileSupport().isValid(emergencyFd)) { + getFileSupport().close(emergencyFd); + emergencyFd = WordFactory.nullPointer(); + } + } + + private void closeDirectory() { + if (directory.isNonNull()) { + Dirent.closedir(directory); + directory = WordFactory.nullPointer(); + } + } + public void teardown() { - // *** this must survive as long as we need the dump path c pointer. - Dirent.closedir(directory); + closeEmergencyDumpFile(); + closeDirectory(); NativeMemory.free(pathBuffer); } } -@com.oracle.svm.core.feature.AutomaticallyRegisteredFeature -class PosixJfrEmergencyDumpFeature extends com.oracle.svm.core.jfr.JfrEmergencyDumpFeature { +@AutomaticallyRegisteredFeature +class PosixJfrEmergencyDumpFeature extends JfrEmergencyDumpFeature { @Override public void afterRegistration(AfterRegistrationAccess access) { PosixJfrEmergencyDumpSupport support = new PosixJfrEmergencyDumpSupport(); - ImageSingletons.add(com.oracle.svm.core.jfr.JfrEmergencyDumpSupport.class, support); + ImageSingletons.add(JfrEmergencyDumpSupport.class, support); ImageSingletons.add(PosixJfrEmergencyDumpSupport.class, support); } } diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsLibCSupport.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsLibCSupport.java index 2f67d1a6dc18..ec951ec6b6f8 100644 --- a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsLibCSupport.java +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/WindowsLibCSupport.java @@ -121,6 +121,12 @@ public int strcmp(CCharPointer s1, CCharPointer s2) { return WindowsLibC.strcmp(s1, s2); } + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public int strncmp(CCharPointer s1, CCharPointer s2, UnsignedWord n) { + return WindowsLibC.strncmp(s1, s2, n); + } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public int isdigit(int c) { diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/WindowsLibC.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/WindowsLibC.java index 721d0c13d7eb..f9e895cd2f7d 100644 --- a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/WindowsLibC.java +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/WindowsLibC.java @@ -77,6 +77,9 @@ public class WindowsLibC { @CFunction(transition = CFunction.Transition.NO_TRANSITION) public static native int strcmp(PointerBase s1, PointerBase s2); + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int strncmp(PointerBase s1, PointerBase s2, UnsignedWord n); + @CFunction(transition = CFunction.Transition.NO_TRANSITION) public static native CCharPointer strcpy(CCharPointer dst, CCharPointer src); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/collections/GrowableWordArrayAccess.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/collections/GrowableWordArrayAccess.java index 9f7874f31eb8..18ea7845d377 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/collections/GrowableWordArrayAccess.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/collections/GrowableWordArrayAccess.java @@ -44,6 +44,14 @@ public static void initialize(GrowableWordArray array) { array.setData(Word.nullPointer()); } + public static void write(GrowableWordArray array, int index, Word value) { + array.getData().write(index, value); + } + + public static T read(GrowableWordArray array, int index) { + return array.getData().read(index); + } + public static Word get(GrowableWordArray array, int i) { assert i >= 0 && i < array.getSize(); return array.getData().addressOf(i).read(); @@ -103,4 +111,38 @@ private static int computeNewCapacity(GrowableWordArray array) { static int wordSize() { return ConfigurationValues.getTarget().wordSize; } + + public static void qsort(GrowableWordArray gwa, int low, int high, Comparator c) { + if (low < high) { + int pivotIndex = partition(gwa, low, high, c); + qsort(gwa, low, pivotIndex - 1, c); + qsort(gwa, pivotIndex + 1, high, c); + } + } + + private static int partition(GrowableWordArray gwa, int low, int high, Comparator c) { + Word pivot = read(gwa, high); + int i = low - 1; + for (int j = low; j < high; j++) { + if (c.compare(read(gwa, j), pivot) <= 0) { + i++; + // Swap i and j + Word temp = read(gwa, i); + write(gwa, i, read(gwa, j)); + write(gwa, j, temp); + } + } + // Swap the pivot with i+1 + Word temp = read(gwa, i + 1); + write(gwa, i + 1, read(gwa, high)); + write(gwa, high, temp); + + // Return the partition index + return i + 1; + } + + @FunctionalInterface + public interface Comparator { + int compare(Word a, Word b); + } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/headers/LibC.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/headers/LibC.java index 65681458609d..1fa2ba5b6d8e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/headers/LibC.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/headers/LibC.java @@ -99,6 +99,11 @@ public static int strcmp(CCharPointer s1, CCharPointer s2) { return libc().strcmp(s1, s2); } + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static int strncmp(CCharPointer s1, CCharPointer s2, UnsignedWord n) { + return libc().strncmp(s1, s2, n); + } + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public static int isdigit(int c) { return libc().isdigit(c); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/headers/LibCSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/headers/LibCSupport.java index b7ed0b46fdae..11c35fe5d5f5 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/headers/LibCSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/headers/LibCSupport.java @@ -81,6 +81,9 @@ public interface LibCSupport { @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) int strcmp(CCharPointer s1, CCharPointer s2); + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + int strncmp(CCharPointer s1, CCharPointer s2, UnsignedWord n); + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) int isdigit(int c); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java index ba27641366c5..d72ad78e3d87 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java @@ -41,10 +41,7 @@ import org.graalvm.word.WordFactory; import com.oracle.svm.core.Uninterruptible; -import com.oracle.svm.core.graal.stackvalue.UnsafeStackValue; -import com.oracle.svm.core.heap.RestrictHeapAccess; import com.oracle.svm.core.heap.VMOperationInfos; -import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository; import com.oracle.svm.core.jdk.UninterruptibleUtils; import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler; import com.oracle.svm.core.jfr.sampler.JfrRecurringCallbackExecutionSampler; @@ -64,7 +61,6 @@ import jdk.graal.compiler.api.replacements.Fold; import jdk.graal.compiler.core.common.NumUtil; -import jdk.graal.compiler.word.Word; /** * This class is used when writing the in-memory JFR data to a file. For all operations, except @@ -171,7 +167,19 @@ public void openFile(String outputFile) { assert lock.isOwner(); filename = outputFile; fd = getFileSupport().create(filename, FileCreationMode.CREATE_OR_REPLACE, FileAccessMode.READ_WRITE); + openFile0(); + } + + // Used by JFR emergency dump + @Override + public void openFile(RawFileDescriptor fd) { + assert lock.isOwner(); + filename = null; + this.fd = fd; + openFile0(); + } + private void openFile0() { chunkStartTicks = JfrTicks.elapsedTicks(); chunkStartNanos = JfrTicks.currentTimeNanos(); nextGeneration = 1; @@ -251,9 +259,12 @@ public void closeFile() { fd = Word.nullPointer(); } - /** Similar to a regular chunk rotation but we do not safepoint, start a new epoch, or re-register threads. - * Similar to a flushpoint but we close the file and also process full sampler buffers. - * Unfortunately, it's not possible to process the active buffers since we are not stopping for a safepoint.*/ + /** + * Similar to a regular chunk rotation but we do not safepoint, start a new epoch, or + * re-register threads. Similar to a flushpoint but we close the file and also process full + * sampler buffers. Unfortunately, it's not possible to process the active buffers since we are + * not stopping for a safepoint. + */ @Override public void closeFileForEmergencyDump() { assert lock.isOwner(); @@ -385,7 +396,7 @@ private long getDeltaToLastCheckpoint(long startOfNewCheckpoint) { private int writeSerializers() { JfrSerializer[] serializers = JfrSerializerSupport.get().getSerializers(); - for (int i =0; i clazz) { classInfoRaw.setInstance(clazz); classInfoTable.putIfAbsent(classInfoRaw); - /* Tagging the traceID epoch bits is not actually required for this class to serialize types.*/ return JfrTraceId.load(clazz); } @@ -137,12 +138,12 @@ public int write(JfrChunkWriter writer, boolean flushpoint) { * Visit all used classes, and collect their packages, modules, classloaders and possibly * referenced classes. * - * This method does not need to be marked uninterruptible since the epoch cannot change while the chunkwriter - * lock is held. - * Unlike other JFR repositories, locking is not needed to protect a data buffer. Similar to other constant repositories, - * writes/reads to the current epochData are allowed to race at flushpoints. - * There is no risk of separating events from constant data due to the write order - * (constants before events during emission, and events before constants during flush). + * This method does not need to be marked uninterruptible since the epoch cannot change while + * the chunkwriter lock is held. Unlike other JFR repositories, locking is not needed to protect + * a data buffer. Similar to other constant repositories, writes/reads to the current epochData + * are allowed to race at flushpoints. There is no risk of separating events from constant data + * due to the write order (constants before events during emission, and events before constants + * during flush). The epochData tables are only cleared at rotation safepoints. */ private void collectTypeInfo(boolean flushpoint) { JfrClassInfoTable classInfoTable = getEpochData(!flushpoint); @@ -154,13 +155,16 @@ private void collectTypeInfo(boolean flushpoint) { Class clazz = entry.getInstance(); assert DynamicHub.fromClass(clazz).isLoaded(); if (flushpoint) { - assert JfrTraceId.isUsedCurrentEpoch(clazz); - visitClass(typeInfo, clazz); - + // Still must check the bit is set since we don't clear the epoch data tables + // until safepoint. + if (JfrTraceId.isUsedCurrentEpoch(clazz)) { + visitClass(typeInfo, clazz); + } } else { - assert JfrTraceId.isUsedPreviousEpoch(clazz); - JfrTraceId.clearUsedPreviousEpoch(clazz); - visitClass(typeInfo, clazz); + if (JfrTraceId.isUsedPreviousEpoch(clazz)) { + JfrTraceId.clearUsedPreviousEpoch(clazz); + visitClass(typeInfo, clazz); + } } entry = entry.getNext(); } @@ -206,7 +210,7 @@ private int writeClasses(JfrChunkWriter writer, TypeInfo typeInfo, boolean flush writer.writeCompressedLong(JfrType.Class.getId()); writer.writeCompressedInt(size); - // Nested loops since the visitor pattern may allocate. + // Nested loops since the visitor pattern may allocate. for (int i = 0; i < table.length; i++) { ClassInfoRaw entry = table[i]; while (entry.isNonNull()) { @@ -226,14 +230,14 @@ private void writeClass(TypeInfo typeInfo, JfrChunkWriter writer, ClassInfoRaw c boolean hasClassLoader = clazz.getClassLoader() != null; writer.writeCompressedLong(classInfoRaw.getId()); - writer.writeCompressedLong(getClassLoaderId(typeInfo, hasClassLoader ? clazz.getClassLoader().getName() : null, hasClassLoader)); + writer.writeCompressedLong(getClassLoaderId(typeInfo, hasClassLoader ? clazz.getClassLoader().getName() : null, hasClassLoader)); writer.writeCompressedLong(getSymbolId(writer, clazz.getName(), flushpoint, true)); writer.writeCompressedLong(getPackageId(typeInfo, packageInfoRaw)); writer.writeCompressedLong(clazz.getModifiers()); writer.writeBoolean(clazz.isHidden()); // We no longer need the buffer. - if(packageInfoRaw.getModifiedUTF8Name().isNonNull()){ + if (packageInfoRaw.getModifiedUTF8Name().isNonNull()) { NullableNativeMemory.free(packageInfoRaw.getModifiedUTF8Name()); } } @@ -248,7 +252,8 @@ private static long getSymbolId(JfrChunkWriter writer, String symbol, boolean fl return SubstrateJVM.getSymbolRepository().getSymbolId(symbol, !flushpoint, replaceDotWithSlash); } - // Copy to a new buffer so each table has its own copy of the data. This simplifies cleanup and mitigates double frees. + // Copy to a new buffer so each table has its own copy of the data. This simplifies cleanup and + // mitigates double frees. @Uninterruptible(reason = "Needed for JfrSymbolRepository.getSymbolId().") private static long getSymbolId(JfrChunkWriter writer, PointerBase source, UnsignedWord length, int hash, boolean flushpoint) { Pointer destination = NullableNativeMemory.malloc(length, NmtCategory.JFR); @@ -283,8 +288,10 @@ private int writePackages(JfrChunkWriter writer, TypeInfo typeInfo, boolean flus private void writePackage(TypeInfo typeInfo, JfrChunkWriter writer, PackageInfoRaw packageInfoRaw, boolean flushpoint) { assert packageInfoRaw.getHash() != 0; writer.writeCompressedLong(packageInfoRaw.getId()); // id - // Packages with the same name use the same buffer for the whole epoch so it's fine to use that address as the symbol repo hash. No further need to deduplicate. - writer.writeCompressedLong(getSymbolId(writer, packageInfoRaw.getModifiedUTF8Name(), packageInfoRaw.getNameLength(), UninterruptibleUtils.Long.hashCode(packageInfoRaw.getModifiedUTF8Name().rawValue()), flushpoint)); + // Packages with the same name use the same buffer for the whole epoch so it's fine to use + // that address as the symbol repo hash. No further need to deduplicate. + writer.writeCompressedLong(getSymbolId(writer, packageInfoRaw.getModifiedUTF8Name(), packageInfoRaw.getNameLength(), + UninterruptibleUtils.Long.hashCode(packageInfoRaw.getModifiedUTF8Name().rawValue()), flushpoint)); writer.writeCompressedLong(getModuleId(typeInfo, packageInfoRaw.getModuleName(), packageInfoRaw.getHasModule())); writer.writeBoolean(false); // exported } @@ -370,14 +377,17 @@ private boolean addPackage(TypeInfo typeInfo, Class clazz) { packageInfoRaw.setName(null); // No allocation free way to get the name String. setPackageNameAndLength(clazz, packageInfoRaw); - /* The empty/null package represented by "" is always traced with id 0. - The id 0 is reserved and does not need to be serialized. */ + /* + * The empty/null package represented by "" is always traced with id 0. The id 0 is reserved + * and does not need to be serialized. + */ if (packageInfoRaw.getNameLength().equal(0)) { return false; } packageInfoRaw.setHash(getHash(packageInfoRaw)); if (isPackageVisited(typeInfo, packageInfoRaw)) { - assert moduleName == (flushedPackages.contains(packageInfoRaw) ? ((PackageInfoRaw)flushedPackages.get(packageInfoRaw)).getModuleName() : ((PackageInfoRaw)typeInfo.packages.get(packageInfoRaw)).getModuleName()); + assert moduleName == (flushedPackages.contains(packageInfoRaw) ? ((PackageInfoRaw) flushedPackages.get(packageInfoRaw)).getModuleName() + : ((PackageInfoRaw) typeInfo.packages.get(packageInfoRaw)).getModuleName()); NullableNativeMemory.free(packageInfoRaw.getModifiedUTF8Name()); return false; } @@ -386,7 +396,8 @@ private boolean addPackage(TypeInfo typeInfo, Class clazz) { packageInfoRaw.setHasModule(hasModule); packageInfoRaw.setModuleName(moduleName); assert !typeInfo.packages.contains(packageInfoRaw); // *** remove later - typeInfo.packages.putNew(packageInfoRaw); // *** Do not free the buffer. A pointer to it is shallow copied into the hash map. + typeInfo.packages.putNew(packageInfoRaw); // *** Do not free the buffer. A pointer to it is + // shallow copied into the hash map. assert typeInfo.packages.contains(packageInfoRaw); return true; } @@ -408,7 +419,7 @@ private long getPackageId(TypeInfo typeInfo, PackageInfoRaw packageInfoRaw) { } private boolean addModule(TypeInfo typeInfo, Module module) { - ModuleInfoRaw moduleInfoRaw = StackValue.get(ModuleInfoRaw.class); + ModuleInfoRaw moduleInfoRaw = StackValue.get(ModuleInfoRaw.class); moduleInfoRaw.setName(module.getName()); moduleInfoRaw.setHash(getHash(module.getName())); if (isModuleVisited(typeInfo, moduleInfoRaw)) { @@ -427,7 +438,7 @@ private boolean isModuleVisited(TypeInfo typeInfo, ModuleInfoRaw moduleInfoRaw) private long getModuleId(TypeInfo typeInfo, String moduleName, boolean hasModule) { if (hasModule) { - ModuleInfoRaw moduleInfoRaw = StackValue.get(ModuleInfoRaw.class); + ModuleInfoRaw moduleInfoRaw = StackValue.get(ModuleInfoRaw.class); moduleInfoRaw.setName(moduleName); moduleInfoRaw.setHash(getHash(moduleName)); if (flushedModules.contains(moduleInfoRaw)) { @@ -440,7 +451,7 @@ private long getModuleId(TypeInfo typeInfo, String moduleName, boolean hasModule } private boolean addClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { - ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); + ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); if (classLoader == null) { classLoaderInfoRaw.setName(BOOTSTRAP_NAME); } else { @@ -471,7 +482,7 @@ private boolean isClassLoaderVisited(TypeInfo typeInfo, ClassLoaderInfoRaw class private long getClassLoaderId(TypeInfo typeInfo, String classLoaderName, boolean hasClassLoader) { if (hasClassLoader) { - ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); + ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); classLoaderInfoRaw.setName(classLoaderName); classLoaderInfoRaw.setHash(getHash(classLoaderName)); if (flushedClassLoaders.contains(classLoaderInfoRaw)) { @@ -499,6 +510,7 @@ private final class TypeInfo { final JfrPackageInfoTable packages = new JfrPackageInfoTable(); final JfrModuleInfoTable modules = new JfrModuleInfoTable(); final JfrClassLoaderInfoTable classLoaders = new JfrClassLoaderInfoTable(); + void reset() { classes.clear(); packages.clear(); @@ -514,7 +526,10 @@ void teardown() { } } - /** This method sets the package name and length. packageInfoRaw may be on the stack or native memory.*/ + /** + * This method sets the package name and length. packageInfoRaw may be on the stack or native + * memory. + */ private void setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoRaw) { DynamicHub hub = DynamicHub.fromClass(clazz); if (!LayoutEncoding.isHybrid(hub.getLayoutEncoding())) { @@ -523,8 +538,10 @@ private void setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoR } } - /* Primitives have the null package, which technically has the name "java.lang", - but JFR still assigns these the reserved 0 id. */ + /* + * Primitives have the null package, which technically has the name "java.lang", but JFR + * still assigns these the reserved 0 id. + */ if (hub.isPrimitive()) { packageInfoRaw.setModifiedUTF8Name(WordFactory.nullPointer()); packageInfoRaw.setNameLength(WordFactory.unsigned(0)); @@ -550,7 +567,8 @@ private void setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoR assert buffer.add(dot).belowOrEqual(bufferEnd); - // Since we're serializing now, we must do replacements here, instead of the symbol repository. + // Since we're serializing now, we must do replacements here, instead of the symbol + // repository. Pointer packageNameEnd = UninterruptibleUtils.String.toModifiedUTF8(str, dot, buffer, bufferEnd, false, dotWithSlash); packageInfoRaw.setModifiedUTF8Name(buffer); @@ -564,21 +582,23 @@ private void setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoR @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) private static int getHash(String imageHeapString) { // It's possible the type exists, but has no name. - if (imageHeapString == null){ + if (imageHeapString == null) { return 0; } long rawPointerValue = Word.objectToUntrackedPointer(imageHeapString).rawValue(); return UninterruptibleUtils.Long.hashCode(rawPointerValue); } - /** We do not have access to an image heap String for packages. - * Instead, sum the value of the serialized String and use that for the hash.*/ + /** + * We do not have access to an image heap String for packages. Instead, sum the value of the + * serialized String and use that for the hash. + */ private static int getHash(PackageInfoRaw packageInfoRaw) { if (packageInfoRaw.getModifiedUTF8Name().isNull() || packageInfoRaw.getNameLength().belowOrEqual(0)) { return 0; } long sum = 0; - for (int i = 0; packageInfoRaw.getNameLength().aboveThan(i); i++){ + for (int i = 0; packageInfoRaw.getNameLength().aboveThan(i); i++) { sum += ((Pointer) packageInfoRaw.getModifiedUTF8Name()).readByte(i); } return UninterruptibleUtils.Long.hashCode(sum); @@ -589,21 +609,24 @@ private interface JfrTypeInfo extends UninterruptibleEntry { @PinnedObjectField @RawField void setName(String value); + @PinnedObjectField @RawField String getName(); + @RawField void setId(long value); + @RawField long getId(); } - @RawStructure private interface ClassInfoRaw extends JfrTypeInfo { @PinnedObjectField @RawField void setInstance(Class value); + @PinnedObjectField @RawField Class getInstance(); @@ -614,19 +637,26 @@ private interface PackageInfoRaw extends JfrTypeInfo { @PinnedObjectField @RawField void setModuleName(String value); + @PinnedObjectField @RawField String getModuleName(); + @RawField void setHasModule(boolean value); + @RawField boolean getHasModule(); + @RawField void setNameLength(UnsignedWord value); + @RawField UnsignedWord getNameLength(); + @RawField void setModifiedUTF8Name(PointerBase value); + @RawField PointerBase getModifiedUTF8Name(); } @@ -636,11 +666,15 @@ private interface ModuleInfoRaw extends JfrTypeInfo { @PinnedObjectField @RawField void setClassLoaderName(String value); + @PinnedObjectField @RawField String getClassLoaderName(); + @RawField - void setHasClassLoader(boolean value); // Needed because CL name may be empty or null, even if CL is non-null + void setHasClassLoader(boolean value); // Needed because CL name may be empty or null, even + // if CL is non-null + @RawField boolean getHasClassLoader(); } @@ -649,12 +683,13 @@ private interface ModuleInfoRaw extends JfrTypeInfo { private interface ClassLoaderInfoRaw extends JfrTypeInfo { @RawField void setClassTraceId(long value); + @RawField long getClassTraceId(); } private abstract class JfrTypeInfoTable extends AbstractUninterruptibleHashtable { - public JfrTypeInfoTable(NmtCategory nmtCategory) { + JfrTypeInfoTable(NmtCategory nmtCategory) { super(nmtCategory); } @@ -683,11 +718,13 @@ private final class JfrClassInfoTable extends JfrTypeInfoTable { public JfrClassInfoTable() { super(NmtCategory.JFR); } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) protected ClassInfoRaw[] createTable(int size) { return new ClassInfoRaw[size]; } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { @@ -705,7 +742,7 @@ protected UninterruptibleEntry copyToHeap(UninterruptibleEntry visitedOnStack) { private final class JfrPackageInfoTable extends JfrTypeInfoTable { @Platforms(Platform.HOSTED_ONLY.class) - public JfrPackageInfoTable() { + JfrPackageInfoTable() { super(NmtCategory.JFR); } @@ -720,6 +757,7 @@ protected PackageInfoRaw[] createTable(int size) { protected UninterruptibleEntry copyToHeap(UninterruptibleEntry visitedOnStack) { return copyToHeap(visitedOnStack, SizeOf.unsigned(PackageInfoRaw.class)); } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { @@ -764,9 +802,10 @@ public void putAll(JfrPackageInfoTable sourceTable) { private final class JfrModuleInfoTable extends JfrTypeInfoTable { @Platforms(Platform.HOSTED_ONLY.class) - public JfrModuleInfoTable() { + JfrModuleInfoTable() { super(NmtCategory.JFR); } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) protected ModuleInfoRaw[] createTable(int size) { @@ -782,9 +821,10 @@ protected UninterruptibleEntry copyToHeap(UninterruptibleEntry visitedOnStack) { private final class JfrClassLoaderInfoTable extends JfrTypeInfoTable { @Platforms(Platform.HOSTED_ONLY.class) - public JfrClassLoaderInfoTable() { + JfrClassLoaderInfoTable() { super(NmtCategory.JFR); } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) protected ClassLoaderInfoRaw[] createTable(int size) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index d94a3e5cf971..3384dccffe3f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.List; +import com.oracle.svm.core.os.RawFileOperationSupport; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platform; @@ -750,7 +751,15 @@ public void vmErrorRotation() { JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); try { boolean existingFile = chunkWriter.hasOpenFile(); - if (existingFile) { + if (!existingFile) { + Log.log().string("no existing chunk file.").newline(); + // If no chunkfile is open, create one. + // TODO when would a file not already be open? must be open for flushes. + // RawFileOperationSupport.RawFileDescriptor fd = JfrEmergencyDumpSupport.singleton().chunkPath(); + // chunkWriter.openFile(fd); + // chunkWriter.closeFileForEmergencyDump(); + } else { + assert chunkWriter.hasOpenFile(); chunkWriter.closeFileForEmergencyDump(); } JfrEmergencyDumpSupport.singleton().onVmError(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/oldobject/JfrOldObjectRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/oldobject/JfrOldObjectRepository.java index 0c7b0871e470..3177ceb6a059 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/oldobject/JfrOldObjectRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/oldobject/JfrOldObjectRepository.java @@ -45,7 +45,6 @@ import com.oracle.svm.core.jfr.JfrNativeEventWriterDataAccess; import com.oracle.svm.core.jfr.JfrRepository; import com.oracle.svm.core.jfr.JfrType; -import com.oracle.svm.core.jfr.SubstrateJVM; import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.locks.VMMutex; diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestGrowableWordArrayQuickSort.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestGrowableWordArrayQuickSort.java new file mode 100644 index 000000000000..1ebf7d07d1cb --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestGrowableWordArrayQuickSort.java @@ -0,0 +1,145 @@ +///* +// * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. +// * Copyright (c) 2021, 2022, 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 com.oracle.svm.test.jfr.events.ClassEvent; +//import jdk.jfr.Recording; +//import jdk.jfr.consumer.RecordedEvent; +//import org.junit.Test; +// +//import java.util.List; +// +//import com.oracle.svm.core.SubstrateUtil; +//import com.oracle.svm.core.headers.LibC; +//import com.oracle.svm.core.nmt.NmtCategory; +//import com.oracle.svm.core.posix.headers.Dirent; +//import com.oracle.svm.core.posix.headers.Errno; +//import com.oracle.svm.core.posix.headers.Fcntl; +//import com.oracle.svm.core.posix.headers.Unistd; +//import org.graalvm.word.Pointer; +//import jdk.graal.compiler.word.Word; +//import org.graalvm.word.WordFactory; +////import jdk.jfr.internal.LogLevel; +////import jdk.jfr.internal.LogTag; +////import jdk.jfr.internal.Logger; +//import org.graalvm.nativeimage.c.type.CCharPointer; +//import org.graalvm.nativeimage.ImageSingletons; +//import org.graalvm.nativeimage.Platform; +//import org.graalvm.nativeimage.Platforms; +//import org.graalvm.nativeimage.StackValue; +// +//import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +//import com.oracle.svm.core.Uninterruptible; +//import com.oracle.svm.core.jfr.JfrEmergencyDumpFeature; +//import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; +//import com.oracle.svm.core.memory.NativeMemory; +//import com.oracle.svm.core.memory.NullableNativeMemory; +//import com.oracle.svm.core.os.RawFileOperationSupport; +//import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; +//import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; +//import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; +//import com.oracle.svm.core.util.BasedOnJDKFile; +//import com.oracle.svm.core.collections.GrowableWordArray; +//import com.oracle.svm.core.collections.GrowableWordArrayAccess; +// +//import jdk.graal.compiler.api.replacements.Fold; +// +//import java.nio.charset.StandardCharsets; +// +//import static com.oracle.svm.core.posix.headers.Fcntl.O_NOFOLLOW; +//import static com.oracle.svm.core.posix.headers.Fcntl.O_RDONLY; +// +//import static org.junit.Assert.assertEquals; +// +//public class TestGrowableWordArrayQuickSort { +// @Test +// public void test() throws Throwable { +// GrowableWordArray gwa = StackValue.get(GrowableWordArray.class); +// GrowableWordArrayAccess.initialize(gwa); +// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(3), NmtCategory.JFR); +// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(1), NmtCategory.JFR); +// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(5), NmtCategory.JFR); +// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(4), NmtCategory.JFR); +// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(2), NmtCategory.JFR); +// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(4), NmtCategory.JFR); +// qsort(gwa, 0,5); +// for (int i=0; i < 6; i ++){ +// System.out.println(GrowableWordArrayAccess.read(gwa, i).rawValue()); +// } +// } +// +// private static void qsort(GrowableWordArray gwa, int low, int high) { +// if (low < high) { +// int pivotIndex = partition(gwa, low, high); +// +// qsort(gwa, low, pivotIndex - 1); +// qsort(gwa, pivotIndex + 1, high); +// } +// } +// +// private static int partition(GrowableWordArray gwa, int low, int high) { +// // Choose the last element as pivot +// CCharPointer pivot = GrowableWordArrayAccess.read(gwa, high); +// +// // Pointer for the greater element +// int i = low - 1; +// +// // Traverse through all elements +// // If element is smaller than or equal to pivot, swap it +// for (int j = low; j < high; j++) { +// if (compare(GrowableWordArrayAccess.read(gwa, j), pivot)) { +// i++; +// +// // Swap elements at i and j +// CCharPointer temp = GrowableWordArrayAccess.read(gwa, i); +// GrowableWordArrayAccess.write(gwa,i, GrowableWordArrayAccess.read(gwa, j)); +// GrowableWordArrayAccess.write(gwa, j, temp); +// } +// } +// +// // Swap the pivot element with the element at i+1 +// CCharPointer temp = GrowableWordArrayAccess.read(gwa, i + 1); +// GrowableWordArrayAccess.write(gwa, i + 1, GrowableWordArrayAccess.read(gwa, high)); +// GrowableWordArrayAccess.write(gwa, high, temp); +// +// // Return the partition index +// return i + 1; +// } +// +//// static boolean compare(CCharPointer a, CCharPointer b) { +//// // Use strcmp(CCharPointer s1, CCharPointer s2) and strchr(CCharPointer str, int c) +//// +//// return a.read() <= b.read(); +//// } +// +// static boolean compare(CCharPointer a, CCharPointer b) { +// // Use strcmp(CCharPointer s1, CCharPointer s2) and strchr(CCharPointer str, int c) +// +// return a.rawValue() <= b.rawValue(); +// } +// +//} From 30b036a84f6fac57d5cabdf8c582e9409ffc6f77 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Wed, 25 Jun 2025 11:23:19 -0400 Subject: [PATCH 10/16] open emergency dump chunk --- .../jfr/PosixJfrEmergencyDumpSupport.java | 59 ++++++++----------- .../com/oracle/svm/core/jfr/SubstrateJVM.java | 15 ++--- 2 files changed, 32 insertions(+), 42 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java index c3f24856f500..ebeb71f89275 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -73,6 +73,8 @@ public class PosixJfrEmergencyDumpSupport implements com.oracle.svm.core.jfr.Jfr private static int ISO8601_LEN = 19; private static final byte FILE_SEPARATOR = "/".getBytes(StandardCharsets.UTF_8)[0]; private static final byte DOT = ".".getBytes(StandardCharsets.UTF_8)[0]; + // It does not really matter what the name is. + private static final byte[] EMERGENCY_CHUNK_BYTES = "emergency_chunk".getBytes(StandardCharsets.UTF_8); private static final byte[] DUMP_FILE_PREFIX = "hs_oom_pid_".getBytes(StandardCharsets.UTF_8); private static final byte[] CHUNKFILE_EXTENSION_BYTES = ".jfr".getBytes(StandardCharsets.UTF_8); private Dirent.DIR directory; @@ -109,30 +111,29 @@ public String getDumpPath() { // Either use create and use the dumpfile itself, or create a new file in the repository // location. + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+3/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L433-L445") public RawFileDescriptor chunkPath() { if (repositoryLocationBytes == null) { if (!openEmergencyDumpFile()) { return WordFactory.nullPointer(); } - // We can directly use the emergency dump file name as the chunk. + // We can directly use the emergency dump file name as the new chunk since there are no other chunk files. return emergencyFd; } + Log.log().string("Creating a new emergency chunk file in the JFR disk repository").newline(); return createEmergencyChunkPath(); } + /** The normal chunkfile name format is: repository path + file separator + date time + extension. + * In this case we just use a hardcoded string instead of date time, which will successfully rank last in lexographic order among other chunkfile names.*/ + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+3/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L418-L431") private RawFileDescriptor createEmergencyChunkPath() { - int idx = 0; clearPathBuffer(); - for (int i = 0; i < repositoryLocationBytes.length; i++) { - getPathBuffer().write(idx++, repositoryLocationBytes[i]); - } + int idx = 0; + idx = writeToPathBuffer(repositoryLocationBytes, idx); getPathBuffer().write(idx++, FILE_SEPARATOR); - - for (int i = 0; i < CHUNKFILE_EXTENSION_BYTES.length; i++) { - getPathBuffer().write(idx++, CHUNKFILE_EXTENSION_BYTES[i]); - } - // TODO what about date time? Need that for sorting. - // repository path + file separator + date time + extension + idx = writeToPathBuffer(EMERGENCY_CHUNK_BYTES, idx); + writeToPathBuffer(CHUNKFILE_EXTENSION_BYTES, idx); return getFileSupport().create(getPathBuffer(), FileCreationMode.CREATE, FileAccessMode.READ_WRITE); } @@ -171,32 +172,21 @@ private boolean openEmergencyDumpFile() { @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L110-L129") private CCharPointer createEmergencyDumpPath() { int idx = 0; - clearPathBuffer(); if (dumpPathBytes != null) { - for (int i = 0; i < dumpPathBytes.length; i++) { - getPathBuffer().write(idx++, dumpPathBytes[i]); - } + idx = writeToPathBuffer(dumpPathBytes, idx); // Add delimiter getPathBuffer().write(idx++, FILE_SEPARATOR); } - for (int i = 0; i < DUMP_FILE_PREFIX.length; i++) { - getPathBuffer().write(idx++, DUMP_FILE_PREFIX[i]); - } - - for (int i = 0; i < pidBytes.length; i++) { - getPathBuffer().write(idx++, pidBytes[i]); - } - - for (int i = 0; i < CHUNKFILE_EXTENSION_BYTES.length; i++) { - getPathBuffer().write(idx++, CHUNKFILE_EXTENSION_BYTES[i]); - } - + idx = writeToPathBuffer(DUMP_FILE_PREFIX, idx); + idx = writeToPathBuffer(pidBytes, idx); + writeToPathBuffer(CHUNKFILE_EXTENSION_BYTES, idx); return getPathBuffer(); } + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+3/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L310-L345") private GrowableWordArray iterateRepository(GrowableWordArray gwa) { int count = 0; // Open directory @@ -307,9 +297,7 @@ private boolean openDirectory() { private CCharPointer getRepositoryLocation() { clearPathBuffer(); - for (int i = 0; i < repositoryLocationBytes.length; i++) { - getPathBuffer().write(i, repositoryLocationBytes[i]); - } + writeToPathBuffer(repositoryLocationBytes,0); return getPathBuffer(); } @@ -374,9 +362,7 @@ private CCharPointer fullyQualified(CCharPointer fn) { // TODO HS uses _path_buffer_file_name_offset to avoid building this part of th path each // time. // Cached in RepositoryIterator::RepositoryIterator and used in fully_qualified - for (int i = 0; i < repositoryLocationBytes.length; i++) { - getPathBuffer().write(idx++, repositoryLocationBytes[i]); - } + idx = writeToPathBuffer(repositoryLocationBytes, idx); // Add delimiter getPathBuffer().write(idx++, FILE_SEPARATOR); @@ -396,6 +382,13 @@ private void clearPathBuffer() { LibC.memset(getPathBuffer(), Word.signed(0), Word.unsigned(JVM_MAXPATHLEN)); } + private int writeToPathBuffer(byte[] bytes, int idx){ + for (int i = 0; i < bytes.length; i++) { + getPathBuffer().write(idx++, bytes[i]); + } + return idx; + } + @Fold static RawFileOperationSupport getFileSupport() { return RawFileOperationSupport.bigEndian(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index 3384dccffe3f..cca41c12b894 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -752,16 +752,13 @@ public void vmErrorRotation() { try { boolean existingFile = chunkWriter.hasOpenFile(); if (!existingFile) { - Log.log().string("no existing chunk file.").newline(); - // If no chunkfile is open, create one. - // TODO when would a file not already be open? must be open for flushes. - // RawFileOperationSupport.RawFileDescriptor fd = JfrEmergencyDumpSupport.singleton().chunkPath(); - // chunkWriter.openFile(fd); - // chunkWriter.closeFileForEmergencyDump(); - } else { - assert chunkWriter.hasOpenFile(); - chunkWriter.closeFileForEmergencyDump(); + // If no chunkfile is open, create one. This case is very unlikely. + Log.log().string("No existing chunk file. Creating one.").newline(); + RawFileOperationSupport.RawFileDescriptor fd = JfrEmergencyDumpSupport.singleton().chunkPath(); + chunkWriter.openFile(fd); } + assert chunkWriter.hasOpenFile(); + chunkWriter.closeFileForEmergencyDump(); JfrEmergencyDumpSupport.singleton().onVmError(); } finally { chunkWriter.unlock(); From a83159945ce3fc28bac708cbff2e1c353df7cbed Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Wed, 25 Jun 2025 15:12:26 -0400 Subject: [PATCH 11/16] Do markChunkFinal and patch header like regular rotatios. Add tests. --- .../jfr/PosixJfrEmergencyDumpSupport.java | 18 +- .../svm/core/jfr/JfrChunkFileWriter.java | 3 +- .../com/oracle/svm/core/jfr/SubstrateJVM.java | 3 +- .../svm/test/jfr/TestEmergencyDump.java | 97 ++++++++ .../jfr/TestGrowableWordArrayQuickSort.java | 216 ++++++------------ 5 files changed, 182 insertions(+), 155 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java index ebeb71f89275..3980811de007 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -117,15 +117,19 @@ public RawFileDescriptor chunkPath() { if (!openEmergencyDumpFile()) { return WordFactory.nullPointer(); } - // We can directly use the emergency dump file name as the new chunk since there are no other chunk files. + // We can directly use the emergency dump file name as the new chunk since there are no + // other chunk files. return emergencyFd; } Log.log().string("Creating a new emergency chunk file in the JFR disk repository").newline(); return createEmergencyChunkPath(); } - /** The normal chunkfile name format is: repository path + file separator + date time + extension. - * In this case we just use a hardcoded string instead of date time, which will successfully rank last in lexographic order among other chunkfile names.*/ + /** + * The normal chunkfile name format is: repository path + file separator + date time + + * extension. In this case we just use a hardcoded string instead of date time, which will + * successfully rank last in lexographic order among other chunkfile names. + */ @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+3/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L418-L431") private RawFileDescriptor createEmergencyChunkPath() { clearPathBuffer(); @@ -297,7 +301,7 @@ private boolean openDirectory() { private CCharPointer getRepositoryLocation() { clearPathBuffer(); - writeToPathBuffer(repositoryLocationBytes,0); + writeToPathBuffer(repositoryLocationBytes, 0); return getPathBuffer(); } @@ -382,7 +386,7 @@ private void clearPathBuffer() { LibC.memset(getPathBuffer(), Word.signed(0), Word.unsigned(JVM_MAXPATHLEN)); } - private int writeToPathBuffer(byte[] bytes, int idx){ + private int writeToPathBuffer(byte[] bytes, int idx) { for (int i = 0; i < bytes.length; i++) { getPathBuffer().write(idx++, bytes[i]); } @@ -420,8 +424,6 @@ class PosixJfrEmergencyDumpFeature extends JfrEmergencyDumpFeature { @Override public void afterRegistration(AfterRegistrationAccess access) { - PosixJfrEmergencyDumpSupport support = new PosixJfrEmergencyDumpSupport(); - ImageSingletons.add(JfrEmergencyDumpSupport.class, support); - ImageSingletons.add(PosixJfrEmergencyDumpSupport.class, support); + ImageSingletons.add(JfrEmergencyDumpSupport.class, new PosixJfrEmergencyDumpSupport()); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java index d72ad78e3d87..2f45494d9a2e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java @@ -275,7 +275,8 @@ public void closeFileForEmergencyDump() { writeThreadCheckpoint(true); writeFlushCheckpoint(true); writeMetadataEvent(); - patchFileHeader(true); + // Header must be marked COMPLETE, unlike at flushpoints. + patchFileHeader(false); getFileSupport().close(fd); filename = null; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index cca41c12b894..03e208a8d985 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -601,7 +601,7 @@ public void setDumpPath(String dumpPathText) { */ public String getDumpPath() { if (JfrEmergencyDumpSupport.singleton().getDumpPath() == null) { - JfrEmergencyDumpSupport.singleton().setDumpPath(Target_jdk_jfr_internal_util_Utils.getPathInProperty("user.home", null).toString()); + JfrEmergencyDumpSupport.singleton().setDumpPath(Target_jdk_jfr_internal_util_Utils.getPathInProperty("user.home", null).toString()); } return JfrEmergencyDumpSupport.singleton().getDumpPath(); } @@ -758,6 +758,7 @@ public void vmErrorRotation() { chunkWriter.openFile(fd); } assert chunkWriter.hasOpenFile(); + chunkWriter.markChunkFinal(); chunkWriter.closeFileForEmergencyDump(); JfrEmergencyDumpSupport.singleton().onVmError(); } finally { diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java new file mode 100644 index 000000000000..8dc9c6cda6f3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2022, 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 com.oracle.svm.test.jfr.events.StringEvent; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static com.oracle.svm.test.jfr.AbstractJfrTest.getEvents; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.oracle.svm.core.jfr.SubstrateJVM; + +/** + * This test commits events across multiple chunk files and ensure that the events all appear in the + * emergency dump. This would indicate that the chunk files from the disk repository we merged + * correctly along with in-flight data. + */ +public class TestEmergencyDump extends JfrRecordingTest { + @Test + public void test() throws Throwable { + List expectedStrings = new ArrayList(); + expectedStrings.add("first"); + expectedStrings.add("second"); + expectedStrings.add("third"); + + String[] testedEvents = new String[]{"com.jfr.String"}; + Recording recording = startRecording(testedEvents); + + // This event will be in chunk #1 in disk repository. + StringEvent e1 = new StringEvent(); + e1.message = expectedStrings.get(0); + e1.commit(); + + // Invoke chunk rotation. + recording.dump(createTempJfrFile()); + + // This event will be in chunk #2 in disk repository. + StringEvent e2 = new StringEvent(); + e2.message = expectedStrings.get(1); + e2.commit(); + + // Invoke chunk rotation. + recording.dump(createTempJfrFile()); + + // This event will be in in-flight and should be flushed upon emergency dump. + StringEvent e3 = new StringEvent(); + e3.message = expectedStrings.get(2); + e3.commit(); + + SubstrateJVM.get().vmErrorRotation(); + recording.stop(); + recording.close(); + + String dumpFile = "hs_oom_pid_" + ProcessHandle.current().pid() + ".jfr"; + Path p = Path.of(dumpFile); + assertTrue("emergency dump file does not exist.", Files.exists(p)); + List events = getEvents(Path.of(dumpFile), testedEvents, true); + for (RecordedEvent event : events) { + assertTrue(expectedStrings.remove(event.getString("message"))); + } + assertEquals(0, expectedStrings.size()); + + Files.deleteIfExists(p); + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestGrowableWordArrayQuickSort.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestGrowableWordArrayQuickSort.java index 1ebf7d07d1cb..f40917f98f40 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestGrowableWordArrayQuickSort.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestGrowableWordArrayQuickSort.java @@ -1,145 +1,71 @@ -///* -// * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. -// * Copyright (c) 2021, 2022, 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 com.oracle.svm.test.jfr.events.ClassEvent; -//import jdk.jfr.Recording; -//import jdk.jfr.consumer.RecordedEvent; -//import org.junit.Test; -// -//import java.util.List; -// -//import com.oracle.svm.core.SubstrateUtil; -//import com.oracle.svm.core.headers.LibC; -//import com.oracle.svm.core.nmt.NmtCategory; -//import com.oracle.svm.core.posix.headers.Dirent; -//import com.oracle.svm.core.posix.headers.Errno; -//import com.oracle.svm.core.posix.headers.Fcntl; -//import com.oracle.svm.core.posix.headers.Unistd; -//import org.graalvm.word.Pointer; -//import jdk.graal.compiler.word.Word; -//import org.graalvm.word.WordFactory; -////import jdk.jfr.internal.LogLevel; -////import jdk.jfr.internal.LogTag; -////import jdk.jfr.internal.Logger; -//import org.graalvm.nativeimage.c.type.CCharPointer; -//import org.graalvm.nativeimage.ImageSingletons; -//import org.graalvm.nativeimage.Platform; -//import org.graalvm.nativeimage.Platforms; -//import org.graalvm.nativeimage.StackValue; -// -//import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; -//import com.oracle.svm.core.Uninterruptible; -//import com.oracle.svm.core.jfr.JfrEmergencyDumpFeature; -//import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; -//import com.oracle.svm.core.memory.NativeMemory; -//import com.oracle.svm.core.memory.NullableNativeMemory; -//import com.oracle.svm.core.os.RawFileOperationSupport; -//import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; -//import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; -//import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; -//import com.oracle.svm.core.util.BasedOnJDKFile; -//import com.oracle.svm.core.collections.GrowableWordArray; -//import com.oracle.svm.core.collections.GrowableWordArrayAccess; -// -//import jdk.graal.compiler.api.replacements.Fold; -// -//import java.nio.charset.StandardCharsets; -// -//import static com.oracle.svm.core.posix.headers.Fcntl.O_NOFOLLOW; -//import static com.oracle.svm.core.posix.headers.Fcntl.O_RDONLY; -// -//import static org.junit.Assert.assertEquals; -// -//public class TestGrowableWordArrayQuickSort { -// @Test -// public void test() throws Throwable { -// GrowableWordArray gwa = StackValue.get(GrowableWordArray.class); -// GrowableWordArrayAccess.initialize(gwa); -// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(3), NmtCategory.JFR); -// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(1), NmtCategory.JFR); -// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(5), NmtCategory.JFR); -// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(4), NmtCategory.JFR); -// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(2), NmtCategory.JFR); -// GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(4), NmtCategory.JFR); -// qsort(gwa, 0,5); -// for (int i=0; i < 6; i ++){ -// System.out.println(GrowableWordArrayAccess.read(gwa, i).rawValue()); -// } -// } -// -// private static void qsort(GrowableWordArray gwa, int low, int high) { -// if (low < high) { -// int pivotIndex = partition(gwa, low, high); -// -// qsort(gwa, low, pivotIndex - 1); -// qsort(gwa, pivotIndex + 1, high); -// } -// } -// -// private static int partition(GrowableWordArray gwa, int low, int high) { -// // Choose the last element as pivot -// CCharPointer pivot = GrowableWordArrayAccess.read(gwa, high); -// -// // Pointer for the greater element -// int i = low - 1; -// -// // Traverse through all elements -// // If element is smaller than or equal to pivot, swap it -// for (int j = low; j < high; j++) { -// if (compare(GrowableWordArrayAccess.read(gwa, j), pivot)) { -// i++; -// -// // Swap elements at i and j -// CCharPointer temp = GrowableWordArrayAccess.read(gwa, i); -// GrowableWordArrayAccess.write(gwa,i, GrowableWordArrayAccess.read(gwa, j)); -// GrowableWordArrayAccess.write(gwa, j, temp); -// } -// } -// -// // Swap the pivot element with the element at i+1 -// CCharPointer temp = GrowableWordArrayAccess.read(gwa, i + 1); -// GrowableWordArrayAccess.write(gwa, i + 1, GrowableWordArrayAccess.read(gwa, high)); -// GrowableWordArrayAccess.write(gwa, high, temp); -// -// // Return the partition index -// return i + 1; -// } -// -//// static boolean compare(CCharPointer a, CCharPointer b) { -//// // Use strcmp(CCharPointer s1, CCharPointer s2) and strchr(CCharPointer str, int c) -//// -//// return a.read() <= b.read(); -//// } -// -// static boolean compare(CCharPointer a, CCharPointer b) { -// // Use strcmp(CCharPointer s1, CCharPointer s2) and strchr(CCharPointer str, int c) -// -// return a.rawValue() <= b.rawValue(); -// } -// -//} +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2025, 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 org.junit.Test; + +import com.oracle.svm.core.nmt.NmtCategory; +import jdk.graal.compiler.word.Word; +import org.graalvm.word.WordFactory; +import org.graalvm.nativeimage.StackValue; + +import com.oracle.svm.core.collections.GrowableWordArray; +import com.oracle.svm.core.collections.GrowableWordArrayAccess; + +import static org.junit.Assert.assertTrue; + +public class TestGrowableWordArrayQuickSort { + @Test + public void test() throws Throwable { + GrowableWordArray gwa = StackValue.get(GrowableWordArray.class); + GrowableWordArrayAccess.initialize(gwa); + GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(3), NmtCategory.JFR); + GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(1), NmtCategory.JFR); + GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(5), NmtCategory.JFR); + GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(4), NmtCategory.JFR); + GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(2), NmtCategory.JFR); + GrowableWordArrayAccess.add(gwa, WordFactory.unsigned(4), NmtCategory.JFR); + GrowableWordArrayAccess.qsort(gwa, 0, gwa.getSize() - 1, TestGrowableWordArrayQuickSort::compare); + long last = 0; + for (int i = 0; i < gwa.getSize(); i++) { + long current = GrowableWordArrayAccess.read(gwa, i).rawValue(); + assertTrue(last <= current); + last = current; + } + } + + static int compare(Word a, Word b) { + if (a.rawValue() < b.rawValue()) { + return -1; + } else if (a.rawValue() == b.rawValue()) { + return 0; + } else { + return 1; + } + } + +} From 533da7a4fffd9edecf6057767eec3f6cb5d786b4 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Fri, 27 Jun 2025 15:23:22 -0400 Subject: [PATCH 12/16] Emit old object sample and jdk.DumpReason events. Fix testEmergencyDump purge in-flight data bug. checkstyle gate fixes cleanup --- .../jfr/PosixJfrEmergencyDumpSupport.java | 38 ++--- .../svm/core/jfr/JfrChunkFileWriter.java | 6 +- .../svm/core/jfr/JfrEmergencyDumpFeature.java | 9 ++ .../svm/core/jfr/JfrEmergencyDumpSupport.java | 27 ++++ .../src/com/oracle/svm/core/jfr/JfrEvent.java | 1 + .../com/oracle/svm/core/jfr/JfrFeature.java | 3 +- .../svm/core/jfr/JfrRecorderThread.java | 1 - .../svm/core/jfr/JfrSymbolRepository.java | 6 +- .../svm/core/jfr/JfrTypeRepository.java | 130 +++++++++--------- .../com/oracle/svm/core/jfr/SubstrateJVM.java | 37 +++-- .../svm/core/jfr/events/DumpReasonEvent.java | 58 ++++++++ .../svm/core/util/PlatformTimeUtils.java | 2 - .../svm/test/jfr/TestEmergencyDump.java | 20 ++- 13 files changed, 227 insertions(+), 111 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/DumpReasonEvent.java diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java index 3980811de007..ce8f626b3121 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -70,7 +70,7 @@ public class PosixJfrEmergencyDumpSupport implements com.oracle.svm.core.jfr.Jfr @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/os/posix/include/jvm_md.h#L57") // private static final int JVM_MAXPATHLEN = 4096; @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L47") // - private static int ISO8601_LEN = 19; + private static final int ISO_8601_LEN = 19; private static final byte FILE_SEPARATOR = "/".getBytes(StandardCharsets.UTF_8)[0]; private static final byte DOT = ".".getBytes(StandardCharsets.UTF_8)[0]; // It does not really matter what the name is. @@ -88,20 +88,24 @@ public class PosixJfrEmergencyDumpSupport implements com.oracle.svm.core.jfr.Jfr public PosixJfrEmergencyDumpSupport() { } + @Override public void initialize() { pidBytes = String.valueOf(ProcessHandle.current().pid()).getBytes(StandardCharsets.UTF_8); pathBuffer = NativeMemory.calloc(JVM_MAXPATHLEN, NmtCategory.JFR); directory = WordFactory.nullPointer(); } + @Override public void setRepositoryLocation(String dirText) { repositoryLocationBytes = dirText.getBytes(StandardCharsets.UTF_8); } + @Override public void setDumpPath(String dumpPathText) { dumpPathBytes = dumpPathText.getBytes(StandardCharsets.UTF_8); } + @Override public String getDumpPath() { if (dumpPathBytes != null) { return new String(dumpPathBytes, StandardCharsets.UTF_8); @@ -109,16 +113,21 @@ public String getDumpPath() { return ""; } - // Either use create and use the dumpfile itself, or create a new file in the repository - // location. + /* + * Either use create and use the dumpfile itself, or create a new file in the repository + * location. + */ + @Override @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+3/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L433-L445") public RawFileDescriptor chunkPath() { if (repositoryLocationBytes == null) { if (!openEmergencyDumpFile()) { return WordFactory.nullPointer(); } - // We can directly use the emergency dump file name as the new chunk since there are no - // other chunk files. + /* + * We can directly use the emergency dump file name as the new chunk since there are no + * other chunk files. + */ return emergencyFd; } Log.log().string("Creating a new emergency chunk file in the JFR disk repository").newline(); @@ -141,6 +150,7 @@ private RawFileDescriptor createEmergencyChunkPath() { return getFileSupport().create(getPathBuffer(), FileCreationMode.CREATE, FileAccessMode.READ_WRITE); } + @Override @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L409-L416") public void onVmError() { Log.log().string("Attempting JFR Emergency Dump").newline(); @@ -213,12 +223,6 @@ private GrowableWordArray iterateRepository(GrowableWordArray gwa) { if (count > 0) { GrowableWordArrayAccess.qsort(gwa, 0, count - 1, PosixJfrEmergencyDumpSupport::compare); } -// for (int i=0; i < count; i ++){ // todo remove -// String name = -// org.graalvm.nativeimage.c.type.CTypeConversion.toJavaString(GrowableWordArrayAccess.read(gwa, -// i)); -// System.out.println("chunk file: "+ name); -// } } return WordFactory.nullPointer(); } @@ -227,7 +231,7 @@ private GrowableWordArray iterateRepository(GrowableWordArray gwa) { static int compare(Word a, Word b) { CCharPointer filenameA = (CCharPointer) a; CCharPointer filenameB = (CCharPointer) b; - int cmp = LibC.strncmp(filenameA, filenameB, WordFactory.unsigned(ISO8601_LEN)); + int cmp = LibC.strncmp(filenameA, filenameB, WordFactory.unsigned(ISO_8601_LEN)); if (cmp == 0) { CCharPointer aDot = SubstrateUtil.strchr(filenameA, DOT); CCharPointer bDot = SubstrateUtil.strchr(filenameB, DOT); @@ -306,8 +310,8 @@ private CCharPointer getRepositoryLocation() { } /** - * See - * {@link com.oracle.svm.core.posix.jvmstat.PosixPerfMemoryProvider#restartableOpen(CCharPointer, int, int)} + * See com.oracle.svm.core.posix.jvmstat.PosixPerfMemoryProvider#restartableOpen(CCharPointer, + * int, int). */ @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") private static int restartableOpen(CCharPointer directory, int flags, int mode) { @@ -363,8 +367,6 @@ private CCharPointer fullyQualified(CCharPointer fn) { clearPathBuffer(); - // TODO HS uses _path_buffer_file_name_offset to avoid building this part of th path each - // time. // Cached in RepositoryIterator::RepositoryIterator and used in fully_qualified idx = writeToPathBuffer(repositoryLocationBytes, idx); @@ -386,7 +388,8 @@ private void clearPathBuffer() { LibC.memset(getPathBuffer(), Word.signed(0), Word.unsigned(JVM_MAXPATHLEN)); } - private int writeToPathBuffer(byte[] bytes, int idx) { + private int writeToPathBuffer(byte[] bytes, int start) { + int idx = start; for (int i = 0; i < bytes.length; i++) { getPathBuffer().write(idx++, bytes[i]); } @@ -412,6 +415,7 @@ private void closeDirectory() { } } + @Override public void teardown() { closeEmergencyDumpFile(); closeDirectory(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java index 2f45494d9a2e..07fd9e6ef044 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java @@ -27,8 +27,6 @@ import static com.oracle.svm.core.jfr.JfrThreadLocal.getJavaBufferList; import static com.oracle.svm.core.jfr.JfrThreadLocal.getNativeBufferList; -import java.nio.charset.StandardCharsets; - import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository; import com.oracle.svm.core.nmt.NmtCategory; import jdk.graal.compiler.word.Word; @@ -172,10 +170,10 @@ public void openFile(String outputFile) { // Used by JFR emergency dump @Override - public void openFile(RawFileDescriptor fd) { + public void openFile(RawFileDescriptor file) { assert lock.isOwner(); filename = null; - this.fd = fd; + fd = file; openFile0(); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java index 207944a631cd..ceb703386fb4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java @@ -29,11 +29,20 @@ import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; +import jdk.graal.compiler.api.replacements.Fold; + +import org.graalvm.nativeimage.ImageSingletons; + /** * The JFR emergency dump mechanism uses platform-specific implementations (see {@link JfrEmergencyDumpSupport}). */ @AutomaticallyRegisteredFeature public class JfrEmergencyDumpFeature implements InternalFeature { + @Fold + static boolean isPresent() { + return ImageSingletons.contains(JfrEmergencyDumpSupport.class); + } + @Override public boolean isInConfiguration(IsInConfigurationAccess access) { return VMInspectionOptions.hasJfrSupport(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java index 7b3d491f23b7..9a4e1abe1f33 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java @@ -1,4 +1,31 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2025, 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; + import com.oracle.svm.core.os.RawFileOperationSupport; import org.graalvm.nativeimage.ImageSingletons; 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 a1cde362cb98..b761c034976f 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 @@ -74,6 +74,7 @@ public final class JfrEvent { public static final JfrEvent ObjectAllocationSample = create("jdk.ObjectAllocationSample", 5, JfrEventFlags.SupportsThrottling); public static final JfrEvent NativeMemoryUsage = create("jdk.NativeMemoryUsage"); public static final JfrEvent NativeMemoryUsageTotal = create("jdk.NativeMemoryUsageTotal"); + public static final JfrEvent DumpReason = create("jdk.DumpReason"); 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 ed06660f6f53..f014258abaef 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 @@ -25,7 +25,6 @@ package com.oracle.svm.core.jfr; import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.graalvm.nativeimage.ImageSingletons; @@ -155,7 +154,7 @@ private static HotSpotDiagnosticMXBean getDiagnosticBean() { @Override public List> getRequiredFeatures() { - return Arrays.asList(ThreadListenerSupportFeature.class, com.oracle.svm.core.jfr.JfrEmergencyDumpFeature.class); + return Arrays.asList(ThreadListenerSupportFeature.class, JfrEmergencyDumpFeature.class); } @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java index 43d531cb2ddd..6e28690d99b6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java @@ -111,7 +111,6 @@ private void work() { } void endRecording() { -// com.oracle.svm.core.jfr.SubstrateJVM.get().vmErrorRotation(); lock.lock(); try { SubstrateJVM.JfrEndRecordingOperation vmOp = new SubstrateJVM.JfrEndRecordingOperation(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java index 40f565c6d2c4..ebeb5cc45581 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java @@ -37,7 +37,6 @@ import org.graalvm.word.WordFactory; import com.oracle.svm.core.Uninterruptible; -import com.oracle.svm.core.c.struct.PinnedObjectField; import com.oracle.svm.core.collections.AbstractUninterruptibleHashtable; import com.oracle.svm.core.collections.UninterruptibleEntry; import com.oracle.svm.core.heap.Heap; @@ -85,7 +84,6 @@ public long getSymbolId(String imageHeapString, boolean previousEpoch, boolean r return 0; } - assert Heap.getHeap().isInImageHeap(""); assert Heap.getHeap().isInImageHeap(imageHeapString); int length = 0; length = UninterruptibleUtils.String.modifiedUTF8Length(imageHeapString, false); @@ -102,7 +100,7 @@ public long getSymbolId(String imageHeapString, boolean previousEpoch, boolean r public long getSymbolId(PointerBase buffer, UnsignedWord length, int hash, boolean previousEpoch) { com.oracle.svm.core.util.VMError.guarantee(buffer.isNonNull()); JfrSymbol symbol = StackValue.get(JfrSymbol.class); - symbol.setModifiedUTF8(buffer); // *** symbol in native memory + symbol.setModifiedUTF8(buffer); // symbol allocated in native memory symbol.setLength(length); symbol.setHash(hash); @@ -134,7 +132,7 @@ public long getSymbolId(PointerBase buffer, UnsignedWord length, int hash, boole JfrNativeEventWriterDataAccess.initialize(data, epochData.buffer); JfrNativeEventWriter.putLong(data, newEntry.getId()); - JfrNativeEventWriter.putString(data, (org.graalvm.word.Pointer) newEntry.getModifiedUTF8(), (int) newEntry.getLength().rawValue()); + JfrNativeEventWriter.putString(data, (Pointer) newEntry.getModifiedUTF8(), (int) newEntry.getLength().rawValue()); if (!JfrNativeEventWriter.commit(data)) { return 0L; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index 8fe675be0cd4..252465cff210 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -64,10 +64,11 @@ public class JfrTypeRepository implements JfrRepository { private final JfrClassLoaderInfoTable flushedClassLoaders; private final TypeInfo typeInfo; - // epochTypeData tables are written to from threads emitting events and read from the - // flushing/rotating thread - // Their purpose is to lazily collect tagged JFR classes, which may later be serialized during - // flush/rotation. + /* + * epochTypeData tables are written to from threads emitting events and read from the + * flushing/rotating thread Their purpose is to lazily collect tagged JFR classes, which may + * later be serialized during flush/rotation. + */ private final JfrClassInfoTable epochTypeData0; private final JfrClassInfoTable epochTypeData1; @@ -119,10 +120,10 @@ public long getClassId(Class clazz) { public int write(JfrChunkWriter writer, boolean flushpoint) { typeInfo.reset(); collectTypeInfo(flushpoint); - int count = writeClasses(writer, typeInfo, flushpoint); - count += writePackages(writer, typeInfo, flushpoint); - count += writeModules(writer, typeInfo, flushpoint); - count += writeClassLoaders(writer, typeInfo, flushpoint); + int count = writeClasses(writer, flushpoint); + count += writePackages(writer, flushpoint); + count += writeModules(writer, flushpoint); + count += writeClassLoaders(writer, flushpoint); if (flushpoint) { flushedClasses.putAll(typeInfo.classes); flushedPackages.putAll(typeInfo.packages); @@ -155,15 +156,17 @@ private void collectTypeInfo(boolean flushpoint) { Class clazz = entry.getInstance(); assert DynamicHub.fromClass(clazz).isLoaded(); if (flushpoint) { - // Still must check the bit is set since we don't clear the epoch data tables - // until safepoint. + /* + * Must check the bit is set since we don't clear the epoch data tables until + * safepoint. + */ if (JfrTraceId.isUsedCurrentEpoch(clazz)) { - visitClass(typeInfo, clazz); + visitClass(clazz); } } else { if (JfrTraceId.isUsedPreviousEpoch(clazz)) { JfrTraceId.clearUsedPreviousEpoch(clazz); - visitClass(typeInfo, clazz); + visitClass(clazz); } } entry = entry.getNext(); @@ -171,37 +174,37 @@ private void collectTypeInfo(boolean flushpoint) { } } - private void visitClass(TypeInfo typeInfo, Class clazz) { - if (clazz != null && addClass(typeInfo, clazz)) { - visitClassLoader(typeInfo, clazz.getClassLoader()); - visitPackage(typeInfo, clazz); - visitClass(typeInfo, clazz.getSuperclass()); + private void visitClass(Class clazz) { + if (clazz != null && addClass(clazz)) { + visitClassLoader(clazz.getClassLoader()); + visitPackage(clazz); + visitClass(clazz.getSuperclass()); } } - private void visitPackage(TypeInfo typeInfo, Class clazz) { - if (addPackage(typeInfo, clazz)) { - visitModule(typeInfo, clazz); + private void visitPackage(Class clazz) { + if (addPackage(clazz)) { + visitModule(clazz); } } - private void visitModule(TypeInfo typeInfo, Class clazz) { + private void visitModule(Class clazz) { Module module = clazz.getModule(); - if (module != null && addModule(typeInfo, module)) { - visitClassLoader(typeInfo, module.getClassLoader()); + if (module != null && addModule(module)) { + visitClassLoader(module.getClassLoader()); } } - private void visitClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { + private void visitClassLoader(ClassLoader classLoader) { // The null class-loader is serialized as the "bootstrap" class-loader. - if (addClassLoader(typeInfo, classLoader)) { + if (addClassLoader(classLoader)) { if (classLoader != null) { - visitClass(typeInfo, classLoader.getClass()); + visitClass(classLoader.getClass()); } } } - private int writeClasses(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { + private int writeClasses(JfrChunkWriter writer, boolean flushpoint) { int size = typeInfo.classes.getSize(); ClassInfoRaw[] table = (ClassInfoRaw[]) typeInfo.classes.getTable(); if (size == 0) { @@ -214,14 +217,14 @@ private int writeClasses(JfrChunkWriter writer, TypeInfo typeInfo, boolean flush for (int i = 0; i < table.length; i++) { ClassInfoRaw entry = table[i]; while (entry.isNonNull()) { - writeClass(typeInfo, writer, entry, flushpoint); + writeClass(writer, entry, flushpoint); entry = entry.getNext(); } } return NON_EMPTY; } - private void writeClass(TypeInfo typeInfo, JfrChunkWriter writer, ClassInfoRaw classInfoRaw, boolean flushpoint) { + private void writeClass(JfrChunkWriter writer, ClassInfoRaw classInfoRaw, boolean flushpoint) { assert classInfoRaw.getHash() != 0; Class clazz = classInfoRaw.getInstance(); PackageInfoRaw packageInfoRaw = StackValue.get(PackageInfoRaw.class); @@ -230,9 +233,9 @@ private void writeClass(TypeInfo typeInfo, JfrChunkWriter writer, ClassInfoRaw c boolean hasClassLoader = clazz.getClassLoader() != null; writer.writeCompressedLong(classInfoRaw.getId()); - writer.writeCompressedLong(getClassLoaderId(typeInfo, hasClassLoader ? clazz.getClassLoader().getName() : null, hasClassLoader)); + writer.writeCompressedLong(getClassLoaderId(hasClassLoader ? clazz.getClassLoader().getName() : null, hasClassLoader)); writer.writeCompressedLong(getSymbolId(writer, clazz.getName(), flushpoint, true)); - writer.writeCompressedLong(getPackageId(typeInfo, packageInfoRaw)); + writer.writeCompressedLong(getPackageId(packageInfoRaw)); writer.writeCompressedLong(clazz.getModifiers()); writer.writeBoolean(clazz.isHidden()); @@ -266,7 +269,7 @@ private static long getSymbolId(JfrChunkWriter writer, PointerBase source, Unsig return SubstrateJVM.getSymbolRepository().getSymbolId(destination, length, hash, !flushpoint); } - private int writePackages(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { + private int writePackages(JfrChunkWriter writer, boolean flushpoint) { int size = typeInfo.packages.getSize(); PackageInfoRaw[] table = (PackageInfoRaw[]) typeInfo.packages.getTable(); if (size == 0) { @@ -278,25 +281,27 @@ private int writePackages(JfrChunkWriter writer, TypeInfo typeInfo, boolean flus for (int i = 0; i < table.length; i++) { PackageInfoRaw packageInfoRaw = table[i]; while (packageInfoRaw.isNonNull()) { - writePackage(typeInfo, writer, packageInfoRaw, flushpoint); + writePackage(writer, packageInfoRaw, flushpoint); packageInfoRaw = packageInfoRaw.getNext(); } } return NON_EMPTY; } - private void writePackage(TypeInfo typeInfo, JfrChunkWriter writer, PackageInfoRaw packageInfoRaw, boolean flushpoint) { + private void writePackage(JfrChunkWriter writer, PackageInfoRaw packageInfoRaw, boolean flushpoint) { assert packageInfoRaw.getHash() != 0; writer.writeCompressedLong(packageInfoRaw.getId()); // id - // Packages with the same name use the same buffer for the whole epoch so it's fine to use - // that address as the symbol repo hash. No further need to deduplicate. + /* + * Packages with the same name use the same buffer for the whole epoch, so it's fine to use + * that address as the symbol repo hash. No further need to deduplicate. + */ writer.writeCompressedLong(getSymbolId(writer, packageInfoRaw.getModifiedUTF8Name(), packageInfoRaw.getNameLength(), UninterruptibleUtils.Long.hashCode(packageInfoRaw.getModifiedUTF8Name().rawValue()), flushpoint)); - writer.writeCompressedLong(getModuleId(typeInfo, packageInfoRaw.getModuleName(), packageInfoRaw.getHasModule())); + writer.writeCompressedLong(getModuleId(packageInfoRaw.getModuleName(), packageInfoRaw.getHasModule())); writer.writeBoolean(false); // exported } - private int writeModules(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { + private int writeModules(JfrChunkWriter writer, boolean flushpoint) { int size = typeInfo.modules.getSize(); ModuleInfoRaw[] table = (ModuleInfoRaw[]) typeInfo.modules.getTable(); if (size == 0) { @@ -308,22 +313,22 @@ private int writeModules(JfrChunkWriter writer, TypeInfo typeInfo, boolean flush for (int i = 0; i < table.length; i++) { ModuleInfoRaw entry = table[i]; while (entry.isNonNull()) { - writeModule(typeInfo, writer, entry, flushpoint); + writeModule(writer, entry, flushpoint); entry = entry.getNext(); } } return NON_EMPTY; } - private void writeModule(TypeInfo typeInfo, JfrChunkWriter writer, ModuleInfoRaw moduleInfoRaw, boolean flushpoint) { + private void writeModule(JfrChunkWriter writer, ModuleInfoRaw moduleInfoRaw, boolean flushpoint) { writer.writeCompressedLong(moduleInfoRaw.getId()); writer.writeCompressedLong(getSymbolId(writer, moduleInfoRaw.getName(), flushpoint, false)); writer.writeCompressedLong(0); // Version, e.g. "11.0.10-internal" writer.writeCompressedLong(0); // Location, e.g. "jrt:/java.base" - writer.writeCompressedLong(getClassLoaderId(typeInfo, moduleInfoRaw.getClassLoaderName(), moduleInfoRaw.getHasClassLoader())); + writer.writeCompressedLong(getClassLoaderId(moduleInfoRaw.getClassLoaderName(), moduleInfoRaw.getHasClassLoader())); } - private static int writeClassLoaders(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { + private int writeClassLoaders(JfrChunkWriter writer, boolean flushpoint) { if (typeInfo.classLoaders.getSize() == 0) { return EMPTY; } @@ -346,13 +351,13 @@ private static void writeClassLoader(JfrChunkWriter writer, ClassLoaderInfoRaw c writer.writeCompressedLong(getSymbolId(writer, classLoaderInfoRaw.getName(), flushpoint, false)); } - private boolean addClass(TypeInfo typeInfo, Class clazz) { + private boolean addClass(Class clazz) { ClassInfoRaw classInfoRaw = StackValue.get(ClassInfoRaw.class); classInfoRaw.setId(JfrTraceId.getTraceId(clazz)); classInfoRaw.setHash(getHash(clazz.getName())); // Once the traceID is set, we can do a look-up. - if (isClassVisited(typeInfo, classInfoRaw)) { + if (isClassVisited(classInfoRaw)) { return false; } @@ -364,12 +369,12 @@ private boolean addClass(TypeInfo typeInfo, Class clazz) { return true; } - private boolean isClassVisited(TypeInfo typeInfo, ClassInfoRaw classInfoRaw) { + private boolean isClassVisited(ClassInfoRaw classInfoRaw) { return typeInfo.classes.contains(classInfoRaw) || flushedClasses.contains(classInfoRaw); } /** We cannot directly call getPackage() or getPackageName() since that may allocate. */ - private boolean addPackage(TypeInfo typeInfo, Class clazz) { + private boolean addPackage(Class clazz) { boolean hasModule = clazz.getModule() != null; String moduleName = hasModule ? clazz.getModule().getName() : null; @@ -385,7 +390,7 @@ private boolean addPackage(TypeInfo typeInfo, Class clazz) { return false; } packageInfoRaw.setHash(getHash(packageInfoRaw)); - if (isPackageVisited(typeInfo, packageInfoRaw)) { + if (isPackageVisited(packageInfoRaw)) { assert moduleName == (flushedPackages.contains(packageInfoRaw) ? ((PackageInfoRaw) flushedPackages.get(packageInfoRaw)).getModuleName() : ((PackageInfoRaw) typeInfo.packages.get(packageInfoRaw)).getModuleName()); NullableNativeMemory.free(packageInfoRaw.getModifiedUTF8Name()); @@ -395,18 +400,17 @@ private boolean addPackage(TypeInfo typeInfo, Class clazz) { packageInfoRaw.setId(++currentPackageId); packageInfoRaw.setHasModule(hasModule); packageInfoRaw.setModuleName(moduleName); - assert !typeInfo.packages.contains(packageInfoRaw); // *** remove later - typeInfo.packages.putNew(packageInfoRaw); // *** Do not free the buffer. A pointer to it is - // shallow copied into the hash map. + typeInfo.packages.putNew(packageInfoRaw); + // Do not free the buffer. A pointer to it is shallow copied into the hash map. assert typeInfo.packages.contains(packageInfoRaw); return true; } - private boolean isPackageVisited(TypeInfo typeInfo, PackageInfoRaw packageInfoRaw) { + private boolean isPackageVisited(PackageInfoRaw packageInfoRaw) { return flushedPackages.contains(packageInfoRaw) || typeInfo.packages.contains(packageInfoRaw); } - private long getPackageId(TypeInfo typeInfo, PackageInfoRaw packageInfoRaw) { + private long getPackageId(PackageInfoRaw packageInfoRaw) { if (packageInfoRaw.getModifiedUTF8Name().isNonNull() && packageInfoRaw.getNameLength().aboveOrEqual(1)) { if (flushedPackages.contains(packageInfoRaw)) { return ((PackageInfoRaw) flushedPackages.get(packageInfoRaw)).getId(); @@ -418,11 +422,11 @@ private long getPackageId(TypeInfo typeInfo, PackageInfoRaw packageInfoRaw) { } } - private boolean addModule(TypeInfo typeInfo, Module module) { + private boolean addModule(Module module) { ModuleInfoRaw moduleInfoRaw = StackValue.get(ModuleInfoRaw.class); moduleInfoRaw.setName(module.getName()); moduleInfoRaw.setHash(getHash(module.getName())); - if (isModuleVisited(typeInfo, moduleInfoRaw)) { + if (isModuleVisited(moduleInfoRaw)) { return false; } moduleInfoRaw.setId(++currentModuleId); @@ -432,11 +436,11 @@ private boolean addModule(TypeInfo typeInfo, Module module) { return true; } - private boolean isModuleVisited(TypeInfo typeInfo, ModuleInfoRaw moduleInfoRaw) { + private boolean isModuleVisited(ModuleInfoRaw moduleInfoRaw) { return typeInfo.modules.contains(moduleInfoRaw) || flushedModules.contains(moduleInfoRaw); } - private long getModuleId(TypeInfo typeInfo, String moduleName, boolean hasModule) { + private long getModuleId(String moduleName, boolean hasModule) { if (hasModule) { ModuleInfoRaw moduleInfoRaw = StackValue.get(ModuleInfoRaw.class); moduleInfoRaw.setName(moduleName); @@ -450,7 +454,7 @@ private long getModuleId(TypeInfo typeInfo, String moduleName, boolean hasModule } } - private boolean addClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { + private boolean addClassLoader(ClassLoader classLoader) { ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); if (classLoader == null) { classLoaderInfoRaw.setName(BOOTSTRAP_NAME); @@ -459,7 +463,7 @@ private boolean addClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { } classLoaderInfoRaw.setHash(getHash(classLoaderInfoRaw.getName())); - if (isClassLoaderVisited(typeInfo, classLoaderInfoRaw)) { + if (isClassLoaderVisited(classLoaderInfoRaw)) { return false; } @@ -476,11 +480,11 @@ private boolean addClassLoader(TypeInfo typeInfo, ClassLoader classLoader) { return true; } - private boolean isClassLoaderVisited(TypeInfo typeInfo, ClassLoaderInfoRaw classLoaderInfoRaw) { + private boolean isClassLoaderVisited(ClassLoaderInfoRaw classLoaderInfoRaw) { return flushedClassLoaders.contains(classLoaderInfoRaw) || typeInfo.classLoaders.contains(classLoaderInfoRaw); } - private long getClassLoaderId(TypeInfo typeInfo, String classLoaderName, boolean hasClassLoader) { + private long getClassLoaderId(String classLoaderName, boolean hasClassLoader) { if (hasClassLoader) { ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); classLoaderInfoRaw.setName(classLoaderName); @@ -671,9 +675,9 @@ private interface ModuleInfoRaw extends JfrTypeInfo { @RawField String getClassLoaderName(); + // Needed because CL name may be empty or null, even if CL is non-null @RawField - void setHasClassLoader(boolean value); // Needed because CL name may be empty or null, even - // if CL is non-null + void setHasClassLoader(boolean value); @RawField boolean getHasClassLoader(); @@ -715,7 +719,7 @@ public void putAll(JfrTypeInfoTable sourceTable) { private final class JfrClassInfoTable extends JfrTypeInfoTable { @Platforms(Platform.HOSTED_ONLY.class) - public JfrClassInfoTable() { + JfrClassInfoTable() { super(NmtCategory.JFR); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index 03e208a8d985..a1ccfa358aa4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -24,7 +24,6 @@ */ package com.oracle.svm.core.jfr; -import java.util.Arrays; import java.util.List; import com.oracle.svm.core.os.RawFileOperationSupport; @@ -35,11 +34,11 @@ import org.graalvm.word.Pointer; import com.oracle.svm.core.Uninterruptible; -import com.oracle.svm.core.heap.Heap; import com.oracle.svm.core.heap.RestrictHeapAccess; import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.jfr.events.JfrAllocationEvents; +import com.oracle.svm.core.jfr.events.DumpReasonEvent; import com.oracle.svm.core.jfr.logging.JfrLogging; import com.oracle.svm.core.jfr.oldobject.JfrOldObjectProfiler; import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository; @@ -339,7 +338,9 @@ public void beginRecording() { return; } - JfrEmergencyDumpSupport.singleton().initialize(); + if (JfrEmergencyDumpSupport.isPresent()) { + JfrEmergencyDumpSupport.singleton().initialize(); + } JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); try { @@ -586,24 +587,31 @@ public void markChunkFinal() { * See {@link JVM#setRepositoryLocation}. */ public void setRepositoryLocation(@SuppressWarnings("unused") String dirText) { - JfrEmergencyDumpSupport.singleton().setRepositoryLocation(dirText); + if (JfrEmergencyDumpSupport.isPresent()) { + JfrEmergencyDumpSupport.singleton().setRepositoryLocation(dirText); + } } /** * See {@code JfrEmergencyDump::set_dump_path}. */ public void setDumpPath(String dumpPathText) { - JfrEmergencyDumpSupport.singleton().setDumpPath(dumpPathText); + if (JfrEmergencyDumpSupport.isPresent()) { + JfrEmergencyDumpSupport.singleton().setDumpPath(dumpPathText); + } } /** * See {@code JVM#getDumpPath()}. */ public String getDumpPath() { - if (JfrEmergencyDumpSupport.singleton().getDumpPath() == null) { - JfrEmergencyDumpSupport.singleton().setDumpPath(Target_jdk_jfr_internal_util_Utils.getPathInProperty("user.home", null).toString()); + if (JfrEmergencyDumpSupport.isPresent()) { + if (JfrEmergencyDumpSupport.singleton().getDumpPath() == null) { + JfrEmergencyDumpSupport.singleton().setDumpPath(Target_jdk_jfr_internal_util_Utils.getPathInProperty("user.home", null).toString()); + } + return JfrEmergencyDumpSupport.singleton().getDumpPath(); } - return JfrEmergencyDumpSupport.singleton().getDumpPath(); + return null; } /** @@ -742,12 +750,15 @@ public Object getConfiguration(Class eventClass) { return DynamicHub.fromClass(eventClass).getJfrEventConfiguration(); } - /** See JfrRecorderService::vm_error_rotation */ + /** See JfrRecorderService::vm_error_rotation. */ @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void vmErrorRotation() { - if (!recording) { + if (!recording || !JfrEmergencyDumpSupport.isPresent()) { return; } + // Hotspot emits GC root paths, but we don't support that yet. So cutoff = 0. + emitOldObjectSamples(0, false, false); + DumpReasonEvent.emit("Out of Memory", -1); JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); try { boolean existingFile = chunkWriter.hasOpenFile(); @@ -800,7 +811,6 @@ protected void operate() { if (!SubstrateJVM.get().recording) { return; } - SubstrateJVM.get().recording = false; JfrExecutionSampler.singleton().update(); @@ -859,8 +869,9 @@ protected void operate() { methodRepo.teardown(); typeRepo.teardown(); oldObjectRepo.teardown(); - JfrEmergencyDumpSupport.singleton().teardown(); - + if (JfrEmergencyDumpSupport.isPresent()) { + JfrEmergencyDumpSupport.singleton().teardown(); + } initialized = false; } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/DumpReasonEvent.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/DumpReasonEvent.java new file mode 100644 index 000000000000..bdac33862509 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/DumpReasonEvent.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2025, 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 com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.jfr.HasJfrSupport; +import com.oracle.svm.core.jfr.JfrEvent; +import com.oracle.svm.core.jfr.JfrNativeEventWriter; +import com.oracle.svm.core.jfr.JfrNativeEventWriterData; +import com.oracle.svm.core.jfr.JfrNativeEventWriterDataAccess; +import com.oracle.svm.core.jfr.JfrTicks; +import org.graalvm.nativeimage.StackValue; + +public class DumpReasonEvent { + public static void emit(String reason, int recordingId) { + if (HasJfrSupport.get()) { + emit0(reason, recordingId); + } + } + + @Uninterruptible(reason = "Accesses a JFR buffer.") + private static void emit0(String reason, int recordingId) { + if (JfrEvent.DumpReason.shouldEmit()) { + JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); + JfrNativeEventWriterDataAccess.initializeThreadLocalNativeBuffer(data); + + JfrNativeEventWriter.beginSmallEvent(data, JfrEvent.DumpReason); + JfrNativeEventWriter.putLong(data, JfrTicks.elapsedTicks()); + JfrNativeEventWriter.putString(data, reason); + JfrNativeEventWriter.putInt(data, recordingId); + JfrNativeEventWriter.endSmallEvent(data); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java index 6f81c9222aac..f65ed9605fcd 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java @@ -33,8 +33,6 @@ import org.graalvm.nativeimage.Platforms; import org.graalvm.word.PointerBase; -import com.oracle.svm.core.Uninterruptible; - import jdk.graal.compiler.api.replacements.Fold; /** diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java index 8dc9c6cda6f3..8ed0f948a7a0 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2021, 2022, Red Hat Inc. All rights reserved. + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2025, 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 @@ -36,7 +36,6 @@ import java.util.ArrayList; import java.util.List; -import static com.oracle.svm.test.jfr.AbstractJfrTest.getEvents; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -50,14 +49,13 @@ public class TestEmergencyDump extends JfrRecordingTest { @Test public void test() throws Throwable { - List expectedStrings = new ArrayList(); + List expectedStrings = new ArrayList<>(); expectedStrings.add("first"); expectedStrings.add("second"); expectedStrings.add("third"); String[] testedEvents = new String[]{"com.jfr.String"}; Recording recording = startRecording(testedEvents); - // This event will be in chunk #1 in disk repository. StringEvent e1 = new StringEvent(); e1.message = expectedStrings.get(0); @@ -93,5 +91,17 @@ public void test() throws Throwable { assertEquals(0, expectedStrings.size()); Files.deleteIfExists(p); + + /* + * This flushes any in-flight data that may have been recorded after the emergency dump but + * before the previous recording was stopped. The emergency dump does not bother to clean up + * this data but this test cannot allow that data to pollute subsequent tests. Starting a + * new recording forces a new chunkfile to be created. When the new recording ends, the in + * flight data is flushed to the new chunk file. + */ + Recording cleanup = new Recording(); + cleanup.start(); + cleanup.stop(); + cleanup.close(); } } From 847d6fbd395df3efab3ca45de7190932ecccbe18 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Wed, 2 Jul 2025 15:46:56 -0400 Subject: [PATCH 13/16] style --- .../jfr/PosixJfrEmergencyDumpSupport.java | 7 ++-- .../svm/core/jfr/JfrChunkFileWriter.java | 7 ++-- .../oracle/svm/core/jfr/JfrChunkWriter.java | 1 + .../svm/core/jfr/JfrEmergencyDumpFeature.java | 3 +- .../svm/core/jfr/JfrEmergencyDumpSupport.java | 7 ++++ .../com/oracle/svm/core/jfr/JfrFeature.java | 2 +- .../svm/core/jfr/JfrGCWhenSerializer.java | 1 + .../core/jfr/JfrNmtCategorySerializer.java | 1 + .../svm/core/jfr/JfrSymbolRepository.java | 6 ++- .../svm/core/jfr/JfrTypeRepository.java | 41 ++++++++++++------- .../svm/core/util/PlatformTimeUtils.java | 3 ++ .../utils/poolparsers/ConstantPoolParser.java | 2 +- 12 files changed, 53 insertions(+), 28 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java index ce8f626b3121..6f8b9793e50b 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -267,7 +267,7 @@ private void writeEmergencyDumpFile(GrowableWordArray sortedChunkFilenames) { long bytesWritten = 0; while (bytesRead < chunkFileSize) { // Start at beginning - getFileSupport().seek(chunkFd, 0); // seems unneeded. idk why??? + getFileSupport().seek(chunkFd, 0); // Read from chunk file to copy block long readResult = getFileSupport().read(chunkFd, copyBlock, WordFactory.unsigned(blockSize)); if (readResult < 0) { // -1 if read failed @@ -341,7 +341,7 @@ private boolean filter(CCharPointer fn) { } } - // Verify if you can open it and receive a valid file descriptor + // Verify it can be opened and receive a valid file descriptor RawFileDescriptor chunkFd = getFileSupport().open(fullyQualified(fn), FileAccessMode.READ_WRITE); if (!getFileSupport().isValid(chunkFd)) { return false; @@ -357,8 +357,7 @@ private boolean filter(CCharPointer fn) { } /** - * Given a chunk file name, it returns the fully qualified filename. See - * RepositoryIterator::fully_qualified + * Given a chunk file name, it returns the fully qualified filename. */ @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L263-L273") private CCharPointer fullyQualified(CCharPointer fn) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java index 07fd9e6ef044..b1b8b57d0e5a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java @@ -27,10 +27,6 @@ import static com.oracle.svm.core.jfr.JfrThreadLocal.getJavaBufferList; import static com.oracle.svm.core.jfr.JfrThreadLocal.getNativeBufferList; -import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository; -import com.oracle.svm.core.nmt.NmtCategory; -import jdk.graal.compiler.word.Word; - import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; @@ -41,11 +37,13 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.jdk.UninterruptibleUtils; +import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository; import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler; import com.oracle.svm.core.jfr.sampler.JfrRecurringCallbackExecutionSampler; import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.locks.VMMutex; import com.oracle.svm.core.memory.NullableNativeMemory; +import com.oracle.svm.core.nmt.NmtCategory; import com.oracle.svm.core.os.RawFileOperationSupport; import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; @@ -59,6 +57,7 @@ import jdk.graal.compiler.api.replacements.Fold; import jdk.graal.compiler.core.common.NumUtil; +import jdk.graal.compiler.word.Word; /** * This class is used when writing the in-memory JFR data to a file. For all operations, except diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java index 3a84229fd858..9cbf4a453e33 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java @@ -47,6 +47,7 @@ public interface JfrChunkWriter extends JfrUnlockedChunkWriter { void markChunkFinal(); void closeFile(); + void closeFileForEmergencyDump(); void setMetadata(byte[] bytes); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java index ceb703386fb4..90b42a9ce1a3 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java @@ -34,7 +34,8 @@ import org.graalvm.nativeimage.ImageSingletons; /** - * The JFR emergency dump mechanism uses platform-specific implementations (see {@link JfrEmergencyDumpSupport}). + * The JFR emergency dump mechanism uses platform-specific implementations (see + * {@link JfrEmergencyDumpSupport}). */ @AutomaticallyRegisteredFeature public class JfrEmergencyDumpFeature implements InternalFeature { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java index 9a4e1abe1f33..963045d3ca95 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java @@ -30,6 +30,7 @@ import org.graalvm.nativeimage.ImageSingletons; import jdk.graal.compiler.api.replacements.Fold; + public interface JfrEmergencyDumpSupport { @Fold static boolean isPresent() { @@ -42,10 +43,16 @@ static JfrEmergencyDumpSupport singleton() { } void initialize(); + void setRepositoryLocation(String dirText); + void setDumpPath(String dumpPathText); + String getDumpPath(); + RawFileOperationSupport.RawFileDescriptor chunkPath(); + void onVmError(); + void teardown(); } 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 f014258abaef..a9393f96b660 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 @@ -154,7 +154,7 @@ private static HotSpotDiagnosticMXBean getDiagnosticBean() { @Override public List> getRequiredFeatures() { - return Arrays.asList(ThreadListenerSupportFeature.class, JfrEmergencyDumpFeature.class); + return Arrays.asList(ThreadListenerSupportFeature.class, JfrEmergencyDumpFeature.class); } @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCWhenSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCWhenSerializer.java index ca20af0a657f..75296f6b6810 100755 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCWhenSerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCWhenSerializer.java @@ -29,6 +29,7 @@ public class JfrGCWhenSerializer implements JfrSerializer { private JfrGCWhen[] values; + @Platforms(Platform.HOSTED_ONLY.class) public JfrGCWhenSerializer() { values = JfrGCWhen.values(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java index cea4e1f0738d..c50de3974364 100755 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNmtCategorySerializer.java @@ -33,6 +33,7 @@ public class JfrNmtCategorySerializer implements JfrSerializer { private NmtCategory[] nmtCategories; + @Platforms(Platform.HOSTED_ONLY.class) public JfrNmtCategorySerializer() { nmtCategories = NmtCategory.values(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java index ebeb5cc45581..9b56fa4179a6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSymbolRepository.java @@ -95,10 +95,9 @@ public long getSymbolId(String imageHeapString, boolean previousEpoch, boolean r return getSymbolId(buffer, WordFactory.unsigned(length), hash, previousEpoch); } - @Uninterruptible(reason = "Locking without transition and result is only valid until epoch changes.", callerMustBe = true) public long getSymbolId(PointerBase buffer, UnsignedWord length, int hash, boolean previousEpoch) { - com.oracle.svm.core.util.VMError.guarantee(buffer.isNonNull()); + assert buffer.isNonNull(); JfrSymbol symbol = StackValue.get(JfrSymbol.class); symbol.setModifiedUTF8(buffer); // symbol allocated in native memory symbol.setLength(length); @@ -181,10 +180,13 @@ private interface JfrSymbol extends UninterruptibleEntry { @RawField void setLength(UnsignedWord value); + @RawField UnsignedWord getLength(); + @RawField void setModifiedUTF8(PointerBase value); + @RawField PointerBase getModifiedUTF8(); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java index 252465cff210..0ce13cbbfc0f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrTypeRepository.java @@ -53,11 +53,22 @@ import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; /** - * Repository that collects and writes used classes, packages, modules, and classloaders. + * Repository that collects and writes used classes, packages, modules, and classloaders. There are + * three kinds of tables used by this class: epochTypeData, flushed*, and typeInfo. The flushed* + * tables record which classes have already been flushed to disk. The epochTypeData tables hold + * classes that have yet to be flushed as well as classes that are already flushed (they are written + * by threads emitting events). The typeInfo table is derived at flushpoints by using the + * epochTypeData and flushed* tables to determine the set of classes that have yet to be flushed. + * + * Unlike other JFR repositories, there are no epoch data buffers that require lock protection. + * Similar to other constant repositories, writes/reads to the current epochData are allowed to race + * at flushpoints. There is no risk of separating events from constant data due to the write order + * (constants before events during emission, and events before constants during flush). The + * epochData tables are only cleared at rotation safepoints. */ public class JfrTypeRepository implements JfrRepository { private static final String BOOTSTRAP_NAME = "bootstrap"; - // The following tables are only used by the flushing/rotating thread + // The following tables are only used by the flushing/rotating thread. private final JfrClassInfoTable flushedClasses; private final JfrPackageInfoTable flushedPackages; private final JfrModuleInfoTable flushedModules; @@ -65,9 +76,9 @@ public class JfrTypeRepository implements JfrRepository { private final TypeInfo typeInfo; /* - * epochTypeData tables are written to from threads emitting events and read from the - * flushing/rotating thread Their purpose is to lazily collect tagged JFR classes, which may - * later be serialized during flush/rotation. + * epochTypeData tables are written by threads emitting events and read from the + * flushing/rotating thread. Their purpose is to lazily collect tagged JFR classes, which are + * later serialized during flush/rotation. */ private final JfrClassInfoTable epochTypeData0; private final JfrClassInfoTable epochTypeData1; @@ -140,11 +151,7 @@ public int write(JfrChunkWriter writer, boolean flushpoint) { * referenced classes. * * This method does not need to be marked uninterruptible since the epoch cannot change while - * the chunkwriter lock is held. Unlike other JFR repositories, locking is not needed to protect - * a data buffer. Similar to other constant repositories, writes/reads to the current epochData - * are allowed to race at flushpoints. There is no risk of separating events from constant data - * due to the write order (constants before events during emission, and events before constants - * during flush). The epochData tables are only cleared at rotation safepoints. + * the chunkwriter lock is held. */ private void collectTypeInfo(boolean flushpoint) { JfrClassInfoTable classInfoTable = getEpochData(!flushpoint); @@ -255,8 +262,10 @@ private static long getSymbolId(JfrChunkWriter writer, String symbol, boolean fl return SubstrateJVM.getSymbolRepository().getSymbolId(symbol, !flushpoint, replaceDotWithSlash); } - // Copy to a new buffer so each table has its own copy of the data. This simplifies cleanup and - // mitigates double frees. + /* + * Copy to a new buffer so each table has its own copy of the data. This simplifies cleanup and + * mitigates double frees. + */ @Uninterruptible(reason = "Needed for JfrSymbolRepository.getSymbolId().") private static long getSymbolId(JfrChunkWriter writer, PointerBase source, UnsignedWord length, int hash, boolean flushpoint) { Pointer destination = NullableNativeMemory.malloc(length, NmtCategory.JFR); @@ -571,8 +580,10 @@ private void setPackageNameAndLength(Class clazz, PackageInfoRaw packageInfoR assert buffer.add(dot).belowOrEqual(bufferEnd); - // Since we're serializing now, we must do replacements here, instead of the symbol - // repository. + /* + * Since we're serializing now, we must do replacements here, instead of the symbol + * repository. + */ Pointer packageNameEnd = UninterruptibleUtils.String.toModifiedUTF8(str, dot, buffer, bufferEnd, false, dotWithSlash); packageInfoRaw.setModifiedUTF8Name(buffer); @@ -702,7 +713,7 @@ private abstract class JfrTypeInfoTable extends AbstractUninterruptibleHashtable protected boolean isEqual(UninterruptibleEntry v0, UninterruptibleEntry v1) { JfrTypeInfo a = (JfrTypeInfo) v0; JfrTypeInfo b = (JfrTypeInfo) v1; - return a.getName() == b.getName(); + return a.getName() != null ? a.getName().equals(b.getName()) : b.getName() == null; } @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java index f65ed9605fcd..ad0e488dca60 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/PlatformTimeUtils.java @@ -74,10 +74,13 @@ public long nanosNow() { public interface SecondsNanos extends PointerBase { @RawField void setNanos(long value); + @RawField long getNanos(); + @RawField void setSeconds(long value); + @RawField long getSeconds(); } diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ConstantPoolParser.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ConstantPoolParser.java index 4395966ead01..30f1842be9af 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ConstantPoolParser.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/poolparsers/ConstantPoolParser.java @@ -71,7 +71,7 @@ public void compareFoundAndExpectedIds() { missingIds.removeAll(foundIds); if (!missingIds.isEmpty()) { - Assert.fail("Error during parsing " + this.getClass().getName() + " constant pool! Missing IDs: " + missingIds + ". Expected IDs: " + expectedIds + ". Found IDs: " + foundIds); + Assert.fail("Error during parsing " + this.getClass().getName() + " constant pool! Missing IDs: " + missingIds + ". Expected IDs: " + expectedIds + ". Found IDs: " + foundIds); } } From cbeda3efd637ac18636b867fe685aeadb5cc0af7 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Mon, 7 Jul 2025 11:09:02 -0400 Subject: [PATCH 14/16] removed unused method --- .../src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java index 90b42a9ce1a3..0473ad26c176 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java @@ -39,11 +39,6 @@ */ @AutomaticallyRegisteredFeature public class JfrEmergencyDumpFeature implements InternalFeature { - @Fold - static boolean isPresent() { - return ImageSingletons.contains(JfrEmergencyDumpSupport.class); - } - @Override public boolean isInConfiguration(IsInConfigurationAccess access) { return VMInspectionOptions.hasJfrSupport(); From 62bf8a94967417f7577ad4ef2f042c5cb662ace1 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Mon, 7 Jul 2025 11:11:12 -0400 Subject: [PATCH 15/16] style --- .../src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java index 0473ad26c176..c74ece24cb1e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java @@ -29,9 +29,6 @@ import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; -import jdk.graal.compiler.api.replacements.Fold; - -import org.graalvm.nativeimage.ImageSingletons; /** * The JFR emergency dump mechanism uses platform-specific implementations (see From 785f415e22bda49854be5fa1eebf752c83207058 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Mon, 7 Jul 2025 11:21:10 -0400 Subject: [PATCH 16/16] style --- .../src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java | 1 - 1 file changed, 1 deletion(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java index c74ece24cb1e..96e0784e5273 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java @@ -29,7 +29,6 @@ import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; - /** * The JFR emergency dump mechanism uses platform-specific implementations (see * {@link JfrEmergencyDumpSupport}).