From ffa5c2ad83091ca84cc5cb79209cb697d4ea7634 Mon Sep 17 00:00:00 2001 From: Gustav Sennton Date: Wed, 14 May 2025 13:28:51 +0000 Subject: [PATCH] Don't trigger getRecentTasks() calls when waiting for an old call TaskbarRecentAppsController#reloadRecentTasksIfNeeded() triggers a call to fetch Recents tasks, that call is expensive as it goes through WM Shell and the system server. If we trigger reloadRecentTasksIfNeeded() multiple times we're queueing up multiple calls to fetch Recents tasks. With this CL we avoid queueing up multiple calls, and instead just make one new call when the old one finishes. That way we can only ever have one call getRecentTasks() call triggered through reloadRecentTasksIfNeeded() posted at any one time. Bug: 415090968 Flag: com.android.window.flags.enable_taskbar_recent_tasks_throttle_bugfix Test: TaskbarRecentAppsControllerTest Change-Id: I58b66e4564af4e64837317a9de7be398395d8568 --- .../taskbar/TaskbarRecentAppsController.kt | 56 +++++++---- .../TaskbarRecentAppsControllerTest.kt | 92 ++++++++++++++++++- 2 files changed, 130 insertions(+), 18 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt index 541cec7728..bc3dc53aa0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt @@ -16,6 +16,8 @@ package com.android.launcher3.taskbar import android.content.Context +import android.util.Log +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import androidx.annotation.VisibleForTesting import com.android.launcher3.BubbleTextView.RunningAppState @@ -57,6 +59,9 @@ class TaskbarRecentAppsController( } } + val enableRecentTasksThrottle = + DesktopExperienceFlags.ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX.isTrue + // TODO(b/343532825): Add a setting to disable Recents even when the flag is on. var canShowRecentApps = enableRecentsInTaskbar() @VisibleForTesting @@ -175,6 +180,11 @@ class TaskbarRecentAppsController( // tasks again if we have already requested it and the task list has not changed private var taskListChangeId = -1 + // Whether we're currently loading recents tasks + private var loadingRecentsTasks = false + // Whether we need to reload recents tasks when the current loading operation is finished + private var needsRecentsTasksReload = false + // Whether we've loaded recents tasks at least once private var recentTasksLoaded = false fun init(taskbarControllers: TaskbarControllers, previousShownTasks: List) { @@ -240,24 +250,34 @@ class TaskbarRecentAppsController( } private fun reloadRecentTasksIfNeeded() { - if (!recentsModel.isTaskListValid(taskListChangeId)) { - taskListChangeId = - recentsModel.getTasks(RecentsFilterState.EMPTY_FILTER) { tasks -> - recentTasksLoaded = true - allRecentTasks = tasks - val oldRunningTaskdIds = runningTaskIds - val oldMinimizedTaskIds = minimizedTaskIds - desktopTasks = - allRecentTasks.filterIsInstance().flatMap { it.tasks } - val runningTasksChanged = oldRunningTaskdIds != runningTaskIds - val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds - if ( - onRecentsOrHotseatChanged() || runningTasksChanged || minimizedTasksChanged - ) { - controllers.taskbarViewController.commitRunningAppsToUI() - } - } + if (recentsModel.isTaskListValid(taskListChangeId)) return + if (enableRecentTasksThrottle && loadingRecentsTasks) { + Log.v(TAG, "reloadRecentTasksIfNeeded: tried to reload while loading recents tasks") + needsRecentsTasksReload = true + return } + Log.v(TAG, "reloadRecentTasksIfNeeded: load recents tasks") + // Only indicate that recents tasks are loading if the enableRecentTasksThrottle flag is on + loadingRecentsTasks = enableRecentTasksThrottle + taskListChangeId = + recentsModel.getTasks(RecentsFilterState.EMPTY_FILTER) { tasks -> + loadingRecentsTasks = false + recentTasksLoaded = true + allRecentTasks = tasks + val oldRunningTaskdIds = runningTaskIds + val oldMinimizedTaskIds = minimizedTaskIds + desktopTasks = allRecentTasks.filterIsInstance().flatMap { it.tasks } + val runningTasksChanged = oldRunningTaskdIds != runningTaskIds + val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds + if (onRecentsOrHotseatChanged() || runningTasksChanged || minimizedTasksChanged) { + controllers.taskbarViewController.commitRunningAppsToUI() + } + if (needsRecentsTasksReload) { + Log.v(TAG, "reloadRecentTasksIfNeeded: reload recents tasks") + needsRecentsTasksReload = false + reloadRecentTasksIfNeeded() + } + } } /** @@ -462,6 +482,8 @@ class TaskbarRecentAppsController( get() = tasks.map { task -> task.key.packageName } private companion object { + private val TAG = "TaskbarRecentAppsController" + const val MAX_RECENT_TASKS = 2 } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt index 329d977fc7..da9cacedeb 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt @@ -24,7 +24,9 @@ import android.content.res.Resources import android.graphics.Rect import android.os.Process import android.os.UserHandle +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.view.Display.DEFAULT_DISPLAY import androidx.test.annotation.UiThreadTest import com.android.internal.R @@ -60,6 +62,7 @@ import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -72,6 +75,7 @@ import org.mockito.kotlin.whenever class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { @get:Rule val mockitoRule = MockitoJUnit.rule() + @get:Rule val setFlagsRule = SetFlagsRule() @get:Rule val disableControllerForCertainTestsWatcher = object : TestWatcher() { @@ -118,6 +122,16 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { recentAppsController = TaskbarRecentAppsController(mockContext, mockRecentsModel) recentAppsController.canShowRunningApps = canShowRunningAndRecentAppsAtInit recentAppsController.canShowRecentApps = canShowRunningAndRecentAppsAtInit + + // To ensure the initial getTasks() call is not seen as "loading" for the rest of the test, + // execute its callback. + doAnswer { + val callback: Consumer> = it.getArgument(1) + callback.accept(arrayListOf()) + taskListChangeId + } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) recentAppsController.init(taskbarControllers, emptyList()) taskbarControllers.onPostInit() @@ -148,7 +162,7 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)), recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), ) - verify(mockRecentsModel, never()).getTasks(any>>()) + verify(mockRecentsModel, never()).getTasks(any(), any>>()) } @Test @@ -166,6 +180,82 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) } + @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_dontCallGetTasks() { + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + // By not invoking the callback passed to getTasks() we here emulate getTasks() loading. + + recentTasksChangedListener?.onRecentTasksChanged() + + // getTasks() is only called two times overall (init + once more). + verify(mockRecentsModel, times(2)).getTasks(any(), any>>()) + } + + @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_getTasksCalledWhenLoadingDone() { + val callbackCaptor = argumentCaptor>>() + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), callbackCaptor.capture()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + // By not invoking the callback passed to getTasks() we here emulate getTasks() loading. + + recentTasksChangedListener?.onRecentTasksChanged() + callbackCaptor.lastValue.accept(emptyList()) + + // getTasks() is called again now that the first getTasks() call finished. + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + } + + @Test + @DisableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_flagDisabled_callGetTasks() { + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + // By not invoking the callback passed to getTasks() we here emulate getTasks() loading. + + recentTasksChangedListener?.onRecentTasksChanged() + + // getTasks() is called once per onRecentTasksChanged() invocation (and once at init) + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + } + + @Test + @DisableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_flagDisabled_getTasksNotCalledWhenLoadingDone() { + val callbackCaptor = argumentCaptor>>() + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), callbackCaptor.capture()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + recentTasksChangedListener?.onRecentTasksChanged() + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + + callbackCaptor.lastValue.accept(emptyList()) + + // getTasks() is called once per onRecentTasksChanged() invocation (and once at init) + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + } + @Test fun getDesktopItemState_nullItemInfo_returnsNotRunning() { setInDesktopMode(true)