From 4c6e5036b78e8ec92996248f9acd792641a10044 Mon Sep 17 00:00:00 2001 From: Jimmy Shiu Date: Thu, 25 Feb 2021 14:43:17 +0800 Subject: ADPF: PerformanceHintManager API implementation Test: Manual test, run bouncy ball Test: atest FrameworksCoreTests:android.os.PerformanceHintManagerTest Bug: 158791282 Change-Id: Ia0bf2b06675b8cf57cdf1668ee90e98026416439 Signed-off-by: Wei Wang --- core/api/current.txt | 12 ++ core/java/android/app/SystemServiceRegistry.java | 13 ++ core/java/android/content/Context.java | 8 + core/java/android/os/IHintManager.aidl | 33 +++ core/java/android/os/IHintSession.aidl | 25 +++ core/java/android/os/PerformanceHintManager.java | 226 +++++++++++++++++++++ .../src/android/os/PerformanceHintManagerTest.java | 211 +++++++++++++++++++ 7 files changed, 528 insertions(+) create mode 100644 core/java/android/os/IHintManager.aidl create mode 100644 core/java/android/os/IHintSession.aidl create mode 100644 core/java/android/os/PerformanceHintManager.java create mode 100644 core/tests/coretests/src/android/os/PerformanceHintManagerTest.java diff --git a/core/api/current.txt b/core/api/current.txt index 1f7da095a1a7..1ac6d946f054 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10644,6 +10644,7 @@ package android.content { field public static final String NOTIFICATION_SERVICE = "notification"; field public static final String NSD_SERVICE = "servicediscovery"; field public static final String PEOPLE_SERVICE = "people"; + field public static final String PERFORMANCE_HINT_SERVICE = "performance_hint"; field public static final String POWER_SERVICE = "power"; field public static final String PRINT_SERVICE = "print"; field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1 @@ -31559,6 +31560,17 @@ package android.os { field public static final int PATTERN_SUFFIX = 4; // 0x4 } + public final class PerformanceHintManager { + method @Nullable public android.os.PerformanceHintManager.Session createHintSession(@NonNull int[], long); + method public long getPreferredUpdateRateNanos(); + } + + public static class PerformanceHintManager.Session implements java.io.Closeable { + method public void close(); + method public void reportActualWorkDuration(long); + method public void updateTargetWorkDuration(long); + } + public final class PersistableBundle extends android.os.BaseBundle implements java.lang.Cloneable android.os.Parcelable { ctor public PersistableBundle(); ctor public PersistableBundle(int); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 47a9fbb5fe4a..91dad2a1f13c 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -158,12 +158,14 @@ import android.os.IBatteryPropertiesRegistrar; import android.os.IBinder; import android.os.IDumpstate; import android.os.IHardwarePropertiesManager; +import android.os.IHintManager; import android.os.IPowerManager; import android.os.IRecoverySystem; import android.os.ISystemUpdateManager; import android.os.IThermalService; import android.os.IUserManager; import android.os.IncidentManager; +import android.os.PerformanceHintManager; import android.os.PowerManager; import android.os.RecoverySystem; import android.os.ServiceManager; @@ -592,6 +594,17 @@ public final class SystemServiceRegistry { ctx.mMainThread.getHandler()); }}); + registerService(Context.PERFORMANCE_HINT_SERVICE, PerformanceHintManager.class, + new CachedServiceFetcher() { + @Override + public PerformanceHintManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder hintBinder = ServiceManager.getServiceOrThrow( + Context.PERFORMANCE_HINT_SERVICE); + IHintManager hintService = IHintManager.Stub.asInterface(hintBinder); + return new PerformanceHintManager(hintService); + }}); + registerService(Context.RECOVERY_SERVICE, RecoverySystem.class, new CachedServiceFetcher() { @Override diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 1226858bba71..5d66cdf880b8 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -5224,6 +5224,14 @@ public abstract class Context { */ public static final String THERMAL_SERVICE = "thermalservice"; + /** + * Use with {@link #getSystemService(String)} to retrieve a + * {@link android.os.PerformanceHintManager} for accessing the performance hinting service. + * + * @see #getSystemService(String) + */ + public static final String PERFORMANCE_HINT_SERVICE = "performance_hint"; + /** * Use with {@link #getSystemService(String)} to retrieve a * {@link android.content.pm.ShortcutManager} for accessing the launcher shortcut service. diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl new file mode 100644 index 000000000000..661b95affaf3 --- /dev/null +++ b/core/java/android/os/IHintManager.aidl @@ -0,0 +1,33 @@ +/* + * + * Copyright 2021, 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 android.os; + +import android.os.IHintSession; + +/** {@hide} */ +interface IHintManager { + /** + * Creates a {@link Session} for the given set of threads and associates to a binder token. + */ + IHintSession createHintSession(in IBinder token, in int[] tids, long durationNanos); + + /** + * Get preferred rate limit in nano second. + */ + long getHintSessionPreferredRate(); +} diff --git a/core/java/android/os/IHintSession.aidl b/core/java/android/os/IHintSession.aidl new file mode 100644 index 000000000000..09bc4cc4eb7e --- /dev/null +++ b/core/java/android/os/IHintSession.aidl @@ -0,0 +1,25 @@ +/* + * + * Copyright 2021, 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 android.os; + +/** {@hide} */ +oneway interface IHintSession { + void updateTargetWorkDuration(long targetDurationNanos); + void reportActualWorkDuration(in long[] actualDurationNanos, in long[] timeStampNanos); + void close(); +} diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java new file mode 100644 index 000000000000..6791844a2a00 --- /dev/null +++ b/core/java/android/os/PerformanceHintManager.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2021 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 android.os; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemService; +import android.content.Context; + +import com.android.internal.util.Preconditions; + +import java.io.Closeable; +import java.util.ArrayList; + +/** The PerformanceHintManager allows apps to send performance hint to system. */ +@SystemService(Context.PERFORMANCE_HINT_SERVICE) +public final class PerformanceHintManager { + private static final String TAG = "PerformanceHintManager"; + private final IHintManager mService; + // HAL preferred update rate + private final long mPreferredRate; + + /** @hide */ + public PerformanceHintManager(IHintManager service) { + mService = service; + try { + mPreferredRate = mService.getHintSessionPreferredRate(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Creates a {@link Session} for the given set of threads and sets their initial target work + * duration. + * + * @param tids The list of threads to be associated with this session. They must be part of + * this process' thread group. + * @param initialTargetWorkDurationNanos The desired duration in nanoseconds for the new + * session. + * @return the new session if it is supported on this device, null if hint session is not + * supported on this device. + */ + @Nullable + public Session createHintSession(@NonNull int[] tids, long initialTargetWorkDurationNanos) { + try { + IBinder token = new Binder(); + IHintSession session = mService.createHintSession(token, tids, + initialTargetWorkDurationNanos); + if (session == null) return null; + return new Session(session, sNanoClock, mPreferredRate, + initialTargetWorkDurationNanos); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get preferred update rate information for this device. + * + * @return the preferred update rate supported by device software. + */ + public long getPreferredUpdateRateNanos() { + return mPreferredRate; + } + + /** + * A Session represents a group of threads with an inter-related workload such that hints for + * their performance should be considered as a unit. The threads in a given session should be + * long-life and not created or destroyed dynamically. + * + *

Each session is expected to have a periodic workload with a target duration for each + * cycle. The cycle duration is likely greater than the target work duration to allow other + * parts of the pipeline to run within the available budget. For example, a renderer thread may + * work at 60hz in order to produce frames at the display's frame but have a target work + * duration of only 6ms.

+ * + *

Any call in this class will change its internal data, so you must do your own thread + * safety to protect from racing.

+ * + *

Note that the target work duration can be {@link #updateTargetWorkDuration(long) updated} + * if workloads change.

+ * + *

After each cycle of work, the client is expected to + * {@link #reportActualWorkDuration(long) report} the actual time taken to complete.

+ * + *

All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.

+ */ + public static class Session implements Closeable { + private final IHintSession mSession; + private final NanoClock mElapsedRealtimeClock; + // Target duration for choosing update rate + private long mTargetDurationInNanos; + // HAL preferred update rate + private long mPreferredRate; + // Last update timestamp + private long mLastUpdateTimeStamp = -1L; + // Cached samples + private final ArrayList mActualDurationNanos; + private final ArrayList mTimeStampNanos; + + /** @hide */ + public Session(IHintSession session, NanoClock elapsedRealtimeClock, long preferredRate, + long durationNanos) { + mSession = session; + mElapsedRealtimeClock = elapsedRealtimeClock; + mTargetDurationInNanos = durationNanos; + mPreferredRate = preferredRate; + mActualDurationNanos = new ArrayList(); + mTimeStampNanos = new ArrayList(); + mLastUpdateTimeStamp = mElapsedRealtimeClock.nanos(); + } + + /** + * Updates this session's target duration for each cycle of work. + * + * @param targetDurationNanos the new desired duration in nanoseconds + */ + public void updateTargetWorkDuration(long targetDurationNanos) { + Preconditions.checkArgumentPositive(targetDurationNanos, "the hint target duration" + + " should be positive."); + try { + mSession.updateTargetWorkDuration(targetDurationNanos); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mTargetDurationInNanos = targetDurationNanos; + /** + * Most of the workload is target_duration dependent, so now clear the cached samples + * as they are most likely obsolete. + */ + mActualDurationNanos.clear(); + mTimeStampNanos.clear(); + mLastUpdateTimeStamp = mElapsedRealtimeClock.nanos(); + } + + /** + * Reports the actual duration for the last cycle of work. + * + *

The system will attempt to adjust the core placement of the threads within the thread + * group and/or the frequency of the core on which they are run to bring the actual duration + * close to the target duration.

+ * + * @param actualDurationNanos how long the thread group took to complete its last task in + * nanoseconds + */ + public void reportActualWorkDuration(long actualDurationNanos) { + Preconditions.checkArgumentPositive(actualDurationNanos, "the actual duration should" + + " be positive."); + final long now = mElapsedRealtimeClock.nanos(); + mActualDurationNanos.add(actualDurationNanos); + mTimeStampNanos.add(now); + + /** + * Use current sample to determine the rate limit. We can pick a shorter rate limit + * if any sample underperformed, however, it could be the lower level system is slow + * to react. So here we explicitly choose the rate limit with the latest sample. + */ + long rateLimit = + actualDurationNanos > mTargetDurationInNanos ? mPreferredRate + : 10 * mPreferredRate; + + if (now - mLastUpdateTimeStamp <= rateLimit) { + return; + } + Preconditions.checkState(mActualDurationNanos.size() == mTimeStampNanos.size()); + final int size = mActualDurationNanos.size(); + long[] actualDurationArray = new long[size]; + long[] timeStampArray = new long[size]; + for (int i = 0; i < size; i++) { + actualDurationArray[i] = mActualDurationNanos.get(i); + timeStampArray[i] = mTimeStampNanos.get(i); + } + try { + mSession.reportActualWorkDuration(actualDurationArray, timeStampArray); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mActualDurationNanos.clear(); + mTimeStampNanos.clear(); + mLastUpdateTimeStamp = now; + } + + /** + * Ends the current hint session. + * + *

Once called, you should not call anything else on this object.

+ */ + public void close() { + try { + mSession.close(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * The interface is to make the FakeClock for testing. + * @hide + */ + public interface NanoClock { + /** Gets the current nanosecond instant of the clock. */ + long nanos(); + } + + private static final NanoClock sNanoClock = new NanoClock() { + public long nanos() { + return SystemClock.elapsedRealtimeNanos(); + } + }; +} diff --git a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java new file mode 100644 index 000000000000..7dea82d7ee54 --- /dev/null +++ b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2021 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 android.os; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeNotNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.os.PerformanceHintManager.Session; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class PerformanceHintManagerTest { + private static final long RATE_1000 = 1000L; + private static final long TARGET_166 = 166L; + private static final long DEFAULT_TARGET_NS = 16666666L; + private PerformanceHintManager mPerformanceHintManager; + + @Mock + private IHintSession mIHintSessionMock; + + @Before + public void setUp() { + mPerformanceHintManager = + InstrumentationRegistry.getInstrumentation().getContext().getSystemService( + PerformanceHintManager.class); + MockitoAnnotations.initMocks(this); + } + + private Session createSession() { + return mPerformanceHintManager.createHintSession( + new int[]{Process.myPid()}, DEFAULT_TARGET_NS); + } + + @Test + public void testCreateHintSession() { + Session a = createSession(); + Session b = createSession(); + if (a == null) { + assertNull(b); + } else { + assertNotEquals(a, b); + } + } + + @Test + public void testGetPreferredUpdateRateNanos() { + if (createSession() != null) { + assertTrue(mPerformanceHintManager.getPreferredUpdateRateNanos() > 0); + } else { + assertEquals(-1, mPerformanceHintManager.getPreferredUpdateRateNanos()); + } + } + + @Test + public void testUpdateTargetWorkDuration() { + Session s = createSession(); + assumeNotNull(s); + s.updateTargetWorkDuration(100); + } + + @Test + public void testUpdateTargetWorkDurationWithNegativeDuration() { + Session s = createSession(); + assumeNotNull(s); + assertThrows(IllegalArgumentException.class, () -> { + s.updateTargetWorkDuration(-1); + }); + } + + @Test + public void testReportActualWorkDuration() { + Session s = createSession(); + assumeNotNull(s); + s.updateTargetWorkDuration(100); + s.reportActualWorkDuration(1); + s.reportActualWorkDuration(100); + s.reportActualWorkDuration(1000); + } + + @Test + public void testReportActualWorkDurationWithIllegalArgument() { + Session s = createSession(); + assumeNotNull(s); + s.updateTargetWorkDuration(100); + assertThrows(IllegalArgumentException.class, () -> { + s.reportActualWorkDuration(-1); + }); + } + + @Test + public void testRateLimitWithDurationFastEnough() throws Exception { + FakeClock fakeClock = new FakeClock(); + Session s = new Session(mIHintSessionMock, fakeClock, RATE_1000, TARGET_166); + + reset(mIHintSessionMock); + fakeClock.setNow(0); + s.updateTargetWorkDuration(TARGET_166); + + s.reportActualWorkDuration(TARGET_166 - 1); + s.reportActualWorkDuration(TARGET_166); + // we should not see update as the rate should be 10X for over-perform case. + verify(mIHintSessionMock, never()).reportActualWorkDuration(any(), any()); + fakeClock.incrementClock(10 * RATE_1000); + s.reportActualWorkDuration(TARGET_166); + verify(mIHintSessionMock, never()).reportActualWorkDuration(any(), any()); + fakeClock.incrementClock(1); + s.reportActualWorkDuration(TARGET_166); + // we should see update after rate limit + verify(mIHintSessionMock, times(1)).reportActualWorkDuration( + eq(new long[] {TARGET_166 - 1, TARGET_166, TARGET_166, TARGET_166}), + eq(new long[] {0, 0, 10 * RATE_1000, 10 * RATE_1000 + 1})); + + reset(mIHintSessionMock); + s.reportActualWorkDuration(TARGET_166); + s.reportActualWorkDuration(TARGET_166 - 1); + s.reportActualWorkDuration(TARGET_166 - 2); + // we should not see update as the rate should be 10X for over-perform case. + verify(mIHintSessionMock, never()).reportActualWorkDuration(any(), any()); + fakeClock.incrementClock(10 * RATE_1000 + 1); + s.reportActualWorkDuration(TARGET_166); + s.reportActualWorkDuration(TARGET_166 - 1); + // we should see update now + verify(mIHintSessionMock, times(1)).reportActualWorkDuration( + eq(new long[] {TARGET_166, TARGET_166 - 1, TARGET_166 - 2, TARGET_166}), + eq(new long[] {10 * RATE_1000 + 1, 10 * RATE_1000 + 1, 10 * RATE_1000 + 1, + (10 * RATE_1000 + 1) * 2})); + } + + @Test + public void testRateLimitWithDurationTooSlow() throws Exception { + FakeClock fakeClock = new FakeClock(); + Session s = new Session(mIHintSessionMock, fakeClock, RATE_1000, TARGET_166); + + reset(mIHintSessionMock); + fakeClock.setNow(0); + s.updateTargetWorkDuration(TARGET_166); + + verify(mIHintSessionMock, times(1)).updateTargetWorkDuration(eq(TARGET_166)); + // shouldn't update before rate limit + s.reportActualWorkDuration(TARGET_166 + 1); + verify(mIHintSessionMock, never()).reportActualWorkDuration(any(), any()); + + // shouldn't update when the time is exactly at rate limit + fakeClock.incrementClock(RATE_1000); + s.reportActualWorkDuration(TARGET_166 + 1); + verify(mIHintSessionMock, never()).reportActualWorkDuration(any(), any()); + + // should be ready for sending hint + fakeClock.incrementClock(1); + s.reportActualWorkDuration(TARGET_166 + 1); + verify(mIHintSessionMock, times(1)).reportActualWorkDuration( + eq(new long[] {TARGET_166 + 1, TARGET_166 + 1, TARGET_166 + 1}), + eq(new long[] {0 , RATE_1000, RATE_1000 + 1})); + } + + @Test + public void testCloseHintSession() { + Session s = createSession(); + assumeNotNull(s); + s.close(); + } + + private static class FakeClock implements PerformanceHintManager.NanoClock { + private long mCurrentTime = 0L; + + @Override + public long nanos() { + return mCurrentTime; + } + + public void setNow(long nanos) { + mCurrentTime = nanos; + } + + public void incrementClock(long nanos) { + mCurrentTime += nanos; + } + } +} -- cgit v1.2.3-59-g8ed1b From 25318b7316a72f5fd2fa170825199f985daf29b7 Mon Sep 17 00:00:00 2001 From: Jimmy Shiu Date: Mon, 15 Mar 2021 22:55:49 +0800 Subject: ADPF: Add HintManagerService Test: Manual test, run bouncy ball Test: atest HintManagerServiceTest Test: adb shell dumpsys hint Bug: 158791282 Change-Id: I50b19ab7629f006decbcddd653fb67588fc4160b Signed-off-by: Wei Wang --- .../server/power/hint/HintManagerService.java | 449 +++++++++++++++++++++ services/core/jni/Android.bp | 7 +- .../com_android_server_hint_HintManagerService.cpp | 166 ++++++++ ...om_android_server_power_PowerManagerService.cpp | 2 +- services/core/jni/onload.cpp | 2 + services/java/com/android/server/SystemServer.java | 5 + .../server/power/hint/HintManagerServiceTest.java | 268 ++++++++++++ 7 files changed, 895 insertions(+), 4 deletions(-) create mode 100644 services/core/java/com/android/server/power/hint/HintManagerService.java create mode 100644 services/core/jni/com_android_server_hint_HintManagerService.cpp create mode 100644 services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java new file mode 100644 index 000000000000..fc7628c28b8b --- /dev/null +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2021 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.server.power.hint; + +import android.app.ActivityManager; +import android.app.IUidObserver; +import android.content.Context; +import android.os.Binder; +import android.os.IBinder; +import android.os.IHintManager; +import android.os.IHintSession; +import android.os.Process; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.DumpUtils; +import com.android.internal.util.Preconditions; +import com.android.server.FgThread; +import com.android.server.SystemService; +import com.android.server.utils.Slogf; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Arrays; + +/** An hint service implementation that runs in System Server process. */ +public final class HintManagerService extends SystemService { + private static final String TAG = "HintManagerService"; + private static final boolean DEBUG = false; + @VisibleForTesting final long mHintSessionPreferredRate; + + @GuardedBy("mLock") + private final ArrayMap> mActiveSessions; + + /** Lock to protect HAL handles and listen list. */ + private final Object mLock = new Object(); + + @VisibleForTesting final UidObserver mUidObserver; + + private final NativeWrapper mNativeWrapper; + + @VisibleForTesting final IHintManager.Stub mService = new BinderService(); + + public HintManagerService(Context context) { + this(context, new Injector()); + } + + @VisibleForTesting + HintManagerService(Context context, Injector injector) { + super(context); + mActiveSessions = new ArrayMap<>(); + mNativeWrapper = injector.createNativeWrapper(); + mNativeWrapper.halInit(); + mHintSessionPreferredRate = mNativeWrapper.halGetHintSessionPreferredRate(); + mUidObserver = new UidObserver(); + } + + @VisibleForTesting + static class Injector { + NativeWrapper createNativeWrapper() { + return new NativeWrapper(); + } + } + + private boolean isHalSupported() { + return mHintSessionPreferredRate != -1; + } + + @Override + public void onStart() { + publishBinderService(Context.PERFORMANCE_HINT_SERVICE, mService, /* allowIsolated= */ true); + } + + @Override + public void onBootPhase(int phase) { + if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { + systemReady(); + } + } + + private void systemReady() { + Slogf.v(TAG, "Initializing HintManager service..."); + try { + ActivityManager.getService().registerUidObserver(mUidObserver, + ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE, + ActivityManager.PROCESS_STATE_UNKNOWN, null); + } catch (RemoteException e) { + // ignored; both services live in system_server + } + + } + + /** + * Wrapper around the static-native methods from native. + * + * This class exists to allow us to mock static native methods in our tests. If mocking static + * methods becomes easier than this in the future, we can delete this class. + */ + @VisibleForTesting + public static class NativeWrapper { + private native void nativeInit(); + + private static native long nativeCreateHintSession(int tgid, int uid, int[] tids, + long durationNanos); + + private static native void nativePauseHintSession(long halPtr); + + private static native void nativeResumeHintSession(long halPtr); + + private static native void nativeCloseHintSession(long halPtr); + + private static native void nativeUpdateTargetWorkDuration( + long halPtr, long targetDurationNanos); + + private static native void nativeReportActualWorkDuration( + long halPtr, long[] actualDurationNanos, long[] timeStampNanos); + + private static native long nativeGetHintSessionPreferredRate(); + + /** Wrapper for HintManager.nativeInit */ + public void halInit() { + nativeInit(); + } + + /** Wrapper for HintManager.nativeCreateHintSession */ + public long halCreateHintSession(int tgid, int uid, int[] tids, long durationNanos) { + return nativeCreateHintSession(tgid, uid, tids, durationNanos); + } + + /** Wrapper for HintManager.nativePauseHintSession */ + public void halPauseHintSession(long halPtr) { + nativePauseHintSession(halPtr); + } + + /** Wrapper for HintManager.nativeResumeHintSession */ + public void halResumeHintSession(long halPtr) { + nativeResumeHintSession(halPtr); + } + + /** Wrapper for HintManager.nativeCloseHintSession */ + public void halCloseHintSession(long halPtr) { + nativeCloseHintSession(halPtr); + } + + /** Wrapper for HintManager.nativeUpdateTargetWorkDuration */ + public void halUpdateTargetWorkDuration(long halPtr, long targetDurationNanos) { + nativeUpdateTargetWorkDuration(halPtr, targetDurationNanos); + } + + /** Wrapper for HintManager.nativeReportActualWorkDuration */ + public void halReportActualWorkDuration( + long halPtr, long[] actualDurationNanos, long[] timeStampNanos) { + nativeReportActualWorkDuration(halPtr, actualDurationNanos, + timeStampNanos); + } + + /** Wrapper for HintManager.nativeGetHintSessionPreferredRate */ + public long halGetHintSessionPreferredRate() { + return nativeGetHintSessionPreferredRate(); + } + } + + @VisibleForTesting + final class UidObserver extends IUidObserver.Stub { + private final SparseArray mProcStatesCache = new SparseArray<>(); + + public boolean isUidForeground(int uid) { + synchronized (mLock) { + return mProcStatesCache.get(uid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) + <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND; + } + } + + @Override + public void onUidGone(int uid, boolean disabled) { + FgThread.getHandler().post(() -> { + synchronized (mLock) { + ArrayMap tokenMap = mActiveSessions.get(uid); + if (tokenMap == null) { + return; + } + for (int i = tokenMap.size() - 1; i >= 0; i--) { + // Will remove the session from tokenMap + tokenMap.valueAt(i).close(); + } + mProcStatesCache.delete(uid); + } + }); + } + + @Override + public void onUidActive(int uid) { + } + + @Override + public void onUidIdle(int uid, boolean disabled) { + } + + /** + * The IUidObserver callback is called from the system_server, so it'll be a direct function + * call from ActivityManagerService. Do not do heavy logic here. + */ + @Override + public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { + FgThread.getHandler().post(() -> { + synchronized (mLock) { + mProcStatesCache.put(uid, procState); + ArrayMap tokenMap = mActiveSessions.get(uid); + if (tokenMap == null) { + return; + } + for (AppHintSession s : tokenMap.values()) { + s.onProcStateChanged(); + } + } + }); + } + + @Override + public void onUidCachedChanged(int uid, boolean cached) { + } + } + + @VisibleForTesting + IHintManager.Stub getBinderServiceInstance() { + return mService; + } + + private boolean checkTidValid(int tgid, int [] tids) { + // Make sure all tids belongs to the same process. + for (int threadId : tids) { + if (!Process.isThreadInProcess(tgid, threadId)) { + return false; + } + } + return true; + } + + @VisibleForTesting + final class BinderService extends IHintManager.Stub { + @Override + public IHintSession createHintSession(IBinder token, int[] tids, long durationNanos) { + if (!isHalSupported()) return null; + + java.util.Objects.requireNonNull(token); + java.util.Objects.requireNonNull(tids); + Preconditions.checkArgument(tids.length != 0, "tids should" + + " not be empty."); + + int uid = Binder.getCallingUid(); + int tid = Binder.getCallingPid(); + int pid = Process.getThreadGroupLeader(tid); + + final long identity = Binder.clearCallingIdentity(); + try { + if (!checkTidValid(pid, tids)) { + throw new SecurityException("Some tid doesn't belong to the process"); + } + + long halSessionPtr = mNativeWrapper.halCreateHintSession(pid, uid, tids, + durationNanos); + if (halSessionPtr == 0) return null; + + AppHintSession hs = new AppHintSession(uid, pid, tids, token, + halSessionPtr, durationNanos); + synchronized (mLock) { + ArrayMap tokenMap = mActiveSessions.get(uid); + if (tokenMap == null) { + tokenMap = new ArrayMap<>(1); + mActiveSessions.put(uid, tokenMap); + } + tokenMap.put(token, hs); + return hs; + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public long getHintSessionPreferredRate() { + return mHintSessionPreferredRate; + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) { + return; + } + synchronized (mLock) { + pw.println("HintSessionPreferredRate: " + mHintSessionPreferredRate); + pw.println("HAL Support: " + isHalSupported()); + pw.println("Active Sessions:"); + for (int i = 0; i < mActiveSessions.size(); i++) { + pw.println("Uid " + mActiveSessions.keyAt(i).toString() + ":"); + ArrayMap tokenMap = mActiveSessions.valueAt(i); + for (int j = 0; j < tokenMap.size(); j++) { + pw.println(" Session " + j + ":"); + tokenMap.valueAt(j).dump(pw, " "); + } + } + } + } + } + + @VisibleForTesting + final class AppHintSession extends IHintSession.Stub implements IBinder.DeathRecipient { + protected final int mUid; + protected final int mPid; + protected final int[] mThreadIds; + protected final IBinder mToken; + protected long mHalSessionPtr; + protected long mTargetDurationNanos; + protected boolean mUpdateAllowed; + + protected AppHintSession( + int uid, int pid, int[] threadIds, IBinder token, + long halSessionPtr, long durationNanos) { + mUid = uid; + mPid = pid; + mToken = token; + mThreadIds = threadIds; + mHalSessionPtr = halSessionPtr; + mTargetDurationNanos = durationNanos; + mUpdateAllowed = true; + updateHintAllowed(); + try { + token.linkToDeath(this, 0); + } catch (RemoteException e) { + mNativeWrapper.halCloseHintSession(mHalSessionPtr); + throw new IllegalStateException("Client already dead", e); + } + } + + @VisibleForTesting + boolean updateHintAllowed() { + synchronized (mLock) { + final boolean allowed = mUidObserver.isUidForeground(mUid); + if (allowed && !mUpdateAllowed) resume(); + if (!allowed && mUpdateAllowed) pause(); + mUpdateAllowed = allowed; + return mUpdateAllowed; + } + } + + @Override + public void updateTargetWorkDuration(long targetDurationNanos) { + synchronized (mLock) { + if (mHalSessionPtr == 0 || !updateHintAllowed()) { + return; + } + Preconditions.checkArgument(targetDurationNanos > 0, "Expected" + + " the target duration to be greater than 0."); + mNativeWrapper.halUpdateTargetWorkDuration(mHalSessionPtr, targetDurationNanos); + mTargetDurationNanos = targetDurationNanos; + } + } + + @Override + public void reportActualWorkDuration(long[] actualDurationNanos, long[] timeStampNanos) { + synchronized (mLock) { + if (mHalSessionPtr == 0 || !updateHintAllowed()) { + return; + } + Preconditions.checkArgument(actualDurationNanos.length != 0, "the count" + + " of hint durations shouldn't be 0."); + Preconditions.checkArgument(actualDurationNanos.length == timeStampNanos.length, + "The length of durations and timestamps should be the same."); + for (int i = 0; i < actualDurationNanos.length; i++) { + if (actualDurationNanos[i] <= 0) { + throw new IllegalArgumentException( + String.format("durations[%d]=%d should be greater than 0", + i, actualDurationNanos[i])); + } + } + mNativeWrapper.halReportActualWorkDuration(mHalSessionPtr, actualDurationNanos, + timeStampNanos); + } + } + + /** TODO: consider monitor session threads and close session if any thread is dead. */ + @Override + public void close() { + synchronized (mLock) { + if (mHalSessionPtr == 0) return; + mNativeWrapper.halCloseHintSession(mHalSessionPtr); + mHalSessionPtr = 0; + mToken.unlinkToDeath(this, 0); + ArrayMap tokenMap = mActiveSessions.get(mUid); + if (tokenMap == null) { + Slogf.w(TAG, "UID %d is note present in active session map", mUid); + } + tokenMap.remove(mToken); + if (tokenMap.isEmpty()) mActiveSessions.remove(mUid); + } + } + + private void onProcStateChanged() { + updateHintAllowed(); + } + + private void pause() { + synchronized (mLock) { + if (mHalSessionPtr == 0) return; + mNativeWrapper.halPauseHintSession(mHalSessionPtr); + } + } + + private void resume() { + synchronized (mLock) { + if (mHalSessionPtr == 0) return; + mNativeWrapper.halResumeHintSession(mHalSessionPtr); + } + } + + private void dump(PrintWriter pw, String prefix) { + synchronized (mLock) { + pw.println(prefix + "SessionPID: " + mPid); + pw.println(prefix + "SessionUID: " + mUid); + pw.println(prefix + "SessionTIDs: " + Arrays.toString(mThreadIds)); + pw.println(prefix + "SessionTargetDurationNanos: " + mTargetDurationNanos); + pw.println(prefix + "SessionAllowed: " + updateHintAllowed()); + } + } + + @Override + public void binderDied() { + close(); + } + + } +} diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 15f57653840b..a99679ade958 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -49,6 +49,7 @@ cc_library_static { "com_android_server_net_NetworkStatsService.cpp", "com_android_server_power_PowerManagerService.cpp", "com_android_server_powerstats_PowerStatsService.cpp", + "com_android_server_hint_HintManagerService.cpp", "com_android_server_SerialService.cpp", "com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp", "com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker.cpp", @@ -158,7 +159,7 @@ cc_defaults { "android.hardware.memtrack-V1-ndk_platform", "android.hardware.power@1.0", "android.hardware.power@1.1", - "android.hardware.power-V1-cpp", + "android.hardware.power-V2-cpp", "android.hardware.power.stats@1.0", "android.hardware.power.stats-V1-ndk_platform", "android.hardware.thermal@1.0", @@ -195,8 +196,8 @@ cc_defaults { "libchrome", "libmojo", ], - } - } + }, + }, } filegroup { diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp new file mode 100644 index 000000000000..000cb839002b --- /dev/null +++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2021 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. + */ + +#define TAG "HintManagerService-JNI" + +//#define LOG_NDEBUG 0 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "jni.h" + +using android::hardware::power::IPowerHintSession; +using android::hardware::power::WorkDuration; + +using android::base::StringPrintf; + +namespace android { + +static power::PowerHalController gPowerHalController; + +static jlong createHintSession(JNIEnv* env, int32_t tgid, int32_t uid, + std::vector threadIds, int64_t durationNanos) { + auto result = + gPowerHalController.createHintSession(tgid, uid, std::move(threadIds), durationNanos); + if (result.isOk()) { + sp appSession = result.value(); + if (appSession) appSession->incStrong(env); + return reinterpret_cast(appSession.get()); + } + return 0; +} + +static void pauseHintSession(JNIEnv* env, int64_t session_ptr) { + sp appSession = reinterpret_cast(session_ptr); + appSession->pause(); +} + +static void resumeHintSession(JNIEnv* env, int64_t session_ptr) { + sp appSession = reinterpret_cast(session_ptr); + appSession->resume(); +} + +static void closeHintSession(JNIEnv* env, int64_t session_ptr) { + sp appSession = reinterpret_cast(session_ptr); + appSession->close(); + appSession->decStrong(env); +} + +static void updateTargetWorkDuration(int64_t session_ptr, int64_t targetDurationNanos) { + sp appSession = reinterpret_cast(session_ptr); + appSession->updateTargetWorkDuration(targetDurationNanos); +} + +static void reportActualWorkDuration(int64_t session_ptr, + const std::vector& actualDurations) { + sp appSession = reinterpret_cast(session_ptr); + appSession->reportActualWorkDuration(actualDurations); +} + +static int64_t getHintSessionPreferredRate() { + int64_t rate = -1; + auto result = gPowerHalController.getHintSessionPreferredRate(); + if (result.isOk()) { + rate = result.value(); + } + return rate; +} + +// ---------------------------------------------------------------------------- +static void nativeInit(JNIEnv* env, jobject obj) { + gPowerHalController.init(); +} + +static jlong nativeCreateHintSession(JNIEnv* env, jclass /* clazz */, jint tgid, jint uid, + jintArray tids, jlong durationNanos) { + ScopedIntArrayRO tidArray(env, tids); + if (nullptr == tidArray.get() || tidArray.size() == 0) { + ALOGW("GetIntArrayElements returns nullptr."); + return 0; + } + std::vector threadIds(tidArray.size()); + for (size_t i = 0; i < tidArray.size(); i++) { + threadIds[i] = tidArray[i]; + } + return createHintSession(env, tgid, uid, std::move(threadIds), durationNanos); +} + +static void nativePauseHintSession(JNIEnv* env, jclass /* clazz */, jlong session_ptr) { + pauseHintSession(env, session_ptr); +} + +static void nativeResumeHintSession(JNIEnv* env, jclass /* clazz */, jlong session_ptr) { + resumeHintSession(env, session_ptr); +} + +static void nativeCloseHintSession(JNIEnv* env, jclass /* clazz */, jlong session_ptr) { + closeHintSession(env, session_ptr); +} + +static void nativeUpdateTargetWorkDuration(JNIEnv* /* env */, jclass /* clazz */, jlong session_ptr, + jlong targetDurationNanos) { + updateTargetWorkDuration(session_ptr, targetDurationNanos); +} + +static void nativeReportActualWorkDuration(JNIEnv* env, jclass /* clazz */, jlong session_ptr, + jlongArray actualDurations, jlongArray timeStamps) { + ScopedLongArrayRO arrayActualDurations(env, actualDurations); + ScopedLongArrayRO arrayTimeStamps(env, timeStamps); + + std::vector actualList(arrayActualDurations.size()); + for (size_t i = 0; i < arrayActualDurations.size(); i++) { + actualList[i].timeStampNanos = arrayTimeStamps[i]; + actualList[i].durationNanos = arrayActualDurations[i]; + } + reportActualWorkDuration(session_ptr, actualList); +} + +static jlong nativeGetHintSessionPreferredRate(JNIEnv* /* env */, jclass /* clazz */) { + return static_cast(getHintSessionPreferredRate()); +} + +// ---------------------------------------------------------------------------- +static const JNINativeMethod sHintManagerServiceMethods[] = { + /* name, signature, funcPtr */ + {"nativeInit", "()V", (void*)nativeInit}, + {"nativeCreateHintSession", "(II[IJ)J", (void*)nativeCreateHintSession}, + {"nativePauseHintSession", "(J)V", (void*)nativePauseHintSession}, + {"nativeResumeHintSession", "(J)V", (void*)nativeResumeHintSession}, + {"nativeCloseHintSession", "(J)V", (void*)nativeCloseHintSession}, + {"nativeUpdateTargetWorkDuration", "(JJ)V", (void*)nativeUpdateTargetWorkDuration}, + {"nativeReportActualWorkDuration", "(J[J[J)V", (void*)nativeReportActualWorkDuration}, + {"nativeGetHintSessionPreferredRate", "()J", (void*)nativeGetHintSessionPreferredRate}, +}; + +int register_android_server_HintManagerService(JNIEnv* env) { + return jniRegisterNativeMethods(env, + "com/android/server/power/hint/" + "HintManagerService$NativeWrapper", + sHintManagerServiceMethods, NELEM(sHintManagerServiceMethods)); +} + +} /* namespace android */ diff --git a/services/core/jni/com_android_server_power_PowerManagerService.cpp b/services/core/jni/com_android_server_power_PowerManagerService.cpp index 9b7e27d891c4..ae7ea3cd90e8 100644 --- a/services/core/jni/com_android_server_power_PowerManagerService.cpp +++ b/services/core/jni/com_android_server_power_PowerManagerService.cpp @@ -100,7 +100,7 @@ static bool setPowerMode(Mode mode, bool enabled) { ALOGD("Excessive delay in setting interactive mode to %s while turning screen %s", enabled ? "true" : "false", enabled ? "on" : "off"); } - return result == power::HalResult::SUCCESSFUL; + return result.isOk(); } void android_server_PowerManagerService_userActivity(nsecs_t eventTime, int32_t eventType, diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 03a01523b8e2..f257686cbf3d 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -30,6 +30,7 @@ int register_android_server_InputManager(JNIEnv* env); int register_android_server_LightsService(JNIEnv* env); int register_android_server_PowerManagerService(JNIEnv* env); int register_android_server_PowerStatsService(JNIEnv* env); +int register_android_server_HintManagerService(JNIEnv* env); int register_android_server_storage_AppFuse(JNIEnv* env); int register_android_server_SerialService(JNIEnv* env); int register_android_server_SystemServer(JNIEnv* env); @@ -79,6 +80,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_broadcastradio_Tuner(vm, env); register_android_server_PowerManagerService(env); register_android_server_PowerStatsService(env); + register_android_server_HintManagerService(env); register_android_server_SerialService(env); register_android_server_InputManager(env); register_android_server_LightsService(env); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 1426579a0f64..47e72ba0e6fc 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -170,6 +170,7 @@ import com.android.server.policy.role.RoleServicePlatformHelperImpl; import com.android.server.power.PowerManagerService; import com.android.server.power.ShutdownThread; import com.android.server.power.ThermalManagerService; +import com.android.server.power.hint.HintManagerService; import com.android.server.powerstats.PowerStatsService; import com.android.server.profcollect.ProfcollectForwardingService; import com.android.server.recoverysystem.RecoverySystemService; @@ -1074,6 +1075,10 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(ThermalManagerService.class); t.traceEnd(); + t.traceBegin("StartHintManager"); + mSystemServiceManager.startService(HintManagerService.class); + t.traceEnd(); + // Now that the power manager has been started, let the activity manager // initialize power management features. t.traceBegin("InitPowerManagement"); diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java new file mode 100644 index 000000000000..aaf40d7e4461 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2021 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.server.power.hint; + + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.Binder; +import android.os.IBinder; +import android.os.IHintSession; +import android.os.Process; + +import com.android.server.FgThread; +import com.android.server.power.hint.HintManagerService.AppHintSession; +import com.android.server.power.hint.HintManagerService.Injector; +import com.android.server.power.hint.HintManagerService.NativeWrapper; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link com.android.server.power.hint.HintManagerService}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:HintManagerServiceTest + */ +public class HintManagerServiceTest { + private static final long DEFAULT_HINT_PREFERRED_RATE = 16666666L; + private static final long DEFAULT_TARGET_DURATION = 16666666L; + private static final int UID = Process.myUid(); + private static final int TID = Process.myPid(); + private static final int TGID = Process.getThreadGroupLeader(TID); + private static final int[] SESSION_TIDS_A = new int[] {TID}; + private static final int[] SESSION_TIDS_B = new int[] {TID}; + private static final long[] DURATIONS_THREE = new long[] {1L, 100L, 1000L}; + private static final long[] TIMESTAMPS_THREE = new long[] {1L, 2L, 3L}; + private static final long[] DURATIONS_ZERO = new long[] {}; + private static final long[] TIMESTAMPS_ZERO = new long[] {}; + private static final long[] TIMESTAMPS_TWO = new long[] {1L, 2L}; + + @Mock private Context mContext; + @Mock private HintManagerService.NativeWrapper mNativeWrapperMock; + + private HintManagerService mService; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mNativeWrapperMock.halGetHintSessionPreferredRate()) + .thenReturn(DEFAULT_HINT_PREFERRED_RATE); + when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A), + eq(DEFAULT_TARGET_DURATION))).thenReturn(1L); + when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_B), + eq(DEFAULT_TARGET_DURATION))).thenReturn(2L); + } + + private HintManagerService createService() { + mService = new HintManagerService(mContext, new Injector() { + NativeWrapper createNativeWrapper() { + return mNativeWrapperMock; + } + }); + return mService; + } + + @Test + public void testInitializeService() { + HintManagerService service = createService(); + verify(mNativeWrapperMock).halInit(); + assertThat(service.mHintSessionPreferredRate).isEqualTo(DEFAULT_HINT_PREFERRED_RATE); + } + + @Test + public void testCreateHintSession() throws Exception { + HintManagerService service = createService(); + IBinder token = new Binder(); + + IHintSession a = service.getBinderServiceInstance().createHintSession(token, + SESSION_TIDS_A, DEFAULT_TARGET_DURATION); + assertNotNull(a); + + IHintSession b = service.getBinderServiceInstance().createHintSession(token, + SESSION_TIDS_B, DEFAULT_TARGET_DURATION); + assertNotEquals(a, b); + } + + @Test + public void testPauseResumeHintSession() throws Exception { + HintManagerService service = createService(); + IBinder token = new Binder(); + + AppHintSession a = (AppHintSession) service.getBinderServiceInstance() + .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION); + + // Set session to background and calling updateHintAllowed() would invoke pause(); + service.mUidObserver.onUidStateChanged( + a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + final Object sync = new Object(); + FgThread.getHandler().post(() -> { + synchronized (sync) { + sync.notify(); + } + }); + synchronized (sync) { + sync.wait(); + } + assumeFalse(a.updateHintAllowed()); + verify(mNativeWrapperMock, times(1)).halPauseHintSession(anyLong()); + + // Set session to foreground and calling updateHintAllowed() would invoke resume(); + service.mUidObserver.onUidStateChanged( + a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0); + FgThread.getHandler().post(() -> { + synchronized (sync) { + sync.notify(); + } + }); + synchronized (sync) { + sync.wait(); + } + assumeTrue(a.updateHintAllowed()); + verify(mNativeWrapperMock, times(1)).halResumeHintSession(anyLong()); + } + + @Test + public void testCloseHintSession() throws Exception { + HintManagerService service = createService(); + IBinder token = new Binder(); + + IHintSession a = service.getBinderServiceInstance().createHintSession(token, + SESSION_TIDS_A, DEFAULT_TARGET_DURATION); + + a.close(); + verify(mNativeWrapperMock, times(1)).halCloseHintSession(anyLong()); + } + + @Test + public void testUpdateTargetWorkDuration() throws Exception { + HintManagerService service = createService(); + IBinder token = new Binder(); + + IHintSession a = service.getBinderServiceInstance().createHintSession(token, + SESSION_TIDS_A, DEFAULT_TARGET_DURATION); + + assertThrows(IllegalArgumentException.class, () -> { + a.updateTargetWorkDuration(-1L); + }); + + assertThrows(IllegalArgumentException.class, () -> { + a.updateTargetWorkDuration(0L); + }); + + a.updateTargetWorkDuration(100L); + verify(mNativeWrapperMock, times(1)).halUpdateTargetWorkDuration(anyLong(), eq(100L)); + } + + @Test + public void testReportActualWorkDuration() throws Exception { + HintManagerService service = createService(); + IBinder token = new Binder(); + + AppHintSession a = (AppHintSession) service.getBinderServiceInstance() + .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION); + + a.updateTargetWorkDuration(100L); + a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_THREE); + verify(mNativeWrapperMock, times(1)).halReportActualWorkDuration(anyLong(), + eq(DURATIONS_THREE), eq(TIMESTAMPS_THREE)); + + assertThrows(IllegalArgumentException.class, () -> { + a.reportActualWorkDuration(DURATIONS_ZERO, TIMESTAMPS_THREE); + }); + + assertThrows(IllegalArgumentException.class, () -> { + a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_ZERO); + }); + + assertThrows(IllegalArgumentException.class, () -> { + a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_TWO); + }); + + reset(mNativeWrapperMock); + // Set session to background, then the duration would not be updated. + service.mUidObserver.onUidStateChanged( + a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); + final Object sync = new Object(); + FgThread.getHandler().post(() -> { + synchronized (sync) { + sync.notify(); + } + }); + synchronized (sync) { + sync.wait(); + } + assumeFalse(a.updateHintAllowed()); + a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_THREE); + verify(mNativeWrapperMock, never()).halReportActualWorkDuration(anyLong(), any(), any()); + } + + @Test + public void testDoHintInBackground() throws Exception { + HintManagerService service = createService(); + IBinder token = new Binder(); + + AppHintSession a = (AppHintSession) service.getBinderServiceInstance() + .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION); + + service.mUidObserver.onUidStateChanged( + a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0); + final Object sync = new Object(); + FgThread.getHandler().post(() -> { + synchronized (sync) { + sync.notify(); + } + }); + synchronized (sync) { + sync.wait(); + } + assertFalse(a.updateHintAllowed()); + } + + @Test + public void testDoHintInForeground() throws Exception { + HintManagerService service = createService(); + IBinder token = new Binder(); + + AppHintSession a = (AppHintSession) service.getBinderServiceInstance() + .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION); + + service.mUidObserver.onUidStateChanged( + a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0); + assertTrue(a.updateHintAllowed()); + } +} -- cgit v1.2.3-59-g8ed1b From 027b2188473bd48c34cdafad1705abac7fff9356 Mon Sep 17 00:00:00 2001 From: Bo Liu Date: Thu, 18 Mar 2021 16:50:38 -0400 Subject: Integrate HWUI with PerformanceHintManager PerformanceHintManager.Session is in java, so add JNI and a HintSessionWrapper class in HardwareRenderer. Then pass the two calls as two std::functions into DrawFrameTask. Note Session is created per HardwareRenderer, not global (per RenderThread). Session includes UI thread, render thread, and the thread pool. Desired duration is from the intended start duration to the frame deadline. Add an actual frame start time to compute Add system properties: debug.hwui.use_hint_manager to enable PerformanceHintManager debug.hwui.target_cpu_time_percent to control percentage of frame time to be used for target cpu duration. Test: Manual test that there are no crashes and values make sense. Bug: 158791282 Change-Id: I83f25433c10daa20033803fb7c4ae45eab34f1d3 --- core/java/android/view/Choreographer.java | 2 +- core/java/android/view/FrameMetrics.java | 23 +++--- graphics/java/android/graphics/FrameInfo.java | 8 +- .../java/android/graphics/HardwareRenderer.java | 68 +++++++++++++--- libs/hwui/FrameInfo.cpp | 29 ++----- libs/hwui/FrameInfo.h | 3 +- libs/hwui/Properties.cpp | 7 ++ libs/hwui/Properties.h | 17 ++++ .../hwui/jni/android_graphics_HardwareRenderer.cpp | 95 ++++++++++++++++++++++ libs/hwui/renderthread/DrawFrameTask.cpp | 29 +++++++ libs/hwui/renderthread/DrawFrameTask.h | 6 ++ libs/hwui/renderthread/RenderProxy.cpp | 6 ++ libs/hwui/renderthread/RenderProxy.h | 2 + libs/hwui/thread/CommonPool.cpp | 26 +++++- libs/hwui/thread/CommonPool.h | 5 ++ 15 files changed, 278 insertions(+), 48 deletions(-) diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 5c65c659e576..7b3a8a64fc37 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -737,7 +737,7 @@ public final class Choreographer { } mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos, vsyncEventData.id, - vsyncEventData.frameDeadline); + vsyncEventData.frameDeadline, startNanos); mFrameScheduled = false; mLastFrameTimeNanos = frameTimeNanos; mLastVsyncEventData = vsyncEventData; diff --git a/core/java/android/view/FrameMetrics.java b/core/java/android/view/FrameMetrics.java index eb49e52d5050..9cdf91a55c39 100644 --- a/core/java/android/view/FrameMetrics.java +++ b/core/java/android/view/FrameMetrics.java @@ -242,18 +242,19 @@ public final class FrameMetrics { int PERFORM_TRAVERSALS_START = 7; int DRAW_START = 8; int FRAME_DEADLINE = 9; - int SYNC_QUEUED = 10; - int SYNC_START = 11; - int ISSUE_DRAW_COMMANDS_START = 12; - int SWAP_BUFFERS = 13; - int FRAME_COMPLETED = 14; - int DEQUEUE_BUFFER_DURATION = 15; - int QUEUE_BUFFER_DURATION = 16; - int GPU_COMPLETED = 17; - int SWAP_BUFFERS_COMPLETED = 18; - int DISPLAY_PRESENT_TIME = 19; + int FRAME_START_TIME = 10; + int SYNC_QUEUED = 11; + int SYNC_START = 12; + int ISSUE_DRAW_COMMANDS_START = 13; + int SWAP_BUFFERS = 14; + int FRAME_COMPLETED = 15; + int DEQUEUE_BUFFER_DURATION = 16; + int QUEUE_BUFFER_DURATION = 17; + int GPU_COMPLETED = 18; + int SWAP_BUFFERS_COMPLETED = 19; + int DISPLAY_PRESENT_TIME = 20; - int FRAME_STATS_COUNT = 20; // must always be last and in sync with + int FRAME_STATS_COUNT = 21; // must always be last and in sync with // FrameInfoIndex::NumIndexes in libs/hwui/FrameInfo.h } diff --git a/graphics/java/android/graphics/FrameInfo.java b/graphics/java/android/graphics/FrameInfo.java index 189be53a397f..786c03ba5150 100644 --- a/graphics/java/android/graphics/FrameInfo.java +++ b/graphics/java/android/graphics/FrameInfo.java @@ -87,18 +87,22 @@ public final class FrameInfo { // When the frame needs to be ready by public static final int FRAME_DEADLINE = 9; + // When frame actually started. + public static final int FRAME_START_TIME = 10; + // Must be the last one // This value must be in sync with `UI_THREAD_FRAME_INFO_SIZE` in FrameInfo.h - private static final int FRAME_INFO_SIZE = FRAME_DEADLINE + 1; + private static final int FRAME_INFO_SIZE = FRAME_START_TIME + 1; /** checkstyle */ public void setVsync(long intendedVsync, long usedVsync, long frameTimelineVsyncId, - long frameDeadline) { + long frameDeadline, long frameStartTime) { frameInfo[FRAME_TIMELINE_VSYNC_ID] = frameTimelineVsyncId; frameInfo[INTENDED_VSYNC] = intendedVsync; frameInfo[VSYNC] = usedVsync; frameInfo[FLAGS] = 0; frameInfo[FRAME_DEADLINE] = frameDeadline; + frameInfo[FRAME_START_TIME] = frameStartTime; } /** checkstyle */ diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java index 88cf96a9eb4e..7589435fc3b7 100644 --- a/graphics/java/android/graphics/HardwareRenderer.java +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -28,6 +28,7 @@ import android.content.res.Configuration; import android.hardware.display.DisplayManager; import android.os.IBinder; import android.os.ParcelFileDescriptor; +import android.os.PerformanceHintManager; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; @@ -165,7 +166,7 @@ public class HardwareRenderer { * to opaque with no light source configured. */ public HardwareRenderer() { - ProcessInitializer.sInstance.initDisplayInfo(); + ProcessInitializer.sInstance.initUsingContext(); mRootNode = RenderNode.adopt(nCreateRootRenderNode()); mRootNode.setClipToBounds(false); mNativeProxy = nCreateProxy(!mOpaque, mRootNode.mNativeRenderNode); @@ -365,7 +366,8 @@ public class HardwareRenderer { */ public @NonNull FrameRenderRequest setVsyncTime(long vsyncTime) { // TODO(b/168552873): populate vsync Id once available to Choreographer public API - mFrameInfo.setVsync(vsyncTime, vsyncTime, FrameInfo.INVALID_VSYNC_ID, Long.MAX_VALUE); + mFrameInfo.setVsync(vsyncTime, vsyncTime, FrameInfo.INVALID_VSYNC_ID, Long.MAX_VALUE, + vsyncTime); mFrameInfo.addFlags(FrameInfo.FLAG_SURFACE_CANVAS); return this; } @@ -835,6 +837,36 @@ public class HardwareRenderer { callback.onPictureCaptured(picture); } + /** called by native */ + static PerformanceHintManager.Session createHintSession(int[] tids) { + PerformanceHintManager performanceHintManager = + ProcessInitializer.sInstance.getHintManager(); + if (performanceHintManager == null) { + return null; + } + // Native code will always set a target duration before reporting actual durations. + // So this is just a placeholder value that's never used. + long targetDurationNanos = 16666667; + return performanceHintManager.createHintSession(tids, targetDurationNanos); + } + + /** called by native */ + static void updateTargetWorkDuration(PerformanceHintManager.Session session, + long targetDurationNanos) { + session.updateTargetWorkDuration(targetDurationNanos); + } + + /** called by native */ + static void reportActualWorkDuration(PerformanceHintManager.Session session, + long actualDurationNanos) { + session.reportActualWorkDuration(actualDurationNanos); + } + + /** called by native */ + static void closeHintSession(PerformanceHintManager.Session session) { + session.close(); + } + /** * Interface used to receive callbacks when a frame is being drawn. * @@ -1071,6 +1103,7 @@ public class HardwareRenderer { private boolean mIsolated = false; private Context mContext; private String mPackageName; + private PerformanceHintManager mPerformanceHintManager; private IGraphicsStats mGraphicsStatsService; private IGraphicsStatsCallback mGraphicsStatsCallback = new IGraphicsStatsCallback.Stub() { @Override @@ -1082,6 +1115,10 @@ public class HardwareRenderer { private ProcessInitializer() { } + synchronized PerformanceHintManager getHintManager() { + return mPerformanceHintManager; + } + synchronized void setPackageName(String name) { if (mInitialized) return; mPackageName = name; @@ -1127,15 +1164,23 @@ public class HardwareRenderer { } } - synchronized void initDisplayInfo() { - if (mDisplayInitialized) return; + synchronized void initUsingContext() { if (mContext == null) return; - // If we're in an isolated sandbox mode then we shouldn't try to communicate with DMS + initDisplayInfo(); + + // HintManager and HintSession are designed to be accessible from isoalted processes + // so not checking for isolated process here. + initHintSession(); + + // Defensively clear out the context in case we were passed a context that can leak + // if we live longer than it, e.g. an activity context. + mContext = null; + } + + private void initDisplayInfo() { + if (mDisplayInitialized) return; if (mIsolated) { - // Defensively clear out the context in case we were passed a context that can leak - // if we live longer than it, e.g. an activity context. - mContext = null; mDisplayInitialized = true; return; } @@ -1167,11 +1212,14 @@ public class HardwareRenderer { display.getRefreshRate(), wideColorDataspace.mNativeDataspace, display.getAppVsyncOffsetNanos(), display.getPresentationDeadlineNanos()); - // Defensively clear out the context - mContext = null; mDisplayInitialized = true; } + private void initHintSession() { + if (mContext == null) return; + mPerformanceHintManager = mContext.getSystemService(PerformanceHintManager.class); + } + private void rotateBuffer() { nRotateProcessStatsBuffer(); requestBuffer(); diff --git a/libs/hwui/FrameInfo.cpp b/libs/hwui/FrameInfo.cpp index 2448cc904104..51fbf363f51c 100644 --- a/libs/hwui/FrameInfo.cpp +++ b/libs/hwui/FrameInfo.cpp @@ -21,29 +21,16 @@ namespace android { namespace uirenderer { const std::array FrameInfoNames{ - "Flags", - "FrameTimelineVsyncId", - "IntendedVsync", - "Vsync", - "InputEventId", - "HandleInputStart", - "AnimationStart", - "PerformTraversalsStart", - "DrawStart", - "FrameDeadline", - "SyncQueued", - "SyncStart", - "IssueDrawCommandsStart", - "SwapBuffers", - "FrameCompleted", - "DequeueBufferDuration", - "QueueBufferDuration", - "GpuCompleted", - "SwapBuffersCompleted", - "DisplayPresentTime", + "Flags", "FrameTimelineVsyncId", "IntendedVsync", + "Vsync", "InputEventId", "HandleInputStart", + "AnimationStart", "PerformTraversalsStart", "DrawStart", + "FrameDeadline", "FrameStartTime", "SyncQueued", + "SyncStart", "IssueDrawCommandsStart", "SwapBuffers", + "FrameCompleted", "DequeueBufferDuration", "QueueBufferDuration", + "GpuCompleted", "SwapBuffersCompleted", "DisplayPresentTime", }; -static_assert(static_cast(FrameInfoIndex::NumIndexes) == 20, +static_assert(static_cast(FrameInfoIndex::NumIndexes) == 21, "Must update value in FrameMetrics.java#FRAME_STATS_COUNT (and here)"); void FrameInfo::importUiThreadInfo(int64_t* info) { diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h index e9b2f4a9bb3b..62ac4ca5fdad 100644 --- a/libs/hwui/FrameInfo.h +++ b/libs/hwui/FrameInfo.h @@ -28,7 +28,7 @@ namespace android { namespace uirenderer { -static constexpr size_t UI_THREAD_FRAME_INFO_SIZE = 10; +static constexpr size_t UI_THREAD_FRAME_INFO_SIZE = 11; enum class FrameInfoIndex { Flags = 0, @@ -41,6 +41,7 @@ enum class FrameInfoIndex { PerformTraversalsStart, DrawStart, FrameDeadline, + FrameStartTime, // End of UI frame info SyncQueued, diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index e58f31fd15eb..7af0a221d228 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -81,6 +81,9 @@ bool Properties::isolatedProcess = false; int Properties::contextPriority = 0; float Properties::defaultSdrWhitePoint = 200.f; +bool Properties::useHintManager = true; +int Properties::targetCpuTimePercentage = 70; + bool Properties::load() { bool prevDebugLayersUpdates = debugLayersUpdates; bool prevDebugOverdraw = debugOverdraw; @@ -128,6 +131,10 @@ bool Properties::load() { runningInEmulator = base::GetBoolProperty(PROPERTY_IS_EMULATOR, false); + useHintManager = base::GetBoolProperty(PROPERTY_USE_HINT_MANAGER, true); + targetCpuTimePercentage = base::GetIntProperty(PROPERTY_TARGET_CPU_TIME_PERCENTAGE, 70); + if (targetCpuTimePercentage <= 0 || targetCpuTimePercentage > 100) targetCpuTimePercentage = 70; + return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index ea9cbd592d29..1cb87be98051 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -157,6 +157,20 @@ enum DebugLevel { */ #define PROPERTY_CAPTURE_SKP_FILENAME "debug.hwui.skp_filename" +/** + * Controls whether HWUI will send timing hints to HintManager for + * better CPU scheduling. Accepted values are "true" and "false". + */ +#define PROPERTY_USE_HINT_MANAGER "debug.hwui.use_hint_manager" + +/** + * Percentage of frame time that's used for CPU work. The rest is + * reserved for GPU work. This is used with use_hint_manager to + * provide timing hints to HintManager. Accepted values are + * integer from 1-100. + */ +#define PROPERTY_TARGET_CPU_TIME_PERCENTAGE "debug.hwui.target_cpu_time_percent" + /** * Property for whether this is running in the emulator. */ @@ -253,6 +267,9 @@ public: static float defaultSdrWhitePoint; + static bool useHintManager; + static int targetCpuTimePercentage; + private: static ProfileType sProfileType; static bool sDisableProfileBars; diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index f24ba5c1c878..bd1da985a33e 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -34,14 +34,18 @@ #include #include #include +#include #include #include #include #include #include +#include + #include #include +#include #include "android_graphics_HardwareRendererObserver.h" @@ -53,6 +57,10 @@ using namespace android::uirenderer::renderthread; struct { jclass clazz; jmethodID invokePictureCapturedCallback; + jmethodID createHintSession; + jmethodID updateTargetWorkDuration; + jmethodID reportActualWorkDuration; + jmethodID closeHintSession; } gHardwareRenderer; struct { @@ -71,6 +79,14 @@ static JNIEnv* getenv(JavaVM* vm) { return env; } +static bool hasExceptionAndClear(JNIEnv* env) { + if (GraphicsJNI::hasException(env)) { + env->ExceptionClear(); + return true; + } + return false; +} + typedef ANativeWindow* (*ANW_fromSurface)(JNIEnv* env, jobject surface); ANW_fromSurface fromSurface; @@ -120,6 +136,67 @@ private: } }; +class HintSessionWrapper : public LightRefBase { +public: + static sp create(JNIEnv* env, RenderProxy* proxy) { + if (!Properties::useHintManager) return nullptr; + + // Include UI thread (self), render thread, and thread pool. + std::vector tids = CommonPool::getThreadIds(); + tids.push_back(proxy->getRenderThreadTid()); + tids.push_back(pthread_gettid_np(pthread_self())); + + jintArray tidsArray = env->NewIntArray(tids.size()); + if (hasExceptionAndClear(env)) return nullptr; + env->SetIntArrayRegion(tidsArray, 0, tids.size(), reinterpret_cast(tids.data())); + if (hasExceptionAndClear(env)) return nullptr; + jobject session = env->CallStaticObjectMethod( + gHardwareRenderer.clazz, gHardwareRenderer.createHintSession, tidsArray); + if (hasExceptionAndClear(env) || !session) return nullptr; + return new HintSessionWrapper(env, session); + } + + ~HintSessionWrapper() { + if (!mSession) return; + JNIEnv* env = getenv(mVm); + env->CallStaticVoidMethod(gHardwareRenderer.clazz, gHardwareRenderer.closeHintSession, + mSession); + hasExceptionAndClear(env); + env->DeleteGlobalRef(mSession); + mSession = nullptr; + } + + void updateTargetWorkDuration(long targetDurationNanos) { + if (!mSession) return; + JNIEnv* env = getenv(mVm); + env->CallStaticVoidMethod(gHardwareRenderer.clazz, + gHardwareRenderer.updateTargetWorkDuration, mSession, + static_cast(targetDurationNanos)); + hasExceptionAndClear(env); + } + + void reportActualWorkDuration(long actualDurationNanos) { + if (!mSession) return; + JNIEnv* env = getenv(mVm); + env->CallStaticVoidMethod(gHardwareRenderer.clazz, + gHardwareRenderer.reportActualWorkDuration, mSession, + static_cast(actualDurationNanos)); + hasExceptionAndClear(env); + } + +private: + HintSessionWrapper(JNIEnv* env, jobject jobject) { + env->GetJavaVM(&mVm); + if (jobject) { + mSession = env->NewGlobalRef(jobject); + LOG_ALWAYS_FATAL_IF(!mSession, "Failed to make global ref"); + } + } + + JavaVM* mVm = nullptr; + jobject mSession = nullptr; +}; + static void android_view_ThreadedRenderer_rotateProcessStatsBuffer(JNIEnv* env, jobject clazz) { RenderProxy::rotateProcessStatsBuffer(); } @@ -147,6 +224,12 @@ static jlong android_view_ThreadedRenderer_createProxy(JNIEnv* env, jobject claz RootRenderNode* rootRenderNode = reinterpret_cast(rootRenderNodePtr); ContextFactoryImpl factory(rootRenderNode); RenderProxy* proxy = new RenderProxy(translucent, rootRenderNode, &factory); + sp wrapper = HintSessionWrapper::create(env, proxy); + if (wrapper) { + proxy->setHintSessionCallbacks( + [wrapper](int64_t nanos) { wrapper->updateTargetWorkDuration(nanos); }, + [wrapper](int64_t nanos) { wrapper->reportActualWorkDuration(nanos); }); + } return (jlong) proxy; } @@ -769,6 +852,18 @@ int register_android_view_ThreadedRenderer(JNIEnv* env) { gHardwareRenderer.invokePictureCapturedCallback = GetStaticMethodIDOrDie(env, hardwareRenderer, "invokePictureCapturedCallback", "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V"); + gHardwareRenderer.createHintSession = + GetStaticMethodIDOrDie(env, hardwareRenderer, "createHintSession", + "([I)Landroid/os/PerformanceHintManager$Session;"); + gHardwareRenderer.updateTargetWorkDuration = + GetStaticMethodIDOrDie(env, hardwareRenderer, "updateTargetWorkDuration", + "(Landroid/os/PerformanceHintManager$Session;J)V"); + gHardwareRenderer.reportActualWorkDuration = + GetStaticMethodIDOrDie(env, hardwareRenderer, "reportActualWorkDuration", + "(Landroid/os/PerformanceHintManager$Session;J)V"); + gHardwareRenderer.closeHintSession = + GetStaticMethodIDOrDie(env, hardwareRenderer, "closeHintSession", + "(Landroid/os/PerformanceHintManager$Session;)V"); jclass frameCallbackClass = FindClassOrDie(env, "android/graphics/HardwareRenderer$FrameDrawingCallback"); diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp index 3408ffda3f9d..7a38a3bc9c05 100644 --- a/libs/hwui/renderthread/DrawFrameTask.cpp +++ b/libs/hwui/renderthread/DrawFrameTask.cpp @@ -21,6 +21,7 @@ #include "../DeferredLayerUpdater.h" #include "../DisplayList.h" +#include "../Properties.h" #include "../RenderNode.h" #include "CanvasContext.h" #include "RenderThread.h" @@ -44,6 +45,12 @@ void DrawFrameTask::setContext(RenderThread* thread, CanvasContext* context, mTargetNode = targetNode; } +void DrawFrameTask::setHintSessionCallbacks(std::function updateTargetWorkDuration, + std::function reportActualWorkDuration) { + mUpdateTargetWorkDuration = std::move(updateTargetWorkDuration); + mReportActualWorkDuration = std::move(reportActualWorkDuration); +} + void DrawFrameTask::pushLayerUpdate(DeferredLayerUpdater* layer) { LOG_ALWAYS_FATAL_IF(!mContext, "Lifecycle violation, there's no context to pushLayerUpdate with!"); @@ -102,6 +109,9 @@ void DrawFrameTask::run() { CanvasContext* context = mContext; std::function callback = std::move(mFrameCallback); mFrameCallback = nullptr; + int64_t intendedVsync = mFrameInfo[static_cast(FrameInfoIndex::IntendedVsync)]; + int64_t frameDeadline = mFrameInfo[static_cast(FrameInfoIndex::FrameDeadline)]; + int64_t frameStartTime = mFrameInfo[static_cast(FrameInfoIndex::FrameStartTime)]; // From this point on anything in "this" is *UNSAFE TO ACCESS* if (canUnblockUiThread) { @@ -124,6 +134,25 @@ void DrawFrameTask::run() { if (!canUnblockUiThread) { unblockUiThread(); } + + // These member callbacks are effectively const as they are set once during init, so it's safe + // to use these directly instead of making local copies. + if (mUpdateTargetWorkDuration && mReportActualWorkDuration) { + constexpr int64_t kSanityCheckLowerBound = 100000; // 0.1ms + constexpr int64_t kSanityCheckUpperBound = 10000000000; // 10s + int64_t targetWorkDuration = frameDeadline - intendedVsync; + targetWorkDuration = targetWorkDuration * Properties::targetCpuTimePercentage / 100; + if (targetWorkDuration > kSanityCheckLowerBound && + targetWorkDuration < kSanityCheckUpperBound && + targetWorkDuration != mLastTargetWorkDuration) { + mLastTargetWorkDuration = targetWorkDuration; + mUpdateTargetWorkDuration(targetWorkDuration); + } + int64_t frameDuration = systemTime(SYSTEM_TIME_MONOTONIC) - frameStartTime; + if (frameDuration > kSanityCheckLowerBound && frameDuration < kSanityCheckUpperBound) { + mReportActualWorkDuration(frameDuration); + } + } } bool DrawFrameTask::syncFrameState(TreeInfo& info) { diff --git a/libs/hwui/renderthread/DrawFrameTask.h b/libs/hwui/renderthread/DrawFrameTask.h index 696cfaef3cd7..3bb574aff59b 100644 --- a/libs/hwui/renderthread/DrawFrameTask.h +++ b/libs/hwui/renderthread/DrawFrameTask.h @@ -61,6 +61,8 @@ public: virtual ~DrawFrameTask(); void setContext(RenderThread* thread, CanvasContext* context, RenderNode* targetNode); + void setHintSessionCallbacks(std::function updateTargetWorkDuration, + std::function reportActualWorkDuration); void setContentDrawBounds(int left, int top, int right, int bottom) { mContentDrawBounds.set(left, top, right, bottom); } @@ -107,6 +109,10 @@ private: std::function mFrameCallback; std::function mFrameCompleteCallback; + + nsecs_t mLastTargetWorkDuration = 0; + std::function mUpdateTargetWorkDuration; + std::function mReportActualWorkDuration; }; } /* namespace renderthread */ diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index 423cc08189ca..9361abd20852 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -76,6 +76,12 @@ void RenderProxy::setName(const char* name) { mRenderThread.queue().runSync([this, name]() { mContext->setName(std::string(name)); }); } +void RenderProxy::setHintSessionCallbacks(std::function updateTargetWorkDuration, + std::function reportActualWorkDuration) { + mDrawFrameTask.setHintSessionCallbacks(std::move(updateTargetWorkDuration), + std::move(reportActualWorkDuration)); +} + void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) { if (window) { ANativeWindow_acquire(window); } mRenderThread.queue().post([this, win = window, enableTimeout]() mutable { diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h index 366d6b5a172c..8d55d3c55570 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -71,6 +71,8 @@ public: void setSwapBehavior(SwapBehavior swapBehavior); bool loadSystemProperties(); void setName(const char* name); + void setHintSessionCallbacks(std::function updateTargetWorkDuration, + std::function reportActualWorkDuration); void setSurface(ANativeWindow* window, bool enableTimeout = true); void setSurfaceControl(ASurfaceControl* surfaceControl); diff --git a/libs/hwui/thread/CommonPool.cpp b/libs/hwui/thread/CommonPool.cpp index d011bdfe945e..dc92f9f0d39a 100644 --- a/libs/hwui/thread/CommonPool.cpp +++ b/libs/hwui/thread/CommonPool.cpp @@ -29,14 +29,23 @@ CommonPool::CommonPool() { ATRACE_CALL(); CommonPool* pool = this; + std::mutex mLock; + std::vector tids(THREAD_COUNT); + std::vector tidConditionVars(THREAD_COUNT); + // Create 2 workers for (int i = 0; i < THREAD_COUNT; i++) { - std::thread worker([pool, i] { + std::thread worker([pool, i, &mLock, &tids, &tidConditionVars] { { std::array name{"hwuiTask"}; snprintf(name.data(), name.size(), "hwuiTask%d", i); auto self = pthread_self(); pthread_setname_np(self, name.data()); + { + std::unique_lock lock(mLock); + tids[i] = pthread_gettid_np(self); + tidConditionVars[i].notify_one(); + } setpriority(PRIO_PROCESS, 0, PRIORITY_FOREGROUND); auto startHook = renderthread::RenderThread::getOnStartHook(); if (startHook) { @@ -47,6 +56,15 @@ CommonPool::CommonPool() { }); worker.detach(); } + { + std::unique_lock lock(mLock); + for (int i = 0; i < THREAD_COUNT; i++) { + while (!tids[i]) { + tidConditionVars[i].wait(lock); + } + } + } + mWorkerThreadIds = std::move(tids); } CommonPool& CommonPool::instance() { @@ -58,6 +76,10 @@ void CommonPool::post(Task&& task) { instance().enqueue(std::move(task)); } +std::vector CommonPool::getThreadIds() { + return instance().mWorkerThreadIds; +} + void CommonPool::enqueue(Task&& task) { std::unique_lock lock(mLock); while (!mWorkQueue.hasSpace()) { @@ -104,4 +126,4 @@ void CommonPool::doWaitForIdle() { } } // namespace uirenderer -} // namespace android \ No newline at end of file +} // namespace android diff --git a/libs/hwui/thread/CommonPool.h b/libs/hwui/thread/CommonPool.h index 7603eeef4692..74f852bd1413 100644 --- a/libs/hwui/thread/CommonPool.h +++ b/libs/hwui/thread/CommonPool.h @@ -25,6 +25,7 @@ #include #include #include +#include namespace android { namespace uirenderer { @@ -97,6 +98,8 @@ public: return task.get_future().get(); }; + static std::vector getThreadIds(); + // For testing purposes only, blocks until all worker threads are parked. static void waitForIdle(); @@ -111,6 +114,8 @@ private: void workerLoop(); + std::vector mWorkerThreadIds; + std::mutex mLock; std::condition_variable mCondition; int mWaitingThreads = 0; -- cgit v1.2.3-59-g8ed1b