Skip to content

Commit 254b87c

Browse files
committed
Add cache for class bytes
1 parent f17e93b commit 254b87c

File tree

4 files changed

+78
-14
lines changed

4 files changed

+78
-14
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ object CacheStores:
1717
def classPaths: Cache[AbstractFile, ClassPath]
1818
def files: Cache[TermName, AbstractFile]
1919
def sources: Cache[AbstractFile, SourceFile]
20+
def classBytes: Cache[AbstractFile, Array[Byte]]
2021

2122
override def toString: String =
2223
s"""CacheStore(
2324
| classPaths = $classPaths,
2425
| files = $files,
2526
| sources = $sources
27+
| classBytes = $classBytes
2628
|)""".stripMargin
2729

2830
/** Default, per-run cache store implementation. */
@@ -47,3 +49,9 @@ object CacheStores:
4749
* `ContextBase.sources`. See also `Context.getSource`.
4850
*/
4951
val sources = NoopCache()
52+
53+
/** By default, we do not cache class bytes across runs. */
54+
val classBytes = NoopCache()
55+
56+
/** By default, we do not cache tasty loaders. */
57+
val tastyLoaders = NoopCache()

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

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,23 @@ object Caches:
1414

1515
/** Cache for values of type `V`, associated with keys of type `K`. */
1616
trait Cache[K, V]:
17+
18+
/** Get the value associated with `key` from the cache, or compute it using
19+
* the by-name parameter `value`.
20+
*
21+
* The value is cached iff `mightContain(key) == true`.
22+
*/
1723
def apply(key: K, value: => V): V
24+
25+
/** Check whether the cache might contain a value for `key`.
26+
*
27+
* `true` means that the cache will cache the value for `key` if requested
28+
* via [[apply]], not that it has already cached it.
29+
*/
30+
def mightContain(key: K): Boolean
31+
1832
def stats(): CacheStats
33+
1934
override def toString: String =
2035
s"${this.getClass.getSimpleName}(stats() = ${stats()})"
2136

@@ -34,6 +49,9 @@ object Caches:
3449
total += 1
3550
value
3651

52+
def mightContain(key: K): Boolean =
53+
false
54+
3755
def stats(): CacheStats =
3856
CacheStats(total, misses = 0, uncached = total)
3957

@@ -72,6 +90,9 @@ object Caches:
7290
map.put(key, (stamp, v))
7391
v
7492

93+
def mightContain(key: K): Boolean =
94+
getStamp(key).isDefined
95+
7596
def stats(): CacheStats =
7697
CacheStats(total, misses, uncached)
7798

@@ -102,6 +123,9 @@ object Caches:
102123
(stamp, value)
103124
)._2
104125

126+
def mightContain(key: K): Boolean =
127+
getStamp(key).isDefined
128+
105129
def stats(): CacheStats =
106130
CacheStats(total.longValue(), misses.longValue(), uncached.longValue())
107131

@@ -122,27 +146,36 @@ object Caches:
122146
final class FileBasedCache[V]() extends Cache[AbstractFile, V]:
123147
private case class FileStamp(lastModified: FileTime, fileKey: Object)
124148

125-
private def getFileStamp(abstractFile: AbstractFile): Option[FileStamp] =
149+
private def getPath(abstractFile: AbstractFile): Option[Path] =
126150
abstractFile.underlyingSource match
127151
case Some(underlyingSource) if underlyingSource ne abstractFile =>
128-
getFileStamp(underlyingSource)
152+
getPath(underlyingSource)
129153
case _ =>
130-
val javaFile = abstractFile.file
131-
if javaFile == null then
132-
None
133-
else
134-
val attrs = Files.readAttributes(javaFile.toPath, classOf[BasicFileAttributes])
135-
val lastModified = attrs.lastModifiedTime()
136-
// This can be `null` on some platforms, but that's okay, we just use
137-
// the last modified timestamp as our stamp in that case.
138-
val fileKey = attrs.fileKey()
139-
Some(FileStamp(lastModified, fileKey))
154+
val javaPath = abstractFile.jpath
155+
if javaPath != null then Some(javaPath) else None
156+
157+
private def getFileStamp(abstractFile: AbstractFile): Option[FileStamp] =
158+
getPath(abstractFile) match
159+
case Some(path) =>
160+
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
161+
val lastModified = attrs.lastModifiedTime()
162+
// This can be `null` on some platforms, but that's okay, we just use
163+
// the last modified timestamp as our stamp in that case.
164+
val fileKey = attrs.fileKey()
165+
Some(FileStamp(lastModified, fileKey))
166+
case None =>
167+
None
140168

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

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

174+
def mightContain(key: AbstractFile): Boolean =
175+
// We just check that a path exists here to avoi IO. `getFileStamp` will
176+
// return `None` iff `getPath` returns `None`.
177+
getPath(key).isDefined
178+
146179
def stats(): CacheStats =
147180
underlying.stats()
148181

@@ -175,5 +208,8 @@ object Caches:
175208
uncached = baseStats.uncached + uncached.longValue()
176209
)
177210

211+
def mightContain(key: K): Boolean =
212+
shouldCache(key) && underlying.mightContain(key)
213+
178214
override def toString: String =
179215
s"FilteringCache(${underlying.toString}, uncached = ${uncached.longValue()})"

compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,17 @@ class ClassfileParser(
294294
private def mismatchError(className: SimpleName) =
295295
throw new IOException(s"class file '${classfile.canonicalPath}' has location not matching its contents: contains class $className")
296296

297-
def run()(using Context): Option[Embedded] = try ctx.base.reusableDataReader.withInstance { reader =>
298-
implicit val reader2 = reader.reset(classfile)
297+
def run()(using Context): Option[Embedded] =
298+
if ctx.cacheStore.classBytes.mightContain(classfile) then
299+
val bytes = ctx.cacheStore.classBytes(classfile, classfile.toByteArray)
300+
given DataReader = AbstractFileReader(bytes)
301+
runWithReader()
302+
else
303+
ctx.base.reusableDataReader.withInstance: reader =>
304+
given DataReader = reader.reset(classfile)
305+
runWithReader()
306+
307+
private def runWithReader()(using Context, DataReader): Option[Embedded] = try {
299308
report.debuglog("[class] >> " + classRoot.fullName)
300309
classfileVersion = parseHeader(classfile)
301310
this.pool = new ConstantPool

compiler/test/dotty/tools/TestCacheStore.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ object TestCacheStore extends CacheStore:
88
/** Use the default global classpath cache. */
99
val classPaths = DefaultCacheStore.classPaths
1010

11+
/** Standard library sources directory */
1112
private val stdLibDir = "library/src"
1213

1314
/** Cache files across runs, without invalidation. */
@@ -20,3 +21,13 @@ object TestCacheStore extends CacheStore:
2021
* a test run.
2122
*/
2223
val sources = FilteringCache(SynchronizedMapCache(), _.canonicalPath.startsWith(stdLibDir))
24+
25+
/** Test output directory */
26+
private val outDir = "out"
27+
28+
/** Cache class bytes across runs, except for classes in the `out` directory.
29+
*
30+
* Classes in the `out` directory are generated during tests, so we do not
31+
* want to cache them.
32+
*/
33+
val classBytes = FilteringCache(SynchronizedMapCache(), !_.canonicalPath.startsWith(outDir))

0 commit comments

Comments
 (0)