diff --git a/compiler/src/dotty/tools/dotc/Driver.scala b/compiler/src/dotty/tools/dotc/Driver.scala index f8daabf3ec5d..4102f7b9ee86 100644 --- a/compiler/src/dotty/tools/dotc/Driver.scala +++ b/compiler/src/dotty/tools/dotc/Driver.scala @@ -70,6 +70,8 @@ class Driver { protected def command: CompilerCommand = ScalacCommand + protected def globalCache: GlobalCache = GlobalCache() + /** Setup context with initialized settings from CLI arguments, then check if there are any settings that * would change the default behaviour of the compiler. * @@ -81,6 +83,7 @@ class Driver { val ictx = rootCtx.fresh val summary = command.distill(args, ictx.settings)(ictx.settingsState)(using ictx) ictx.setSettings(summary.sstate) + ictx.setGlobalCache(globalCache) MacroClassLoader.init(ictx) Positioned.init(using ictx) diff --git a/compiler/src/dotty/tools/dotc/GlobalCache.scala b/compiler/src/dotty/tools/dotc/GlobalCache.scala new file mode 100644 index 000000000000..2ff9aae46222 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/GlobalCache.scala @@ -0,0 +1,62 @@ +package dotty.tools.dotc + +import java.nio.file.Files +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.FileTime + +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Decorators.em +import dotty.tools.dotc.report +import dotty.tools.io.{AbstractFile, ClassPath} + +trait GlobalCache: + def getOrCreateClassPath(key: AbstractFile, createValue: => ClassPath)(using Context): ClassPath + +/** Global cache that can be shared across [[Driver]] instances. + * + * This class is thread-safe. + */ +private final class GlobalCacheImpl() extends GlobalCache: + private val classPathCache = FileBasedCache[ClassPath]() + + def getOrCreateClassPath(key: AbstractFile, createValue: => ClassPath)(using Context): ClassPath = + classPathCache.getOrCreate(key.file.nn.toPath, () => createValue) + +object GlobalCache: + def apply(): GlobalCache = + GlobalCacheImpl() + + object NoGlobalCache extends GlobalCache: + def getOrCreateClassPath(key: AbstractFile, createValue: => ClassPath)(using Context): ClassPath = + report.configurationWarning(em"Not GlobalCache set") + createValue + +/** A cache for values associated with files on disk, that invalidates + * the cached value when the file is modified. + * + * See https://github.com/scala/bug/issues/10295 for some context on the + * invalidation strategy. + * + * Moved from [[ZipAndJarFileLookupFactory]] in December 2025. + * + * @author @allanrenucci + */ +private class FileBasedCache[T]: + private case class Stamp(lastModified: FileTime, fileKey: Object) + private val cache = collection.mutable.Map.empty[java.nio.file.Path, (Stamp, T)] + + def getOrCreate(path: java.nio.file.Path, create: () => T): T = + cache.synchronized: + val attrs = Files.readAttributes(path, classOf[BasicFileAttributes]) + val lastModified = attrs.lastModifiedTime() + // null on some platforms, but that's okay, we just use the last + // modified timestamp as our stamp in that case + val fileKey = attrs.fileKey() + val stamp = Stamp(lastModified, fileKey) + cache.get(path) match + case Some((cachedStamp, cached)) if cachedStamp == stamp => + cached + case _ => + val value = create() + cache.put(path, (stamp, value)) + value diff --git a/compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala b/compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala index d5473e6b26c3..a4b693873390 100644 --- a/compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala +++ b/compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala @@ -22,17 +22,12 @@ import FileUtils.* * when there are a lot of projects having a lot of common dependencies. */ sealed trait ZipAndJarFileLookupFactory { - private val cache = new FileBasedCache[ClassPath] - def create(zipFile: AbstractFile)(using Context): ClassPath = val release = Option(ctx.settings.javaOutputVersion.value).filter(_.nonEmpty) if (ctx.settings.YdisableFlatCpCaching.value || zipFile.file == null) createForZipFile(zipFile, release) - else createUsingCache(zipFile, release) + else ctx.globalCache.getOrCreateClassPath(zipFile, createForZipFile(zipFile, release)) protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath - - private def createUsingCache(zipFile: AbstractFile, release: Option[String]): ClassPath = - cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile, release)) } /** @@ -172,29 +167,3 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory { override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath = ZipArchiveSourcePath(zipFile.file) } - -final class FileBasedCache[T] { - private case class Stamp(lastModified: FileTime, fileKey: Object) - private val cache = collection.mutable.Map.empty[java.nio.file.Path, (Stamp, T)] - - def getOrCreate(path: java.nio.file.Path, create: () => T): T = cache.synchronized { - val attrs = Files.readAttributes(path, classOf[BasicFileAttributes]) - val lastModified = attrs.lastModifiedTime() - // only null on some platforms, but that's okay, we just use the last modified timestamp as our stamp - val fileKey = attrs.fileKey() - val stamp = Stamp(lastModified, fileKey) - cache.get(path) match { - case Some((cachedStamp, cached)) if cachedStamp == stamp => cached - case _ => - val value = create() - cache.put(path, (stamp, value)) - value - } - } - - def clear(): Unit = cache.synchronized { - // TODO support closing - // cache.valuesIterator.foreach(_.close()) - cache.clear() - } -} diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 9896cfaa6a1d..ebc1c156a41f 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -57,8 +57,9 @@ object Contexts { private val (importInfoLoc, store9) = store8.newLocation[ImportInfo | Null]() private val (typeAssignerLoc, store10) = store9.newLocation[TypeAssigner](TypeAssigner) private val (progressCallbackLoc, store11) = store10.newLocation[ProgressCallback | Null]() + private val (globalCacheLoc, store12) = store11.newLocation[GlobalCache]() - private val initialStore = store11 + private val initialStore = store12 /** The current context */ inline def ctx(using ctx: Context): Context = ctx @@ -189,6 +190,8 @@ object Contexts { val local = progressCallback if local != null then op(local) + def globalCache: GlobalCache = store(globalCacheLoc) + /** The current plain printer */ def printerFn: Context => Printer = store(printerFnLoc) @@ -712,6 +715,7 @@ object Contexts { def setCompilerCallback(callback: CompilerCallback): this.type = updateStore(compilerCallbackLoc, callback) def setIncCallback(callback: IncrementalCallback): this.type = updateStore(incCallbackLoc, callback) def setProgressCallback(callback: ProgressCallback): this.type = updateStore(progressCallbackLoc, callback) + def setGlobalCache(globalCache: GlobalCache): this.type = updateStore(globalCacheLoc, globalCache) def setPrinterFn(printer: Context => Printer): this.type = updateStore(printerFnLoc, printer) def setSettings(settingsState: SettingsState): this.type = updateStore(settingsStateLoc, settingsState) def setRun(run: Run | Null): this.type = updateStore(runLoc, run) @@ -775,6 +779,7 @@ object Contexts { .updated(notNullInfosLoc, Nil) .updated(compilationUnitLoc, NoCompilationUnit) .updated(profilerLoc, Profiler.NoOp) + .updated(globalCacheLoc, GlobalCache.NoGlobalCache) c._searchHistory = new SearchRoot c._gadtState = GadtState(GadtConstraint.empty) c diff --git a/compiler/test/dotty/tools/DottyTest.scala b/compiler/test/dotty/tools/DottyTest.scala index 76d2fdcb6d26..8bcc1d61a9d6 100644 --- a/compiler/test/dotty/tools/DottyTest.scala +++ b/compiler/test/dotty/tools/DottyTest.scala @@ -12,7 +12,7 @@ import dotc.core.Symbols._ import Types._, Symbols._, Decorators._ import dotc.core.Decorators._ import dotc.ast.tpd -import dotc.Compiler +import dotc.{Compiler, GlobalCache} import dotc.core.Phases.Phase @@ -20,6 +20,8 @@ trait DottyTest extends ContextEscapeDetection { dotc.parsing.Scanners // initialize keywords + private val globalCache: GlobalCache = GlobalCache() + implicit var ctx: Context = initialCtx protected def initialCtx: FreshContext = { @@ -42,6 +44,7 @@ trait DottyTest extends ContextEscapeDetection { fc.setSetting(fc.settings.classpath, TestConfiguration.basicClasspath) fc.setSetting(fc.settings.language, List("experimental.erasedDefinitions").asInstanceOf) fc.setProperty(ContextDoc, new ContextDocstrings) + fc.setGlobalCache(globalCache) } protected def defaultCompiler: Compiler = new Compiler() diff --git a/compiler/test/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactoryTest.scala b/compiler/test/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactoryTest.scala index db14ff3b1fb4..0dfdb676c26c 100644 --- a/compiler/test/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactoryTest.scala +++ b/compiler/test/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactoryTest.scala @@ -6,6 +6,7 @@ import org.junit.Test import java.nio.file._ import java.nio.file.attribute.FileTime +import dotty.tools.dotc.GlobalCache import dotty.tools.dotc.core.Contexts.{Context, ContextBase, ctx} import dotty.tools.io.AbstractFile @@ -18,7 +19,9 @@ class ZipAndJarFileLookupFactoryTest { val f = Files.createTempFile("test-", ".jar") Files.delete(f) - given Context = new ContextBase().initialCtx + val freshContext = new ContextBase().initialCtx.fresh + freshContext.setGlobalCache(GlobalCache()) + given Context = freshContext assert(!ctx.settings.YdisableFlatCpCaching.value) // we're testing with our JAR metadata caching enabled. def createCp = ZipAndJarClassPathFactory.create(AbstractFile.getFile(f)) diff --git a/sbt-bridge/src/dotty/tools/xsbt/CompilerBridge.java b/sbt-bridge/src/dotty/tools/xsbt/CompilerBridge.java index 6e2095a9df1e..3608e5da6336 100644 --- a/sbt-bridge/src/dotty/tools/xsbt/CompilerBridge.java +++ b/sbt-bridge/src/dotty/tools/xsbt/CompilerBridge.java @@ -5,6 +5,8 @@ package dotty.tools.xsbt; +import dotty.tools.dotc.GlobalCache; + import xsbti.AnalysisCallback; import xsbti.Logger; import xsbti.Reporter; @@ -15,10 +17,12 @@ import xsbti.compile.Output; public final class CompilerBridge implements CompilerInterface2 { + private final GlobalCache globalCache = GlobalCache.apply(); + @Override public void run(VirtualFile[] sources, DependencyChanges changes, String[] options, Output output, AnalysisCallback callback, Reporter delegate, CompileProgress progress, Logger log) { - CompilerBridgeDriver driver = new CompilerBridgeDriver(options, output); + CompilerBridgeDriver driver = new CompilerBridgeDriver(options, output, globalCache); driver.run(sources, callback, log, delegate, progress); } } diff --git a/sbt-bridge/src/dotty/tools/xsbt/CompilerBridgeDriver.java b/sbt-bridge/src/dotty/tools/xsbt/CompilerBridgeDriver.java index fbbe68235d13..9cfd1a7018f7 100644 --- a/sbt-bridge/src/dotty/tools/xsbt/CompilerBridgeDriver.java +++ b/sbt-bridge/src/dotty/tools/xsbt/CompilerBridgeDriver.java @@ -7,6 +7,7 @@ import dotty.tools.dotc.Compiler; import dotty.tools.dotc.Driver; +import dotty.tools.dotc.GlobalCache; import dotty.tools.dotc.ScalacCommand; import dotty.tools.dotc.config.Properties; import dotty.tools.dotc.core.Contexts; @@ -35,8 +36,9 @@ public class CompilerBridgeDriver extends Driver { private final String[] scalacOptions; private final String[] args; + private final GlobalCache globalCache; - public CompilerBridgeDriver(String[] scalacOptions, Output output) { + public CompilerBridgeDriver(String[] scalacOptions, Output output, GlobalCache globalCache) { super(); this.scalacOptions = scalacOptions; @@ -47,6 +49,12 @@ public CompilerBridgeDriver(String[] scalacOptions, Output output) { System.arraycopy(scalacOptions, 0, args, 0, scalacOptions.length); args[scalacOptions.length] = "-d"; args[scalacOptions.length + 1] = output.getSingleOutputAsPath().get().toAbsolutePath().toString(); + + this.globalCache = globalCache; + } + + public CompilerBridgeDriver(String[] scalacOptions, Output output) { + this(scalacOptions, output, GlobalCache.apply()); } private static final String StopInfoError = @@ -61,6 +69,11 @@ public boolean sourcesRequired() { return false; } + @Override + public GlobalCache globalCache() { + return this.globalCache; + } + private static VirtualFile asVirtualFile(SourceFile sourceFile, DelegatingReporter reporter, HashMap lookup) { return lookup.computeIfAbsent(sourceFile.file(), path -> {