diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index bbc73d7fe7eb..5deb5c67412b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -918,6 +918,9 @@ public Boolean getValue(OptionValues values) { @Option(help = "file:doc-files/FlightRecorderLoggingHelp.txt")// public static final RuntimeOptionKey FlightRecorderLogging = new RuntimeOptionKey<>("all=warning", Immutable); + @Option(help = "file:doc-files/FlightRecorderOptionsHelp.txt")// + public static final RuntimeOptionKey FlightRecorderOptions = new RuntimeOptionKey<>("", Immutable); + public static String reportsPath() { Path reportsPath = ImageSingletons.lookup(ReportingSupport.class).reportsPath; if (reportsPath.isAbsolute()) { 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 new file mode 100644 index 000000000000..c6e3bfbee71f --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FlightRecorderOptionsHelp.txt @@ -0,0 +1,30 @@ +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: + +globalbuffercount=20 (Optional) Number of global JFR buffers. This is a legacy option. + This value cannot be changed once JFR has been initialized. + The default value is determined by the values for memorysize and globalbuffersize. + +globalbuffersize=512k (Optional) Size of each global JFR buffer. This is a legacy option. + This value cannot be changed once JFR has been initialized. + The default value is determined by the values for memorysize and globalbuffercount. + +maxchunksize=12m (Optional) Maximum size of each individual JFR data chunk. + This value cannot be changed once JFR has been initialized. + +memorysize=10m (Optional) Total size of all global JFR buffers. + This value cannot be changed once JFR has been initialized. + +repositorypath=... (Optional) Path to the location where JFR recordings are stored until they are + written to a permanent file. + The default location is the temporary directory for the operating system. + +stackdepth=64 (Optional) Stack depth for stack traces. + Setting this value greater than the default may cause a performance degradation. + This value cannot be changed once JFR has been initialized. + +thread_buffer_size=8k (Optional) Size of each thread-local JFR buffer. + This value cannot be changed once JFR has been initialized. + +preserve-repository=false (Optional) Preserve files stored in the disk repository after the process exits. 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 a8a24b6eab57..3bccc1a81d96 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 @@ -56,7 +56,9 @@ import jdk.jfr.internal.LogTag; import jdk.jfr.internal.Logger; import jdk.jfr.internal.OldObjectSample; +import jdk.jfr.internal.Options; import jdk.jfr.internal.PrivateAccess; +import jdk.jfr.internal.Repository; import jdk.jfr.internal.SecuritySupport; import jdk.jfr.internal.jfc.JFC; @@ -74,10 +76,6 @@ public JfrManager(boolean hostedEnabled) { this.hostedEnabled = hostedEnabled; } - public static boolean isJFREnabled() { - return SubstrateOptions.FlightRecorder.getValue() || !SubstrateOptions.StartFlightRecording.getValue().isEmpty(); - } - @Fold public static JfrManager get() { return ImageSingletons.lookup(JfrManager.class); @@ -87,7 +85,9 @@ public RuntimeSupport.Hook startupHook() { return isFirstIsolate -> { parseFlightRecorderLogging(SubstrateOptions.FlightRecorderLogging.getValue()); periodicEventSetup(); - if (isJFREnabled()) { + + boolean startRecording = SubstrateOptions.FlightRecorder.getValue() || !SubstrateOptions.StartFlightRecording.getValue().isEmpty(); + if (startRecording) { initRecording(); } }; @@ -116,17 +116,27 @@ private static void periodicEventSetup() throws SecurityException { } private static void initRecording() { - Map args = parseStartFlightRecording(); - String name = args.get(JfrStartArgument.Name); - String[] settings = parseSettings(args); - Long delay = parseDuration(args, JfrStartArgument.Delay); - Long duration = parseDuration(args, JfrStartArgument.Duration); - Boolean disk = parseBoolean(args, JfrStartArgument.Disk); - String path = args.get(JfrStartArgument.Filename); - Long maxAge = parseDuration(args, JfrStartArgument.MaxAge); - Long maxSize = parseMaxSize(args, JfrStartArgument.MaxSize); - Boolean dumpOnExit = parseBoolean(args, JfrStartArgument.DumpOnExit); - Boolean pathToGcRoots = parseBoolean(args, JfrStartArgument.PathToGCRoots); + Map startArgs = parseJfrOptions(SubstrateOptions.StartFlightRecording.getValue(), JfrStartArgument.values()); + Map optionsArgs = parseJfrOptions(SubstrateOptions.FlightRecorderOptions.getValue(), FlightRecorderOptionsArgument.values()); + + String name = startArgs.get(JfrStartArgument.Name); + String[] settings = parseSettings(startArgs); + Long delay = parseDuration(startArgs, JfrStartArgument.Delay); + Long duration = parseDuration(startArgs, JfrStartArgument.Duration); + Boolean disk = parseBoolean(startArgs, JfrStartArgument.Disk); + String path = startArgs.get(JfrStartArgument.Filename); + Long maxAge = parseDuration(startArgs, JfrStartArgument.MaxAge); + Long maxSize = parseMaxSize(startArgs, JfrStartArgument.MaxSize); + Boolean dumpOnExit = parseBoolean(startArgs, JfrStartArgument.DumpOnExit); + Boolean pathToGcRoots = parseBoolean(startArgs, JfrStartArgument.PathToGCRoots); + String stackDepth = optionsArgs.get(FlightRecorderOptionsArgument.StackDepth); + Long maxChunkSize = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.MaxChunkSize); + Long memorySize = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.MemorySize); + Long globalBufferSize = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.GlobalBufferSize); + Long globalBufferCount = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.GlobalBufferCount); + Long threadBufferSize = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.ThreadBufferSize); + Boolean preserveRepo = parseBoolean(optionsArgs, FlightRecorderOptionsArgument.PreserveRepository); + String repositoryPath = optionsArgs.get(FlightRecorderOptionsArgument.RepositoryPath); try { if (Logger.shouldLog(LogTag.JFR_DCMD, LogLevel.DEBUG)) { @@ -183,6 +193,47 @@ private static void initRecording() { } } + if (stackDepth != null) { + try { + Options.setStackDepth(Integer.valueOf(stackDepth)); + } catch (Throwable e) { + throw new Exception("Could not start recording, stack depth is not an integer.", e); + } + } + + if (repositoryPath != null) { + try { + SecuritySupport.SafePath repositorySafePath = new SecuritySupport.SafePath(repositoryPath); + Repository.getRepository().setBasePath(repositorySafePath); + } catch (Throwable e) { + throw new Exception("Could not start recording, repository path is invalid.", e); + } + } + + if (maxChunkSize != null) { + Options.setMaxChunkSize(maxChunkSize); + } + + if (preserveRepo != null) { + Options.setPreserveRepository(preserveRepo); + } + + if (threadBufferSize != null) { + Options.setThreadBufferSize(threadBufferSize); + } + + if (globalBufferCount != null) { + Options.setGlobalBufferCount(globalBufferCount); + } + + if (globalBufferSize != null) { + Options.setGlobalBufferSize(globalBufferSize); + } + + if (memorySize != null) { + Options.setMemorySize(memorySize); + } + Recording recording = new Recording(); if (name != null) { recording.setName(name); @@ -298,15 +349,13 @@ private static String getPath(Path path) { } } - private static Map parseStartFlightRecording() { - Map optionsMap = new HashMap<>(); - String text = SubstrateOptions.StartFlightRecording.getValue(); - if (!text.isEmpty()) { - JfrStartArgument[] possibleArguments = JfrStartArgument.values(); - String[] options = text.split(","); + private static Map parseJfrOptions(String userInput, JfrArgument[] possibleArguments) { + Map optionsMap = new HashMap<>(); + if (!userInput.isEmpty()) { + String[] options = userInput.split(","); for (String option : options) { String[] keyVal = option.split("="); - JfrStartArgument arg = findArgument(possibleArguments, keyVal[0]); + JfrArgument arg = findArgument(possibleArguments, keyVal[0]); if (arg == null) { throw VMError.shouldNotReachHere("Unknown argument '" + keyVal[0] + "' in " + SubstrateOptions.StartFlightRecording.getName()); } @@ -316,7 +365,7 @@ private static Map parseStartFlightRecording() { return optionsMap; } - private static String[] parseSettings(Map args) throws UserException { + private static String[] parseSettings(Map args) throws UserException { String settings = args.get(JfrStartArgument.Settings); if (settings == null) { return new String[]{DEFAULT_JFC_NAME}; @@ -328,7 +377,7 @@ private static String[] parseSettings(Map args) throws } @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null allowed as return value") - private static Boolean parseBoolean(Map args, JfrStartArgument key) throws IllegalArgumentException { + private static Boolean parseBoolean(Map args, JfrArgument key) throws IllegalArgumentException { String value = args.get(key); if (value == null) { return null; @@ -337,11 +386,11 @@ private static Boolean parseBoolean(Map args, JfrStart } else if ("false".equalsIgnoreCase(value)) { return false; } else { - throw VMError.shouldNotReachHere("Could not parse JFR argument '" + key.cmdLineKey + "=" + value + "'. Expected a boolean value."); + throw VMError.shouldNotReachHere("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. Expected a boolean value."); } } - private static Long parseDuration(Map args, JfrStartArgument key) { + private static Long parseDuration(Map args, JfrStartArgument key) { String value = args.get(key); if (value != null) { try { @@ -385,7 +434,7 @@ private static Long parseDuration(Map args, JfrStartAr return null; } - private static Long parseMaxSize(Map args, JfrStartArgument key) { + private static Long parseMaxSize(Map args, JfrArgument key) { final String value = args.get(key); if (value != null) { try { @@ -418,7 +467,7 @@ private static Long parseMaxSize(Map args, JfrStartArg return number; } } catch (IllegalArgumentException e) { - throw VMError.shouldNotReachHere("Could not parse JFR argument '" + key.cmdLineKey + "=" + value + "'. " + e.getMessage()); + throw VMError.shouldNotReachHere("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); } } return null; @@ -432,16 +481,20 @@ private static int indexOfFirstNonDigitCharacter(String durationText) { return idx; } - private static JfrStartArgument findArgument(JfrStartArgument[] possibleArguments, String value) { - for (JfrStartArgument arg : possibleArguments) { - if (arg.cmdLineKey.equals(value)) { + private static JfrArgument findArgument(JfrArgument[] possibleArguments, String value) { + for (JfrArgument arg : possibleArguments) { + if (arg.getCmdLineKey().equals(value)) { return arg; } } return null; } - private enum JfrStartArgument { + private interface JfrArgument { + String getCmdLineKey(); + } + + private enum JfrStartArgument implements JfrArgument { Name("name"), Settings("settings"), Delay("delay"), @@ -458,5 +511,32 @@ private enum JfrStartArgument { JfrStartArgument(String key) { this.cmdLineKey = key; } + + @Override + public String getCmdLineKey() { + return cmdLineKey; + } + } + + private enum FlightRecorderOptionsArgument implements JfrArgument { + GlobalBufferCount("globalbuffercount"), + GlobalBufferSize("globalbuffersize"), + MaxChunkSize("maxchunksize"), + MemorySize("memorysize"), + RepositoryPath("repositorypath"), + StackDepth("stackdepth"), + ThreadBufferSize("thread_buffer_size"), + PreserveRepository("preserve-repository"); + + private final String cmdLineKey; + + FlightRecorderOptionsArgument(String key) { + this.cmdLineKey = key; + } + + @Override + public String getCmdLineKey() { + return cmdLineKey; + } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrOptionSet.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrOptionSet.java index 977e82957dad..a4a73eb3eb24 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrOptionSet.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrOptionSet.java @@ -24,17 +24,25 @@ */ package com.oracle.svm.core.jfr; -import jdk.graal.compiler.core.common.NumUtil; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.os.VirtualMemoryProvider; +import jdk.graal.compiler.core.common.NumUtil; import jdk.jfr.internal.Options; /** * Holds all JFR-related options that can be set by the user. It is also used to validate and adjust * the option values as needed. + * + * Similar to OpenJDK, the options set via -XX:FlightRecorderOptions cannot be changed after JFR is + * initialized. This means that after {@link SubstrateJVM#createJFR(boolean)} is called, this class + * is no longer needed. + * + * This class is used to store options set at the OpenJDK Java-level which propagate down to the VM + * level via {@link jdk.jfr.internal.JVM}. The option values are stored here until they are + * eventually used when first recording is created and JFR is initialized. */ public class JfrOptionSet { private static final int MEMORY_SIZE = 1; @@ -54,6 +62,7 @@ public class JfrOptionSet { public final JfrOptionLong globalBufferCount; public final JfrOptionLong memorySize; public final JfrOptionLong maxChunkSize; + public final JfrOptionLong stackDepth; @Platforms(Platform.HOSTED_ONLY.class) public JfrOptionSet() { @@ -62,6 +71,7 @@ public JfrOptionSet() { globalBufferCount = new JfrOptionLong(Options.getGlobalBufferCount()); memorySize = new JfrOptionLong(Options.getMemorySize()); maxChunkSize = new JfrOptionLong(Options.getMaxChunkSize()); + stackDepth = new JfrOptionLong(Options.getStackDepth()); } public void validateAndAdjustMemoryOptions() { 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 8b4975136958..0a5a241e3f40 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,7 +26,6 @@ import java.util.List; -import com.oracle.svm.core.log.Log; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platform; @@ -39,6 +38,7 @@ import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.jfr.logging.JfrLogging; import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler; +import com.oracle.svm.core.log.Log; import com.oracle.svm.core.sampler.SamplerBufferPool; import com.oracle.svm.core.sampler.SubstrateSigprofHandler; import com.oracle.svm.core.thread.JavaThreads; @@ -209,6 +209,7 @@ public boolean createJFR(boolean simulateFailure) { threadLocal.initialize(options.threadBufferSize.getValue()); globalMemory.initialize(options.globalBufferSize.getValue(), options.globalBufferCount.getValue()); unlockedChunkWriter.initialize(options.maxChunkSize.getValue()); + stackTraceRepo.setStackTraceDepth(NumUtil.safeToInt(options.stackDepth.getValue())); recorderThread.start(); @@ -444,7 +445,7 @@ public void setCompressedIntegers(boolean compressed) { * See {@link JVM#setStackDepth}. */ public void setStackDepth(int depth) { - stackTraceRepo.setStackTraceDepth(depth); + options.stackDepth.setUserValue(depth); } /**