Merge "[6/N] WindowDecorViewHost: Warm up SCVHs" into main
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 a5205ee..4c77eaf 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 @@
/** 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 @@
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 @@
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 f9e3be9..0cd0f4a 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 @@
@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 adb0ba6..47cfaee 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.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 @@
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 bf0b118..da41e1b 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 @@
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 @@
* 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 @@
@VisibleForTesting
val viewHostAdapter: SurfaceControlViewHostAdapter =
SurfaceControlViewHostAdapter(context, display),
-) : WindowDecorViewHost {
+) : WindowDecorViewHost, Warmable {
@VisibleForTesting val rootView = FrameLayout(context)
private var currentUpdateJob: Job? = null
@@ -52,6 +59,30 @@
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 0000000..2cb0f89
--- /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 40583f8..92f5def 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 @@
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 @@
* 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 @@
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 @@
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 245393a..d99a482 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 @@
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,