diff options
| author | 2021-04-11 00:16:23 +0000 | |
|---|---|---|
| committer | 2021-04-11 00:16:23 +0000 | |
| commit | 080abd80f4f648f2e45fb4eb1e409bca1b128d4f (patch) | |
| tree | 98cea2c2e704763cdcc44f61e4f94960382d275b | |
| parent | 84f96c845ba75c43d7ad4679065086b79d773175 (diff) | |
| parent | 027b2188473bd48c34cdafad1705abac7fff9356 (diff) | |
Merge changes from topic "android_dynamic_performance_framework" into sc-dev
* changes:
Integrate HWUI with PerformanceHintManager
ADPF: Add HintManagerService
ADPF: PerformanceHintManager API implementation
29 files changed, 1701 insertions, 52 deletions
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<PerformanceHintManager>() { + @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<RecoverySystem>() { @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 @@ -5226,6 +5226,14 @@ public abstract class Context { /** * 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. * * @see #getSystemService(String) 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. + * + * <p>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.</p> + * + * <p>Any call in this class will change its internal data, so you must do your own thread + * safety to protect from racing.</p> + * + * <p>Note that the target work duration can be {@link #updateTargetWorkDuration(long) updated} + * if workloads change.</p> + * + * <p>After each cycle of work, the client is expected to + * {@link #reportActualWorkDuration(long) report} the actual time taken to complete.</p> + * + * <p>All timings should be in {@link SystemClock#elapsedRealtimeNanos()}.</p> + */ + 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<Long> mActualDurationNanos; + private final ArrayList<Long> mTimeStampNanos; + + /** @hide */ + public Session(IHintSession session, NanoClock elapsedRealtimeClock, long preferredRate, + long durationNanos) { + mSession = session; + mElapsedRealtimeClock = elapsedRealtimeClock; + mTargetDurationInNanos = durationNanos; + mPreferredRate = preferredRate; + mActualDurationNanos = new ArrayList<Long>(); + mTimeStampNanos = new ArrayList<Long>(); + 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. + * + * <p>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.</p> + * + * @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. + * + * <p>Once called, you should not call anything else on this object.</p> + */ + 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/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/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; + } + } +} 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<int>(FrameInfoIndex::NumIndexes) == 20, +static_assert(static_cast<int>(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 @@ -158,6 +158,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. */ #define PROPERTY_IS_EMULATOR "ro.boot.qemu" @@ -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 <renderthread/RenderProxy.h> #include <renderthread/RenderTask.h> #include <renderthread/RenderThread.h> +#include <thread/CommonPool.h> #include <utils/Color.h> #include <utils/RefBase.h> #include <utils/StrongPointer.h> #include <utils/Timers.h> #include <utils/TraceUtils.h> +#include <pthread.h> + #include <algorithm> #include <atomic> +#include <vector> #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<HintSessionWrapper> { +public: + static sp<HintSessionWrapper> create(JNIEnv* env, RenderProxy* proxy) { + if (!Properties::useHintManager) return nullptr; + + // Include UI thread (self), render thread, and thread pool. + std::vector<int> 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<jint*>(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<jlong>(targetDurationNanos)); + hasExceptionAndClear(env); + } + + void reportActualWorkDuration(long actualDurationNanos) { + if (!mSession) return; + JNIEnv* env = getenv(mVm); + env->CallStaticVoidMethod(gHardwareRenderer.clazz, + gHardwareRenderer.reportActualWorkDuration, mSession, + static_cast<jlong>(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<RootRenderNode*>(rootRenderNodePtr); ContextFactoryImpl factory(rootRenderNode); RenderProxy* proxy = new RenderProxy(translucent, rootRenderNode, &factory); + sp<HintSessionWrapper> 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<void(int64_t)> updateTargetWorkDuration, + std::function<void(int64_t)> 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<void(int64_t)> callback = std::move(mFrameCallback); mFrameCallback = nullptr; + int64_t intendedVsync = mFrameInfo[static_cast<int>(FrameInfoIndex::IntendedVsync)]; + int64_t frameDeadline = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameDeadline)]; + int64_t frameStartTime = mFrameInfo[static_cast<int>(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<void(int64_t)> updateTargetWorkDuration, + std::function<void(int64_t)> reportActualWorkDuration); void setContentDrawBounds(int left, int top, int right, int bottom) { mContentDrawBounds.set(left, top, right, bottom); } @@ -107,6 +109,10 @@ private: std::function<void(int64_t)> mFrameCallback; std::function<void(int64_t)> mFrameCompleteCallback; + + nsecs_t mLastTargetWorkDuration = 0; + std::function<void(int64_t)> mUpdateTargetWorkDuration; + std::function<void(int64_t)> 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<void(int64_t)> updateTargetWorkDuration, + std::function<void(int64_t)> 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<void(int64_t)> updateTargetWorkDuration, + std::function<void(int64_t)> 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<int> tids(THREAD_COUNT); + std::vector<std::condition_variable> 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<char, 20> 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<int> 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 <functional> #include <future> #include <mutex> +#include <vector> namespace android { namespace uirenderer { @@ -97,6 +98,8 @@ public: return task.get_future().get(); }; + static std::vector<int> getThreadIds(); + // For testing purposes only, blocks until all worker threads are parked. static void waitForIdle(); @@ -111,6 +114,8 @@ private: void workerLoop(); + std::vector<int> mWorkerThreadIds; + std::mutex mLock; std::condition_variable mCondition; int mWaitingThreads = 0; 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<Integer, ArrayMap<IBinder, AppHintSession>> 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<Integer> 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<IBinder, AppHintSession> 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<IBinder, AppHintSession> 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<IBinder, AppHintSession> 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<IBinder, AppHintSession> 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<IBinder, AppHintSession> 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 <android-base/stringprintf.h> +#include <android/hardware/power/IPower.h> +#include <android_runtime/AndroidRuntime.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedPrimitiveArray.h> +#include <powermanager/PowerHalController.h> +#include <utils/Log.h> + +#include <unistd.h> +#include <cinttypes> + +#include <sys/types.h> + +#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<int32_t> threadIds, int64_t durationNanos) { + auto result = + gPowerHalController.createHintSession(tgid, uid, std::move(threadIds), durationNanos); + if (result.isOk()) { + sp<IPowerHintSession> appSession = result.value(); + if (appSession) appSession->incStrong(env); + return reinterpret_cast<jlong>(appSession.get()); + } + return 0; +} + +static void pauseHintSession(JNIEnv* env, int64_t session_ptr) { + sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + appSession->pause(); +} + +static void resumeHintSession(JNIEnv* env, int64_t session_ptr) { + sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + appSession->resume(); +} + +static void closeHintSession(JNIEnv* env, int64_t session_ptr) { + sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + appSession->close(); + appSession->decStrong(env); +} + +static void updateTargetWorkDuration(int64_t session_ptr, int64_t targetDurationNanos) { + sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + appSession->updateTargetWorkDuration(targetDurationNanos); +} + +static void reportActualWorkDuration(int64_t session_ptr, + const std::vector<WorkDuration>& actualDurations) { + sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(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<int32_t> 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<WorkDuration> 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<jlong>(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()); + } +} |