diff --git a/docs/reference-manual/native-image/JFR.md b/docs/reference-manual/native-image/JFR.md index c2000c825b34..4f3e0b4eeb2d 100644 --- a/docs/reference-manual/native-image/JFR.md +++ b/docs/reference-manual/native-image/JFR.md @@ -83,7 +83,7 @@ Otherwise, this option expects a comma-separated list of tag combinations, each This section outlines the JFR features that are available in Native Image. -On Windows, Native Image supports local JFR recordings written to `.jfr` files. JFR emergency dumps on out-of-memory, remote JMX access to `FlightRecorderMXBean`, and JFR control through `jcmd` are not currently available on Windows. +On Windows, Native Image supports local JFR recordings written to `.jfr` files and JFR emergency dumps on out-of-memory. Remote JMX access to `FlightRecorderMXBean` and JFR control through `jcmd` are not currently available on Windows. ### Method Profiling and Stack Traces diff --git a/substratevm/mx.substratevm/mx_substratevm.py b/substratevm/mx.substratevm/mx_substratevm.py index 15519a24ec98..f6b531b823c0 100644 --- a/substratevm/mx.substratevm/mx_substratevm.py +++ b/substratevm/mx.substratevm/mx_substratevm.py @@ -417,7 +417,7 @@ def image_demo_task(extra_image_args=None, flightrecorder=True): helloworld(image_args + javac_command) if '--static' not in image_args: helloworld(image_args + ['--shared']) # Build and run helloworld as shared library - if not mx.is_windows() and flightrecorder: + if flightrecorder: helloworld(image_args + ['-J-XX:StartFlightRecording=dumponexit=true']) # Build and run helloworld with FlightRecorder at image build time if '--static' not in image_args: cinterfacetutorial(extra_image_args) diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/Time.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/Time.java index 2b7ef0af3dd1..cdef89fa70b2 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/Time.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/Time.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -97,6 +97,23 @@ public interface itimerval extends PointerBase { @CStruct(addStructKeyword = true) public interface tm extends PointerBase { + @CField + int tm_sec(); + + @CField + int tm_min(); + + @CField + int tm_hour(); + + @CField + int tm_mday(); + + @CField + int tm_mon(); + + @CField + int tm_year(); } @CEnum diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java index c1d16ed10946..a952e39f372a 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/jfr/PosixJfrEmergencyDumpSupport.java @@ -41,21 +41,19 @@ import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.collections.GrowableWordArray; -import com.oracle.svm.core.collections.GrowableWordArrayAccess; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.headers.LibC; +import com.oracle.svm.core.jfr.AbstractJfrEmergencyDumpSupport; 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.nmt.NmtCategory; -import com.oracle.svm.core.os.RawFileOperationSupport; -import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; -import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; +import com.oracle.svm.core.os.RawFileOperationSupport.RawFilePath; 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.Time; import com.oracle.svm.core.posix.headers.Unistd; import com.oracle.svm.shared.Uninterruptible; import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; @@ -70,220 +68,85 @@ @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25-ga/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L43-L445") @SingletonTraits(access = AllAccess.class, layeredCallbacks = NoLayeredCallbacks.class, layeredInstallationKind = Duplicable.class, other = PartiallyLayerAware.class) -public class PosixJfrEmergencyDumpSupport implements com.oracle.svm.core.jfr.JfrEmergencyDumpSupport { - private static final int CHUNK_FILE_HEADER_SIZE = 68; - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25-ga/src/hotspot/os/posix/include/jvm_md.h#L57") // - private static final int JVM_MAXPATHLEN = 4096; - private static final int ISO_8601_LEN = 19; +public class PosixJfrEmergencyDumpSupport extends AbstractJfrEmergencyDumpSupport { 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 int directoryFd; private byte[] pidBytes; + private byte[] cwdBytes; private byte[] dumpPathBytes; private byte[] repositoryLocationBytes; - private byte[] cwdBytes; - private RawFileDescriptor emergencyFd; private CCharPointer pathBuffer; private boolean pathBufferInitialized; - private int emergencyChunkPathCallCount; - private String openFileWarning; - private String openDirectoryWarning; @Platforms(Platform.HOSTED_ONLY.class) public PosixJfrEmergencyDumpSupport() { } @Override - public void initialize() { - savePid(); + protected void allocatePathBufferIfNeeded() { if (!pathBufferInitialized) { pathBuffer = NativeMemory.calloc(JVM_MAXPATHLEN + 1, NmtCategory.JFR); pathBufferInitialized = true; } - directory = Word.nullPointer(); - directoryFd = -1; - saveCwd(); - } - - private void savePid() { - if (pidBytes == null) { - pidBytes = Long.toString(ProcessHandle.current().pid()).getBytes(StandardCharsets.UTF_8); - } } - private void saveCwd() { - if (cwdBytes == null) { - String cwd = System.getProperty("user.dir"); - if (cwd != null) { - cwdBytes = cwd.getBytes(StandardCharsets.UTF_8); - } - } + @Override + protected void initializeRepositoryState() { + directory = Word.nullPointer(); + directoryFd = -1; } @Override - public void setRepositoryLocation(String dirText) { - repositoryLocationBytes = dirText.getBytes(StandardCharsets.UTF_8); - if (isRepositoryLocationTooLong(repositoryLocationBytes)) { - openDirectoryWarning = "Unable to open repository " + dirText + ". Repository path is too long."; - } else { - openDirectoryWarning = "Unable to open repository " + dirText; - } + protected void setPid(String pid) { + pidBytes = pid.getBytes(StandardCharsets.UTF_8); } - /** 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) { - savePid(); // setDumpPath may be called before initalize() when setting JFR arguments. - if (isDumpPathTooLong(dumpPathBytes)) { - openFileWarning = "Unable to create an emergency dump file at the location set by dumppath=" + new String(dumpPathBytes, StandardCharsets.UTF_8) + ". Dump path is too long."; - } else { - 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."; - } - + protected void setSavedCwdText(String cwd) { + cwdBytes = cwd.getBytes(StandardCharsets.UTF_8); } @Override - public String getDumpPath() { - if (dumpPathBytes != null) { - return new String(dumpPathBytes, StandardCharsets.UTF_8); - } - return ""; + protected void setDumpPathText(String dumpPath) { + dumpPathBytes = dumpPath == null ? null : dumpPath.getBytes(StandardCharsets.UTF_8); } - /** - * This method either creates and uses the dumpfile itself as a new chunk, or creates a new file - * in the repository location. - */ @Override - public RawFileDescriptor chunkPath() { - if (repositoryLocationBytes == null) { - if (!openEmergencyDumpFile()) { - return Word.nullPointer(); - } - /* - * We can directly use the emergency dump file name as the new chunk since there are no - * other chunk files. - */ - RawFileDescriptor fd = emergencyFd; - emergencyFd = Word.nullPointer(); - return fd; - } - return createEmergencyChunkPath(); + protected void setDumpPathToSavedCwd() { + dumpPathBytes = cwdBytes; } - /** - * 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. - */ - private RawFileDescriptor createEmergencyChunkPath() { - emergencyChunkPathCallCount++; - if (isRepositoryLocationTooLong(repositoryLocationBytes)) { - return Word.nullPointer(); - } - 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((RawFileOperationSupport.RawFilePath) getPathBuffer(), FileCreationMode.CREATE_OR_REPLACE, FileAccessMode.READ_WRITE); + @Override + protected void setRepositoryLocationText(String repositoryLocation) { + repositoryLocationBytes = repositoryLocation.getBytes(StandardCharsets.UTF_8); } @Override - public void onVmError() { - if (repositoryLocationBytes == null) { - return; - } - if (openEmergencyDumpFile()) { - GrowableWordArray sortedChunkFilenames = StackValue.get(GrowableWordArray.class); - GrowableWordArrayAccess.initialize(sortedChunkFilenames); - try { - if (openDirectory()) { - try { - /* - * Keep the repository directory open for the whole scan so validation and - * chunk reopening stay anchored to the same directory instance. - */ - iterateRepository(sortedChunkFilenames); - writeEmergencyDumpFile(sortedChunkFilenames); - } finally { - closeDirectory(); - } - } - } finally { - freeChunkFilenames(sortedChunkFilenames); - GrowableWordArrayAccess.freeData(sortedChunkFilenames); - closeEmergencyDumpFile(); - sortedChunkFilenames = Word.nullPointer(); - } + protected long getPathBufferAddress() { + if (!pathBufferInitialized) { + return 0L; } + return pathBuffer.rawValue(); } - private boolean openEmergencyDumpFile() { - if (getFileSupport().isValid(emergencyFd)) { - return true; - } - // O_CREAT | O_RDWR and S_IREAD | S_IWRITE permissions - emergencyFd = createEmergencyDumpFile(); - if (!getFileSupport().isValid(emergencyFd)) { - SubstrateJVM.getLogging().logJfrWarning(openFileWarning); - // Fallback. Try to create it in the current directory. - dumpPathBytes = null; - emergencyFd = createEmergencyDumpFile(); - } - return getFileSupport().isValid(emergencyFd); + @Override + protected int pidLength() { + return pidBytes.length; } - private RawFileDescriptor createEmergencyDumpFile() { - CCharPointer path = createEmergencyDumpPath(); - if (path.isNull()) { - return Word.nullPointer(); - } - return getFileSupport().create((RawFileOperationSupport.RawFilePath) path, FileCreationMode.CREATE, FileAccessMode.READ_WRITE); + @Override + protected int dumpPathLength() { + return dumpPathBytes == null ? -1 : dumpPathBytes.length; } - private CCharPointer createEmergencyDumpPath() { - int idx = 0; - clearPathBuffer(); - - if (dumpPathBytes == null) { - dumpPathBytes = cwdBytes; - } - if (isDumpPathTooLong(dumpPathBytes)) { - return Word.nullPointer(); - } - 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(); + @Override + protected int repositoryLocationLength() { + return repositoryLocationBytes == null ? -1 : repositoryLocationBytes.length; } - private void iterateRepository(GrowableWordArray gwa) { + @Override + protected void iterateRepository(GrowableWordArray gwa) { int count = 0; if (directory.isNull()) { return; @@ -291,102 +154,19 @@ private void iterateRepository(GrowableWordArray gwa) { // 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. - if (filter(entry)) { - CCharPointer fn = entry.d_name(); - CCharPointer fnCopy = LibC.strdup(fn); - if (fnCopy.isNull()) { - SubstrateJVM.getLogging().logJfrSystemError("Unable to copy chunk filename during jfr emergency dump"); - continue; - } - // Append filtered files to list. - if (!GrowableWordArrayAccess.add(gwa, (Word) (Pointer) fnCopy, NmtCategory.JFR)) { - LibC.free(fnCopy); - SubstrateJVM.getLogging().logJfrSystemError("Unable to add chunk filename to list during jfr emergency dump"); - } else { - count++; - } + CCharPointer fn = entry.d_name(); + int filenameLength = (int) SubstrateUtil.strlen(fn).rawValue(); + if (addUsableChunkFilename(gwa, (Word) (Pointer) fn, filenameLength)) { + count++; } } - if (count > 0) { - GrowableWordArrayAccess.qsort(gwa, 0, count - 1, PosixJfrEmergencyDumpSupport::compare); - } + sortChunkFilenames(gwa, count); } - static int compare(Word a, Word b) { - CCharPointer filenameA = (CCharPointer) ((Pointer) a); - CCharPointer filenameB = (CCharPointer) ((Pointer) b); - int lengthA = (int) SubstrateUtil.strlen(filenameA).rawValue(); - int lengthB = (int) SubstrateUtil.strlen(filenameB).rawValue(); - boolean emergencyChunkA = isEmergencyChunkFilename(filenameA, lengthA); - boolean emergencyChunkB = isEmergencyChunkFilename(filenameB, lengthB); - if (emergencyChunkA || emergencyChunkB) { - assert !(emergencyChunkA && emergencyChunkB) : "repository must not contain multiple emergency chunk files"; - if (emergencyChunkA && emergencyChunkB) { - return 0; - } - return emergencyChunkA ? 1 : -1; - } - - 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) ((Pointer) GrowableWordArrayAccess.get(sortedChunkFilenames, i)); - RawFileDescriptor chunkFd = openRepositoryFile(fn); - if (getFileSupport().isValid(chunkFd)) { - - // Read it's size - long chunkFileSize = getFileSupport().size(chunkFd); - long bytesRead = 0; - if (!getFileSupport().seek(chunkFd, 0)) { - SubstrateJVM.getLogging().logJfrInfo("Unable to recover JFR data, seek failed."); - getFileSupport().close(chunkFd); - continue; - } - while (bytesRead < chunkFileSize) { - // Read from chunk file to copy block - long readResult = getFileSupport().read(chunkFd, copyBlock, Word.unsigned(blockSize)); - if (readResult <= 0) { - if (readResult < 0) { - SubstrateJVM.getLogging().logJfrInfo("Unable to recover JFR data, read failed."); - } - break; - } - bytesRead += readResult; - // Write from copy block to dump file - if (!getFileSupport().write(emergencyFd, copyBlock, Word.unsigned(readResult))) { - SubstrateJVM.getLogging().logJfrInfo("Unable to recover JFR data, write failed."); - break; - } - } - getFileSupport().close(chunkFd); - } - } - NullableNativeMemory.free(copyBlock); + @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") + @Override + protected RawFileDescriptor openRepositoryFile(Word filename) { + return openRepositoryFile((CCharPointer) ((Pointer) filename)); } @Uninterruptible(reason = "LibC.errno() must not be overwritten accidentally.") @@ -399,19 +179,11 @@ private RawFileDescriptor openRepositoryFile(CCharPointer fn) { return Word.signed(restartableOpenat(directoryFd, fn, O_RDONLY() | O_NOFOLLOW(), 0)); } - private static void freeChunkFilenames(GrowableWordArray chunkFilenames) { - for (int i = 0; i < chunkFilenames.getSize(); i++) { - CCharPointer fn = (CCharPointer) ((Pointer) GrowableWordArrayAccess.get(chunkFilenames, i)); - if (fn.isNonNull()) { - LibC.free(fn); - } - } - } - - private boolean openDirectory() { + @Override + protected boolean openRepository() { CCharPointer repositoryLocation = getRepositoryLocation(); if (repositoryLocation.isNull()) { - SubstrateJVM.getLogging().logJfrSystemError(openDirectoryWarning); + SubstrateJVM.getLogging().logJfrSystemError(getOpenDirectoryWarning()); return false; } int fd = Fcntl.NoTransitions.restartableOpen(repositoryLocation, O_RDONLY() | O_NOFOLLOW(), 0); @@ -422,7 +194,7 @@ private boolean openDirectory() { directory = Dirent.fdopendir(fd); if (directory.isNull()) { Unistd.NoTransitions.close(fd); - SubstrateJVM.getLogging().logJfrSystemError(openDirectoryWarning); + SubstrateJVM.getLogging().logJfrSystemError(getOpenDirectoryWarning()); return false; } directoryFd = fd; @@ -430,7 +202,7 @@ private boolean openDirectory() { } private CCharPointer getRepositoryLocation() { - if (repositoryLocationBytes == null || isRepositoryLocationTooLong(repositoryLocationBytes)) { + if (repositoryLocationBytes == null || isRepositoryLocationTooLong()) { return Word.nullPointer(); } clearPathBuffer(); @@ -448,98 +220,52 @@ private static int restartableOpenat(int fd, CCharPointer filename, int flags, i return result; } - private boolean filter(Dirent.dirent entry) { - CCharPointer fn = entry.d_name(); - - // 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; - } + @Override + protected int appendCurrentDateTimeToPathBuffer(int idx) { + Time.timeval time = StackValue.get(Time.timeval.class); + if (Time.NoTransitions.gettimeofday(time, Word.nullPointer()) != 0) { + return -1; } - boolean emergencyChunk = isEmergencyChunkFilename(fn, filenameLength); - // Merge timestamped repository chunks and the synthetic emergency repository chunk only. - if (!emergencyChunk && !hasChunkFilenameFormat(fn, filenameLength - CHUNKFILE_EXTENSION_BYTES.length)) { - return false; + Time.tm localTime = StackValue.get(Time.tm.class); + if (Time.NoTransitions.localtime_r(time.addressOftv_sec(), localTime).isNull()) { + return -1; } + return writeDateTimeToPathBuffer(idx, localTime.tm_year() + 1900, localTime.tm_mon() + 1, localTime.tm_mday(), localTime.tm_hour(), localTime.tm_min(), localTime.tm_sec()); + } - // Verify it can be opened and receive a valid file descriptor. - RawFileDescriptor chunkFd = openRepositoryFile(fn); - if (!getFileSupport().isValid(chunkFd)) { - return false; - } + @Override + protected RawFilePath pathBuffer() { + return (RawFilePath) getPathBuffer(); + } - // Verify file size - long chunkFileSize = getFileSupport().size(chunkFd); - getFileSupport().close(chunkFd); - if (chunkFileSize < CHUNK_FILE_HEADER_SIZE) { - return false; - } - return true; + private CCharPointer getPathBuffer() { + return pathBuffer; } - private static boolean isEmergencyChunkFilename(CCharPointer fn, int filenameLength) { - int expectedLength = EMERGENCY_CHUNK_BYTES.length + CHUNKFILE_EXTENSION_BYTES.length; - if (filenameLength != expectedLength) { - return false; - } - for (int i = 0; i < EMERGENCY_CHUNK_BYTES.length; i++) { - if (fn.read(i) != EMERGENCY_CHUNK_BYTES[i]) { - return false; - } - } - for (int i = 0; i < CHUNKFILE_EXTENSION_BYTES.length; i++) { - if (fn.read(EMERGENCY_CHUNK_BYTES.length + i) != CHUNKFILE_EXTENSION_BYTES[i]) { - return false; - } - } - return true; + @Override + protected void clearPathBuffer() { + LibC.memset(getPathBuffer(), Word.signed(0), Word.unsigned(JVM_MAXPATHLEN)); } - private static boolean hasChunkFilenameFormat(CCharPointer fn, int baseNameLength) { - if (baseNameLength < ISO_8601_LEN) { - return false; - } - // Repository chunks are timestamped as yyyy_MM_dd_HH_mm_ss[_NN].jfr. - for (int i = 0; i < ISO_8601_LEN; i++) { - byte ch = fn.read(i); - if (i == 4 || i == 7 || i == 10 || i == 13 || i == 16) { - if (ch != '_') { - return false; - } - } else if (ch < '0' || ch > '9') { - return false; - } - } - if (baseNameLength == ISO_8601_LEN) { - return true; - } - if (fn.read(ISO_8601_LEN) != '_' || baseNameLength == ISO_8601_LEN + 1) { - return false; - } - for (int i = ISO_8601_LEN + 1; i < baseNameLength; i++) { - byte ch = fn.read(i); - if (ch < '0' || ch > '9') { - return false; - } - } - return true; + @Override + protected int appendDumpPathToPathBuffer(int idx) { + return writeToPathBuffer(dumpPathBytes, idx); } - private CCharPointer getPathBuffer() { - return pathBuffer; + @Override + protected int appendRepositoryLocationToPathBuffer(int idx) { + return writeToPathBuffer(repositoryLocationBytes, idx); } - private void clearPathBuffer() { - LibC.memset(getPathBuffer(), Word.signed(0), Word.unsigned(JVM_MAXPATHLEN)); + @Override + protected int appendPidToPathBuffer(int idx) { + return writeToPathBuffer(pidBytes, idx); + } + + @Override + protected int appendPathSeparatorToPathBuffer(int idx) { + getPathBuffer().write(idx, FILE_SEPARATOR); + return idx + 1; } private int writeToPathBuffer(byte[] bytes, int start) { @@ -550,26 +276,28 @@ private int writeToPathBuffer(byte[] bytes, int start) { return idx; } - private static boolean isRepositoryLocationTooLong(byte[] repositoryLocation) { - return repositoryLocation != null && repositoryLocation.length + 1L + EMERGENCY_CHUNK_BYTES.length + CHUNKFILE_EXTENSION_BYTES.length >= JVM_MAXPATHLEN; + @Override + protected int filenameCharAt(Word filename, int index) { + return filename.readByte(index); } - private boolean isDumpPathTooLong(byte[] dumpPath) { - return dumpPath != null && dumpPath.length + 1L + DUMP_FILE_PREFIX.length + pidBytes.length + CHUNKFILE_EXTENSION_BYTES.length >= JVM_MAXPATHLEN; + @Override + protected Word copyChunkFilename(Word filename, int filenameLength) { + return (Word) (Pointer) LibC.strdup((CCharPointer) ((Pointer) filename)); } - static RawFileOperationSupport getFileSupport() { - return RawFileOperationSupport.bigEndian(); + @Override + protected void writePathBufferChar(int index, int ch) { + getPathBuffer().write(index, (byte) ch); } - private void closeEmergencyDumpFile() { - if (getFileSupport().isValid(emergencyFd)) { - getFileSupport().close(emergencyFd); - emergencyFd = Word.nullPointer(); - } + @Override + protected void freeChunkFilename(Word filename) { + LibC.free(filename); } - private void closeDirectory() { + @Override + protected void closeRepository() { if (directory.isNonNull()) { Dirent.closedir(directory); directory = Word.nullPointer(); @@ -578,31 +306,13 @@ private void closeDirectory() { } @Override - public void teardown() { - closeEmergencyDumpFile(); - closeDirectory(); + protected void freePathBufferIfInitialized() { if (pathBufferInitialized) { NativeMemory.free(pathBuffer); + pathBuffer = Word.nullPointer(); pathBufferInitialized = false; } } - - public static class TestingBackdoor { - public static long getPathBufferAddress(PosixJfrEmergencyDumpSupport support) { - if (!support.pathBufferInitialized) { - return 0L; - } - return support.pathBuffer.rawValue(); - } - - public static int getEmergencyChunkPathCallCount(PosixJfrEmergencyDumpSupport support) { - return support.emergencyChunkPathCallCount; - } - - public static void resetEmergencyChunkPathCallCount(PosixJfrEmergencyDumpSupport support) { - support.emergencyChunkPathCallCount = 0; - } - } } @AutomaticallyRegisteredFeature @@ -610,7 +320,7 @@ public static void resetEmergencyChunkPathCallCount(PosixJfrEmergencyDumpSupport class PosixJfrEmergencyDumpFeature implements InternalFeature { @Override public boolean isInConfiguration(IsInConfigurationAccess access) { - return VMInspectionOptions.hasJfrSupport(); + return VMInspectionOptions.hasJfrSupport() && (Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.DARWIN.class)); } @Override diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/FileAPI.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/FileAPI.java index d6da53e31cb1..72121c6ed7ea 100644 --- a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/FileAPI.java +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/FileAPI.java @@ -29,6 +29,9 @@ import org.graalvm.nativeimage.c.CContext; import org.graalvm.nativeimage.c.constant.CConstant; import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.struct.CField; +import org.graalvm.nativeimage.c.struct.CFieldAddress; +import org.graalvm.nativeimage.c.struct.CStruct; import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CIntPointer; import org.graalvm.nativeimage.c.type.CLongPointer; @@ -81,6 +84,18 @@ public static native HANDLE CreateFileW(WCharPointer lpFileName, int dwDesiredAc @CConstant public static native int FILE_ATTRIBUTE_NORMAL(); + @CConstant + public static native int FILE_ATTRIBUTE_DIRECTORY(); + + @CConstant + public static native int FILE_ATTRIBUTE_REPARSE_POINT(); + + @CConstant + public static native int FILE_FLAG_OPEN_REPARSE_POINT(); + + @CConstant + public static native int FILE_FLAG_BACKUP_SEMANTICS(); + @CConstant public static native int FILE_BEGIN(); @@ -109,6 +124,30 @@ public static native int WriteFile(HANDLE hFile, CCharPointer lpBuffer, int nNum @CFunction(transition = NO_TRANSITION) public static native int GetTempPathW(int nBufferLength, WCharPointer lpBuffer); + @CFunction(transition = NO_TRANSITION) + public static native int GetFileAttributesW(WCharPointer lpFileName); + + @CFunction(transition = NO_TRANSITION) + public static native HANDLE FindFirstFileW(WCharPointer lpFileName, WIN32_FIND_DATAW lpFindFileData); + + @CFunction(transition = NO_TRANSITION) + public static native int FindNextFileW(HANDLE hFindFile, WIN32_FIND_DATAW lpFindFileData); + + @CFunction(transition = NO_TRANSITION) + public static native int FindClose(HANDLE hFindFile); + + @CConstant + public static native int INVALID_FILE_ATTRIBUTES(); + + @CStruct("WIN32_FIND_DATAW") + public interface WIN32_FIND_DATAW extends PointerBase { + @CField("dwFileAttributes") + int getFileAttributes(); + + @CFieldAddress("cFileName") + WCharPointer getFileName(); + } + public static class NoTransition { @CFunction(transition = NO_TRANSITION) public static native int ReadFile(HANDLE hFile, CCharPointer lpBuffer, int nNumberOfBytesToRead, CIntPointer lpNumberOfBytesRead, PointerBase lpOverlapped); diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/SysinfoAPI.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/SysinfoAPI.java index 783c340016ef..f81bc100db37 100644 --- a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/SysinfoAPI.java +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/headers/SysinfoAPI.java @@ -53,6 +53,10 @@ public class SysinfoAPI { @CFunction(transition = NO_TRANSITION) public static native void GetSystemInfo(SYSTEM_INFO lpSystemInfo); + /** Retrieves the current local date and time. */ + @CFunction(transition = NO_TRANSITION) + public static native void GetLocalTime(SYSTEMTIME lpSystemTime); + /** Structure containing information about the current computer system. */ @CStruct public interface SYSTEM_INFO extends PointerBase { @@ -90,6 +94,34 @@ public interface SYSTEM_INFO extends PointerBase { short wProcessorRevision(); } + /** Structure containing a date and time. */ + @CStruct + public interface SYSTEMTIME extends PointerBase { + @CField + short wYear(); + + @CField + short wMonth(); + + @CField + short wDayOfWeek(); + + @CField + short wDay(); + + @CField + short wHour(); + + @CField + short wMinute(); + + @CField + short wSecond(); + + @CField + short wMilliseconds(); + } + /** * Retrieves the current system date and time. The information is in Coordinated Universal Time * (UTC) format. diff --git a/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/jfr/WindowsJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/jfr/WindowsJfrEmergencyDumpSupport.java new file mode 100644 index 000000000000..03a484d5213e --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.windows/src/com/oracle/svm/core/windows/jfr/WindowsJfrEmergencyDumpSupport.java @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 2026, IBM 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.windows.jfr; + +import static com.oracle.svm.core.windows.headers.WinBase.INVALID_HANDLE_VALUE; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.word.Pointer; +import org.graalvm.word.impl.Word; + +import com.oracle.svm.core.VMInspectionOptions; +import com.oracle.svm.core.collections.GrowableWordArray; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.jfr.AbstractJfrEmergencyDumpSupport; +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.nmt.NmtCategory; +import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; +import com.oracle.svm.core.os.RawFileOperationSupport.RawFilePath; +import com.oracle.svm.core.windows.headers.FileAPI; +import com.oracle.svm.core.windows.headers.FileAPI.WIN32_FIND_DATAW; +import com.oracle.svm.core.windows.headers.SysinfoAPI; +import com.oracle.svm.core.windows.headers.SysinfoAPI.SYSTEMTIME; +import com.oracle.svm.core.windows.headers.WinBase; +import com.oracle.svm.core.windows.headers.WinBase.HANDLE; +import com.oracle.svm.core.windows.headers.WindowsLibC; +import com.oracle.svm.core.windows.headers.WindowsLibC.WCharPointer; +import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.shared.singletons.traits.BuiltinTraits.AllAccess; +import com.oracle.svm.shared.singletons.traits.BuiltinTraits.BuildtimeAccessOnly; +import com.oracle.svm.shared.singletons.traits.BuiltinTraits.NoLayeredCallbacks; +import com.oracle.svm.shared.singletons.traits.BuiltinTraits.PartiallyLayerAware; +import com.oracle.svm.shared.singletons.traits.SingletonLayeredInstallationKind.Duplicable; +import com.oracle.svm.shared.singletons.traits.SingletonTraits; +import com.oracle.svm.shared.util.BasedOnJDKFile; + +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25-ga/src/hotspot/share/jfr/recorder/repository/jfrEmergencyDump.cpp#L43-L445") +@SingletonTraits(access = AllAccess.class, layeredCallbacks = NoLayeredCallbacks.class, layeredInstallationKind = Duplicable.class, other = PartiallyLayerAware.class) +public class WindowsJfrEmergencyDumpSupport extends AbstractJfrEmergencyDumpSupport { + private static final char FILE_SEPARATOR = '\\'; + private static final char ALT_FILE_SEPARATOR = '/'; + private static final char WILDCARD = '*'; + + private char[] pidChars; + private char[] cwdChars; + private char[] dumpPathChars; + private char[] repositoryLocationChars; + private WCharPointer pathBuffer; + private boolean pathBufferInitialized; + private HANDLE repositoryDirectoryGuardHandle; + private HANDLE repositoryFindHandle; + + @Platforms(Platform.HOSTED_ONLY.class) + public WindowsJfrEmergencyDumpSupport() { + } + + @Override + protected void allocatePathBufferIfNeeded() { + if (!pathBufferInitialized) { + pathBuffer = NativeMemory.calloc(Word.unsigned(JVM_MAXPATHLEN + 1).multiply(Character.BYTES), NmtCategory.JFR); + pathBufferInitialized = true; + } + } + + @Override + protected void initializeRepositoryState() { + repositoryDirectoryGuardHandle = Word.nullPointer(); + repositoryFindHandle = Word.nullPointer(); + } + + @Override + protected void setPid(String pid) { + pidChars = pid.toCharArray(); + } + + @Override + protected void setSavedCwdText(String cwd) { + cwdChars = cwd.toCharArray(); + } + + @Override + protected void setDumpPathText(String dumpPath) { + dumpPathChars = dumpPath == null ? null : dumpPath.toCharArray(); + } + + @Override + protected void setDumpPathToSavedCwd() { + dumpPathChars = cwdChars; + } + + @Override + protected void setRepositoryLocationText(String repositoryLocation) { + repositoryLocationChars = repositoryLocation.toCharArray(); + } + + @Override + protected long getPathBufferAddress() { + if (!pathBufferInitialized) { + return 0L; + } + return pathBuffer.rawValue(); + } + + @Override + protected int pidLength() { + return pidChars.length; + } + + @Override + protected int dumpPathLength() { + return dumpPathChars == null ? -1 : dumpPathChars.length; + } + + @Override + protected int repositoryLocationLength() { + return repositoryLocationChars == null ? -1 : repositoryLocationChars.length; + } + + @Override + protected void iterateRepository(GrowableWordArray gwa) { + if (!isValidHandle(repositoryDirectoryGuardHandle)) { + return; + } + + WIN32_FIND_DATAW findData = StackValue.get(WIN32_FIND_DATAW.class); + /* + * Win32 directory enumeration is path-based. The guard handle above keeps the already + * validated non-reparse repository directory open for the duration of the scan. + */ + HANDLE handle = FileAPI.FindFirstFileW(createRepositorySearchPath(), findData); + if (handle.equal(INVALID_HANDLE_VALUE())) { + SubstrateJVM.getLogging().logJfrSystemError(getOpenDirectoryWarning()); + return; + } + repositoryFindHandle = handle; + + int count = 0; + boolean done = false; + while (!done) { + if (isRegularFileAttributes(findData.getFileAttributes())) { + WCharPointer fn = findData.getFileName(); + int filenameLength = stringLength(fn); + if (addUsableChunkFilename(gwa, (Word) (Pointer) fn, filenameLength)) { + count++; + } + } + done = FileAPI.FindNextFileW(repositoryFindHandle, findData) == 0; + } + + sortChunkFilenames(gwa, count); + } + + private WCharPointer createRepositorySearchPath() { + clearPathBuffer(); + int idx = 0; + idx = appendCharsToPathBuffer(repositoryLocationChars, idx); + idx = appendPathSeparatorToPathBuffer(idx); + writePathBufferChar(idx++, WILDCARD); + writePathBufferNull(idx); + return pathBuffer; + } + + @Override + protected RawFileDescriptor openRepositoryFile(Word filename) { + return openRepositoryFile((WCharPointer) ((Pointer) filename)); + } + + private RawFileDescriptor openRepositoryFile(WCharPointer fn) { + WCharPointer path = createRepositoryFilePath(fn); + if (path.isNull() || !hasRegularFileAttributes(path)) { + return Word.nullPointer(); + } + HANDLE h = FileAPI.CreateFileW(path, FileAPI.GENERIC_READ(), FileAPI.FILE_SHARE_READ() | FileAPI.FILE_SHARE_WRITE() | FileAPI.FILE_SHARE_DELETE(), Word.nullPointer(), + FileAPI.OPEN_EXISTING(), FileAPI.FILE_ATTRIBUTE_NORMAL() | FileAPI.FILE_FLAG_OPEN_REPARSE_POINT(), Word.nullPointer()); + if (h.equal(INVALID_HANDLE_VALUE())) { + return Word.nullPointer(); + } + return (RawFileDescriptor) Word.pointer(h.rawValue()); + } + + private WCharPointer createRepositoryFilePath(WCharPointer fn) { + if (repositoryLocationChars == null || isRepositoryLocationTooLong()) { + return Word.nullPointer(); + } + int filenameLength = stringLength(fn); + int separatorLength = needsFileSeparator(repositoryLocationChars) ? 1 : 0; + if (repositoryLocationChars.length + separatorLength + filenameLength >= JVM_MAXPATHLEN) { + return Word.nullPointer(); + } + clearPathBuffer(); + int idx = 0; + idx = appendCharsToPathBuffer(repositoryLocationChars, idx); + idx = appendPathSeparatorToPathBuffer(idx); + idx = appendCharsToPathBuffer(fn, idx); + writePathBufferNull(idx); + return pathBuffer; + } + + @Override + protected boolean openRepository() { + WCharPointer repositoryLocation = getRepositoryLocation(); + if (repositoryLocation.isNull() || !hasDirectoryAttributes(repositoryLocation)) { + SubstrateJVM.getLogging().logJfrSystemError(getOpenDirectoryWarning()); + return false; + } + + repositoryDirectoryGuardHandle = FileAPI.CreateFileW(repositoryLocation, FileAPI.GENERIC_READ(), FileAPI.FILE_SHARE_READ() | FileAPI.FILE_SHARE_WRITE(), Word.nullPointer(), + FileAPI.OPEN_EXISTING(), FileAPI.FILE_FLAG_BACKUP_SEMANTICS() | FileAPI.FILE_FLAG_OPEN_REPARSE_POINT(), Word.nullPointer()); + if (!isValidHandle(repositoryDirectoryGuardHandle)) { + SubstrateJVM.getLogging().logJfrSystemError(getOpenDirectoryWarning()); + return false; + } + return true; + } + + private WCharPointer getRepositoryLocation() { + if (repositoryLocationChars == null || isRepositoryLocationTooLong()) { + return Word.nullPointer(); + } + clearPathBuffer(); + int idx = appendCharsToPathBuffer(repositoryLocationChars, 0); + writePathBufferNull(idx); + return pathBuffer; + } + + private static boolean hasRegularFileAttributes(WCharPointer path) { + int attributes = FileAPI.GetFileAttributesW(path); + return attributes != FileAPI.INVALID_FILE_ATTRIBUTES() && isRegularFileAttributes(attributes); + } + + private static boolean hasDirectoryAttributes(WCharPointer path) { + int attributes = FileAPI.GetFileAttributesW(path); + return attributes != FileAPI.INVALID_FILE_ATTRIBUTES() && isDirectoryAttributes(attributes); + } + + private static boolean isRegularFileAttributes(int attributes) { + return (attributes & FileAPI.FILE_ATTRIBUTE_DIRECTORY()) == 0 && (attributes & FileAPI.FILE_ATTRIBUTE_REPARSE_POINT()) == 0; + } + + private static boolean isDirectoryAttributes(int attributes) { + return (attributes & FileAPI.FILE_ATTRIBUTE_DIRECTORY()) != 0 && (attributes & FileAPI.FILE_ATTRIBUTE_REPARSE_POINT()) == 0; + } + + @Override + protected int appendCurrentDateTimeToPathBuffer(int idx) { + SYSTEMTIME localTime = StackValue.get(SYSTEMTIME.class); + SysinfoAPI.GetLocalTime(localTime); + return writeDateTimeToPathBuffer(idx, localTime.wYear(), localTime.wMonth(), localTime.wDay(), localTime.wHour(), localTime.wMinute(), localTime.wSecond()); + } + + @Override + protected RawFilePath pathBuffer() { + return (RawFilePath) pathBuffer; + } + + @Override + protected void clearPathBuffer() { + WindowsLibC.memset(pathBuffer, Word.signed(0), Word.unsigned(JVM_MAXPATHLEN + 1).multiply(Character.BYTES)); + } + + @Override + protected int appendDumpPathToPathBuffer(int idx) { + return appendCharsToPathBuffer(dumpPathChars, idx); + } + + @Override + protected int appendRepositoryLocationToPathBuffer(int idx) { + return appendCharsToPathBuffer(repositoryLocationChars, idx); + } + + @Override + protected int appendPidToPathBuffer(int idx) { + return appendCharsToPathBuffer(pidChars, idx); + } + + private int appendCharsToPathBuffer(char[] chars, int start) { + int idx = start; + for (char ch : chars) { + writePathBufferChar(idx++, ch); + } + return idx; + } + + private int appendCharsToPathBuffer(WCharPointer chars, int start) { + int idx = start; + int sourceIdx = 0; + char ch; + while ((ch = charAt(chars, sourceIdx++)) != 0) { + writePathBufferChar(idx++, ch); + } + return idx; + } + + @Override + protected int appendPathSeparatorToPathBuffer(int idx) { + int result = idx; + if (idx == 0 || needsFileSeparator(idx)) { + writePathBufferChar(result, FILE_SEPARATOR); + result++; + } + return result; + } + + private boolean needsFileSeparator(int idx) { + return idx == 0 || !isFileSeparator(charAt(pathBuffer, idx - 1)); + } + + private static boolean needsFileSeparator(char[] path) { + return path.length == 0 || !isFileSeparator(path[path.length - 1]); + } + + private static boolean isFileSeparator(char ch) { + return ch == FILE_SEPARATOR || ch == ALT_FILE_SEPARATOR; + } + + @Override + protected void writePathBufferChar(int index, int ch) { + ((Pointer) pathBuffer).writeChar(index * Character.BYTES, (char) ch); + } + + private void writePathBufferNull(int index) { + writePathBufferChar(index, (char) 0); + } + + private static char charAt(WCharPointer pointer, int index) { + return ((Pointer) pointer).readChar(index * Character.BYTES); + } + + private static int stringLength(WCharPointer pointer) { + return (int) WindowsLibC.wcslen(pointer).rawValue(); + } + + @Override + protected Word copyChunkFilename(Word filename, int filenameLength) { + WCharPointer fn = (WCharPointer) ((Pointer) filename); + WCharPointer copy = NullableNativeMemory.malloc(Word.unsigned(filenameLength + 1).multiply(Character.BYTES), NmtCategory.JFR); + if (copy.isNull()) { + return Word.nullPointer(); + } + Pointer copyPtr = (Pointer) copy; + for (int i = 0; i < filenameLength; i++) { + copyPtr.writeChar(i * Character.BYTES, charAt(fn, i)); + } + copyPtr.writeChar(filenameLength * Character.BYTES, (char) 0); + return (Word) (Pointer) copy; + } + + @Override + protected int filenameCharAt(Word filename, int index) { + return charAt((WCharPointer) ((Pointer) filename), index); + } + + @Override + protected void freeChunkFilename(Word filename) { + NullableNativeMemory.free(filename); + } + + private void closeFindHandle() { + if (isValidHandle(repositoryFindHandle)) { + FileAPI.FindClose(repositoryFindHandle); + repositoryFindHandle = Word.nullPointer(); + } + } + + @Override + protected void closeRepository() { + closeFindHandle(); + if (isValidHandle(repositoryDirectoryGuardHandle)) { + WinBase.CloseHandle(repositoryDirectoryGuardHandle); + repositoryDirectoryGuardHandle = Word.nullPointer(); + } + } + + private static boolean isValidHandle(HANDLE handle) { + return handle.isNonNull() && !handle.equal(INVALID_HANDLE_VALUE()); + } + + @Override + protected void freePathBufferIfInitialized() { + if (pathBufferInitialized) { + NativeMemory.free(pathBuffer); + pathBuffer = Word.nullPointer(); + pathBufferInitialized = false; + } + } +} + +@AutomaticallyRegisteredFeature +@SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class, other = PartiallyLayerAware.class) +class WindowsJfrEmergencyDumpFeature implements InternalFeature { + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return VMInspectionOptions.hasJfrSupport() && Platform.includedIn(Platform.WINDOWS.class); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + ImageSingletons.add(JfrEmergencyDumpSupport.class, new WindowsJfrEmergencyDumpSupport()); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/AbstractJfrEmergencyDumpSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/AbstractJfrEmergencyDumpSupport.java new file mode 100644 index 000000000000..46bb1cbc24b2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/AbstractJfrEmergencyDumpSupport.java @@ -0,0 +1,642 @@ +/* + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2025, Red Hat Inc. All rights reserved. + * Copyright (c) 2026, 2026, IBM 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 org.graalvm.nativeimage.StackValue; +import org.graalvm.word.Pointer; +import org.graalvm.word.impl.Word; + +import com.oracle.svm.core.collections.GrowableWordArray; +import com.oracle.svm.core.collections.GrowableWordArrayAccess; +import com.oracle.svm.core.memory.NullableNativeMemory; +import com.oracle.svm.core.nmt.NmtCategory; +import com.oracle.svm.core.os.RawFileOperationSupport; +import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; +import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; +import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; +import com.oracle.svm.core.os.RawFileOperationSupport.RawFilePath; + +public abstract class AbstractJfrEmergencyDumpSupport implements JfrEmergencyDumpSupport { + protected static final int CHUNK_FILE_HEADER_SIZE = 68; + protected static final int JVM_MAXPATHLEN = 4096; + protected static final int ISO_8601_LEN = 19; + protected static final int DOT = '.'; + protected static final int CHUNKFILE_EXTENSION_LEN = 4; + protected static final int DUMP_FILE_PREFIX_LEN = 12; + protected static final int EMERGENCY_CHUNK_CREATE_ATTEMPTS = 100; + protected static final String DUMP_FILE_PREFIX = "svm_oom_pid_"; + protected static final String CHUNKFILE_EXTENSION = ".jfr"; + + private final GrowableWordArrayAccess.Comparator chunkFilenameComparator = new ChunkFilenameComparator(); + + private String pidText; + private String cwdText; + private String dumpPathText; + private boolean repositoryLocationSet; + private RawFileDescriptor emergencyFd; + private int emergencyChunkPathCallCount; + private String openFileWarning; + private String openDirectoryWarning; + + @Override + public final void initialize() { + savePid(); + allocatePathBufferIfNeeded(); + initializeRepositoryState(); + saveCwd(); + } + + protected final void savePid() { + if (pidText == null) { + pidText = Long.toString(ProcessHandle.current().pid()); + setPid(pidText); + } + } + + protected final void saveCwd() { + if (cwdText == null) { + String cwd = System.getProperty("user.dir"); + if (cwd != null) { + cwdText = cwd; + setSavedCwdText(cwdText); + } + } + } + + @Override + public final void setRepositoryLocation(String dirText) { + repositoryLocationSet = true; + setRepositoryLocationText(dirText); + if (isRepositoryLocationTooLong()) { + openDirectoryWarning = "Unable to open repository " + dirText + ". Repository path is too long."; + } else { + openDirectoryWarning = "Unable to open repository " + dirText; + } + } + + @Override + public final void setDumpPath(String dumpPath) { + if (dumpPath == null || dumpPath.isEmpty()) { + saveCwd(); + dumpPathText = cwdText; + setDumpPathToSavedCwd(); + } else { + dumpPathText = dumpPath; + setDumpPathText(dumpPathText); + } + + if (dumpPathText != null) { + savePid(); + if (isDumpPathTooLong()) { + openFileWarning = "Unable to create an emergency dump file at the location set by dumppath=" + dumpPathText + ". Dump path is too long."; + } else { + openFileWarning = "Unable to create an emergency dump file at the location set by dumppath=" + dumpPathText; + } + } else { + openFileWarning = "Unable to create an emergency dump file. Dump path could not be set."; + } + } + + @Override + public final String getDumpPath() { + if (dumpPathText != null) { + return dumpPathText; + } + return ""; + } + + @Override + public final RawFileDescriptor chunkPath() { + if (!repositoryLocationSet) { + if (!openEmergencyDumpFile()) { + return Word.nullPointer(); + } + RawFileDescriptor fd = emergencyFd; + emergencyFd = Word.nullPointer(); + return fd; + } + return createEmergencyChunkPath(); + } + + protected final RawFileDescriptor createEmergencyChunkPath() { + if (isRepositoryLocationTooLong()) { + return Word.nullPointer(); + } + clearPathBuffer(); + int idx = 0; + idx = appendRepositoryLocationToPathBuffer(idx); + idx = appendPathSeparatorToPathBuffer(idx); + idx = appendCurrentDateTimeToPathBuffer(idx); + if (idx < 0) { + return Word.nullPointer(); + } + return createEmergencyChunkPath(pathBuffer(), idx); + } + + protected final RawFileDescriptor createEmergencyChunkPath(RawFilePath path, int baseNameEndIndex) { + emergencyChunkPathCallCount++; + if (path.isNull()) { + return Word.nullPointer(); + } + for (int attempt = 0; attempt < EMERGENCY_CHUNK_CREATE_ATTEMPTS; attempt++) { + int idx = appendEmergencyChunkSuffix(baseNameEndIndex, attempt); + if (idx < 0) { + return Word.nullPointer(); + } + idx = writeChunkFileExtension(idx); + writePathBufferChar(idx, 0); + + RawFileDescriptor fd = getFileSupport().create(path, FileCreationMode.CREATE, FileAccessMode.READ_WRITE); + if (getFileSupport().isValid(fd)) { + return fd; + } + } + return Word.nullPointer(); + } + + protected final boolean isDumpPathTooLong() { + int dumpPathLength = dumpPathLength(); + return dumpPathLength >= 0 && dumpPathLength + 1L + DUMP_FILE_PREFIX_LEN + pidLength() + CHUNKFILE_EXTENSION_LEN >= JVM_MAXPATHLEN; + } + + protected final boolean isRepositoryLocationTooLong() { + int repositoryLocationLength = repositoryLocationLength(); + return repositoryLocationLength >= 0 && repositoryLocationLength + 1L + ISO_8601_LEN + CHUNKFILE_EXTENSION_LEN >= JVM_MAXPATHLEN; + } + + protected final boolean isValidChunkFilename(Word filename, int filenameLength) { + if (filenameLength <= CHUNKFILE_EXTENSION_LEN) { + return false; + } + if (!hasChunkFileExtension(filename, filenameLength)) { + return false; + } + return hasChunkFilenameFormat(filename, filenameLength - CHUNKFILE_EXTENSION_LEN); + } + + private boolean hasChunkFileExtension(Word filename, int filenameLength) { + for (int i = 0; i < CHUNKFILE_EXTENSION_LEN; i++) { + int idx1 = CHUNKFILE_EXTENSION_LEN - i - 1; + int idx2 = filenameLength - i - 1; + if (chunkFileExtensionCharAt(idx1) != filenameCharAt(filename, idx2)) { + return false; + } + } + return true; + } + + private static int chunkFileExtensionCharAt(int index) { + return switch (index) { + case 0 -> '.'; + case 1 -> 'j'; + case 2 -> 'f'; + case 3 -> 'r'; + default -> 0; + }; + } + + private boolean hasChunkFilenameFormat(Word filename, int baseNameLength) { + if (baseNameLength < ISO_8601_LEN) { + return false; + } + // Repository chunks are timestamped as yyyy_MM_dd_HH_mm_ss[_NN].jfr. + for (int i = 0; i < ISO_8601_LEN; i++) { + int ch = filenameCharAt(filename, i); + if (i == 4 || i == 7 || i == 10 || i == 13 || i == 16) { + if (ch != '_') { + return false; + } + } else if (ch < '0' || ch > '9') { + return false; + } + } + if (baseNameLength == ISO_8601_LEN) { + return true; + } + if (filenameCharAt(filename, ISO_8601_LEN) != '_' || baseNameLength == ISO_8601_LEN + 1) { + return false; + } + for (int i = ISO_8601_LEN + 1; i < baseNameLength; i++) { + int ch = filenameCharAt(filename, i); + if (ch < '0' || ch > '9') { + return false; + } + } + return true; + } + + protected final RawFilePath createEmergencyDumpPath() { + int idx = 0; + clearPathBuffer(); + + if (isDumpPathTooLong()) { + return Word.nullPointer(); + } + if (dumpPathLength() >= 0) { + idx = appendDumpPathToPathBuffer(idx); + idx = appendPathSeparatorToPathBuffer(idx); + } + + idx = writeDumpFilePrefix(idx); + idx = appendPidToPathBuffer(idx); + idx = writeChunkFileExtension(idx); + writePathBufferChar(idx, 0); + return pathBuffer(); + } + + protected final int compareChunkFilenames(Word a, Word b) { + int cmp = compareFilenameCharacters(a, b, ISO_8601_LEN); + if (cmp == 0) { + int aLen = filenameIndexOf(a, DOT); + int bLen = filenameIndexOf(b, DOT); + if (aLen < bLen) { + return -1; + } + if (aLen > bLen) { + return 1; + } + cmp = compareFilenameCharacters(a, b, aLen); + } + return cmp; + } + + private int filenameIndexOf(Word filename, int needle) { + int idx = 0; + int ch; + while ((ch = filenameCharAt(filename, idx)) != 0) { + if (ch == needle) { + return idx; + } + idx++; + } + return idx; + } + + protected final GrowableWordArrayAccess.Comparator chunkFilenameComparator() { + return chunkFilenameComparator; + } + + private int compareFilenameCharacters(Word a, Word b, int length) { + for (int i = 0; i < length; i++) { + int cmp = Integer.compare(filenameCharAt(a, i), filenameCharAt(b, i)); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + protected final int writeDateTimeToPathBuffer(int idx, int year, int month, int day, int hour, int minute, int second) { + int pos = idx; + pos = writeFourDigits(pos, year); + writePathBufferChar(pos++, '_'); + pos = writeTwoDigits(pos, month); + writePathBufferChar(pos++, '_'); + pos = writeTwoDigits(pos, day); + writePathBufferChar(pos++, '_'); + pos = writeTwoDigits(pos, hour); + writePathBufferChar(pos++, '_'); + pos = writeTwoDigits(pos, minute); + writePathBufferChar(pos++, '_'); + return writeTwoDigits(pos, second); + } + + private int appendEmergencyChunkSuffix(int idx, int attempt) { + int pos = idx; + if (attempt != 0) { + writePathBufferChar(pos++, '_'); + pos = writeDecimal(pos, attempt); + } + return pos + CHUNKFILE_EXTENSION_LEN >= JVM_MAXPATHLEN ? -1 : pos; + } + + private int writeChunkFileExtension(int idx) { + int pos = idx; + for (int i = 0; i < CHUNKFILE_EXTENSION_LEN; i++) { + writePathBufferChar(pos++, chunkFileExtensionCharAt(i)); + } + return pos; + } + + private static int dumpFilePrefixCharAt(int index) { + return switch (index) { + case 0 -> 's'; + case 1 -> 'v'; + case 2 -> 'm'; + case 3 -> '_'; + case 4 -> 'o'; + case 5 -> 'o'; + case 6 -> 'm'; + case 7 -> '_'; + case 8 -> 'p'; + case 9 -> 'i'; + case 10 -> 'd'; + case 11 -> '_'; + default -> 0; + }; + } + + private int writeDumpFilePrefix(int idx) { + int pos = idx; + for (int i = 0; i < DUMP_FILE_PREFIX_LEN; i++) { + writePathBufferChar(pos++, dumpFilePrefixCharAt(i)); + } + return pos; + } + + protected final boolean addUsableChunkFilename(GrowableWordArray chunkFilenames, Word filename, int filenameLength) { + if (!isUsableChunkFile(filename, filenameLength)) { + return false; + } + return addChunkFilename(chunkFilenames, copyChunkFilename(filename, filenameLength)); + } + + protected final boolean addChunkFilename(GrowableWordArray chunkFilenames, Word filenameCopy) { + if (filenameCopy.rawValue() == 0) { + SubstrateJVM.getLogging().logJfrSystemError("Unable to copy chunk filename during jfr emergency dump"); + return false; + } + if (!GrowableWordArrayAccess.add(chunkFilenames, filenameCopy, NmtCategory.JFR)) { + freeChunkFilename(filenameCopy); + SubstrateJVM.getLogging().logJfrSystemError("Unable to add chunk filename to list during jfr emergency dump"); + return false; + } + return true; + } + + protected final boolean isUsableChunkFile(Word filename, int filenameLength) { + if (!isValidChunkFilename(filename, filenameLength)) { + return false; + } + + RawFileDescriptor chunkFd = openRepositoryFile(filename); + if (!getFileSupport().isValid(chunkFd)) { + return false; + } + + long chunkFileSize = getFileSupport().size(chunkFd); + getFileSupport().close(chunkFd); + return chunkFileSize >= CHUNK_FILE_HEADER_SIZE; + } + + protected final void sortChunkFilenames(GrowableWordArray chunkFilenames, int count) { + if (count > 0) { + GrowableWordArrayAccess.qsort(chunkFilenames, 0, count - 1, chunkFilenameComparator); + } + } + + private int writeFourDigits(int idx, int value) { + int pos = writeTwoDigits(idx, value / 100); + return writeTwoDigits(pos, value % 100); + } + + private int writeTwoDigits(int idx, int value) { + int pos = idx; + writePathBufferChar(pos++, '0' + ((value / 10) % 10)); + writePathBufferChar(pos++, '0' + (value % 10)); + return pos; + } + + private int writeDecimal(int idx, int value) { + int pos = idx; + int divisor = 1; + while (value / divisor >= 10) { + divisor *= 10; + } + while (divisor > 0) { + writePathBufferChar(pos++, '0' + ((value / divisor) % 10)); + divisor /= 10; + } + return pos; + } + + @Override + public final void onVmError() { + if (!repositoryLocationSet) { + return; + } + if (openEmergencyDumpFile()) { + GrowableWordArray sortedChunkFilenames = StackValue.get(GrowableWordArray.class); + GrowableWordArrayAccess.initialize(sortedChunkFilenames); + try { + if (openRepository()) { + try { + iterateRepository(sortedChunkFilenames); + writeEmergencyDumpFile(sortedChunkFilenames); + } finally { + closeRepository(); + } + } + } finally { + freeChunkFilenames(sortedChunkFilenames); + GrowableWordArrayAccess.freeData(sortedChunkFilenames); + closeEmergencyDumpFile(); + sortedChunkFilenames = Word.nullPointer(); + } + } + } + + private boolean openEmergencyDumpFile() { + if (getFileSupport().isValid(emergencyFd)) { + return true; + } + emergencyFd = createEmergencyDumpFile(); + if (!getFileSupport().isValid(emergencyFd)) { + SubstrateJVM.getLogging().logJfrWarning(openFileWarning); + useCurrentDirectoryDumpPath(); + emergencyFd = createEmergencyDumpFile(); + } + return getFileSupport().isValid(emergencyFd); + } + + private RawFileDescriptor createEmergencyDumpFile() { + RawFilePath path = createEmergencyDumpPath(); + if (path.isNull()) { + return Word.nullPointer(); + } + return getFileSupport().create(path, FileCreationMode.CREATE, FileAccessMode.READ_WRITE); + } + + private void useCurrentDirectoryDumpPath() { + dumpPathText = cwdText; + setDumpPathToSavedCwd(); + } + + protected final String getOpenDirectoryWarning() { + return openDirectoryWarning; + } + + protected final 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++) { + RawFileDescriptor chunkFd = openRepositoryFile(GrowableWordArrayAccess.get(sortedChunkFilenames, i)); + if (getFileSupport().isValid(chunkFd)) { + long chunkFileSize = getFileSupport().size(chunkFd); + long bytesRead = 0; + if (!getFileSupport().seek(chunkFd, 0)) { + SubstrateJVM.getLogging().logJfrInfo("Unable to recover JFR data, seek failed."); + getFileSupport().close(chunkFd); + continue; + } + while (bytesRead < chunkFileSize) { + long readResult = getFileSupport().read(chunkFd, copyBlock, Word.unsigned(blockSize)); + if (readResult <= 0) { + if (readResult < 0) { + SubstrateJVM.getLogging().logJfrInfo("Unable to recover JFR data, read failed."); + } + break; + } + bytesRead += readResult; + if (!getFileSupport().write(emergencyFd, copyBlock, Word.unsigned(readResult))) { + SubstrateJVM.getLogging().logJfrInfo("Unable to recover JFR data, write failed."); + break; + } + } + getFileSupport().close(chunkFd); + } + } + NullableNativeMemory.free(copyBlock); + } + + protected final void freeChunkFilenames(GrowableWordArray chunkFilenames) { + for (int i = 0; i < chunkFilenames.getSize(); i++) { + Word filename = GrowableWordArrayAccess.get(chunkFilenames, i); + if (filename.rawValue() != 0) { + freeChunkFilename(filename); + } + } + } + + protected final void closeEmergencyDumpFile() { + if (getFileSupport().isValid(emergencyFd)) { + getFileSupport().close(emergencyFd); + emergencyFd = Word.nullPointer(); + } + } + + protected final int getEmergencyChunkPathCallCount() { + return emergencyChunkPathCallCount; + } + + protected final void resetEmergencyChunkPathCallCount() { + emergencyChunkPathCallCount = 0; + } + + @Override + public final void teardown() { + closeEmergencyDumpFile(); + closeRepository(); + freePathBufferIfInitialized(); + } + + public static class TestingBackdoor { + public static long getPathBufferAddress(AbstractJfrEmergencyDumpSupport support) { + return support.getPathBufferAddress(); + } + + public static int getEmergencyChunkPathCallCount(AbstractJfrEmergencyDumpSupport support) { + return support.getEmergencyChunkPathCallCount(); + } + + public static void resetEmergencyChunkPathCallCount(AbstractJfrEmergencyDumpSupport support) { + support.resetEmergencyChunkPathCallCount(); + } + } + + protected static RawFileOperationSupport getFileSupport() { + return RawFileOperationSupport.bigEndian(); + } + + private final class ChunkFilenameComparator implements GrowableWordArrayAccess.Comparator { + @Override + public int compare(Word a, Word b) { + return compareChunkFilenames(a, b); + } + } + + protected abstract void setPid(String pid); + + protected abstract void setSavedCwdText(String cwd); + + protected abstract void setDumpPathText(String dumpPath); + + protected abstract void setDumpPathToSavedCwd(); + + protected abstract void setRepositoryLocationText(String repositoryLocation); + + protected abstract void allocatePathBufferIfNeeded(); + + protected abstract void initializeRepositoryState(); + + protected abstract void freePathBufferIfInitialized(); + + protected abstract long getPathBufferAddress(); + + protected abstract int pidLength(); + + protected abstract int dumpPathLength(); + + protected abstract int repositoryLocationLength(); + + protected abstract RawFilePath pathBuffer(); + + protected abstract void clearPathBuffer(); + + protected abstract int appendDumpPathToPathBuffer(int idx); + + protected abstract int appendRepositoryLocationToPathBuffer(int idx); + + protected abstract int appendPidToPathBuffer(int idx); + + protected abstract int appendPathSeparatorToPathBuffer(int idx); + + protected abstract int appendCurrentDateTimeToPathBuffer(int idx); + + protected abstract boolean openRepository(); + + protected abstract void iterateRepository(GrowableWordArray sortedChunkFilenames); + + protected abstract RawFileDescriptor openRepositoryFile(Word filename); + + protected abstract int filenameCharAt(Word filename, int index); + + protected abstract void writePathBufferChar(int index, int ch); + + protected abstract Word copyChunkFilename(Word filename, int filenameLength); + + protected abstract void freeChunkFilename(Word filename); + + protected abstract void closeRepository(); +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/JfrEmergencyDumpTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/JfrEmergencyDumpTest.java new file mode 100644 index 000000000000..84d517cf6d3a --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/JfrEmergencyDumpTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 2026, IBM 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.core.jfr.AbstractJfrEmergencyDumpSupport; +import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; + +public abstract class JfrEmergencyDumpTest extends JfrRecordingTest { + protected static JfrEmergencyDumpSupport getEmergencyDumpSupport() { + if (!JfrEmergencyDumpSupport.isPresent()) { + return null; + } + return JfrEmergencyDumpSupport.singleton(); + } + + protected static long getPathBufferAddress(JfrEmergencyDumpSupport support) { + if (support instanceof AbstractJfrEmergencyDumpSupport emergencyDumpSupport) { + return AbstractJfrEmergencyDumpSupport.TestingBackdoor.getPathBufferAddress(emergencyDumpSupport); + } + return 0L; + } + + protected static int getEmergencyChunkPathCallCount(JfrEmergencyDumpSupport support) { + if (support instanceof AbstractJfrEmergencyDumpSupport emergencyDumpSupport) { + return AbstractJfrEmergencyDumpSupport.TestingBackdoor.getEmergencyChunkPathCallCount(emergencyDumpSupport); + } + return 0; + } + + protected static void resetEmergencyChunkPathCallCount(JfrEmergencyDumpSupport support) { + if (support instanceof AbstractJfrEmergencyDumpSupport emergencyDumpSupport) { + AbstractJfrEmergencyDumpSupport.TestingBackdoor.resetEmergencyChunkPathCallCount(emergencyDumpSupport); + } + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java index 8d46258297ba..d06f0273d141 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDump.java @@ -26,8 +26,12 @@ package com.oracle.svm.test.jfr; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import com.oracle.svm.core.jfr.HasJfrSupport; +import com.oracle.svm.core.jfr.JfrEvent; +import com.oracle.svm.test.jfr.events.StringEvent; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import org.junit.Test; import java.io.IOException; import java.nio.file.Files; @@ -35,39 +39,36 @@ import java.util.ArrayList; import java.util.List; -import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; -import com.oracle.svm.core.jfr.HasJfrSupport; -import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; -import com.oracle.svm.core.jfr.JfrEvent; import com.oracle.svm.core.jfr.SubstrateJVM; -import com.oracle.svm.test.jfr.events.StringEvent; - -import jdk.jfr.Recording; -import jdk.jfr.consumer.RecordedEvent; /** * 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 { +public class TestEmergencyDump extends JfrEmergencyDumpTest { private static final String STRING_EVENT_NAME = "com.jfr.String"; private static final String OUT_OF_MEMORY_REASON = "Out of Memory"; @Test public void test() throws Throwable { - if (!HasJfrSupport.get() || !JfrEmergencyDumpSupport.isPresent()) { + if (!HasJfrSupport.get()) { /* Prevent that the code below is reachable on platforms that don't support JFR. */ return; } String[] testedEvents = new String[]{STRING_EVENT_NAME, JfrEvent.DumpReason.getName()}; Path dumpFile = Path.of("svm_oom_pid_" + ProcessHandle.current().pid() + ".jfr"); - runEmergencyDumpScenario(testedEvents); - assertEmergencyDump(dumpFile, testedEvents, createExpectedStrings()); - Files.deleteIfExists(dumpFile); - assertNoResidualTestedEvents(testedEvents); + try { + runEmergencyDumpScenario(testedEvents); + assertEmergencyDump(dumpFile, testedEvents, createExpectedStrings()); + } finally { + Files.deleteIfExists(dumpFile); + assertNoResidualTestedEvents(testedEvents); + } } private void runEmergencyDumpScenario(String[] testedEvents) throws Throwable { diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java index 2dd446d6b025..f25442c46fbc 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpConstantPool.java @@ -38,7 +38,6 @@ import org.junit.Test; import com.oracle.svm.core.jfr.HasJfrSupport; -import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; import com.oracle.svm.core.jfr.SubstrateJVM; import com.oracle.svm.test.jfr.events.ClassEvent; @@ -50,12 +49,12 @@ * Verifies that the previous-epoch type and symbol constant pools required by in-flight class * events are serialized correctly during an emergency dump. */ -public class TestEmergencyDumpConstantPool extends JfrRecordingTest { +public class TestEmergencyDumpConstantPool extends JfrEmergencyDumpTest { private static final String CLASS_EVENT_NAME = "com.jfr.Class"; @Test public void test() throws Throwable { - if (!HasJfrSupport.get() || !JfrEmergencyDumpSupport.isPresent()) { + if (!HasJfrSupport.get()) { /* Prevent that the code below is reachable on platforms that don't support JFR. */ return; } @@ -67,16 +66,16 @@ public void test() throws Throwable { Class utf8NamedClass = Utf8Cläss漢Test.class; // Checkstyle: resume - Recording recording = startRecording(events); - emitClassEvent(String.class); - emitClassEvent(EmergencyDumpHelper.class); - emitClassEvent(utf8NamedClass); + Recording recording = null; + try { + recording = startRecording(events); + emitClassEvent(String.class); + emitClassEvent(EmergencyDumpHelper.class); + emitClassEvent(utf8NamedClass); - SubstrateJVM.get().dumpOnOutOfMemoryError(); - recording.stop(); - recording.close(); + SubstrateJVM.get().dumpOnOutOfMemoryError(); + recording.stop(); - try { assertTrue("emergency dump file does not exist.", Files.exists(dumpFile)); List dumpedEvents = getEvents(dumpFile, events, true); @@ -94,8 +93,14 @@ public void test() throws Throwable { } assertEquals(0, expectedClasses.size()); } finally { - Files.deleteIfExists(dumpFile); - assertNoResidualTestedEvents(events); + try { + if (recording != null) { + recording.close(); + } + } finally { + Files.deleteIfExists(dumpFile); + assertNoResidualTestedEvents(events); + } } } diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java index 3d7488e70ec0..fa32d120a2b2 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpMetadataOnly.java @@ -35,20 +35,19 @@ import org.junit.Test; -import com.oracle.svm.core.jfr.HasJfrSupport; -import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; import com.oracle.svm.core.jfr.JfrEvent; +import com.oracle.svm.core.jfr.HasJfrSupport; import com.oracle.svm.core.jfr.SubstrateJVM; import jdk.jfr.Recording; import jdk.jfr.consumer.RecordedEvent; -public class TestEmergencyDumpMetadataOnly extends JfrRecordingTest { +public class TestEmergencyDumpMetadataOnly extends JfrEmergencyDumpTest { private static final String OUT_OF_MEMORY_REASON = "Out of Memory"; @Test public void test() throws Throwable { - if (!HasJfrSupport.get() || !JfrEmergencyDumpSupport.isPresent()) { + if (!HasJfrSupport.get()) { /* Prevent that the code below is reachable on platforms that don't support JFR. */ return; } @@ -57,12 +56,12 @@ public void test() throws Throwable { Path dumpFile = getEmergencyDumpFile(); Files.deleteIfExists(dumpFile); - Recording recording = startRecording(events); - SubstrateJVM.get().dumpOnOutOfMemoryError(); - recording.stop(); - recording.close(); - + Recording recording = null; try { + recording = startRecording(events); + SubstrateJVM.get().dumpOnOutOfMemoryError(); + recording.stop(); + assertTrue("emergency dump file does not exist.", Files.exists(dumpFile)); List dumpedEvents = getEvents(dumpFile, events, true); @@ -73,8 +72,14 @@ public void test() throws Throwable { assertEquals(OUT_OF_MEMORY_REASON, dumpReason.getString("reason")); assertEquals(-1, dumpReason.getInt("recordingId")); } finally { - Files.deleteIfExists(dumpFile); - assertNoResidualTestedEvents(events); + try { + if (recording != null) { + recording.close(); + } + } finally { + Files.deleteIfExists(dumpFile); + assertNoResidualTestedEvents(events); + } } } diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java index 86778aba6d21..655e7e6ff634 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpRepositoryFallback.java @@ -38,10 +38,9 @@ import org.junit.Test; import com.oracle.svm.core.jfr.HasJfrSupport; -import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; import com.oracle.svm.core.jfr.JfrEvent; +import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; import com.oracle.svm.core.jfr.SubstrateJVM; -import com.oracle.svm.core.posix.jfr.PosixJfrEmergencyDumpSupport; import com.oracle.svm.shared.util.ClassUtil; import com.oracle.svm.test.jfr.events.StringEvent; @@ -49,40 +48,49 @@ import jdk.jfr.Recording; import jdk.jfr.consumer.RecordedEvent; -public class TestEmergencyDumpRepositoryFallback extends AbstractJfrTest { +public class TestEmergencyDumpRepositoryFallback extends JfrEmergencyDumpTest { private static final String STRING_EVENT_NAME = "com.jfr.String"; private static final String OUT_OF_MEMORY_REASON = "Out of Memory"; + private static final String NON_ASCII_PATH_PART = "Gr\u00fc\u00dfe_\u4f60\u597d"; @Test public void testRepositoryEmergencyChunkIsMergedIntoEmergencyDump() throws Throwable { - if (!HasJfrSupport.get() || !JfrEmergencyDumpSupport.isPresent()) { + if (!HasJfrSupport.get()) { return; } - if (!(JfrEmergencyDumpSupport.singleton() instanceof PosixJfrEmergencyDumpSupport support)) { + JfrEmergencyDumpSupport support = getEmergencyDumpSupport(); + if (support == null) { return; } - Path repositoryDir = Files.createTempDirectory(ClassUtil.getUnqualifiedName(getClass()) + "-repository-"); - Path dumpDir = Files.createTempDirectory(ClassUtil.getUnqualifiedName(getClass()) + "-dump-"); + String tempDirectoryPrefix = ClassUtil.getUnqualifiedName(getClass()) + "-" + NON_ASCII_PATH_PART; + Path repositoryDir = Files.createTempDirectory(tempDirectoryPrefix + "-repository-"); + Path dumpDir = Files.createTempDirectory(tempDirectoryPrefix + "-dump-"); String[] events = new String[]{STRING_EVENT_NAME, JfrEvent.DumpReason.getName()}; try { - PosixJfrEmergencyDumpSupport.TestingBackdoor.resetEmergencyChunkPathCallCount(support); + resetEmergencyChunkPathCallCount(support); SubstrateJVM.get().setRepositoryLocation(repositoryDir.toString()); SubstrateJVM.get().setDumpPath(dumpDir.toString()); - Recording recording = createInMemoryRecording(events); - /* - * JFR may already have an active repository chunk open for the recording. Close it so - * dumpOnOutOfMemoryError() has to create the emergency repository chunk itself. - */ - SubstrateJVM.get().setOutput(null); - emitStringEvent("repository-fallback"); - SubstrateJVM.get().dumpOnOutOfMemoryError(); - recording.stop(); - recording.close(); + Recording recording = null; + try { + recording = createInMemoryRecording(events); + /* + * JFR may already have an active repository chunk open for the recording. Close it so + * dumpOnOutOfMemoryError() has to create the emergency repository chunk itself. + */ + SubstrateJVM.get().setOutput(null); + emitStringEvent("repository-fallback"); + SubstrateJVM.get().dumpOnOutOfMemoryError(); + recording.stop(); + } finally { + if (recording != null) { + recording.close(); + } + } assertTrue("expected repository fallback emergency chunk path to be used", - PosixJfrEmergencyDumpSupport.TestingBackdoor.getEmergencyChunkPathCallCount(support) > 0); + getEmergencyChunkPathCallCount(support) > 0); Path dumpFile = dumpDir.resolve("svm_oom_pid_" + ProcessHandle.current().pid() + ".jfr"); assertTrue("emergency dump file does not exist.", Files.exists(dumpFile)); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpSupportLifecycle.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpSupportLifecycle.java index 7a6e7abda574..85b0b26d5d38 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpSupportLifecycle.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEmergencyDumpSupportLifecycle.java @@ -32,27 +32,33 @@ import com.oracle.svm.core.jfr.HasJfrSupport; import com.oracle.svm.core.jfr.JfrEmergencyDumpSupport; -import com.oracle.svm.core.posix.jfr.PosixJfrEmergencyDumpSupport; -public class TestEmergencyDumpSupportLifecycle extends AbstractJfrTest { +public class TestEmergencyDumpSupportLifecycle extends JfrEmergencyDumpTest { @Test public void testRepeatedInitializeReusesPathBuffer() { - if (!HasJfrSupport.get() || !JfrEmergencyDumpSupport.isPresent()) { + if (!HasJfrSupport.get()) { return; } - if (!(JfrEmergencyDumpSupport.singleton() instanceof PosixJfrEmergencyDumpSupport support)) { + JfrEmergencyDumpSupport support = getEmergencyDumpSupport(); + if (support == null) { return; } + boolean wasInitialized = getPathBufferAddress(support) != 0L; support.teardown(); - support.initialize(); - long firstAddress = PosixJfrEmergencyDumpSupport.TestingBackdoor.getPathBufferAddress(support); - assertTrue(firstAddress != 0L); + try { + support.initialize(); + long firstAddress = getPathBufferAddress(support); + assertTrue(firstAddress != 0L); - support.initialize(); - long secondAddress = PosixJfrEmergencyDumpSupport.TestingBackdoor.getPathBufferAddress(support); - assertEquals(firstAddress, secondAddress); - - support.teardown(); + support.initialize(); + long secondAddress = getPathBufferAddress(support); + assertEquals(firstAddress, secondAddress); + } finally { + support.teardown(); + if (wasInitialized) { + support.initialize(); + } + } } } diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestRecordingDestinationPath.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestRecordingDestinationPath.java index 9294bb321da2..931d20f7302c 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestRecordingDestinationPath.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestRecordingDestinationPath.java @@ -27,28 +27,31 @@ import static org.junit.Assert.assertEquals; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.List; import org.junit.Test; import com.oracle.svm.test.jfr.events.ClassEvent; +import jdk.jfr.Configuration; import jdk.jfr.Recording; import jdk.jfr.consumer.RecordedEvent; public class TestRecordingDestinationPath extends JfrRecordingTest { + private static final String CLASS_EVENT_NAME = "com.jfr.Class"; + private static final String NON_ASCII_PATH_PART = "Gr\u00fc\u00dfe_\u4f60\u597d"; + @Test public void testNonAsciiDestinationPath() throws Throwable { - String[] events = new String[]{"com.jfr.Class"}; - Path path = Files.createTempFile("TestRecordingDestinationPath_Gr\u00fc\u00dfe_\u4f60\u597d_", ".jfr"); + String[] events = new String[]{CLASS_EVENT_NAME}; + Path path = createNonAsciiJfrPath("destination"); try { Recording recording = startRecording(events, getDefaultConfiguration(), null, path); - - ClassEvent event = new ClassEvent(); - event.clazz = TestRecordingDestinationPath.class; - event.commit(); + emitClassEvent(); stopRecording(recording, TestRecordingDestinationPath::validateEvents); } finally { @@ -56,6 +59,45 @@ public void testNonAsciiDestinationPath() throws Throwable { } } + @Test + public void testNonAsciiDumpPath() throws Throwable { + String[] events = new String[]{CLASS_EVENT_NAME}; + Path path = createNonAsciiJfrPath("dump"); + Recording recording = null; + try { + recording = createInMemoryRecording(events); + emitClassEvent(); + + recording.dump(path); + checkRecording(TestRecordingDestinationPath::validateEvents, path, new JfrRecordingState(events), true); + } finally { + if (recording != null) { + recording.close(); + } + Files.deleteIfExists(path); + } + } + + private static Path createNonAsciiJfrPath(String prefix) throws IOException { + return Files.createTempFile("TestRecordingDestinationPath_" + prefix + "_" + NON_ASCII_PATH_PART + "_", ".jfr"); + } + + private static Recording createInMemoryRecording(String[] events) throws Throwable { + Configuration config = getDefaultConfiguration(); + Recording recording = config == null ? new Recording() : new Recording(config); + for (String event : events) { + recording.enable(event).withThreshold(Duration.ZERO); + } + recording.start(); + return recording; + } + + private static void emitClassEvent() { + ClassEvent event = new ClassEvent(); + event.clazz = TestRecordingDestinationPath.class; + event.commit(); + } + private static void validateEvents(List events) { assertEquals(1, events.size()); }