Skip to content

Commit f1485f6

Browse files
committed
Experimental support for benchmarking compilation under SBT
Works by driving a child SBT process via stdin/stdout scraping. ``` > compilation/jmh:run HotSbtBenchmark -f 1 -psource=scalap [info] Compiling 1 Scala source to /Users/jz/code/compiler-benchmark/compilation/target/scala-2.11/classes... Processing 46 classes from /Users/jz/code/compiler-benchmark/compilation/target/scala-2.11/classes with "reflection" generator Writing out Java source to /Users/jz/code/compiler-benchmark/compilation/target/scala-2.11/src_managed/jmh and resources to /Users/jz/code/compiler-benchmark/compilation/target/scala-2.11/resource_managed/jmh [info] Compiling 5 Java sources to /Users/jz/code/compiler-benchmark/compilation/target/scala-2.11/classes... [info] Running scala.bench.ScalacBenchmarkRunner HotSbtBenchmark -f 1 -psource=scalap [info] # JMH version: 1.19 [info] # VM version: JDK 1.8.0_112, VM 25.112-b16 [info] # VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/bin/java [info] # VM options: -Dsbt.launcher=/usr/local/Cellar/sbt/0.13.15/libexec/bin/sbt-launch.jar [info] # Warmup: 10 iterations, 10 s each [info] # Measurement: 10 iterations, 10 s each [info] # Timeout: 10 min per iteration [info] # Threads: 1 thread, will synchronize iterations [info] # Benchmark mode: Sampling time [info] # Benchmark: scala.tools.nsc.HotSbtBenchmark.compile [info] # Parameters: (corpusVersion = a8c43dc, extraArgs = , source = scalap) [info] [info] # Run progress: 0.00% complete, ETA 00:03:20 [info] # Fork: 1 of 1 [info] # Warmup Iteration 1: 6683.623 ms/op [info] # Warmup Iteration 2: 2724.200 ±(99.9%) 2851.803 ms/op [info] # Warmup Iteration 3: 1867.863 ±(99.9%) 314.998 ms/op [info] # Warmup Iteration 4: 2167.197 ±(99.9%) 3228.941 ms/op [info] # Warmup Iteration 5: 2198.235 ±(99.9%) 713.756 ms/op [info] # Warmup Iteration 6: 1831.862 ±(99.9%) 452.490 ms/op [info] # Warmup Iteration 7: 1726.306 ±(99.9%) 115.085 ms/op [info] # Warmup Iteration 8: 1856.679 ±(99.9%) 1093.828 ms/op [info] # Warmup Iteration 9: 1615.406 ±(99.9%) 796.722 ms/op [info] # Warmup Iteration 10: 1492.573 ±(99.9%) 62.763 ms/op ... ⚡ sbt [info] Loading global plugins from /Users/jz/.sbt/0.13/plugins [info] Loading project definition from /Users/jz/code/compiler-benchmark/project [info] Set current project to compiler-benchmark (in build file:/Users/jz/code/compiler-benchmark/) > compilation/jmh:run HotScalacBenchmark -f 1 -psource=scalap [info] Running scala.bench.ScalacBenchmarkRunner HotScalacBenchmark -f 1 -psource=scalap [info] # JMH version: 1.19 [info] # VM version: JDK 1.8.0_112, VM 25.112-b16 [info] # VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/bin/java [info] # VM options: -Dsbt.launcher=/usr/local/Cellar/sbt/0.13.15/libexec/bin/sbt-launch.jar [info] # Warmup: 10 iterations, 10 s each [info] # Measurement: 10 iterations, 10 s each [info] # Timeout: 10 min per iteration [info] # Threads: 1 thread, will synchronize iterations [info] # Benchmark mode: Sampling time [info] # Benchmark: scala.tools.nsc.HotScalacBenchmark.compile [info] # Parameters: (corpusVersion = a8c43dc, extraArgs = , source = scalap) [info] [info] # Run progress: 0.00% complete, ETA 00:03:20 [info] # Fork: 1 of 1 [info] # Warmup Iteration 1: 5312.086 ms/op [info] # Warmup Iteration 2: 2054.790 ±(99.9%) 1420.469 ms/op [info] # Warmup Iteration 3: 1456.622 ±(99.9%) 141.405 ms/op [info] # Warmup Iteration 4: 1297.613 ±(99.9%) 100.157 ms/op [info] # Warmup Iteration 5: 1248.038 ±(99.9%) 38.623 ms/op [info] # Warmup Iteration 6: 1221.941 ±(99.9%) 31.619 ms/op [info] # Warmup Iteration 7: 1200.969 ±(99.9%) 33.985 ms/op [info] # Warmup Iteration 8: 1210.756 ±(99.9%) 44.681 ms/op ```
1 parent e3ce19b commit f1485f6

File tree

3 files changed

+193
-25
lines changed

3 files changed

+193
-25
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package scala.tools.nsc
2+
3+
import java.io.{File, IOException}
4+
import java.net.URL
5+
import java.nio.file._
6+
import java.nio.file.attribute.BasicFileAttributes
7+
8+
import com.typesafe.config.ConfigFactory
9+
10+
import scala.collection.JavaConverters._
11+
12+
object BenchmarkUtils {
13+
def deleteRecursive(directory: Path): Unit = {
14+
Files.walkFileTree(directory, new SimpleFileVisitor[Path]() {
15+
override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
16+
Files.delete(file)
17+
FileVisitResult.CONTINUE
18+
}
19+
override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = {
20+
Files.delete(dir)
21+
FileVisitResult.CONTINUE
22+
}
23+
})
24+
}
25+
def initDeps(corpusSourcePath: Path): Seq[Path] = {
26+
val depsDir = Paths.get(ConfigFactory.load.getString("deps.localdir"))
27+
val depsFile = corpusSourcePath.resolve("deps.txt")
28+
val depsClasspath = Seq.newBuilder[Path]
29+
if (Files.exists(depsFile)) {
30+
val res = new StringBuilder()
31+
for (depUrlString <- Files.lines(depsFile).iterator().asScala) {
32+
val depUrl = new URL(depUrlString)
33+
val filename = Paths.get(depUrl.getPath).getFileName.toString
34+
val depFile = depsDir.resolve(filename)
35+
// TODO: check hash if file exists, or after downloading
36+
if (!Files.exists(depFile)) {
37+
if (!Files.exists(depsDir)) Files.createDirectories(depsDir)
38+
val in = depUrl.openStream
39+
Files.copy(in, depFile, StandardCopyOption.REPLACE_EXISTING)
40+
in.close()
41+
}
42+
if (res.nonEmpty) res.append(File.pathSeparator)
43+
depsClasspath += depFile
44+
}
45+
depsClasspath.result()
46+
} else Nil
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package scala.tools.nsc
2+
3+
import java.io._
4+
import java.net.URL
5+
import java.nio.file._
6+
import java.nio.file.attribute.BasicFileAttributes
7+
import java.util.concurrent.TimeUnit
8+
9+
import com.typesafe.config.ConfigFactory
10+
import org.openjdk.jmh.annotations.Mode.SampleTime
11+
import org.openjdk.jmh.annotations._
12+
13+
import scala.collection.JavaConverters._
14+
15+
@State(Scope.Benchmark)
16+
@BenchmarkMode(Array(SampleTime))
17+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
18+
@Warmup(iterations = 10, time = 10, timeUnit = TimeUnit.SECONDS)
19+
@Measurement(iterations = 10, time = 10, timeUnit = TimeUnit.SECONDS)
20+
@Fork(value = 3)
21+
class HotSbtBenchmark {
22+
@Param(value = Array())
23+
var source: String = _
24+
25+
@Param(value = Array(""))
26+
var extraArgs: String = _
27+
28+
// This parameter is set by ScalacBenchmarkRunner / UploadingRunner based on the Scala version.
29+
// When running the benchmark directly the "latest" symlink is used.
30+
@Param(value = Array("latest"))
31+
var corpusVersion: String = _
32+
33+
var sbtProcess: Process = _
34+
var inputRedirect: ProcessBuilder.Redirect = _
35+
var outputRedirect: ProcessBuilder.Redirect = _
36+
var tempDir: Path = _
37+
var scalaHome: Path = _
38+
var processOutputReader: BufferedReader = _
39+
var processInputReader: BufferedWriter = _
40+
var output= new java.lang.StringBuilder()
41+
42+
def buildDef =
43+
s"""
44+
|scalaHome := Some(file("${scalaHome.toAbsolutePath.toString}"))
45+
|
46+
|val cleanClasses = taskKey[Unit]("clean the classes directory")
47+
|
48+
|cleanClasses := IO.delete((classDirectory in Compile).value)
49+
|
50+
|scalaSource in Compile := file("${corpusSourcePath.toAbsolutePath.toString}")
51+
|
52+
|libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value
53+
|libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
54+
|
55+
|// TODO support .java sources
56+
""".stripMargin
57+
58+
@Setup(Level.Trial) def spawn(): Unit = {
59+
tempDir = Files.createTempDirectory("sbt-")
60+
scalaHome = Files.createTempDirectory("scalaHome-")
61+
initDepsClasspath()
62+
Files.createDirectory(tempDir.resolve("project"))
63+
Files.write(tempDir.resolve("project/build.properties"), java.util.Arrays.asList("sbt.version=0.13.15"))
64+
Files.write(tempDir.resolve("build.sbt"), buildDef.getBytes("UTF-8"))
65+
val sbtLaucherPath = System.getProperty("sbt.launcher")
66+
if (sbtLaucherPath == null) sys.error("System property -Dsbt.launcher absent")
67+
val builder = new ProcessBuilder(sys.props("java.home") + "/bin/java", "-Xms2G", "-Xmx2G", "-Dsbt.log.format=false", "-jar", sbtLaucherPath)
68+
builder.directory(tempDir.toFile)
69+
inputRedirect = builder.redirectInput()
70+
outputRedirect = builder.redirectOutput()
71+
sbtProcess = builder.start()
72+
processOutputReader = new BufferedReader(new InputStreamReader(sbtProcess.getInputStream))
73+
processInputReader = new BufferedWriter(new OutputStreamWriter(sbtProcess.getOutputStream))
74+
awaitPrompt()
75+
}
76+
77+
@Benchmark
78+
def compile(): Unit = {
79+
issue(";cleanClasses;compile")
80+
awaitPrompt()
81+
}
82+
83+
def issue(str: String) = {
84+
processInputReader.write(str + "\n")
85+
processInputReader.flush()
86+
}
87+
88+
def awaitPrompt(): Unit = {
89+
output.setLength(0)
90+
var line = ""
91+
val buffer = new Array[Char](128)
92+
var read : Int = -1
93+
while (true) {
94+
read = processOutputReader.read(buffer)
95+
if (read == -1) sys.error("EOF")
96+
else {
97+
output.append(buffer, 0, read)
98+
if (output.toString.contains("\n> ")) {
99+
if (output.toString.contains("[error")) sys.error(output.toString)
100+
return
101+
}
102+
}
103+
}
104+
105+
}
106+
107+
private def corpusSourcePath = Paths.get(s"../corpus/$source/$corpusVersion")
108+
109+
def initDepsClasspath(): Unit = {
110+
val libDir = tempDir.resolve("lib")
111+
Files.createDirectories(libDir)
112+
for (depFile <- BenchmarkUtils.initDeps(corpusSourcePath)) {
113+
val libDirFile = libDir.resolve(depFile.getFileName)
114+
Files.copy(depFile, libDir)
115+
}
116+
117+
val scalaHomeLibDir = scalaHome.resolve("lib")
118+
Files.createDirectories(scalaHomeLibDir)
119+
for (elem <- sys.props("java.class.path").split(File.pathSeparatorChar)) {
120+
val jarFile = Paths.get(elem)
121+
var name = jarFile.getFileName.toString
122+
if (name.startsWith("scala") && name.endsWith(".jar")) {
123+
if (name.startsWith("scala-library"))
124+
name = "scala-library.jar"
125+
else if (name.startsWith("scala-reflect"))
126+
name = "scala-reflect.jar"
127+
else if (name.startsWith("scala-compiler"))
128+
name = "scala-compiler.jar"
129+
Files.copy(jarFile, scalaHomeLibDir.resolve(name))
130+
}
131+
}
132+
133+
}
134+
135+
@TearDown(Level.Trial) def terminate(): Unit = {
136+
processOutputReader.close()
137+
sbtProcess.destroyForcibly()
138+
BenchmarkUtils.deleteRecursive(tempDir)
139+
BenchmarkUtils.deleteRecursive(scalaHome)
140+
}
141+
}

compilation/src/main/scala/scala/tools/nsc/ScalacBenchmark.scala

+4-25
Original file line numberDiff line numberDiff line change
@@ -82,37 +82,16 @@ class ScalacBenchmark {
8282
tempDir = tempFile
8383
}
8484
@TearDown(Level.Trial) def clearTemp(): Unit = {
85-
val directory = tempDir.toPath
86-
Files.walkFileTree(directory, new SimpleFileVisitor[Path]() {
87-
override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
88-
Files.delete(file)
89-
FileVisitResult.CONTINUE
90-
}
91-
override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = {
92-
Files.delete(dir)
93-
FileVisitResult.CONTINUE
94-
}
95-
})
85+
BenchmarkUtils.deleteRecursive(tempDir.toPath)
9686
}
9787

9888
private def corpusSourcePath = Paths.get(s"../corpus/$source/$corpusVersion")
9989

10090
@Setup(Level.Trial) def initDepsClasspath(): Unit = {
101-
val depsDir = Paths.get(ConfigFactory.load.getString("deps.localdir"))
102-
val depsFile = corpusSourcePath.resolve("deps.txt")
103-
if (Files.exists(depsFile)) {
91+
val classPath = BenchmarkUtils.initDeps(corpusSourcePath)
92+
if (classPath.nonEmpty) {
10493
val res = new StringBuilder()
105-
for (depUrlString <- Files.lines(depsFile).iterator().asScala) {
106-
val depUrl = new URL(depUrlString)
107-
val filename = Paths.get(depUrl.getPath).getFileName.toString
108-
val depFile = depsDir.resolve(filename)
109-
// TODO: check hash if file exists, or after downloading
110-
if (!Files.exists(depFile)) {
111-
if (!Files.exists(depsDir)) Files.createDirectories(depsDir)
112-
val in = depUrl.openStream
113-
Files.copy(in, depFile, StandardCopyOption.REPLACE_EXISTING)
114-
in.close()
115-
}
94+
for (depFile <- classPath) {
11695
if (res.nonEmpty) res.append(File.pathSeparator)
11796
res.append(depFile.toAbsolutePath.normalize.toString)
11897
}

0 commit comments

Comments
 (0)