Add manager that hints to the garbage collector to collect the garbage (#1387)

This commit is contained in:
David Perez 2024-05-24 11:07:31 -05:00 committed by Álison Fernandes
parent 03a97258e5
commit 5a908c1d01
6 changed files with 199 additions and 7 deletions

View file

@ -33,6 +33,8 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManagerImpl
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
@ -97,6 +99,14 @@ object PlatformManagerModule {
@Singleton
fun provideBitwardenDispatchers(): DispatcherManager = DispatcherManagerImpl()
@Provides
@Singleton
fun provideGarbageCollectionManager(
dispatcherManager: DispatcherManager,
): GarbageCollectionManager = GarbageCollectionManagerImpl(
dispatcherManager = dispatcherManager,
)
@Provides
@Singleton
fun provideSdkClientManager(

View file

@ -0,0 +1,12 @@
package com.x8bit.bitwarden.data.platform.manager.garbage
/**
* A manager for interfacing with the garbage collector.
*/
interface GarbageCollectionManager {
/**
* Calls the garbage collector on the [Runtime] in an effort to clear the unused resources in
* the heap.
*/
fun tryCollect()
}

View file

@ -0,0 +1,46 @@
package com.x8bit.bitwarden.data.platform.manager.garbage
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* Default implementation of the [GarbageCollectionManager].
*/
@Suppress("ExplicitGarbageCollectionCall")
class GarbageCollectionManagerImpl(
private val garbageCollector: () -> Unit = { Runtime.getRuntime().gc() },
dispatcherManager: DispatcherManager,
) : GarbageCollectionManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private var collectionJob: Job = Job().apply { complete() }
override fun tryCollect() {
collectionJob.cancel()
collectionJob = unconfinedScope.launch {
delay(timeMillis = GARBAGE_COLLECTION_INITIAL_DELAY_MS)
repeat(times = GARBAGE_COLLECTION_ATTEMPTS) {
delay(timeMillis = GARBAGE_COLLECTION_BASE_BACKOFF_MS * it)
garbageCollector()
}
}
}
}
/**
* The number of time the garbage collector should be called.
*/
private const val GARBAGE_COLLECTION_ATTEMPTS: Int = 10
/**
* The base delay, in milliseconds, between a garbage collection attempt. The duration will be
* multiplied by the number of attempts made thus far.
*/
private const val GARBAGE_COLLECTION_BASE_BACKOFF_MS: Long = 100L
/**
* The initial delay, in milliseconds, before the first garbage collection attempt.
*/
private const val GARBAGE_COLLECTION_INITIAL_DELAY_MS: Long = 100L

View file

@ -13,15 +13,13 @@ import kotlinx.coroutines.test.setMain
* A faked implementation of [DispatcherManager] that uses [UnconfinedTestDispatcher].
*/
@OptIn(ExperimentalCoroutinesApi::class)
class FakeDispatcherManager : DispatcherManager {
override val default: CoroutineDispatcher = UnconfinedTestDispatcher()
class FakeDispatcherManager(
override val default: CoroutineDispatcher = UnconfinedTestDispatcher(),
override val io: CoroutineDispatcher = UnconfinedTestDispatcher(),
override val unconfined: CoroutineDispatcher = UnconfinedTestDispatcher(),
) : DispatcherManager {
override val main: MainCoroutineDispatcher = Dispatchers.Main
override val io: CoroutineDispatcher = UnconfinedTestDispatcher()
override val unconfined: CoroutineDispatcher = UnconfinedTestDispatcher()
/**
* Updates the main dispatcher to use the provided [dispatcher]. Used in conjunction with
* [resetMain].

View file

@ -0,0 +1,113 @@
package com.x8bit.bitwarden.data.platform.manager.garbage
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
import kotlinx.coroutines.test.StandardTestDispatcher
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class GarbageCollectionManagerTest {
private var garbageCollectionCount = 0
private val dispatcher = StandardTestDispatcher()
private val garbageCollectionManager: GarbageCollectionManager = GarbageCollectionManagerImpl(
garbageCollector = { garbageCollectionCount++ },
dispatcherManager = FakeDispatcherManager(unconfined = dispatcher),
)
@Test
fun `tryCollect should attempt to garbage collect 10 times in increasing intervals`() {
garbageCollectionManager.tryCollect()
// We do nothing right away
dispatcher.scheduler.runCurrent()
assertEquals(0, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 100L)
assertEquals(1, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 200L)
assertEquals(2, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 300L)
assertEquals(3, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 400L)
assertEquals(4, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 500L)
assertEquals(5, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 600L)
assertEquals(6, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 700L)
assertEquals(7, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 800L)
assertEquals(8, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 900L)
assertEquals(9, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 1_000L)
assertEquals(10, garbageCollectionCount)
// We should stop at this point, even 10 seconds later we should not have run again
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 10_000L)
assertEquals(10, garbageCollectionCount)
}
@Test
fun `tryCollect should restart the intervals when called multiple times`() {
garbageCollectionManager.tryCollect()
// We do nothing right away
dispatcher.scheduler.runCurrent()
assertEquals(0, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 100L)
assertEquals(1, garbageCollectionCount)
garbageCollectionManager.tryCollect()
// We do nothing right away
dispatcher.scheduler.runCurrent()
assertEquals(1, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 100L)
assertEquals(2, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 200L)
assertEquals(3, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 300L)
assertEquals(4, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 400L)
assertEquals(5, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 500L)
assertEquals(6, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 600L)
assertEquals(7, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 700L)
assertEquals(8, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 800L)
assertEquals(9, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 900L)
assertEquals(10, garbageCollectionCount)
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 1_000L)
assertEquals(11, garbageCollectionCount)
// We should stop at this point, even 10 seconds later we should not have run again
dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 10_000L)
assertEquals(11, garbageCollectionCount)
}
}

View file

@ -3,6 +3,9 @@ package com.x8bit.bitwarden.data.util
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
import io.mockk.MockKMatcherScope
import io.mockk.every
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Assertions.assertEquals
@ -47,3 +50,13 @@ inline fun <reified T : Any> mockBuilder(crossinline block: MockKMatcherScope.(T
this.self as T
}
}
/**
* A helper method that calls both [TestCoroutineScheduler.advanceTimeBy] and
* [TestCoroutineScheduler.runCurrent].
*/
@OptIn(ExperimentalCoroutinesApi::class)
fun TestDispatcher.advanceTimeByAndRunCurrent(delayTimeMillis: Long) {
scheduler.advanceTimeBy(delayTimeMillis = delayTimeMillis)
scheduler.runCurrent()
}