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