[New IC] Reduce memory usage of classpath snapshot cache

- Remove soft references that are too old.
 - Compute memory usage based on Runtime.totalMemory() instead of
   Runtime.maxMemory() as the latter is not reliable.
 - Remove debug logs that haven't proved to be useful.
 - Collect metrics on the number of cache hits (in addition to cache
   misses).

#KT-52329 In Progress

[New IC] Reduce memory usage of classpath snapshot cache

Use object interning for commonly shared objects. These include:
  - supertypes of classes
  - package names of classes

One experiment showed that with the above optimization, memory usage was
reduced from 660 MB down to 280 MB (+ 4 MB for the interning pool).

More aggressive object interning didn't reduce memory usage much
further, but would increase interning overhead and code complexity, so
we didn't do this to more objects.

Note that this commit optimizes the size of classpath snapshots in
memory, not their serialized data on disk. (I attempted the latter,
but the size was only reduced from 160 MB down to 130 MB, while the code
complexity became much higher as multiple interning pools would need to
be stored to disk and later loaded from disk, each per classpath entry
snapshot or shrunk classpath snapshot.)

#KT-52329 Fixed
This commit is contained in:
Hung Nguyen
2022-05-10 13:43:25 +01:00
committed by nataliya.valtman
parent 8b17a42866
commit 08e6eb07c1
4 changed files with 184 additions and 127 deletions
@@ -25,7 +25,9 @@ enum class BuildPerformanceMetric(val parent: BuildPerformanceMetric? = null, va
CLASSPATH_ENTRY_COUNT(parent = SHRINK_AND_SAVE_CLASSPATH_SNAPSHOT_EXECUTION_COUNT, "Number of classpath entries", type = SizeMetricType.NUMBER),
CLASSPATH_SNAPSHOT_SIZE(parent = SHRINK_AND_SAVE_CLASSPATH_SNAPSHOT_EXECUTION_COUNT, "Size of classpath snapshot", type = SizeMetricType.BYTES),
SHRUNK_CLASSPATH_SNAPSHOT_SIZE(parent = SHRINK_AND_SAVE_CLASSPATH_SNAPSHOT_EXECUTION_COUNT, "Size of shrunk classpath snapshot", type = SizeMetricType.BYTES),
LOAD_CLASSPATH_ENTRY_SNAPSHOT_CACHE_MISSES(parent = null, "Number of cache misses when loading classpath entry snapshots", type = SizeMetricType.NUMBER),
LOAD_CLASSPATH_SNAPSHOT_EXECUTION_COUNT(parent = null, "Number of times classpath snapshot is loaded", type = SizeMetricType.NUMBER),
LOAD_CLASSPATH_ENTRY_SNAPSHOT_CACHE_HITS(parent = LOAD_CLASSPATH_SNAPSHOT_EXECUTION_COUNT, "Number of cache hits when loading classpath entry snapshots", type = SizeMetricType.NUMBER),
LOAD_CLASSPATH_ENTRY_SNAPSHOT_CACHE_MISSES(parent = LOAD_CLASSPATH_SNAPSHOT_EXECUTION_COUNT, "Number of cache misses when loading classpath entry snapshots", type = SizeMetricType.NUMBER),
;
companion object {
@@ -5,11 +5,15 @@
package org.jetbrains.kotlin.incremental.classpathDiff
import com.intellij.util.containers.Interner
import com.intellij.util.io.DataExternalizer
import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric
import org.jetbrains.kotlin.incremental.KotlinClassInfo
import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import java.io.DataInput
import java.io.DataOutput
import java.io.File
@@ -20,21 +24,19 @@ object CachedClasspathSnapshotSerializer {
// Note: This cache is shared across builds, so we need to be careful if the snapshot file's path hasn't changed but its contents have
// changed. Luckily, each snapshot file is currently the output of a Gradle (non-incremental) transform, so that case will not happen.
// TODO: Make this code safer (not relying on how the snapshot files are produced and whether Gradle maintains the above guarantee). For
// example, if the transform is incremental, the above case may happen (the output directory of am incremental transform is unchanged
// even though its inputs/outputs have changed). Potential solutions: Write the file's content hash in the file's
// name or to another file next to it.
private val cache = InMemoryCacheWithEviction<File, ClasspathEntrySnapshot>(maxTimePeriods = 20, maxMemoryUsageRatio = 0.8)
// example, if the transform is incremental, the above case may happen (the output directory of an incremental transform is unchanged
// even though its inputs/outputs have changed). Potential solutions: Write the file's content hash in the file's name or to another
// file next to it, or check that its timestamp and size haven't changed (we'll need to deal with directories too).
private val cache = InMemoryCacheWithEviction<File, ClasspathEntrySnapshot>(
maxTimePeriodsToKeepStrongReferences = 20,
maxTimePeriodsToKeepSoftReferences = 1000,
maxMemoryUsageRatioToKeepStrongReferences = 0.8
)
fun load(classpathEntrySnapshotFiles: List<File>, reporter: ClasspathSnapshotBuildReporter): ClasspathSnapshot {
cache.newTimePeriod()
reporter.reportVerbose {
val counts = cache.countCacheEntriesForDebug()
@Suppress("SpellCheckingInspection")
"Load classpath snapshot, cache size = ${counts.first + counts.second + counts.third}" +
" (${counts.first} strong refs, ${counts.second + counts.third} soft refs, ${counts.third} are gc'd)"
}
var cacheMisses: Long = 0
var cacheMisses = 0L
val classpathSnapshot = ClasspathSnapshot(classpathEntrySnapshotFiles.map { snapshotFile ->
cache.computeIfAbsent(snapshotFile) {
cacheMisses++
@@ -43,14 +45,15 @@ object CachedClasspathSnapshotSerializer {
})
cache.evictEntries()
reporter.addMetric(BuildPerformanceMetric.LOAD_CLASSPATH_SNAPSHOT_EXECUTION_COUNT, 1)
reporter.addMetric(BuildPerformanceMetric.LOAD_CLASSPATH_ENTRY_SNAPSHOT_CACHE_HITS, classpathEntrySnapshotFiles.size - cacheMisses)
reporter.addMetric(BuildPerformanceMetric.LOAD_CLASSPATH_ENTRY_SNAPSHOT_CACHE_MISSES, cacheMisses)
reporter.reportVerbose { "Loaded classpath snapshot, cache misses = $cacheMisses / ${classpathEntrySnapshotFiles.size}" }
return classpathSnapshot
}
}
open class DataExternalizerForSealedClass<T>(
internal open class DataExternalizerForSealedClass<T>(
val baseClass: Class<T>,
val inheritorClasses: List<Class<out T>>,
val inheritorExternalizers: List<DataExternalizer<*>>
@@ -59,13 +62,13 @@ open class DataExternalizerForSealedClass<T>(
override fun save(output: DataOutput, objectToExternalize: T) {
val inheritorClassIndex =
inheritorClasses.indexOfFirst { it.isAssignableFrom(objectToExternalize!!::class.java) }.also { check(it != -1) }
output.writeInt(inheritorClassIndex)
output.writeByte(inheritorClassIndex.also { check(it <= Byte.MAX_VALUE) }) // Write byte so the data is smaller
@Suppress("UNCHECKED_CAST")
(inheritorExternalizers[inheritorClassIndex] as DataExternalizer<T>).save(output, objectToExternalize)
}
override fun read(input: DataInput): T {
val inheritorClassIndex = input.readInt()
val inheritorClassIndex = input.readByte().toInt()
@Suppress("UNCHECKED_CAST")
return inheritorExternalizers[inheritorClassIndex].read(input) as T
}
@@ -84,19 +87,19 @@ object ClasspathEntrySnapshotExternalizer : DataExternalizer<ClasspathEntrySnaps
}
}
object ClassSnapshotExternalizer : DataExternalizerForSealedClass<ClassSnapshot>(
internal object ClassSnapshotExternalizer : DataExternalizerForSealedClass<ClassSnapshot>(
baseClass = ClassSnapshot::class.java,
inheritorClasses = listOf(AccessibleClassSnapshot::class.java, InaccessibleClassSnapshot::class.java),
inheritorExternalizers = listOf(AccessibleClassSnapshotExternalizer, InaccessibleClassSnapshotExternalizer)
)
object AccessibleClassSnapshotExternalizer : DataExternalizerForSealedClass<AccessibleClassSnapshot>(
internal object AccessibleClassSnapshotExternalizer : DataExternalizerForSealedClass<AccessibleClassSnapshot>(
baseClass = AccessibleClassSnapshot::class.java,
inheritorClasses = listOf(KotlinClassSnapshot::class.java, JavaClassSnapshot::class.java),
inheritorExternalizers = listOf(KotlinClassSnapshotExternalizer, JavaClassSnapshotExternalizer)
)
object KotlinClassSnapshotExternalizer : DataExternalizerForSealedClass<KotlinClassSnapshot>(
private object KotlinClassSnapshotExternalizer : DataExternalizerForSealedClass<KotlinClassSnapshot>(
baseClass = KotlinClassSnapshot::class.java,
inheritorClasses = listOf(
RegularKotlinClassSnapshot::class.java,
@@ -110,7 +113,7 @@ object KotlinClassSnapshotExternalizer : DataExternalizerForSealedClass<KotlinCl
)
)
object RegularKotlinClassSnapshotExternalizer : DataExternalizer<RegularKotlinClassSnapshot> {
private object RegularKotlinClassSnapshotExternalizer : DataExternalizer<RegularKotlinClassSnapshot> {
override fun save(output: DataOutput, snapshot: RegularKotlinClassSnapshot) {
ClassIdExternalizer.save(output, snapshot.classId)
@@ -121,15 +124,16 @@ object RegularKotlinClassSnapshotExternalizer : DataExternalizer<RegularKotlinCl
override fun read(input: DataInput): RegularKotlinClassSnapshot {
return RegularKotlinClassSnapshot(
classId = ClassIdExternalizer.read(input),
// To reduce memory usage, apply object interning to classId's package name and supertypes as they are commonly shared
classId = ClassIdExternalizerWithInterning.read(input),
classAbiHash = LongExternalizer.read(input),
classMemberLevelSnapshot = NullableValueExternalizer(KotlinClassInfoExternalizer).read(input),
supertypes = ListExternalizer(JvmClassNameExternalizer).read(input)
supertypes = ListExternalizer(JvmClassNameExternalizerWithInterning).read(input)
)
}
}
object PackageFacadeKotlinClassSnapshotExternalizer : DataExternalizer<PackageFacadeKotlinClassSnapshot> {
private object PackageFacadeKotlinClassSnapshotExternalizer : DataExternalizer<PackageFacadeKotlinClassSnapshot> {
override fun save(output: DataOutput, snapshot: PackageFacadeKotlinClassSnapshot) {
ClassIdExternalizer.save(output, snapshot.classId)
@@ -140,7 +144,8 @@ object PackageFacadeKotlinClassSnapshotExternalizer : DataExternalizer<PackageFa
override fun read(input: DataInput): PackageFacadeKotlinClassSnapshot {
return PackageFacadeKotlinClassSnapshot(
classId = ClassIdExternalizer.read(input),
// To reduce memory usage, apply object interning to classId's package name as they are commonly shared
classId = ClassIdExternalizerWithInterning.read(input),
classAbiHash = LongExternalizer.read(input),
classMemberLevelSnapshot = NullableValueExternalizer(KotlinClassInfoExternalizer).read(input),
packageMemberNames = SetExternalizer(StringExternalizer).read(input)
@@ -148,7 +153,7 @@ object PackageFacadeKotlinClassSnapshotExternalizer : DataExternalizer<PackageFa
}
}
object MultifileClassKotlinClassSnapshotExternalizer : DataExternalizer<MultifileClassKotlinClassSnapshot> {
private object MultifileClassKotlinClassSnapshotExternalizer : DataExternalizer<MultifileClassKotlinClassSnapshot> {
override fun save(output: DataOutput, snapshot: MultifileClassKotlinClassSnapshot) {
ClassIdExternalizer.save(output, snapshot.classId)
@@ -159,7 +164,8 @@ object MultifileClassKotlinClassSnapshotExternalizer : DataExternalizer<Multifil
override fun read(input: DataInput): MultifileClassKotlinClassSnapshot {
return MultifileClassKotlinClassSnapshot(
classId = ClassIdExternalizer.read(input),
// To reduce memory usage, apply object interning to classId's package name as they are commonly shared
classId = ClassIdExternalizerWithInterning.read(input),
classAbiHash = LongExternalizer.read(input),
classMemberLevelSnapshot = NullableValueExternalizer(KotlinClassInfoExternalizer).read(input),
constantNames = SetExternalizer(StringExternalizer).read(input)
@@ -167,7 +173,7 @@ object MultifileClassKotlinClassSnapshotExternalizer : DataExternalizer<Multifil
}
}
object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo> {
internal object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo> {
override fun save(output: DataOutput, info: KotlinClassInfo) {
ClassIdExternalizer.save(output, info.classId)
@@ -181,7 +187,8 @@ object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo> {
override fun read(input: DataInput): KotlinClassInfo {
return KotlinClassInfo(
classId = ClassIdExternalizer.read(input),
// To reduce memory usage, apply object interning to classId's package name as they are commonly shared
classId = ClassIdExternalizerWithInterning.read(input),
classKind = KotlinClassHeader.Kind.getById(IntExternalizer.read(input)),
classHeaderData = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
classHeaderStrings = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
@@ -192,7 +199,7 @@ object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo> {
}
}
object JavaClassSnapshotExternalizer : DataExternalizer<JavaClassSnapshot> {
private object JavaClassSnapshotExternalizer : DataExternalizer<JavaClassSnapshot> {
override fun save(output: DataOutput, snapshot: JavaClassSnapshot) {
ClassIdExternalizer.save(output, snapshot.classId)
@@ -203,15 +210,16 @@ object JavaClassSnapshotExternalizer : DataExternalizer<JavaClassSnapshot> {
override fun read(input: DataInput): JavaClassSnapshot {
return JavaClassSnapshot(
classId = ClassIdExternalizer.read(input),
// To reduce memory usage, apply object interning to classId's package name and supertypes as they are commonly shared
classId = ClassIdExternalizerWithInterning.read(input),
classAbiHash = LongExternalizer.read(input),
classMemberLevelSnapshot = NullableValueExternalizer(JavaClassMemberLevelSnapshotExternalizer).read(input),
supertypes = ListExternalizer(JvmClassNameExternalizer).read(input)
supertypes = ListExternalizer(JvmClassNameExternalizerWithInterning).read(input)
)
}
}
object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer<JavaClassMemberLevelSnapshot> {
private object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer<JavaClassMemberLevelSnapshot> {
override fun save(output: DataOutput, snapshot: JavaClassMemberLevelSnapshot) {
JavaElementSnapshotExternalizer.save(output, snapshot.classAbiExcludingMembers)
@@ -228,7 +236,7 @@ object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer<JavaClassMemb
}
}
object JavaElementSnapshotExternalizer : DataExternalizer<JavaElementSnapshot> {
private object JavaElementSnapshotExternalizer : DataExternalizer<JavaElementSnapshot> {
override fun save(output: DataOutput, value: JavaElementSnapshot) {
StringExternalizer.save(output, value.name)
@@ -243,7 +251,7 @@ object JavaElementSnapshotExternalizer : DataExternalizer<JavaElementSnapshot> {
}
}
object InaccessibleClassSnapshotExternalizer : DataExternalizer<InaccessibleClassSnapshot> {
private object InaccessibleClassSnapshotExternalizer : DataExternalizer<InaccessibleClassSnapshot> {
override fun save(output: DataOutput, snapshot: InaccessibleClassSnapshot) {
// Nothing to save
@@ -253,3 +261,34 @@ object InaccessibleClassSnapshotExternalizer : DataExternalizer<InaccessibleClas
return InaccessibleClassSnapshot
}
}
private object ClassIdExternalizerWithInterning : DataExternalizer<ClassId> by ClassIdExternalizer {
override fun read(input: DataInput): ClassId {
return ClassId(
// To reduce memory usage, apply object interning to package name as they are commonly shared.
// (Don't apply object interning to relative class name as they are not commonly shared.)
/* packageFqName */ FqNameExternalizerWithInterning.read(input),
/* relativeClassName */ FqNameExternalizer.read(input),
/* isLocal */ input.readBoolean()
)
}
}
private object FqNameExternalizerWithInterning : DataExternalizer<FqName> by FqNameExternalizer {
private val fqNameInterner by lazy { Interner.createWeakInterner<FqName>() }
override fun read(input: DataInput): FqName {
return fqNameInterner.intern(FqNameExternalizer.read(input))
}
}
private object JvmClassNameExternalizerWithInterning : DataExternalizer<JvmClassName> by JvmClassNameExternalizer {
private val jvmClassNameInterner by lazy { Interner.createWeakInterner<JvmClassName>() }
override fun read(input: DataInput): JvmClassName {
return jvmClassNameInterner.intern(JvmClassNameExternalizer.read(input))
}
}
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.incremental.classpathDiff
import com.google.common.annotations.VisibleForTesting
import org.jetbrains.kotlin.incremental.classpathDiff.InMemoryCacheWithEviction.EntryState.*
import org.jetbrains.kotlin.utils.ThreadSafe
import java.lang.ref.SoftReference
import java.util.concurrent.ConcurrentHashMap
@@ -17,35 +18,43 @@ import javax.annotation.concurrent.NotThreadSafe
* In-memory cache that uses a combination of strong references and [SoftReference]s so that it adapts to memory availability.
*
* Cache eviction is performed when a user of this cache calls [evictEntries]. Evicted cache entries' values will be converted from strong
* references into [SoftReference]s so that they can still be used for as long as the JVM allows them.
* references to [SoftReference]s first. After that, they will either become strong references again if they are used again or get
* garbage collected/removed from this cache at some point.
*
* There are 2 types of cache eviction:
* - Least recently used: Oldest entries will be evicted (see [maxTimePeriods])
* - Memory usage limit: If memory is limited, all entries will be evicted (see [maxMemoryUsageRatio])
* - Least recently used: Oldest entries will be evicted
* - Memory usage limit: If memory is limited, all entries will be evicted
*/
@ThreadSafe
class InMemoryCacheWithEviction<KEY, VALUE>(
/**
* Cache entries' values that were not used within [maxTimePeriods] will be converted into [SoftReference]s (they can still be used for
* some more time until being garbage collected).
* Cache entries' values that were not used within [maxTimePeriodsToKeepStrongReferences] will be converted to [SoftReference]s.
*
* The time period starts from 0 and will increment by 1 whenever [newTimePeriod] is called.
*/
private val maxTimePeriods: Int,
private val maxTimePeriodsToKeepStrongReferences: Int,
/**
* If [memoryUsageRatio] > [maxMemoryUsageRatio], all cache entries' values will be converted into [SoftReference]s (they can still be
* used for some more time until being garbage collected).
* Cache entries' values that were not used within [maxTimePeriodsToKeepStrongReferences] + [maxTimePeriodsToKeepSoftReferences] will be
* removed from this cache.
*/
private val maxMemoryUsageRatio: Double,
private val maxTimePeriodsToKeepSoftReferences: Int,
/**
* [TEST-ONLY] Function that returns the current memory usage ratio. It's here only for unit tests to provide custom values.
* Production code should not provide a value (the default function below will be used).
* If [memoryUsageRatio] > [maxMemoryUsageRatioToKeepStrongReferences], all cache entries' values will be converted to [SoftReference]s.
*/
private val maxMemoryUsageRatioToKeepStrongReferences: Double,
/**
* Function that returns the current memory usage ratio. NOTE: Production code should not provide this function (the default function
* below will be used). This parameter is here only to allow writing unit tests.
*/
private val memoryUsageRatio: () -> Double = {
Runtime.getRuntime().let { (it.totalMemory() - it.freeMemory()).toDouble() / it.maxMemory() }
// Note: In the following formula, memory usage ratio = used memory / total memory. In practice, the JVM may be able to increase
// total memory to Runtime.maxMemory(), which means that the effective memory usage ratio could be smaller. However, it's also
// possible that the JVM won't be able to do that (e.g., if Runtime.maxMemory() is too high or not set), so we can't rely on that.
1.0 - Runtime.getRuntime().let { it.freeMemory().toDouble() / it.totalMemory() }
}
) {
@@ -73,20 +82,21 @@ class InMemoryCacheWithEviction<KEY, VALUE>(
fun evictEntries() {
writeLock { // Write lock so that other threads don't read/write the cache while this thread is updating it
// If memory is limited, evict all entries (turn their values into `SoftReference`s)
val entriesToEvict = if (memoryUsageRatio() > maxMemoryUsageRatio) {
cache.values
} else {
// Otherwise, evict least-recently-used entries
val lowestTimePeriodToRetain = currentTimePeriod.get() - maxTimePeriods + 1
if (lowestTimePeriodToRetain > 0) {
cache.filterValues { it.lastUsed() < lowestTimePeriodToRetain }.values
} else emptyList()
}
entriesToEvict.forEach { it.updateToSoftReference() }
val lowestTimePeriodToKeepStrongRefs = currentTimePeriod.get() - maxTimePeriodsToKeepStrongReferences + 1
val lowestTimePeriodToKeepSoftRefs = lowestTimePeriodToKeepStrongRefs - maxTimePeriodsToKeepSoftReferences
// Also remove entries whose values are already garbage collected
cache.filterValues { it.valueWasGarbageCollected() }.keys.forEach {
// If memory is limited, convert all entries' values to `SoftReference`s
if (memoryUsageRatio() > maxMemoryUsageRatioToKeepStrongReferences) {
cache.values.forEach { it.updateToSoftReference() }
} else {
// Otherwise, convert least-recently-used entries' values to `SoftReference`s
cache.filterValues { it.lastUsed() < lowestTimePeriodToKeepStrongRefs }.values.forEach {
it.updateToSoftReference()
}
}
// Remove soft-reference entries that are least recently used or are already garbage collected
cache.filterValues { it.lastUsed() < lowestTimePeriodToKeepSoftRefs || it.valueWasGarbageCollected() }.keys.forEach {
cache.remove(it)
}
}
@@ -112,41 +122,17 @@ class InMemoryCacheWithEviction<KEY, VALUE>(
}
}
/**
* Returns the following numbers for debugging (in order):
* - Number of cache entries whose values are strong references
* - Number of cache entries whose values are referred to by a [SoftReference] and are not yet garbage collected
* - Number of cache entries whose values are referred to by a [SoftReference] and have already been garbage collected
*/
fun countCacheEntriesForDebug(): Triple<Int, Int, Int> {
return readLock {
var strongRefs = 0
var aliveSoftRefs = 0
var deadSoftRefs = 0
// Note: Each iteration of the loop is atomic, but this loop is not atomic (new cache entries may be added or existing cache
// entries may be updated by `computeIfAbsent` while this loop is running). If we need this loop to be atomic, we can use
// `writeLock` instead of `readLock`, but we don't need it to be atomic.
cache.values.forEach {
synchronized(it) {
when {
!it.wasEvicted() -> strongRefs++
it.get() != null -> aliveSoftRefs++
else -> deadSoftRefs++
}
}
}
Triple(strongRefs, aliveSoftRefs, deadSoftRefs)
}
}
@VisibleForTesting
internal enum class EntryState { STRONG_REF, SOFT_REF, ABSENT }
@VisibleForTesting
internal fun keyWasEvicted(key: KEY): Boolean {
internal fun getEntryState(key: KEY): EntryState {
return readLock {
cache[key]?.let {
synchronized(it) {
it.wasEvicted()
if (it.valueIsAStrongReference()) STRONG_REF else SOFT_REF
}
} ?: true
} ?: ABSENT
}
}
@@ -180,7 +166,7 @@ private class CacheEntryValue<VALUE> private constructor(
}
}
fun wasEvicted(): Boolean = (strongRef == null)
fun valueIsAStrongReference(): Boolean = (strongRef != null)
fun valueWasGarbageCollected(): Boolean = (get() == null)
@@ -6,17 +6,21 @@
package org.jetbrains.kotlin.incremental.classpathDiff
import com.google.common.util.concurrent.AtomicDouble
import org.jetbrains.kotlin.incremental.classpathDiff.InMemoryCacheWithEviction.EntryState.*
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.fail
class InMemoryCacheWithEvictionTest {
@Test
fun testComputeIfAbsent() {
val cache = InMemoryCacheWithEviction<Int, Any>(maxTimePeriods = 10, maxMemoryUsageRatio = 1.0, memoryUsageRatio = { 0.5 })
val cache = InMemoryCacheWithEviction<Int, Any>(
maxTimePeriodsToKeepStrongReferences = 10,
maxTimePeriodsToKeepSoftReferences = 10,
maxMemoryUsageRatioToKeepStrongReferences = 0.8,
memoryUsageRatio = { 0.5 }
)
// Check when the entries are not yet present
assertEquals("One", cache.computeIfAbsent(1) { "One" })
@@ -29,82 +33,108 @@ class InMemoryCacheWithEvictionTest {
@Test
fun testLeastRecentlyUsedCacheEviction() {
val cache = InMemoryCacheWithEviction<Int, Any>(maxTimePeriods = 2, maxMemoryUsageRatio = 1.0, memoryUsageRatio = { 0.5 })
val cache = InMemoryCacheWithEviction<Int, Any>(
maxTimePeriodsToKeepStrongReferences = 2,
maxTimePeriodsToKeepSoftReferences = 3,
maxMemoryUsageRatioToKeepStrongReferences = 0.8,
memoryUsageRatio = { 0.5 }
)
// Time period 0 - Cache entry 0 is added, no cache entries are evicted
cache.computeIfAbsent(0) { "Zero" }
cache.evictEntries()
assertFalse(cache.keyWasEvicted(0))
// Time period 1 - Cache entry 1 is added, no cache entries are evicted
// Check that cache entries change states at different time periods depending on when they were last used
cache.newTimePeriod()
cache.computeIfAbsent(1) { "One" }
cache.evictEntries()
assertFalse(cache.keyWasEvicted(0))
assertFalse(cache.keyWasEvicted(1))
// Time period 2 - Cache entry 2 is added, cache entry 0 is evicted (because maxTimePeriods = 2)
cache.newTimePeriod()
cache.computeIfAbsent(2) { "Two" }
cache.computeIfAbsent(3) { "Three" }
cache.evictEntries()
assertTrue(cache.keyWasEvicted(0))
assertFalse(cache.keyWasEvicted(1))
assertFalse(cache.keyWasEvicted(2))
assertEquals(STRONG_REF, cache.getEntryState(1))
assertEquals(STRONG_REF, cache.getEntryState(2))
assertEquals(STRONG_REF, cache.getEntryState(3))
cache.newTimePeriod()
cache.computeIfAbsent(2) { fail("Must not run") }
cache.evictEntries()
assertEquals(STRONG_REF, cache.getEntryState(1))
assertEquals(STRONG_REF, cache.getEntryState(2))
assertEquals(STRONG_REF, cache.getEntryState(3))
cache.newTimePeriod()
cache.computeIfAbsent(3) { fail("Must not run") }
cache.evictEntries()
assertEquals(SOFT_REF, cache.getEntryState(1))
assertEquals(STRONG_REF, cache.getEntryState(2))
assertEquals(STRONG_REF, cache.getEntryState(3))
// Time period 3 - Cache entry 1 is evicted
cache.newTimePeriod()
cache.evictEntries()
assertTrue(cache.keyWasEvicted(0))
assertTrue(cache.keyWasEvicted(1))
assertFalse(cache.keyWasEvicted(2))
assertEquals(SOFT_REF, cache.getEntryState(1))
assertEquals(SOFT_REF, cache.getEntryState(2))
assertEquals(STRONG_REF, cache.getEntryState(3))
// Time period 4 - Cache entry 2 is evicted
cache.newTimePeriod()
cache.evictEntries()
assertTrue(cache.keyWasEvicted(0))
assertTrue(cache.keyWasEvicted(1))
assertTrue(cache.keyWasEvicted(2))
assertEquals(SOFT_REF, cache.getEntryState(1))
assertEquals(SOFT_REF, cache.getEntryState(2))
assertEquals(SOFT_REF, cache.getEntryState(3))
cache.newTimePeriod()
cache.evictEntries()
assertEquals(ABSENT, cache.getEntryState(1))
assertEquals(SOFT_REF, cache.getEntryState(2))
assertEquals(SOFT_REF, cache.getEntryState(3))
cache.newTimePeriod()
cache.evictEntries()
assertEquals(ABSENT, cache.getEntryState(1))
assertEquals(ABSENT, cache.getEntryState(2))
assertEquals(SOFT_REF, cache.getEntryState(3))
cache.newTimePeriod()
cache.evictEntries()
assertEquals(ABSENT, cache.getEntryState(1))
assertEquals(ABSENT, cache.getEntryState(2))
assertEquals(ABSENT, cache.getEntryState(3))
}
@Test
fun testMemoryUsageLimitCacheEviction() {
val memoryUsageRatio = AtomicDouble(0.5)
val cache = InMemoryCacheWithEviction<Int, Any>(
maxTimePeriods = 10,
maxMemoryUsageRatio = 0.8,
maxTimePeriodsToKeepStrongReferences = 10,
maxTimePeriodsToKeepSoftReferences = 10,
maxMemoryUsageRatioToKeepStrongReferences = 0.8,
memoryUsageRatio = { memoryUsageRatio.get() }
)
// Time period 0 - Cache entry 0 is added, no cache entries are evicted
cache.computeIfAbsent(0) { "Zero" }
cache.evictEntries()
assertFalse(cache.keyWasEvicted(0))
assertEquals(STRONG_REF, cache.getEntryState(0))
// Time period 1 - Cache entry 1 is added, no cache entries are evicted
cache.newTimePeriod()
cache.computeIfAbsent(1) { "One" }
cache.evictEntries()
assertFalse(cache.keyWasEvicted(0))
assertFalse(cache.keyWasEvicted(1))
assertEquals(STRONG_REF, cache.getEntryState(0))
assertEquals(STRONG_REF, cache.getEntryState(1))
// Memory usage increases to above the limit (maxMemoryUsageRatio = 0.8)
// Memory usage increases to above the limit (0.8)
memoryUsageRatio.set(0.9)
// Time period 2 - Cache entry 2 is added, all cache entries are evicted
cache.newTimePeriod()
cache.computeIfAbsent(2) { "Two" }
cache.evictEntries()
assertTrue(cache.keyWasEvicted(0))
assertTrue(cache.keyWasEvicted(1))
assertTrue(cache.keyWasEvicted(2))
assertEquals(SOFT_REF, cache.getEntryState(0))
assertEquals(SOFT_REF, cache.getEntryState(1))
assertEquals(SOFT_REF, cache.getEntryState(2))
// Memory usage decreases back to below the limit (maxMemoryUsageRatio = 0.8)
// Memory usage decreases back to below the limit (0.8)
memoryUsageRatio.set(0.5)
// Time period 3 - Cache entry 3 is added, again no cache entries are evicted
cache.newTimePeriod()
cache.computeIfAbsent(3) { "Three" }
cache.evictEntries()
assertFalse(cache.keyWasEvicted(3))
assertEquals(STRONG_REF, cache.getEntryState(3))
}
}