Skip to content

Commit 0fe9168

Browse files
committed
Introduce CacheStore to store classfile, file and source caches
1 parent 8e8c8b9 commit 0fe9168

File tree

4 files changed

+218
-36
lines changed

4 files changed

+218
-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: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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 interface
16+
*
17+
* @tparam K
18+
* the type of keys
19+
* @tparam S
20+
* the type of stamps
21+
* @tparam V
22+
* the type of cached values
23+
*/
24+
trait Cache[K, V]:
25+
def apply(key: K, value: => V): V
26+
def stats(): CacheStats
27+
override def toString: String =
28+
s"${this.getClass.getSimpleName}(stats() = ${stats()})"
29+
30+
/** Statistics about a cache */
31+
final class CacheStats(total: Long, misses: Long, uncached: Long):
32+
val hits: Long = total - misses - uncached
33+
34+
override def toString: String =
35+
s"(total = $total, hits = $hits, misses = $misses, uncached = $uncached)"
36+
37+
/** A no-op cache implementation that does not cache anything. */
38+
final class NoopCache[K, V] extends Cache[K, V]:
39+
private var total = 0L
40+
41+
def apply(key: K, value: => V): V =
42+
total += 1
43+
value
44+
45+
def stats(): CacheStats =
46+
CacheStats(total, misses = 0, uncached = total)
47+
48+
/** A thread-unsafe cache implementation based on a mutable [[Map]].
49+
*
50+
* Entries are not evicted.
51+
*/
52+
final class MapCache[K, S, V](getStamp: K => Option[S]) extends Cache[K, V]:
53+
private val map = Map.empty[K, (S, V)]
54+
private var total = 0L
55+
private var misses = 0L
56+
private var uncached = 0L
57+
58+
def apply(key: K, value: => V): V =
59+
total += 1
60+
getStamp(key) match
61+
case None =>
62+
uncached += 1
63+
value
64+
case Some(stamp) =>
65+
map.get(key) match
66+
case Some((cachedStamp, cachedValue)) if cachedStamp == stamp =>
67+
cachedValue
68+
case _ =>
69+
misses += 1
70+
val v = value
71+
map.put(key, (stamp, v))
72+
v
73+
74+
def stats(): CacheStats =
75+
CacheStats(total, misses, uncached)
76+
77+
/** A thread-safe cache implementation based on a Java [[ConcurrentHashMap]].
78+
*
79+
* Entries are not evicted.
80+
*/
81+
final class SynchronizedMapCache[K, S, V](getStamp: K => Option[S]) extends Cache[K, V]:
82+
private val map = ConcurrentHashMap[K, (S, V)]()
83+
private val total = LongAdder()
84+
private val misses = LongAdder()
85+
private val uncached = LongAdder()
86+
87+
def apply(key: K, value: => V): V =
88+
total.increment()
89+
getStamp(key) match
90+
case None =>
91+
uncached.increment()
92+
value
93+
case Some(stamp) =>
94+
map.compute(
95+
key,
96+
(_, cached) =>
97+
if cached != null && cached._1 == stamp then
98+
cached
99+
else
100+
misses.increment()
101+
(stamp, value)
102+
)._2
103+
104+
def stats(): CacheStats =
105+
CacheStats(total.longValue(), misses.longValue(), uncached.longValue())
106+
107+
/** A cache where keys are [[AbstractFile]]s.
108+
*
109+
* The cache uses file modification time and file key (inode) as stamp to
110+
* invalidate cached entries when the underlying file has changed.
111+
*
112+
* For files with an underlying source (e.g. files inside a zip/jar), the
113+
* stamp is obtained from the underlying source file.
114+
*
115+
* If the [[AbstractFile]] does not correspond to a physical file on disk, no
116+
* caching is performed.
117+
*
118+
* See https://github.com/scala/bug/issues/10295 for discussion about the
119+
* invalidation strategy.
120+
*/
121+
final class FileBasedCache[V]() extends Cache[AbstractFile, V]:
122+
private case class FileStamp(lastModified: FileTime, fileKey: Object)
123+
124+
private def getFileStamp(abstractFile: AbstractFile): Option[FileStamp] =
125+
abstractFile.underlyingSource match
126+
case Some(underlyingSource) if underlyingSource ne abstractFile =>
127+
getFileStamp(underlyingSource)
128+
case _ =>
129+
val javaFile = abstractFile.file
130+
if javaFile == null then
131+
None
132+
else
133+
val attrs = Files.readAttributes(javaFile.toPath, classOf[BasicFileAttributes])
134+
val lastModified = attrs.lastModifiedTime()
135+
// This can be `null` on some platforms, but that's okay, we just use
136+
// the last modified timestamp as our stamp in that case.
137+
val fileKey = attrs.fileKey()
138+
Some(FileStamp(lastModified, fileKey))
139+
140+
private val underlying = SynchronizedMapCache[AbstractFile, FileStamp, V](getFileStamp)
141+
142+
def apply(key: AbstractFile, value: => V): V =
143+
underlying(key, value)
144+
145+
def stats(): CacheStats =
146+
underlying.stats()
147+
148+
override def toString: String =
149+
s"FileBasedCache(${underlying.toString})"
150+
151+

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+
* See also [[Caches.CacheStore.sourceFile]] and [[Caches.CacheStore.file]]
1005+
* 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)