Skip to content

Commit 18065cd

Browse files
committed
Add cache for class bytes
1 parent 9f7eae8 commit 18065cd

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
@@ -22,8 +22,23 @@ object Caches:
2222
* the type of cached values
2323
*/
2424
trait Cache[K, V]:
25+
26+
/** Get the value associated with `key` from the cache, or compute it using
27+
* the by-name parameter `value`.
28+
*
29+
* The value is cached iff `mightContain(key) == true`.
30+
*/
2531
def apply(key: K, value: => V): V
32+
33+
/** Check whether the cache might contain a value for `key`.
34+
*
35+
* `true` means that the cache will cache the value for `key` if requested
36+
* via [[apply]], not that it has already cached it.
37+
*/
38+
def mightContain(key: K): Boolean
39+
2640
def stats(): CacheStats
41+
2742
override def toString: String =
2843
s"${this.getClass.getSimpleName}(stats() = ${stats()})"
2944

@@ -42,6 +57,9 @@ object Caches:
4257
total += 1
4358
value
4459

60+
def mightContain(key: K): Boolean =
61+
false
62+
4563
def stats(): CacheStats =
4664
CacheStats(total, misses = 0, uncached = total)
4765

@@ -73,6 +91,9 @@ object Caches:
7391
map.put(key, (stamp, v))
7492
v
7593

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

@@ -103,6 +124,9 @@ object Caches:
103124
(stamp, value)
104125
)._2
105126

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

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

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

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

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

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

@@ -176,5 +209,8 @@ object Caches:
176209
uncached = baseStats.uncached + uncached.longValue()
177210
)
178211

212+
def mightContain(key: K): Boolean =
213+
shouldCache(key) && underlying.mightContain(key)
214+
179215
override def toString: String =
180216
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)