From 35624e1fbc06ac4516faf73bda821e538e75658c Mon Sep 17 00:00:00 2001 From: Jaden Peterson Date: Fri, 12 Sep 2025 11:09:41 -0400 Subject: [PATCH 1/2] Use our own AbstractClassLoaderCache --- .../common/AnnexClassLoaderCacheImpl.scala | 34 +++++++++++++++++++ .../workers/zinc/compile/ZincRunner.scala | 6 +--- 2 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 src/main/scala/higherkindness/rules_scala/workers/common/AnnexClassLoaderCacheImpl.scala diff --git a/src/main/scala/higherkindness/rules_scala/workers/common/AnnexClassLoaderCacheImpl.scala b/src/main/scala/higherkindness/rules_scala/workers/common/AnnexClassLoaderCacheImpl.scala new file mode 100644 index 00000000..3410c6bc --- /dev/null +++ b/src/main/scala/higherkindness/rules_scala/workers/common/AnnexClassLoaderCacheImpl.scala @@ -0,0 +1,34 @@ +package higherkindness.rules_scala.workers.common + +import java.io.File +import java.net.URLClassLoader +import sbt.internal.inc.classpath.AbstractClassLoaderCache +import scala.collection.concurrent + +/** + * [[AnnexClassLoaderCacheImpl]] is mostly identical to [[sbt.internal.inc.classpath.ClassLoaderCacheImpl]], with a few + * exceptions: + * - It doesn't check the modification time of the compiler classpath files. This is because within the context of + * [[higherkindness.rules_scala.workers.zinc.compile.ZincRunner]], file paths being equal implies the contents of + * those files are equal and checking the modification time is unnecessary overhead. + * - It doesn't use [[java.lang.ref.SoftReference]]s, to minimize the reloading of the compiler + * - It doesn't do any locking + */ +class AnnexClassLoaderCacheImpl(override val commonParent: ClassLoader) extends AbstractClassLoaderCache { + private val delegate = new concurrent.TrieMap[List[File], ClassLoader]() + + override def apply(files: List[File]): ClassLoader = + cachedCustomClassloader(files, () => new URLClassLoader(files.map(_.toURI.toURL).toArray, commonParent)) + + override def cachedCustomClassloader(files: List[File], makeClassLoader: () => ClassLoader): ClassLoader = + delegate.getOrElseUpdate(files, makeClassLoader()) + + override def close(): Unit = { + delegate.values.foreach { + case classLoader: AutoCloseable => classLoader.close() + case _ => + } + + delegate.clear() + } +} diff --git a/src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala b/src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala index 09ba6a03..2be55a39 100644 --- a/src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala +++ b/src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala @@ -57,10 +57,8 @@ object ZincRunner extends WorkerMain[Unit] { // dynamic execution. The concurrency error happens very rarely, so it's hard to reproduce. override protected val mayInterruptWorkerTasks = false - private val classloaderCache = new ClassLoaderCache(new URLClassLoader(Array())) + private val classloaderCache = new ClassLoaderCache(new AnnexClassLoaderCacheImpl(new URLClassLoader(Array.empty))) - // prevents GC of the soft reference in classloaderCache - private var lastCompiler: AnyRef = null private def compileScala( task: WorkTask[Unit], parsedArguments: CommonArguments, @@ -99,8 +97,6 @@ object ZincRunner extends WorkerMain[Unit] { .scalaCompiler(scalaInstance, parsedArguments.compilerBridge) .withClassLoaderCache(classloaderCache) - lastCompiler = scalaCompiler - InterruptUtil.throwIfInterrupted(task.isCancelled) scalaCompiler.compile( From f72b917d79eb3077ef5b0ff5a2984820d6bac2c7 Mon Sep 17 00:00:00 2001 From: Jaden Peterson Date: Fri, 12 Sep 2025 11:09:51 -0400 Subject: [PATCH 2/2] Disable the xsbt-analyzer phase --- .../workers/zinc/compile/ZincRunner.scala | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala b/src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala index 2be55a39..6c2e663f 100644 --- a/src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala +++ b/src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala @@ -21,6 +21,7 @@ import sbt.internal.inc.classpath.ClassLoaderCache import sbt.internal.inc.javac.DiagnosticsReporter import sbt.internal.inc.{CompileOutput, PlainVirtualFile, PlainVirtualFileConverter, ZincUtil} import sbt.internal.util.LoggerWriter +import scala.collection.View import scala.jdk.CollectionConverters.* import scala.util.control.NonFatal import xsbti.compile.{DependencyChanges, ScalaInstance} @@ -81,17 +82,20 @@ object ZincRunner extends WorkerMain[Unit] { val shouldIncludeSourceRoot = !scalaInstance.actualVersion.startsWith("0.") && scalaInstance.actualVersion.startsWith("3") - val scalacOptions = - parsedArguments.plugins.view.map(p => s"-Xplugin:$p").toArray ++ - parsedArguments.compilerOptions ++ - parsedArguments.compilerOptionsReferencingPaths.toArray ++ + val scalacOptions = ( + // We don't use this phase, so we disable it to speed up compilation by a teeny tiny amount + View("-Yskip:xsbt-analyzer") ++ + parsedArguments.plugins.view.map(p => s"-Xplugin:$p") ++ + parsedArguments.compilerOptions.view ++ + parsedArguments.compilerOptionsReferencingPaths.view ++ ( if (shouldIncludeSourceRoot) { - Array("-sourceroot", task.workDir.toAbsolutePath.toString) + View("-sourceroot", task.workDir.toAbsolutePath.toString) } else { - Array.empty[String] + View.empty } ) + ).toArray val scalaCompiler = ZincUtil .scalaCompiler(scalaInstance, parsedArguments.compilerBridge)