Skip to content

Commit 2347b17

Browse files
committed
Add benchmark results
The benchmark results indicate that the overhead from the compiler plugin is ~4x compared to regular compilation, but CPU flamegraphs indicate that this overhead may be related to a JVM mis-configuration instead of logic inside the plugin instead. Until we better understand the performance overhead, let's done down claims in the readme about "low compile overhead". I still think 4x overhead will be acceptable for many projects given that this job runs only in CI for LSIF uploads.
1 parent 4aae7e5 commit 2347b17

File tree

9 files changed

+237
-95
lines changed

9 files changed

+237
-95
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ using SemanticDB as an intermediary representation for LSIF:
6363
LSIF because compiler plugins does not have access to a project-wide context,
6464
which is necessary to produce accurate definitions and hovers in multi-module
6565
projects with external library dependencies.
66-
- **Performance**: SemanticDB is fast to write and read. The compiler adds low
67-
overhead on compilation and the final conversion from SemanticDB to LSIF can
68-
be safely parallelized.
66+
- **Performance**: SemanticDB is fast to write and read. Each compilation unit
67+
can be processed independently to keep memory usage low. The final conversion
68+
from SemanticDB to LSIF can be safely parallelized.
6969
- **Cross-language**: SemanticDB has a
7070
[spec](https://scalameta.org/docs/semanticdb/specification.html) for Java and
7171
Scala enabling cross-language navigation in hybrid Java/Scala codebases.
@@ -149,3 +149,7 @@ write tests because:
149149
code (which is always multiline). Modern versions of Java support multiline
150150
string literals, but they're not supported in Java 8, which is supported by
151151
lsif-java.
152+
153+
## Benchmarks
154+
155+
See [docs/benchmarks.md] for benchmark results.

build.sbt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,13 @@ lazy val snapshots = project
138138
)
139139
.dependsOn(minimizedScala, unit)
140140
.enablePlugins(BuildInfoPlugin)
141+
142+
lazy val bench = project
143+
.in(file("tests/benchmarks"))
144+
.settings(
145+
moduleName := "lsif-java-bench",
146+
fork.in(run) := true,
147+
skip.in(publish) := true
148+
)
149+
.dependsOn(unit)
150+
.enablePlugins(JmhPlugin)

docs/benchmarks.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Benchmarks results
2+
3+
```
4+
sbt:root> bench/jmh:run -i 3 -wi 3 -f1 -t1
5+
...
6+
[info] Benchmark (lib) Mode Cnt Score Error Units Overhead
7+
[info] CompileBench.compile guava ss 3 10456.139 ± 9794.634 ms/op 1x
8+
[info] CompileBench.compileSemanticdb guava ss 3 48218.454 ± 29336.227 ms/op 4.6x
9+
[info] CompileBench.compile bytebuddy ss 3 5821.383 ± 9201.910 ms/op 1x
10+
[info] CompileBench.compileSemanticdb bytebuddy ss 3 21521.375 ± 5582.770 ms/op 3.7x
11+
```
12+
13+
- Date: Monday 15 Feb 2021.
14+
- Hardware: MacBook Pro (16-inch, 2019), 2,6 GHz 6-Core Intel Core i7, 32 GB
15+
2667 MHz DDR4.
16+
- Interpretation: enabling the SemanticDB compiler plugin slows down normal
17+
compilation by ~4x.
18+
- Recommendation: do not enable the SemanticDB compiler plugin during local
19+
edit-and-test workflows. The compiler plugin is primarily intended to be
20+
enabled in custom CI jobs to upload LSIF indexes.
21+
- Note: the CPU flamegraphs below indicate a significant fraction of the
22+
performance overhead comes from `jvmtiError 23`, which hints that the overhead
23+
is not related to the logic of the compiler plugin itself.
24+
25+
![](https://i.imgur.com/OPldbmD.png).

project/plugins.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.0")
66
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.6-21-464e4ec4")
77
addSbtPlugin("com.sourcegraph" % "sbt-sourcegraph" % "0.1.8")
88
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.6.0")
9+
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.0")
910

1011
// sbt-jdi-tools appears to fix an error related to this message:
1112
// [error] (plugin / Compile / compileIncremental) java.lang.NoClassDefFoundError: com/sun/tools/javac/code/Symbol
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package benchmarks
2+
3+
import java.nio.charset.StandardCharsets
4+
import java.nio.file.FileSystems
5+
import java.nio.file.Files
6+
import java.nio.file.Path
7+
import java.nio.file.PathMatcher
8+
import java.util.concurrent.TimeUnit
9+
10+
import scala.meta.inputs.Input
11+
import scala.meta.internal.io.FileIO
12+
13+
import org.openjdk.jmh.annotations._
14+
import tests.DeleteVisitor
15+
import tests.Dependencies
16+
import tests.TestCompiler
17+
18+
@State(Scope.Benchmark)
19+
class CompileBench {
20+
21+
var deps: Dependencies = _
22+
var tmp: Path = _
23+
var compiler: TestCompiler = _
24+
var javaPattern: PathMatcher = _
25+
@Param(Array("bytebuddy", "guava"))
26+
var lib: String = _
27+
28+
val libs = Map(
29+
"bytebuddy" -> "net.bytebuddy:byte-buddy:1.10.20",
30+
"guava" -> "com.google.guava:guava:30.1-jre"
31+
)
32+
33+
@Setup()
34+
def setup(): Unit = {
35+
javaPattern = FileSystems.getDefault.getPathMatcher("glob:**.java")
36+
tmp = Files.createTempDirectory("benchmarks")
37+
deps = Dependencies.resolveDependencies(List(libs(lib)), Nil)
38+
compiler = new TestCompiler(deps.classpathSyntax, List.empty[String], tmp)
39+
}
40+
41+
@TearDown()
42+
def teardown(): Unit = {
43+
Files.walkFileTree(tmp, new DeleteVisitor)
44+
}
45+
46+
def foreachSource(fn: Input.VirtualFile => Int): Long = {
47+
var sum = 0L
48+
deps
49+
.sources
50+
.foreach { source =>
51+
FileIO.withJarFileSystem(source, create = false, close = true) { root =>
52+
val files =
53+
FileIO
54+
.listAllFilesRecursively(root)
55+
.iterator
56+
.filter(p => javaPattern.matches(p.toNIO))
57+
.toArray
58+
files.foreach { source =>
59+
val text = FileIO.slurp(source, StandardCharsets.UTF_8)
60+
val relativePath = source.toString().stripPrefix("/")
61+
val input = Input.VirtualFile(relativePath, text)
62+
sum += fn(input)
63+
}
64+
}
65+
}
66+
sum
67+
}
68+
69+
@Benchmark
70+
@BenchmarkMode(Array(Mode.SingleShotTime))
71+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
72+
def compile(): Long = {
73+
foreachSource { input =>
74+
compiler.compile(input.path, input.text).textDocument.getOccurrencesCount
75+
}
76+
}
77+
78+
@Benchmark
79+
@BenchmarkMode(Array(Mode.SingleShotTime))
80+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
81+
def compileSemanticdb(): Long = {
82+
foreachSource { input =>
83+
compiler
84+
.compileSemanticdb(input.path, input.text)
85+
.textDocument
86+
.getOccurrencesCount
87+
}
88+
}
89+
90+
}

tests/snapshots/src/main/scala/tests/LibrarySnapshotGenerator.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import java.nio.file.Files
66
import java.util.concurrent.atomic.AtomicInteger
77

88
import scala.collection.parallel.CollectionConverters._
9-
import scala.jdk.CollectionConverters._
109

1110
import scala.meta.internal.io.FileIO
1211
import scala.meta.io.AbsolutePath
@@ -35,7 +34,7 @@ class LibrarySnapshotGenerator extends SnapshotGenerator {
3534
println(s"indexing library '$name'")
3635
val deps = Dependencies
3736
.resolveDependencies(name :: provided.toList, repos)
38-
val options = List("-Xlint:none").asJava
37+
val options = List("-Xlint:none")
3938
val counter = new AtomicInteger()
4039
val targetroot = Files.createTempDirectory("semanticdb-javac")
4140

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package tests
2+
3+
import com.sourcegraph.semanticdb_javac.Semanticdb
4+
5+
case class CompileResult(
6+
byteCode: Array[Byte],
7+
stdout: String,
8+
textDocument: Semanticdb.TextDocument,
9+
isSuccess: Boolean
10+
)

tests/unit/src/main/scala/tests/TestCompiler.java

Lines changed: 0 additions & 90 deletions
This file was deleted.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package tests
2+
3+
import java.io.StringWriter
4+
import java.nio.file.Files
5+
import java.nio.file.Path
6+
import java.util.Collections.singletonList
7+
import javax.tools.ToolProvider
8+
9+
import scala.collection.mutable.ListBuffer
10+
import scala.jdk.CollectionConverters._
11+
12+
import com.sourcegraph.semanticdb_javac.Semanticdb
13+
14+
object TestCompiler {
15+
private val PROCESSOR_PATH = System.getProperty("java.class.path")
16+
}
17+
18+
class TestCompiler(
19+
val classpath: String,
20+
val options: List[String],
21+
val targetroot: Path
22+
) {
23+
24+
val sourceroot = Files.createTempDirectory("semanticdb-javac")
25+
val compiler = ToolProvider.getSystemJavaCompiler
26+
val fileManager =
27+
new SimpleFileManager(compiler.getStandardFileManager(null, null, null))
28+
29+
def this(targetroot: Path) {
30+
this(TestCompiler.PROCESSOR_PATH, Nil, targetroot)
31+
}
32+
33+
def compileSemanticdb(
34+
relativePath: String,
35+
testSource: String
36+
): CompileResult = {
37+
compile(
38+
relativePath,
39+
testSource,
40+
List(
41+
String.format(
42+
"-Xplugin:semanticdb -verbose -text:on -sourceroot:%s -targetroot:%s",
43+
sourceroot,
44+
targetroot
45+
)
46+
)
47+
)
48+
}
49+
def compile(
50+
relativePath: String,
51+
testSource: String,
52+
extraArguments: List[String] = Nil
53+
): CompileResult = {
54+
val output = new StringWriter
55+
val source = sourceroot.resolve(relativePath)
56+
val compilationUnits = singletonList(
57+
new SimpleSourceFile(source, testSource)
58+
)
59+
val arguments = ListBuffer.empty[String]
60+
arguments += "-processorpath"
61+
arguments += TestCompiler.PROCESSOR_PATH
62+
arguments += "-classpath"
63+
arguments += classpath
64+
arguments ++= extraArguments
65+
arguments ++= options
66+
val task = compiler.getTask(
67+
output,
68+
fileManager,
69+
null,
70+
arguments.asJava,
71+
null,
72+
compilationUnits
73+
)
74+
val isSuccess = task.call()
75+
var bytecode = new Array[Byte](0)
76+
if (!fileManager.compiled.isEmpty)
77+
bytecode = fileManager.compiled.iterator.next.getCompiledBinaries
78+
var textDocument = Semanticdb.TextDocument.newBuilder.build
79+
val outputPath = targetroot
80+
.resolve("META-INF")
81+
.resolve("semanticdb")
82+
.resolve(relativePath + ".semanticdb")
83+
if (Files.isRegularFile(outputPath)) {
84+
val textDocuments = Semanticdb
85+
.TextDocuments
86+
.parseFrom(Files.readAllBytes(outputPath))
87+
if (textDocuments.getDocumentsCount > 0)
88+
textDocument = textDocuments.getDocuments(0)
89+
}
90+
val stdout = output.toString
91+
CompileResult(bytecode, stdout, textDocument, isSuccess)
92+
}
93+
}

0 commit comments

Comments
 (0)