Skip to content

Commit be29631

Browse files
authored
Merge pull request scala#7294 from retronym/topic/jitwatch
Documentation and automation for using JITWatch to analyse JMH benchmarks
2 parents a84ce5b + a2de76f commit be29631

File tree

3 files changed

+151
-1
lines changed

3 files changed

+151
-1
lines changed

build.sbt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,8 @@ lazy val bench = project.in(file("test") / "benchmarks")
618618
else "org.scala-lang" % "scala-compiler" % benchmarkScalaVersion :: Nil
619619
},
620620
scalacOptions ++= Seq("-feature", "-opt:l:inline", "-opt-inline-from:**")
621-
)
621+
).settings(inConfig(JmhPlugin.JmhKeys.Jmh)(scalabuild.JitWatchFilePlugin.jitwatchSettings))
622+
622623

623624
lazy val junit = project.in(file("test") / "junit")
624625
.dependsOn(library, reflect, compiler, partest, scaladoc)

project/JitWatch.scala

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package scalabuild
2+
3+
import java.io.FileWriter
4+
import java.util.Properties
5+
6+
import sbt._
7+
import Keys._
8+
9+
object JitWatchFilePlugin extends AutoPlugin {
10+
override def trigger = allRequirements
11+
override def requires = sbt.plugins.JvmPlugin
12+
val jitwatchConfigFileContents = taskKey[Properties]("Contents of file suitable for `jitwatch/launchUI -Djitwatch.config.file=jitwatch.properties`")
13+
val jitwatchConfigFile = taskKey[Unit]("file suitable for `jitwatch/launchUI.sh -Djitwatch.config.file=jitwatch.properties`")
14+
15+
override lazy val projectSettings = List(Compile, Test).flatMap(c => inConfig(c)(jitwatchSettings))
16+
17+
def jitwatchSettings: Seq[Setting[_]] = Seq(
18+
jitwatchConfigFileContents := {
19+
val sourcesValue = sources.value
20+
val depdependencyClasspathValue = dependencyClasspath.value ++ internalDependencyClasspath.value
21+
val props = new java.util.Properties
22+
val classpathString = (classDirectory.value +: depdependencyClasspathValue.map(_.data.toString)).mkString(",")
23+
val artifacts: Seq[Artifact] = depdependencyClasspathValue.flatMap(_.get(Keys.artifact.key))
24+
val dependencyModuleIds: Set[ModuleID] = depdependencyClasspathValue.flatMap(_.get(Keys.moduleID.key)).toSet
25+
props.put("Classes", classpathString)
26+
27+
// JDK sources from $JAVA_HOME/src.zip
28+
val javaHomeSrc = {
29+
val javaDir = javaHome.value.getOrElse(new File(System.getProperty("java.home")))
30+
val src1 = javaDir / "src.zip"
31+
val src2 = javaDir.getParentFile / "src.zip"
32+
if (src1.exists()) src1 else src2
33+
}
34+
35+
// Transitive sources from the projects that contribute to this classpath.
36+
val projects: Seq[ProjectRef] = buildDependencies.value.classpathTransitiveRefs(thisProjectRef.value) :+ thisProjectRef.value
37+
val projectArtifacts: Map[ProjectRef, Seq[Artifact]] = projects.map(project => (project -> (Keys.artifacts in project get settingsData.value).getOrElse(Nil))).toMap
38+
val artifactNameToProject: Map[String, Seq[ProjectRef]] = projects.groupBy(project => (Keys.name in project get settingsData.value).getOrElse(""))
39+
val transitiveSourceDirectories = projects.flatMap { project =>
40+
val projectArtifacts: Seq[Artifact] = (Keys.artifacts in project get settingsData.value).getOrElse(Nil)
41+
val matching = projectArtifacts.filter(artifacts.contains(_))
42+
val configs = matching.flatMap(artifact => artifact.configurations).distinct
43+
val sourceDirectories: Seq[File] = configs.flatMap { configRef =>
44+
(Keys.sourceDirectories in project in sbt.Configuration.of(configRef.name.capitalize, configRef.name)).get(settingsData.value).toList.flatten
45+
}
46+
sourceDirectories
47+
}.distinct
48+
val transitiveSourceDirectories2 = artifacts.flatMap { artifact =>
49+
val projects = artifactNameToProject.getOrElse(artifact.name, Nil)
50+
projects.flatMap { project: ProjectRef =>
51+
val configs = artifact.configurations
52+
val sourceDirectories: Seq[File] = configs.toList.flatMap { configRef =>
53+
(Keys.sourceDirectories in project in sbt.Configuration.of(configRef.name.capitalize, configRef.name)).get(settingsData.value).toList.flatten
54+
}
55+
sourceDirectories
56+
}
57+
}
58+
59+
// Download and add transitive sources from the classpath
60+
val classiferArtifacts: Seq[(ModuleID, Artifact, File)] = updateClassifiers.value.configurations.flatMap(_.details.flatMap(_.modules.flatMap(report => report.artifacts.map(x => (report.module, x._1, x._2)))))
61+
val sourceClassiferArtifacts = classiferArtifacts.filter(tuple => tuple._2.classifier == Some("sources") && dependencyModuleIds.contains(tuple._1))
62+
63+
val externalSources = sourceClassiferArtifacts.map(_._3)
64+
val internalAndExternalSources = (sourceDirectory.value +: javaHomeSrc +: (transitiveSourceDirectories ++ transitiveSourceDirectories2).distinct) ++ externalSources
65+
props.put("Sources", internalAndExternalSources.map(_.getAbsolutePath).mkString(","))
66+
val baseDir = baseDirectory.value
67+
val lastLogDir = Keys.forkOptions.value.workingDirectory match {
68+
case Some(dir) => dir
69+
case _=> baseDir
70+
}
71+
props.put("LastLogDir", lastLogDir.getAbsolutePath)
72+
props
73+
},
74+
75+
jitwatchConfigFile := {
76+
val f = target.value / ("jitwatch-" + configuration.value.name + ".properties")
77+
val contents = jitwatchConfigFileContents.value
78+
val log = streams.value.log
79+
val fw = new FileWriter(f)
80+
try {
81+
jitwatchConfigFileContents.value.store(fw, null)
82+
log.info(s"./launchUI.sh -Djitwatch.config.file=" + f.getAbsolutePath)
83+
} finally {
84+
fw.close()
85+
}
86+
}
87+
)
88+
}

test/benchmarks/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,74 @@ For an Oracle (or other compatible) JVM not set up by your distribution,
7272
you may also need to copy or link the disassembler library
7373
to the `jre/lib/`_`architecture`_ directory inside your JVM installation directory.
7474

75+
The JITWatch project has [hsdis build instructions](https://github.com/AdoptOpenJDK/jitwatch/wiki/Building-hsdis).
76+
One way to obtain HSDIS is to use [the binaries](https://lafo.ssw.uni-linz.ac.at/pub/graal-external-deps/hsdis/intel/) which are used in the [Graal build](https://github.com/oracle/graal/blob/master/compiler/mx.compiler/mx_graal_tools.py#L94-L119).
77+
7578
To show the assembly code corresponding to the code generated by the JIT compiler for specific methods,
7679
add `-XX:CompileCommand=print,scala.collection.mutable.OpenHashMap::*`,
7780
for example, to show all of the methods in the `scala.collection.mutable.OpenHashMap` class.
7881

7982
To show it for _all_ methods, add `-XX:+PrintAssembly`.
8083
(This is usually excessive.)
8184

85+
### Using JITWatch
86+
87+
[JITWatch](https://github.com/AdoptOpenJDK/jitwatch) is useful to understand how the JVM has JIT compiled
88+
code.
89+
90+
If you install `hsdis`, as described above, machine code disassembly is also created.
91+
92+
You can generate the `hotspot.log` file for a benchmark run by adding the [required JVM options](https://github.com/AdoptOpenJDK/jitwatch/wiki/Building-hsdis)
93+
to JMH benchmark execution:
94+
95+
```
96+
sbt:root> bench/jmh:run scala.collection.mutable.ArrayOpsBenchmark.insertInteger -psize=1000 -f1 -jvmArgs -XX:+UnlockDiagnosticVMOptions -jvmArgs -XX:+TraceClassLoading -jvmArgs -XX:+LogCompilation -jvmArgs -XX:LogFile=hotspot.log -jvmArgs -XX:+PrintAssembly
97+
...
98+
[info] Loaded disassembler from /Users/jz/.jabba/jdk/1.8.172/Contents/Home/jre/lib/hsdis-amd64.dylib
99+
[info] Decoding compiled method 0x0000000113f60bd0:
100+
[info] Code:
101+
[info] [Disassembling for mach='i386:x86-64']
102+
[info] [Entry Point]
103+
[info] [Constants]
104+
[info] # {method} {0x000000010ffa0000} 'hashCode' '()I' in 'java/lang/String'
105+
[info] # [sp+0x40] (sp of caller)
106+
[info] 0x0000000113f60d40: mov r10d,DWORD PTR [rsi+0x8]
107+
[info] 0x0000000113f60d44: shl r10,0x3
108+
...
109+
[info] # Run complete. Total time: 00:00:30
110+
[info] Benchmark (size) Mode Cnt Score Error Units
111+
[info] ArrayOpsBenchmark.insertInteger 1000 avgt 10 188199.582 ± 5930.520 ns/op
112+
```
113+
114+
JITWatch requires configuration of the class and source path. We can generate that with a custom
115+
task in our build:
116+
117+
```
118+
sbt> bench/jmh:jitwatchConfigFile
119+
[info] Resolving jline#jline;2.14.6 ...
120+
jmh
121+
[info] ./launchUI.sh -Djitwatch.config.file=/Users/jz/code/scala/test/benchmarks/target/jitwatch-jmh.properties
122+
123+
sbt> ^C
124+
```
125+
126+
Build jitwatch.
127+
128+
```
129+
$ git clone https://github.com/AdoptOpenJDK/jitwatch
130+
$ cd jitwatch
131+
$ mvn install
132+
```
133+
134+
Launch with the generated config file.
135+
```
136+
$ ./launchUI.sh -Djitwatch.config.file=/Users/jz/code/scala/test/benchmarks/target/jitwatch-jmh.properties
137+
```
138+
139+
140+
141+
Select the generated `hotspot.log`, `start`, and then browse the the benchmark to start gleaning insights!
142+
82143
## Useful reading
83144
* [OpenJDK advice on microbenchmarks](https://wiki.openjdk.java.net/display/HotSpot/MicroBenchmarks)
84145
* Brian Goetz's "Java theory and practice" articles:

0 commit comments

Comments
 (0)