From 08e6eb07c1388fea64fd58f89262f4b686e3e911 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Tue, 10 May 2022 13:43:25 +0100 Subject: [PATCH] [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 --- .../report/metrics/BuildPerformanceMetric.kt | 4 +- .../ClasspathSnapshotSerializer.kt | 105 +++++++++++------ .../InMemoryCacheWithEviction.kt | 96 +++++++--------- .../InMemoryCacheWithEvictionTest.kt | 106 +++++++++++------- 4 files changed, 184 insertions(+), 127 deletions(-) diff --git a/build-common/src/org/jetbrains/kotlin/build/report/metrics/BuildPerformanceMetric.kt b/build-common/src/org/jetbrains/kotlin/build/report/metrics/BuildPerformanceMetric.kt index e4f3ce276a4..8da88c8f9b6 100644 --- a/build-common/src/org/jetbrains/kotlin/build/report/metrics/BuildPerformanceMetric.kt +++ b/build-common/src/org/jetbrains/kotlin/build/report/metrics/BuildPerformanceMetric.kt @@ -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 { diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt index 88b98c26ef6..b2b05c69257 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt @@ -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(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( + maxTimePeriodsToKeepStrongReferences = 20, + maxTimePeriodsToKeepSoftReferences = 1000, + maxMemoryUsageRatioToKeepStrongReferences = 0.8 + ) fun load(classpathEntrySnapshotFiles: List, 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( +internal open class DataExternalizerForSealedClass( val baseClass: Class, val inheritorClasses: List>, val inheritorExternalizers: List> @@ -59,13 +62,13 @@ open class DataExternalizerForSealedClass( 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).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( +internal object ClassSnapshotExternalizer : DataExternalizerForSealedClass( baseClass = ClassSnapshot::class.java, inheritorClasses = listOf(AccessibleClassSnapshot::class.java, InaccessibleClassSnapshot::class.java), inheritorExternalizers = listOf(AccessibleClassSnapshotExternalizer, InaccessibleClassSnapshotExternalizer) ) -object AccessibleClassSnapshotExternalizer : DataExternalizerForSealedClass( +internal object AccessibleClassSnapshotExternalizer : DataExternalizerForSealedClass( baseClass = AccessibleClassSnapshot::class.java, inheritorClasses = listOf(KotlinClassSnapshot::class.java, JavaClassSnapshot::class.java), inheritorExternalizers = listOf(KotlinClassSnapshotExternalizer, JavaClassSnapshotExternalizer) ) -object KotlinClassSnapshotExternalizer : DataExternalizerForSealedClass( +private object KotlinClassSnapshotExternalizer : DataExternalizerForSealedClass( baseClass = KotlinClassSnapshot::class.java, inheritorClasses = listOf( RegularKotlinClassSnapshot::class.java, @@ -110,7 +113,7 @@ object KotlinClassSnapshotExternalizer : DataExternalizerForSealedClass { +private object RegularKotlinClassSnapshotExternalizer : DataExternalizer { override fun save(output: DataOutput, snapshot: RegularKotlinClassSnapshot) { ClassIdExternalizer.save(output, snapshot.classId) @@ -121,15 +124,16 @@ object RegularKotlinClassSnapshotExternalizer : DataExternalizer { +private object PackageFacadeKotlinClassSnapshotExternalizer : DataExternalizer { override fun save(output: DataOutput, snapshot: PackageFacadeKotlinClassSnapshot) { ClassIdExternalizer.save(output, snapshot.classId) @@ -140,7 +144,8 @@ object PackageFacadeKotlinClassSnapshotExternalizer : DataExternalizer { +private object MultifileClassKotlinClassSnapshotExternalizer : DataExternalizer { override fun save(output: DataOutput, snapshot: MultifileClassKotlinClassSnapshot) { ClassIdExternalizer.save(output, snapshot.classId) @@ -159,7 +164,8 @@ object MultifileClassKotlinClassSnapshotExternalizer : DataExternalizer { +internal object KotlinClassInfoExternalizer : DataExternalizer { override fun save(output: DataOutput, info: KotlinClassInfo) { ClassIdExternalizer.save(output, info.classId) @@ -181,7 +187,8 @@ object KotlinClassInfoExternalizer : DataExternalizer { 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 { } } -object JavaClassSnapshotExternalizer : DataExternalizer { +private object JavaClassSnapshotExternalizer : DataExternalizer { override fun save(output: DataOutput, snapshot: JavaClassSnapshot) { ClassIdExternalizer.save(output, snapshot.classId) @@ -203,15 +210,16 @@ object JavaClassSnapshotExternalizer : DataExternalizer { 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 { +private object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer { override fun save(output: DataOutput, snapshot: JavaClassMemberLevelSnapshot) { JavaElementSnapshotExternalizer.save(output, snapshot.classAbiExcludingMembers) @@ -228,7 +236,7 @@ object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer { +private object JavaElementSnapshotExternalizer : DataExternalizer { override fun save(output: DataOutput, value: JavaElementSnapshot) { StringExternalizer.save(output, value.name) @@ -243,7 +251,7 @@ object JavaElementSnapshotExternalizer : DataExternalizer { } } -object InaccessibleClassSnapshotExternalizer : DataExternalizer { +private object InaccessibleClassSnapshotExternalizer : DataExternalizer { override fun save(output: DataOutput, snapshot: InaccessibleClassSnapshot) { // Nothing to save @@ -253,3 +261,34 @@ object InaccessibleClassSnapshotExternalizer : DataExternalizer 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 by FqNameExternalizer { + + private val fqNameInterner by lazy { Interner.createWeakInterner() } + + override fun read(input: DataInput): FqName { + return fqNameInterner.intern(FqNameExternalizer.read(input)) + } +} + +private object JvmClassNameExternalizerWithInterning : DataExternalizer by JvmClassNameExternalizer { + + private val jvmClassNameInterner by lazy { Interner.createWeakInterner() } + + override fun read(input: DataInput): JvmClassName { + return jvmClassNameInterner.intern(JvmClassNameExternalizer.read(input)) + } +} diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/InMemoryCacheWithEviction.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/InMemoryCacheWithEviction.kt index f6ed81f12ea..cf763e7fe12 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/InMemoryCacheWithEviction.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/InMemoryCacheWithEviction.kt @@ -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( /** - * 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( 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( } } - /** - * 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 { - 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 private constructor( } } - fun wasEvicted(): Boolean = (strongRef == null) + fun valueIsAStrongReference(): Boolean = (strongRef != null) fun valueWasGarbageCollected(): Boolean = (get() == null) diff --git a/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/InMemoryCacheWithEvictionTest.kt b/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/InMemoryCacheWithEvictionTest.kt index 1a98eb15916..42d3b53a73c 100644 --- a/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/InMemoryCacheWithEvictionTest.kt +++ b/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/InMemoryCacheWithEvictionTest.kt @@ -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(maxTimePeriods = 10, maxMemoryUsageRatio = 1.0, memoryUsageRatio = { 0.5 }) + val cache = InMemoryCacheWithEviction( + 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(maxTimePeriods = 2, maxMemoryUsageRatio = 1.0, memoryUsageRatio = { 0.5 }) + val cache = InMemoryCacheWithEviction( + 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( - 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)) } }