Skip to content

Commit f539802

Browse files
committed
Introduce CacheStore to store classfile, file and source caches
1 parent 8ff2fca commit f539802

File tree

4 files changed

+216
-36
lines changed

4 files changed

+216
-36
lines changed

compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,12 @@ import FileUtils.*
2222
* when there are a lot of projects having a lot of common dependencies.
2323
*/
2424
sealed trait ZipAndJarFileLookupFactory {
25-
private val cache = new FileBasedCache[ClassPath]
26-
2725
def create(zipFile: AbstractFile)(using Context): ClassPath =
2826
val release = Option(ctx.settings.javaOutputVersion.value).filter(_.nonEmpty)
2927
if (ctx.settings.YdisableFlatCpCaching.value || zipFile.file == null) createForZipFile(zipFile, release)
30-
else createUsingCache(zipFile, release)
28+
else ctx.cacheStore.classPaths(zipFile, createForZipFile(zipFile, release))
3129

3230
protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath
33-
34-
private def createUsingCache(zipFile: AbstractFile, release: Option[String]): ClassPath =
35-
cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile, release))
3631
}
3732

3833
/**
@@ -172,29 +167,3 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
172167

173168
override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath = ZipArchiveSourcePath(zipFile.file)
174169
}
175-
176-
final class FileBasedCache[T] {
177-
private case class Stamp(lastModified: FileTime, fileKey: Object)
178-
private val cache = collection.mutable.Map.empty[java.nio.file.Path, (Stamp, T)]
179-
180-
def getOrCreate(path: java.nio.file.Path, create: () => T): T = cache.synchronized {
181-
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
182-
val lastModified = attrs.lastModifiedTime()
183-
// only null on some platforms, but that's okay, we just use the last modified timestamp as our stamp
184-
val fileKey = attrs.fileKey()
185-
val stamp = Stamp(lastModified, fileKey)
186-
cache.get(path) match {
187-
case Some((cachedStamp, cached)) if cachedStamp == stamp => cached
188-
case _ =>
189-
val value = create()
190-
cache.put(path, (stamp, value))
191-
value
192-
}
193-
}
194-
195-
def clear(): Unit = cache.synchronized {
196-
// TODO support closing
197-
// cache.valuesIterator.foreach(_.close())
198-
cache.clear()
199-
}
200-
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package dotty.tools.dotc.core
2+
3+
import dotty.tools.dotc.core.Caches.{Cache, FileBasedCache, NoopCache}
4+
import dotty.tools.dotc.core.Names.TermName
5+
import dotty.tools.dotc.util.SourceFile
6+
import dotty.tools.io.{AbstractFile, ClassPath}
7+
8+
object CacheStores:
9+
10+
/** A store of caches used by the compiler.
11+
*
12+
* These caches can be shared across different runs.
13+
*
14+
* Set on a [[Context]] via `setCacheStore` and retrieved via `cacheStore`.
15+
*/
16+
trait CacheStore:
17+
def classPaths: Cache[AbstractFile, ClassPath]
18+
def files: Cache[TermName, AbstractFile]
19+
def sources: Cache[AbstractFile, SourceFile]
20+
21+
override def toString: String =
22+
s"""CacheStore(
23+
| classPaths = $classPaths,
24+
| files = $files,
25+
| sources = $sources
26+
|)""".stripMargin
27+
28+
/** Default, per-run cache store implementation. */
29+
object DefaultCacheStore extends CacheStore:
30+
31+
/** A unique global cache for classpaths, shared across all runs.
32+
*
33+
* This instance is thread-safe.
34+
*/
35+
val classPaths = FileBasedCache()
36+
37+
/** By default, we do not cache files across runs.
38+
*
39+
* Regardless, files are always cached within a single run via
40+
* `ContextBase.files`. See also `Context.getFile`.
41+
*/
42+
val files = NoopCache()
43+
44+
/** By default, we do not cache source files across runs.
45+
*
46+
* Regardless, source files are always cached within a single run via
47+
* `ContextBase.sources`. See also `Context.getSource`.
48+
*/
49+
val sources = NoopCache()
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package dotty.tools.dotc.core
2+
3+
import java.nio.file.Files
4+
import java.nio.file.Path
5+
import java.nio.file.attribute.{BasicFileAttributes, FileTime}
6+
import java.util.concurrent.ConcurrentHashMap
7+
import java.util.concurrent.atomic.LongAdder
8+
9+
import scala.collection.mutable.Map
10+
11+
import dotty.tools.io.AbstractFile
12+
13+
object Caches:
14+
15+
/** Cache for values of type `V`, associated with keys of type `K`. */
16+
trait Cache[K, V]:
17+
def apply(key: K, value: => V): V
18+
def stats(): CacheStats
19+
override def toString: String =
20+
s"${this.getClass.getSimpleName}(stats() = ${stats()})"
21+
22+
/** Statistics about a cache */
23+
final class CacheStats(total: Long, misses: Long, uncached: Long):
24+
val hits: Long = total - misses - uncached
25+
26+
override def toString: String =
27+
s"(total = $total, hits = $hits, misses = $misses, uncached = $uncached)"
28+
29+
/** A no-op cache implementation that does not cache anything. */
30+
final class NoopCache[K, V] extends Cache[K, V]:
31+
private var total = 0L
32+
33+
def apply(key: K, value: => V): V =
34+
total += 1
35+
value
36+
37+
def stats(): CacheStats =
38+
CacheStats(total, misses = 0, uncached = total)
39+
40+
/** A thread-unsafe cache implementation based on a mutable [[Map]].
41+
*
42+
* Entries are not evicted.
43+
*
44+
* @param getStamp
45+
* Function to obtain a stamp for a given key. If the function returns
46+
* `None`, no caching is performed for that key. If the function returns
47+
* `Some(stamp)`, the stamp is used to validate cached entries: cache
48+
* values are only reused if the stamp matches the cached stamp.
49+
*/
50+
final class MapCache[K, S, V](getStamp: K => Option[S]) extends Cache[K, V]:
51+
private val map = Map.empty[K, (S, V)]
52+
private var total = 0L
53+
private var misses = 0L
54+
private var uncached = 0L
55+
56+
def apply(key: K, value: => V): V =
57+
total += 1
58+
getStamp(key) match
59+
case None =>
60+
uncached += 1
61+
value
62+
case Some(stamp) =>
63+
map.get(key) match
64+
case Some((cachedStamp, cachedValue)) if cachedStamp == stamp =>
65+
cachedValue
66+
case _ =>
67+
misses += 1
68+
val v = value
69+
map.put(key, (stamp, v))
70+
v
71+
72+
def stats(): CacheStats =
73+
CacheStats(total, misses, uncached)
74+
75+
/** A thread-safe cache implementation based on a Java [[ConcurrentHashMap]].
76+
*
77+
* Entries are not evicted.
78+
*/
79+
final class SynchronizedMapCache[K, S, V](getStamp: K => Option[S]) extends Cache[K, V]:
80+
private val map = ConcurrentHashMap[K, (S, V)]()
81+
private val total = LongAdder()
82+
private val misses = LongAdder()
83+
private val uncached = LongAdder()
84+
85+
def apply(key: K, value: => V): V =
86+
total.increment()
87+
getStamp(key) match
88+
case None =>
89+
uncached.increment()
90+
value
91+
case Some(stamp) =>
92+
map.compute(
93+
key,
94+
(_, cached) =>
95+
if cached != null && cached._1 == stamp then
96+
cached
97+
else
98+
misses.increment()
99+
(stamp, value)
100+
)._2
101+
102+
def stats(): CacheStats =
103+
CacheStats(total.longValue(), misses.longValue(), uncached.longValue())
104+
105+
/** A cache where keys are [[AbstractFile]]s.
106+
*
107+
* The cache uses file modification time and file key (inode) as stamp to
108+
* invalidate cached entries when the underlying file has changed.
109+
*
110+
* For files with an underlying source (e.g. files inside a zip/jar), the
111+
* stamp is obtained from the underlying source file.
112+
*
113+
* If the [[AbstractFile]] does not correspond to a physical file on disk, no
114+
* caching is performed.
115+
*
116+
* See https://github.com/scala/bug/issues/10295 for discussion about the
117+
* invalidation strategy.
118+
*/
119+
final class FileBasedCache[V]() extends Cache[AbstractFile, V]:
120+
private case class FileStamp(lastModified: FileTime, fileKey: Object)
121+
122+
private def getFileStamp(abstractFile: AbstractFile): Option[FileStamp] =
123+
abstractFile.underlyingSource match
124+
case Some(underlyingSource) if underlyingSource ne abstractFile =>
125+
getFileStamp(underlyingSource)
126+
case _ =>
127+
val javaFile = abstractFile.file
128+
if javaFile == null then
129+
None
130+
else
131+
val attrs = Files.readAttributes(javaFile.toPath, classOf[BasicFileAttributes])
132+
val lastModified = attrs.lastModifiedTime()
133+
// This can be `null` on some platforms, but that's okay, we just use
134+
// the last modified timestamp as our stamp in that case.
135+
val fileKey = attrs.fileKey()
136+
Some(FileStamp(lastModified, fileKey))
137+
138+
private val underlying = SynchronizedMapCache[AbstractFile, FileStamp, V](getFileStamp)
139+
140+
def apply(key: AbstractFile, value: => V): V =
141+
underlying(key, value)
142+
143+
def stats(): CacheStats =
144+
underlying.stats()
145+
146+
override def toString: String =
147+
s"FileBasedCache(${underlying.toString})"
148+
149+

compiler/src/dotty/tools/dotc/core/Contexts.scala

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dotc
33
package core
44

55
import interfaces.CompilerCallback
6+
import CacheStores.{CacheStore, DefaultCacheStore}
67
import Decorators.*
78
import Periods.*
89
import Names.*
@@ -57,8 +58,9 @@ object Contexts {
5758
private val (importInfoLoc, store9) = store8.newLocation[ImportInfo | Null]()
5859
private val (typeAssignerLoc, store10) = store9.newLocation[TypeAssigner](TypeAssigner)
5960
private val (progressCallbackLoc, store11) = store10.newLocation[ProgressCallback | Null]()
61+
private val (cacheStoreLoc, store12) = store11.newLocation[CacheStore](DefaultCacheStore)
6062

61-
private val initialStore = store11
63+
private val initialStore = store12
6264

6365
/** The current context */
6466
inline def ctx(using ctx: Context): Context = ctx
@@ -189,6 +191,8 @@ object Contexts {
189191
val local = progressCallback
190192
if local != null then op(local)
191193

194+
def cacheStore: CacheStore = store(cacheStoreLoc)
195+
192196
/** The current plain printer */
193197
def printerFn: Context => Printer = store(printerFnLoc)
194198

@@ -256,7 +260,9 @@ object Contexts {
256260
/** Sourcefile corresponding to given abstract file, memoized */
257261
def getSource(file: AbstractFile, codec: => Codec = Codec(settings.encoding.value)) = {
258262
util.Stats.record("Context.getSource")
259-
base.sources.getOrElseUpdate(file, SourceFile(file, codec))
263+
// `base.sources` is run-local (it is reset at the beginning of each run),
264+
// while `cacheStore.sources` can cache files across runs.
265+
base.sources.getOrElseUpdate(file, cacheStore.sources(file, SourceFile(file, codec)))
260266
}
261267

262268
/** SourceFile with given path name, memoized */
@@ -273,7 +279,9 @@ object Contexts {
273279
file
274280
case None =>
275281
try
276-
val file = new PlainFile(Path(name.toString))
282+
// `base.files` is run-local (it is reset at the beginning of each run),
283+
// while `cacheStore.files` can cache files across runs.
284+
val file = cacheStore.files(name, new PlainFile(Path(name.toString)))
277285
base.files(name) = file
278286
file
279287
catch
@@ -712,6 +720,7 @@ object Contexts {
712720
def setCompilerCallback(callback: CompilerCallback): this.type = updateStore(compilerCallbackLoc, callback)
713721
def setIncCallback(callback: IncrementalCallback): this.type = updateStore(incCallbackLoc, callback)
714722
def setProgressCallback(callback: ProgressCallback): this.type = updateStore(progressCallbackLoc, callback)
723+
def setCacheStore(cacheStore: CacheStore): this.type = updateStore(cacheStoreLoc, cacheStore)
715724
def setPrinterFn(printer: Context => Printer): this.type = updateStore(printerFnLoc, printer)
716725
def setSettings(settingsState: SettingsState): this.type = updateStore(settingsStateLoc, settingsState)
717726
def setRun(run: Run | Null): this.type = updateStore(runLoc, run)
@@ -990,7 +999,11 @@ object Contexts {
990999
private var _nextSymId: Int = 0
9911000
def nextSymId: Int = { _nextSymId += 1; _nextSymId }
9921001

993-
/** Sources and Files that were loaded */
1002+
/** Sources and Files that were loaded.
1003+
*
1004+
* Those are intra-run caches. See also [[CacheStore.sources]] and
1005+
* [[CCacheStore.files]] for inter-run caching of source files and files.
1006+
*/
9941007
val sources: util.HashMap[AbstractFile, SourceFile] = util.HashMap[AbstractFile, SourceFile]()
9951008
val files: util.HashMap[TermName, AbstractFile] = util.HashMap()
9961009

0 commit comments

Comments
 (0)