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;
+        }
+    }
+}