From db5a55977defd3d02d5dd0bcd4d039b9beded834 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Tue, 14 May 2024 14:43:16 -0400 Subject: [PATCH] Attach API and JCMD --- .../svm/core/DumpRuntimeCompilationDcmd.java | 47 +++ ...DumpRuntimeCompilationOnSignalFeature.java | 29 +- .../core/DumpRuntimeCompilationSupport.java | 52 +++ .../core/DumpThreadStacksOnSignalFeature.java | 73 +--- .../svm/core/DumpThreadStacksSupport.java | 94 ++++++ .../oracle/svm/core/VMInspectionOptions.java | 11 +- .../svm/core/attach/AttachApiFeature.java | 77 +++++ .../svm/core/attach/AttachApiSupport.java | 177 ++++++++++ .../svm/core/attach/AttachListenerThread.java | 266 +++++++++++++++ .../oracle/svm/core/dcmd/AbstractDcmd.java | 99 ++++++ .../src/com/oracle/svm/core/dcmd/Dcmd.java | 47 +++ .../com/oracle/svm/core/dcmd/DcmdFeature.java | 51 +++ .../com/oracle/svm/core/dcmd/DcmdOption.java | 57 ++++ .../svm/core/dcmd/DcmdParseException.java | 36 ++ .../com/oracle/svm/core/dcmd/DcmdSupport.java | 89 +++++ .../com/oracle/svm/core/dcmd/HelpDcmd.java | 70 ++++ .../svm/core/heap/dump/HeapDumpDcmd.java | 71 ++++ .../svm/core/jfr/JfrArgumentParser.java | 297 +++++++++++++++++ .../com/oracle/svm/core/jfr/JfrFeature.java | 11 + .../com/oracle/svm/core/jfr/JfrManager.java | 280 +++------------- .../svm/core/jfr/dcmd/JfrCheckDcmd.java | 63 ++++ .../oracle/svm/core/jfr/dcmd/JfrDumpDcmd.java | 314 ++++++++++++++++++ .../svm/core/jfr/dcmd/JfrStartDcmd.java | 125 +++++++ .../oracle/svm/core/jfr/dcmd/JfrStopDcmd.java | 113 +++++++ .../svm/core/jvmstat/SystemCounters.java | 6 + .../svm/core/nmt/NativeMemoryTracking.java | 42 ++- .../src/com/oracle/svm/core/nmt/NmtDcmd.java | 59 ++++ .../com/oracle/svm/core/nmt/NmtFeature.java | 4 + .../svm/core/thread/ThreadDumpStacksDcmd.java | 50 +++ .../svm/core/thread/ThreadDumpToFileDcmd.java | 90 +++++ .../svm/hosted/heap/HeapDumpFeature.java | 5 + .../native-image.properties | 2 +- .../oracle/svm/test/attach/AttachTest.java | 164 +++++++++ 33 files changed, 2639 insertions(+), 332 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationSupport.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksSupport.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiFeature.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiSupport.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachListenerThread.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/Dcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdFeature.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdOption.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdParseException.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdSupport.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrCheckDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrDumpDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStartDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStopDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpStacksDcmd.java create mode 100644 substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpToFileDcmd.java create mode 100644 substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/attach/AttachTest.java diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationDcmd.java new file mode 100644 index 000000000000..b754c3c4fcd1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationDcmd.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core; + +import com.oracle.svm.core.dcmd.AbstractDcmd; +import com.oracle.svm.core.dcmd.DcmdParseException; + +public class DumpRuntimeCompilationDcmd extends AbstractDcmd { + + public DumpRuntimeCompilationDcmd() { + this.name = "VM.runtime_compilation"; + this.impact = "low"; + } + + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + if (arguments.length > 1) { + throw new DcmdParseException("Too many arguments specified"); + } + + DumpRuntimeCompilationSupport.dump(); + return "Dump created."; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationOnSignalFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationOnSignalFeature.java index 981fd5380bdd..bebcea073967 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationOnSignalFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationOnSignalFeature.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,14 +27,13 @@ import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platform.WINDOWS; +import org.graalvm.nativeimage.ImageSingletons; +import com.oracle.svm.core.dcmd.DcmdSupport; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.graal.RuntimeCompilation; -import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.jdk.RuntimeSupport; -import com.oracle.svm.core.log.Log; -import com.oracle.svm.core.thread.JavaVMOperation; import jdk.internal.misc.Signal; @@ -47,7 +47,11 @@ public boolean isInConfiguration(IsInConfigurationAccess access) { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { - RuntimeSupport.getRuntimeSupport().addStartupHook(new DumpRuntimeCompilationStartupHook()); + if (VMInspectionOptions.hasAttachSupport()) { + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new DumpRuntimeCompilationDcmd()); + } else { + RuntimeSupport.getRuntimeSupport().addStartupHook(new DumpRuntimeCompilationStartupHook()); + } } } @@ -67,20 +71,7 @@ static void install() { @Override public void handle(Signal arg0) { - DumpRuntimeCompiledMethodsOperation vmOp = new DumpRuntimeCompiledMethodsOperation(); - vmOp.enqueue(); + DumpRuntimeCompilationSupport.dump(); } - private static class DumpRuntimeCompiledMethodsOperation extends JavaVMOperation { - DumpRuntimeCompiledMethodsOperation() { - super(VMOperationInfos.get(DumpRuntimeCompiledMethodsOperation.class, "Dump runtime compiled methods", SystemEffect.SAFEPOINT)); - } - - @Override - protected void operate() { - Log log = Log.log(); - SubstrateDiagnostics.dumpRuntimeCompilation(log); - log.flush(); - } - } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationSupport.java new file mode 100644 index 000000000000..1e74f988584c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpRuntimeCompilationSupport.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core; + +import com.oracle.svm.core.heap.VMOperationInfos; +import com.oracle.svm.core.log.Log; +import com.oracle.svm.core.thread.JavaVMOperation; + +public class DumpRuntimeCompilationSupport { + + public static void dump() { + DumpRuntimeCompiledMethodsOperation vmOp = new DumpRuntimeCompiledMethodsOperation(); + vmOp.enqueue(); + } + + private static class DumpRuntimeCompiledMethodsOperation extends JavaVMOperation { + DumpRuntimeCompiledMethodsOperation() { + super(VMOperationInfos.get(DumpRuntimeCompiledMethodsOperation.class, "Dump runtime compiled methods", SystemEffect.SAFEPOINT)); + } + + @Override + protected void operate() { + Log log = Log.log(); + SubstrateDiagnostics.dumpRuntimeCompilation(log); + log.flush(); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksOnSignalFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksOnSignalFeature.java index b4bd97ef37b2..a189cec871ff 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksOnSignalFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksOnSignalFeature.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,22 +25,15 @@ */ package com.oracle.svm.core; -import org.graalvm.nativeimage.CurrentIsolate; -import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platform.WINDOWS; +import org.graalvm.nativeimage.ImageSingletons; +import com.oracle.svm.core.dcmd.DcmdSupport; +import com.oracle.svm.core.thread.ThreadDumpStacksDcmd; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; -import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.jdk.RuntimeSupport; -import com.oracle.svm.core.log.Log; -import com.oracle.svm.core.stack.JavaStackWalker; -import com.oracle.svm.core.stack.ThreadStackPrinter.StackFramePrintVisitor; -import com.oracle.svm.core.thread.JavaThreads; -import com.oracle.svm.core.thread.JavaVMOperation; -import com.oracle.svm.core.thread.PlatformThreads; -import com.oracle.svm.core.thread.VMThreads; import jdk.internal.misc.Signal; @@ -53,7 +47,11 @@ public boolean isInConfiguration(IsInConfigurationAccess access) { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { - RuntimeSupport.getRuntimeSupport().addStartupHook(new DumpThreadStacksOnSignalStartupHook()); + if (Platform.includedIn(WINDOWS.class) || !VMInspectionOptions.hasAttachSupport()) { + RuntimeSupport.getRuntimeSupport().addStartupHook(new DumpThreadStacksOnSignalStartupHook()); + } else { + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new ThreadDumpStacksDcmd()); + } } } @@ -73,55 +71,6 @@ static void install() { @Override public void handle(Signal arg0) { - DumpAllStacksOperation vmOp = new DumpAllStacksOperation(); - vmOp.enqueue(); - } - - private static class DumpAllStacksOperation extends JavaVMOperation { - DumpAllStacksOperation() { - super(VMOperationInfos.get(DumpAllStacksOperation.class, "Dump all stacks", SystemEffect.SAFEPOINT)); - } - - @Override - protected void operate() { - Log log = Log.log(); - log.string("Full thread dump:").newline().newline(); - for (IsolateThread vmThread = VMThreads.firstThread(); vmThread.isNonNull(); vmThread = VMThreads.nextThread(vmThread)) { - if (vmThread == CurrentIsolate.getCurrentThread()) { - /* Skip the signal handler stack */ - continue; - } - try { - dumpStack(log, vmThread); - } catch (Exception e) { - log.string("Exception during dumpStack: ").string(e.getClass().getName()).newline(); - log.string(e.getMessage()).newline(); - } - } - log.flush(); - } - - private static void dumpStack(Log log, IsolateThread vmThread) { - Thread javaThread = PlatformThreads.fromVMThread(vmThread); - if (javaThread != null) { - log.character('"').string(javaThread.getName()).character('"'); - log.string(" #").signed(JavaThreads.getThreadId(javaThread)); - if (javaThread.isDaemon()) { - log.string(" daemon"); - } - } else { - log.string("(no Java thread)"); - } - log.string(" thread=").zhex(vmThread); - if (javaThread != null) { - log.string(" state=").string(javaThread.getState().name()); - } - log.newline(); - - log.indent(true); - StackFramePrintVisitor visitor = new StackFramePrintVisitor(); - JavaStackWalker.walkThread(vmThread, visitor, log); - log.indent(false); - } + DumpThreadStacksSupport.dump(); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksSupport.java new file mode 100644 index 000000000000..d1f506c3c55c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DumpThreadStacksSupport.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core; + +import org.graalvm.nativeimage.CurrentIsolate; +import org.graalvm.nativeimage.IsolateThread; + +import com.oracle.svm.core.heap.VMOperationInfos; +import com.oracle.svm.core.log.Log; +import com.oracle.svm.core.stack.JavaStackWalker; +import com.oracle.svm.core.stack.ThreadStackPrinter.StackFramePrintVisitor; +import com.oracle.svm.core.thread.JavaThreads; +import com.oracle.svm.core.thread.JavaVMOperation; +import com.oracle.svm.core.thread.PlatformThreads; +import com.oracle.svm.core.thread.VMThreads; + +public class DumpThreadStacksSupport { + public static void dump() { + DumpAllStacksOperation vmOp = new DumpAllStacksOperation(); + vmOp.enqueue(); + } + + private static class DumpAllStacksOperation extends JavaVMOperation { + DumpAllStacksOperation() { + super(VMOperationInfos.get(DumpAllStacksOperation.class, "Dump all stacks", SystemEffect.SAFEPOINT)); + } + + @Override + protected void operate() { + Log log = Log.log(); + log.string("Full thread dump:").newline().newline(); + for (IsolateThread vmThread = VMThreads.firstThread(); vmThread.isNonNull(); vmThread = VMThreads.nextThread(vmThread)) { + if (vmThread == CurrentIsolate.getCurrentThread()) { + /* Skip the signal handler stack */ + continue; + } + try { + dumpStack(log, vmThread); + } catch (Exception e) { + log.string("Exception during dumpStack: ").string(e.getClass().getName()).newline(); + log.string(e.getMessage()).newline(); + } + } + log.flush(); + } + + private static void dumpStack(Log log, IsolateThread vmThread) { + Thread javaThread = PlatformThreads.fromVMThread(vmThread); + if (javaThread != null) { + log.character('"').string(javaThread.getName()).character('"'); + log.string(" #").signed(JavaThreads.getThreadId(javaThread)); + if (javaThread.isDaemon()) { + log.string(" daemon"); + } + } else { + log.string("(no Java thread)"); + } + log.string(" thread=").zhex(vmThread); + if (javaThread != null) { + log.string(" state=").string(javaThread.getState().name()); + } + log.newline(); + + log.indent(true); + StackFramePrintVisitor visitor = new StackFramePrintVisitor(); + JavaStackWalker.walkThread(vmThread, visitor, log); + log.indent(false); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java index 794755a206c3..ef20b67c48de 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/VMInspectionOptions.java @@ -60,11 +60,13 @@ public final class VMInspectionOptions { private static final String MONITORING_JMXSERVER_NAME = "jmxserver"; private static final String MONITORING_THREADDUMP_NAME = "threaddump"; private static final String MONITORING_NMT_NAME = "nmt"; + private static final String MONITORING_ATTACH_NAME = "attach"; private static final List MONITORING_ALL_VALUES = List.of(MONITORING_HEAPDUMP_NAME, MONITORING_JFR_NAME, MONITORING_JVMSTAT_NAME, MONITORING_JMXCLIENT_NAME, MONITORING_JMXSERVER_NAME, - MONITORING_THREADDUMP_NAME, MONITORING_NMT_NAME, MONITORING_ALL_NAME, MONITORING_DEFAULT_NAME); + MONITORING_THREADDUMP_NAME, MONITORING_NMT_NAME, MONITORING_ATTACH_NAME, MONITORING_ALL_NAME, MONITORING_DEFAULT_NAME); private static final String MONITORING_ALLOWED_VALUES_TEXT = "'" + MONITORING_HEAPDUMP_NAME + "', '" + MONITORING_JFR_NAME + "', '" + MONITORING_JVMSTAT_NAME + "', '" + MONITORING_JMXSERVER_NAME + - "' (experimental), '" + MONITORING_JMXCLIENT_NAME + "' (experimental), '" + MONITORING_THREADDUMP_NAME + "', '" + MONITORING_NMT_NAME + "' (experimental), or '" + + "' (experimental), '" + MONITORING_JMXCLIENT_NAME + "' (experimental), '" + MONITORING_THREADDUMP_NAME + "', '" + MONITORING_NMT_NAME + "', '" + MONITORING_ATTACH_NAME + + "' (experimental), or '" + MONITORING_ALL_NAME + "' (deprecated behavior: defaults to '" + MONITORING_ALL_NAME + "' if no argument is provided)"; static { @@ -178,6 +180,11 @@ public static boolean hasNativeMemoryTrackingSupport() { return hasAllOrKeywordMonitoringSupport(MONITORING_NMT_NAME); } + @Fold + public static boolean hasAttachSupport() { + return hasAllOrKeywordMonitoringSupport(MONITORING_ATTACH_NAME) && !Platform.includedIn(WINDOWS.class); + } + static class DeprecatedOptions { @Option(help = "Enables features that allow the VM to be inspected during run time.", type = OptionType.User, // deprecated = true, deprecationMessage = "Please use '--" + ENABLE_MONITORING_OPTION + "'") // diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiFeature.java new file mode 100644 index 000000000000..e82ff86c1bd3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiFeature.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.attach; + +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.svm.core.VMInspectionOptions; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.jdk.RuntimeSupport; +import jdk.internal.misc.Signal; + +@AutomaticallyRegisteredFeature +public class AttachApiFeature implements InternalFeature { + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return VMInspectionOptions.hasAttachSupport(); + } + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + RuntimeSupport.getRuntimeSupport().addStartupHook(new AttachApiStartupHook()); + RuntimeSupport.getRuntimeSupport().addShutdownHook(new AttachApiShutdownHook()); + ImageSingletons.add(AttachApiSupport.class, new AttachApiSupport()); + } +} + +final class AttachApiStartupHook implements RuntimeSupport.Hook { + @Override + public void execute(boolean isFirstIsolate) { + if (isFirstIsolate) { + SigquitHandler.install(); + } + } +} + +final class AttachApiShutdownHook implements RuntimeSupport.Hook { + @Override + public void execute(boolean isFirstIsolate) { + AttachApiSupport.singleton().teardown(); + } +} + +class SigquitHandler implements Signal.Handler { + static void install() { + Signal.handle(new Signal("QUIT"), new SigquitHandler()); + } + + @Override + public void handle(Signal arg0) { + AttachApiSupport.singleton().maybeInitialize(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiSupport.java new file mode 100644 index 000000000000..cc9fcf5529bf --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachApiSupport.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.attach; + +import org.graalvm.nativeimage.ImageSingletons; + +import java.io.File; +import java.io.IOException; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.ServerSocketChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; +import com.oracle.svm.core.log.Log; + +import jdk.graal.compiler.api.replacements.Fold; + +import static java.net.StandardProtocolFamily.UNIX; + +/** + * This class is responsible for initialization/shutdown of the Attach-API. This includes performing + * the initialization handshake and setting up the UNIX domain sockets. Similar to Hotspot, once + * initialized, it will dispatch a dedicated thread to handle new connections. + */ +public class AttachApiSupport { + private boolean initialized; + private ServerSocketChannel serverChannel; + AttachListenerThread attachListenerThread; + private Path socketFile; + + @Fold + public static AttachApiSupport singleton() { + return ImageSingletons.lookup(AttachApiSupport.class); + } + + synchronized void maybeInitialize() { + if (isInitTrigger()) { + if (!initialized) { + init(); + } else if (!Files.exists(getSocketFilePath())) { + // If socket file is missing, but we're already initialized, restart. + teardown(); + init(); + } + } + } + + private synchronized void init() { + assert (!initialized); + // Set up Attach API unix domain socket + serverChannel = createServerSocket(); + if (serverChannel != null) { + attachListenerThread = new AttachListenerThread(serverChannel); + attachListenerThread.start(); + initialized = true; + } + } + + /** Stop dedicated thread. Close socket. Uninitialized. Can be initialized again. */ + public synchronized void teardown() { + if (initialized) { + try { + attachListenerThread.shutdown(); + attachListenerThread.join(); + serverChannel.close(); + socketFile = null; + initialized = false; + // .close() does not delete the file. + Files.deleteIfExists(getSocketFilePath()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + /** + * This method determines whether the SIGQUIT we've received is actually a signal to start the + * Attach API handshake. It is loosely based on AttachListener::is_init_trigger() in + * attachListener.cpp in jdk-24+2. + */ + private static boolean isInitTrigger() { + // Determine whether an attempt to use the Attach API is being made. + File attachFile = new File(".attach_pid" + ProcessHandle.current().pid()); + + if (!attachFile.exists()) { + // Check the alternate location. + String tempDir = System.getProperty("java.io.tmpdir"); + attachFile = new File(tempDir + "/.attach_pid" + ProcessHandle.current().pid()); + if (!attachFile.exists()) { + Log.log().string("Attach-API could not find .attach_pid file").newline(); + return false; + } + } + return true; + } + + private Path getSocketFilePath() { + if (socketFile == null) { + socketFile = Paths.get(getSocketPathString()); + } + return socketFile; + } + + private static String getSocketPathString() { + long pid = ProcessHandle.current().pid(); + String tempDir = System.getProperty("java.io.tmpdir"); + if (tempDir == null) { + tempDir = "/tmp"; + } + if (!Files.isDirectory(Paths.get(tempDir))) { + throw new RuntimeException("Could not find temporary directory."); + } + return tempDir + "/.java_pid" + pid; + } + + /** + * This method creates the server socket channel that will be handed over to the dedicated + * attach API listener thread. We must set specific permissions on the socket file, otherwise + * the client will not accept it. It is important that the permissions are set before the + * filename is set to the correct name the client is polling for. + */ + private ServerSocketChannel createServerSocket() { + String socketPathString = getSocketPathString(); + Path initialPath = Paths.get(socketPathString + "_tmp"); + Path finalPath = getSocketFilePath(); + var address = UnixDomainSocketAddress.of(initialPath); + ServerSocketChannel sc = null; + try { + sc = ServerSocketChannel.open(UNIX); + // Create the socket file + sc.bind(address); + // Change the file permissions + Set permissions = PosixFilePermissions.fromString("rw-------"); + Files.setPosixFilePermissions(initialPath, permissions); + + // Rename socket file so it can begin being used. + Files.move(initialPath, finalPath); + Files.deleteIfExists(initialPath); + } catch (IOException e) { + try { + Files.deleteIfExists(initialPath); + Files.deleteIfExists(finalPath); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + Log.log().string("Unable to create server socket. " + e).newline(); + } + return sc; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachListenerThread.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachListenerThread.java new file mode 100644 index 000000000000..9d0448675c59 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/attach/AttachListenerThread.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.attach; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedByInterruptException; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; + +import org.graalvm.nativeimage.ImageSingletons; +import com.oracle.svm.core.dcmd.DcmdSupport; +import com.oracle.svm.core.dcmd.DcmdParseException; +import com.oracle.svm.core.log.Log; +import com.oracle.svm.core.util.BasedOnJDKFile; + +/** + * This class is responsible for receiving connections and dispatching to the appropriate tool (jcmd + * etc). + */ +public final class AttachListenerThread extends Thread { + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/share/services/attachListener.hpp#L143") // + private static final int ARG_LENGTH_MAX = 1024; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/share/services/attachListener.hpp#L144") // + private static final int ARG_COUNT_MAX = 3; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/os/posix/attachListener_posix.cpp#L84") // + private static final char VERSION = '1'; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/os/posix/attachListener_posix.cpp#L259") // + private static final int VERSION_SIZE = 8; + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/hotspot/os/posix/attachListener_posix.cpp#L87") // + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.attach/share/classes/sun/tools/attach/HotSpotVirtualMachine.java#L401") // + private static final String ATTACH_ERROR_BAD_VERSION = "101"; + private static final String RESPONSE_CODE_OK = "0"; + private static final String RESPONSE_CODE_BAD = "1"; + private static final String JCMD_COMMAND_STRING = "jcmd"; + + private ServerSocketChannel serverSocketChannel; + private volatile boolean shutdown = false; + + public AttachListenerThread(ServerSocketChannel serverSocketChannel) { + super("AttachListener"); + this.serverSocketChannel = serverSocketChannel; + setDaemon(true); + } + + /** + * The method is the main loop where the dedicated listener thread accepts client connections + * and handles commands. It is loosely based on AttachListenerThread::thread_entry in + * attachListener.cpp in jdk-24+2. + */ + @Override + public void run() { + AttachRequest request = null; + while (true) { + try { + // Dequeue a connection from socket. May block inside here waiting for connections. + request = dequeue(serverSocketChannel); + + // Check if this thread been signalled to finish executing. + if (shutdown) { + return; + } + + // Find the correct handler to dispatch too. + if (request.name.equals(JCMD_COMMAND_STRING)) { + String response = jcmd(request.arguments); + sendResponse(request.clientChannel, response, RESPONSE_CODE_OK); + } else { + sendResponse(request.clientChannel, "Invalid Operation. Only JCMD is supported currently.", RESPONSE_CODE_BAD); + } + + request.clientChannel.close(); + + } catch (IOException e) { + request.closeConnection(); + AttachApiSupport.singleton().teardown(); + } catch (DcmdParseException e) { + sendResponse(request.clientChannel, e.getMessage(), RESPONSE_CODE_BAD); + request.closeConnection(); + } + } + } + + /** + * This method will loop or block until a valid actionable request is received. It is loosely + * based on PosixAttachListener::dequeue() in attachListener_posix.cpp in jdk-24+2. + */ + private AttachRequest dequeue(ServerSocketChannel serverChannel) throws IOException { + AttachRequest request = new AttachRequest(); + while (true) { + + if (shutdown) { + return null; + } + + try { + // Block waiting for a connection + request.clientChannel = serverChannel.accept(); + } catch (ClosedByInterruptException e) { + // Allow unblocking if a teardown has been signalled. + return null; + } + + readRequest(request); + if (request.error != null) { + sendResponse(request.clientChannel, null, request.error); + request.reset(); + } else if (request.name == null || request.arguments == null) { + // Didn't get any usable request data. Try again. + request.reset(); + } else { + return request; + } + } + } + + /** + * This method reads and processes a single request from the socket. It is loosely based on + * PosixAttachListener::read_request in attachListener_posix.cpp in jdk-24+2. + */ + private static void readRequest(AttachRequest request) throws IOException { + int expectedStringCount = 2 + ARG_COUNT_MAX; + int maxLen = (VERSION_SIZE + 1) + (ARG_LENGTH_MAX + 1) + (ARG_COUNT_MAX * (ARG_LENGTH_MAX + 1)); + int strCount = 0; + long left = maxLen; + ByteBuffer buf = ByteBuffer.allocate(maxLen); + + // The current position to inspect. + int bufIdx = 0; + // The start of the arguments. + int argIdx = 0; + // The start of the command type name. + int nameIdx = 0; + + // Start reading messages + while (strCount < expectedStringCount && left > 0) { + + // Do a single read. + int bytesRead = request.clientChannel.read(buf); + + // Check if finished or error. + if (bytesRead < 0) { + break; + } + + // Process data from a single read. + for (int i = 0; i < bytesRead; i++) { + if (buf.get(bufIdx) == 0) { + if (strCount == 0) { + // The first string should be the version identifier. + if ((char) buf.get(bufIdx - 1) == VERSION) { + nameIdx = bufIdx + 1; + } else { + /* + * Version is no good. Drain reads to avoid "Connection reset by peer" + * before sending error code and starting again. + */ + request.error = ATTACH_ERROR_BAD_VERSION; + } + } else if (strCount == 1) { + // The second string specifies the command type. + argIdx = bufIdx + 1; + request.name = StandardCharsets.UTF_8.decode(buf.slice(nameIdx, bufIdx - nameIdx)).toString(); + } + strCount++; + } + bufIdx++; + } + left -= bytesRead; + } + + // Only set arguments if we read real data. + if (argIdx > 0 && bufIdx > 0) { + // Remove non-printable characters from the result. + request.arguments = StandardCharsets.UTF_8.decode(buf.slice(argIdx, bufIdx - 1 - argIdx)).toString().replaceAll("\\P{Print}", ""); + } + } + + private static String jcmd(String arguments) throws DcmdParseException { + return ImageSingletons.lookup(DcmdSupport.class).parseAndExecute(arguments); + } + + /** + * This method sends response data, or error data back to the client. It is loosely based on + * PosixAttachOperation::complete in in attachListener_posix.cpp in jdk-24+2. + */ + private static void sendResponse(SocketChannel clientChannel, String response, String code) { + try { + // Send result + ByteBuffer buffer = ByteBuffer.allocate(32); + buffer.clear(); + buffer.put((code + "\n").getBytes(StandardCharsets.UTF_8)); + buffer.flip(); + clientChannel.write(buffer); + + if (response != null && !response.isEmpty()) { + // Send data + byte[] responseBytes = response.getBytes(); + buffer = ByteBuffer.allocate(responseBytes.length); + buffer.clear(); + buffer.put(responseBytes); + buffer.flip(); + clientChannel.write(buffer); + } + } catch (IOException e) { + Log.log().string("Unable to send Attach API response: " + e).newline(); + } + } + + /** This method is called to notify the listener thread that it should finish. */ + void shutdown() { + shutdown = true; + this.interrupt(); + } + + /** This represents one individual connection/command request. */ + static class AttachRequest { + public String name; + public String arguments; + public SocketChannel clientChannel; + public String error; + + public void reset() { + closeConnection(); + clientChannel = null; + error = null; + name = null; + arguments = null; + } + + public void closeConnection() { + if (clientChannel != null && clientChannel.isConnected()) { + try { + clientChannel.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDcmd.java new file mode 100644 index 000000000000..154d8fb06944 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/AbstractDcmd.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +public abstract class AbstractDcmd implements Dcmd { + protected DcmdOption[] options; + protected String[] examples; + protected String name; + protected String description; + protected String impact; + + @Override + public DcmdOption[] getOptions() { + return options; + } + + @Override + public String[] getExample() { + return examples; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String getImpact() { + return impact; + } + + @Override + public String printHelp() { + StringBuilder sb = new StringBuilder(); + sb.append(getName()).append("\n"); + if (getDescription() != null) { + sb.append(getDescription()).append("\n"); + } + sb.append("Impact: ").append(this.getImpact()).append("\n"); + sb.append("Syntax: ").append(getName()); + + if (getOptions() != null) { + sb.append(" [options]\n"); + sb.append("Options:\n"); + for (DcmdOption option : this.getOptions()) { + sb.append("\t").append(option.getName()).append(": "); + if (option.isRequired()) { + sb.append("[Required] "); + } else { + sb.append("[Optional] "); + } + sb.append(option.getDescription()); + if (option.getDefaultValue() != null) { + sb.append(" Default value: ").append(option.getDefaultValue()); + } + sb.append("\n"); + } + } + sb.append("\n"); + + if (getExample() != null) { + sb.append("Examples:\n"); + for (String example : this.getExample()) { + sb.append("\t").append(example).append("\n"); + } + sb.append("\n"); + } + return sb.toString(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/Dcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/Dcmd.java new file mode 100644 index 000000000000..7088125e75ff --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/Dcmd.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +public interface Dcmd { + /** + * Parse arguments and set internal fields to be used during execution. Returns a string to + * later be sent through the server socket as a response. + */ + String parseAndExecute(String[] arguments) throws DcmdParseException; + + String getName(); + + String getDescription(); + + String getImpact(); + + String[] getExample(); + + DcmdOption[] getOptions(); + + String printHelp(); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdFeature.java new file mode 100644 index 000000000000..fe1290e1c869 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdFeature.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.svm.core.VMInspectionOptions; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.thread.ThreadDumpToFileDcmd; + +@AutomaticallyRegisteredFeature +public class DcmdFeature implements InternalFeature { + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return VMInspectionOptions.hasAttachSupport(); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + ImageSingletons.add(DcmdSupport.class, new DcmdSupport()); + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new HelpDcmd()); + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new ThreadDumpToFileDcmd()); + } + +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdOption.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdOption.java new file mode 100644 index 000000000000..025a2c31c3e4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdOption.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +public class DcmdOption { + private String name; + private String description; + private boolean required; + private String defaultValue; + + public DcmdOption(String name, String description, boolean required, String defaultValue) { + this.name = name; + this.description = description; + this.required = required; + this.defaultValue = defaultValue; + } + + String getName() { + return name; + } + + String getDescription() { + return description; + } + + boolean isRequired() { + return required; + } + + String getDefaultValue() { + return defaultValue; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdParseException.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdParseException.java new file mode 100644 index 000000000000..4735dce5dde7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdParseException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +/** For validation of input arguments. */ +public class DcmdParseException extends Exception { + public DcmdParseException(String message) { + super(message); + } + + private static final long serialVersionUID = 1L; +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdSupport.java new file mode 100644 index 000000000000..6742ff8c8a46 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/DcmdSupport.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.Platform.HOSTED_ONLY; + +import java.util.ArrayList; +import java.util.List; + +/** + * Diagnostic commands should only be registered at build time and are effectively singletons + * managed by this class. The Attach-API uses this class directly, which then dispatches the + * appropriate diagnostic command to handle the request. + */ +public class DcmdSupport { + private List dcmds; + + @Platforms(HOSTED_ONLY.class) + public DcmdSupport() { + dcmds = new ArrayList<>(); + } + + Dcmd getDcmd(String cmdName) { + for (Dcmd dcmd : dcmds) { + if (dcmd.getName().equals(cmdName)) { + return dcmd; + } + } + return null; + } + + /** Should be called by relevant features that want to be accessed via diagnostic commands. */ + @Platforms(HOSTED_ONLY.class) + public void registerDcmd(Dcmd dcmd) { + dcmds.add(dcmd); + } + + String[] getRegisteredCommands() { + String[] commands = new String[dcmds.size()]; + for (int i = 0; i < dcmds.size(); i++) { + commands[i] = dcmds.get(i).getName(); + } + return commands; + } + + /** + * This method is to be used at runtime by the Attach-API. It connects the Attach-API with the + * DCMD infrastructure. + */ + public String parseAndExecute(String arguments) throws DcmdParseException { + String[] argumentsSplit = arguments.split(" "); + assert argumentsSplit.length > 0; + String cmdName = argumentsSplit[0]; + + Dcmd dcmd = getDcmd(cmdName); + + if (dcmd == null) { + return "The requested command is not supported."; + } + + return dcmd.parseAndExecute(argumentsSplit); + } + +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDcmd.java new file mode 100644 index 000000000000..8ba26d3b33a5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/dcmd/HelpDcmd.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.dcmd; + +import org.graalvm.nativeimage.ImageSingletons; + +public class HelpDcmd extends AbstractDcmd { + + public HelpDcmd() { + this.options = new DcmdOption[]{new DcmdOption("command name", "The name of the command for which we want help", false, null)}; + this.examples = new String[]{ + "$ jcmd help JFR.stop", + "$ jcmd help VM.native_memory" + }; + this.name = "help"; + this.description = "For more information about a specific command use 'help '. With no argument this will show a list of available commands."; + this.impact = "low"; + } + + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + String commandName = null; + if (arguments.length > 1) { + commandName = arguments[1]; + } + if (arguments.length > 2) { + throw new DcmdParseException("Too many arguments specified"); + } + + if (commandName == null) { + String[] commands = ImageSingletons.lookup(DcmdSupport.class).getRegisteredCommands(); + StringBuilder sb = new StringBuilder(); + for (String command : commands) { + sb.append(command).append("\n"); + } + sb.append(getName()).append("\n"); + return sb.toString(); + } else { + Dcmd dcmd = ImageSingletons.lookup(DcmdSupport.class).getDcmd(commandName); + if (dcmd == null) { + throw new DcmdParseException("Specified command was not found: " + commandName); + } + return dcmd.printHelp(); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpDcmd.java new file mode 100644 index 000000000000..9a6f6966f2cd --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpDcmd.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.heap.dump; + +import com.oracle.svm.core.dcmd.AbstractDcmd; +import com.oracle.svm.core.dcmd.DcmdOption; + +import java.io.IOException; +import com.oracle.svm.core.dcmd.DcmdParseException; + +public class HeapDumpDcmd extends AbstractDcmd { + + public HeapDumpDcmd() { + this.options = new DcmdOption[]{ + new DcmdOption("filename", "File path of where to put the heap dump.", true, null) + }; + + this.name = "GC.heap_dump"; + this.description = "Generate a HPROF format dump of the heap."; + this.impact = "medium"; + } + + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + String path = null; + if (arguments.length != 2) { + throw new DcmdParseException("Must specify file to dump to."); + } + if (arguments[1].contains("filename=")) { + String[] pathArgumentSplit = arguments[1].split("="); + if (pathArgumentSplit.length != 2) { + throw new DcmdParseException("Must specify file to dump to."); + } + path = pathArgumentSplit[1]; + } + + if (path == null) { + return "The argument 'filename' is mandatory."; + } + try { + HeapDumping.singleton().dumpHeap(path, true); + } catch (IOException e) { + return "Could not dump heap: " + e; + } + return "Dumped to: " + path; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java new file mode 100644 index 000000000000..a81c28eb5e9b --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrArgumentParser.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jfr; + +import com.oracle.svm.core.option.RuntimeOptionKey; + +import java.io.Serial; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import jdk.graal.compiler.core.common.SuppressFBWarnings; + +public class JfrArgumentParser { + private static final String DEFAULT_JFC_NAME = "default"; + + public static Map parseJfrOptions(RuntimeOptionKey runtimeOptionKey, JfrArgument[] possibleArguments) throws JfrArgumentParsingFailed { + String userInput = runtimeOptionKey.getValue(); + if (!userInput.isEmpty()) { + String[] options = userInput.split(","); + return parseJfrOptions(options, possibleArguments); + } + return new HashMap<>(); + } + + public static Map parseJfrOptions(String[] options, JfrArgument[] possibleArguments) throws JfrArgumentParsingFailed { + Map optionsMap = new HashMap<>(); + + for (String option : options) { + String[] keyVal = option.split("="); + if (keyVal.length != 2) { + throw new JfrArgumentParsingFailed("Invalid command structure."); + } + JfrArgument arg = findArgument(possibleArguments, keyVal[0]); + if (arg == null) { + throw new JfrArgumentParsingFailed("Unknown argument '" + keyVal[0] + "' in JFR options"); + } + optionsMap.put(arg, keyVal[1]); + } + + return optionsMap; + } + + public static String[] parseSettings(Map args) { + String settings = args.get(JfrStartArgument.Settings); + if (settings == null) { + return new String[]{DEFAULT_JFC_NAME}; + } else if (settings.equals("none")) { + return new String[0]; + } else { + return settings.split(","); + } + } + + @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null allowed as return value") + public static Boolean parseBoolean(Map args, JfrArgument key) throws JfrArgumentParsingFailed { + String value = args.get(key); + if (value == null) { + return null; + } else if ("true".equalsIgnoreCase(value)) { + return true; + } else if ("false".equalsIgnoreCase(value)) { + return false; + } else { + throw new JfrArgumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. Expected a boolean value."); + } + } + + public static Long parseDuration(Map args, JfrArgument key) throws JfrArgumentParsingFailed { + String value = args.get(key); + if (value != null) { + try { + int idx = indexOfFirstNonDigitCharacter(value); + long time; + try { + time = Long.parseLong(value.substring(0, idx)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Expected a number."); + } + + if (idx == value.length()) { + // only accept missing unit if the value is 0 + if (time != 0) { + throw new IllegalArgumentException("Unit is required."); + } + return 0L; + } + + String unit = value.substring(idx); + return switch (unit) { + case "ns" -> Duration.ofNanos(time).toNanos(); + case "us" -> Duration.ofNanos(time * 1000).toNanos(); + case "ms" -> Duration.ofMillis(time).toNanos(); + case "s" -> Duration.ofSeconds(time).toNanos(); + case "m" -> Duration.ofMinutes(time).toNanos(); + case "h" -> Duration.ofHours(time).toNanos(); + case "d" -> Duration.ofDays(time).toNanos(); + default -> throw new IllegalArgumentException("Unit is invalid."); + }; + } catch (IllegalArgumentException e) { + throw new JfrArgumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); + } + } + return null; + } + + public static Integer parseInteger(Map args, JfrArgument key) throws JfrArgumentParsingFailed { + String value = args.get(key); + if (value != null) { + try { + return Integer.valueOf(value); + } catch (Throwable e) { + throw new JfrArgumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); + } + } + return null; + } + + public static Long parseMaxSize(Map args, JfrArgument key) throws JfrArgumentParsingFailed { + String value = args.get(key); + if (value == null) { + return null; + } + + try { + int idx = indexOfFirstNonDigitCharacter(value); + long number; + try { + number = Long.parseLong(value.substring(0, idx)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Expected a positive number."); + } + + // Missing unit, number is plain bytes + if (idx == value.length()) { + return number; + } + + final char unit = value.substring(idx).charAt(0); + return switch (unit) { + case 'k', 'K' -> number * 1024; + case 'm', 'M' -> number * 1024 * 1024; + case 'g', 'G' -> number * 1024 * 1024 * 1024; + default -> number; // Unknown unit, number is treated as plain bytes + }; + } catch (IllegalArgumentException e) { + throw new JfrArgumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); + } + } + + private static int indexOfFirstNonDigitCharacter(String durationText) { + int idx = 0; + while (idx < durationText.length() && Character.isDigit(durationText.charAt(idx))) { + idx++; + } + return idx; + } + + private static JfrArgument findArgument(JfrArgument[] possibleArguments, String value) { + for (JfrArgument arg : possibleArguments) { + if (arg.getCmdLineKey().equals(value)) { + return arg; + } + } + return null; + } + + public interface JfrArgument { + String getCmdLineKey(); + } + + /** + * Options available with the JFR.start diagnostic command or when starting JFR upon launching + * the application. + */ + public enum JfrStartArgument implements JfrArgument { + Name("name"), + Settings("settings"), + Delay("delay"), + Duration("duration"), + Filename("filename"), + Disk("disk"), + MaxAge("maxage"), + MaxSize("maxsize"), + DumpOnExit("dumponexit"), + PathToGCRoots("path-to-gc-roots"); + + private final String cmdLineKey; + + JfrStartArgument(String key) { + this.cmdLineKey = key; + } + + @Override + public String getCmdLineKey() { + return cmdLineKey; + } + } + + public enum FlightRecorderOptionsArgument implements JfrArgument { + GlobalBufferSize("globalbuffersize"), + MaxChunkSize("maxchunksize"), + MemorySize("memorysize"), + OldObjectQueueSize("old-object-queue-size"), + RepositoryPath("repository"), + StackDepth("stackdepth"), + ThreadBufferSize("threadbuffersize"), + PreserveRepository("preserve-repository"); + + private final String cmdLineKey; + + FlightRecorderOptionsArgument(String key) { + this.cmdLineKey = key; + } + + @Override + public String getCmdLineKey() { + return cmdLineKey; + } + } + + /** Options available with the JFR.dump diagnostic command. */ + public enum DumpArgument implements JfrArgument { + Begin("begin"), + End("end"), + Filename("filename"), + MaxAge("maxage"), + MaxSize("maxsize"), + Name("name"), + PathToGCRoots("path-to-gc-roots"); + + private final String cmdLineKey; + + DumpArgument(String key) { + this.cmdLineKey = key; + } + + @Override + public String getCmdLineKey() { + return cmdLineKey; + } + } + + /** Options available with the JFR.stop diagnostic command. */ + public enum StopArgument implements JfrArgument { + Filename("filename"), + Name("name"); + + private final String cmdLineKey; + + StopArgument(String key) { + this.cmdLineKey = key; + } + + @Override + public String getCmdLineKey() { + return cmdLineKey; + } + } + + public static class JfrArgumentParsingFailed extends Exception { + @Serial private static final long serialVersionUID = -1050173145647068124L; + + JfrArgumentParsingFailed(String message, Throwable cause) { + super(message, cause); + } + + JfrArgumentParsingFailed(String message) { + super(message); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java index fccc3376c54e..1af2cdd8bc0c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java @@ -32,11 +32,16 @@ import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport; +import com.oracle.svm.core.dcmd.DcmdSupport; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.jdk.RuntimeSupport; +import com.oracle.svm.core.jfr.dcmd.JfrStartDcmd; +import com.oracle.svm.core.jfr.dcmd.JfrStopDcmd; +import com.oracle.svm.core.jfr.dcmd.JfrCheckDcmd; +import com.oracle.svm.core.jfr.dcmd.JfrDumpDcmd; import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.jfr.traceid.JfrTraceIdMap; import com.oracle.svm.core.sampler.SamplerJfrStackTraceSerializer; @@ -192,5 +197,11 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { runtime.addInitializationHook(JfrManager.initializationHook()); runtime.addStartupHook(JfrManager.startupHook()); runtime.addShutdownHook(JfrManager.shutdownHook()); + if (VMInspectionOptions.hasAttachSupport()) { + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new JfrStartDcmd()); + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new JfrStopDcmd()); + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new JfrCheckDcmd()); + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new JfrDumpDcmd()); + } } } 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 045de0e9059d..6c06f187cbfc 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 @@ -1,5 +1,6 @@ /* - * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,7 +27,6 @@ import java.io.FileNotFoundException; import java.io.IOException; -import java.io.Serial; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; @@ -37,19 +37,20 @@ import java.util.HashMap; import java.util.Map; +import com.oracle.svm.core.dcmd.DcmdParseException; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.jdk.RuntimeSupport; +import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgument; +import com.oracle.svm.core.jfr.JfrArgumentParser.JfrStartArgument; +import com.oracle.svm.core.jfr.JfrArgumentParser.FlightRecorderOptionsArgument; import com.oracle.svm.core.jfr.events.EndChunkNativePeriodicEvents; import com.oracle.svm.core.jfr.events.EveryChunkNativePeriodicEvents; -import com.oracle.svm.core.option.RuntimeOptionKey; -import com.oracle.svm.core.util.UserError.UserException; import jdk.graal.compiler.api.replacements.Fold; -import jdk.graal.compiler.core.common.SuppressFBWarnings; import jdk.graal.compiler.serviceprovider.JavaVersionUtil; import jdk.jfr.FlightRecorder; import jdk.jfr.Recording; @@ -63,11 +64,18 @@ import jdk.jfr.internal.SecuritySupport; import jdk.jfr.internal.jfc.JFC; +import static com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgumentParsingFailed; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseBoolean; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseDuration; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseInteger; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseMaxSize; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseSettings; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseJfrOptions; + /** * Called during VM startup and teardown. Also triggers the JFR argument parsing. */ public class JfrManager { - private static final String DEFAULT_JFC_NAME = "default"; @Platforms(Platform.HOSTED_ONLY.class) // final boolean hostedEnabled; @@ -86,7 +94,11 @@ public static RuntimeSupport.Hook initializationHook() { /* Parse arguments early on so that we can tear down the isolate more easily if it fails. */ return isFirstIsolate -> { parseFlightRecorderLogging(); - parseFlightRecorderOptions(); + try { + parseFlightRecorderOptions(); + } catch (JfrArgumentParsingFailed e) { + throw new RuntimeException(e); + } }; } @@ -101,7 +113,7 @@ public static RuntimeSupport.Hook startupHook() { }; } - private static void parseFlightRecorderOptions() { + private static void parseFlightRecorderOptions() throws JfrArgumentParsingFailed { Map optionsArgs = parseJfrOptions(SubstrateOptions.FlightRecorderOptions, FlightRecorderOptionsArgument.values()); Long globalBufferSize = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.GlobalBufferSize); Long maxChunkSize = parseMaxSize(optionsArgs, FlightRecorderOptionsArgument.MaxChunkSize); @@ -128,7 +140,7 @@ private static void parseFlightRecorderOptions() { if (oldObjectQueueSize >= 0) { SubstrateJVM.getOldObjectProfiler().configure(oldObjectQueueSize); } else { - throw argumentParsingFailed(FlightRecorderOptionsArgument.OldObjectQueueSize.getCmdLineKey() + " must be greater or equal 0."); + throw new JfrArgumentParsingFailed(FlightRecorderOptionsArgument.OldObjectQueueSize.getCmdLineKey() + " must be greater or equal 0."); } } @@ -137,7 +149,7 @@ private static void parseFlightRecorderOptions() { SecuritySupport.SafePath repositorySafePath = new SecuritySupport.SafePath(repositoryPath); Repository.getRepository().setBasePath(repositorySafePath); } catch (Throwable e) { - throw argumentParsingFailed("Could not use " + repositoryPath + " as repository. " + e.getMessage(), e); + throw new JfrArgumentParsingFailed("Could not use " + repositoryPath + " as repository. " + e.getMessage(), e); } } @@ -178,7 +190,25 @@ private static void periodicEventSetup() throws SecurityException { } private static void initRecording() { - Map startArgs = parseJfrOptions(SubstrateOptions.StartFlightRecording, JfrStartArgument.values()); + try { + Map startArgs = JfrArgumentParser.parseJfrOptions(SubstrateOptions.StartFlightRecording, JfrStartArgument.values()); + initRecording(startArgs); + } catch (JfrArgumentParsingFailed e) { + throw new RuntimeException(e); + } + } + + public static String initRecording(String[] options) throws DcmdParseException { + try { + Map startArgs = JfrArgumentParser.parseJfrOptions(options, JfrStartArgument.values()); + + return initRecording(startArgs); + } catch (JfrArgumentParsingFailed e) { + throw new DcmdParseException(e.getMessage()); + } + } + + private static String initRecording(Map startArgs) throws JfrArgumentParsingFailed { String name = startArgs.get(JfrStartArgument.Name); String[] settings = parseSettings(startArgs); Long delay = parseDuration(startArgs, JfrStartArgument.Delay); @@ -205,26 +235,26 @@ private static void initRecording() { if (name != null) { try { Integer.parseInt(name); - throw argumentParsingFailed("Name of recording can't be numeric"); + throw new JfrArgumentParsingFailed("Name of recording can't be numeric"); } catch (NumberFormatException nfe) { // ok, can't be mixed up with name } } if (duration == null && Boolean.FALSE.equals(dumpOnExit) && path != null) { - throw argumentParsingFailed("Filename can only be set for a time bound recording or if dumponexit=true. Set duration/dumponexit or omit filename."); + throw new JfrArgumentParsingFailed("Filename can only be set for a time bound recording or if dumponexit=true. Set duration/dumponexit or omit filename."); } if (settings.length == 1 && settings[0].length() == 0) { - throw argumentParsingFailed("No settings specified. Use settings=none to start without any settings"); + throw new JfrArgumentParsingFailed("No settings specified. Use settings=none to start without any settings"); } Map s = new HashMap<>(); for (String configName : settings) { try { s.putAll(JFC.createKnown(configName).getSettings()); } catch (FileNotFoundException e) { - throw argumentParsingFailed("Could not find settings file'" + configName + "'", e); + throw new JfrArgumentParsingFailed("Could not find settings file'" + configName + "'", e); } catch (IOException | ParseException e) { - throw argumentParsingFailed("Could not parse settings file '" + settings[0] + "'", e); + throw new JfrArgumentParsingFailed("Could not parse settings file '" + settings[0] + "'", e); } } @@ -233,14 +263,14 @@ private static void initRecording() { if (duration != null) { if (duration < 1000L * 1000L * 1000L) { // to avoid typo, duration below 1s makes no sense - throw argumentParsingFailed("Could not start recording, duration must be at least 1 second."); + throw new JfrArgumentParsingFailed("Could not start recording, duration must be at least 1 second."); } } if (delay != null) { if (delay < 1000L * 1000L * 1000) { // to avoid typo, delay shorter than 1s makes no sense. - throw argumentParsingFailed("Could not start recording, delay must be at least 1 second."); + throw new JfrArgumentParsingFailed("Could not start recording, delay must be at least 1 second."); } } @@ -323,9 +353,10 @@ private static void initRecording() { msg.append(System.getProperty("line.separator")); } Logger.log(LogTag.JFR_SYSTEM, LogLevel.INFO, msg.toString()); + return recording.getName(); } - private static SecuritySupport.SafePath resolvePath(Recording recording, String filename) throws InvalidPathException { + public static SecuritySupport.SafePath resolvePath(Recording recording, String filename) throws InvalidPathException { if (filename == null) { return makeGenerated(recording, Paths.get(".")); } @@ -359,215 +390,4 @@ private static String getPath(Path path) { return path.toString(); } } - - private static Map parseJfrOptions(RuntimeOptionKey runtimeOptionKey, JfrArgument[] possibleArguments) { - Map optionsMap = new HashMap<>(); - String userInput = runtimeOptionKey.getValue(); - if (!userInput.isEmpty()) { - String[] options = userInput.split(","); - for (String option : options) { - String[] keyVal = option.split("="); - JfrArgument arg = findArgument(possibleArguments, keyVal[0]); - if (arg == null) { - throw argumentParsingFailed("Unknown argument '" + keyVal[0] + "' in " + runtimeOptionKey.getName()); - } - optionsMap.put(arg, keyVal[1]); - } - } - return optionsMap; - } - - private static String[] parseSettings(Map args) throws UserException { - String settings = args.get(JfrStartArgument.Settings); - if (settings == null) { - return new String[]{DEFAULT_JFC_NAME}; - } else if (settings.equals("none")) { - return new String[0]; - } else { - return settings.split(","); - } - } - - @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null allowed as return value") - private static Boolean parseBoolean(Map args, JfrArgument key) throws IllegalArgumentException { - String value = args.get(key); - if (value == null) { - return null; - } else if ("true".equalsIgnoreCase(value)) { - return true; - } else if ("false".equalsIgnoreCase(value)) { - return false; - } else { - throw argumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. Expected a boolean value."); - } - } - - private static Long parseDuration(Map args, JfrStartArgument key) { - String value = args.get(key); - if (value != null) { - try { - int idx = indexOfFirstNonDigitCharacter(value); - long time; - try { - time = Long.parseLong(value.substring(0, idx)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Expected a number."); - } - - if (idx == value.length()) { - // only accept missing unit if the value is 0 - if (time != 0) { - throw new IllegalArgumentException("Unit is required."); - } - return 0L; - } - - String unit = value.substring(idx); - return switch (unit) { - case "ns" -> Duration.ofNanos(time).toNanos(); - case "us" -> Duration.ofNanos(time * 1000).toNanos(); - case "ms" -> Duration.ofMillis(time).toNanos(); - case "s" -> Duration.ofSeconds(time).toNanos(); - case "m" -> Duration.ofMinutes(time).toNanos(); - case "h" -> Duration.ofHours(time).toNanos(); - case "d" -> Duration.ofDays(time).toNanos(); - default -> throw new IllegalArgumentException("Unit is invalid."); - }; - } catch (IllegalArgumentException e) { - throw argumentParsingFailed("Could not parse JFR argument '" + key.cmdLineKey + "=" + value + "'. " + e.getMessage()); - } - } - return null; - } - - private static Integer parseInteger(Map args, JfrArgument key) { - String value = args.get(key); - if (value != null) { - try { - return Integer.valueOf(value); - } catch (Throwable e) { - throw argumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); - } - } - return null; - } - - private static Long parseMaxSize(Map args, JfrArgument key) { - String value = args.get(key); - if (value == null) { - return null; - } - - try { - int idx = indexOfFirstNonDigitCharacter(value); - long number; - try { - number = Long.parseLong(value.substring(0, idx)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Expected a positive number."); - } - - // Missing unit, number is plain bytes - if (idx == value.length()) { - return number; - } - - final char unit = value.substring(idx).charAt(0); - return switch (unit) { - case 'k', 'K' -> number * 1024; - case 'm', 'M' -> number * 1024 * 1024; - case 'g', 'G' -> number * 1024 * 1024 * 1024; - default -> number; // Unknown unit, number is treated as plain bytes - }; - } catch (IllegalArgumentException e) { - throw argumentParsingFailed("Could not parse JFR argument '" + key.getCmdLineKey() + "=" + value + "'. " + e.getMessage()); - } - } - - private static int indexOfFirstNonDigitCharacter(String durationText) { - int idx = 0; - while (idx < durationText.length() && Character.isDigit(durationText.charAt(idx))) { - idx++; - } - return idx; - } - - private static JfrArgument findArgument(JfrArgument[] possibleArguments, String value) { - for (JfrArgument arg : possibleArguments) { - if (arg.getCmdLineKey().equals(value)) { - return arg; - } - } - return null; - } - - private static RuntimeException argumentParsingFailed(String message) { - throw new JfrArgumentParsingFailed(message); - } - - private static RuntimeException argumentParsingFailed(String message, Throwable cause) { - throw new JfrArgumentParsingFailed(message, cause); - } - - private interface JfrArgument { - String getCmdLineKey(); - } - - private enum JfrStartArgument implements JfrArgument { - Name("name"), - Settings("settings"), - Delay("delay"), - Duration("duration"), - Filename("filename"), - Disk("disk"), - MaxAge("maxage"), - MaxSize("maxsize"), - DumpOnExit("dumponexit"), - PathToGCRoots("path-to-gc-roots"); - - private final String cmdLineKey; - - JfrStartArgument(String key) { - this.cmdLineKey = key; - } - - @Override - public String getCmdLineKey() { - return cmdLineKey; - } - } - - private enum FlightRecorderOptionsArgument implements JfrArgument { - GlobalBufferSize("globalbuffersize"), - MaxChunkSize("maxchunksize"), - MemorySize("memorysize"), - OldObjectQueueSize("old-object-queue-size"), - RepositoryPath("repository"), - StackDepth("stackdepth"), - ThreadBufferSize("threadbuffersize"), - PreserveRepository("preserve-repository"); - - private final String cmdLineKey; - - FlightRecorderOptionsArgument(String key) { - this.cmdLineKey = key; - } - - @Override - public String getCmdLineKey() { - return cmdLineKey; - } - } - - private static class JfrArgumentParsingFailed extends RuntimeException { - @Serial private static final long serialVersionUID = -1050173145647068124L; - - JfrArgumentParsingFailed(String message, Throwable cause) { - super(message, cause); - } - - JfrArgumentParsingFailed(String message) { - super(message); - } - } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrCheckDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrCheckDcmd.java new file mode 100644 index 000000000000..5cc9f991231a --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrCheckDcmd.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jfr.dcmd; + +import com.oracle.svm.core.dcmd.AbstractDcmd; +import com.oracle.svm.core.dcmd.DcmdParseException; +import jdk.jfr.FlightRecorder; +import jdk.jfr.Recording; + +import java.util.List; + +public class JfrCheckDcmd extends AbstractDcmd { + + public JfrCheckDcmd() { + this.name = "JFR.check"; + this.description = "Checks running JFR recording(s)"; + this.impact = "low"; + } + + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + if (arguments.length > 1) { + throw new DcmdParseException("Too many arguments specified"); + } + StringBuilder sb = new StringBuilder(); + List recordings = FlightRecorder.getFlightRecorder().getRecordings(); + + if (recordings.isEmpty()) { + return "No recordings."; + } + + for (Recording recording : recordings) { + sb.append("Recording \"").append(recording.getId()).append("\": name=").append(recording.getName()); + sb.append(" maxsize=").append(recording.getMaxSize()).append("B"); + sb.append(" (").append(recording.getState().toString()).append(")\n"); + } + return sb.toString(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrDumpDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrDumpDcmd.java new file mode 100644 index 000000000000..579627480223 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrDumpDcmd.java @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jfr.dcmd; + +import com.oracle.svm.core.dcmd.AbstractDcmd; +import com.oracle.svm.core.dcmd.DcmdOption; +import com.oracle.svm.core.dcmd.DcmdParseException; +import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgument; +import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgumentParsingFailed; +import com.oracle.svm.core.jfr.JfrManager; +import com.oracle.svm.core.util.BasedOnJDKFile; + +import jdk.jfr.FlightRecorder; +import jdk.jfr.Recording; +import jdk.jfr.internal.PlatformRecorder; +import jdk.jfr.internal.PlatformRecording; +import jdk.jfr.internal.SecuritySupport; +import jdk.jfr.internal.WriteableUserPath; +import jdk.jfr.internal.PrivateAccess; + +import java.io.IOException; +import java.nio.file.InvalidPathException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Map; + +import static com.oracle.svm.core.jfr.JfrArgumentParser.DumpArgument; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseBoolean; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseDuration; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseJfrOptions; +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseMaxSize; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class JfrDumpDcmd extends AbstractDcmd { + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L197-L277") // + public JfrDumpDcmd() { + this.options = new DcmdOption[]{ + new DcmdOption("filename", "Name of the file to which the flight recording data is\n" + + " dumped. If no filename is given, a filename is generated from the PID\n" + + " and the current date. The filename may also be a directory in which\n" + + " case, the filename is generated from the PID and the current date in\n" + + " the specified directory.", false, null), + new DcmdOption("name", "Name of the recording. If no name is given, data from all\n" + + " recordings is dumped.", false, null), + new DcmdOption("maxage", "Length of time for dumping the flight recording data to a\n" + + " file. (INTEGER followed by 's' for seconds 'm' for minutes or 'h' for\n" + + " hours)", false, null), + new DcmdOption("maxsize", "Maximum size for the amount of data to dump from a flight\n" + + " recording in bytes if one of the following suffixes is not used:\n" + + " 'm' or 'M' for megabytes OR 'g' or 'G' for gigabytes.", false, null), + new DcmdOption("path-to-gc-roots", " Flag for saving the path to garbage collection (GC) roots\n" + + " at the time the recording data is dumped. The path information is\n" + + " useful for finding memory leaks but collecting it can cause the\n" + + " application to pause for a short period of time. Turn on this flag\n" + + " only when you have an application that you suspect has a memory\n" + + " leak. (BOOLEAN)", false, null), + new DcmdOption("begin", "Specify the time from which recording data will be included\n" + + " in the dump file. The format is specified as local time.\n" + + " (STRING)", false, null), + new DcmdOption("end", "Specify the time to which recording data will be included\n" + + " in the dump file. The format is specified as local time.\n" + + " (STRING)", false, null) + }; + this.examples = new String[]{ + "$ jcmd JFR.dump", + "$ jcmd JFR.dump filename=recording.jfr", + "$ jcmd JFR.dump name=1 filename=/recordings/recording.jfr", + "$ jcmd JFR.dump maxage=1h maxsize=50M", + "$ jcmd JFR.dump begin=-1h", + "$ jcmd JFR.dump begin=-15m end=-5m", + "$ jcmd JFR.dump begin=13:15 end=21:30:00" + }; + this.name = "JFR.dump"; + this.description = "Copies contents of a JFR recording to file. Either the name or the recording id must be specified."; + this.impact = "low"; + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L55-L118") // + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + String recordingName; + String filename; + Long maxAge; + Long maxSize; + Boolean pathToGcRoots; + String begin; + String end; + try { + Map dumpArgs = parseJfrOptions(Arrays.copyOfRange(arguments, 1, arguments.length), DumpArgument.values()); + recordingName = dumpArgs.get(DumpArgument.Name); + filename = dumpArgs.get(DumpArgument.Filename); + maxAge = parseDuration(dumpArgs, DumpArgument.MaxAge); + maxSize = parseMaxSize(dumpArgs, DumpArgument.MaxSize); + pathToGcRoots = parseBoolean(dumpArgs, DumpArgument.PathToGCRoots); + begin = dumpArgs.get(DumpArgument.Begin); + end = dumpArgs.get(DumpArgument.End); + } catch (JfrArgumentParsingFailed e) { + throw new DcmdParseException(e.getMessage()); + } + + if (FlightRecorder.getFlightRecorder().getRecordings().isEmpty()) { + throw new DcmdParseException("No recordings to dump from. Use JFR.start to start a recording."); + } + + if (maxAge != null) { + if (maxAge < 0) { + throw new DcmdParseException("Dump failed, maxage can't be negative."); + } + if (maxAge == 0) { + maxAge = Long.MAX_VALUE / 2; // a high value that won't overflow + } + } + + if (maxSize != null) { + if (maxSize < 0) { + throw new DcmdParseException("Dump failed, maxsize can't be negative."); + } + if (maxSize == 0) { + maxSize = Long.MAX_VALUE / 2; // a high value that won't overflow + } + } + + Instant beginTime = parseTime(begin, "begin"); + Instant endTime = parseTime(end, "end"); + + if (beginTime != null && endTime != null) { + if (endTime.isBefore(beginTime)) { + throw new DcmdParseException("Dump failed, begin must precede end."); + } + } + + Duration duration; + if (maxAge != null) { + duration = Duration.ofNanos(maxAge); + beginTime = Instant.now().minus(duration); + } + + Recording recording = null; + if (recordingName != null) { + for (Recording rec : FlightRecorder.getFlightRecorder().getRecordings()) { + if (rec.getName().equals(recordingName)) { + recording = rec; + break; + } + } + if (recording == null) { + throw new DcmdParseException("Could not find specified recording with name: " + recordingName); + } + } + PlatformRecorder recorder = PrivateAccess.getInstance().getPlatformRecorder(); + try { + synchronized (recorder) { + dump(recorder, recording, filename, maxSize, pathToGcRoots, beginTime, endTime); + } + } catch (IOException | InvalidPathException e) { + throw new DcmdParseException("Dump failed. Could not copy recording data."); + } + return "Dump created."; + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L120-L141") // + /* Mostly identical to Hotspot. */ + private static void dump(PlatformRecorder recorder, Recording recording, String filename, Long maxSize, Boolean pathToGcRoots, Instant beginTime, Instant endTime) + throws DcmdParseException, IOException { + try (PlatformRecording r = newSnapShot(recorder, recording, pathToGcRoots)) { + r.filter(beginTime, endTime, maxSize); + if (r.getChunks().isEmpty()) { + throw new DcmdParseException("Dump failed. No data found in the specified interval."); + } + /* + * If a filename exists, use it. If a filename doesn't exist, use the destination set + * earlier. If destination doesn't exist, generate a filename + */ + WriteableUserPath wup = null; + if (recording != null) { + PlatformRecording pRecording = PrivateAccess.getInstance().getPlatformRecording(recording); + wup = pRecording.getDestination(); + } + if (filename != null || wup == null) { + SecuritySupport.SafePath safe = JfrManager.resolvePath(recording, filename); + wup = new WriteableUserPath(safe.toPath()); + } + r.dumpStopped(wup); + } + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L143-L182") // + /* Mostly identical to Hotspot. */ + private static Instant parseTime(String time, String parameter) throws DcmdParseException { + if (time == null) { + return null; + } + try { + return Instant.parse(time); + } catch (DateTimeParseException dtp) { + // fall through + } + try { + LocalDateTime ldt = LocalDateTime.parse(time); + return ZonedDateTime.of(ldt, ZoneId.systemDefault()).toInstant(); + } catch (DateTimeParseException dtp) { + // fall through + } + try { + LocalTime lt = LocalTime.parse(time); + LocalDate ld = LocalDate.now(); + Instant instant = ZonedDateTime.of(ld, lt, ZoneId.systemDefault()).toInstant(); + Instant now = Instant.now(); + if (instant.isAfter(now) && !instant.isBefore(now.plusSeconds(3600))) { + // User must have meant previous day + ld = ld.minusDays(1); + } + return ZonedDateTime.of(ld, lt, ZoneId.systemDefault()).toInstant(); + } catch (DateTimeParseException dtp) { + // fall through + } + + if (time.startsWith("-")) { + try { + long durationNanos = parseTimespan(time.substring(1)); + Duration duration = Duration.ofNanos(durationNanos); + return Instant.now().minus(duration); + } catch (NumberFormatException nfe) { + // fall through + } + } + throw new DcmdParseException("Dump failed, not a valid time: " + parameter); + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java#L184-L194") // + /* Mostly identical to Hotspot. */ + private static PlatformRecording newSnapShot(PlatformRecorder recorder, Recording recording, Boolean pathToGcRoots) throws IOException { + if (recording == null) { + // Operate on all recordings + PlatformRecording snapshot = recorder.newTemporaryRecording(); + recorder.fillWithRecordedData(snapshot, pathToGcRoots); + return snapshot; + } + + PlatformRecording pr = PrivateAccess.getInstance().getPlatformRecording(recording); + return pr.newSnapshotClone("Dumped by user", pathToGcRoots); + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/util/ValueParser.java#L44-L74") // + /* Mostly identical to Hotspot. */ + private static long parseTimespan(String s) { + if (s.endsWith("ns")) { + return Long.parseLong(s.substring(0, s.length() - 2).trim()); + } + if (s.endsWith("us")) { + return MICROSECONDS.toNanos(Long.parseLong(s.substring(0, s.length() - 2).trim())); + } + if (s.endsWith("ms")) { + return MILLISECONDS.toNanos(Long.parseLong(s.substring(0, s.length() - 2).trim())); + } + if (s.endsWith("s")) { + return SECONDS.toNanos(Long.parseLong(s.substring(0, s.length() - 1).trim())); + } + if (s.endsWith("m")) { + return MINUTES.toNanos(Long.parseLong(s.substring(0, s.length() - 1).trim())); + } + if (s.endsWith("h")) { + return HOURS.toNanos(Long.parseLong(s.substring(0, s.length() - 1).trim())); + } + if (s.endsWith("d")) { + return DAYS.toNanos(Long.parseLong(s.substring(0, s.length() - 1).trim())); + } + + try { + Long.parseLong(s); + } catch (NumberFormatException nfe) { + throw new NumberFormatException("'" + s + "' is not a valid timespan. Should be numeric value followed by a unit, i.e. 20 ms. Valid units are ns, us, s, m, h and d."); + } + // Only accept values with units + throw new NumberFormatException("Timespan + '" + s + "' is missing unit. Valid units are ns, us, s, m, h and d."); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStartDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStartDcmd.java new file mode 100644 index 000000000000..1178ecfcecd5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStartDcmd.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jfr.dcmd; + +import com.oracle.svm.core.dcmd.DcmdOption; +import com.oracle.svm.core.dcmd.AbstractDcmd; +import com.oracle.svm.core.dcmd.DcmdParseException; +import java.util.Arrays; +import com.oracle.svm.core.util.BasedOnJDKFile; + +import com.oracle.svm.core.jfr.JfrManager; + +public class JfrStartDcmd extends AbstractDcmd { + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStart.java#L348-L461") // + public JfrStartDcmd() { + this.options = new DcmdOption[]{ + new DcmdOption("delay", "Length of time to wait before starting to record\n" + + " (INTEGER followed by 's' for seconds 'm' for minutes or h' for\n" + + " hours, 0s)", false, "0s"), + new DcmdOption("disk", "Flag for also writing the data to disk while recording\n" + + " (BOOLEAN)", false, "true"), + new DcmdOption("dumponexit", "Flag for writing the recording to disk when the Java\n" + + " Virtual Machine (JVM) shuts down. If set to 'true' and no value\n" + + " is given for filename, the recording is written to a file in the\n" + + " directory where the process was started. The file name is a\n" + + " system-generated name that contains the process ID, the recording\n" + + " ID and the current time stamp. (For example:\n" + + " id-1-2021_09_14_09_00.jfr) (BOOLEAN)", false, "false"), + new DcmdOption("duration", "Length of time to record. Note that 0s means forever\n" + + " (INTEGER followed by 's' for seconds 'm' for minutes or 'h' for\n" + + " hours)", false, "0s"), + new DcmdOption("filename", "Name of the file to which the flight recording data is\n" + + " written when the recording is stopped. If no filename is given, a\n" + + " filename is generated from the PID and the current date and is\n" + + " placed in the directory where the process was started. The\n" + + " filename may also be a directory in which case, the filename is\n" + + " generated from the PID and the current date in the specified\n" + + " directory.", false, null), + new DcmdOption("maxage", "Maximum time to keep the recorded data on disk. This\n" + + " parameter is valid only when the disk parameter is set to true.\n" + + " Note 0s means forever. (INTEGER followed by 's' for seconds 'm'\n" + + " for minutes or 'h' for hours, 0s)", false, "No max age."), + new DcmdOption("maxsize", "Maximum size of the data to keep on disk in bytes if\n" + + " one of the following suffixes is not used: 'm' or 'M' for\n" + + " megabytes OR 'g' or 'G' for gigabytes. This parameter is valid\n" + + " only when the disk parameter is set to 'true'. The value must not\n" + + " be less than the value for the maxchunksize parameter set with\n" + + " the JFR.configure command.", false, "No max size"), + new DcmdOption("name", "Name of the recording. If no name is provided, a name\n" + + " is generated. Make note of the generated name that is shown in\n" + + " the response to the command so that you can use it with other\n" + + " commands.", false, "System-generated default name"), + new DcmdOption("path-to-gc-root", "Flag for saving the path to garbage collection (GC)\n" + + " roots at the end of a recording. The path information is useful\n" + + " for finding memory leaks but collecting it is time consuming.\n" + + " Turn on this flag only when you have an application that you\n" + + " suspect has a memory leak. If the settings parameter is set to\n" + + " 'profile', then the information collected includes the stack\n" + + " trace from where the potential leaking object wasallocated. (BOOLEAN)", false, "false"), + new DcmdOption("settings", " Name of the settings file that identifies which events\n" + + " to record. To specify more than one file, use the settings\n" + + " parameter repeatedly. Include the path if the file is not in\n" + + " JAVA-HOME/lib/jfr. The following profiles are included with the\n" + + " JDK in the JAVA-HOME/lib/jfr directory: 'default.jfc': collects a\n" + + " predefined set of information with low overhead, so it has minimal\n" + + " impact on performance and can be used with recordings that run\n" + + " continuously; 'profile.jfc': Provides more data than the\n" + + " 'default.jfc' profile, but with more overhead and impact on\n" + + " performance. Use this configuration for short periods of time\n" + + " when more information is needed. Use none to start a recording\n" + + " without a predefined configuration file. (STRING)", false, "JAVA-HOME/lib/jfr/default.jfc") + }; + this.examples = new String[]{ + "$ jcmd JFR.start", + "$ jcmd JFR.start filename=dump.jfr", + "$ jcmd JFR.start filename=/directory/recordings", + "$ jcmd JFR.start maxage=1h maxsize=1000M", + "$ jcmd JFR.start delay=5m settings=my.jfc" + }; + this.name = "JFR.start"; + this.description = "Starts a new JFR recording."; + this.impact = "medium"; + } + + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + String recordingName = JfrManager.initRecording(Arrays.copyOfRange(arguments, 1, arguments.length)); + return "Started recording " + recordingName + "\n"; + } + + @Override + public String getName() { + return "JFR.start"; + } + + @Override + public String getImpact() { + return "Medium"; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStopDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStopDcmd.java new file mode 100644 index 000000000000..1098bebd5a0a --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/dcmd/JfrStopDcmd.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jfr.dcmd; + +import com.oracle.svm.core.dcmd.DcmdOption; +import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgument; +import com.oracle.svm.core.jfr.JfrArgumentParser.JfrArgumentParsingFailed; +import com.oracle.svm.core.dcmd.AbstractDcmd; +import com.oracle.svm.core.dcmd.DcmdParseException; +import com.oracle.svm.core.util.BasedOnJDKFile; +import jdk.jfr.FlightRecorder; +import jdk.jfr.Recording; +import jdk.jfr.internal.SecuritySupport; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +import static com.oracle.svm.core.jfr.JfrArgumentParser.parseJfrOptions; +import static com.oracle.svm.core.jfr.JfrArgumentParser.StopArgument; +import static com.oracle.svm.core.jfr.JfrManager.resolvePath; + +public class JfrStopDcmd extends AbstractDcmd { + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStop.java#L74-L100") // + public JfrStopDcmd() { + this.options = new DcmdOption[]{ + new DcmdOption("filename", "Name of the file to which the recording is written when the\n" + + " recording is stopped. If no path is provided here or when the recording was started,\n" + + " the data from the recording is discarded.", false, null), + new DcmdOption("name", "Name of the recording to stop.", true, null) + }; + this.examples = new String[]{ + "$ jcmd JFR.stop name=1", + "$ jcmd JFR.stop name=benchmark filename=/directory/recordings", + "$ jcmd JFR.stop name=5 filename=recording.jfr" + }; + this.name = "JFR.stop"; + this.description = "Stops a JFR recording."; + this.impact = "low"; + } + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+2/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStop.java#L44-L71") // + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + String response; + if (arguments.length > 3) { + throw new DcmdParseException("Too many arguments specified"); + } + + try { + Map stopArgs = parseJfrOptions(Arrays.copyOfRange(arguments, 1, arguments.length), StopArgument.values()); + String recordingName = stopArgs.get(StopArgument.Name); + String filename = stopArgs.get(StopArgument.Filename); + Recording target = null; + + if (recordingName == null) { + throw new DcmdParseException("The name of the recording to stop is required but was not specified."); + } + + for (Recording recording : FlightRecorder.getFlightRecorder().getRecordings()) { + if (recording.getName().equals(recordingName)) { + target = recording; + break; + } + } + if (target == null) { + throw new DcmdParseException("Could not find specified recording with name: " + recordingName); + } + + if (filename != null) { + SecuritySupport.SafePath safePath = resolvePath(target, filename); + target.setDestination(safePath.toPath()); + } + + target.stop(); + target.close(); + response = "Stopped recording: " + target.getName(); + + } catch (JfrArgumentParsingFailed e) { + throw new DcmdParseException("Invalid arguments provided: " + e.getMessage()); + } catch (NumberFormatException e) { + throw new DcmdParseException("Invalid recording name specified: " + e.getMessage()); + } catch (IOException e) { + throw new DcmdParseException("Invalid dump path specified: " + e.getMessage()); + } + return response + "\n"; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jvmstat/SystemCounters.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jvmstat/SystemCounters.java index 66ffbf021f4c..80293bc533aa 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jvmstat/SystemCounters.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jvmstat/SystemCounters.java @@ -37,6 +37,7 @@ import com.oracle.svm.core.JavaMainWrapper; import com.oracle.svm.core.heap.Heap; +import com.oracle.svm.core.util.BasedOnJDKFile; import com.sun.management.OperatingSystemMXBean; /** @@ -44,6 +45,8 @@ * are specified at image build time). */ class SystemCounters implements PerfDataHolder, VMOperationListener { + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/master/src/hotspot/share/services/runtimeService.cpp#L72") // + private static final String ATTACH_SUPPORTED = "1"; // Constants. private final PerfLongConstant initDoneTime; private final PerfStringConstant javaCommand; @@ -52,6 +55,7 @@ class SystemCounters implements PerfDataHolder, VMOperationListener { private final PerfLongConstant frequency; private final PerfLongConstant loadedClasses; private final PerfLongConstant processors; + private final PerfStringConstant jvmCapabilities; // Exported system properties. private final PerfStringConstant tempDir; @@ -98,6 +102,7 @@ class SystemCounters implements PerfDataHolder, VMOperationListener { osName = perfManager.createStringConstant("java.property.os.name"); userDir = perfManager.createStringConstant("java.property.user.dir"); userName = perfManager.createStringConstant("java.property.user.name"); + jvmCapabilities = perfManager.createStringConstant("sun.rt.jvmCapabilities"); gcInProgress = perfManager.createLongVariable("com.oracle.svm.gcInProgress", PerfUnit.NONE); @@ -132,6 +137,7 @@ public void allocate() { osName.allocate(getSystemProperty("os.name")); userDir.allocate(getSystemProperty("user.dir")); userName.allocate(getSystemProperty("user.name")); + jvmCapabilities.allocate(ATTACH_SUPPORTED); gcInProgress.allocate(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java index 149d897ede1a..7a0a26f84235 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NativeMemoryTracking.java @@ -38,10 +38,10 @@ import org.graalvm.word.WordFactory; import com.oracle.svm.core.Uninterruptible; -import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.jdk.RuntimeSupport; import com.oracle.svm.core.memory.NativeMemory; import com.oracle.svm.core.util.UnsignedUtils; +import com.oracle.svm.core.VMInspectionOptions; import jdk.graal.compiler.api.replacements.Fold; @@ -201,24 +201,30 @@ public static RuntimeSupport.Hook shutdownHook() { return isFirstIsolate -> NativeMemoryTracking.singleton().printStatistics(); } - private void printStatistics() { + public void printStatistics() { if (VMInspectionOptions.PrintNMTStatistics.getValue()) { - System.out.println(); - System.out.println("Native memory tracking"); - System.out.println(" Peak total used memory: " + getPeakTotalUsedMemory() + " bytes"); - System.out.println(" Total alive allocations at peak usage: " + getCountAtTotalPeakUsage()); - System.out.println(" Total used memory: " + getTotalUsedMemory() + " bytes"); - System.out.println(" Total alive allocations: " + getTotalCount()); - - for (int i = 0; i < NmtCategory.values().length; i++) { - String name = NmtCategory.values()[i].getName(); - NmtMallocMemoryInfo info = getInfo(i); - - System.out.println(" " + name + " peak used memory: " + info.getPeakUsed() + " bytes"); - System.out.println(" " + name + " alive allocations at peak: " + info.getCountAtPeakUsage()); - System.out.println(" " + name + " currently used memory: " + info.getUsed() + " bytes"); - System.out.println(" " + name + " currently alive allocations: " + info.getCount()); - } + System.out.println(generateReportString()); + } + } + + public String generateReportString() { + StringBuilder stringBuilder = new StringBuilder(2500); + stringBuilder.append("\n"); + stringBuilder.append("Native memory tracking\n"); + stringBuilder.append(" Peak total used memory: " + getPeakTotalUsedMemory() + " bytes\n"); + stringBuilder.append(" Total alive allocations at peak usage: " + getCountAtTotalPeakUsage() + "\n"); + stringBuilder.append(" Total used memory: " + getTotalUsedMemory() + " bytes\n"); + stringBuilder.append(" Total alive allocations: " + getTotalCount() + "\n"); + + for (int i = 0; i < NmtCategory.values().length; i++) { + String name = NmtCategory.values()[i].getName(); + NmtMallocMemoryInfo info = getInfo(i); + + stringBuilder.append(" " + name + " peak used memory: " + info.getPeakUsed() + " bytes\n"); + stringBuilder.append(" " + name + " alive allocations at peak: " + info.getCountAtPeakUsage() + "\n"); + stringBuilder.append(" " + name + " currently used memory: " + info.getUsed() + " bytes\n"); + stringBuilder.append(" " + name + " currently alive allocations: " + info.getCount() + "\n"); } + return stringBuilder.toString(); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtDcmd.java new file mode 100644 index 000000000000..ce7854c9c43b --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtDcmd.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.nmt; + +import com.oracle.svm.core.dcmd.AbstractDcmd; +import com.oracle.svm.core.dcmd.DcmdOption; +import com.oracle.svm.core.dcmd.DcmdParseException; +import com.oracle.svm.core.util.BasedOnJDKFile; + +public class NmtDcmd extends AbstractDcmd { + + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24%2B2/src/hotspot/share/nmt/nmtDCmd.cpp#L34-L64") // + public NmtDcmd() { + this.options = new DcmdOption[]{new DcmdOption("summary", + "request runtime to report current memory summary," + " which includes total reserved and committed memory," + " along with memory usage summary by each subsystem. BOOLEAN.", + false, "false")}; + this.examples = new String[]{"$ jcmd VM.native_memory summary"}; + this.name = "VM.native_memory"; + this.description = "Print native memory usage"; + this.impact = "low"; + } + + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + if (arguments.length != 2) { + throw new DcmdParseException("Exactly 1 argument should be specified. See $ jcmd help VM.native_memory"); + } + if (arguments[1].equals("detailed")) { + return "Detailed NMT mode is not supported yet."; + } else if (arguments[1].equals("summary")) { + return NativeMemoryTracking.singleton().generateReportString(); + } + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtFeature.java index 9b7786681fe4..c33c8076db83 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/nmt/NmtFeature.java @@ -28,6 +28,7 @@ import org.graalvm.nativeimage.ImageSingletons; +import com.oracle.svm.core.dcmd.DcmdSupport; import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; @@ -48,5 +49,8 @@ public void afterRegistration(AfterRegistrationAccess access) { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { RuntimeSupport.getRuntimeSupport().addShutdownHook(NativeMemoryTracking.shutdownHook()); + if (VMInspectionOptions.hasAttachSupport()) { + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new NmtDcmd()); + } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpStacksDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpStacksDcmd.java new file mode 100644 index 000000000000..c564c7ccb20c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpStacksDcmd.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.thread; + +import com.oracle.svm.core.DumpThreadStacksSupport; +import com.oracle.svm.core.dcmd.AbstractDcmd; + +import com.oracle.svm.core.dcmd.DcmdParseException; + +public class ThreadDumpStacksDcmd extends AbstractDcmd { + + public ThreadDumpStacksDcmd() { + this.name = "Thread.dump_stacks"; + this.description = "Dumps stacks of platform threads to log output."; + this.impact = "Medium"; + } + + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + if (arguments.length > 1) { + throw new DcmdParseException("Too many arguments specified"); + } + DumpThreadStacksSupport.dump(); + return "Threads dumped."; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpToFileDcmd.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpToFileDcmd.java new file mode 100644 index 000000000000..679f40ad39a4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadDumpToFileDcmd.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.thread; + +import com.oracle.svm.core.dcmd.AbstractDcmd; +import com.oracle.svm.core.dcmd.DcmdParseException; +import com.oracle.svm.core.dcmd.DcmdOption; +import jdk.internal.vm.ThreadDumper; + +public class ThreadDumpToFileDcmd extends AbstractDcmd { + + public ThreadDumpToFileDcmd() { + this.options = new DcmdOption[]{ + new DcmdOption("filepath", "The file path to the output file. (STRING)", true, null), + new DcmdOption("overwrite", "May overwrite existing file. (BOOLEAN)", false, "false"), + new DcmdOption("format", "Output format (\"plain\" or \"json\") (STRING)", false, "plain") + + }; + this.name = "Thread.dump_to_file"; + this.description = "Dumps thread stacks (including virtual) to a specified file."; + this.impact = "High"; + this.examples = new String[]{ + "$ jcmd Thread.dump_to_file filepath=/some/path/my_file.txt", + "$ jcmd Thread.dump_to_file format=json overwrite=true filepath=/some/path/my_file.json"}; + } + + @Override + public String parseAndExecute(String[] arguments) throws DcmdParseException { + + String pathString = null; + boolean overwrite = false; + boolean useJson = false; + for (String argument : arguments) { + if (argument.contains("filepath=")) { + pathString = extractArgument(argument); + } else if (argument.contains("overwrite=")) { + overwrite = Boolean.parseBoolean(extractArgument(argument)); + } else if (argument.contains("format=")) { + String result = extractArgument(argument); + if (result.equals("json")) { + useJson = true; + } else if (!result.equals("plain")) { + throw new DcmdParseException("Format must be either json or plain, but provided: " + result); + } + } + } + + if (pathString == null) { + return "The argument 'filepath' is mandatory."; + } + + if (useJson) { + ThreadDumper.dumpThreadsToJson(pathString, overwrite); + } else { + ThreadDumper.dumpThreads(pathString, overwrite); + } + return "Created " + pathString; + } + + private static String extractArgument(String input) throws DcmdParseException { + String[] pathArgumentSplit = input.split("="); + if (pathArgumentSplit.length != 2) { + throw new DcmdParseException("Invalid command structure."); + } + return pathArgumentSplit[1]; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpFeature.java index c572af6f462a..bb7ebeb4bb10 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpFeature.java @@ -36,10 +36,12 @@ import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.impl.HeapDumpSupport; +import com.oracle.svm.core.dcmd.DcmdSupport; import com.oracle.svm.core.VMInspectionOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.heap.dump.HProfType; +import com.oracle.svm.core.heap.dump.HeapDumpDcmd; import com.oracle.svm.core.heap.dump.HeapDumpMetadata; import com.oracle.svm.core.heap.dump.HeapDumpShutdownHook; import com.oracle.svm.core.heap.dump.HeapDumpStartupHook; @@ -92,6 +94,9 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { if (VMInspectionOptions.hasHeapDumpSupport()) { RuntimeSupport.getRuntimeSupport().addStartupHook(new HeapDumpStartupHook()); RuntimeSupport.getRuntimeSupport().addShutdownHook(new HeapDumpShutdownHook()); + if (VMInspectionOptions.hasAttachSupport()) { + ImageSingletons.lookup(DcmdSupport.class).registerDcmd(new HeapDumpDcmd()); + } } } diff --git a/substratevm/src/com.oracle.svm.test/src/META-INF/native-image/com.oracle.svm.test/native-image.properties b/substratevm/src/com.oracle.svm.test/src/META-INF/native-image/com.oracle.svm.test/native-image.properties index e1470477f070..83e390273833 100644 --- a/substratevm/src/com.oracle.svm.test/src/META-INF/native-image/com.oracle.svm.test/native-image.properties +++ b/substratevm/src/com.oracle.svm.test/src/META-INF/native-image/com.oracle.svm.test/native-image.properties @@ -8,5 +8,5 @@ Args= \ --features=com.oracle.svm.test.jfr.JfrTestFeature \ --add-opens=java.base/java.lang=ALL-UNNAMED \ --add-exports=org.graalvm.nativeimage.base/com.oracle.svm.util=ALL-UNNAMED \ - --enable-monitoring=jvmstat,jfr,jmxserver,jmxclient,nmt \ + --enable-monitoring=all \ -J--enable-preview \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/attach/AttachTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/attach/AttachTest.java new file mode 100644 index 000000000000..b6f9e21d11ce --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/attach/AttachTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.test.attach; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.oracle.svm.core.attach.AttachApiSupport; +import com.oracle.svm.core.dcmd.HelpDcmd; +import com.oracle.svm.core.nmt.NmtDcmd; +import com.oracle.svm.core.jfr.dcmd.JfrCheckDcmd; +import com.oracle.svm.core.jfr.dcmd.JfrStopDcmd; +import com.oracle.svm.core.jfr.dcmd.JfrStartDcmd; +import com.oracle.svm.core.jfr.dcmd.JfrDumpDcmd; +import com.oracle.svm.core.heap.dump.HeapDumpDcmd; +import com.oracle.svm.core.thread.ThreadDumpStacksDcmd; + +import org.junit.Test; +import org.graalvm.nativeimage.ImageSingletons; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +public class AttachTest { + /** + * This test ensure attaching to the VM works and that all known DCMDs get registered correctly. + */ + @Test + public void testAttachAndDcmdRegistration() throws IOException, InterruptedException { + long pid = ProcessHandle.current().pid(); + List command = new ArrayList<>(); + command.add(System.getenv("JAVA_HOME") + "/bin/jcmd"); + command.add(String.valueOf(pid)); + command.add("help"); + // If the process crashes, this test will fail, not hang. + ProcessBuilder pb = new ProcessBuilder(command); + Process jcmdProc = pb.start(); + + InputStream stdout = jcmdProc.getInputStream(); + BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdout)); + + List expectedStrings = new ArrayList<>(List.of( + new HelpDcmd().getName(), + new NmtDcmd().getName(), + new JfrCheckDcmd().getName(), + new JfrStopDcmd().getName(), + new JfrDumpDcmd().getName(), + new JfrStartDcmd().getName(), + new HeapDumpDcmd().getName(), + new ThreadDumpStacksDcmd().getName())); + + String line; + while ((line = stdoutReader.readLine()) != null) { + expectedStrings.remove(line); + } + assertTrue("Not all DCMDs were registered correctly. ", expectedStrings.isEmpty()); + + int exitCode = jcmdProc.waitFor(); + assertEquals(0, exitCode); + } + + /** This test verifies an edge case. It checks the teardown/restart process. */ + @Test + public void testBadSocketFile() throws IOException, InterruptedException { + checkJcmd(); + // Abruptly delete socket file. + String tempDir = System.getProperty("java.io.tmpdir"); + File attachFile = new File(tempDir + "/.java_pid" + ProcessHandle.current().pid()); + boolean deletedSocketFile = Files.deleteIfExists(attachFile.toPath()); + + assertTrue(deletedSocketFile); + + // Issue command again and verify response is still correct. + checkJcmd(); + } + + @Test + public void testConcurrentAttach() throws InterruptedException { + int threadCount = 100; + Thread[] threads = new Thread[threadCount]; + + ImageSingletons.lookup(AttachApiSupport.class).teardown(); + + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(() -> { + try { + checkJcmd(); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + }); + threads[i].start(); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + } + + /** This is a helper method that uses JCMD to attach to the VM and request help info. */ + static void checkJcmd() throws IOException, InterruptedException { + long pid = ProcessHandle.current().pid(); + List command = new ArrayList<>(); + command.add(System.getenv("JAVA_HOME") + "/bin/jcmd"); + command.add(String.valueOf(pid)); + command.add("help"); + ProcessBuilder pb = new ProcessBuilder(command); + Process jcmdProc; + try { + jcmdProc = pb.start(); + } catch (IOException e) { + // Couldn't start process. + return; + } + + InputStream stdout = jcmdProc.getInputStream(); + BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdout)); + + boolean foundExpectedResponse = false; + String line; + while (true) { + line = stdoutReader.readLine(); + if (line == null) { + break; + } else if (line.contains("help")) { + foundExpectedResponse = true; + } + } + assertTrue(foundExpectedResponse); + + int exitCode = jcmdProc.waitFor(); + assertEquals(0, exitCode); + } +}