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/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/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/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.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..6f8b9793e50b --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -0,0 +1,432 @@ +/* + * 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 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; + +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 { + @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 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. + 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; + private byte[] pidBytes; + private byte[] dumpPathBytes; + private byte[] repositoryLocationBytes; + private RawFileDescriptor emergencyFd; + private CCharPointer pathBuffer; + + @Platforms(Platform.HOSTED_ONLY.class) + 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); + } + return ""; + } + + /* + * 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. + */ + 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() { + clearPathBuffer(); + int idx = 0; + idx = writeToPathBuffer(repositoryLocationBytes, idx); + getPathBuffer().write(idx++, FILE_SEPARATOR); + idx = writeToPathBuffer(EMERGENCY_CHUNK_BYTES, idx); + writeToPathBuffer(CHUNKFILE_EXTENSION_BYTES, idx); + 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(); + if (openEmergencyDumpFile()) { + GrowableWordArray sortedChunkFilenames = StackValue.get(GrowableWordArray.class); + GrowableWordArrayAccess.initialize(sortedChunkFilenames); + try { + iterateRepository(sortedChunkFilenames); + writeEmergencyDumpFile(sortedChunkFilenames); + closeEmergencyDumpFile(); + } finally { + GrowableWordArrayAccess.freeData(sortedChunkFilenames); + sortedChunkFilenames = Word.nullPointer(); + } + } + } + + @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; + } + // 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); + } + return getFileSupport().isValid(emergencyFd); + } + + @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) { + idx = writeToPathBuffer(dumpPathBytes, idx); + // Add delimiter + getPathBuffer().write(idx++, FILE_SEPARATOR); + } + + 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 + if (openDirectory()) { + if (directory.isNull()) { + 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()) { + // 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); + } + } + 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(ISO_8601_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); + // 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; + } + 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); + } + + private boolean openDirectory() { + int fd = restartableOpen(getRepositoryLocation(), O_RDONLY() | O_NOFOLLOW(), 0); + if (fd == -1) { + return false; + } + + directory = Dirent.fdopendir(fd); + if (directory.isNull()) { + Unistd.NoTransitions.close(fd); + return false; + } + return true; + } + + private CCharPointer getRepositoryLocation() { + clearPathBuffer(); + writeToPathBuffer(repositoryLocationBytes, 0); + return getPathBuffer(); + } + + /** + * 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) { + int result; + do { + result = Fcntl.NoTransitions.open(directory, flags, mode); + } while (result == -1 && LibC.errno() == Errno.EINTR()); + + return result; + } + + @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 length + int filenameLength = (int) SubstrateUtil.strlen(fn).rawValue(); + if (filenameLength <= CHUNKFILE_EXTENSION_BYTES.length) { + return false; + } + + // 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; + if (CHUNKFILE_EXTENSION_BYTES[idx1] != ((Pointer) fn).readByte(idx2)) { + return false; + } + } + + // 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; + } + + // Verify file size + long chunkFileSize = getFileSupport().size(chunkFd); + if (chunkFileSize < CHUNK_FILE_HEADER_SIZE) { + return false; + } + getFileSupport().close(chunkFd); + return true; + } + + /** + * 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) { + long fnLength = SubstrateUtil.strlen(fn).rawValue(); + int idx = 0; + + clearPathBuffer(); + + // Cached in RepositoryIterator::RepositoryIterator and used in fully_qualified + idx = writeToPathBuffer(repositoryLocationBytes, idx); + + // 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 void clearPathBuffer() { + // Terminate the string with 0. + LibC.memset(getPathBuffer(), Word.signed(0), Word.unsigned(JVM_MAXPATHLEN)); + } + + private int writeToPathBuffer(byte[] bytes, int start) { + int idx = start; + for (int i = 0; i < bytes.length; i++) { + getPathBuffer().write(idx++, bytes[i]); + } + return idx; + } + + @Fold + 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(); + } + } + + @Override + public void teardown() { + closeEmergencyDumpFile(); + closeDirectory(); + NativeMemory.free(pathBuffer); + } +} + +@AutomaticallyRegisteredFeature +class PosixJfrEmergencyDumpFeature extends JfrEmergencyDumpFeature { + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + ImageSingletons.add(JfrEmergencyDumpSupport.class, new PosixJfrEmergencyDumpSupport()); + } +} 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/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.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/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/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/heap/OutOfMemoryUtil.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/OutOfMemoryUtil.java index 7536f4a2fed4..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; @@ -65,6 +66,9 @@ private static void reportOutOfMemoryError0(OutOfMemoryError error) { 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/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..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,20 +27,23 @@ import static com.oracle.svm.core.jfr.JfrThreadLocal.getJavaBufferList; import static com.oracle.svm.core.jfr.JfrThreadLocal.getNativeBufferList; -import java.nio.charset.StandardCharsets; - 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.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; @@ -161,7 +164,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 file) { + assert lock.isOwner(); + filename = null; + fd = file; + openFile0(); + } + private void openFile0() { chunkStartTicks = JfrTicks.elapsedTicks(); chunkStartNanos = JfrTicks.currentTimeNanos(); nextGeneration = 1; @@ -241,6 +256,30 @@ 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. + */ + @Override + public void closeFileForEmergencyDump() { + assert lock.isOwner(); + + SamplerBuffersAccess.processFullBuffers(false); + flushStorage(true); + + writeThreadCheckpoint(true); + writeFlushCheckpoint(true); + writeMetadataEvent(); + // Header must be marked COMPLETE, unlike at flushpoints. + patchFileHeader(false); + + 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); @@ -355,8 +394,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 < serializers.length; i++) { + serializers[i].write(this); } return serializers.length; } @@ -524,10 +563,17 @@ public void writeString(String str) { if (str.isEmpty()) { getFileSupport().writeByte(fd, StringEncoding.EMPTY_STRING.getValue()); } else { - byte[] bytes = str.getBytes(StandardCharsets.UTF_8); getFileSupport().writeByte(fd, StringEncoding.UTF8_BYTE_ARRAY.getValue()); - writeCompressedInt(bytes.length); - getFileSupport().write(fd, bytes); + + int length = UninterruptibleUtils.String.modifiedUTF8Length(str, false); + 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)); + NullableNativeMemory.free(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 89b0de50c3b4..f0e4d56fc1b7 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 @@ -30,6 +30,7 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.util.VMError; +import com.oracle.svm.core.os.RawFileOperationSupport; /** * Dummy implementation of a {@link JfrChunkWriter} that does not perform any file system @@ -94,6 +95,11 @@ public void openFile(String outputFile) { VMError.shouldNotReachHere(ERROR_MESSAGE); } + @Override + public void openFile(RawFileOperationSupport.RawFileDescriptor fd) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public void write(JfrBuffer buffer) { @@ -115,6 +121,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..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 @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.jfr; +import com.oracle.svm.core.os.RawFileOperationSupport; + public interface JfrChunkWriter extends JfrUnlockedChunkWriter { void unlock(); @@ -36,6 +38,8 @@ public interface JfrChunkWriter extends JfrUnlockedChunkWriter { void openFile(String outputFile); + void openFile(RawFileOperationSupport.RawFileDescriptor fd); + void write(JfrBuffer buffer); void flush(); @@ -44,6 +48,8 @@ public interface JfrChunkWriter extends JfrUnlockedChunkWriter { void closeFile(); + void closeFileForEmergencyDump(); + void setMetadata(byte[] bytes); boolean shouldRotateDisk(); 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..96e0784e5273 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpFeature.java @@ -0,0 +1,42 @@ +/* + * 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.VMInspectionOptions; +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}). + */ +@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..963045d3ca95 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.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; + +import com.oracle.svm.core.os.RawFileOperationSupport; +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(); + + RawFileOperationSupport.RawFileDescriptor chunkPath(); + + void onVmError(); + + void teardown(); +} 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 08ad4dfd9046..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 @@ -24,7 +24,7 @@ */ package com.oracle.svm.core.jfr; -import java.util.Collections; +import java.util.Arrays; import java.util.List; import org.graalvm.nativeimage.ImageSingletons; @@ -154,7 +154,7 @@ private static HotSpotDiagnosticMXBean getDiagnosticBean() { @Override public List> getRequiredFeatures() { - return Collections.singletonList(ThreadListenerSupportFeature.class); + return Arrays.asList(ThreadListenerSupportFeature.class, JfrEmergencyDumpFeature.class); } @Override 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 6689e2a58be1..90f4f5ef29b2 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 @@ -31,18 +31,22 @@ * Used to serialize all predefined frame types into the chunk. */ public class JfrFrameTypeSerializer implements JfrSerializer { + // This is necessary to avoid code that may allocate so JFR can be used when OOM. + private JfrFrameType[] frameTypes; + @Platforms(Platform.HOSTED_ONLY.class) public JfrFrameTypeSerializer() { + frameTypes = JfrFrameType.values(); + } @Override public void write(JfrChunkWriter writer) { - JfrFrameType[] values = JfrFrameType.values(); writer.writeCompressedLong(JfrType.FrameType.getId()); - writer.writeCompressedLong(values.length); - for (JfrFrameType value : values) { - writer.writeCompressedLong(value.getId()); - writer.writeString(value.getText()); + writer.writeCompressedLong(frameTypes.length); + for (int i = 0; i < frameTypes.length; i++) { + writer.writeCompressedLong(frameTypes[i].getId()); + writer.writeString(frameTypes[i].getText()); } } } 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 fab40f240072..aaf290473c76 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 @@ -41,8 +41,9 @@ public void write(JfrChunkWriter writer) { // GCCauses has null entries List 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..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 @@ -28,18 +28,20 @@ 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..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 @@ -32,19 +32,21 @@ 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..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 @@ -24,15 +24,19 @@ */ 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; import com.oracle.svm.core.collections.AbstractUninterruptibleHashtable; import com.oracle.svm.core.collections.UninterruptibleEntry; import com.oracle.svm.core.heap.Heap; @@ -41,6 +45,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 +80,28 @@ 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(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) { + assert buffer.isNonNull(); + JfrSymbol symbol = StackValue.get(JfrSymbol.class); + symbol.setModifiedUTF8(buffer); // symbol allocated 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 +112,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 +127,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, (Pointer) newEntry.getModifiedUTF8(), (int) newEntry.getLength().rawValue()); if (!JfrNativeEventWriter.commit(data)) { return 0L; } @@ -163,19 +178,17 @@ private interface JfrSymbol extends UninterruptibleEntry { @RawField void setId(long value); - @PinnedObjectField @RawField - String getValue(); + void setLength(UnsignedWord value); - @PinnedObjectField @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..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 @@ -24,131 +24,232 @@ */ 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.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. + * 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 final Set> flushedClasses = new HashSet<>(); - private final Map flushedPackages = new HashMap<>(); - private final Map flushedModules = new HashMap<>(); - private final Map flushedClassLoaders = new HashMap<>(); + private static final String BOOTSTRAP_NAME = "bootstrap"; + // 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 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; + + private final UninterruptibleUtils.CharReplacer dotWithSlash; private long currentPackageId = 0; private long currentModuleId = 0; private long currentClassLoaderId = 0; @Platforms(Platform.HOSTED_ONLY.class) public JfrTypeRepository() { + flushedClasses = new JfrClassInfoTable(); + 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(); + } + + @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) { + 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); + return JfrTraceId.load(clazz); } @Override public int write(JfrChunkWriter writer, boolean flushpoint) { - TypeInfo typeInfo = collectTypeInfo(flushpoint); - int count = writeClasses(writer, typeInfo, flushpoint); - count += writePackages(writer, typeInfo, flushpoint); - count += writeModules(writer, typeInfo, flushpoint); - count += writeClassLoaders(writer, typeInfo, flushpoint); - + typeInfo.reset(); + collectTypeInfo(flushpoint); + int count = writeClasses(writer, flushpoint); + count += writePackages(writer, flushpoint); + count += writeModules(writer, flushpoint); + count += writeClassLoaders(writer, 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; } /** * 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. */ - 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) { + 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) { + /* + * Must check the bit is set since we don't clear the epoch data tables until + * safepoint. + */ + if (JfrTraceId.isUsedCurrentEpoch(clazz)) { + visitClass(clazz); + } + } else { + if (JfrTraceId.isUsedPreviousEpoch(clazz)) { + JfrTraceId.clearUsedPreviousEpoch(clazz); + visitClass(clazz); + } } + entry = entry.getNext(); } - }); - 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()); - 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, Package pkg, Module module) { - if (pkg != null && addPackage(typeInfo, pkg, module)) { - visitModule(typeInfo, module); + private void visitPackage(Class clazz) { + if (addPackage(clazz)) { + visitModule(clazz); } } - private void visitModule(TypeInfo typeInfo, Module module) { - if (module != null && addModule(typeInfo, module)) { - visitClassLoader(typeInfo, module.getClassLoader()); + private void visitModule(Class clazz) { + Module module = clazz.getModule(); + 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 (classLoader != null && addClassLoader(typeInfo, classLoader)) { - visitClass(typeInfo, classLoader.getClass()); + if (addClassLoader(classLoader)) { + if (classLoader != null) { + visitClass(classLoader.getClass()); + } } } - private int writeClasses(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { - if (typeInfo.classes.isEmpty()) { + private int writeClasses(JfrChunkWriter writer, boolean flushpoint) { + 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); + + // Nested loops since the visitor pattern may allocate. + for (int i = 0; i < table.length; i++) { + ClassInfoRaw entry = table[i]; + while (entry.isNonNull()) { + writeClass(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())); + private void writeClass(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(packageInfoRaw)); + + boolean hasClassLoader = clazz.getClassLoader() != null; + writer.writeCompressedLong(classInfoRaw.getId()); + writer.writeCompressedLong(getClassLoaderId(hasClassLoader ? clazz.getClassLoader().getName() : null, hasClassLoader)); writer.writeCompressedLong(getSymbolId(writer, clazz.getName(), flushpoint, true)); - writer.writeCompressedLong(getPackageId(typeInfo, clazz.getPackage())); + writer.writeCompressedLong(getPackageId(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().") @@ -161,160 +262,248 @@ private static long getSymbolId(JfrChunkWriter writer, String symbol, boolean fl return SubstrateJVM.getSymbolRepository().getSymbolId(symbol, !flushpoint, replaceDotWithSlash); } - private int writePackages(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { - if (typeInfo.packages.isEmpty()) { + /* + * 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, boolean flushpoint) { + 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(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(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(packageInfoRaw.getModuleName(), packageInfoRaw.getHasModule())); writer.writeBoolean(false); // exported } - private int writeModules(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { - if (typeInfo.modules.isEmpty()) { + private int writeModules(JfrChunkWriter writer, boolean flushpoint) { + 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(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(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(moduleInfoRaw.getClassLoaderName(), moduleInfoRaw.getHasClassLoader())); } - private static int writeClassLoaders(JfrChunkWriter writer, TypeInfo typeInfo, boolean flushpoint) { - if (typeInfo.classLoaders.isEmpty()) { + private int writeClassLoaders(JfrChunkWriter writer, boolean flushpoint) { + 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) { - 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)); - } + private static void writeClassLoader(JfrChunkWriter writer, ClassLoaderInfoRaw classLoaderInfoRaw, boolean flushpoint) { + writer.writeCompressedLong(classLoaderInfoRaw.getId()); + writer.writeCompressedLong(classLoaderInfoRaw.getClassTraceId()); + writer.writeCompressedLong(getSymbolId(writer, classLoaderInfoRaw.getName(), flushpoint, false)); } - private static class PackageInfo { - private final long id; - private final Module module; - - PackageInfo(long id, Module module) { - this.id = id; - this.module = module; - } - } + private boolean addClass(Class clazz) { + ClassInfoRaw classInfoRaw = StackValue.get(ClassInfoRaw.class); + classInfoRaw.setId(JfrTraceId.getTraceId(clazz)); + classInfoRaw.setHash(getHash(clazz.getName())); - private boolean addClass(TypeInfo typeInfo, Class clazz) { - if (isClassVisited(typeInfo, clazz)) { + // Once the traceID is set, we can do a look-up. + if (isClassVisited(classInfoRaw)) { return false; } - return typeInfo.classes.add(clazz); + + classInfoRaw.setName(clazz.getName()); + classInfoRaw.setInstance(clazz); + assert !typeInfo.classes.contains(classInfoRaw); + typeInfo.classes.putNew(classInfoRaw); + assert typeInfo.classes.contains(classInfoRaw); + return true; } - private boolean isClassVisited(TypeInfo typeInfo, Class clazz) { - return typeInfo.classes.contains(clazz) || flushedClasses.contains(clazz); + private boolean isClassVisited(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); + /** We cannot directly call getPackage() or getPackageName() since that may allocate. */ + private boolean addPackage(Class clazz) { + boolean hasModule = clazz.getModule() != null; + 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. 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(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)); + + packageInfoRaw.setId(++currentPackageId); + packageInfoRaw.setHasModule(hasModule); + packageInfoRaw.setModuleName(moduleName); + 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, Package pkg) { - return flushedPackages.containsKey(pkg.getName()) || typeInfo.packages.containsKey(pkg.getName()); + private boolean isPackageVisited(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(PackageInfoRaw packageInfoRaw) { + if (packageInfoRaw.getModifiedUTF8Name().isNonNull() && packageInfoRaw.getNameLength().aboveOrEqual(1)) { + 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 { + // Empty package has reserved ID 0 return 0; } } - private boolean addModule(TypeInfo typeInfo, Module module) { - if (isModuleVisited(typeInfo, module)) { + private boolean addModule(Module module) { + ModuleInfoRaw moduleInfoRaw = StackValue.get(ModuleInfoRaw.class); + moduleInfoRaw.setName(module.getName()); + moduleInfoRaw.setHash(getHash(module.getName())); + if (isModuleVisited(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(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(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)) { + private boolean addClassLoader(ClassLoader classLoader) { + ClassLoaderInfoRaw classLoaderInfoRaw = StackValue.get(ClassLoaderInfoRaw.class); + if (classLoader == null) { + classLoaderInfoRaw.setName(BOOTSTRAP_NAME); + } else { + classLoaderInfoRaw.setName(classLoader.getName()); + } + + classLoaderInfoRaw.setHash(getHash(classLoaderInfoRaw.getName())); + if (isClassLoaderVisited(classLoaderInfoRaw)) { return false; } - typeInfo.classLoaders.put(classLoader, ++currentClassLoaderId); + + 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; } - private boolean isClassLoaderVisited(TypeInfo typeInfo, ClassLoader classLoader) { - return flushedClassLoaders.containsKey(classLoader) || typeInfo.classLoaders.containsKey(classLoader); + private boolean isClassLoaderVisited(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(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(); } + // Bootstrap classloader return 0; } @@ -326,12 +515,352 @@ private void clearEpochData() { currentPackageId = 0; currentModuleId = 0; currentClassLoaderId = 0; + getEpochData(true).clear(); + } + + 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(); + } + } + + /** + * 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())) { + while (hub.hubIsArray()) { + hub = hub.getComponentType(); + } + } + + /* + * 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, dotWithSlash); + Pointer buffer = NullableNativeMemory.malloc(utf8Length, NmtCategory.JFR); + Pointer bufferEnd = buffer.add(utf8Length); + + // If malloc fails, set a blank package name. + if (buffer.isNull()) { + packageInfoRaw.setModifiedUTF8Name(WordFactory.nullPointer()); + packageInfoRaw.setNameLength(WordFactory.unsigned(0)); + return; + } + + assert buffer.add(dot).belowOrEqual(bufferEnd); + + /* + * 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 + packageInfoRaw.setNameLength(packageNameLength); + if (dot == 0) { + assert packageNameLength.equal(0); + } + } + + @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) { + 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. + */ + 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 + @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(); + } + + @RawStructure + 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(); } - 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<>(); + @RawStructure + private interface ModuleInfoRaw extends JfrTypeInfo { + @PinnedObjectField + @RawField + void setClassLoaderName(String value); + + @PinnedObjectField + @RawField + String getClassLoaderName(); + + // Needed because CL name may be empty or null, even if CL is non-null + @RawField + void setHasClassLoader(boolean value); + + @RawField + boolean getHasClassLoader(); + } + + @RawStructure + private interface ClassLoaderInfoRaw extends JfrTypeInfo { + @RawField + void setClassTraceId(long value); + + @RawField + long getClassTraceId(); + } + + private abstract class JfrTypeInfoTable extends AbstractUninterruptibleHashtable { + 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() != null ? a.getName().equals(b.getName()) : b.getName() == null; + } + + @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) + 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)); + } + } + + private final class JfrPackageInfoTable extends JfrTypeInfoTable { + @Platforms(Platform.HOSTED_ONLY.class) + 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) { + // 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; + } + + @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 the 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) + 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) + 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..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 @@ -26,6 +26,7 @@ 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; @@ -33,9 +34,11 @@ import org.graalvm.word.Pointer; import com.oracle.svm.core.Uninterruptible; +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; @@ -101,7 +104,6 @@ public class SubstrateJVM { * in). */ private volatile boolean recording; - private String dumpPath; @Platforms(Platform.HOSTED_ONLY.class) public SubstrateJVM(List configurations, boolean writeFile) { @@ -336,6 +338,10 @@ public void beginRecording() { 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 @@ -581,24 +587,31 @@ 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. + if (JfrEmergencyDumpSupport.isPresent()) { + JfrEmergencyDumpSupport.singleton().setRepositoryLocation(dirText); + } } /** * See {@code JfrEmergencyDump::set_dump_path}. */ public void setDumpPath(String dumpPathText) { - dumpPath = dumpPathText; + if (JfrEmergencyDumpSupport.isPresent()) { + 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.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 dumpPath; + return null; } /** @@ -737,6 +750,34 @@ 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() { + 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(); + if (!existingFile) { + // 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.markChunkFinal(); + 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)); @@ -770,7 +811,6 @@ protected void operate() { if (!SubstrateJVM.get().recording) { return; } - SubstrateJVM.get().recording = false; JfrExecutionSampler.singleton().update(); @@ -829,7 +869,9 @@ protected void operate() { methodRepo.teardown(); typeRepo.teardown(); oldObjectRepo.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/jfr/oldobject/JfrOldObjectRepository.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/oldobject/JfrOldObjectRepository.java index 18441411bbe0..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; @@ -85,7 +84,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..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 @@ -24,11 +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 com.oracle.svm.core.Uninterruptible; +import org.graalvm.word.PointerBase; import jdk.graal.compiler.api.replacements.Fold; @@ -48,26 +51,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 +68,20 @@ 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(); + } } 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..8ed0f948a7a0 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java @@ -0,0 +1,107 @@ +/* + * 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 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 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); + + /* + * 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(); + } +} 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..f40917f98f40 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestGrowableWordArrayQuickSort.java @@ -0,0 +1,71 @@ +/* + * 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; + } + } + +} 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..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 + " 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); } }