Skip to content
Draft
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
36 changes: 35 additions & 1 deletion compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -471,10 +471,44 @@ class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader {
classfileParser.run()
}

object TastyLoader:

/** A cache of tasty bytes read from AbstractFiles to avoid reading and
* decompressing the same tasty file over multiple compiler runs.
*/
private val tastyBytesCache = collection.mutable.WeakHashMap[AbstractFile, (Array[Byte], Long)]()

/** Maximum number of files to cache tasty bytes for.
*
* Heuristic: cache entries up to 10% of max memory, assuming an average
* tasty file size of 10 KB (in the standard library, there are currently 936
* tasty files totalling ~6.9 MB, which is ~7.4 KB per (uncompressed) tasty
* file). For 1 GB max memory, this gives a cache size of ~10_000 entries,
* which should be more than enough in practice.
*
* This limit would probably only be reached when running many consecutive
* compilations in the same JVM (e.g. in a build server).
*/
private val maxTastyBytesCacheSize = Runtime.getRuntime.maxMemory() / 10 / 10_000

/** Get the bytes of the given tasty file, using the cache if possible. */
def getTastyBytes(tastyFile: AbstractFile): Array[Byte] =
tastyBytesCache.synchronized:
Copy link
Member

Choose a reason for hiding this comment

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

is an AbstractFile a good key for a weakhashmap? i.e. the same exact AbstractFile will be used over and over? I would think perhaps between runs the abstract file is GC'd possibly

Copy link
Member

Choose a reason for hiding this comment

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

I guess it depends on how dotty's zipclasspath does its own caching?

Copy link
Member Author

@mbovel mbovel Dec 3, 2025

Choose a reason for hiding this comment

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

I guess it depends on how dotty's zipclasspath does its own caching?

Yes exactly. This relies on classpaths being cached here:

/**
* A trait providing an optional cache for classpath entries obtained from zip and jar files.
* It allows us to e.g. reduce significantly memory used by PresentationCompilers in Scala IDE
* 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)
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))
}

Only in that case do we get stable AbstractFiles instances across runs.

I tried that for two reasons:

  • Absolute path and modified time are not enough as keys; we can get files with different content but same absolute path and modified time. It happens when loading classes from the JRT at least (that's why [Experiment] Global file content cache #24630 fails).
  • It's a good way to avoid memory leaks: we only retains content for AbstractFiles that are reachable. Otherwise the WeakMap will let the GC do its job, which is what we want! That's similar to Cache AbstractFile.toByteArray #24644, but with an external map instead of using a field. So we avoid caching too much with this solution, but the drawback of course is that we risk caching too little.

val modifiedTime = tastyFile.lastModified
tastyBytesCache.get(tastyFile) match
case Some((bytes, time)) if time == modifiedTime =>
bytes
case _ =>
val bytes = tastyFile.toByteArray
if tastyBytesCache.size >= maxTastyBytesCacheSize then
tastyBytesCache.clear()
tastyBytesCache.put(tastyFile, (bytes, modifiedTime))
bytes

class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
val isBestEffortTasty = tastyFile.hasBetastyExtension

lazy val tastyBytes = tastyFile.toByteArray
private def tastyBytes = TastyLoader.getTastyBytes(tastyFile)

private lazy val unpickler: tasty.DottyUnpickler =
handleUnpicklingExceptions:
Expand Down
Loading