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,