Skip to content

Commit d7eec06

Browse files
authored
Merge pull request #30 from retronym/topic/sbt-benchmark
Experimental support for benchmarking compilation under SBT
2 parents 6c75226 + b495678 commit d7eec06

File tree

4 files changed

+198
-26
lines changed

4 files changed

+198
-26
lines changed

build.sbt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ lazy val addJavaOptions = javaOptions ++= {
6161
}
6262
List(
6363
"-DscalaVersion=" + scalaVersion.value,
64-
"-DscalaRef=" + refOf(scalaVersion.value)
64+
"-DscalaRef=" + refOf(scalaVersion.value),
65+
"-Dsbt.launcher=" + (sys.props("java.class.path").split(java.io.File.pathSeparatorChar).find(_.contains("sbt-launch")).getOrElse(""))
6566
)
6667
}
6768

Lines changed: 48 additions & 0 deletions
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+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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+
@Param(value = Array("0.13.15"))
29+
var sbtVersion: String = _
30+
31+
// This parameter is set by ScalacBenchmarkRunner / UploadingRunner based on the Scala version.
32+
// When running the benchmark directly the "latest" symlink is used.
33+
@Param(value = Array("latest"))
34+
var corpusVersion: String = _
35+
36+
var sbtProcess: Process = _
37+
var inputRedirect: ProcessBuilder.Redirect = _
38+
var outputRedirect: ProcessBuilder.Redirect = _
39+
var tempDir: Path = _
40+
var scalaHome: Path = _
41+
var processOutputReader: BufferedReader = _
42+
var processInputReader: BufferedWriter = _
43+
var output= new java.lang.StringBuilder()
44+
45+
def buildDef =
46+
s"""
47+
|scalaHome := Some(file("${scalaHome.toAbsolutePath.toString}"))
48+
|
49+
|val cleanClasses = taskKey[Unit]("clean the classes directory")
50+
|
51+
|cleanClasses := IO.delete((classDirectory in Compile).value)
52+
|
53+
|scalaSource in Compile := file("${corpusSourcePath.toAbsolutePath.toString}")
54+
|
55+
|libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value
56+
|libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
57+
|
58+
|// TODO support .java sources
59+
""".stripMargin
60+
61+
@Setup(Level.Trial) def spawn(): Unit = {
62+
tempDir = Files.createTempDirectory("sbt-")
63+
scalaHome = Files.createTempDirectory("scalaHome-")
64+
initDepsClasspath()
65+
Files.createDirectory(tempDir.resolve("project"))
66+
Files.write(tempDir.resolve("project/build.properties"), java.util.Arrays.asList("sbt.version=" + sbtVersion))
67+
Files.write(tempDir.resolve("build.sbt"), buildDef.getBytes("UTF-8"))
68+
val sbtLaucherPath = System.getProperty("sbt.launcher")
69+
if (sbtLaucherPath == null) sys.error("System property -Dsbt.launcher absent")
70+
val builder = new ProcessBuilder(sys.props("java.home") + "/bin/java", "-Xms2G", "-Xmx2G", "-Dsbt.log.format=false", "-jar", sbtLaucherPath)
71+
builder.directory(tempDir.toFile)
72+
inputRedirect = builder.redirectInput()
73+
outputRedirect = builder.redirectOutput()
74+
sbtProcess = builder.start()
75+
processOutputReader = new BufferedReader(new InputStreamReader(sbtProcess.getInputStream))
76+
processInputReader = new BufferedWriter(new OutputStreamWriter(sbtProcess.getOutputStream))
77+
awaitPrompt()
78+
}
79+
80+
@Benchmark
81+
def compile(): Unit = {
82+
issue(";cleanClasses;compile")
83+
awaitPrompt()
84+
}
85+
86+
def issue(str: String) = {
87+
processInputReader.write(str + "\n")
88+
processInputReader.flush()
89+
}
90+
91+
def awaitPrompt(): Unit = {
92+
output.setLength(0)
93+
var line = ""
94+
val buffer = new Array[Char](128)
95+
var read : Int = -1
96+
while (true) {
97+
read = processOutputReader.read(buffer)
98+
if (read == -1) sys.error("EOF: " + output.toString)
99+
else {
100+
output.append(buffer, 0, read)
101+
if (output.toString.contains("\n> ")) {
102+
if (output.toString.contains("[error")) sys.error(output.toString)
103+
return
104+
}
105+
}
106+
}
107+
108+
}
109+
110+
private def corpusSourcePath = Paths.get(s"../corpus/$source/$corpusVersion")
111+
112+
def initDepsClasspath(): Unit = {
113+
val libDir = tempDir.resolve("lib")
114+
Files.createDirectories(libDir)
115+
for (depFile <- BenchmarkUtils.initDeps(corpusSourcePath)) {
116+
val libDirFile = libDir.resolve(depFile.getFileName)
117+
Files.copy(depFile, libDir)
118+
}
119+
120+
val scalaHomeLibDir = scalaHome.resolve("lib")
121+
Files.createDirectories(scalaHomeLibDir)
122+
for (elem <- sys.props("java.class.path").split(File.pathSeparatorChar)) {
123+
val jarFile = Paths.get(elem)
124+
var name = jarFile.getFileName.toString
125+
if (name.startsWith("scala") && name.endsWith(".jar")) {
126+
if (name.startsWith("scala-library"))
127+
name = "scala-library.jar"
128+
else if (name.startsWith("scala-reflect"))
129+
name = "scala-reflect.jar"
130+
else if (name.startsWith("scala-compiler"))
131+
name = "scala-compiler.jar"
132+
Files.copy(jarFile, scalaHomeLibDir.resolve(name))
133+
}
134+
}
135+
136+
}
137+
138+
@TearDown(Level.Trial) def terminate(): Unit = {
139+
processOutputReader.close()
140+
sbtProcess.destroyForcibly()
141+
BenchmarkUtils.deleteRecursive(tempDir)
142+
BenchmarkUtils.deleteRecursive(scalaHome)
143+
}
144+
}

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

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

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

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

0 commit comments

Comments
 (0)