diff options
| author | 2024-12-05 19:04:06 +0000 | |
|---|---|---|
| committer | 2024-12-05 19:04:06 +0000 | |
| commit | 283cd08f00aaec5fd02c28d1df2a1ece22050bd7 (patch) | |
| tree | 56020c79c003588f35ac67b3169fd14002b3b237 | |
| parent | 363ab6afd59a21a61a6755082a85e15b69ffd31e (diff) | |
| parent | b0e5e5b4a139f0bd4d87c885f842b88853b78cf8 (diff) | |
Merge "Add mechanism to boost the shell background thread as needed" into main
6 files changed, 309 insertions, 6 deletions
| diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/BoostExecutor.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/BoostExecutor.kt new file mode 100644 index 000000000000..498d0e406e4b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/BoostExecutor.kt @@ -0,0 +1,47 @@ +/* + * 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.common + +import android.os.Looper +import java.util.concurrent.Executor + +/** Executor implementation which can be boosted temporarily to a different thread priority.  */ +interface BoostExecutor : Executor { +    /** +     * Requests that the executor is boosted until {@link #resetBoost()} is called. +     */ +    fun setBoost() {} + +    /** +     * Requests that the executor is not boosted (only resets if there are no other boost requests +     * in progress). +     */ +    fun resetBoost() {} + +    /** +     * Returns whether the executor is boosted. +     */ +    fun isBoosted() : Boolean { +        return false +    } + +    /** +     * Returns the looper for this executor. +     */ +    fun getLooper() : Looper? { +        return Looper.myLooper() +    } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java index 736d954513b1..803f16ce39c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java @@ -16,15 +16,50 @@  package com.android.wm.shell.common; +import static android.os.Process.THREAD_PRIORITY_DEFAULT; +import static android.os.Process.setThreadPriority; +  import android.annotation.NonNull;  import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +import androidx.annotation.VisibleForTesting; + +import java.util.function.BiConsumer;  /** Executor implementation which is backed by a Handler. */  public class HandlerExecutor implements ShellExecutor { +    @NonNull      private final Handler mHandler; +    // See android.os.Process#THREAD_PRIORITY_* +    private final int mDefaultThreadPriority; +    private final int mBoostedThreadPriority; +    // Number of current requests to boost thread priority +    private int mBoostCount; +    private final Object mBoostLock = new Object(); +    // Default function for setting thread priority (tid, priority) +    private BiConsumer<Integer, Integer> mSetThreadPriorityFn = +            HandlerExecutor::setThreadPriorityInternal;      public HandlerExecutor(@NonNull Handler handler) { +        this(handler, THREAD_PRIORITY_DEFAULT, THREAD_PRIORITY_DEFAULT); +    } + +    /** +     * Used only if this executor can be boosted, if so, it can be boosted to the given +     * {@param boostPriority}. +     */ +    public HandlerExecutor(@NonNull Handler handler, int defaultThreadPriority, +            int boostedThreadPriority) {          mHandler = handler; +        mDefaultThreadPriority = defaultThreadPriority; +        mBoostedThreadPriority = boostedThreadPriority; +    } + +    @VisibleForTesting +    void replaceSetThreadPriorityFn(BiConsumer<Integer, Integer> setThreadPriorityFn) { +        mSetThreadPriorityFn = setThreadPriorityFn;      }      @Override @@ -56,9 +91,54 @@ public class HandlerExecutor implements ShellExecutor {      }      @Override +    public void setBoost() { +        synchronized (mBoostLock) { +            if (mDefaultThreadPriority == mBoostedThreadPriority) { +                // Nothing to boost +                return; +            } +            if (mBoostCount == 0) { +                mSetThreadPriorityFn.accept( +                        ((HandlerThread) mHandler.getLooper().getThread()).getThreadId(), +                        mBoostedThreadPriority); +            } +            mBoostCount++; +        } +    } + +    @Override +    public void resetBoost() { +        synchronized (mBoostLock) { +            mBoostCount--; +            if (mBoostCount == 0) { +                mSetThreadPriorityFn.accept( +                        ((HandlerThread) mHandler.getLooper().getThread()).getThreadId(), +                        mDefaultThreadPriority); +            } +        } +    } + +    @Override +    public boolean isBoosted() { +        synchronized (mBoostLock) { +            return mBoostCount > 0; +        } +    } + +    @Override +    @NonNull +    public Looper getLooper() { +        return mHandler.getLooper(); +    } + +    @Override      public void assertCurrentThread() {          if (!mHandler.getLooper().isCurrentThread()) {              throw new IllegalStateException("must be called on " + mHandler);          }      } + +    private static void setThreadPriorityInternal(Integer tid, Integer priority) { +        setThreadPriority(tid, priority); +    }  } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java index 2c2961fd4b65..9e5071e8347b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java @@ -18,15 +18,15 @@ package com.android.wm.shell.common;  import java.lang.reflect.Array;  import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor;  import java.util.concurrent.TimeUnit;  import java.util.function.Supplier;  /**   * Super basic Executor interface that adds support for delayed execution and removing callbacks. - * Intended to wrap Handler while better-supporting testing. + * Intended to wrap Handler while better-supporting testing.  Not every ShellExecutor implementation + * may support boosting.   */ -public interface ShellExecutor extends Executor { +public interface ShellExecutor extends BoostExecutor {      /**       * Executes the given runnable. If the caller is running on the same looper as this executor, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java index c5644a8f6876..d7ddbdeaa6da 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java @@ -18,6 +18,7 @@ package com.android.wm.shell.dagger;  import static android.os.Process.THREAD_PRIORITY_BACKGROUND;  import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_FOREGROUND;  import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;  import android.content.Context; @@ -205,13 +206,14 @@ public abstract class WMShellConcurrencyModule {      }      /** -     * Provides a Shell background thread Executor for low priority background tasks. +     * Provides a Shell background thread Executor for low priority background tasks.  The thread +     * may also be boosted to THREAD_PRIORITY_FOREGROUND if necessary.       */      @WMSingleton      @Provides      @ShellBackgroundThread      public static ShellExecutor provideSharedBackgroundExecutor(              @ShellBackgroundThread Handler handler) { -        return new HandlerExecutor(handler); +        return new HandlerExecutor(handler, THREAD_PRIORITY_BACKGROUND, THREAD_PRIORITY_FOREGROUND);      }  } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md index 9d015357b60b..837a6dd32ff2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md @@ -36,7 +36,8 @@ the product.    thread)    - This is always another thread even if config_enableShellMainThread is not set true    - **Note**: -    - This thread runs with `THREAD_PRIORITY_BACKGROUND` priority +    - This thread runs with `THREAD_PRIORITY_BACKGROUND` priority but can be requested to be boosted +      to `THREAD_PRIORITY_FOREGROUND`  - `ShellAnimationThread` (currently only used for Transitions and Splitscreen, but potentially all    animations could be offloaded here)  - `ShellSplashScreenThread` (only for use with splashscreens) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/HandlerExecutorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/HandlerExecutorTest.kt new file mode 100644 index 000000000000..799b48c2504f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/HandlerExecutorTest.kt @@ -0,0 +1,173 @@ +/* + * 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.common + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat +import java.util.function.BiConsumer +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.MockitoSession +import org.mockito.kotlin.whenever + +/** + * Tests for HandlerExecutor. + * + * Build/Install/Run: + *  atest WMShellUnitTests:HandlerExecutorTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class HandlerExecutorTest : ShellTestCase() { + +    class TestSetThreadPriorityFn : BiConsumer<Int, Int> { +        var lastSetPriority = UNSET_THREAD_PRIORITY +            private set +        var callCount = 0 +            private set + +        override fun accept(tid: Int, priority: Int) { +            lastSetPriority = priority +            callCount++ +        } + +        fun reset() { +            lastSetPriority = UNSET_THREAD_PRIORITY +            callCount = 0 +        } +    } + +    val testSetPriorityFn = TestSetThreadPriorityFn() + +    @Test +    fun defaultExecutorDisallowBoost() { +        val executor = createTestHandlerExecutor() + +        executor.setBoost() + +        assertThat(executor.isBoosted()).isFalse() +    } + +    @Test +    fun boostExecutor_resetWhenNotSet_expectNoOp() { +        val executor = createTestHandlerExecutor(DEFAULT_THREAD_PRIORITY, BOOSTED_THREAD_PRIORITY) +        val mockSession: MockitoSession = ExtendedMockito.mockitoSession() +            .mockStatic(android.os.Process::class.java) +            .startMocking() + +        try { +            // Try to reset and ensure we never try to set the thread priority +            executor.resetBoost() + +            assertThat(testSetPriorityFn.callCount).isEqualTo(0) +            assertThat(executor.isBoosted()).isFalse() +        } finally { +            mockSession.finishMocking() +        } +    } + +    @Test +    fun boostExecutor_setResetBoost_expectThreadPriorityUpdated() { +        val executor = createTestHandlerExecutor(DEFAULT_THREAD_PRIORITY, BOOSTED_THREAD_PRIORITY) +        val mockSession: MockitoSession = ExtendedMockito.mockitoSession() +            .mockStatic(android.os.Process::class.java) +            .startMocking() + +        try { +            // Boost and ensure the boosted thread priority is requested +            executor.setBoost() + +            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(BOOSTED_THREAD_PRIORITY) +            assertThat(testSetPriorityFn.callCount).isEqualTo(1) +            assertThat(executor.isBoosted()).isTrue() + +            // Reset and ensure the default thread priority is requested +            executor.resetBoost() + +            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(DEFAULT_THREAD_PRIORITY) +            assertThat(testSetPriorityFn.callCount).isEqualTo(2) +            assertThat(executor.isBoosted()).isFalse() +        } finally { +            mockSession.finishMocking() +        } +    } + +    @Test +    fun boostExecutor_overlappingBoost_expectResetOnlyWhenNotOverlapping() { +        val executor = createTestHandlerExecutor(DEFAULT_THREAD_PRIORITY, BOOSTED_THREAD_PRIORITY) +        val mockSession: MockitoSession = ExtendedMockito.mockitoSession() +            .mockStatic(android.os.Process::class.java) +            .startMocking() + +        try { +            // Set and ensure we only update the thread priority once +            executor.setBoost() +            executor.setBoost() + +            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(BOOSTED_THREAD_PRIORITY) +            assertThat(testSetPriorityFn.callCount).isEqualTo(1) +            assertThat(executor.isBoosted()).isTrue() + +            // Reset and ensure we are still boosted and the thread priority doesn't change +            executor.resetBoost() + +            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(BOOSTED_THREAD_PRIORITY) +            assertThat(testSetPriorityFn.callCount).isEqualTo(1) +            assertThat(executor.isBoosted()).isTrue() + +            // Reset again and ensure we update the thread priority accordingly +            executor.resetBoost() + +            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(DEFAULT_THREAD_PRIORITY) +            assertThat(testSetPriorityFn.callCount).isEqualTo(2) +            assertThat(executor.isBoosted()).isFalse() +        } finally { +            mockSession.finishMocking() +        } +    } + +    /** +     * Creates a test handler executor backed by a mocked handler thread. +     */ +    private fun createTestHandlerExecutor( +        defaultThreadPriority: Int = DEFAULT_THREAD_PRIORITY, +        boostedThreadPriority: Int = DEFAULT_THREAD_PRIORITY +    ) : HandlerExecutor { +        val handler = mock(Handler::class.java) +        val looper = mock(Looper::class.java) +        val thread = mock(HandlerThread::class.java) +        whenever(handler.looper).thenReturn(looper) +        whenever(looper.thread).thenReturn(thread) +        whenever(thread.threadId).thenReturn(1234) +        val executor = HandlerExecutor(handler, defaultThreadPriority, boostedThreadPriority) +        executor.replaceSetThreadPriorityFn(testSetPriorityFn) +        return executor +    } + +    companion object { +        private const val UNSET_THREAD_PRIORITY = 0 +        private const val DEFAULT_THREAD_PRIORITY = 1 +        private const val BOOSTED_THREAD_PRIORITY = 1000 +    } +}
\ No newline at end of file |