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 <wvw@google.com>
diff --git a/core/api/current.txt b/core/api/current.txt
index 1f7da09..1ac6d94 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10644,6 +10644,7 @@
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 @@
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 47a9fbb..91dad2a 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -158,12 +158,14 @@
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 @@
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 1226858..5d66cdf 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5226,6 +5226,14 @@
/**
* 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 0000000..661b95a
--- /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 0000000..09bc4cc
--- /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 0000000..6791844
--- /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/tests/coretests/src/android/os/PerformanceHintManagerTest.java b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
new file mode 100644
index 0000000..7dea82d
--- /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;
+ }
+ }
+}