[patch] JFR profiler
Jason Zaugg
jzaugg at gmail.com
Thu Aug 6 03:47:18 UTC 2020
I'd like to contribute a profiler that collects Java Flight Recorder profiles.
I use jcmd to control the profiler. JMX would be conceptually cleaner
but jcmd is the most stable API across JDK 8-14.
Unlike -prof async, I don't warmup the profiler itself by letting it run during
the warmup phase and discarding the samples. The details of how to do
this via jcmd are JVM version dependent and it doesn't seem worth dealing
with that.
The user must specify -jvmArgs -XX:+UnlockCommercialFeatures if using
older versions of Oracle JDK.
My current integration in sbt-jmh drives external tools
(jfr-flamegraph, Flamegraph.pl)
to generate flamegraphs. To keep this version minimal, I've instead added a
post processing hook and left these details to the user. Recent versions of
JDK Mission Control have built-in support for flamegraphs so generating them
as SVGs is needed less often.
I've manually tested this with Oracle JDK8 and OpenJDK 1.14.0-1.
-jason
# HG changeset patch
# User Jason Zaugg <jzaugg at gmail.com>
# Date 1596683672 -36000
# Thu Aug 06 13:14:32 2020 +1000
# Branch jfr-profiler
# Node ID 75a30f19f6dcd1b8a461388a47587614831ec067
# Parent dab3a1912d28d8b8f6798a68b9f34789e3b002d6
Add a profiler for JFR
JFR profiling is enabled during measurement iterations
and the .jfr file is dumped to a per-trial output
directory.
A hook is provided to run a post-processing step. This could
be used to generate flamegraphs using jfr-flame-graph.
diff -r dab3a1912d28 -r 75a30f19f6dc
jmh-core/src/main/java/org/openjdk/jmh/profile/JavaFlightRecorderProfiler.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jmh-core/src/main/java/org/openjdk/jmh/profile/JavaFlightRecorderProfiler.java
Thu Aug 06 13:14:32 2020 +1000
@@ -0,0 +1,260 @@
+/*
+ * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * 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 org.openjdk.jmh.profile;
+
+import joptsimple.OptionException;
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+import org.openjdk.jmh.infra.BenchmarkParams;
+import org.openjdk.jmh.infra.IterationParams;
+import org.openjdk.jmh.results.BenchmarkResult;
+import org.openjdk.jmh.results.IterationResult;
+import org.openjdk.jmh.results.Result;
+import org.openjdk.jmh.results.TextResult;
+import org.openjdk.jmh.runner.IterationType;
+import org.openjdk.jmh.util.Utils;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.ServiceLoader;
+
+/**
+ * A profiler based on Java Flight Recorder.
+ *
+ * @author Jason Zaugg
+ */
+public final class JavaFlightRecorderProfiler implements
ExternalProfiler, InternalProfiler {
+
+ private final boolean verbose;
+ private final File outDir;
+ private final boolean debugNonSafePoints;
+ private final String configName;
+ private final Collection<String> flightRecorderOptions = new ArrayList<>();
+ private PostProcessor postProcessor = null;
+
+ private boolean measurementStarted = false;
+ private int measurementIterationCount;
+ private String profileName;
+ private final List<File> generated = new ArrayList<>();
+
+ public JavaFlightRecorderProfiler(String initLine) throws
ProfilerException {
+ OptionParser parser = new OptionParser();
+
+ parser.formatHelpWith(new ProfilerOptionFormatter("jfr"));
+
+ OptionSpec<String> optDir = parser.accepts("dir",
+ "Output directory.")
+ .withRequiredArg().ofType(String.class).describedAs("dir");
+
+ OptionSpec<String> optConfig = parser.accepts("configName",
+ "Name of a predefined Flight Recorder configuration, e.g.
profile or default")
+ .withRequiredArg().ofType(String.class).describedAs("name").defaultsTo("profile");
+
+ OptionSpec<Boolean> optDebugNonSafePoints =
parser.accepts("debugNonSafePoints",
+ "Gather cpu samples asynchronously, rather than at the
subsequent safepoint.")
+ .withRequiredArg().ofType(Boolean.class).describedAs("bool").defaultsTo(true);
+
+ OptionSpec<Integer> optStackDepth = parser.accepts("stackDepth",
+ "Maximum number of stack frames collected for each event.")
+ .withRequiredArg().ofType(Integer.class).describedAs("frames");
+
+ OptionSpec<String> optPostProcessor = parser.accepts("postProcessor",
+ "The fully qualified name of a class that implements " +
PostProcessor.class +
+ ". This must have a public, no-argument constructor.")
+ .withRequiredArg().ofType(String.class).describedAs("fqcn");
+
+ OptionSpec<Boolean> optVerbose = parser.accepts("verbose",
+ "Output the sequence of commands")
+ .withRequiredArg().ofType(Boolean.class).defaultsTo(false).describedAs("bool");
+
+ OptionSet set = ProfilerUtils.parseInitLine(initLine, parser);
+
+ try {
+ this.debugNonSafePoints = optDebugNonSafePoints.value(set);
+ this.configName = optConfig.value(set);
+ if (set.has(optStackDepth)) {
+ flightRecorderOptions.add("stackdepth=" +
optStackDepth.value(set));
+ }
+ verbose = optVerbose.value(set);
+ if (!set.has(optDir)) {
+ outDir = new File(System.getProperty("user.dir"));
+ } else {
+ outDir = new File(set.valueOf(optDir));
+ }
+ if (set.has(optPostProcessor)) {
+ try {
+ ClassLoader loader =
Thread.currentThread().getContextClassLoader();
+ Class<?> postProcessorClass =
loader.loadClass(optPostProcessor.value(set));
+ postProcessor = (PostProcessor)
postProcessorClass.getDeclaredConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new ProfilerException(e);
+ }
+ }
+ } catch (OptionException e) {
+ throw new ProfilerException(e.getMessage());
+ }
+ }
+
+ @Override
+ public void beforeIteration(BenchmarkParams benchmarkParams,
IterationParams iterationParams) {
+ if (iterationParams.getType() == IterationType.MEASUREMENT) {
+ if (!measurementStarted) {
+ profileName = benchmarkParams.id();
+ execute(benchmarkParams.getJvm(), "JFR.start",
Collections.singletonList("settings=" + configName));
+ measurementStarted = true;
+ }
+ }
+ }
+
+ @Override
+ public Collection<? extends Result>
afterIteration(BenchmarkParams benchmarkParams, IterationParams
iterationParams,
+
IterationResult iterationResult) {
+ if (iterationParams.getType() == IterationType.MEASUREMENT) {
+ measurementIterationCount += 1;
+ if (measurementIterationCount == iterationParams.getCount()) {
+ File trialOutDir = createTrialOutDir(benchmarkParams);
+ File jfrFile = new File(trialOutDir, "profile.jfr");
+ String filenameOption = "filename=" +
jfrFile.getAbsolutePath();
+ execute(benchmarkParams.getJvm(), "JFR.stop",
Collections.singletonList(filenameOption));
+ generated.add(jfrFile);
+ if (postProcessor != null) {
+
generated.addAll(postProcessor.postProcess(benchmarkParams, jfrFile));
+ }
+ return Collections.singletonList(result());
+ }
+ }
+
+ return Collections.emptyList();
+ }
+
+ private TextResult result() {
+ StringWriter output = new StringWriter();
+ PrintWriter pw = new PrintWriter(output);
+ pw.println("JFR profiler results:");
+ for (File file : generated) {
+ pw.print(" ");
+ pw.println(file.getPath());
+ }
+ pw.flush();
+ pw.close();
+ return new TextResult(output.toString(), "jfr");
+ }
+
+ private File createTrialOutDir(BenchmarkParams benchmarkParams) {
+ String fileName = benchmarkParams.id();
+ File trialOutDir = new File(this.outDir, fileName);
+ trialOutDir.mkdirs();
+ return trialOutDir;
+ }
+
+ private void execute(String jvm, String cmd, Collection<String> options) {
+ long pid = Utils.getPid();
+ ArrayList<String> fullCommand = new ArrayList<>();
+ fullCommand.add(findJcmd(jvm).getAbsolutePath());
+ fullCommand.add(String.valueOf(pid));
+ fullCommand.add(cmd);
+ fullCommand.add("name=" + profileName);
+ fullCommand.addAll(options);
+ if (verbose) {
+ System.out.println("[jfr] " + fullCommand);
+ }
+ // TODO Use JMX to control FlightRecorder when the baseline
of JDK support in JMH
+ // advances to a version with that included.
+ Collection<String> errorOutput =
Utils.tryWith(fullCommand.toArray(new String[0]));
+ if (!errorOutput.isEmpty()) {
+ throw new RuntimeException("Error executing: " +
fullCommand + System.lineSeparator() +
+ Utils.join(errorOutput, System.lineSeparator()));
+ }
+ }
+ private File findJcmd(String jvm) {
+ File jcmd;
+ File firstTry = new File(new File(jvm).getParent(), "jcmd");
+ if (firstTry.exists()) {
+ jcmd = firstTry;
+ } else {
+ jcmd = new File(new File(jvm.replace("jre/bin/java",
"bin/jcmd")).getParent(), "jcmd");
+ }
+ return jcmd;
+ }
+
+ @Override
+ public Collection<String> addJVMInvokeOptions(BenchmarkParams params) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Collection<String> addJVMOptions(BenchmarkParams params) {
+ List<String> args = new ArrayList<>();
+ if (debugNonSafePoints) {
+ args.add("-XX:+UnlockDiagnosticVMOptions");
+ args.add("-XX:+DebugNonSafepoints");
+ }
+
+ if (!flightRecorderOptions.isEmpty()) {
+ args.add("-XX:FlightRecorderOptions=" +
Utils.join(flightRecorderOptions, ","));
+ }
+
+ // Unnecessary / deprecated for removal on JDKs 13.
+ // TODO Could make this conditional on
majorVersion(params.getJdkVersion()) < 13
+ args.add("-XX:+IgnoreUnrecognizedVMOptions");
+ args.add("-XX:+FlightRecorder");
+
+ return args;
+ }
+
+ @Override
+ public void beforeTrial(BenchmarkParams benchmarkParams) {
+ }
+
+ @Override
+ public Collection<? extends Result> afterTrial(BenchmarkResult
br, long pid, File stdOut, File stdErr) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean allowPrintOut() {
+ return true;
+ }
+
+ @Override
+ public boolean allowPrintErr() {
+ return true;
+ }
+
+ @Override
+ public String getDescription() {
+ return "JFR profiler provider.";
+ }
+
+ public interface PostProcessor {
+ List<File> postProcess(BenchmarkParams benchmarkParams, File jfrFile);
+ }
+}
diff -r dab3a1912d28 -r 75a30f19f6dc
jmh-core/src/main/java/org/openjdk/jmh/profile/ProfilerFactory.java
--- a/jmh-core/src/main/java/org/openjdk/jmh/profile/ProfilerFactory.java
Wed Aug 05 09:25:40 2020 +0200
+++ b/jmh-core/src/main/java/org/openjdk/jmh/profile/ProfilerFactory.java
Thu Aug 06 13:14:32 2020 +1000
@@ -173,6 +173,7 @@
BUILT_IN.put("hs_gc", HotspotMemoryProfiler.class);
BUILT_IN.put("hs_rt", HotspotRuntimeProfiler.class);
BUILT_IN.put("hs_thr", HotspotThreadProfiler.class);
+ BUILT_IN.put("jfr", JavaFlightRecorderProfiler.class);
BUILT_IN.put("stack", StackProfiler.class);
BUILT_IN.put("perf", LinuxPerfProfiler.class);
BUILT_IN.put("perfnorm", LinuxPerfNormProfiler.class);
More information about the jmh-dev
mailing list