Skip to content

Commit 57c0720

Browse files
committed
Limit the size of the ByteCodeRepository cache
I observed cases (eg Scaladoc tests) where we end up with 17k+ ClassNodes, which makes 500 MB.
1 parent a4e71b1 commit 57c0720

File tree

4 files changed

+39
-5
lines changed

4 files changed

+39
-5
lines changed

src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ abstract class BCodeSkelBuilder extends BCodeHelpers {
133133

134134
if (settings.YoptInlinerEnabled) {
135135
// The inliner needs to find all classes in the code repo, also those being compiled
136-
byteCodeRepository.classes(cnode.name) = Some((cnode, ByteCodeRepository.CompilationUnit))
136+
byteCodeRepository.add(cnode, ByteCodeRepository.CompilationUnit)
137137
}
138138

139139
assert(cd.symbol == claszSymbol, "Someone messed up BCodePhase.claszSymbol during genPlainClass().")

src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,8 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes {
413413
// phase travel required, see implementation of `compiles`. for nested classes, it checks if the
414414
// enclosingTopLevelClass is being compiled. after flatten, all classes are considered top-level,
415415
// so `compiles` would return `false`.
416-
if (exitingPickler(currentRun.compiles(classSym))) buildFromSymbol
416+
if (exitingPickler(currentRun.compiles(classSym))) buildFromSymbol // InlineInfo required for classes being compiled, we have to create the classfile attribute
417+
else if (!inlinerEnabled) BTypes.EmptyInlineInfo // For other classes, we need the InlineInfo only inf the inliner is enabled.
417418
else {
418419
// For classes not being compiled, the InlineInfo is read from the classfile attribute. This
419420
// fixes an issue with mixed-in methods: the mixin phase enters mixin methods only to class

src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import OptimizerReporting._
1717
import BytecodeUtils._
1818
import ByteCodeRepository._
1919
import BTypes.InternalName
20+
import java.util.concurrent.atomic.AtomicLong
2021

2122
/**
2223
* The ByteCodeRepository provides utilities to read the bytecode of classfiles from the compilation
@@ -26,16 +27,48 @@ import BTypes.InternalName
2627
* @param classes Cache for parsed ClassNodes. Also stores the source of the bytecode:
2728
* [[Classfile]] if read from `classPath`, [[CompilationUnit]] if the bytecode
2829
* corresponds to a class being compiled.
30+
* The `Long` field encodes the age of the node in the map, which allows removing
31+
* old entries when the map grows too large.
2932
* For Java classes in mixed compilation, the map contains `None`: there is no
3033
* ClassNode generated by the backend and also no classfile that could be parsed.
3134
*/
32-
class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val classes: collection.concurrent.Map[InternalName, Option[(ClassNode, Source)]]) {
35+
class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val classes: collection.concurrent.Map[InternalName, Option[(ClassNode, Source, Long)]]) {
36+
37+
private val maxCacheSize = 1500
38+
private val targetSize = 500
39+
40+
private val idCounter = new AtomicLong(0)
41+
42+
/**
43+
* Prevent the code repository from growing too large. Profiling reveals that the average size
44+
* of a ClassNode is about 30 kb. I observed having 17k+ classes in the cache, i.e., 500 mb.
45+
*
46+
* We can only remove classes with `Source == Classfile`, those can be parsed again if requested.
47+
*/
48+
private def limitCacheSize(): Unit = {
49+
if (classes.count(c => c._2.isDefined && c._2.get._2 == Classfile) > maxCacheSize) {
50+
val removeId = idCounter.get - targetSize
51+
val toRemove = classes.iterator.collect({
52+
case (name, Some((_, Classfile, id))) if id < removeId => name
53+
}).toList
54+
toRemove foreach classes.remove
55+
}
56+
}
57+
58+
def add(classNode: ClassNode, source: Source) = {
59+
classes(classNode.name) = Some((classNode, source, idCounter.incrementAndGet()))
60+
}
61+
3362
/**
3463
* The class node and source for an internal name. If the class node is not yet available, it is
3564
* parsed from the classfile on the compile classpath.
3665
*/
3766
def classNodeAndSource(internalName: InternalName): Option[(ClassNode, Source)] = {
38-
classes.getOrElseUpdate(internalName, parseClass(internalName).map((_, Classfile)))
67+
val r = classes.getOrElseUpdate(internalName, {
68+
limitCacheSize()
69+
parseClass(internalName).map((_, Classfile, idCounter.incrementAndGet()))
70+
})
71+
r.map(v => (v._1, v._2))
3972
}
4073

4174
/**

test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class InlinerIllegalAccessTest extends ClearAfterClass {
3131
val compiler = InlinerIllegalAccessTest.compiler
3232
import compiler.genBCode.bTypes._
3333

34-
def addToRepo(cls: List[ClassNode]): Unit = for (c <- cls) byteCodeRepository.classes(c.name) = Some((c, ByteCodeRepository.Classfile))
34+
def addToRepo(cls: List[ClassNode]): Unit = for (c <- cls) byteCodeRepository.add(c, ByteCodeRepository.Classfile)
3535
def assertEmpty(ins: Option[AbstractInsnNode]) = for (i <- ins) throw new AssertionError(textify(i))
3636

3737
@Test

0 commit comments

Comments
 (0)