Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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.

Copy link
Member Author

Choose a reason for hiding this comment

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

I followed your advice in my implementation of SynchronizedMapCache in #24780.


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
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