Skip to content
Open
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
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.cacheStore.classPaths(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()
}
}
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/config/JavaPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,5 @@ class JavaPlatform extends Platform {
new ClassfileLoader(bin)

def newTastyLoader(bin: AbstractFile)(using Context): SymbolLoader =
new TastyLoader(bin)
new TastyLoader(bin, ctx.cacheStore.tastyBytes)
}
59 changes: 59 additions & 0 deletions compiler/src/dotty/tools/dotc/core/CacheStores.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package dotty.tools.dotc.core

import dotty.tools.dotc.core.Caches.{Cache, FileBasedCache, NoopCache}
import dotty.tools.dotc.core.Names.TermName
import dotty.tools.dotc.util.SourceFile
import dotty.tools.io.{AbstractFile, ClassPath}

object CacheStores:

/** A store of caches used by the compiler.
*
* These caches can be shared across different runs.
*
* Set on a [[Context]] via `setCacheStore` and retrieved via `cacheStore`.
*/
trait CacheStore:
def classPaths: Cache[AbstractFile, ClassPath]
def files: Cache[TermName, AbstractFile]
def sources: Cache[AbstractFile, SourceFile]
def classBytes: Cache[AbstractFile, Array[Byte]]
def tastyBytes: Cache[AbstractFile, Array[Byte]]

override def toString: String =
s"""CacheStore(
| classPaths = $classPaths,
| files = $files,
| sources = $sources
| classBytes = $classBytes
| tastyBytes = $tastyBytes
|)""".stripMargin

/** Default, per-run cache store implementation. */
object DefaultCacheStore extends CacheStore:

/** A unique global cache for classpaths, shared across all runs.
*
* This instance is thread-safe.
*/
val classPaths = FileBasedCache()

/** By default, we do not cache files across runs.
*
* Regardless, files are always cached within a single run via
* `ContextBase.files`. See also `Context.getFile`.
*/
val files = NoopCache()

/** By default, we do not cache source files across runs.
*
* Regardless, source files are always cached within a single run via
* `ContextBase.sources`. See also `Context.getSource`.
*/
val sources = NoopCache()

/** By default, we do not cache class bytes. */
val classBytes = NoopCache()

/** By default, we do not cache tasty bytes. */
val tastyBytes = NoopCache()
222 changes: 222 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Caches.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package dotty.tools.dotc.core

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.{BasicFileAttributes, FileTime}
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.LongAdder

import scala.collection.mutable.Map

import dotty.tools.io.AbstractFile

object Caches:

/** Cache for values of type `V`, associated with keys of type `K`. */
trait Cache[K, V]:

/** Get the value associated with `key` from the cache, or compute it using
* the by-name parameter `value`.
*
* The value is cached iff `mightContain(key) == true`.
*/
def apply(key: K, value: => V): V

/** Check whether the cache might contain a value for `key`.
*
* `true` means that the cache will cache the value for `key` if requested
* via [[apply]], not that it has already cached it.
*/
def mightContain(key: K): Boolean

def stats(): CacheStats

override def toString: String =
s"${this.getClass.getSimpleName}(stats() = ${stats()})"

/** Statistics about a cache */
final case class CacheStats(total: Long, misses: Long, size: Long, uncached: Long):
val hits: Long = total - misses - uncached

override def toString: String =
s"(total = $total, hits = $hits, misses = $misses, size = $size, uncached = $uncached)"

/** A no-op cache implementation that does not cache anything. */
final class NoopCache[K, V] extends Cache[K, V]:
private var total = 0L

def apply(key: K, value: => V): V =
total += 1
value

def mightContain(key: K): Boolean =
false

def stats(): CacheStats =
CacheStats(total, misses = 0, size = 0, uncached = total)

/** Default value for stamp function that indicates no stamping. */
private def noStamp[K](key: K): Option[Unit] = Some(())

/** A thread-unsafe cache implementation based on a mutable [[Map]].
*
* Entries are not evicted.
*
* @param getStamp
* Function to obtain a stamp for a given key. If the function returns
* `None`, no caching is performed for that key. If the function returns
* `Some(stamp)`, the stamp is used to validate cached entries: cache
* values are only reused if the stamp matches the cached stamp.
*/
final class MapCache[K, S, V](getStamp: K => Option[S] = noStamp) extends Cache[K, V]:
private val map = Map.empty[K, (S, V)]
private var total = 0L
private var misses = 0L
private var uncached = 0L

def apply(key: K, value: => V): V =
total += 1
getStamp(key) match
case None =>
uncached += 1
value
case Some(stamp) =>
map.get(key) match
case Some((cachedStamp, cachedValue)) if cachedStamp == stamp =>
cachedValue
case _ =>
misses += 1
val v = value
map.put(key, (stamp, v))
v

def mightContain(key: K): Boolean =
getStamp(key).isDefined

def stats(): CacheStats =
CacheStats(total, misses, map.size, uncached)

/** A thread-safe cache implementation based on a Java [[ConcurrentHashMap]].
*
* Entries are not evicted.
*
* @param getStamp
* Function to obtain a stamp for a given key. If the function returns
* `None`, no caching is performed for that key. If the function returns
* `Some(stamp)`, the stamp is used to validate cached entries: cache
* values are only reused if the stamp matches the cached stamp.
*/
final class SynchronizedMapCache[K, S, V](getStamp: K => Option[S] = noStamp) extends Cache[K, V]:
private val map = ConcurrentHashMap[K, (S, V)]()
private val total = LongAdder()
private val misses = LongAdder()
private val uncached = LongAdder()

def apply(key: K, value: => V): V =
total.increment()
getStamp(key) match
case None =>
uncached.increment()
value
case Some(stamp) =>
map.compute(
key,
(_, cached) =>
if cached != null && cached._1 == stamp then
cached
else
misses.increment()
(stamp, value)
)._2

def mightContain(key: K): Boolean =
getStamp(key).isDefined

def stats(): CacheStats =
CacheStats(total.longValue(), misses.longValue(), map.size(), uncached.longValue())

/** A cache where keys are [[AbstractFile]]s.
*
* The cache uses file modification time and file key (inode) as stamp to
* invalidate cached entries when the underlying file has changed.
*
* For files with an underlying source (e.g. files inside a zip/jar), the
* stamp is obtained from the underlying source file.
*
* If the [[AbstractFile]] does not correspond to a physical file on disk, no
* caching is performed.
*
* See https://github.com/scala/bug/issues/10295 for discussion about the
* invalidation strategy.
*/
final class FileBasedCache[V]() extends Cache[AbstractFile, V]:
private case class FileStamp(lastModified: FileTime, fileKey: Object)

private def getPath(abstractFile: AbstractFile): Option[Path] =
abstractFile.underlyingSource match
case Some(underlyingSource) if underlyingSource ne abstractFile =>
getPath(underlyingSource)
case _ =>
val javaPath = abstractFile.jpath
if javaPath != null then Some(javaPath) else None

private def getFileStamp(abstractFile: AbstractFile): Option[FileStamp] =
getPath(abstractFile) match
case Some(path) =>
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
val lastModified = attrs.lastModifiedTime()
// This can be `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()
Some(FileStamp(lastModified, fileKey))
case None =>
None

private val underlying = SynchronizedMapCache[AbstractFile, FileStamp, V](getFileStamp)

def apply(key: AbstractFile, value: => V): V =
underlying(key, value)

def mightContain(key: AbstractFile): Boolean =
// We just check that a path exists here to avoi IO. `getFileStamp` will
// return `None` iff `getPath` returns `None`.
getPath(key).isDefined

def stats(): CacheStats =
underlying.stats()

override def toString: String =
s"FileBasedCache(${underlying.toString})"

/** Filtering cache wrapper that only caches values whose key satisfies a
* given predicate.
*
* @param underlying
* Underlying cache
* @param shouldCache
* Should the value associated with the given key should be cached?
*/
final class FilteringCache[K, V](underlying: Cache[K, V], shouldCache: K => Boolean) extends Cache[K, V]:
private val uncached = LongAdder()

def apply(key: K, value: => V): V =
if shouldCache(key) then
underlying(key, value)
else
uncached.increment()
value

def stats(): CacheStats =
val baseStats = underlying.stats()
CacheStats(
total = baseStats.total + uncached.longValue(),
misses = baseStats.misses,
size = baseStats.size,
uncached = baseStats.uncached + uncached.longValue()
)

def mightContain(key: K): Boolean =
shouldCache(key) && underlying.mightContain(key)

override def toString: String =
s"FilteringCache(${underlying.toString}, uncached = ${uncached.longValue()})"
Loading
Loading