Merge "Add cpu user mode run time rate limiter per UID" into main
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 8346112..a0bc77e 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -24,6 +24,7 @@
 import static com.android.server.power.hint.Flags.powerhintThreadCleanup;
 import static com.android.server.power.hint.Flags.resetOnForkEnabled;
 
+import android.Manifest;
 import android.adpf.ISessionManager;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -59,6 +60,9 @@
 import android.os.ServiceManager;
 import android.os.SessionCreationConfig;
 import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.system.Os;
+import android.system.OsConstants;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -79,7 +83,10 @@
 import com.android.server.power.hint.HintManagerService.AppHintSession.SessionModes;
 import com.android.server.utils.Slogf;
 
+import java.io.BufferedReader;
 import java.io.FileDescriptor;
+import java.io.FileReader;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
@@ -95,6 +102,8 @@
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /** An hint service implementation that runs in System Server process. */
 public final class HintManagerService extends SystemService {
@@ -103,10 +112,10 @@
 
     private static final int EVENT_CLEAN_UP_UID = 3;
     @VisibleForTesting  static final int CLEAN_UP_UID_DELAY_MILLIS = 1000;
-    // The minimum interval between the headroom calls as rate limiting.
-    private static final int DEFAULT_GPU_HEADROOM_INTERVAL_MILLIS = 1000;
-    private static final int DEFAULT_CPU_HEADROOM_INTERVAL_MILLIS = 1000;
 
+    // example: cpu  2255 34 2290 22625563 6290 127 456
+    private static final Pattern PROC_STAT_CPU_TIME_TOTAL_PATTERN =
+            Pattern.compile("cpu\\s+(?<user>[0-9]+)\\s(?<nice>[0-9]+).+");
 
     @VisibleForTesting final long mHintSessionPreferredRate;
 
@@ -192,10 +201,26 @@
     private static final String PROPERTY_HWUI_ENABLE_HINT_MANAGER = "debug.hwui.use_hint_manager";
     private static final String PROPERTY_USE_HAL_HEADROOMS = "persist.hms.use_hal_headrooms";
     private static final String PROPERTY_CHECK_HEADROOM_TID = "persist.hms.check_headroom_tid";
-    private static final String PROPERTY_CHECK_HEADROOM_AFFINITY = "persist.hms.check_affinity";
+    private static final String PROPERTY_CHECK_HEADROOM_AFFINITY =
+            "persist.hms.check_headroom_affinity";
+    private static final String PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS =
+            "persist.hms.check_headroom_proc_stat_min_millis";
     private Boolean mFMQUsesIntegratedEventFlag = false;
 
     private final Object mCpuHeadroomLock = new Object();
+    @VisibleForTesting
+    final float mJiffyMillis;
+    private final int mCheckHeadroomProcStatMinMillis;
+    @GuardedBy("mCpuHeadroomLock")
+    private long mLastCpuUserModeTimeCheckedMillis = 0;
+    @GuardedBy("mCpuHeadroomLock")
+    private long mLastCpuUserModeJiffies = 0;
+    @GuardedBy("mCpuHeadroomLock")
+    private final Map<Integer, Long> mUidToLastUserModeJiffies;
+    @VisibleForTesting
+    private String mProcStatFilePathOverride = null;
+    @VisibleForTesting
+    private boolean mEnforceCpuHeadroomUserModeCpuTimeCheck = false;
 
     private ISessionManager mSessionManager;
 
@@ -310,8 +335,16 @@
                 new GpuHeadroomParamsInternal().calculationWindowMillis;
         if (mSupportInfo.headroom.isCpuSupported) {
             mCpuHeadroomCache = new HeadroomCache<>(2, mSupportInfo.headroom.cpuMinIntervalMillis);
+            mUidToLastUserModeJiffies = new ArrayMap<>();
+            long jiffyHz = Os.sysconf(OsConstants._SC_CLK_TCK);
+            mJiffyMillis = 1000.0f / jiffyHz;
+            mCheckHeadroomProcStatMinMillis = SystemProperties.getInt(
+                    PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS, 50);
         } else {
             mCpuHeadroomCache = null;
+            mUidToLastUserModeJiffies = null;
+            mJiffyMillis = 0.0f;
+            mCheckHeadroomProcStatMinMillis = 0;
         }
         if (mSupportInfo.headroom.isGpuSupported) {
             mGpuHeadroomCache = new HeadroomCache<>(2, mSupportInfo.headroom.gpuMinIntervalMillis);
@@ -370,6 +403,12 @@
         return supportInfo;
     }
 
+    @VisibleForTesting
+    void setProcStatPathOverride(String override) {
+        mProcStatFilePathOverride = override;
+        mEnforceCpuHeadroomUserModeCpuTimeCheck = true;
+    }
+
     private ServiceThread createCleanUpThread() {
         final ServiceThread handlerThread = new ServiceThread(TAG,
                 Process.THREAD_PRIORITY_LOWEST, true /*allowIo*/);
@@ -851,6 +890,11 @@
                         mChannelMap.remove(uid);
                     }
                 }
+                synchronized (mCpuHeadroomLock) {
+                    if (mSupportInfo.headroom.isCpuSupported && mUidToLastUserModeJiffies != null) {
+                        mUidToLastUserModeJiffies.remove(uid);
+                    }
+                }
             });
         }
 
@@ -1230,7 +1274,7 @@
             // Only call into AM if the tid is either isolated or invalid
             if (isolatedPids == null) {
                 // To avoid deadlock, do not call into AMS if the call is from system.
-                if (uid == Process.SYSTEM_UID) {
+                if (UserHandle.getAppId(uid) == Process.SYSTEM_UID) {
                     return tid;
                 }
                 isolatedPids = mAmInternal.getIsolatedProcesses(uid);
@@ -1485,14 +1529,17 @@
                 throw new UnsupportedOperationException();
             }
             checkCpuHeadroomParams(params);
+            final int uid = Binder.getCallingUid();
+            final int pid = Binder.getCallingPid();
             final CpuHeadroomParams halParams = new CpuHeadroomParams();
-            halParams.tids = new int[]{Binder.getCallingPid()};
+            halParams.tids = new int[]{pid};
             halParams.calculationType = params.calculationType;
             halParams.calculationWindowMillis = params.calculationWindowMillis;
             if (params.usesDeviceHeadroom) {
                 halParams.tids = new int[]{};
             } else if (params.tids != null && params.tids.length > 0) {
-                if (SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_TID, true)) {
+                if (UserHandle.getAppId(uid) != Process.SYSTEM_UID && SystemProperties.getBoolean(
+                        PROPERTY_CHECK_HEADROOM_TID, true)) {
                     final int tgid = Process.getThreadGroupLeader(Binder.getCallingPid());
                     for (int tid : params.tids) {
                         if (Process.getThreadGroupLeader(tid) != tgid) {
@@ -1515,6 +1562,20 @@
                     if (res != null) return res;
                 }
             }
+            final boolean shouldCheckUserModeCpuTime =
+                    mEnforceCpuHeadroomUserModeCpuTimeCheck
+                            || (UserHandle.getAppId(uid) != Process.SYSTEM_UID
+                            && mContext.checkCallingPermission(
+                            Manifest.permission.DEVICE_POWER)
+                            == PackageManager.PERMISSION_DENIED);
+
+            if (shouldCheckUserModeCpuTime) {
+                synchronized (mCpuHeadroomLock) {
+                    if (!checkPerUidUserModeCpuTimeElapsedLocked(uid)) {
+                        return null;
+                    }
+                }
+            }
             // return from HAL directly
             try {
                 final CpuHeadroomResult result = mPowerHal.getCpuHeadroom(halParams);
@@ -1528,6 +1589,11 @@
                         mCpuHeadroomCache.add(halParams, result);
                     }
                 }
+                if (shouldCheckUserModeCpuTime) {
+                    synchronized (mCpuHeadroomLock) {
+                        mUidToLastUserModeJiffies.put(uid, mLastCpuUserModeJiffies);
+                    }
+                }
                 return result;
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to get CPU headroom from Power HAL", e);
@@ -1556,6 +1622,40 @@
             }
         }
 
+        // check if there has been sufficient user mode cpu time elapsed since last call
+        // from the same uid
+        @GuardedBy("mCpuHeadroomLock")
+        private boolean checkPerUidUserModeCpuTimeElapsedLocked(int uid) {
+            // skip checking proc stat if it's within mCheckHeadroomProcStatMinMillis
+            if (System.currentTimeMillis() - mLastCpuUserModeTimeCheckedMillis
+                    > mCheckHeadroomProcStatMinMillis) {
+                try {
+                    mLastCpuUserModeJiffies = getUserModeJiffies();
+                } catch (Exception e) {
+                    Slog.e(TAG, "Failed to get user mode CPU time", e);
+                    return false;
+                }
+                mLastCpuUserModeTimeCheckedMillis = System.currentTimeMillis();
+            }
+            if (mUidToLastUserModeJiffies.containsKey(uid)) {
+                long uidLastUserModeJiffies = mUidToLastUserModeJiffies.get(uid);
+                if ((mLastCpuUserModeJiffies - uidLastUserModeJiffies) * mJiffyMillis
+                        < mSupportInfo.headroom.cpuMinIntervalMillis) {
+                    Slog.w(TAG, "UID " + uid + " is requesting CPU headroom too soon");
+                    Slog.d(TAG, "UID " + uid + " last request at "
+                            + uidLastUserModeJiffies * mJiffyMillis
+                            + "ms with device currently at "
+                            + mLastCpuUserModeJiffies * mJiffyMillis
+                            + "ms, the interval: "
+                            + (mLastCpuUserModeJiffies - uidLastUserModeJiffies)
+                            * mJiffyMillis + "ms is less than require minimum interval "
+                            + mSupportInfo.headroom.cpuMinIntervalMillis + "ms");
+                    return false;
+                }
+            }
+            return true;
+        }
+
         private void checkCpuHeadroomParams(CpuHeadroomParamsInternal params) {
             boolean calculationTypeMatched = false;
             try {
@@ -1731,6 +1831,27 @@
             }
         }
 
+        private long getUserModeJiffies() throws IOException {
+            String filePath =
+                    mProcStatFilePathOverride == null ? "/proc/stat" : mProcStatFilePathOverride;
+            try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    Matcher matcher = PROC_STAT_CPU_TIME_TOTAL_PATTERN.matcher(line.trim());
+                    if (matcher.find()) {
+                        long userJiffies = Long.parseLong(matcher.group("user"));
+                        long niceJiffies = Long.parseLong(matcher.group("nice"));
+                        Slog.d(TAG,
+                                "user: " + userJiffies + " nice: " + niceJiffies
+                                        + " total " + (userJiffies + niceJiffies));
+                        reader.close();
+                        return userJiffies + niceJiffies;
+                    }
+                }
+            }
+            throw new IllegalStateException("Can't find cpu line in " + filePath);
+        }
+
         private boolean checkGraphicsPipelineValid(SessionCreationConfig creationConfig, int uid) {
             if (creationConfig.modesToEnable == null) {
                 return true;
diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 35f421e..de6f9bd 100644
--- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -78,12 +78,15 @@
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
 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.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -93,6 +96,8 @@
 import org.mockito.stubbing.Answer;
 
 import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
 import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -111,6 +116,7 @@
  */
 public class HintManagerServiceTest {
     private static final String TAG = "HintManagerServiceTest";
+    private List<File> mFilesCreated = new ArrayList<>();
 
     private static WorkDuration makeWorkDuration(
             long timestamp, long duration, long workPeriodStartTime,
@@ -192,9 +198,9 @@
         mSupportInfo.sessionTags = -1;
         mSupportInfo.headroom = new SupportInfo.HeadroomSupportInfo();
         mSupportInfo.headroom.isCpuSupported = true;
-        mSupportInfo.headroom.cpuMinIntervalMillis = 2000;
+        mSupportInfo.headroom.cpuMinIntervalMillis = 1000;
         mSupportInfo.headroom.isGpuSupported = true;
-        mSupportInfo.headroom.gpuMinIntervalMillis = 2000;
+        mSupportInfo.headroom.gpuMinIntervalMillis = 1000;
         mSupportInfo.compositionData = new SupportInfo.CompositionDataSupportInfo();
         return mSupportInfo;
     }
@@ -243,6 +249,13 @@
         LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock);
     }
 
+    @After
+    public void tearDown() {
+        for (File file : mFilesCreated) {
+            file.delete();
+        }
+    }
+
     /**
      * Mocks the creation calls, but without support for new createHintSessionWithConfig method
      */
@@ -1327,6 +1340,58 @@
         });
     }
 
+    @Test
+    public void testCpuHeadroomCpuProcStatPath() throws Exception {
+        File dir = InstrumentationRegistry.getTargetContext().getFilesDir();
+        dir.mkdir();
+        String procStatFileStr = "mock_proc_stat";
+        File file = new File(dir, procStatFileStr);
+        mFilesCreated.add(file);
+        try (FileOutputStream output = new FileOutputStream(file)) {
+            output.write("cpu  2000 3000 4000 0 0 0 0 0 0 0".getBytes());
+        }
+        HintManagerService service = createService();
+        service.setProcStatPathOverride(file.getPath());
+
+        CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal();
+        CpuHeadroomParams halParams1 = new CpuHeadroomParams();
+        halParams1.calculationType = CpuHeadroomParams.CalculationType.MIN;
+        halParams1.tids = new int[]{Process.myPid()};
+
+        float headroom1 = 0.1f;
+        CpuHeadroomResult halRet1 = CpuHeadroomResult.globalHeadroom(headroom1);
+        when(mIPowerMock.getCpuHeadroom(eq(halParams1))).thenReturn(halRet1);
+        clearInvocations(mIPowerMock);
+        assertEquals(halRet1, service.getBinderServiceInstance().getCpuHeadroom(params1));
+        verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams1));
+        // expire the cache but cpu proc hasn't changed so we expect no value return
+        Thread.sleep(1100);
+        clearInvocations(mIPowerMock);
+        assertEquals(null, service.getBinderServiceInstance().getCpuHeadroom(params1));
+        verify(mIPowerMock, times(0)).getCpuHeadroom(eq(halParams1));
+
+        // update user jiffies with 500 equivalent jiffies, which is not sufficient cpu time
+        Thread.sleep(1100);
+        try (FileOutputStream output = new FileOutputStream(file)) {
+            output.write(("cpu  " + (2000 + (int) (500 / service.mJiffyMillis))
+                    + " 3000 4000 0 0 0 0 0 0 0").getBytes());
+        }
+        clearInvocations(mIPowerMock);
+        assertEquals(null, service.getBinderServiceInstance().getCpuHeadroom(params1));
+        verify(mIPowerMock, times(0)).getCpuHeadroom(eq(halParams1));
+
+        // update nice jiffies with 600 equivalent jiffies, now it exceeds 1000ms requirement
+        Thread.sleep(1100);
+        try (FileOutputStream output = new FileOutputStream(file)) {
+            output.write(("cpu  " + (2000 + (int) (500 / service.mJiffyMillis))
+                    + " " + +(3000 + (int) (600 / service.mJiffyMillis))
+                    + " 4000 0 0 0 0 0 0 0").getBytes());
+        }
+        clearInvocations(mIPowerMock);
+        assertEquals(halRet1, service.getBinderServiceInstance().getCpuHeadroom(params1));
+        verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams1));
+    }
+
 
     @Test
     @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
@@ -1397,8 +1462,8 @@
         verify(mIPowerMock, times(0)).getCpuHeadroom(eq(halParams3));
         verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams4));
 
-        // after 1 more second it should be served with cache still
-        Thread.sleep(1000);
+        // after 500ms more it should be served with cache
+        Thread.sleep(500);
         clearInvocations(mIPowerMock);
         assertEquals(halRet1, service.getBinderServiceInstance().getCpuHeadroom(params1));
         assertEquals(halRet2, service.getBinderServiceInstance().getCpuHeadroom(params2));
@@ -1410,8 +1475,8 @@
         verify(mIPowerMock, times(0)).getCpuHeadroom(eq(halParams3));
         verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams4));
 
-        // after 2+ seconds it should be served from HAL as it exceeds 2000 millis interval
-        Thread.sleep(1100);
+        // after 1+ seconds it should be served from HAL as it exceeds 1000 millis interval
+        Thread.sleep(600);
         clearInvocations(mIPowerMock);
         assertEquals(halRet1, service.getBinderServiceInstance().getCpuHeadroom(params1));
         assertEquals(halRet2, service.getBinderServiceInstance().getCpuHeadroom(params2));
@@ -1519,8 +1584,8 @@
         verify(mIPowerMock, times(0)).getGpuHeadroom(eq(halParams1));
         verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams2));
 
-        // after 1 more second it should be served with cache still
-        Thread.sleep(1000);
+        // after 500ms it should be served with cache
+        Thread.sleep(500);
         clearInvocations(mIPowerMock);
         assertEquals(halRet1, service.getBinderServiceInstance().getGpuHeadroom(params1));
         assertEquals(halRet2, service.getBinderServiceInstance().getGpuHeadroom(params2));
@@ -1528,8 +1593,8 @@
         verify(mIPowerMock, times(0)).getGpuHeadroom(eq(halParams1));
         verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams2));
 
-        // after 2+ seconds it should be served from HAL as it exceeds 2000 millis interval
-        Thread.sleep(1100);
+        // after 1+ seconds it should be served from HAL as it exceeds 1000 millis interval
+        Thread.sleep(600);
         clearInvocations(mIPowerMock);
         assertEquals(halRet1, service.getBinderServiceInstance().getGpuHeadroom(params1));
         assertEquals(halRet2, service.getBinderServiceInstance().getGpuHeadroom(params2));