Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/Driver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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)

Expand Down
62 changes: 62 additions & 0 deletions compiler/src/dotty/tools/dotc/GlobalCache.scala
Original file line number Diff line number Diff line change
@@ -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)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if its no longer based on weak references, then can we used a concurrent map rather than synchronized?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or i guess you could want a per-key synchronization

Copy link
Member Author

@mbovel mbovel Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that it is a different cache than the one we discussed in #24650. This one already exists and caches ClassPath instances, not file contents. I just moved that code and haven't changed it.

There might be room for improvement, but it should be addressed separately. The questions I am trying to answer in this PR are just: can and should we make that global cache less global by explicitly passing instances to drivers.

But I agree it might be smarter to use a specialized concurrent map.


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
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

/**
Expand Down Expand Up @@ -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()
}
}
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion compiler/test/dotty/tools/DottyTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ 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

trait DottyTest extends ContextEscapeDetection {

dotc.parsing.Scanners // initialize keywords

private val globalCache: GlobalCache = GlobalCache()

implicit var ctx: Context = initialCtx

protected def initialCtx: FreshContext = {
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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))
Expand Down
6 changes: 5 additions & 1 deletion sbt-bridge/src/dotty/tools/xsbt/CompilerBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package dotty.tools.xsbt;

import dotty.tools.dotc.GlobalCache;

import xsbti.AnalysisCallback;
import xsbti.Logger;
import xsbti.Reporter;
Expand All @@ -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);
}
}
15 changes: 14 additions & 1 deletion sbt-bridge/src/dotty/tools/xsbt/CompilerBridgeDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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 =
Expand All @@ -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<AbstractFile, VirtualFile> lookup) {
return lookup.computeIfAbsent(sourceFile.file(), path -> {
Expand Down
Loading