diff options
| author | 2023-05-10 16:52:58 +0200 | |
|---|---|---|
| committer | 2023-05-12 17:29:15 +0200 | |
| commit | dff9e1cc5e15eaba1519e8ab98b1b4e885c98609 (patch) | |
| tree | 0f5ec8e18692950642f5644757bef12c34dc32a1 | |
| parent | 2d3194900d7c417db78758fdff63c602b62e8945 (diff) | |
Trigger background memory trim when SystemUI is idle
SystemUI never receives BACKGROUND level trim commmand - this means that
several caches (especially font) can stay around for a long time before
they reach their set limits and trigger a trim. This causes significant
increase in private memory in cases where many font variants are
allocated - for example when lockscreen clock is animated through
different font variations.
This adds a BACKGROUND trim command when SysUI goes idle after
transitioning to AoD or simply turning off the screen. This is the
closest SysUI has to "background" state where it can safely cleanup its
resources.
Bug: 275486055
Test: atest ResourceTrimmer
memory benchmark with LockscreenAodTransitionBenchmark showing
~20MB of descrease in RssAnon after a 30 iteration benchmark run
Change-Id: Ia4abd0d9c3cf3b3a71bdd31f1009f37b18c99ae9
6 files changed, 359 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt new file mode 100644 index 000000000000..4eb7d44ebfa6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard + +import android.annotation.WorkerThread +import android.content.ComponentCallbacks2 +import android.os.Trace +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.utils.GlobalWindowManager +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Releases cached resources on allocated by keyguard. + * + * We release most resources when device goes idle since that's the least likely time it'll cause + * jank during use. Idle in this case means after lockscreen -> AoD transition completes or when the + * device screen is turned off, depending on settings. + */ +@SysUISingleton +class ResourceTrimmer +@Inject +constructor( + private val keyguardInteractor: KeyguardInteractor, + private val globalWindowManager: GlobalWindowManager, + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, +) : CoreStartable, WakefulnessLifecycle.Observer { + + override fun start() { + Log.d(LOG_TAG, "Resource trimmer registered.") + applicationScope.launch(bgDispatcher) { + // We need to wait for the AoD transition (and animation) to complete. + // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f + // signal. This is to make sure we don't clear font caches during animation which + // would jank and leave stale data in memory. + val isDozingFully = + keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged() + combine( + keyguardInteractor.wakefulnessModel.map { it.state }, + keyguardInteractor.isDreaming, + isDozingFully, + ::Triple + ) + .distinctUntilChanged() + .collect { onWakefulnessUpdated(it.first, it.second, it.third) } + } + } + + @WorkerThread + private fun onWakefulnessUpdated( + wakefulness: WakefulnessState, + isDreaming: Boolean, + isDozingFully: Boolean + ) { + if (DEBUG) { + Log.d( + LOG_TAG, + "Wakefulness: $wakefulness Dreaming: $isDreaming DozeAmount: $isDozingFully" + ) + } + // There are three scenarios: + // * No dozing and no AoD at all - where we go directly to ASLEEP with isDreaming = false + // and dozeAmount == 0f + // * Dozing without Aod - where we go to ASLEEP with isDreaming = true and dozeAmount jumps + // to 1f + // * AoD - where we go to ASLEEP with iDreaming = true and dozeAmount slowly increases + // to 1f + val dozeDisabledAndScreenOff = wakefulness == WakefulnessState.ASLEEP && !isDreaming + val dozeEnabledAndDozeAnimationCompleted = + wakefulness == WakefulnessState.ASLEEP && isDreaming && isDozingFully + if (dozeDisabledAndScreenOff || dozeEnabledAndDozeAnimationCompleted) { + Trace.beginSection("ResourceTrimmer#trimMemory") + Log.d(LOG_TAG, "SysUI asleep, trimming memory.") + globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + Trace.endSection() + } + } + + companion object { + private const val LOG_TAG = "ResourceTrimmer" + private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 255556c80121..d7c039d9b519 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -87,6 +87,7 @@ import java.util.concurrent.Executor; KeyguardRepositoryModule.class, KeyguardFaceAuthModule.class, StartKeyguardTransitionModule.class, + ResourceTrimmerModule.class, }) public class KeyguardModule { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java new file mode 100644 index 000000000000..d693326f0dba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.dagger; + +import com.android.systemui.CoreStartable; +import com.android.systemui.keyguard.ResourceTrimmer; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; + +/** + * Binds {@link ResourceTrimmer} into {@link CoreStartable} set. + */ +@Module +public abstract class ResourceTrimmerModule { + + /** Bind ResourceTrimmer into CoreStarteables. */ + @Binds + @IntoMap + @ClassKey(ResourceTrimmer.class) + public abstract CoreStartable bindResourceTrimmer(ResourceTrimmer resourceTrimmer); +} diff --git a/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt new file mode 100644 index 000000000000..038fddc1f7a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt @@ -0,0 +1,16 @@ +package com.android.systemui.utils + +import android.view.WindowManagerGlobal +import javax.inject.Inject + +/** Interface to talk to [WindowManagerGlobal] */ +class GlobalWindowManager @Inject constructor() { + /** + * Sends a trim memory command to [WindowManagerGlobal]. + * + * @param level One of levels from [ComponentCallbacks2] starting with TRIM_ + */ + fun trimMemory(level: Int) { + WindowManagerGlobal.getInstance().trimMemory(level) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt new file mode 100644 index 000000000000..931f82ce9f6b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt @@ -0,0 +1,190 @@ +package com.android.systemui.keyguard + +import android.content.ComponentCallbacks2 +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.data.repository.FakeCommandQueue +import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.WakeSleepReason +import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.utils.GlobalWindowManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ResourceTrimmerTest : SysuiTestCase() { + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val keyguardRepository = FakeKeyguardRepository() + + @Mock private lateinit var globalWindowManager: GlobalWindowManager + @Mock private lateinit var featureFlags: FeatureFlags + private lateinit var resourceTrimmer: ResourceTrimmer + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + keyguardRepository.setWakefulnessModel( + WakefulnessModel(WakefulnessState.AWAKE, WakeSleepReason.OTHER, WakeSleepReason.OTHER) + ) + keyguardRepository.setDozeAmount(0f) + + val interactor = + KeyguardInteractor( + keyguardRepository, + FakeCommandQueue(), + featureFlags, + FakeKeyguardBouncerRepository() + ) + resourceTrimmer = + ResourceTrimmer( + interactor, + globalWindowManager, + testScope.backgroundScope, + testDispatcher + ) + resourceTrimmer.start() + } + + @Test + fun noChange_noOutputChanges() = + testScope.runTest { + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + } + + @Test + fun dozeAodDisabled_sleep_trimsMemory() = + testScope.runTest { + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + testScope.runCurrent() + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + } + + @Test + fun dozeEnabled_sleepWithFullDozeAmount_trimsMemory() = + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(1f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + testScope.runCurrent() + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + } + + @Test + fun dozeEnabled_sleepWithoutFullDozeAmount_doesntTrimMemory() = + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(0f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + } + + @Test + fun aodEnabled_sleepWithFullDozeAmount_trimsMemoryOnce() { + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(0f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + + generateSequence(0f) { it + 0.1f } + .takeWhile { it < 1f } + .forEach { + keyguardRepository.setDozeAmount(it) + testScope.runCurrent() + } + verifyZeroInteractions(globalWindowManager) + + keyguardRepository.setDozeAmount(1f) + testScope.runCurrent() + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + } + } + + @Test + fun aodEnabled_deviceWakesHalfWayThrough_doesNotTrimMemory() { + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(0f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + + generateSequence(0f) { it + 0.1f } + .takeWhile { it < 0.8f } + .forEach { + keyguardRepository.setDozeAmount(it) + testScope.runCurrent() + } + verifyZeroInteractions(globalWindowManager) + + generateSequence(0.8f) { it - 0.1f } + .takeWhile { it >= 0f } + .forEach { + keyguardRepository.setDozeAmount(it) + testScope.runCurrent() + } + + keyguardRepository.setDozeAmount(0f) + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index fd8c4b81063d..b52a76839a99 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -147,6 +147,10 @@ class FakeKeyguardRepository : KeyguardRepository { _isAodAvailable.value = isAodAvailable } + fun setDreaming(isDreaming: Boolean) { + _isDreaming.value = isDreaming + } + fun setDreamingWithOverlay(isDreaming: Boolean) { _isDreamingWithOverlay.value = isDreaming } |