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..60ee007e95d9 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 @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.posix; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CCharPointerPointer; import org.graalvm.word.PointerBase; @@ -107,6 +109,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..eefe71ecf40a 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 result) { 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()); + result.setNanos(ts.tv_nsec()); + result.setSeconds(ts.tv_sec()); } } diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/Fcntl.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/Fcntl.java index 4c06021f338e..58cf3617e605 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/Fcntl.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/Fcntl.java @@ -26,6 +26,8 @@ import static org.graalvm.nativeimage.c.function.CFunction.Transition.NO_TRANSITION; +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.headers.LibC; import org.graalvm.nativeimage.c.CContext; import org.graalvm.nativeimage.c.constant.CConstant; import org.graalvm.nativeimage.c.function.CFunction; @@ -74,5 +76,15 @@ public static class NoTransitions { @CFunction(transition = NO_TRANSITION) public static native int unlinkat(int dirfd, CCharPointer pathname, int flags); + @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") + public static int restartableOpen(CCharPointer directory, int flags, int mode) { + int result; + do { + result = open(directory, flags, mode); + } while (result == -1 && LibC.errno() == Errno.EINTR()); + + return result; + } + } } 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..e38539e7e5c5 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 time = UnsafeStackValue.get(SecondsNanos.class); + PlatformTimeUtils.singleton().javaTimeSystemUTC(time); - SecondsNanos time = PlatformTimeUtils.singleton().javaTimeSystemUTC(); - - long diff = time.seconds() - offsetInSeconds; + long diff = time.getSeconds() - offsetInSeconds; if (diff >= maxDiffSecs || diff <= minDiffSecs) { return -1; } - return (diff * 1000000000) + time.nanos(); + return (diff * 1000000000) + time.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..c40de339a3fb --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -0,0 +1,469 @@ +/* + * 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.VMInspectionOptions; +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.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.feature.InternalFeature; +import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; +import com.oracle.svm.core.jfr.SubstrateJVM; +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 = '/'; + private static final byte DOT = '.'; + // 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 = "svm_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 byte[] cwdBytes; + private RawFileDescriptor emergencyFd; + private CCharPointer pathBuffer; + private String openFileWarning; + private String openDirectoryWarning; + + @Platforms(Platform.HOSTED_ONLY.class) + public PosixJfrEmergencyDumpSupport() { + } + + @Override + public void initialize() { + pidBytes = Long.toString(ProcessHandle.current().pid()).getBytes(StandardCharsets.UTF_8); + pathBuffer = NativeMemory.calloc(JVM_MAXPATHLEN + 1, NmtCategory.JFR); + directory = Word.nullPointer(); + saveCwd(); + } + + private void saveCwd() { + if (cwdBytes == null) { + String cwd = System.getProperty("user.dir"); + if (cwd != null) { + cwdBytes = cwd.getBytes(StandardCharsets.UTF_8); + } + } + } + + @Override + public void setRepositoryLocation(String dirText) { + repositoryLocationBytes = dirText.getBytes(StandardCharsets.UTF_8); + openDirectoryWarning = "Unable to open repository " + dirText; + } + + /** This method is called during JFR initialization. */ + @Override + public void setDumpPath(String dumpPathText) { + if (dumpPathText == null || dumpPathText.isEmpty()) { + saveCwd(); + dumpPathBytes = cwdBytes; + } else { + dumpPathBytes = dumpPathText.getBytes(StandardCharsets.UTF_8); + } + + if (dumpPathBytes != null) { + openFileWarning = "Unable to create an emergency dump file at the location set by dumppath=" + new String(dumpPathBytes, StandardCharsets.UTF_8); + } else { + openFileWarning = "Unable to create an emergency dump file. Dump path could not be set."; + } + + } + + @Override + public String getDumpPath() { + if (dumpPathBytes != null) { + return new String(dumpPathBytes, StandardCharsets.UTF_8); + } + return ""; + } + + /** + * This method either creates and uses the dumpfile itself as a new chunk, or creates 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) { + openEmergencyDumpFile(); + /* + * We can directly use the emergency dump file name as the new chunk since there are no + * other chunk files. + */ + return emergencyFd; + } + 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); + idx = writeToPathBuffer(CHUNKFILE_EXTENSION_BYTES, idx); + getPathBuffer().write(idx++, (byte) 0); + 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() { + 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") + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+2/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L86-L89") + 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)) { + SubstrateJVM.getLogging().logJfrWarning(openFileWarning); + // 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) { + dumpPathBytes = cwdBytes; + } + if (dumpPathBytes != null) { + idx = writeToPathBuffer(dumpPathBytes, idx); + // Add delimiter + getPathBuffer().write(idx++, FILE_SEPARATOR); + } + + idx = writeToPathBuffer(DUMP_FILE_PREFIX, idx); + idx = writeToPathBuffer(pidBytes, idx); + idx = writeToPathBuffer(CHUNKFILE_EXTENSION_BYTES, idx); + getPathBuffer().write(idx, (byte) 0); + return getPathBuffer(); + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+3/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L310-L345") + private void iterateRepository(GrowableWordArray gwa) { + int count = 0; + // Open directory + if (openDirectory()) { + if (directory.isNull()) { + return; + } + // 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 + if (!GrowableWordArrayAccess.add(gwa, (Word) fn, NmtCategory.JFR)) { + SubstrateJVM.getLogging().logJfrSystemError("Unable to add chunk filename to list during jfr emergency dump"); + } + count++; + } + } + closeDirectory(); + if (count > 0) { + GrowableWordArrayAccess.qsort(gwa, 0, count - 1, PosixJfrEmergencyDumpSupport::compare); + } + } + } + + @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, Word.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, Word.unsigned(aLen)); + } + return cmp; + } + + private void writeEmergencyDumpFile(GrowableWordArray sortedChunkFilenames) { + int blockSize = 1024 * 1024; + Pointer copyBlock = NullableNativeMemory.malloc(blockSize, NmtCategory.JFR); + if (copyBlock.isNull()) { + SubstrateJVM.getLogging().logJfrSystemError("Unable to malloc memory during jfr emergency dump"); + SubstrateJVM.getLogging().logJfrSystemError("Unable to write jfr emergency dump file"); + return; + } + + for (int i = 0; i < sortedChunkFilenames.getSize(); i++) { + CCharPointer fn = (CCharPointer) GrowableWordArrayAccess.get(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, Word.unsigned(blockSize)); + if (readResult < 0) { // -1 if read failed + SubstrateJVM.getLogging().logJfrInfo("Unable to recover JFR data, read failed."); + break; + } + bytesRead += readResult; + assert bytesRead - bytesWritten <= blockSize; + // Write from copy block to dump file + if (!getFileSupport().write(emergencyFd, copyBlock, Word.unsigned(bytesRead - bytesWritten))) { + SubstrateJVM.getLogging().logJfrInfo("Unable to recover JFR data, write failed."); + break; + } + bytesWritten = bytesRead; + } + getFileSupport().close(chunkFd); + } + } + NullableNativeMemory.free(copyBlock); + } + + private boolean openDirectory() { + int fd = Fcntl.NoTransitions.restartableOpen(getRepositoryLocation(), O_RDONLY() | O_NOFOLLOW(), 0); + if (fd == -1) { + return false; + } + + directory = Dirent.fdopendir(fd); + if (directory.isNull()) { + Unistd.NoTransitions.close(fd); + SubstrateJVM.getLogging().logJfrSystemError(openDirectoryWarning); + 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)); + } + getPathBuffer().write(idx++, (byte) 0); + return getPathBuffer(); + } + + private CCharPointer getPathBuffer() { + return pathBuffer; + } + + private void clearPathBuffer() { + 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 = Word.nullPointer(); + } + } + + private void closeDirectory() { + if (directory.isNonNull()) { + Dirent.closedir(directory); + directory = Word.nullPointer(); + } + } + + @Override + public void teardown() { + closeEmergencyDumpFile(); + closeDirectory(); + NativeMemory.free(pathBuffer); + } +} + +@AutomaticallyRegisteredFeature +class PosixJfrEmergencyDumpFeature implements InternalFeature { + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return VMInspectionOptions.hasJfrSupport(); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + ImageSingletons.add(JfrEmergencyDumpSupport.class, new PosixJfrEmergencyDumpSupport()); + } +} diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/PosixPerfMemoryProvider.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/PosixPerfMemoryProvider.java index 3522eb6b9d05..d23fa5a5cd45 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/PosixPerfMemoryProvider.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jvmstat/PosixPerfMemoryProvider.java @@ -349,7 +349,7 @@ private static boolean makeUserTmpDir(CCharPointer directory) { @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+13/src/hotspot/os/posix/perfMemory_posix.cpp#L291-L341") private static SecureDirectory openDirectorySecure(CCharPointer directory) { - int fd = restartableOpen(directory, O_RDONLY() | O_NOFOLLOW(), 0); + int fd = Fcntl.NoTransitions.restartableOpen(directory, O_RDONLY() | O_NOFOLLOW(), 0); if (fd == -1) { return null; } @@ -465,16 +465,6 @@ private static boolean canFileBeDeleted(int pid) { return false; } - @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; - } - @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") private static int restartableOpenat(int fd, CCharPointer filename, int flags, int mode) { int result; 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 5bcb90796d61..89dfb5dff604 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 @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.windows; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CCharPointerPointer; import org.graalvm.word.PointerBase; @@ -126,6 +128,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..074e3df736ca 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 result) { 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); + + result.setNanos(nanos); + result.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..f784d50ae3be 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 @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.collections; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + import jdk.graal.compiler.word.Word; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; @@ -131,8 +133,9 @@ public UninterruptibleEntry[] getTable() { } @Override - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) public UninterruptibleEntry get(UninterruptibleEntry valueOnStack) { + assert valueOnStack.isNonNull(); int index = Integer.remainderUnsigned(valueOnStack.getHash(), DEFAULT_TABLE_LENGTH); UninterruptibleEntry entry = table[index]; while (entry.isNonNull()) { @@ -171,6 +174,12 @@ public boolean putIfAbsent(UninterruptibleEntry valueOnStack) { } } + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public boolean contains(UninterruptibleEntry valueOnStack) { + 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..4e1a792c2387 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,11 @@ public static void initialize(GrowableWordArray array) { array.setData(Word.nullPointer()); } + public static void set(GrowableWordArray array, int i, Word value) { + assert i >= 0 && i < array.getSize(); + array.getData().addressOf(i).write(value); + } + public static Word get(GrowableWordArray array, int i) { assert i >= 0 && i < array.getSize(); return array.getData().addressOf(i).read(); @@ -103,4 +108,38 @@ private static int computeNewCapacity(GrowableWordArray array) { static int wordSize() { return ConfigurationValues.getTarget().wordSize; } + + public static void qsort(GrowableWordArray array, int low, int high, Comparator c) { + if (low < high) { + int pivotIndex = partition(array, low, high, c); + qsort(array, low, pivotIndex - 1, c); + qsort(array, pivotIndex + 1, high, c); + } + } + + private static int partition(GrowableWordArray array, int low, int high, Comparator c) { + Word pivot = get(array, high); + int i = low - 1; + for (int j = low; j < high; j++) { + if (c.compare(get(array, j), pivot) <= 0) { + i++; + // Swap i and j + Word temp = get(array, i); + set(array, i, get(array, j)); + set(array, j, temp); + } + } + // Swap the pivot with i+1 + Word temp = get(array, i + 1); + set(array, i + 1, get(array, high)); + set(array, 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/doc-files/FlightRecorderOptionsHelp.txt b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FlightRecorderOptionsHelp.txt index 12eb9eee7f49..30b29a301996 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FlightRecorderOptionsHelp.txt +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FlightRecorderOptionsHelp.txt @@ -2,6 +2,8 @@ Usage: -XX:FlightRecorderOptions=[option[=value][,...]] This option expects a comma separated list of key-value pairs. None of the options are mandatory. Possible option keys are as follows: +dumppath=... (Optional) Path to where emergency dumps should be written. + globalbuffersize=512k (Optional) Size of each global JFR buffer. This value cannot be changed once JFR has been initialized. The default value is determined by the value for memorysize. 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 7731c98acc6b..81cb1840bd35 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 @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.headers; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CCharPointerPointer; @@ -103,6 +105,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..6e5fd2d142e0 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 @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.headers; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CCharPointerPointer; import org.graalvm.word.PointerBase; @@ -81,6 +83,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 3acd94a3f8ab..1e8d23ea8e69 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.thread.VMOperation; @@ -71,6 +72,9 @@ private static void reportOutOfMemoryError0(OutOfMemoryError error) { if (VMInspectionOptions.hasHeapDumpSupport() && SubstrateOptions.HeapDumpOnOutOfMemoryError.getValue()) { HeapDumping.singleton().dumpHeapOnOutOfMemoryError(); } + if (VMInspectionOptions.hasJfrSupport()) { + SubstrateJVM.get().vmOutOfMemoryErrorRotation(); + } 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 46dafe7b2417..9e564ec39186 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 @@ -1163,7 +1163,7 @@ int getClassFileAccessFlags() { } @Substitute - private DynamicHub getComponentType() { + public DynamicHub getComponentType() { return componentType; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java index cb36c9624710..ca79ddab8496 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java @@ -544,7 +544,7 @@ public static class String { * Gets the number of bytes for a char in modified UTF8 format. */ @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - private static int modifiedUTF8Length(char c) { + public static int modifiedUTF8Length(char c) { if (c >= 0x0001 && c <= 0x007F) { // ASCII character. return 1; @@ -561,7 +561,7 @@ private static int modifiedUTF8Length(char c) { * Write a char in modified UTF8 format into the buffer. */ @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - private static Pointer writeModifiedUTF8(Pointer buffer, char c) { + public static Pointer writeModifiedUTF8(Pointer buffer, char c) { Pointer pos = buffer; if (c >= 0x0001 && c <= 0x007F) { pos.writeByte(0, (byte) c); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java index 2cd2a792909a..e306e611e538 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java @@ -144,6 +144,7 @@ public interface JfrArgument { } public enum FlightRecorderOptionsArgument implements JfrArgument { + DumpPath("dumppath"), GlobalBufferSize("globalbuffersize"), MaxChunkSize("maxchunksize"), MemorySize("memorysize"), 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..17f439c79a43 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,15 +27,17 @@ 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.c.struct.SizeOf; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.word.Pointer; import org.graalvm.word.UnsignedWord; 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; @@ -46,11 +48,13 @@ import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; import com.oracle.svm.core.sampler.SamplerBuffersAccess; -import com.oracle.svm.core.thread.JavaVMOperation; +import com.oracle.svm.core.thread.NativeVMOperation; +import com.oracle.svm.core.thread.NativeVMOperationData; import com.oracle.svm.core.thread.RecurringCallbackSupport; import com.oracle.svm.core.thread.VMOperation; import com.oracle.svm.core.thread.VMOperationControl; import com.oracle.svm.core.thread.VMThreads; +import com.oracle.svm.core.UnmanagedMemoryUtil; import jdk.graal.compiler.api.replacements.Fold; import jdk.graal.compiler.core.common.NumUtil; @@ -77,6 +81,7 @@ public final class JfrChunkFileWriter implements JfrChunkWriter { private static final short FLAG_COMPRESSED_INTS = 0b01; private static final short FLAG_CHUNK_FINAL = 0b10; + private final JfrChangeEpochOperation epochChangeOp; private final VMMutex lock; private final JfrGlobalMemory globalMemory; private final JfrMetadata metadata; @@ -86,7 +91,7 @@ public final class JfrChunkFileWriter implements JfrChunkWriter { private long notificationThreshold; - private String filename; + private String fileToOpen; private RawFileDescriptor fd; private long chunkStartTicks; private long chunkStartNanos; @@ -104,6 +109,7 @@ public JfrChunkFileWriter(JfrGlobalMemory globalMemory, JfrStackTraceRepository this.globalMemory = globalMemory; this.metadata = new JfrMetadata(null); this.compressedInts = true; + this.epochChangeOp = new JfrChangeEpochOperation(); /* * Repositories earlier in the write order may reference entries of repositories later in @@ -143,25 +149,31 @@ public long getChunkStartNanos() { } @Override - public void setFilename(String filename) { + public void setFilename(String fileToOpen) { assert lock.isOwner(); - this.filename = filename; + this.fileToOpen = fileToOpen; } @Override public void maybeOpenFile() { assert lock.isOwner(); - if (filename != null) { - openFile(filename); + assert !hasOpenFile(); + if (fileToOpen != null) { + openFile(fileToOpen); } } @Override public void openFile(String outputFile) { assert lock.isOwner(); - filename = outputFile; - fd = getFileSupport().create(filename, FileCreationMode.CREATE_OR_REPLACE, FileAccessMode.READ_WRITE); + openFile(getFileSupport().create(outputFile, FileCreationMode.CREATE_OR_REPLACE, FileAccessMode.READ_WRITE)); + } + @Override + public void openFile(RawFileDescriptor file) { + assert lock.isOwner(); + fileToOpen = null; + fd = file; chunkStartTicks = JfrTicks.elapsedTicks(); chunkStartNanos = JfrTicks.currentTimeNanos(); nextGeneration = 1; @@ -170,7 +182,6 @@ public void openFile(String outputFile) { lastMetadataId = -1; metadataPosition = -1; lastCheckpointOffset = -1; - writeFileHeader(); } @@ -222,8 +233,10 @@ public void closeFile() { * Switch to a new epoch. This is done at a safepoint to ensure that we end up with * consistent data, even if multiple threads have JFR events in progress. */ - JfrChangeEpochOperation op = new JfrChangeEpochOperation(); - op.enqueue(); + int size = SizeOf.get(NativeVMOperationData.class); + NativeVMOperationData data = StackValue.get(size); + UnmanagedMemoryUtil.fill((Pointer) data, Word.unsigned(size), (byte) 0); + epochChangeOp.enqueue(data); /* * After changing the epoch, all subsequently triggered JFR events will be recorded into the @@ -237,7 +250,6 @@ public void closeFile() { patchFileHeader(false); getFileSupport().close(fd); - filename = null; fd = Word.nullPointer(); } @@ -355,8 +367,9 @@ private long getDeltaToLastCheckpoint(long startOfNewCheckpoint) { private int writeSerializers() { JfrSerializer[] serializers = JfrSerializerSupport.get().getSerializers(); - for (JfrSerializer serializer : serializers) { - serializer.write(this); + // noinspection ForLoopReplaceableByForEach: must be allocation free. + for (int i = 0; i < serializers.length; i++) { + serializers[i].write(this); } return serializers.length; } @@ -524,10 +537,34 @@ 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); + writeCompressedInt(length); + int bufferSize = 64; + Pointer buffer = StackValue.get(bufferSize); + Pointer bufferEnd = buffer.add(bufferSize); + int charsWritten = 0; + UnsignedWord totalBytesWritten = Word.unsigned(0); + while (charsWritten < str.length()) { + // Fill up the buffer as much as possible + Pointer pos = buffer; + while (charsWritten < str.length()) { + char ch = UninterruptibleUtils.String.charAt(str, charsWritten); + int nextCharSize = UninterruptibleUtils.String.modifiedUTF8Length(ch); + if (pos.add(nextCharSize).aboveThan(bufferEnd)) { + // buffer is too full to add the next char + break; + } + pos = UninterruptibleUtils.String.writeModifiedUTF8(pos, ch); + charsWritten++; + } + // Write the contents of the buffer to disk + UnsignedWord bytesToDisk = pos.subtract(buffer); + getFileSupport().write(fd, buffer, bytesToDisk); + totalBytesWritten = totalBytesWritten.add(bytesToDisk); + } + assert totalBytesWritten.equal(length); } } @@ -629,13 +666,14 @@ public byte getValue() { } } - private class JfrChangeEpochOperation extends JavaVMOperation { + private class JfrChangeEpochOperation extends NativeVMOperation { + @Platforms(Platform.HOSTED_ONLY.class) protected JfrChangeEpochOperation() { super(VMOperationInfos.get(JfrChangeEpochOperation.class, "JFR change epoch", SystemEffect.SAFEPOINT)); } @Override - protected void operate() { + protected void operate(NativeVMOperationData d) { changeEpoch(); } 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..78a0100d6280 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.RawFileDescriptor; /** * Dummy implementation of a {@link JfrChunkWriter} that does not perform any file system @@ -91,18 +92,23 @@ public void maybeOpenFile() { @Override public void openFile(String outputFile) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void openFile(RawFileDescriptor fd) { + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public void write(JfrBuffer buffer) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override public void flush() { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override @@ -112,7 +118,7 @@ public void markChunkFinal() { @Override public void closeFile() { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override @@ -133,41 +139,41 @@ public long beginEvent() { @Override public void endEvent(long start) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override public void writeBoolean(boolean value) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override public void writeByte(byte value) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override public void writeBytes(byte[] values) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override public void writeCompressedInt(int value) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override public void writePaddedInt(long value) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override public void writeCompressedLong(long value) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } @Override public void writeString(String str) { - VMError.shouldNotReachHere(ERROR_MESSAGE); + throw VMError.shouldNotReachHere(ERROR_MESSAGE); } } 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..4a72c28599fd 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(); 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..ed1c6a208963 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEmergencyDumpSupport.java @@ -0,0 +1,64 @@ +/* + * 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; + +/** + * JFR emergency dumps are snapshots generated when the VM shuts down due to unexpected + * circumstances such as OOME or VM crash. Currently, only dumping on OOME is supported. Emergency + * dumps are a best effort attempt to persist in-flight data and consolidate data in the on-disk JFR + * chunk repository into a snapshot. This process is allocation free. + */ +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/JfrFrameTypeSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFrameTypeSerializer.java index 6689e2a58be1..cc5359b261dc 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 { + private final 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); + // noinspection ForLoopReplaceableByForEach: must be allocation free. + 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..fd7b2fad0bf7 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,10 @@ public void write(JfrChunkWriter writer) { // GCCauses has null entries List causes = GCCause.getGCCauses(); int nonNullItems = 0; - for (GCCause cause : causes) { - if (cause != null) { + + // noinspection ForLoopReplaceableByForEach: must be allocation free. + for (int i = 0; i < causes.size(); i++) { + if (causes.get(i) != null) { nonNullItems++; } } @@ -51,10 +53,11 @@ 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()); + // noinspection ForLoopReplaceableByForEach: must be allocation free. + 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..6638b3531623 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,10 @@ 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()); + // noinspection ForLoopReplaceableByForEach: must be allocation free. + 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..44fcc484b68d 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,21 @@ 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()); + // noinspection ForLoopReplaceableByForEach: must be allocation free. + 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/JfrManager.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java index a83bcebbb805..05053e1a3a89 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java @@ -104,6 +104,7 @@ private static void parseFlightRecorderOptions() throws JfrArgumentParsingFailed Integer stackDepth = parseInteger(optionsArgs, FlightRecorderOptionsArgument.StackDepth); Boolean preserveRepository = parseBoolean(optionsArgs, FlightRecorderOptionsArgument.PreserveRepository); Long threadBufferSize = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.ThreadBufferSize); + String dumpPath = optionsArgs.get(FlightRecorderOptionsArgument.DumpPath); if (globalBufferSize != null) { Options.setGlobalBufferSize(globalBufferSize); @@ -144,6 +145,16 @@ private static void parseFlightRecorderOptions() throws JfrArgumentParsingFailed if (threadBufferSize != null) { Options.setThreadBufferSize(threadBufferSize); } + + try { + if (dumpPath != null) { + Options.setDumpPath(Paths.get(dumpPath)); + } else { + Options.setDumpPath(null); + } + } catch (Throwable e) { + throw new JfrArgumentParsingFailed("Could not use " + dumpPath + " as emergency dump path. " + e.getMessage(), e); + } } private static void setRepositoryBasePath(String repositoryPath) throws IOException { 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..2b63fbd385ab 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,19 +32,21 @@ import com.oracle.svm.core.monitor.MonitorInflationCause; public class JfrMonitorInflationCauseSerializer implements JfrSerializer { + private final MonitorInflationCause[] causes; + @Platforms(Platform.HOSTED_ONLY.class) public JfrMonitorInflationCauseSerializer() { + causes = 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.writeCompressedLong(causes.length); + for (int i = 0; i < causes.length; i++) { writer.writeCompressedInt(i); - writer.writeString(inflationCauses[i].getText()); + writer.writeString(causes[i].getText()); } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java index 0e77cd0f0e18..061194c0e1c9 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java @@ -348,8 +348,13 @@ private static JfrBuffer reuseOrReallocateBuffer(JfrBuffer oldBuffer, UnsignedWo if (oldBuffer.getSize().belowThan(minNewSize)) { // Grow the buffer because it is too small. UnsignedWord newSize = oldBuffer.getSize(); - while (newSize.belowThan(minNewSize)) { - newSize = newSize.multiply(2); + if (newSize.equal(0)) { + // avoid infinite loops + newSize = minNewSize; + } else { + while (newSize.belowThan(minNewSize)) { + newSize = newSize.multiply(2); + } } JfrBuffer result = JfrBufferAccess.allocate(newSize, oldBuffer.getBufferType()); @@ -364,7 +369,7 @@ private static JfrBuffer reuseOrReallocateBuffer(JfrBuffer oldBuffer, UnsignedWo JfrBufferAccess.free(oldBuffer); - assert result.getSize().aboveThan(minNewSize); + assert result.getSize().aboveOrEqual(minNewSize); return result; } else { // Reuse the existing buffer because enough data was already flushed in the meanwhile. 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..26b42d204d5f 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 final 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()); + // noinspection ForLoopReplaceableByForEach: must be allocation free. + 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..a9c252541af6 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,20 @@ */ package com.oracle.svm.core.jfr; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + +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 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 +46,7 @@ import com.oracle.svm.core.jdk.UninterruptibleUtils.ReplaceDotWithSlash; import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.locks.VMMutex; +import com.oracle.svm.core.memory.NullableNativeMemory; import com.oracle.svm.core.nmt.NmtCategory; import jdk.graal.compiler.core.common.SuppressFBWarnings; @@ -75,18 +81,38 @@ 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); + int length = UninterruptibleUtils.String.modifiedUTF8Length(imageHeapString, false); + Pointer buffer = NullableNativeMemory.malloc(length, NmtCategory.JFR); + if (buffer.isNull()) { + return 0; + } + UninterruptibleUtils.String.toModifiedUTF8(imageHeapString, imageHeapString.length(), buffer, buffer.add(length), false, replaceDotWithSlash ? dotWithSlash : null); - JfrSymbol symbol = StackValue.get(JfrSymbol.class); - symbol.setValue(imageHeapString); - symbol.setReplaceDotWithSlash(replaceDotWithSlash); + return getSymbolId(buffer, Word.unsigned(length), previousEpoch); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static int getHash(Pointer buffer, UnsignedWord length) { + long sum = 0; + for (int i = 0; length.aboveThan(i); i++) { + sum += buffer.readByte(i); + } + return UninterruptibleUtils.Long.hashCode(sum); + } + + @Uninterruptible(reason = "Locking without transition and result is only valid until epoch changes.", callerMustBe = true) + public long getSymbolId(Pointer buffer, UnsignedWord length, boolean previousEpoch) { - long rawPointerValue = Word.objectToUntrackedPointer(imageHeapString).rawValue(); - symbol.setHash(UninterruptibleUtils.Long.hashCode(rawPointerValue)); + assert buffer.isNonNull(); + JfrSymbol symbol = StackValue.get(JfrSymbol.class); + symbol.setModifiedUTF8(buffer); // symbol allocated in native memory + symbol.setLength(length); + symbol.setHash(getHash(buffer, length)); /* * Get an existing entry from the hashtable or insert a new entry. This needs to be atomic @@ -98,11 +124,13 @@ public long getSymbolId(String imageHeapString, boolean previousEpoch, boolean r JfrSymbolEpochData epochData = getEpochData(previousEpoch); JfrSymbol existingEntry = (JfrSymbol) epochData.table.get(symbol); if (existingEntry.isNonNull()) { + NullableNativeMemory.free(symbol.getModifiedUTF8()); return existingEntry.getId(); } JfrSymbol newEntry = (JfrSymbol) epochData.table.putNew(symbol); if (newEntry.isNull()) { + NullableNativeMemory.free(symbol.getModifiedUTF8()); return 0L; } @@ -111,13 +139,13 @@ 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, newEntry.getModifiedUTF8(), (int) newEntry.getLength().rawValue()); if (!JfrNativeEventWriter.commit(data)) { + NullableNativeMemory.free(symbol.getModifiedUTF8()); return 0L; } @@ -163,19 +191,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); + Pointer getModifiedUTF8(); } private static class JfrSymbolHashtable extends AbstractUninterruptibleHashtable { @@ -204,7 +230,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 +242,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..c8a0890f1921 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 final 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..6affa6166627 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,231 @@ */ 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.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 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: {@code epochTypeData*}, {@code flushed*}, and + * {@link #typeInfo}. The {@code flushed*} tables record which classes have already been flushed to + * disk. The {@code 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 + * {@link #typeInfo} table is derived at flushpoints by using the {@code epochTypeData*} and + * {@code 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 {@code 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 + * {@code 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 +261,243 @@ 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, Pointer source, UnsignedWord length, boolean flushpoint) { + Pointer destination = NullableNativeMemory.malloc(length, NmtCategory.JFR); + if (destination.isNull()) { + return 0L; + } + UnmanagedMemoryUtil.copy(source, destination, length); + + assert writer.isLockedByCurrentThread(); + return SubstrateJVM.getSymbolRepository().getSymbolId(destination, length, !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 + writer.writeCompressedLong(getSymbolId(writer, packageInfoRaw.getModifiedUTF8Name(), packageInfoRaw.getNameLength(), 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 +509,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(Word.nullPointer()); + packageInfoRaw.setNameLength(Word.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(Word.nullPointer()); + packageInfoRaw.setNameLength(Word.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 += (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(Pointer value); + + @RawField + Pointer getModifiedUTF8Name(); + } + + @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 static final class TypeInfo { - final Set> classes = new HashSet<>(); - final Map packages = new HashMap<>(); - final Map modules = new HashMap<>(); - final Map classLoaders = new HashMap<>(); + private final class 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(Word.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. + Pointer 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(sourceInfo.getModifiedUTF8Name(), 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..6707ac28e826 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,8 @@ import java.util.List; +import com.oracle.svm.core.os.RawFileOperationSupport; +import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platform; @@ -33,9 +35,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; @@ -49,6 +53,7 @@ import com.oracle.svm.core.thread.JavaThreads; import com.oracle.svm.core.thread.JavaVMOperation; import com.oracle.svm.core.thread.VMThreads; +import com.oracle.svm.core.util.BasedOnJDKFile; import com.oracle.svm.core.util.VMError; import jdk.graal.compiler.api.replacements.Fold; @@ -73,6 +78,8 @@ * */ public class SubstrateJVM { + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+13/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L569") // + private static final String OUT_OF_MEMORY = "Out of Memory"; private final List knownConfigurations; private final JfrOptionSet options; private final JfrNativeEventSetting[] eventSettings; @@ -101,7 +108,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 +342,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 +591,28 @@ 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()) { + return JfrEmergencyDumpSupport.singleton().getDumpPath(); } - return dumpPath; + return ""; } /** @@ -737,6 +751,37 @@ public Object getConfiguration(Class eventClass) { return DynamicHub.fromClass(eventClass).getJfrEventConfiguration(); } + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+3/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L559-L572") + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26%2B3/src/hotspot/share/jfr/recorder/service/jfrRecorderService.cpp#L510-L526") + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Used on OOME for emergency dumps") + public void vmOutOfMemoryErrorRotation() { + 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. + RawFileDescriptor fd = JfrEmergencyDumpSupport.singleton().chunkPath(); + if (RawFileOperationSupport.bigEndian().isValid(fd)) { + chunkWriter.openFile(fd); + } + } + if (chunkWriter.hasOpenFile()) { + chunkWriter.markChunkFinal(); + chunkWriter.closeFile(); + } + 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 +815,6 @@ protected void operate() { if (!SubstrateJVM.get().recording) { return; } - SubstrateJVM.get().recording = false; JfrExecutionSampler.singleton().update(); @@ -829,7 +873,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/logging/JfrLogTag.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/logging/JfrLogTag.java index e788cffb26f2..1652fc9f2e35 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/logging/JfrLogTag.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/logging/JfrLogTag.java @@ -33,7 +33,7 @@ * not provide the individual log tags. */ @BasedOnJDKClass(className = "jdk.jfr.internal.LogTag") -enum JfrLogTag { +public enum JfrLogTag { JFR, SYSTEM, EVENT, diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/logging/JfrLogging.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/logging/JfrLogging.java index 2ce716570fc1..5baacc6c1890 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/logging/JfrLogging.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/logging/JfrLogging.java @@ -62,8 +62,20 @@ public void parseConfiguration(String config) { } @RestrictHeapAccess(access = NO_ALLOCATION, reason = "May be used during OOME emergency dump.") - public void warnInternal(String message) { + public void logJfrSystemError(String message) { int tagSetId = SubstrateUtil.cast(LogTag.JFR_SYSTEM, Target_jdk_jfr_internal_LogTag.class).id; + log(tagSetId, JfrLogConfiguration.JfrLogLevel.ERROR.level, message); + } + + @RestrictHeapAccess(access = NO_ALLOCATION, reason = "May be used during OOME emergency dump.") + public void logJfrInfo(String message) { + int tagSetId = SubstrateUtil.cast(LogTag.JFR, Target_jdk_jfr_internal_LogTag.class).id; + log(tagSetId, JfrLogConfiguration.JfrLogLevel.INFO.level, message); + } + + @RestrictHeapAccess(access = NO_ALLOCATION, reason = "May be used during OOME emergency dump.") + public void logJfrWarning(String message) { + int tagSetId = SubstrateUtil.cast(LogTag.JFR, Target_jdk_jfr_internal_LogTag.class).id; log(tagSetId, JfrLogConfiguration.JfrLogLevel.WARNING.level, message); } 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..7757586841f0 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,15 @@ 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); + javaTimeSystemUTC(t); + long seconds = t.getSeconds(); + long nanos = t.getNanos(); long now = seconds * 1000000000 + nanos; if (now > last) { last = now; @@ -75,5 +67,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..8f18897a0a4f --- /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().vmOutOfMemoryErrorRotation(); + recording.stop(); + recording.close(); + + String dumpFile = "svm_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..66f0fd920706 --- /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 java.util.random.RandomGenerator; + +import static org.junit.Assert.assertTrue; + +public class TestGrowableWordArrayQuickSort { + @Test + public void test() throws Throwable { + RandomGenerator randomGenerator = RandomGenerator.getDefault(); + GrowableWordArray gwa = StackValue.get(GrowableWordArray.class); + GrowableWordArrayAccess.initialize(gwa); + long nextLong = 0; + for (int i = 0; i < 1000; i++) { + // Occasionally insert duplicates + if (i % 50 != 0) { + nextLong = randomGenerator.nextLong(); + } + GrowableWordArrayAccess.add(gwa, WordFactory.signed(nextLong), NmtCategory.JFR); + } + + GrowableWordArrayAccess.qsort(gwa, 0, gwa.getSize() - 1, TestGrowableWordArrayQuickSort::compare); + long last = GrowableWordArrayAccess.get(gwa, 0).rawValue(); + for (int i = 0; i < gwa.getSize(); i++) { + long current = GrowableWordArrayAccess.get(gwa, i).rawValue(); + assertTrue(last <= current); + last = current; + } + } + + static int compare(Word a, Word b) { + return Long.compare(a.rawValue(), b.rawValue()); + } + +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestJfrSymbolRepository.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestJfrSymbolRepository.java new file mode 100644 index 000000000000..ef50fa31a3aa --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestJfrSymbolRepository.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2022, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.test.jfr; + +import jdk.jfr.Recording; +import org.junit.Test; +import com.oracle.svm.core.jfr.JfrSymbolRepository; +import com.oracle.svm.core.jfr.SubstrateJVM; +import com.oracle.svm.core.Uninterruptible; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public class TestJfrSymbolRepository extends JfrRecordingTest { + @Test + public void test() throws Throwable { + // Ensure JFR is created in case this is the first test to run + String[] events = new String[]{}; + Recording recording = startRecording(events); + stopRecording(recording, null); + + String str1 = "string1"; + String str1copy = "string1"; + String str2 = "string2"; + + JfrSymbolRepository repo = SubstrateJVM.getSymbolRepository(); + + long id1 = getSymbolId(repo, str1); + long id2 = getSymbolId(repo, str2); + long id1copy = getSymbolId(repo, str1copy); + assertEquals(id1, id1copy); + assertNotEquals(id1, id2); + } + + @Uninterruptible(reason = "Needed for JfrSymbolRepository.getSymbolId().") + private static long getSymbolId(JfrSymbolRepository repo, String str) { + return repo.getSymbolId(str, false); + } +} 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); } }