Skip to content

Commit

Permalink
Cache compilers in zinc nailgun instance
Browse files Browse the repository at this point in the history
  • Loading branch information
illicitonion committed Dec 24, 2018
1 parent ab1cd47 commit 3e798c1
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 223 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ def do_compile(self, invalidation_check, compile_contexts, classpath_product):
invalid_targets = [vt.target for vt in invalidation_check.invalid_vts]
valid_targets = [vt.target for vt in invalidation_check.all_vts if vt.valid]

if self.execution_strategy == self.HERMETIC:
if self.execution_strategy in {self.HERMETIC, self.HERMETIC_WITH_NAILGUN}:
self._set_direcotry_digests_for_valid_target_classpath_directories(valid_targets, compile_contexts)

for valid_target in valid_targets:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
from pants.base.exceptions import TaskError
from pants.base.hash_utils import hash_file
from pants.base.workunit import WorkUnitLabel
from pants.engine.fs import DirectoryToMaterialize
from pants.engine.isolated_process import ExecuteProcessRequest
from pants.engine.fs import Digest, DirectoryToMaterialize
from pants.engine.isolated_process import ExecuteProcessRequest, ProcessExecutionFailure
from pants.java.distribution.distribution import DistributionLocator
from pants.util.contextutil import open_zip
from pants.util.dirutil import fast_relpath, safe_open
Expand Down Expand Up @@ -209,7 +209,7 @@ def __init__(self, *args, **kwargs):
# Validate zinc options.
ZincCompile.validate_arguments(self.context.log, self.get_options().whitelisted_args,
self._args)
if self.execution_strategy == self.HERMETIC:
if self.execution_strategy in {self.HERMETIC, self.HERMETIC_WITH_NAILGUN}:
# TODO: Make incremental compiles work. See:
# hermetically https://github.com/pantsbuild/pants/issues/6517
if self.get_options().incremental:
Expand Down Expand Up @@ -296,7 +296,7 @@ def compile(self, ctx, args, dependency_classpath, upstream_analysis,
if self.get_options().capture_classpath:
self._record_compile_classpath(absolute_classpath, ctx.target, ctx.classes_dir.path)

self._verify_zinc_classpath(absolute_classpath, allow_dist=(self.execution_strategy != self.HERMETIC))
self._verify_zinc_classpath(absolute_classpath, allow_dist=(self.execution_strategy not in {self.HERMETIC, self.HERMETIC_WITH_NAILGUN}))
# TODO: Investigate upstream_analysis for hermetic compiles
self._verify_zinc_classpath(upstream_analysis.keys())

Expand All @@ -318,7 +318,7 @@ def relative_to_exec_root(path):

zinc_args = []
zinc_args.extend([
'-log-level', self.get_options().level,
'-log-level', 'warn',
'-analysis-cache', analysis_cache,
'-classpath', ':'.join(relative_classpath),
'-d', classes_dir,
Expand All @@ -328,6 +328,8 @@ def relative_to_exec_root(path):

compiler_bridge_classpath_entry = self._zinc.compile_compiler_bridge(self.context)
zinc_args.extend(['-compiled-bridge-jar', relative_to_exec_root(compiler_bridge_classpath_entry.path)])
zinc_args.extend(['-scala-compiler'] + [jar for jar in scala_path if 'scala-compiler' in jar])
zinc_args.extend(['-scala-library'] + [jar for jar in scala_path if 'scala-library' in jar])
zinc_args.extend(['-scala-path', ':'.join(scala_path)])

zinc_args.extend(self._javac_plugin_args(javac_plugin_map))
Expand Down Expand Up @@ -387,7 +389,7 @@ def relative_to_exec_root(path):
fp.write(arg)
fp.write(b'\n')

if self.execution_strategy == self.HERMETIC:
if self.execution_strategy in {self.HERMETIC, self.HERMETIC_WITH_NAILGUN}:
zinc_relpath = fast_relpath(self._zinc.zinc, get_buildroot())

snapshots = [
Expand All @@ -412,12 +414,16 @@ def relative_to_exec_root(path):
)

merged_input_digest = self.context._scheduler.merge_directories(
tuple(s.directory_digest for s in (snapshots)) + directory_digests
tuple(s.directory_digest for s in (snapshots)) + directory_digests + (Digest("a776d0d9c4281410e800232083c84622ff33943013905b036956b4a6c160413f", 80),)
)

# TODO: Extract something common from Executor._create_command to make the command line
# TODO: Lean on distribution for the bin/java appending here
argv = tuple(['.jdk/bin/java'] + jvm_options + ['-cp', zinc_relpath, Zinc.ZINC_COMPILE_MAIN] + zinc_args)
if self.execution_strategy == self.HERMETIC:
argv = tuple(['.jdk/bin/java'] + jvm_options + ['-cp', zinc_relpath, Zinc.ZINC_COMPILE_MAIN] + zinc_args)
else:
argv = tuple(['./ng', Zinc.ZINC_COMPILE_MAIN, '--nailgun-compiler-cache-dir', '/tmp/compiler-cache'] + zinc_args)

req = ExecuteProcessRequest(
argv=argv,
input_files=merged_input_digest,
Expand All @@ -427,7 +433,20 @@ def relative_to_exec_root(path):
# Since this is always hermetic, we need to use `underlying_dist`
jdk_home=text_type(self._zinc.underlying_dist.home),
)
res = self.context.execute_process_synchronously_or_raise(req, self.name(), [WorkUnitLabel.COMPILER])

retry_iteration = 0

while True:
try:
res = self.context.execute_process_synchronously_or_raise(req, self.name(), [WorkUnitLabel.COMPILER])
break
except ProcessExecutionFailure as e:
if e.exit_code == 227:
env = {'_retry_iteration': '{}'.format(retry_iteration)}
retry_iteration += 1
req = req.copy(env=env)
continue
raise

# TODO: Materialize as a batch in do_compile or somewhere
self.context._scheduler.materialize_directories((
Expand Down
3 changes: 2 additions & 1 deletion src/python/pants/backend/jvm/tasks/nailgun_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ class NailgunTaskBase(JvmToolTaskMixin, TaskBase):
NAILGUN = 'nailgun'
SUBPROCESS = 'subprocess'
HERMETIC = 'hermetic'
HERMETIC_WITH_NAILGUN = 'hermetic-with-nailgun'

@classmethod
def register_options(cls, register):
super(NailgunTaskBase, cls).register_options(register)
register('--execution-strategy', choices=[cls.NAILGUN, cls.SUBPROCESS, cls.HERMETIC], default=cls.NAILGUN,
register('--execution-strategy', choices=[cls.NAILGUN, cls.SUBPROCESS, cls.HERMETIC, cls.HERMETIC_WITH_NAILGUN], default=cls.NAILGUN,
help='If set to nailgun, nailgun will be enabled and repeated invocations of this '
'task will be quicker. If set to subprocess, then the task will be run without nailgun.')
register('--nailgun-timeout-seconds', advanced=True, default=10, type=float,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@ package org.pantsbuild.zinc.bootstrapper

import org.pantsbuild.buck.util.zip.ZipScrubber
import java.io.File
import xsbti.compile.{
ClasspathOptionsUtil,
ScalaInstance => XScalaInstance
}
import xsbti.compile.{ClasspathOptionsUtil, ScalaInstance}
import sbt.internal.inc.{AnalyzingCompiler, RawCompiler}
import sbt.util.Logger

object BootstrapperUtils {
val CompilerInterfaceId = "compiler-interface"
val JavaClassVersion = System.getProperty("java.class.version")

def compilerInterface(output: File, compilerBridgeSrc: File, compilerInterface: File, scalaInstance: XScalaInstance, log: Logger): Unit = {
def compilerInterface(output: File, compilerBridgeSrc: File, compilerInterface: File, scalaInstance: ScalaInstance, log: Logger): Unit = {
def compile(targetJar: File): Unit =
AnalyzingCompiler.compileSources(
Seq(compilerBridgeSrc),
Expand Down
8 changes: 7 additions & 1 deletion src/scala/org/pantsbuild/zinc/compiler/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ scala_library(
publication_metadata=pants_library('The SBT incremental compiler for nailgun')
),
dependencies=[
'3rdparty/jvm/com/github/scopt',
'3rdparty/jvm/com/martiansoftware:nailgun-server',
'3rdparty/jvm/org/scala-lang/modules:scala-java8-compat',
'3rdparty/jvm/org/scala-sbt:io',
Expand All @@ -18,10 +19,15 @@ scala_library(
'3rdparty:jsr305',
'src/scala/org/pantsbuild/zinc/analysis',
'src/scala/org/pantsbuild/zinc/cache',
'src/scala/org/pantsbuild/zinc/options',
'src/scala/org/pantsbuild/zinc/scalautil',
'src/scala/org/pantsbuild/zinc/util',
],
strict_deps=True,
platform='java8',
)

jvm_binary(
name = "nailgun-compiler-bin",
dependencies = [":compiler"],
main = "org.pantsbuild.zinc.compiler.Main",
)
71 changes: 71 additions & 0 deletions src/scala/org/pantsbuild/zinc/compiler/CompilerCache.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.pantsbuild.zinc.compiler

import com.google.common.hash.{HashCode, Hashing}
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.{FileAlreadyExistsException, Files, StandardCopyOption}
import java.util.concurrent.Callable
import org.pantsbuild.zinc.cache.Cache
import org.pantsbuild.zinc.compiler.CompilerUtils.newScalaCompiler
import org.pantsbuild.zinc.compiler.InputUtils.ScalaJars
import org.pantsbuild.zinc.scalautil.ScalaUtils
import sbt.internal.inc.ZincUtil
import xsbti.compile.{ClasspathOptionsUtil, Compilers}

class CompilerCache(limit: Int) {
val cache = Cache[HashCode, Compilers](limit)

def make(scalaJars: ScalaJars, javaHome: Option[File], compiledBridgeJar: DigestedFile): Compilers = {
val instance = ScalaUtils.scalaInstance(scalaJars.compiler.file, scalaJars.extra.map { _.file }, scalaJars.library.file)
ZincUtil.compilers(instance, ClasspathOptionsUtil.auto, javaHome, newScalaCompiler(instance, compiledBridgeJar.file))
}

def get(compilerCacheDir: File, scalaJars: ScalaJars, javaHome: Option[File], compiledBridgeJar: DigestedFile): Compilers = {
val cacheKeyBuilder = Hashing.sha256().newHasher();

for (file <- Seq(scalaJars.compiler, scalaJars.library) ++ scalaJars.extra) {
cacheKeyBuilder.putBytes(HashCode.fromString(file.digest.fingerprintHex).asBytes)
cacheKeyBuilder.putLong(file.digest.sizeBytes)
}

javaHome match {
case Some(file) => cacheKeyBuilder.putString(file.getCanonicalPath, StandardCharsets.UTF_8)
case None => {}
}

val cacheKey = cacheKeyBuilder.hash()
cache.get(cacheKey, new Callable[Compilers] { def call(): Compilers = {
val versionedCompilerCacheDir = new File(compilerCacheDir, cacheKey.toString)

val newScalaCompilerJar = CompilerCache.rename(scalaJars.compiler, versionedCompilerCacheDir, "scala-compiler")
val newScalaLibraryJar = CompilerCache.rename(scalaJars.library, versionedCompilerCacheDir, "scala-library")
val newScalaExtraJars = scalaJars.extra.map { CompilerCache.rename(_, versionedCompilerCacheDir, "scala-extra") }
val newCompiledBridgeJar = CompilerCache.rename(compiledBridgeJar, versionedCompilerCacheDir, "compiled-bridge")
if (!versionedCompilerCacheDir.exists) {
val tempVersionedCompilerCacheDir = compilerCacheDir.toPath
.resolve(s"${ cacheKey.toString }.tmp")
Files.createDirectories(tempVersionedCompilerCacheDir)
Files.copy(scalaJars.compiler.file.toPath, tempVersionedCompilerCacheDir.resolve(newScalaCompilerJar.file.getName))
Files.copy(scalaJars.library.file.toPath, tempVersionedCompilerCacheDir.resolve(newScalaLibraryJar.file.getName))
for ((oldExtra, newExtra) <- scalaJars.extra zip newScalaExtraJars)
Files.copy(oldExtra.file.toPath, tempVersionedCompilerCacheDir.resolve(newExtra.file.getName))
Files.copy(compiledBridgeJar.file.toPath, tempVersionedCompilerCacheDir.resolve(newCompiledBridgeJar.file.getName))
try {
Files.move(tempVersionedCompilerCacheDir, versionedCompilerCacheDir.toPath, StandardCopyOption.ATOMIC_MOVE)
} catch {
case _: FileAlreadyExistsException => {
// Ignore - trust that someone else atomically created the directory properly
}
}
}

make(ScalaJars(newScalaCompilerJar, newScalaLibraryJar, newScalaExtraJars), javaHome, newCompiledBridgeJar)
}})
}
}

object CompilerCache {
def rename(file: DigestedFile, dir: File, namePrefix: String): DigestedFile = {
DigestedFile(new File(dir, s"${namePrefix}-${file.digest.fingerprintHex}-${file.digest.sizeBytes}.jar"), Some(file.digest))
}
}
48 changes: 3 additions & 45 deletions src/scala/org/pantsbuild/zinc/compiler/CompilerUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,69 +6,27 @@ package org.pantsbuild.zinc.compiler

import java.io.File
import java.net.URLClassLoader
import sbt.internal.inc.{
AnalyzingCompiler,
CompileOutput,
IncrementalCompilerImpl,
RawCompiler,
ScalaInstance,
javac,
ZincUtil
}
import sbt.internal.inc.AnalyzingCompiler
import sbt.internal.inc.classpath.ClassLoaderCache
import sbt.io.Path
import sbt.io.syntax._
import sbt.util.Logger
import xsbti.compile.{
ClasspathOptionsUtil,
CompilerCache,
Compilers,
GlobalsCache,
Inputs,
JavaTools,
ScalaCompiler,
ScalaInstance => XScalaInstance,
ZincCompilerUtil
}

import scala.compat.java8.OptionConverters._

import org.pantsbuild.zinc.cache.Cache
import org.pantsbuild.zinc.cache.Cache.Implicits
import xsbti.compile.{ClasspathOptionsUtil, CompilerCache => XCompilerCache, GlobalsCache, ZincCompilerUtil, ScalaInstance}
import org.pantsbuild.zinc.util.Util

object CompilerUtils {
val JavaClassVersion = System.getProperty("java.class.version")

private val compilerCacheLimit = Util.intProperty("zinc.compiler.cache.limit", 5)
private val residentCacheLimit = Util.intProperty("zinc.resident.cache.limit", 0)

/**
* Static cache for resident scala compilers.
*/
private val residentCache: GlobalsCache = {
val maxCompilers = residentCacheLimit
if (maxCompilers <= 0)
CompilerCache.fresh
else
CompilerCache.createCacheFor(maxCompilers)
}

/**
* Cache of classloaders: see https://github.com/pantsbuild/pants/issues/4744
*/
private val classLoaderCache: Option[ClassLoaderCache] =
Some(new ClassLoaderCache(new URLClassLoader(Array())))

/**
* Get the instance of the GlobalsCache.
*/
def getGlobalsCache = residentCache

/**
* Create a new scala compiler.
*/
def newScalaCompiler(instance: XScalaInstance, bridgeJar: File): AnalyzingCompiler =
def newScalaCompiler(instance: ScalaInstance, bridgeJar: File): AnalyzingCompiler =
new AnalyzingCompiler(
instance,
ZincCompilerUtil.constantBridgeProvider(instance, bridgeJar),
Expand Down
50 changes: 50 additions & 0 deletions src/scala/org/pantsbuild/zinc/compiler/DigestedFile.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.pantsbuild.zinc.compiler

import com.google.common.hash.HashCode
import java.io.File
import java.nio.file.{Files, Path}
import java.security.MessageDigest
import org.pantsbuild.zinc.util.Util

case class Digest(fingerprintHex: String, sizeBytes: Long)

case class DigestedFile(file: File, private val _digest: Option[Digest] = None) extends scopt.Read[DigestedFile] {

lazy val digest = _digest.getOrElse { DigestedFile.digest(file.toPath) }

override def arity: Int = 1

override def reads: String => DigestedFile = DigestedFile.fromString

def normalise(relativeTo: File): DigestedFile = {
DigestedFile(Util.normalise(Some(relativeTo))(file), _digest)
}
}

object DigestedFile {
def digest(path: Path): Digest = {
val digest = MessageDigest.getInstance("SHA-256")
val bytes = Files.readAllBytes(path)
Digest(HashCode.fromBytes(digest.digest(bytes)).toString, bytes.size)
}

def fromString(s: String): DigestedFile = {
s.split("=", 2) match {
case arr if arr.size == 1 => {
val file = new File(arr(0))
DigestedFile(file, None)
}
case arr if arr.size == 2 => {
val file = new File(arr(0))
val digest = arr(1).split("-", 2) match {
case digestParts if digestParts.size == 2 => Digest(digestParts(0), digestParts(1).toLong)
case _ => throw new RuntimeException(s"Bad digest: ${arr(1)}")
}
DigestedFile(file, Some(digest))
}
}
}

implicit def digestedFileRead: scopt.Read[DigestedFile] = scopt.Read.stringRead.map { DigestedFile.fromString }

}
Loading

0 comments on commit 3e798c1

Please sign in to comment.