From fa6987b5bff17640985ce0ab4d07440ef1189c45 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Fri, 27 Jul 2018 22:29:51 +0200 Subject: Make hot compilation 2x faster by properly reusing classloaders (#393) So far, Mill was caching ScalaInstance which contains a classloader but this is not enough: Zinc creates its own classloader by combining the ScalaInstance classloader with the path to the compiler-bridge. Zinc takes care of caching this classloader in each instance of ScalaCompiler. We can take advantage of this by caching an instance of Compilers since it contains everything we want to cache (ScalaCompiler and ScalaInstance). This significantly reduces the amount of classloading that happens at each compilation step, as measured by running: export _JAVA_OPTIONS="-XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+TraceClassUnloading" mill -i foo.compile Then looking at the output of `out/mill-worker-1/logs` while adding a new line in a source file in `foo` and running `mill -i foo.compile` again. The speedup is going to depend on the project, but I measured a ~2x improvement when running on the Mill build `time(core.compile())` and `rm -rf out/core/compile/` in a loop until results stabilized. See also the results that were obtained when a very similar issue was fixed in sbt itself: https://github.com/sbt/sbt/pull/2754 --- .../src/mill/scalalib/worker/ScalaWorker.scala | 43 ++++++++++++---------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/scalalib/worker/src/mill/scalalib/worker/ScalaWorker.scala b/scalalib/worker/src/mill/scalalib/worker/ScalaWorker.scala index 3e12565e..76f185c5 100644 --- a/scalalib/worker/src/mill/scalalib/worker/ScalaWorker.scala +++ b/scalalib/worker/src/mill/scalalib/worker/ScalaWorker.scala @@ -25,11 +25,14 @@ case class MockedLookup(am: File => Optional[CompileAnalysis]) extends PerClassp class ScalaWorker(ctx0: mill.util.Ctx, compilerBridgeClasspath: Array[String]) extends mill.scalalib.ScalaWorkerApi{ - @volatile var scalaInstanceCache = Option.empty[(Long, ScalaInstance)] - - def compileZincBridge(scalaVersion: String, - sourcesJar: Path, - compilerJars: Array[File]) = { + @volatile var compilersCache = Option.empty[(Long, Compilers)] + + /** Compile the bridge if it doesn't exist yet and return the output directory. + * TODO: Proper invalidation, see #389 + */ + def compileZincBridgeIfNeeded(scalaVersion: String, + sourcesJar: Path, + compilerJars: Array[File]): Path = { val workingDir = ctx0.dest / scalaVersion val compiledDest = workingDir / 'compiled if (!exists(workingDir)) { @@ -52,7 +55,6 @@ class ScalaWorker(ctx0: mill.util.Ctx, .get .invoke(null, argsArray) } - compiledDest } @@ -86,11 +88,14 @@ class ScalaWorker(ctx0: mill.util.Ctx, val compileClasspathFiles = compileClasspath.map(_.toIO).toArray val compilerJars = compilerClasspath.toArray.map(_.toIO) - val compilerBridge = compileZincBridge(scalaVersion, compilerBridgeSources, compilerJars) + val compilerBridge = compileZincBridgeIfNeeded(scalaVersion, compilerBridgeSources, compilerJars) + + val ic = new sbt.internal.inc.IncrementalCompilerImpl() - val scalaInstanceSig = compilerClasspath.map(p => p.toString().hashCode + p.mtime.toMillis).sum - val scalaInstance = scalaInstanceCache match{ - case Some((k, v)) if k == scalaInstanceSig => v + val compilerBridgeSig = compilerBridge.mtime.toMillis + val compilersSig = compilerBridgeSig + compilerClasspath.map(p => p.toString().hashCode + p.mtime.toMillis).sum + val compilers = compilersCache match { + case Some((k, v)) if k == compilersSig => v case _ => val scalaInstance = new ScalaInstance( version = scalaVersion, @@ -100,13 +105,18 @@ class ScalaWorker(ctx0: mill.util.Ctx, allJars = compilerJars, explicitActual = None ) - scalaInstanceCache = Some((scalaInstanceSig, scalaInstance)) - scalaInstance + val compilers = ic.compilers( + scalaInstance, + ClasspathOptionsUtil.boot, + None, + ZincUtil.scalaCompiler(scalaInstance, compilerBridge.toIO) + ) + compilersCache = Some((compilersSig, compilers)) + compilers } mkdir(ctx.dest) - val ic = new sbt.internal.inc.IncrementalCompilerImpl() val logger = { val consoleAppender = MainAppender.defaultScreen(ConsoleOut.printStreamOut( @@ -150,12 +160,7 @@ class ScalaWorker(ctx0: mill.util.Ctx, maxErrors = 10, sourcePositionMappers = Array(), order = CompileOrder.Mixed, - compilers = ic.compilers( - scalaInstance, - ClasspathOptionsUtil.boot, - None, - ZincUtil.scalaCompiler(scalaInstance, compilerBridge.toIO) - ), + compilers = compilers, setup = ic.setup( lookup, skip = false, -- cgit v1.2.3