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)) } }