Skip to content

Commit 86aa5af

Browse files
authored
Merge pull request #106 from retronym/topic/jmh-snapshot
Use JMH's first party support for JFR Profiler
2 parents bd23f3a + cc23b78 commit 86aa5af

File tree

4 files changed

+128
-5
lines changed

4 files changed

+128
-5
lines changed

build.sbt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,15 @@ addCommandAlias("cold", "compilation/jmh:run ColdScalacBenchmark -foe true")
9696

9797
commands ++= build.Profiler.commands
9898

99+
// duplicated in project/build.sbt
100+
val jmhV = "1.25"
101+
99102
def addJmh(project: Project): Project = {
100103
// IntelliJ SBT project import doesn't like sbt-jmh's default setup, which results the prod and test
101104
// output paths overlapping. This is because sbt-jmh declares the `jmh` config as extending `test`, but
102105
// configures `classDirectory in Jmh := classDirectory in Compile`.
103106
project.enablePlugins(JmhPlugin).overrideConfigs(JmhConfig.extend(Compile)).settings(
104-
version in Jmh := "1.24" // duplicated in project/build.sbt
107+
version in Jmh := jmhV
105108
)
106109
}
107110

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package scala.bench;
2+
3+
import org.openjdk.jmh.infra.BenchmarkParams;
4+
import org.openjdk.jmh.profile.JavaFlightRecorderProfiler;
5+
import org.openjdk.jmh.util.InputStreamDrainer;
6+
import org.openjdk.jmh.util.Utils;
7+
8+
import java.io.ByteArrayOutputStream;
9+
import java.io.File;
10+
import java.io.IOException;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.util.*;
15+
import java.util.stream.Collectors;
16+
17+
public final class JfrToFlamegraph implements JavaFlightRecorderProfiler.PostProcessor {
18+
private static final Path JFR_FLAMEGRAPH_DIR = findJfrFlamegraph();
19+
private static final Path FLAMEGRAPH_DIR = findFlamegraphDir();
20+
private enum Direction {
21+
FORWARD,
22+
REVERSE
23+
}
24+
25+
public List<File> postProcess(BenchmarkParams benchmarkParams, File jfrFile) {
26+
ArrayList<File> generated = new ArrayList<>();
27+
List<String> events = Arrays.asList("cpu", "allocation-tlab", "allocation-outside-tlab");
28+
if (JFR_FLAMEGRAPH_DIR != null) {
29+
for (String event : events) {
30+
Path csvFile = createCollapsed(event, jfrFile.toPath());
31+
generated.add(csvFile.toFile());
32+
if (FLAMEGRAPH_DIR != null) {
33+
for (Direction direction : EnumSet.allOf(Direction.class)) {
34+
Path svgPath = flameGraph(csvFile, Arrays.asList(), direction, Arrays.asList());
35+
generated.add(svgPath.toFile());
36+
}
37+
}
38+
}
39+
}
40+
return generated;
41+
}
42+
43+
private Path createCollapsed(String eventName, Path jfrFile) {
44+
ArrayList<String> args = new ArrayList<>();
45+
args.add("bash");
46+
args.add("-e");
47+
args.add(JFR_FLAMEGRAPH_DIR.toAbsolutePath().toString());
48+
args.add("--output-type");
49+
args.add("folded");
50+
args.add("--jfrdump");
51+
args.add(jfrFile.toAbsolutePath().toString());
52+
args.add("--event");
53+
args.add(eventName);
54+
Path outFile = jfrFile.resolveSibling(jfrFile.getFileName().toString().replace(".jfr", "-" + eventName.toLowerCase() + ".csv"));
55+
args.add("--output");
56+
args.add(outFile.toString());
57+
Collection<String> errorOutput = Utils.tryWith(args.toArray(new String[0]));
58+
if (errorOutput.isEmpty()) {
59+
return outFile;
60+
} else {
61+
throw new RuntimeException("Error in :" + args.stream().collect(Collectors.joining(" ")), new RuntimeException("Failed to convert JFR to CSV: " + errorOutput.stream().collect(Collectors.joining(System.lineSeparator()) )));
62+
}
63+
}
64+
65+
private static Path findJfrFlamegraph() {
66+
Path jfrFlameGraphDir = Paths.get(System.getenv("JFR_FLAME_GRAPH_DIR"));
67+
Path script = jfrFlameGraphDir.resolve("jfr-flame-graph");
68+
if (!Files.exists(script)) {
69+
script = jfrFlameGraphDir.resolve("build/install/jfr-flame-graph/bin/jfr-flame-graph");
70+
}
71+
if (!Files.exists(script)) {
72+
return null;
73+
} else {
74+
return script.toAbsolutePath();
75+
}
76+
}
77+
78+
private static Path flameGraph(Path collapsedPath, List<String> extra, Direction direction, Collection<? extends String> flameGraphOptions) {
79+
ArrayList<String> args = new ArrayList<>();
80+
args.add("perl");
81+
args.add(FLAMEGRAPH_DIR.resolve("flamegraph.pl").toAbsolutePath().toString());
82+
args.addAll(flameGraphOptions);
83+
args.addAll(extra);
84+
if (direction == Direction.REVERSE) {
85+
args.add("--reverse");
86+
}
87+
args.addAll(Arrays.asList("--minwidth", "1", "--colors", "java", "--cp", "--width", "1800"));
88+
args.add(collapsedPath.toAbsolutePath().toString());
89+
Path outputFile = collapsedPath.resolveSibling(collapsedPath.getFileName().toString().replace(".csv", "-" + direction.name().toLowerCase() + ".svg"));
90+
ProcessBuilder processBuilder = new ProcessBuilder(args);
91+
processBuilder.redirectOutput(outputFile.toFile());
92+
try {
93+
Process p = processBuilder.start();
94+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
95+
InputStreamDrainer errDrainer = new InputStreamDrainer(p.getErrorStream(), baos);
96+
InputStreamDrainer outDrainer = new InputStreamDrainer(p.getInputStream(), baos);
97+
int err = p.waitFor();
98+
errDrainer.start();
99+
outDrainer.start();
100+
errDrainer.join();
101+
outDrainer.join();
102+
if (err != 0) {
103+
throw new RuntimeException("Non zero return code from " + args + System.lineSeparator() + baos.toString());
104+
}
105+
} catch (IOException | InterruptedException e) {
106+
throw new RuntimeException(e);
107+
}
108+
return outputFile;
109+
}
110+
111+
112+
private static Path findFlamegraphDir() {
113+
String flameGraphHome = System.getenv("FLAME_GRAPH_DIR");
114+
if (flameGraphHome == null) {
115+
return null;
116+
} else {
117+
return Paths.get(flameGraphHome);
118+
}
119+
}
120+
}

project/Profilers.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,17 @@ case object basic extends Profiler("basic") {
3535
def command(outDir: File): String = "-jvmArgs -Xprof -prof hs_comp -prof gc -prof stack -prof hs_rt -prof scala.tools.nsc.ThreadCpuTimeProfiler"
3636
}
3737
case object jfr extends Profiler("jfr") {
38-
def command(outDir: File): String = s"""-prof "jmh.extras.JFR:dir=${outDir.getAbsolutePath};flameGraphOpts=$flameGraphOpts;verbose=true" """
38+
def command(outDir: File): String = s"""-prof "jfr:dir=${outDir.getAbsolutePath};stackDepth=1024;postProcessor=scala.bench.JfrToFlamegraph;verbose=true" """
3939
}
4040
sealed abstract class async(event: String) extends Profiler("async-" + event) {
4141
val framebuf = 33554432
4242
def command(outDir: File): String = {
43-
s"""-prof "async:dir=${outDir.getAbsolutePath};libPath=${System.getenv("ASYNC_PROFILER_DIR")}/build/libasyncProfiler.so;minwidth=1;width=1800;verbose=true;event=$event;filter=${event == "wall"};flat=40;trace=10;framebuf=${framebuf};output=flamegraph,jfr,text" """
43+
s"""-prof -jvmArgs -XX:+UnlockCommercialFeatures "async:dir=${outDir.getAbsolutePath};libPath=${System.getenv("ASYNC_PROFILER_DIR")}/build/libasyncProfiler.so;minwidth=1;width=1800;verbose=true;event=$event;filter=${event == "wall"};flat=40;trace=10;framebuf=${framebuf};output=flamegraph,jfr,text" """
4444
} // + ";simplename=true" TODO add this after upgrading next sbt-jmh release
4545
}
4646
case object asyncCpu extends async("cpu")
4747
case object asyncAlloc extends async("alloc")
4848
case object asyncWall extends async("wall")
4949
case object perfNorm extends Profiler("perfNorm") {
5050
def command(outDir: File): String = "-prof perfnorm"
51-
}
51+
}

project/build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
val jmhV = "1.24" // duplicated in build.sbt
1+
val jmhV = "1.25" // duplicated in build.sbt
22

33
libraryDependencies ++= List(
44
"org.openjdk.jmh" % "jmh-core" % jmhV,

0 commit comments

Comments
 (0)