diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java index 7004156c1c39..b358ff68c185 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java @@ -36,6 +36,7 @@ import com.oracle.svm.shared.singletons.AutomaticallyRegisteredImageSingleton; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.imagelayer.ImageLayerBuildingSupport; +import com.oracle.svm.core.util.AbstractImageHeapList; import com.oracle.svm.core.util.DuplicatedInNativeCode; import com.oracle.svm.core.util.ImageHeapList; import com.oracle.svm.shared.Uninterruptible; @@ -89,7 +90,7 @@ public static GCCause fromId(int causeId) { return getGCCauses().get(causeId); } - public static List getGCCauses() { + public static AbstractImageHeapList getGCCauses() { return ImageSingletons.lookup(GCCauseSupport.class).gcCauses; } @@ -102,7 +103,7 @@ public static void registerGCCause(GCCause cause) { @AutomaticallyRegisteredImageSingleton @SingletonTraits(access = AllAccess.class, layeredCallbacks = SingleLayer.class, layeredInstallationKind = InitialLayerOnly.class) class GCCauseSupport { - final List gcCauses = ImageHeapList.create(GCCause.class, null); + final AbstractImageHeapList gcCauses = ImageHeapList.create(GCCause.class, null); @Platforms(Platform.HOSTED_ONLY.class) Object collectGCCauses(Object obj) { 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 b84f4d990b63..cba41df0462c 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 @@ -78,10 +78,6 @@ private static void reportOutOfMemoryError0(OutOfMemoryError error) { (!ImageLayerBuildingSupport.buildingImageLayer() || HeapDumpMetadata.isLayeredMetadataAvailable())) { HeapDumping.singleton().dumpHeapOnOutOfMemoryError(); } - if (HasJfrSupport.get()) { - SubstrateJVM.get().vmOutOfMemoryErrorRotation(); - } - if (SubstrateGCOptions.ExitOnOutOfMemoryError.getValue()) { if (LibC.isSupported()) { Log.log().string("Terminating due to java.lang.OutOfMemoryError: ").string(JDKUtils.getRawMessage(error)).newline(); @@ -92,7 +88,15 @@ private static void reportOutOfMemoryError0(OutOfMemoryError error) { } if (SubstrateGCOptions.ReportFatalErrorOnOutOfMemoryError.getValue()) { + dumpJfrOnOutOfMemoryError(); throw VMError.shouldNotReachHere("reporting due to java.lang.OutOfMemoryError"); } } + + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Can't allocate while out of memory.") + private static void dumpJfrOnOutOfMemoryError() { + if (HasJfrSupport.get()) { + SubstrateJVM.get().dumpOnOutOfMemoryError(); + } + } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java index 3caa04765bf4..4d66fb80d6eb 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java @@ -29,8 +29,8 @@ import org.graalvm.word.Pointer; import org.graalvm.word.PointerBase; import org.graalvm.word.UnsignedWord; -import org.graalvm.word.WordBase; import org.graalvm.word.impl.Word; +import org.graalvm.word.WordBase; import com.oracle.svm.shared.util.SubstrateUtil; import com.oracle.svm.shared.Uninterruptible; @@ -520,7 +520,35 @@ public static int compareUnsigned(int x, int y) { } } + public static class Character { + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static boolean isHighSurrogate(char ch) { + return ch >= java.lang.Character.MIN_HIGH_SURROGATE && ch < (java.lang.Character.MAX_HIGH_SURROGATE + 1); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static boolean isLowSurrogate(char ch) { + return ch >= java.lang.Character.MIN_LOW_SURROGATE && ch < (java.lang.Character.MAX_LOW_SURROGATE + 1); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static boolean isSurrogate(char ch) { + return isHighSurrogate(ch) || isLowSurrogate(ch); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static int toCodePoint(char high, char low) { + return ((high << 10) + low) + (java.lang.Character.MIN_SUPPLEMENTARY_CODE_POINT - (java.lang.Character.MIN_HIGH_SURROGATE << 10) - java.lang.Character.MIN_LOW_SURROGATE); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static int charCount(int codePoint) { + return codePoint >= java.lang.Character.MIN_SUPPLEMENTARY_CODE_POINT ? 2 : 1; + } + } + public static class String { + private static final int MALFORMED_UTF8_REPLACEMENT = '?'; /** * Gets the number of bytes for a char in modified UTF8 format. @@ -531,7 +559,9 @@ private static int modifiedUTF8Length(char c) { } /** - * Gets the number of bytes for a char in UTF-8 format. + * Gets the number of bytes for a single UTF-16 code unit in UTF-8 format. This helper does + * not combine surrogate pairs; callers that need code point semantics must use + * {@link #utf8Length(int)} or one of the string-based overloads. */ @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) private static int utf8Length(char c) { @@ -554,26 +584,6 @@ public static int utf8Length(int codePoint) { } } - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - private static boolean isHighSurrogate(char ch) { - return ch >= Character.MIN_HIGH_SURROGATE && ch < (Character.MAX_HIGH_SURROGATE + 1); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - private static boolean isLowSurrogate(char ch) { - return ch >= Character.MIN_LOW_SURROGATE && ch < (Character.MAX_LOW_SURROGATE + 1); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - private static int toCodePoint(char high, char low) { - return ((high << 10) + low) + (Character.MIN_SUPPLEMENTARY_CODE_POINT - (Character.MIN_HIGH_SURROGATE << 10) - Character.MIN_LOW_SURROGATE); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public static int charCount(int codePoint) { - return codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT ? 2 : 1; - } - /** * Write a char in modified UTF8 format into the buffer. */ @@ -596,14 +606,6 @@ private static Pointer writeModifiedUTF8(Pointer buffer, char c) { return pos; } - /** - * Write a char in UTF-8 format into the buffer. - */ - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - private static Pointer writeUTF8(Pointer buffer, char c) { - return writeUTF8(buffer, (int) c); - } - /** * Write a code point in UTF-8 format into the buffer. */ @@ -673,23 +675,9 @@ public static int utf8Length(java.lang.String string, CharReplacer replacer) { public static int utf8Length(java.lang.String string, int stringLength, CharReplacer replacer) { int result = 0; for (int index = 0; index < stringLength;) { - char ch = charAt(string, index); - if (replacer != null) { - ch = replacer.replace(ch); - } - if (isHighSurrogate(ch) && index + 1 < stringLength) { - char low = charAt(string, index + 1); - if (replacer != null) { - low = replacer.replace(low); - } - if (isLowSurrogate(low)) { - result += utf8Length(toCodePoint(ch, low)); - index += 2; - continue; - } - } - result += utf8Length(ch); - index++; + int codePoint = utf8CodePointAt(string, index, stringLength, replacer); + result += utf8Length(codePoint); + index += Character.charCount(codePoint); } return result; } @@ -749,44 +737,66 @@ public static Pointer toUTF8(java.lang.String string, Pointer buffer, Pointer bu public static Pointer toUTF8(java.lang.String string, int stringLength, Pointer buffer, Pointer bufferEnd, CharReplacer replacer) { Pointer pos = buffer; for (int index = 0; index < stringLength;) { - char ch = charAt(string, index); - if (replacer != null) { - ch = replacer.replace(ch); - } - if (isHighSurrogate(ch) && index + 1 < stringLength) { - char low = charAt(string, index + 1); - if (replacer != null) { - low = replacer.replace(low); - } - if (isLowSurrogate(low)) { - pos = writeUTF8(pos, toCodePoint(ch, low)); - index += 2; - continue; - } - } - pos = writeUTF8(pos, ch); - index++; + int codePoint = utf8CodePointAt(string, index, stringLength, replacer); + pos = writeUTF8(pos, codePoint); + index += Character.charCount(codePoint); } VMError.guarantee(pos.belowOrEqual(bufferEnd), "Must not write out of bounds."); return pos; } + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static int toUTF8UntilLimit(java.lang.String string, Pointer buffer, Pointer bufferEnd, int maxBytes) { + Pointer pos = buffer; + int bytesWritten = 0; + int index = 0; + while (index < string.length()) { + int codePoint = utf8CodePointAt(string, index, string.length(), null); + int byteLength = utf8Length(codePoint); + if (maxBytes - bytesWritten < byteLength) { + break; + } + pos = writeUTF8(pos, codePoint); + index += Character.charCount(codePoint); + bytesWritten += byteLength; + } + VMError.guarantee(pos.belowOrEqual(bufferEnd), "Must not write out of bounds."); + return bytesWritten; + } + /** - * Returns the Unicode code point at the given index in the string, combining surrogate - * pairs into a single code point when applicable. + * If {@code replacer} is non-null, it is applied to individual chars before UTF-8 encoding. + * Valid surrogate pairs are combined into code points before replacement and are therefore + * not passed to the replacer. */ @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public static int codePointAt(java.lang.String string, int index) { + private static int utf8CodePointAt(java.lang.String string, int index, int stringLength, CharReplacer replacer) { char ch = charAt(string, index); - if (isHighSurrogate(ch) && index + 1 < string.length()) { + if (Character.isHighSurrogate(ch) && index + 1 < stringLength) { char low = charAt(string, index + 1); - if (isLowSurrogate(low)) { - return toCodePoint(ch, low); + if (Character.isLowSurrogate(low)) { + return Character.toCodePoint(ch, low); } } + if (replacer != null) { + ch = replacer.replace(ch); + } + if (Character.isSurrogate(ch)) { + return MALFORMED_UTF8_REPLACEMENT; + } return ch; } + /** + * Returns the Unicode code point at the given index in the string, combining surrogate + * pairs into a single code point when applicable. Unpaired surrogates are returned as the + * malformed UTF-8 replacement used by the other UTF-8 helpers in this class. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static int codePointAt(java.lang.String string, int index) { + return utf8CodePointAt(string, index, string.length(), null); + } + /** * Returns a character from a string at {@code index} position based on the encoding format. */ 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 855d113b0d96..f17e9cf9be8f 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 @@ -152,7 +152,7 @@ public long getChunkStartNanos() { } @Override - public void setFilename(String fileToOpen) { + public void setFileToOpen(String fileToOpen) { assert lock.isOwner(); this.fileToOpen = fileToOpen; } @@ -321,6 +321,7 @@ private byte getAndIncrementGeneration() { } private void writeFlushCheckpoint() { + assert lock.isOwner(); long start = beginCheckpointEvent(JfrCheckpointType.Flush); long poolCountPos = writeCheckpointPoolCountPlaceholder(); int poolCount = newChunk ? writeSerializers() : 0; @@ -334,18 +335,20 @@ private void writeFlushCheckpoint() { @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") private void writePreviousEpochFlushCheckpoint() { + assert lock.isOwner(); long start = beginCheckpointEvent(JfrCheckpointType.Flush); long poolCountPos = writeCheckpointPoolCountPlaceholder(); int poolCount = newChunk ? writeSerializers() : 0; poolCount += stackTraceRepo.write(this, false); poolCount += methodRepo.write(this, false); poolCount += oldObjectRepo.write(this, false); - poolCount += typeRepo.writePreviousEpoch(this); + poolCount += typeRepo.writeAndClearPreviousEpoch(this); poolCount += symbolRepo.write(this, false); endCheckpointEvent(start, poolCountPos, poolCount); } private void writeThreadCheckpoint() { + assert lock.isOwner(); /* The code below is only atomic enough because the epoch can't change while flushing. */ if (SubstrateJVM.getThreadRepo().hasUnflushedData()) { long start = beginCheckpointEvent(JfrCheckpointType.Threads); @@ -357,6 +360,7 @@ private void writeThreadCheckpoint() { @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") private void writePreviousEpochThreadCheckpoint() { + assert lock.isOwner(); if (threadRepo.hasUnflushedPreviousEpochData()) { long start = beginCheckpointEvent(JfrCheckpointType.Threads); long poolCountPos = writeCheckpointPoolCountPlaceholder(); @@ -570,20 +574,20 @@ public void writeString(String str) { int bufferSize = 64; Pointer buffer = StackValue.get(bufferSize); Pointer bufferEnd = buffer.add(bufferSize); - int charsWritten = 0; + int charsProcessed = 0; UnsignedWord totalBytesWritten = Word.unsigned(0); - while (charsWritten < str.length()) { + while (charsProcessed < str.length()) { // Fill up the buffer as much as possible Pointer pos = buffer; - while (charsWritten < str.length()) { - int codePoint = UninterruptibleUtils.String.codePointAt(str, charsWritten); + while (charsProcessed < str.length()) { + int codePoint = UninterruptibleUtils.String.codePointAt(str, charsProcessed); int nextCharSize = UninterruptibleUtils.String.utf8Length(codePoint); if (pos.add(nextCharSize).aboveThan(bufferEnd)) { // buffer is too full to add the next char break; } pos = UninterruptibleUtils.String.writeUTF8(pos, codePoint); - charsWritten += UninterruptibleUtils.String.charCount(codePoint); + charsProcessed += UninterruptibleUtils.Character.charCount(codePoint); } // Write the contents of the buffer to disk UnsignedWord bytesToDisk = pos.subtract(buffer); 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 d35f6a20162a..d3a9fb8400dc 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 @@ -81,7 +81,7 @@ public long getChunkStartNanos() { } @Override - public void setFilename(String filename) { + public void setFileToOpen(String filename) { /* 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 4a72c28599fd..036ce293a3f6 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 @@ -32,7 +32,7 @@ public interface JfrChunkWriter extends JfrUnlockedChunkWriter { long getChunkStartNanos(); - void setFilename(String filename); + void setFileToOpen(String filename); void maybeOpenFile(); 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 ed1c6a208963..ff5bee560e0e 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 @@ -32,10 +32,10 @@ import jdk.graal.compiler.api.replacements.Fold; /** - * JFR emergency dumps are snapshots generated when the VM shuts down due to unexpected - * circumstances such as OOME or VM crash. Currently, only dumping on OOME is supported. Emergency - * dumps are a best effort attempt to persist in-flight data and consolidate data in the on-disk JFR - * chunk repository into a snapshot. This process is allocation free. + * JFR emergency dumps are terminal snapshots of an active recording. They are currently generated + * for fatal OutOfMemoryError reporting and are a best effort attempt to persist in-flight data and + * consolidate data in the on-disk JFR chunk repository into a snapshot. This process is allocation + * free. */ public interface JfrEmergencyDumpSupport { @Fold diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFrameTypeSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFrameTypeSerializer.java index ed2500c89760..5fbf4ed9d5e3 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFrameTypeSerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFrameTypeSerializer.java @@ -27,6 +27,8 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.heap.RestrictHeapAccess; + /** * Used to serialize all predefined frame types into the chunk. */ @@ -39,6 +41,7 @@ public JfrFrameTypeSerializer() { } @Override + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void write(JfrChunkWriter writer) { writer.writeCompressedLong(JfrType.FrameType.getId()); writer.writeCompressedLong(frameTypes.length); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCCauseSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCCauseSerializer.java index aaf290473c76..4f1219581b2c 100755 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCCauseSerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrGCCauseSerializer.java @@ -24,12 +24,12 @@ */ package com.oracle.svm.core.jfr; -import java.util.List; - import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.heap.GCCause; +import com.oracle.svm.core.heap.RestrictHeapAccess; +import com.oracle.svm.core.util.AbstractImageHeapList; public class JfrGCCauseSerializer implements JfrSerializer { @Platforms(Platform.HOSTED_ONLY.class) @@ -37,9 +37,10 @@ public JfrGCCauseSerializer() { } @Override + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void write(JfrChunkWriter writer) { // GCCauses has null entries - List causes = GCCause.getGCCauses(); + AbstractImageHeapList causes = GCCause.getGCCauses(); int nonNullItems = 0; for (int i = 0; i < causes.size(); i++) { 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 3e4bd76e2c3a..f931940b5bdb 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 @@ -27,12 +27,15 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.heap.RestrictHeapAccess; + public class JfrGCNameSerializer implements JfrSerializer { @Platforms(Platform.HOSTED_ONLY.class) public JfrGCNameSerializer() { } @Override + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void write(JfrChunkWriter writer) { JfrGCName[] gcNames = JfrGCNames.singleton().getNames(); writer.writeCompressedLong(JfrType.GCName.getId()); 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 75296f6b6810..179a233a957d 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 @@ -27,8 +27,10 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.heap.RestrictHeapAccess; + public class JfrGCWhenSerializer implements JfrSerializer { - private JfrGCWhen[] values; + private final JfrGCWhen[] values; @Platforms(Platform.HOSTED_ONLY.class) public JfrGCWhenSerializer() { @@ -36,6 +38,7 @@ public JfrGCWhenSerializer() { } @Override + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void write(JfrChunkWriter writer) { writer.writeCompressedLong(JfrType.GCWhen.getId()); writer.writeCompressedLong(values.length); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java index a7a55f92a2e8..031c606f456d 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java @@ -33,7 +33,6 @@ import java.io.IOException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Map; import org.graalvm.nativeimage.ImageSingletons; @@ -154,7 +153,7 @@ private static void parseFlightRecorderOptions() throws JfrArgumentParsingFailed try { if (dumpPath != null) { - Options.setDumpPath(Paths.get(dumpPath)); + Options.setDumpPath(Path.of(dumpPath)); } else { Options.setDumpPath(null); } @@ -164,7 +163,7 @@ private static void parseFlightRecorderOptions() throws JfrArgumentParsingFailed } private static void setRepositoryBasePath(String repositoryPath) throws IOException { - Path path = Paths.get(repositoryPath); + Path path = Path.of(repositoryPath); SubstrateUtil.cast(Repository.getRepository(), Target_jdk_jfr_internal_Repository.class).setBasePath(path); } 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 2b63fbd385ab..9f17fd462b7f 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 @@ -29,6 +29,7 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.heap.RestrictHeapAccess; import com.oracle.svm.core.monitor.MonitorInflationCause; public class JfrMonitorInflationCauseSerializer implements JfrSerializer { @@ -40,6 +41,7 @@ public JfrMonitorInflationCauseSerializer() { } @Override + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void write(JfrChunkWriter writer) { writer.writeCompressedLong(JfrType.MonitorInflationCause.getId()); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java index ab68f5a81653..8d4c0af38d62 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java @@ -100,7 +100,7 @@ public static void beginEvent(JfrNativeEventWriterData data, JfrEvent event, boo @Uninterruptible(reason = "Accesses a native JFR buffer.", callerMustBe = true) public static JfrEventWriteStatus endSmallEvent(JfrNativeEventWriterData data) { JfrEventWriteStatus status = endEvent(data, false); - VMError.guarantee(status != JfrEventWriteStatus.RetryLarge); + assert status != JfrEventWriteStatus.RetryLarge; return status; } @@ -347,13 +347,9 @@ private static JfrBuffer reuseOrReallocateBuffer(JfrBuffer oldBuffer, UnsignedWo if (oldBuffer.getSize().belowThan(minNewSize)) { // Grow the buffer because it is too small. UnsignedWord newSize = oldBuffer.getSize(); - if (newSize.equal(0)) { - // avoid infinite loops - newSize = minNewSize; - } else { - while (newSize.belowThan(minNewSize)) { - newSize = newSize.multiply(2); - } + assert newSize.aboveThan(0) : "JFR buffer size must be positive."; + while (newSize.belowThan(minNewSize)) { + newSize = newSize.multiply(2); } JfrBuffer result = JfrBufferAccess.allocate(newSize, oldBuffer.getBufferType()); 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 d04922e0a0b3..94a6ef210abe 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 @@ -29,6 +29,7 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.heap.RestrictHeapAccess; import com.oracle.svm.core.nmt.NmtCategory; public class JfrNmtCategorySerializer implements JfrSerializer { @@ -40,6 +41,7 @@ public JfrNmtCategorySerializer() { } @Override + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void write(JfrChunkWriter writer) { writer.writeCompressedLong(JfrType.NMTType.getId()); writer.writeCompressedLong(nmtCategories.length); 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 e2112eb5c36a..a3f58ba735b1 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 @@ -113,7 +113,7 @@ private void work() { void endRecording() { lock.lock(); try { - SubstrateJVM.get().enqueueRegularEndRecordingOperation(); + SubstrateJVM.get().endRecordingOperation(); } finally { lock.unlock(); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSerializer.java index 0e0a58640fad..071de1e1dfa1 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrSerializer.java @@ -24,7 +24,10 @@ */ package com.oracle.svm.core.jfr; +import com.oracle.svm.core.heap.RestrictHeapAccess; + /** Serializers are only written upon a new chunk. */ public interface JfrSerializer { + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") void write(JfrChunkWriter writer); } 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 98a9d9e37054..cce90f6b0612 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 @@ -61,6 +61,7 @@ public class JfrSymbolRepository implements JfrRepository { private final JfrSymbolEpochData epochData0; private final JfrSymbolEpochData epochData1; private final CharReplacer dotWithSlash; + private static final int STACK_STRING_BUFFER_SIZE = 256; @Platforms(Platform.HOSTED_ONLY.class) public JfrSymbolRepository() { @@ -93,6 +94,12 @@ public long getSymbolId(String imageHeapString, boolean previousEpoch, boolean r assert Heap.getHeap().isInImageHeap(imageHeapString); int encodedLength = UninterruptibleUtils.String.utf8Length(imageHeapString, replaceDotWithSlash ? dotWithSlash : null); + if (encodedLength <= STACK_STRING_BUFFER_SIZE) { + Pointer buffer = StackValue.get(STACK_STRING_BUFFER_SIZE); + UninterruptibleUtils.String.toUTF8(imageHeapString, imageHeapString.length(), buffer, buffer.add(encodedLength), replaceDotWithSlash ? dotWithSlash : null); + return getSymbolIdFromTemporaryBuffer(buffer, Word.unsigned(encodedLength), previousEpoch); + } + Pointer buffer = NullableNativeMemory.malloc(encodedLength, NmtCategory.JFR); if (buffer.isNull()) { return 0; @@ -103,7 +110,7 @@ public long getSymbolId(String imageHeapString, boolean previousEpoch, boolean r } @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) - private static int getHash(Pointer buffer, UnsignedWord length) { + private static int computeHash(Pointer buffer, UnsignedWord length) { int hash = 0; for (int i = 0; length.aboveThan(i); i++) { hash = 31 * hash + buffer.readByte(i); @@ -111,14 +118,16 @@ private static int getHash(Pointer buffer, UnsignedWord length) { return hash; } + /** + * Gets the symbol id for a UTF-8 byte sequence that lives in the C heap. If an identical symbol + * is already present, this method frees {@code buffer}; otherwise the repository takes + * ownership of it. + */ @Uninterruptible(reason = "Locking without transition and result is only valid until epoch changes.", callerMustBe = true) public long getSymbolId(Pointer buffer, UnsignedWord length, boolean previousEpoch) { - assert buffer.isNonNull(); JfrSymbol symbol = StackValue.get(JfrSymbol.class); - symbol.setUtf8(buffer); // symbol allocated in native memory - symbol.setLength(length); - symbol.setHash(getHash(buffer, length)); + initializeSymbol(symbol, buffer, length); /* * Get an existing entry from the hashtable or insert a new entry. This needs to be atomic @@ -134,36 +143,74 @@ public long getSymbolId(Pointer buffer, UnsignedWord length, boolean previousEpo return existingEntry.getId(); } - JfrSymbol newEntry = (JfrSymbol) epochData.table.putNew(symbol); - if (newEntry.isNull()) { - NullableNativeMemory.free(symbol.getUtf8()); - return 0L; - } + return addNewSymbol(epochData, symbol); + } finally { + mutex.unlock(); + } + } - /* New entry, so serialize it to the buffer. */ - if (epochData.buffer.isNull()) { - epochData.buffer = JfrBufferAccess.allocate(JfrBufferType.C_HEAP); - } + @Uninterruptible(reason = "Locking without transition and result is only valid until epoch changes.", callerMustBe = true) + private long getSymbolIdFromTemporaryBuffer(Pointer buffer, UnsignedWord length, boolean previousEpoch) { + assert buffer.isNonNull(); + JfrSymbol symbol = StackValue.get(JfrSymbol.class); + initializeSymbol(symbol, buffer, length); - JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); - JfrNativeEventWriterDataAccess.initialize(data, epochData.buffer); + mutex.lockNoTransition(); + try { + JfrSymbolEpochData epochData = getEpochData(previousEpoch); + JfrSymbol existingEntry = (JfrSymbol) epochData.table.get(symbol); + if (existingEntry.isNonNull()) { + return existingEntry.getId(); + } - JfrNativeEventWriter.putLong(data, newEntry.getId()); - JfrNativeEventWriter.putString(data, newEntry.getUtf8(), (int) newEntry.getLength().rawValue()); - if (!JfrNativeEventWriter.commit(data)) { - epochData.table.remove(symbol); + Pointer nativeBuffer = NullableNativeMemory.malloc(length, NmtCategory.JFR); + if (nativeBuffer.isNull()) { return 0L; } - - epochData.unflushedEntries++; - /* The buffer may have been replaced with a new one. */ - epochData.buffer = data.getJfrBuffer(); - return newEntry.getId(); + LibC.memcpy(nativeBuffer, buffer, length); + symbol.setUtf8(nativeBuffer); + return addNewSymbol(epochData, symbol); } finally { mutex.unlock(); } } + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static void initializeSymbol(JfrSymbol symbol, Pointer buffer, UnsignedWord length) { + symbol.setUtf8(buffer); + symbol.setLength(length); + symbol.setHash(computeHash(buffer, length)); + } + + @Uninterruptible(reason = "Locking without transition requires that the whole critical section is uninterruptible.") + private static long addNewSymbol(JfrSymbolEpochData epochData, JfrSymbol symbol) { + JfrSymbol newEntry = (JfrSymbol) epochData.table.putNew(symbol); + if (newEntry.isNull()) { + NullableNativeMemory.free(symbol.getUtf8()); + return 0L; + } + + /* New entry, so serialize it to the buffer. */ + if (epochData.buffer.isNull()) { + epochData.buffer = JfrBufferAccess.allocate(JfrBufferType.C_HEAP); + } + + JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); + JfrNativeEventWriterDataAccess.initialize(data, epochData.buffer); + + JfrNativeEventWriter.putLong(data, newEntry.getId()); + JfrNativeEventWriter.putString(data, newEntry.getUtf8(), (int) newEntry.getLength().rawValue()); + if (!JfrNativeEventWriter.commit(data)) { + epochData.table.remove(symbol); + return 0L; + } + + epochData.unflushedEntries++; + /* The buffer may have been replaced with a new one. */ + epochData.buffer = data.getJfrBuffer(); + return newEntry.getId(); + } + @Override @Uninterruptible(reason = "Locking without transition requires that the whole critical section is uninterruptible.") public int write(JfrChunkWriter writer, boolean flushpoint) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadLocal.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadLocal.java index 1ec71fa180bf..f11ed177299e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadLocal.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadLocal.java @@ -41,7 +41,6 @@ import com.oracle.svm.core.jfr.events.ThreadEndEvent; import com.oracle.svm.core.jfr.events.ThreadStartEvent; import com.oracle.svm.core.sampler.SamplerBuffer; -import com.oracle.svm.core.sampler.SamplerBufferPool; import com.oracle.svm.core.sampler.SamplerStatistics; import com.oracle.svm.core.thread.JavaThreads; import com.oracle.svm.core.thread.PlatformThreads; @@ -190,78 +189,6 @@ public static void stopRecording(IsolateThread isolateThread, boolean freeJavaBu } } - @Uninterruptible(reason = "Accesses various JFR buffers.") - public static void stopRecordingAfterEmergencyDump(IsolateThread isolateThread, boolean freeJavaBuffer, SamplerBufferPool samplerBufferPool) { - /* Discard event buffers. The emergency dump file was already finalized. */ - JfrBuffer nb = nativeBuffer.get(isolateThread); - nativeBuffer.set(isolateThread, Word.nullPointer()); - discardBufferAndFree(nb); - - JfrBuffer jb = javaBuffer.get(isolateThread); - if (freeJavaBuffer) { - javaBuffer.set(isolateThread, Word.nullPointer()); - discardBufferAndFree(jb); - } else { - // Do not reset the thread local since we may need it to reinstate the buffer in the - // next recording. - discardBufferAndRetire(jb); - } - - /* Clear the other event-related thread-locals. */ - javaEventWriter.set(isolateThread, null); - dataLost.set(isolateThread, Word.unsigned(0)); - - /* Clear stacktrace-related thread-locals. */ - missedSamples.set(isolateThread, 0); - unparseableStacks.set(isolateThread, 0); - - SamplerBuffer buffer = samplerBuffer.get(isolateThread); - if (buffer.isNonNull()) { - samplerBufferPool.releaseBuffer(buffer); - samplerBuffer.set(isolateThread, Word.nullPointer()); - } - } - - @Uninterruptible(reason = "Locking without transition requires that the whole critical section is uninterruptible.") - private static void discardBufferAndFree(JfrBuffer buffer) { - if (buffer.isNull()) { - return; - } - - /* Retired buffers can be freed right away. */ - JfrBufferNode node = buffer.getNode(); - if (node.isNull()) { - assert JfrBufferAccess.isRetired(buffer); - JfrBufferAccess.free(buffer); - return; - } - - /* Free the buffer but leave the node alive as it may still be needed. */ - JfrBufferNodeAccess.lockNoTransition(node); - try { - node.setBuffer(Word.nullPointer()); - JfrBufferAccess.free(buffer); - } finally { - JfrBufferNodeAccess.unlock(node); - } - } - - @Uninterruptible(reason = "Locking without transition requires that the whole critical section is uninterruptible.") - private static void discardBufferAndRetire(JfrBuffer buffer) { - assert VMOperation.isInProgressAtSafepoint(); - if (buffer.isNull() || JfrBufferAccess.isRetired(buffer)) { - return; - } - - JfrBufferNode node = buffer.getNode(); - JfrBufferNodeAccess.lockNoTransition(node); - try { - JfrBufferAccess.setRetired(buffer); - } finally { - JfrBufferNodeAccess.unlock(node); - } - } - @Uninterruptible(reason = "Locking without transition requires that the whole critical section is uninterruptible.") private static void flushToGlobalMemoryAndFreeBuffer(JfrBuffer buffer) { if (buffer.isNull()) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadRepository.java index a53da8074d4e..02fce3a41177 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadRepository.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThreadRepository.java @@ -177,6 +177,12 @@ private void registerThread0(Thread thread, boolean isVirtual) { } } + /** + * Virtual threads only need to be registered once per epoch. This fast path lets repeated + * virtual-thread remounts avoid taking the global thread repository lock. The JFR epoch is + * bumped when recording starts so stale epoch ids from a previous recording do not suppress + * registration for a fresh recording. + */ @Uninterruptible(reason = "Epoch must not change while in this method.", callerMustBe = true) private static boolean isVirtualThreadAlreadyRegistered(Thread thread) { assert JavaThreads.isVirtual(thread); 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 c8a0890f1921..b62cf088cf49 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 @@ -27,6 +27,8 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.heap.RestrictHeapAccess; + /** * Used to serialize all possible thread states into the chunk. */ @@ -39,6 +41,7 @@ public JfrThreadStateSerializer() { } @Override + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void write(JfrChunkWriter writer) { writer.writeCompressedLong(JfrType.ThreadState.getId()); 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 b6d5ce420f18..f25366ed9036 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 @@ -72,7 +72,6 @@ */ public class JfrTypeRepository implements JfrRepository { private static final String BOOTSTRAP_NAME = "bootstrap"; - private static final String EMPTY_NAME = ""; private final EconomicSet> flushedClasses; private final JfrPackageTable flushedPackages; @@ -104,8 +103,7 @@ public JfrTypeRepository() { } public void teardown() { - clearEpochData(); - getEpochData(false).clear(); + reset(); previousEpochSnapshot.teardown(); flushedPackages.teardown(); } @@ -123,8 +121,13 @@ public void reset() { epochTypeData1.clear(); } - @Uninterruptible(reason = "Result is only valid until epoch changes.") + @Uninterruptible(reason = "Result is only valid until epoch changes.", callerMustBe = true) private JfrClassInfoTable getEpochData(boolean previousEpoch) { + return getEpochData0(previousEpoch); + } + + @Uninterruptible(reason = "Result is only valid until epoch changes.") + private JfrClassInfoTable getEpochData0(boolean previousEpoch) { boolean epoch = previousEpoch ? JfrTraceIdEpoch.getInstance().previousEpoch() : JfrTraceIdEpoch.getInstance().currentEpoch(); return epoch ? epochTypeData0 : epochTypeData1; } @@ -172,19 +175,27 @@ public int write(JfrChunkWriter writer, boolean flushpoint) { return count; } - return writePreviousEpoch(writer); + return writeAndClearPreviousEpoch(writer); } @RestrictHeapAccess(access = NO_ALLOCATION, reason = "Used on OOME for emergency dumps") - int writePreviousEpoch(JfrChunkWriter writer) { + int writeAndClearPreviousEpoch(JfrChunkWriter writer) { int count = writePreviousEpochSnapshot(writer); - clearEpochData(); + flushedClasses.clear(); + flushedModules.clear(); + flushedClassLoaders.clear(); + flushedPackages.clear(); + previousEpochSnapshot.reset(); + currentPackageId = 0; + currentModuleId = 0; + currentClassLoaderId = 0; + getEpochData0(true).clear(); return count; } private TypeInfo collectCurrentTypeInfo() { TypeInfo typeInfo = new TypeInfo(); - ClassInfoRaw[] table = (ClassInfoRaw[]) getEpochData(false).getTable(); + ClassInfoRaw[] table = (ClassInfoRaw[]) getEpochData0(false).getTable(); for (int i = 0; i < table.length; i++) { ClassInfoRaw entry = table[i]; while (entry.isNonNull()) { @@ -200,7 +211,7 @@ private TypeInfo collectCurrentTypeInfo() { } private void buildPreviousEpochSnapshot() { - ClassInfoRaw[] table = (ClassInfoRaw[]) getEpochData(true).getTable(); + ClassInfoRaw[] table = (ClassInfoRaw[]) getEpochData0(true).getTable(); for (int i = 0; i < table.length; i++) { ClassInfoRaw entry = table[i]; while (entry.isNonNull()) { @@ -287,11 +298,8 @@ private long getSymbolId(String symbol, boolean previousEpoch, boolean replaceDo return 0L; } int encodedLength = UninterruptibleUtils.String.utf8Length(symbol, replaceDotWithSlash ? dotWithSlash : null); - if (encodedLength == 0) { - return SubstrateJVM.getSymbolRepository().getSymbolId(EMPTY_NAME, previousEpoch); - } - Pointer buffer = NullableNativeMemory.malloc(encodedLength, NmtCategory.JFR); + Pointer buffer = NullableNativeMemory.malloc(encodedLength == 0 ? 1 : encodedLength, NmtCategory.JFR); if (buffer.isNull()) { return 0L; } @@ -304,9 +312,6 @@ private static long getSymbolId(Pointer source, UnsignedWord length, boolean has if (!hasName) { return 0L; } - if (length.equal(0)) { - return SubstrateJVM.getSymbolRepository().getSymbolId(EMPTY_NAME, previousEpoch); - } Pointer destination = NullableNativeMemory.malloc(length, NmtCategory.JFR); if (destination.isNull()) { @@ -332,7 +337,7 @@ private void writePackage(TypeInfo typeInfo, JfrChunkWriter writer, PackageKey p writer.writeCompressedLong(pkgInfo.id); writer.writeCompressedLong(getSymbolId(writer, pkgKey.name, true)); writer.writeCompressedLong(getModuleId(typeInfo, pkgKey.module)); - writer.writeBoolean(false); + writer.writeBoolean(false); // exported } private int writeModules(JfrChunkWriter writer, TypeInfo typeInfo) { @@ -350,8 +355,8 @@ private int writeModules(JfrChunkWriter writer, TypeInfo typeInfo) { private void writeModule(TypeInfo typeInfo, JfrChunkWriter writer, Module module, long id) { writer.writeCompressedLong(id); writer.writeCompressedLong(getSymbolId(writer, module.getName(), false)); - writer.writeCompressedLong(0); - writer.writeCompressedLong(0); + writer.writeCompressedLong(0); // Version + writer.writeCompressedLong(0); // Location writer.writeCompressedLong(getClassLoaderId(typeInfo, module.getClassLoader())); } @@ -418,7 +423,7 @@ private int writePreviousEpochPackages(JfrChunkWriter writer) { writer.writeCompressedLong(entry.getId()); writer.writeCompressedLong(entry.getNameSymbolId()); writer.writeCompressedLong(entry.getModuleId()); - writer.writeBoolean(false); + writer.writeBoolean(false); // exported entry = entry.getNext(); } } @@ -438,8 +443,8 @@ private int writePreviousEpochModules(JfrChunkWriter writer) { while (entry.isNonNull()) { writer.writeCompressedLong(entry.getId()); writer.writeCompressedLong(entry.getNameSymbolId()); - writer.writeCompressedLong(0); - writer.writeCompressedLong(0); + writer.writeCompressedLong(0); // Version + writer.writeCompressedLong(0); // Location writer.writeCompressedLong(entry.getClassLoaderId()); entry = entry.getNext(); } @@ -734,18 +739,6 @@ private long getPackageId(PreviousEpochTypeSnapshot snapshot, Class clazz) { return packageEntry.getId(); } - private void clearEpochData() { - flushedClasses.clear(); - flushedModules.clear(); - flushedClassLoaders.clear(); - flushedPackages.clear(); - previousEpochSnapshot.reset(); - currentPackageId = 0; - currentModuleId = 0; - currentClassLoaderId = 0; - getEpochData(true).clear(); - } - /** * This method sets the package name and length. The target may be stack or heap allocated. */ diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrVMOperationNameSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrVMOperationNameSerializer.java index 569b77f25c11..4ee6214533ce 100755 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrVMOperationNameSerializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrVMOperationNameSerializer.java @@ -27,6 +27,7 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.heap.RestrictHeapAccess; import com.oracle.svm.core.heap.VMOperationInfos; public class JfrVMOperationNameSerializer implements JfrSerializer { @@ -35,6 +36,7 @@ public JfrVMOperationNameSerializer() { } @Override + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") public void write(JfrChunkWriter writer) { String[] vmOperationNames = VMOperationInfos.getNames(); writer.writeCompressedLong(JfrType.VMOperation.getId()); 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 a792581f67ad..c7773100445f 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 @@ -45,6 +45,7 @@ import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository; import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler; import com.oracle.svm.core.jfr.throttling.JfrEventThrottling; +import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.log.Log; import com.oracle.svm.core.sampler.SamplerBufferPool; import com.oracle.svm.core.sampler.SamplerBuffersAccess; @@ -103,13 +104,6 @@ public class SubstrateJVM { private final JfrUnlockedChunkWriter unlockedChunkWriter; private final JfrRecorderThread recorderThread; private final JfrOldObjectProfiler oldObjectProfiler; - /* - * Emergency dumps must not allocate, so they need a preallocated end-recording VM operation. - * Regular Recording.stop() calls still allocate a fresh operation because a JavaVMOperation - * instance may only be enqueued once at a time, and stop() can race the emergency enqueue. - */ - private volatile JfrEmergencyEndRecordingOperation emergencyEndRecordingOperation; - private final JfrLogging jfrLogging; private final JfrEventThrottling eventThrottler; @@ -120,7 +114,7 @@ public class SubstrateJVM { * in). */ private volatile boolean recording; - private volatile boolean emergencyRecordingCleanupPending; + private String dumpPath; @Platforms(Platform.HOSTED_ONLY.class) public SubstrateJVM(List configurations, boolean writeFile) { @@ -152,7 +146,6 @@ public SubstrateJVM(List configurations, boolean writeFile) { initialized = false; recording = false; - emergencyRecordingCleanupPending = false; } @Fold @@ -265,14 +258,10 @@ public boolean createJFR(boolean simulateFailure) { unlockedChunkWriter.initialize(options.maxChunkSize.getValue()); stackTraceRepo.setStackTraceDepth(NumUtil.safeToInt(options.stackDepth.getValue())); + if (JfrEmergencyDumpSupport.isPresent()) { + JfrEmergencyDumpSupport.singleton().initialize(); + } - /* - * Preallocate the stop-recording VM operation while the runtime is healthy, so the - * emergency-dump path does not need to allocate it after an OOM. This must not happen in - * the hosted constructor because JavaVMOperation initialization touches runtime-only random - * accessors. - */ - emergencyEndRecordingOperation = new JfrEmergencyEndRecordingOperation(); recorderThread.start(); initialized = true; @@ -358,22 +347,10 @@ public void storeMetadataDescriptor(byte[] bytes) { * See {@link JVM#beginRecording}. */ public void beginRecording() { - /* - * Emergency dumps end the native recording asynchronously. Before starting a fresh - * recording, wait until that cleanup completed so no stale repository state can leak into - * the new chunk. - */ - while (emergencyRecordingCleanupPending) { - Thread.yield(); - } if (recording) { return; } - if (JfrEmergencyDumpSupport.isPresent()) { - JfrEmergencyDumpSupport.singleton().initialize(); - } - JfrChunkWriter chunkWriter = unlockedChunkWriter.lock(); try { // It is possible that setOutput was called with a filename earlier. In that case, we @@ -397,15 +374,10 @@ public void endRecording() { recorderThread.endRecording(); } - void enqueueRegularEndRecordingOperation() { + void endRecordingOperation() { new JfrEndRecordingOperation().enqueue(); } - @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") - void enqueueEmergencyEndRecordingOperation() { - emergencyEndRecordingOperation.enqueue(); - } - /** * See {@link JVM#getClassId}. */ @@ -438,7 +410,7 @@ public void setOutput(String file) { } } } else { - chunkWriter.setFilename(file); + chunkWriter.setFileToOpen(file); } } finally { chunkWriter.unlock(); @@ -636,6 +608,7 @@ public void setRepositoryLocation(@SuppressWarnings("unused") String dirText) { * See {@code JfrEmergencyDump::set_dump_path}. */ public void setDumpPath(String dumpPathText) { + dumpPath = dumpPathText; if (JfrEmergencyDumpSupport.isPresent()) { JfrEmergencyDumpSupport.singleton().setDumpPath(dumpPathText); } @@ -649,7 +622,7 @@ public String getDumpPath() { return JfrEmergencyDumpSupport.singleton().getDumpPath(); } // The JDK side passes JVM.getDumpPath() to Path.of(...), so keep this non-null. - return ""; + return dumpPath == null ? "" : dumpPath; } /** @@ -800,7 +773,7 @@ public Object getConfiguration(Class eventClass) { @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25-ga/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L559-L572") @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25-ga/src/hotspot/share/jfr/recorder/service/jfrRecorderService.cpp#L510-L526") @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") - public void vmOutOfMemoryErrorRotation() { + public void dumpOnOutOfMemoryError() { if (!recording || !JfrEmergencyDumpSupport.isPresent()) { return; } @@ -821,14 +794,6 @@ public void vmOutOfMemoryErrorRotation() { chunkWriter.markChunkFinal(); chunkWriter.closeFile(); } - /* - * The emergency dump is a terminal snapshot for the current native recording state. If - * we returned with recording still enabled, later stop/close operations could emit - * additional JFR data into buffers without any open chunk file and leak that stale data - * into subsequent chunks or recordings. - */ - emergencyRecordingCleanupPending = true; - enqueueEmergencyEndRecordingOperation(); } finally { chunkWriter.unlock(); } @@ -846,6 +811,7 @@ protected void operate() { SubstrateJVM.getOldObjectProfiler().reset(); JfrAllocationEvents.reset(); + JfrTraceIdEpoch.getInstance().changeEpoch(); SubstrateJVM.get().recording = true; /* Recording is enabled, so JFR events can be triggered at any time. */ SubstrateJVM.getThreadRepo().registerRunningThreads(); @@ -898,43 +864,6 @@ protected void operate() { SubstrateJVM.getSymbolRepository().reset(); SubstrateJVM.getOldObjectRepository().reset(); SubstrateJVM.getOldObjectProfiler().teardown(); - SubstrateJVM.get().emergencyRecordingCleanupPending = false; - } - } - - private class JfrEmergencyEndRecordingOperation extends JavaVMOperation { - JfrEmergencyEndRecordingOperation() { - super(VMOperationInfos.get(JfrEmergencyEndRecordingOperation.class, "JFR emergency end recording", SystemEffect.SAFEPOINT)); - } - - @Override - protected void operate() { - if (!recording) { - return; - } - recording = false; - JfrExecutionSampler.singleton().update(); - - /* - * The emergency dump file was already finalized, so this cleanup only needs to discard - * stale in-memory state before the next recording can start. - */ - for (IsolateThread isolateThread = VMThreads.firstThread(); isolateThread.isNonNull(); isolateThread = VMThreads.nextThread(isolateThread)) { - JfrThreadLocal.stopRecordingAfterEmergencyDump(isolateThread, false, samplerBufferPool); - } - SamplerBuffersAccess.releaseFullBuffers(samplerBufferPool); - - threadLocal.teardown(); - samplerBufferPool.teardown(); - globalMemory.clear(); - threadRepo.reset(); - stackTraceRepo.reset(); - methodRepo.reset(); - typeRepo.reset(); - symbolRepo.reset(); - oldObjectRepo.reset(); - oldObjectProfiler.teardown(); - emergencyRecordingCleanupPending = false; } } 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 8af13c568f72..3f9e9e06918c 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 @@ -135,21 +135,13 @@ private static void writeDescription(JfrNativeEventWriterData data, String prefi int textLength = UninterruptibleUtils.String.utf8Length(text); int maxTextLength = OBJECT_DESCRIPTION_MAX_LENGTH - prefixLength; boolean tooLong = textLength > maxTextLength; - int maxEncodedTextLength = tooLong ? maxTextLength - ELLIPSIS_LENGTH : maxTextLength; + if (tooLong) { + maxTextLength -= ELLIPSIS_LENGTH; + } Pointer pos = UninterruptibleUtils.String.toUTF8(prefix, buffer, bufferEnd); - int encodedTextLength = 0; - for (int index = 0; index < text.length();) { - int codePoint = UninterruptibleUtils.String.codePointAt(text, index); - int byteLength = UninterruptibleUtils.String.utf8Length(codePoint); - int remaining = maxEncodedTextLength - encodedTextLength; - if (remaining < byteLength) { - break; - } - pos = UninterruptibleUtils.String.writeUTF8(pos, codePoint); - index += UninterruptibleUtils.String.charCount(codePoint); - encodedTextLength += byteLength; - } + int encodedTextLength = UninterruptibleUtils.String.toUTF8UntilLimit(text, pos, bufferEnd, maxTextLength); + pos = pos.add(encodedTextLength); if (tooLong) { pos = UninterruptibleUtils.String.toUTF8(ELLIPSIS, pos, bufferEnd); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java index eaccf724c7f6..df4921cf802b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java @@ -85,17 +85,6 @@ public static void processFullBuffers(boolean useSafepointChecks) { SubstrateJVM.getSamplerBufferPool().adjustBufferCount(); } - @Uninterruptible(reason = "Prevent JFR recording and epoch change.") - public static void releaseFullBuffers(SamplerBufferPool samplerBufferPool) { - while (true) { - SamplerBuffer buffer = samplerBufferPool.popFullBuffer(); - if (buffer.isNull()) { - break; - } - samplerBufferPool.releaseBuffer(buffer); - } - } - @Uninterruptible(reason = "The callee explicitly does a safepoint check.", calleeMustBe = false) private static void safepointCheck() { safepointCheck0(); 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 8eb45b3ebef2..3ae4c223c06f 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 @@ -78,7 +78,7 @@ private void runEmergencyDumpScenario(String[] testedEvents) throws Throwable { recording.dump(createTempJfrFile()); emitStringEvent("third \uD83D\uDE80"); - SubstrateJVM.get().vmOutOfMemoryErrorRotation(); + SubstrateJVM.get().dumpOnOutOfMemoryError(); } finally { recording.stop(); recording.close(); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java index 4a5c12a96dfc..c60412f614f1 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java @@ -71,7 +71,7 @@ public void test() throws Throwable { emitClassEvent(EmergencyDumpHelper.class); emitClassEvent(utf8NamedClass); - SubstrateJVM.get().vmOutOfMemoryErrorRotation(); + SubstrateJVM.get().dumpOnOutOfMemoryError(); recording.stop(); recording.close(); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java index baf402e1a7cd..6d6019803bb5 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java @@ -57,7 +57,7 @@ public void test() throws Throwable { Files.deleteIfExists(dumpFile); Recording recording = startRecording(events); - SubstrateJVM.get().vmOutOfMemoryErrorRotation(); + SubstrateJVM.get().dumpOnOutOfMemoryError(); recording.stop(); recording.close(); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRecoveredOutOfMemory.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRecoveredOutOfMemory.java deleted file mode 100644 index ff4f992d8adf..000000000000 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRecoveredOutOfMemory.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.test.jfr; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.junit.Test; - -import com.oracle.svm.core.heap.OutOfMemoryUtil; -import com.oracle.svm.core.jfr.HasJfrSupport; -import com.oracle.svm.core.jfr.JfrEvent; -import com.oracle.svm.core.jfr.SubstrateJVM; -import com.oracle.svm.shared.util.ClassUtil; -import com.oracle.svm.test.jfr.events.StringEvent; - -import jdk.jfr.Recording; -import jdk.jfr.consumer.RecordedEvent; - -public class TestEmergencyDumpRecoveredOutOfMemory extends JfrRecordingTest { - private static final String STRING_EVENT_NAME = "com.jfr.String"; - private static final String OUT_OF_MEMORY_REASON = "Out of Memory"; - private static final int ITERATIONS = 3; - private static final String ACTUAL_OOM_WORKER_PROPERTY = "com.oracle.svm.test.jfr.actualOomWorker"; - private static final String ACTUAL_OOM_DUMP_DIR_PROPERTY = "com.oracle.svm.test.jfr.actualOomDumpDir"; - private static final String ACTUAL_OOM_MESSAGE_PROPERTY = "com.oracle.svm.test.jfr.actualOomMessage"; - private static final String ACTUAL_OOM_MAX_HEAP = "-Xmx512m"; - private static final int ACTUAL_OOM_TIMEOUT_SECONDS = 120; - private static final int ACTUAL_OOM_PAYLOAD_COUNT = 4; - private static final int ACTUAL_OOM_PAYLOAD_SIZE = 4 * 1024 * 1024; - - @Test - public void testRecoveredOutOfMemoryCreatesIndependentEmergencyDumps() throws Throwable { - if (isActualOomWorker()) { - return; - } - if (!HasJfrSupport.get()) { - return; - } - - String[] events = new String[]{STRING_EVENT_NAME, JfrEvent.DumpReason.getName()}; - long pid = ProcessHandle.current().pid(); - Path rootDumpDir = Files.createTempDirectory(ClassUtil.getUnqualifiedName(getClass()) + "-"); - List dumpFiles = new ArrayList<>(ITERATIONS); - try { - for (int i = 0; i < ITERATIONS; i++) { - String message = "oom-iteration-" + i; - Path dumpDir = Files.createDirectory(rootDumpDir.resolve("dump-" + i)); - SubstrateJVM.get().setDumpPath(dumpDir.toString()); - - Recording recording = startRecording(events); - emitStringEvent(message); - - try { - OutOfMemoryUtil.heapSizeExceeded(); - fail("Expected OutOfMemoryError"); - } catch (OutOfMemoryError expected) { - // Expected. The process stays alive and should be able to record again. - } - - recording.stop(); - recording.close(); - - Path dumpFile = dumpDir.resolve("svm_oom_pid_" + pid + ".jfr"); - assertTrue("emergency dump file does not exist.", Files.exists(dumpFile)); - verifyDump(dumpFile, message); - dumpFiles.add(dumpFile); - assertNoResidualTestedEvents(events); - } - assertEquals(ITERATIONS, dumpFiles.size()); - for (Path dumpFile : dumpFiles) { - assertTrue("expected emergency dump file to still exist: " + dumpFile, Files.exists(dumpFile)); - } - } finally { - deleteRecursively(rootDumpDir); - } - } - - @Test - public void testActualOutOfMemoryCreatesEmergencyDump() throws Throwable { - if (!HasJfrSupport.get()) { - return; - } - if (isActualOomWorker()) { - runActualOutOfMemoryWorker(); - return; - } - - String message = "actual-oom"; - Path dumpDir = Files.createTempDirectory(ClassUtil.getUnqualifiedName(getClass()) + "-actual-oom-"); - try { - WorkerResult worker = runActualOutOfMemoryWorkerProcess(dumpDir, message); - assertEquals(worker.output(), 0, worker.exitCode()); - - Path dumpFile = dumpDir.resolve("svm_oom_pid_" + worker.pid() + ".jfr"); - assertTrue("emergency dump file does not exist.", Files.exists(dumpFile)); - verifyDump(dumpFile, message); - } finally { - deleteRecursively(dumpDir); - } - } - - private static void emitStringEvent(String message) { - StringEvent event = new StringEvent(); - event.message = message; - event.commit(); - } - - private void runActualOutOfMemoryWorker() throws Throwable { - /* - * Hosted analysis can reach this helper while building the junit image. Bail out early on - * configurations that do not initialize the runtime JFR singletons. - */ - if (!HasJfrSupport.get()) { - return; - } - String dumpDirProperty = System.getProperty(ACTUAL_OOM_DUMP_DIR_PROPERTY); - String message = System.getProperty(ACTUAL_OOM_MESSAGE_PROPERTY); - assertTrue("missing dump dir property for actual OOM worker", dumpDirProperty != null && !dumpDirProperty.isEmpty()); - assertTrue("missing message property for actual OOM worker", message != null && !message.isEmpty()); - - String[] events = new String[]{STRING_EVENT_NAME, JfrEvent.DumpReason.getName()}; - Path dumpDir = Path.of(dumpDirProperty); - SubstrateJVM.get().setDumpPath(dumpDir.toString()); - - Recording recording = startRecording(events); - emitStringEvent(message); - try { - exhaustHeapRecursively(); - fail("Expected OutOfMemoryError"); - } catch (OutOfMemoryError expected) { - System.gc(); - } - - recording.stop(); - recording.close(); - - Path dumpFile = dumpDir.resolve("svm_oom_pid_" + ProcessHandle.current().pid() + ".jfr"); - assertTrue("emergency dump file does not exist.", Files.exists(dumpFile)); - verifyDump(dumpFile, message); - assertNoResidualTestedEvents(events); - } - - private WorkerResult runActualOutOfMemoryWorkerProcess(Path dumpDir, String message) throws IOException, InterruptedException { - String executable = ProcessHandle.current().info().command().orElseThrow(); - List command = new ArrayList<>(); - command.add(executable); - command.add(ACTUAL_OOM_MAX_HEAP); - command.add("-D" + ACTUAL_OOM_WORKER_PROPERTY + "=true"); - command.add("-D" + ACTUAL_OOM_DUMP_DIR_PROPERTY + "=" + dumpDir); - command.add("-D" + ACTUAL_OOM_MESSAGE_PROPERTY + "=" + message); - command.add("--run-explicit"); - command.add(getClass().getName()); - - Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); - long pid = process.pid(); - boolean finished = process.waitFor(ACTUAL_OOM_TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!finished) { - process.destroyForcibly(); - fail("timed out waiting for actual OOM worker subprocess"); - } - - String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - return new WorkerResult(pid, process.exitValue(), output); - } - - private static RecursivePayload exhaustHeapRecursively() { - RecursivePayload payload = new RecursivePayload(); - payload.blocks = new byte[ACTUAL_OOM_PAYLOAD_COUNT][]; - for (int i = 0; i < payload.blocks.length; i++) { - payload.blocks[i] = new byte[ACTUAL_OOM_PAYLOAD_SIZE]; - } - payload.next = exhaustHeapRecursively(); - return payload; - } - - private static void verifyDump(Path dumpFile, String expectedMessage) throws IOException { - List events = getEvents(dumpFile, new String[]{STRING_EVENT_NAME, JfrEvent.DumpReason.getName()}, true); - assertEquals(2, events.size()); - - boolean foundString = false; - boolean foundDumpReason = false; - for (RecordedEvent event : events) { - String eventName = event.getEventType().getName(); - if (STRING_EVENT_NAME.equals(eventName)) { - assertEquals(expectedMessage, event.getString("message")); - foundString = true; - } else if (JfrEvent.DumpReason.getName().equals(eventName)) { - assertEquals(OUT_OF_MEMORY_REASON, event.getString("reason")); - assertEquals(-1, event.getInt("recordingId")); - foundDumpReason = true; - } - } - - assertTrue("Expected StringEvent not found in emergency dump", foundString); - assertTrue("Expected DumpReason event not found in emergency dump", foundDumpReason); - } - - private static void deleteRecursively(Path root) throws IOException { - if (!Files.exists(root)) { - return; - } - try (var paths = Files.walk(root)) { - paths.sorted(Comparator.reverseOrder()).forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } catch (RuntimeException e) { - if (e.getCause() instanceof IOException ioException) { - throw ioException; - } - throw e; - } - } - - private static boolean isActualOomWorker() { - return Boolean.getBoolean(ACTUAL_OOM_WORKER_PROPERTY); - } - - private static final class RecursivePayload { - private byte[][] blocks; - // Keep the recursive allocation chain reachable until the OOM is thrown. - @SuppressWarnings("unused") private RecursivePayload next; - } - - private record WorkerResult(long pid, int exitCode, String output) { - } -} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java index e32920a8b5f2..77f8517ef88d 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java @@ -73,11 +73,11 @@ public void testRepositoryEmergencyChunkIsMergedIntoEmergencyDump() throws Throw Recording recording = createInMemoryRecording(events); /* * JFR may already have an active repository chunk open for the recording. Close it so - * vmOutOfMemoryErrorRotation() has to create the emergency repository chunk itself. + * dumpOnOutOfMemoryError() has to create the emergency repository chunk itself. */ SubstrateJVM.get().setOutput(null); emitStringEvent("repository-fallback"); - SubstrateJVM.get().vmOutOfMemoryErrorRotation(); + SubstrateJVM.get().dumpOnOutOfMemoryError(); recording.stop(); recording.close(); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestJfrSymbolRepository.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestJfrSymbolRepository.java index 0d03594b213e..b4aeeccf62c7 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestJfrSymbolRepository.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestJfrSymbolRepository.java @@ -43,6 +43,7 @@ public class TestJfrSymbolRepository extends JfrRecordingTest { private static final String EMBEDDED_NUL_SYMBOL = "modified-symbol\0encoding"; private static final String EMOJI_SYMBOL = "modified-symbol \uD83D\uDE80"; + private static final String MALFORMED_SURROGATE_SYMBOL = "modified-symbol \uD83D x"; @Test public void test() throws Throwable { @@ -67,6 +68,7 @@ public void test() throws Throwable { assertSymbolUsesUTF8Encoding(path, EMBEDDED_NUL_SYMBOL); assertSymbolUsesUTF8Encoding(path, EMOJI_SYMBOL); + assertSymbolUsesUTF8Encoding(path, MALFORMED_SURROGATE_SYMBOL); } @Uninterruptible(reason = "Needed for JfrSymbolRepository.getSymbolId().") @@ -84,6 +86,8 @@ private static void assertLookupInvariants(JfrSymbolRepository repo) { long embeddedNul2 = getSymbolId(repo, EMBEDDED_NUL_SYMBOL); long emoji1 = getSymbolId(repo, EMOJI_SYMBOL); long emoji2 = getSymbolId(repo, EMOJI_SYMBOL); + long malformedSurrogate1 = getSymbolId(repo, MALFORMED_SURROGATE_SYMBOL); + long malformedSurrogate2 = getSymbolId(repo, MALFORMED_SURROGATE_SYMBOL); long nullId = getSymbolId(repo, null); assertNotEquals(0, id1); @@ -101,6 +105,12 @@ private static void assertLookupInvariants(JfrSymbolRepository repo) { assertNotEquals(id1, emoji1); assertNotEquals(id2, emoji1); assertNotEquals(embeddedNul1, emoji1); + assertNotEquals(0, malformedSurrogate1); + assertEquals(malformedSurrogate1, malformedSurrogate2); + assertNotEquals(id1, malformedSurrogate1); + assertNotEquals(id2, malformedSurrogate1); + assertNotEquals(embeddedNul1, malformedSurrogate1); + assertNotEquals(emoji1, malformedSurrogate1); assertEquals(0, nullId); }