diff options
| author | 2024-12-06 03:40:09 +0000 | |
|---|---|---|
| committer | 2024-12-06 03:40:09 +0000 | |
| commit | 570058de957c8244a138efc30d7d65f46f7f161a (patch) | |
| tree | a610031f4e7ace63f9347b1cb936669d1e3aeec4 | |
| parent | 4110ab51d8da8518b17b985528f904697de1226d (diff) | |
| parent | 86495d9b7afaf482c2e4923ebe90eb963b9e7664 (diff) | |
Merge "[6/N] WindowDecorViewHost: Warm up SCVHs" into main
7 files changed, 155 insertions, 10 deletions
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index a5205ee24d05..4c77eaf80a29 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -91,6 +91,9 @@ public class DesktopModeStatus { /** The maximum override density allowed for tasks inside the desktop. */ private static final int DESKTOP_DENSITY_MAX = 1000; + /** The number of [WindowDecorViewHost] instances to warm up on system start. */ + private static final int WINDOW_DECOR_PRE_WARM_SIZE = 2; + /** * Sysprop declaring whether to enters desktop mode by default when the windowing mode of the * display's root TaskDisplayArea is set to WINDOWING_MODE_FREEFORM. @@ -122,6 +125,14 @@ public class DesktopModeStatus { private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit"; /** + * Sysprop declaring the number of [WindowDecorViewHost] instances to warm up on system start. + * + * <p>If it is not defined, then [WINDOW_DECOR_PRE_WARM_SIZE] is used. + */ + private static final String WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP = + "persist.wm.debug.desktop_window_decor_pre_warm_size"; + + /** * Return {@code true} if veiled resizing is active. If false, fluid resizing is used. */ public static boolean isVeiledResizeEnabled() { @@ -176,6 +187,12 @@ public class DesktopModeStatus { return 0; } + /** The number of [WindowDecorViewHost] instances to warm up on system start. */ + public static int getWindowDecorPreWarmSize() { + return SystemProperties.getInt(WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP, + WINDOW_DECOR_PRE_WARM_SIZE); + } + /** * Return {@code true} if the current device supports desktop mode. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index f9e3be9c770f..0cd0f4a97bbf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -349,10 +349,13 @@ public abstract class WMShellModule { @Provides static WindowDecorViewHostSupplier<WindowDecorViewHost> provideWindowDecorViewHostSupplier( @NonNull Context context, - @ShellMainThread @NonNull CoroutineScope mainScope) { + @ShellMainThread @NonNull CoroutineScope mainScope, + @NonNull ShellInit shellInit) { final int poolSize = DesktopModeStatus.getWindowDecorScvhPoolSize(context); + final int preWarmSize = DesktopModeStatus.getWindowDecorPreWarmSize(); if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context) && poolSize > 0) { - return new PooledWindowDecorViewHostSupplier(mainScope, poolSize); + return new PooledWindowDecorViewHostSupplier( + context, mainScope, shellInit, poolSize, preWarmSize); } return new DefaultWindowDecorViewHostSupplier(mainScope); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt index adb0ba643e0d..47cfaeed6157 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt @@ -21,25 +21,57 @@ import android.util.Pools import android.view.Display import android.view.SurfaceControl import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.sysui.ShellInit import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be * expensive to recreate for each new or updated window decoration. * - * Callers can obtain a [WindowDecorViewHost] using [acquire], which will return a pooled - * object if available, or create a new instance and return it if needed. When finished using a - * [WindowDecorViewHost], it must be released using [release] to allow it to be sent back - * into the pool and reused later on. + * Callers can obtain a [WindowDecorViewHost] using [acquire], which will return a pooled object if + * available, or create a new instance and return it if needed. When finished using a + * [WindowDecorViewHost], it must be released using [release] to allow it to be sent back into the + * pool and reused later on. + * + * This class also supports pre-warming [ReusableWindowDecorViewHost] instances, which will be put + * into the pool immediately after creation. */ class PooledWindowDecorViewHostSupplier( + private val context: Context, @ShellMainThread private val mainScope: CoroutineScope, + shellInit: ShellInit, maxPoolSize: Int, + private val preWarmSize: Int, ) : WindowDecorViewHostSupplier<WindowDecorViewHost> { private val pool: Pools.Pool<WindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize) private var nextDecorViewHostId = 0 + init { + require(preWarmSize <= maxPoolSize) { "Pre-warm size should not exceed pool size" } + shellInit.addInitCallback(this::onShellInit, this) + } + + private fun onShellInit() { + if (preWarmSize <= 0) { + return + } + preWarmViewHosts(preWarmSize) + } + + private fun preWarmViewHosts(preWarmSize: Int) { + mainScope.launch { + // Applying isn't needed, as the surface was never actually shown. + val t = SurfaceControl.Transaction() + repeat(preWarmSize) { + val warmedViewHost = newInstance(context, context.display).apply { warmUp() } + // Put the warmed view host in the pool by releasing it. + release(warmedViewHost, t) + } + } + } + override fun acquire(context: Context, display: Display): WindowDecorViewHost { val pooledViewHost = pool.acquire() if (pooledViewHost != null) { @@ -64,7 +96,7 @@ class PooledWindowDecorViewHostSupplier( context = context, mainScope = mainScope, display = display, - id = nextDecorViewHostId++ + id = nextDecorViewHostId++, ) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt index bf0b1186254f..da41e1b1d8d8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt @@ -17,11 +17,15 @@ package com.android.wm.shell.windowdecor.common.viewhost import android.content.Context import android.content.res.Configuration +import android.graphics.PixelFormat import android.graphics.Region import android.view.Display import android.view.SurfaceControl import android.view.View import android.view.WindowManager +import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE +import android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH +import android.view.WindowManager.LayoutParams.TYPE_APPLICATION import android.widget.FrameLayout import androidx.tracing.Trace import com.android.internal.annotations.VisibleForTesting @@ -35,6 +39,9 @@ import kotlinx.coroutines.launch * 1) Replacing the root [View], meaning [WindowDecorViewHost.updateView] maybe be called with * different [View] instances. This is useful when reusing [WindowDecorViewHost]s instances for * vastly different view hierarchies, such as Desktop Windowing's App Handles and App Headers. + * 2) Pre-warming of the underlying [SurfaceControlViewHostAdapter]s. Useful because their creation + * and first root view assignment are expensive, which is undesirable in latency-sensitive code + * paths like during a shell transition. */ class ReusableWindowDecorViewHost( private val context: Context, @@ -44,7 +51,7 @@ class ReusableWindowDecorViewHost( @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter = SurfaceControlViewHostAdapter(context, display), -) : WindowDecorViewHost { +) : WindowDecorViewHost, Warmable { @VisibleForTesting val rootView = FrameLayout(context) private var currentUpdateJob: Job? = null @@ -52,6 +59,30 @@ class ReusableWindowDecorViewHost( override val surfaceControl: SurfaceControl get() = viewHostAdapter.rootSurface + override fun warmUp() { + if (viewHostAdapter.isInitialized()) { + // Already warmed up. + return + } + Trace.beginSection("$TAG#warmUp") + viewHostAdapter.prepareViewHost(context.resources.configuration, touchableRegion = null) + viewHostAdapter.updateView( + rootView, + WindowManager.LayoutParams( + 0 /* width*/, + 0 /* height */, + TYPE_APPLICATION, + FLAG_NOT_FOCUSABLE or FLAG_SPLIT_TOUCH, + PixelFormat.TRANSPARENT, + ) + .apply { + setTitle("View root of $TAG#$id") + setTrustedOverlay() + }, + ) + Trace.endSection() + } + override fun updateView( view: View, attrs: WindowManager.LayoutParams, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/Warmable.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/Warmable.kt new file mode 100644 index 000000000000..2cb0f891436b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/Warmable.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 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.wm.shell.windowdecor.common.viewhost + +/** + * An interface for an object that can be warmed up before it's needed. + */ +interface Warmable { + fun warmUp() +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt index 40583f80003c..92f5def508c7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt @@ -18,14 +18,19 @@ package com.android.wm.shell.windowdecor.common.viewhost import android.content.res.Configuration import android.graphics.Region import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl import android.view.View import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -38,9 +43,13 @@ import org.mockito.kotlin.mock * Build/Install/Run: atest WMShellUnitTests:PooledWindowDecorViewHostSupplierTest */ @SmallTest +@RunWithLooper @RunWith(AndroidTestingRunner::class) class PooledWindowDecorViewHostSupplierTest : ShellTestCase() { + private val testExecutor = TestShellExecutor() + private val testShellInit = ShellInit(testExecutor) + private lateinit var supplier: PooledWindowDecorViewHostSupplier @Test @@ -48,6 +57,27 @@ class PooledWindowDecorViewHostSupplierTest : ShellTestCase() { MockitoAnnotations.initMocks(this) } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun onInit_warmsAndPoolsViewHosts() = runTest { + supplier = createSupplier(maxPoolSize = 5, preWarmSize = 2) + + testExecutor.flushAll() + advanceUntilIdle() + + val viewHost1 = supplier.acquire(context, context.display) as ReusableWindowDecorViewHost + val viewHost2 = supplier.acquire(context, context.display) as ReusableWindowDecorViewHost + + // Acquired warmed up view hosts from the pool. + assertThat(viewHost1.viewHostAdapter.isInitialized()).isTrue() + assertThat(viewHost2.viewHostAdapter.isInitialized()).isTrue() + } + + @Test(expected = Throwable::class) + fun onInit_warmUpSizeExceedsPoolSize_throws() = runTest { + createSupplier(maxPoolSize = 3, preWarmSize = 4) + } + @Test fun acquire_poolBelowLimit_caches() = runTest { supplier = createSupplier(maxPoolSize = 5) @@ -97,8 +127,9 @@ class PooledWindowDecorViewHostSupplierTest : ShellTestCase() { assertThat(viewHost2.released).isTrue() } - private fun CoroutineScope.createSupplier(maxPoolSize: Int) = - PooledWindowDecorViewHostSupplier(this, maxPoolSize) + private fun CoroutineScope.createSupplier(maxPoolSize: Int, preWarmSize: Int = 0) = + PooledWindowDecorViewHostSupplier(context, this, testShellInit, maxPoolSize, preWarmSize) + .also { testShellInit.init() } private class FakeWindowDecorViewHost : WindowDecorViewHost { var released = false diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt index 245393a6d44e..d99a4825e580 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt @@ -157,6 +157,14 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { verify(reusableVH.viewHostAdapter).release(t) } + @Test + fun warmUp_addsRootView() = runTest { + val reusableVH = createReusableViewHost().apply { warmUp() } + + assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() + assertThat(reusableVH.view()).isEqualTo(reusableVH.rootView) + } + private fun CoroutineScope.createReusableViewHost() = ReusableWindowDecorViewHost( context = context, |