Merge "Trigger background memory trim when SystemUI is idle" into udc-dev

This commit is contained in:
Jernej Virag
2023-05-15 06:43:31 +00:00
committed by Android (Google) Code Review
6 changed files with 359 additions and 0 deletions

View File

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

View File

@@ -87,6 +87,7 @@ import java.util.concurrent.Executor;
KeyguardRepositoryModule.class,
KeyguardFaceAuthModule.class,
StartKeyguardTransitionModule.class,
ResourceTrimmerModule.class,
})
public class KeyguardModule {
/**

View File

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

View File

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

View File

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

View File

@@ -147,6 +147,10 @@ class FakeKeyguardRepository : KeyguardRepository {
_isAodAvailable.value = isAodAvailable
}
fun setDreaming(isDreaming: Boolean) {
_isDreaming.value = isDreaming
}
fun setDreamingWithOverlay(isDreaming: Boolean) {
_isDreamingWithOverlay.value = isDreaming
}