summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jernej Virag <jernej@google.com> 2023-05-10 16:52:58 +0200
committer Jernej Virag <jernej@google.com> 2023-05-12 17:29:15 +0200
commitdff9e1cc5e15eaba1519e8ab98b1b4e885c98609 (patch)
tree0f5ec8e18692950642f5644757bef12c34dc32a1
parent2d3194900d7c417db78758fdff63c602b62e8945 (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
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt110
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java38
-rw-r--r--packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt16
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt190
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt4
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
}