Skip to content

Commit b5658e7

Browse files
committed
Cache file bytes in a configurable cache
1 parent 320e5ea commit b5658e7

File tree

14 files changed

+127
-59
lines changed

14 files changed

+127
-59
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package dotty.tools.dotc
2+
3+
import java.util.concurrent.ConcurrentHashMap
4+
import java.util.concurrent.atomic.LongAdder
5+
6+
import scala.jdk.CollectionConverters.*
7+
8+
import dotty.tools.dotc.core.Contexts.Context
9+
import dotty.tools.io.{AbstractFile, FileExtension}
10+
11+
trait GlobalCache:
12+
/** Get the content of a file, possibly caching it globally.
13+
*
14+
* Implementations must be thread-safe.
15+
*/
16+
def getFileContent(file: AbstractFile): Array[Byte]
17+
18+
object GlobalCache:
19+
/** A global cache that keeps file contents in memory without any size limit.
20+
*
21+
* @param shouldCache
22+
* A predicate that determines whether an [[AbstracFile]] should be cached.
23+
*/
24+
class ConcurrentGlobalCache(shouldCache: AbstractFile => Boolean) extends GlobalCache:
25+
private val cache = ConcurrentHashMap[AbstractFile, Array[Byte]]()
26+
private val totalByExt = ConcurrentHashMap[FileExtension, LongAdder]()
27+
private val missesByExt = ConcurrentHashMap[FileExtension, LongAdder]()
28+
private val uncachedByExt = ConcurrentHashMap[FileExtension, LongAdder]()
29+
30+
override def getFileContent(file: AbstractFile): Array[Byte] =
31+
totalByExt.computeIfAbsent(file.ext, _ => LongAdder()).increment()
32+
if shouldCache(file) then
33+
cache.computeIfAbsent(file, f =>
34+
missesByExt.computeIfAbsent(file.ext, _ => LongAdder()).increment()
35+
//println(s"Caching file: ${file.canonicalPath}")
36+
f.toByteArray
37+
)
38+
else
39+
uncachedByExt.computeIfAbsent(file.ext, _ => LongAdder()).increment()
40+
file.toByteArray
41+
42+
final def printCacheStats(): Unit =
43+
println(this.getClass.getSimpleName + " statistics:")
44+
totalByExt.forEach: (ext, totalAdder) =>
45+
val misses = missesByExt.computeIfAbsent(ext, _ => LongAdder()).longValue()
46+
val uncached = uncachedByExt.computeIfAbsent(ext, _ => LongAdder()).longValue()
47+
val total = totalAdder.longValue()
48+
val hits = total - misses - uncached
49+
val files = cache.asScala.filter(_._1.ext == ext)
50+
val sizeMB = files.map(_._2.length.toLong).sum.toDouble / (1024 * 1024)
51+
println(f"- *.$ext: hits: $hits, misses: $misses, uncached: $uncached, total: $total, cache size: $sizeMB%.2f MB")
52+
53+
/** A global cache that does not cache anything.
54+
*
55+
* This is the default value for [[GlobalCache]].
56+
*/
57+
object NoGlobalCache extends GlobalCache:
58+
override def getFileContent(file: AbstractFile): Array[Byte] =
59+
file.toByteArray

compiler/src/dotty/tools/dotc/config/JavaPlatform.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,5 @@ class JavaPlatform extends Platform {
7575
new ClassfileLoader(bin)
7676

7777
def newTastyLoader(bin: AbstractFile)(using Context): SymbolLoader =
78-
new TastyLoader(bin)
78+
new TastyLoader(bin, ctx)
7979
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ object Contexts {
5757
private val (importInfoLoc, store9) = store8.newLocation[ImportInfo | Null]()
5858
private val (typeAssignerLoc, store10) = store9.newLocation[TypeAssigner](TypeAssigner)
5959
private val (progressCallbackLoc, store11) = store10.newLocation[ProgressCallback | Null]()
60+
private val (globalCacheLoc, store12) = store11.newLocation[GlobalCache]()
6061

61-
private val initialStore = store11
62+
private val initialStore = store12
6263

6364
/** The current context */
6465
inline def ctx(using ctx: Context): Context = ctx
@@ -189,6 +190,8 @@ object Contexts {
189190
val local = progressCallback
190191
if local != null then op(local)
191192

193+
def globalCache: GlobalCache = store(globalCacheLoc)
194+
192195
/** The current plain printer */
193196
def printerFn: Context => Printer = store(printerFnLoc)
194197

@@ -712,6 +715,7 @@ object Contexts {
712715
def setCompilerCallback(callback: CompilerCallback): this.type = updateStore(compilerCallbackLoc, callback)
713716
def setIncCallback(callback: IncrementalCallback): this.type = updateStore(incCallbackLoc, callback)
714717
def setProgressCallback(callback: ProgressCallback): this.type = updateStore(progressCallbackLoc, callback)
718+
def setGlobalCache(globalCache: GlobalCache): this.type = updateStore(globalCacheLoc, globalCache)
715719
def setPrinterFn(printer: Context => Printer): this.type = updateStore(printerFnLoc, printer)
716720
def setSettings(settingsState: SettingsState): this.type = updateStore(settingsStateLoc, settingsState)
717721
def setRun(run: Run | Null): this.type = updateStore(runLoc, run)
@@ -775,6 +779,7 @@ object Contexts {
775779
.updated(notNullInfosLoc, Nil)
776780
.updated(compilationUnitLoc, NoCompilationUnit)
777781
.updated(profilerLoc, Profiler.NoOp)
782+
.updated(globalCacheLoc, GlobalCache.NoGlobalCache)
778783
c._searchHistory = new SearchRoot
779784
c._gadtState = GadtState(GadtConstraint.empty)
780785
c

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,10 @@ class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader {
471471
classfileParser.run()
472472
}
473473

474-
class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
474+
class TastyLoader(val tastyFile: AbstractFile, creationContext: Context) extends SymbolLoader {
475475
val isBestEffortTasty = tastyFile.hasBetastyExtension
476476

477-
lazy val tastyBytes = tastyFile.toByteArray
477+
private def tastyBytes = creationContext.globalCache.getFileContent(tastyFile)
478478

479479
private lazy val unpickler: tasty.DottyUnpickler =
480480
handleUnpicklingExceptions:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ class ClassfileParser(
295295
throw new IOException(s"class file '${classfile.canonicalPath}' has location not matching its contents: contains class $className")
296296

297297
def run()(using Context): Option[Embedded] = try ctx.base.reusableDataReader.withInstance { reader =>
298-
implicit val reader2 = reader.reset(classfile)
298+
implicit val reader2: ReusableDataReader = reader.reset(classfile)(using ctx)
299299
report.debuglog("[class] >> " + classRoot.fullName)
300300
classfileVersion = parseHeader(classfile)
301301
this.pool = new ConstantPool

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class ClassfileTastyUUIDParser(classfile: AbstractFile)(ictx: Context) {
2828
private var classfileVersion: Header.Version = Header.Version.Unknown
2929

3030
def checkTastyUUID(tastyUUID: UUID)(using Context): Unit = try ctx.base.reusableDataReader.withInstance { reader =>
31-
implicit val reader2 = reader.reset(classfile)
31+
implicit val reader2: ReusableDataReader = reader.reset(classfile)
3232
this.classfileVersion = ClassfileParser.parseHeader(classfile)
3333
this.pool = new ConstantPool
3434
checkTastyAttr(tastyUUID)

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

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ package classfile
66
import java.io.{DataInputStream, InputStream}
77
import java.nio.{BufferUnderflowException, ByteBuffer}
88

9+
import dotty.tools.io.AbstractFile
10+
import dotty.tools.dotc.core.Contexts.{ctx, Context}
11+
912
final class ReusableDataReader() extends DataReader {
1013
private var data = new Array[Byte](32768)
1114
private var bb: ByteBuffer = ByteBuffer.wrap(data)
@@ -33,49 +36,17 @@ final class ReusableDataReader() extends DataReader {
3336

3437
private def nextPositivePowerOfTwo(target: Int): Int = 1 << -Integer.numberOfLeadingZeros(target - 1)
3538

36-
def reset(file: dotty.tools.io.AbstractFile): this.type = {
39+
def reset(file: AbstractFile)(using Context): this.type = {
3740
this.size = 0
38-
file.sizeOption match {
39-
case Some(size) =>
40-
if (size > data.length) {
41-
data = new Array[Byte](nextPositivePowerOfTwo(size))
42-
} else {
43-
java.util.Arrays.fill(data, 0.toByte)
44-
}
45-
val input = file.input
46-
try {
47-
var endOfInput = false
48-
while (!endOfInput) {
49-
val remaining = data.length - this.size
50-
if (remaining == 0) endOfInput = true
51-
else {
52-
val read = input.read(data, this.size, remaining)
53-
if (read < 0) endOfInput = true
54-
else this.size += read
55-
}
56-
}
57-
bb = ByteBuffer.wrap(data, 0, size)
58-
} finally {
59-
input.close()
60-
}
61-
case None =>
62-
val input = file.input
63-
try {
64-
var endOfInput = false
65-
while (!endOfInput) {
66-
val remaining = data.length - size
67-
if (remaining == 0) {
68-
data = java.util.Arrays.copyOf(data, nextPositivePowerOfTwo(size))
69-
}
70-
val read = input.read(data, this.size, data.length - this.size)
71-
if (read < 0) endOfInput = true
72-
else this.size += read
73-
}
74-
bb = ByteBuffer.wrap(data, 0, size)
75-
} finally {
76-
input.close()
77-
}
41+
val bytes = ctx.globalCache.getFileContent(file)
42+
val size = bytes.length
43+
if (size > data.length) {
44+
data = new Array[Byte](nextPositivePowerOfTwo(size))
45+
} else {
46+
java.util.Arrays.fill(data, 0.toByte)
7847
}
48+
System.arraycopy(bytes, 0, data, 0, size)
49+
bb = ByteBuffer.wrap(data, 0, size)
7950
this
8051
}
8152

compiler/src/dotty/tools/dotc/util/SourceFile.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,13 @@ object SourceFile {
304304
def isScript(file: AbstractFile | Null, content: Array[Char]): Boolean =
305305
ScriptSourceFile.hasScriptHeader(content)
306306

307-
def apply(file: AbstractFile | Null, codec: Codec): SourceFile =
307+
def apply(file: AbstractFile | Null, codec: Codec)(using Context): SourceFile =
308308
// Files.exists is slow on Java 8 (https://rules.sonarsource.com/java/tag/performance/RSPEC-3725),
309309
// so cope with failure.
310310
val chars =
311-
try new String(file.toByteArray, codec.charSet).toCharArray
311+
try
312+
val bytes = ctx.globalCache.getFileContent(file)
313+
new String(bytes, codec.charSet).toCharArray
312314
catch
313315
case _: FileSystemException => Array.empty[Char]
314316

compiler/src/dotty/tools/io/AbstractFile.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ abstract class AbstractFile extends Iterable[AbstractFile] {
171171
@throws(classOf[IOException])
172172
def toByteArray: Array[Byte] = {
173173
val in = input
174+
println("Reading file: " + this.canonicalPath)
175+
// Print stack trace to help track down file handle leaks
176+
//println(new Exception().getStackTrace().take(10).mkString("\n"))
174177
sizeOption match {
175178
case Some(size) =>
176179
var rest = size

compiler/test/dotty/tools/DottyTest.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ trait DottyTest extends ContextEscapeDetection {
4242
fc.setSetting(fc.settings.classpath, TestConfiguration.basicClasspath)
4343
fc.setSetting(fc.settings.language, List("experimental.erasedDefinitions").asInstanceOf)
4444
fc.setProperty(ContextDoc, new ContextDocstrings)
45+
fc.setGlobalCache(TestGlobalCache)
4546
}
4647

4748
protected def defaultCompiler: Compiler = new Compiler()

0 commit comments

Comments
 (0)