diff --git a/core/api/current.txt b/core/api/current.txt index 1f7da095a1a7d..1ac6d946f0540 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 47a9fbb5fe4a8..91dad2a1f13c3 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -158,12 +158,14 @@ import android.os.IBatteryPropertiesRegistrar; import android.os.IBinder; import android.os.IDumpstate; import android.os.IHardwarePropertiesManager; +import android.os.IHintManager; import android.os.IPowerManager; import android.os.IRecoverySystem; import android.os.ISystemUpdateManager; import android.os.IThermalService; import android.os.IUserManager; import android.os.IncidentManager; +import android.os.PerformanceHintManager; import android.os.PowerManager; import android.os.RecoverySystem; import android.os.ServiceManager; @@ -592,6 +594,17 @@ public final class SystemServiceRegistry { ctx.mMainThread.getHandler()); }}); + registerService(Context.PERFORMANCE_HINT_SERVICE, PerformanceHintManager.class, + new CachedServiceFetcher() { + @Override + public PerformanceHintManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder hintBinder = ServiceManager.getServiceOrThrow( + Context.PERFORMANCE_HINT_SERVICE); + IHintManager hintService = IHintManager.Stub.asInterface(hintBinder); + return new PerformanceHintManager(hintService); + }}); + registerService(Context.RECOVERY_SERVICE, RecoverySystem.class, new CachedServiceFetcher() { @Override diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 1226858bba71e..5d66cdf880b82 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -5224,6 +5224,14 @@ public abstract class Context { */ public static final String THERMAL_SERVICE = "thermalservice"; + /** + * Use with {@link #getSystemService(String)} to retrieve a + * {@link android.os.PerformanceHintManager} for accessing the performance hinting service. + * + * @see #getSystemService(String) + */ + public static final String PERFORMANCE_HINT_SERVICE = "performance_hint"; + /** * Use with {@link #getSystemService(String)} to retrieve a * {@link android.content.pm.ShortcutManager} for accessing the launcher shortcut service. diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl new file mode 100644 index 0000000000000..661b95affaf3d --- /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 0000000000000..09bc4cc4eb7e7 --- /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 0000000000000..6791844a2a007 --- /dev/null +++ b/core/java/android/os/PerformanceHintManager.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemService; +import android.content.Context; + +import com.android.internal.util.Preconditions; + +import java.io.Closeable; +import java.util.ArrayList; + +/** The PerformanceHintManager allows apps to send performance hint to system. */ +@SystemService(Context.PERFORMANCE_HINT_SERVICE) +public final class PerformanceHintManager { + private static final String TAG = "PerformanceHintManager"; + private final IHintManager mService; + // HAL preferred update rate + private final long mPreferredRate; + + /** @hide */ + public PerformanceHintManager(IHintManager service) { + mService = service; + try { + mPreferredRate = mService.getHintSessionPreferredRate(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Creates a {@link Session} for the given set of threads and sets their initial target work + * duration. + * + * @param tids The list of threads to be associated with this session. They must be part of + * this process' thread group. + * @param initialTargetWorkDurationNanos The desired duration in nanoseconds for the new + * session. + * @return the new session if it is supported on this device, null if hint session is not + * supported on this device. + */ + @Nullable + public Session createHintSession(@NonNull int[] tids, long initialTargetWorkDurationNanos) { + try { + IBinder token = new Binder(); + IHintSession session = mService.createHintSession(token, tids, + initialTargetWorkDurationNanos); + if (session == null) return null; + return new Session(session, sNanoClock, mPreferredRate, + initialTargetWorkDurationNanos); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get preferred update rate information for this device. + * + * @return the preferred update rate supported by device software. + */ + public long getPreferredUpdateRateNanos() { + return mPreferredRate; + } + + /** + * A Session represents a group of threads with an inter-related workload such that hints for + * their performance should be considered as a unit. The threads in a given session should be + * long-life and not created or destroyed dynamically. + * + *

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

+ * + *

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

+ * + *

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

+ * + *

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

+ * + *

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

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

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

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

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

+ */ + public void close() { + try { + mSession.close(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * The interface is to make the FakeClock for testing. + * @hide + */ + public interface NanoClock { + /** Gets the current nanosecond instant of the clock. */ + long nanos(); + } + + private static final NanoClock sNanoClock = new NanoClock() { + public long nanos() { + return SystemClock.elapsedRealtimeNanos(); + } + }; +} diff --git a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java new file mode 100644 index 0000000000000..7dea82d7ee54c --- /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; + } + } +}