summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Xiang Wang <xwxw@google.com> 2023-10-24 10:48:31 -0700
committer Xiang Wang <xwxw@google.com> 2024-03-19 11:32:53 -0700
commitf63a6af05269eef76671e9533e578ac7a6e43e51 (patch)
tree45e6c4aa5300f98abf91b3d71ddefb14942bab48
parent25530406d7584bb9997bb5469c421d5b9547bedc (diff)
Add support for ADPF hint session tid cleanup
Bug: 309701979 Test: atest HintManagerServiceTest Change-Id: I94440e52efd54918d1e293e02aabb6e7820510df
-rw-r--r--core/java/android/os/Process.java52
-rw-r--r--core/jni/android_util_Process.cpp37
-rw-r--r--services/core/Android.bp1
-rw-r--r--services/core/java/com/android/server/power/hint/Android.bp12
-rw-r--r--services/core/java/com/android/server/power/hint/HintManagerService.java378
-rw-r--r--services/core/java/com/android/server/power/hint/flags.aconfig8
-rw-r--r--services/java/com/android/server/SystemServer.java8
-rw-r--r--services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java193
-rw-r--r--services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING15
9 files changed, 652 insertions, 52 deletions
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 7020a38ed08a..db06a6ba0ef5 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -48,6 +48,7 @@ import libcore.io.IoUtils;
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
/**
@@ -588,6 +589,8 @@ public class Process {
**/
public static final int THREAD_GROUP_RESTRICTED = 7;
+ /** @hide */
+ public static final int SIGNAL_DEFAULT = 0;
public static final int SIGNAL_QUIT = 3;
public static final int SIGNAL_KILL = 9;
public static final int SIGNAL_USR1 = 10;
@@ -1437,6 +1440,49 @@ public class Process {
sendSignal(pid, SIGNAL_KILL);
}
+ /**
+ * Check the tgid and tid pair to see if the tid still exists and belong to the tgid.
+ *
+ * TOCTOU warning: the status of the tid can change at the time this method returns. This should
+ * be used in very rare cases such as checking if a (tid, tgid) pair that is known to exist
+ * recently no longer exists now. As the possibility of the same tid to be reused under the same
+ * tgid during a short window is rare. And even if it happens the caller logic should be robust
+ * to handle it without error.
+ *
+ * @throws IllegalArgumentException if tgid or tid is not positive.
+ * @throws SecurityException if the caller doesn't have the permission, this method is expected
+ * to be used by system process with {@link #SYSTEM_UID} because it
+ * internally uses tkill(2).
+ * @throws NoSuchElementException if the Linux process with pid as the tid has exited or it
+ * doesn't belong to the tgid.
+ * @hide
+ */
+ public static final void checkTid(int tgid, int tid)
+ throws IllegalArgumentException, SecurityException, NoSuchElementException {
+ sendTgSignalThrows(tgid, tid, SIGNAL_DEFAULT);
+ }
+
+ /**
+ * Check if the pid still exists.
+ *
+ * TOCTOU warning: the status of the pid can change at the time this method returns. This should
+ * be used in very rare cases such as checking if a pid that belongs to an isolated process of a
+ * uid known to exist recently no longer exists now. As the possibility of the same pid to be
+ * reused again under the same uid during a short window is rare. And even if it happens the
+ * caller logic should be robust to handle it without error.
+ *
+ * @throws IllegalArgumentException if pid is not positive.
+ * @throws SecurityException if the caller doesn't have the permission, this method is expected
+ * to be used by system process with {@link #SYSTEM_UID} because it
+ * internally uses kill(2).
+ * @throws NoSuchElementException if the Linux process with the pid has exited.
+ * @hide
+ */
+ public static final void checkPid(int pid)
+ throws IllegalArgumentException, SecurityException, NoSuchElementException {
+ sendSignalThrows(pid, SIGNAL_DEFAULT);
+ }
+
/** @hide */
public static final native int setUid(int uid);
@@ -1451,6 +1497,12 @@ public class Process {
*/
public static final native void sendSignal(int pid, int signal);
+ private static native void sendSignalThrows(int pid, int signal)
+ throws IllegalArgumentException, SecurityException, NoSuchElementException;
+
+ private static native void sendTgSignalThrows(int pid, int tgid, int signal)
+ throws IllegalArgumentException, SecurityException, NoSuchElementException;
+
/**
* @hide
* Private impl for avoiding a log message... DO NOT USE without doing
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index d2e58bb62c46..982189e30beb 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -1137,6 +1137,41 @@ void android_os_Process_sendSignalQuiet(JNIEnv* env, jobject clazz, jint pid, ji
}
}
+void android_os_Process_sendSignalThrows(JNIEnv* env, jobject clazz, jint pid, jint sig) {
+ if (pid <= 0) {
+ jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", "Invalid argument: pid(%d)",
+ pid);
+ return;
+ }
+ int ret = kill(pid, sig);
+ if (ret < 0) {
+ if (errno == ESRCH) {
+ jniThrowExceptionFmt(env, "java/util/NoSuchElementException",
+ "Process with pid %d not found", pid);
+ } else {
+ signalExceptionForError(env, errno, pid);
+ }
+ }
+}
+
+void android_os_Process_sendTgSignalThrows(JNIEnv* env, jobject clazz, jint tgid, jint tid,
+ jint sig) {
+ if (tgid <= 0 || tid <= 0) {
+ jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException",
+ "Invalid argument: tgid(%d), tid(%d)", tid, tgid);
+ return;
+ }
+ int ret = tgkill(tgid, tid, sig);
+ if (ret < 0) {
+ if (errno == ESRCH) {
+ jniThrowExceptionFmt(env, "java/util/NoSuchElementException",
+ "Process with tid %d and tgid %d not found", tid, tgid);
+ } else {
+ signalExceptionForError(env, errno, tid);
+ }
+ }
+}
+
static jlong android_os_Process_getElapsedCpuTime(JNIEnv* env, jobject clazz)
{
struct timespec ts;
@@ -1357,6 +1392,8 @@ static const JNINativeMethod methods[] = {
{"setGid", "(I)I", (void*)android_os_Process_setGid},
{"sendSignal", "(II)V", (void*)android_os_Process_sendSignal},
{"sendSignalQuiet", "(II)V", (void*)android_os_Process_sendSignalQuiet},
+ {"sendSignalThrows", "(II)V", (void*)android_os_Process_sendSignalThrows},
+ {"sendTgSignalThrows", "(III)V", (void*)android_os_Process_sendTgSignalThrows},
{"setProcessFrozen", "(IIZ)V", (void*)android_os_Process_setProcessFrozen},
{"getFreeMemory", "()J", (void*)android_os_Process_getFreeMemory},
{"getTotalMemory", "()J", (void*)android_os_Process_getTotalMemory},
diff --git a/services/core/Android.bp b/services/core/Android.bp
index d1d7ee7ba0e4..7f5867fb1a74 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -242,6 +242,7 @@ java_library_static {
"apache-commons-math",
"backstage_power_flags_lib",
"notification_flags_lib",
+ "power_hint_flags_lib",
"biometrics_flags_lib",
"am_flags_lib",
"com_android_server_accessibility_flags_lib",
diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp
new file mode 100644
index 000000000000..8a98de673c3d
--- /dev/null
+++ b/services/core/java/com/android/server/power/hint/Android.bp
@@ -0,0 +1,12 @@
+aconfig_declarations {
+ name: "power_hint_flags",
+ package: "com.android.server.power.hint",
+ srcs: [
+ "flags.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "power_hint_flags_lib",
+ aconfig_declarations: "power_hint_flags",
+}
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 aa1a41eee220..3f1b1c1e99df 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -17,6 +17,7 @@
package com.android.server.power.hint;
import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
+import static com.android.server.power.hint.Flags.powerhintThreadCleanup;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -26,9 +27,12 @@ import android.app.UidObserver;
import android.content.Context;
import android.hardware.power.WorkDuration;
import android.os.Binder;
+import android.os.Handler;
import android.os.IBinder;
import android.os.IHintManager;
import android.os.IHintSession;
+import android.os.Looper;
+import android.os.Message;
import android.os.PerformanceHintManager;
import android.os.Process;
import android.os.RemoteException;
@@ -36,6 +40,8 @@ import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.IntArray;
+import android.util.Slog;
import android.util.SparseIntArray;
import android.util.StatsEvent;
@@ -46,20 +52,31 @@ import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.Preconditions;
import com.android.server.FgThread;
import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
import com.android.server.SystemService;
import com.android.server.utils.Slogf;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
/** 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;
+
+ private static final int EVENT_CLEAN_UP_UID = 3;
+ @VisibleForTesting static final int CLEAN_UP_UID_DELAY_MILLIS = 1000;
+
+
@VisibleForTesting final long mHintSessionPreferredRate;
// Multi-level map storing all active AppHintSessions.
@@ -73,9 +90,15 @@ public final class HintManagerService extends SystemService {
/** Lock to protect HAL handles and listen list. */
private final Object mLock = new Object();
+ @GuardedBy("mNonIsolatedTidsLock")
+ private final Map<Integer, Set<Long>> mNonIsolatedTids;
+
+ private final Object mNonIsolatedTidsLock = new Object();
+
@VisibleForTesting final MyUidObserver mUidObserver;
private final NativeWrapper mNativeWrapper;
+ private final CleanUpHandler mCleanUpHandler;
private final ActivityManagerInternal mAmInternal;
@@ -94,6 +117,13 @@ public final class HintManagerService extends SystemService {
HintManagerService(Context context, Injector injector) {
super(context);
mContext = context;
+ if (powerhintThreadCleanup()) {
+ mCleanUpHandler = new CleanUpHandler(createCleanUpThread().getLooper());
+ mNonIsolatedTids = new HashMap<>();
+ } else {
+ mCleanUpHandler = null;
+ mNonIsolatedTids = null;
+ }
mActiveSessions = new ArrayMap<>();
mNativeWrapper = injector.createNativeWrapper();
mNativeWrapper.halInit();
@@ -103,6 +133,13 @@ public final class HintManagerService extends SystemService {
LocalServices.getService(ActivityManagerInternal.class));
}
+ private ServiceThread createCleanUpThread() {
+ final ServiceThread handlerThread = new ServiceThread(TAG,
+ Process.THREAD_PRIORITY_LOWEST, true /*allowIo*/);
+ handlerThread.start();
+ return handlerThread;
+ }
+
@VisibleForTesting
static class Injector {
NativeWrapper createNativeWrapper() {
@@ -306,7 +343,18 @@ public final class HintManagerService extends SystemService {
public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
FgThread.getHandler().post(() -> {
synchronized (mCacheLock) {
- mProcStatesCache.put(uid, procState);
+ if (powerhintThreadCleanup()) {
+ final boolean before = isUidForeground(uid);
+ mProcStatesCache.put(uid, procState);
+ final boolean after = isUidForeground(uid);
+ if (before != after) {
+ final Message msg = mCleanUpHandler.obtainMessage(EVENT_CLEAN_UP_UID,
+ uid);
+ mCleanUpHandler.sendMessageDelayed(msg, CLEAN_UP_UID_DELAY_MILLIS);
+ }
+ } else {
+ mProcStatesCache.put(uid, procState);
+ }
}
boolean shouldAllowUpdate = isUidForeground(uid);
synchronized (mLock) {
@@ -314,9 +362,10 @@ public final class HintManagerService extends SystemService {
if (tokenMap == null) {
return;
}
- for (ArraySet<AppHintSession> sessionSet : tokenMap.values()) {
- for (AppHintSession s : sessionSet) {
- s.onProcStateChanged(shouldAllowUpdate);
+ for (int i = tokenMap.size() - 1; i >= 0; i--) {
+ final ArraySet<AppHintSession> sessionSet = tokenMap.valueAt(i);
+ for (int j = sessionSet.size() - 1; j >= 0; j--) {
+ sessionSet.valueAt(j).onProcStateChanged(shouldAllowUpdate);
}
}
}
@@ -324,52 +373,237 @@ public final class HintManagerService extends SystemService {
}
}
+ final class CleanUpHandler extends Handler {
+ // status of processed tid used for caching
+ private static final int TID_NOT_CHECKED = 0;
+ private static final int TID_PASSED_CHECK = 1;
+ private static final int TID_EXITED = 2;
+
+ CleanUpHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == EVENT_CLEAN_UP_UID) {
+ if (hasEqualMessages(msg.what, msg.obj)) {
+ removeEqualMessages(msg.what, msg.obj);
+ final Message newMsg = obtainMessage(msg.what, msg.obj);
+ sendMessageDelayed(newMsg, CLEAN_UP_UID_DELAY_MILLIS);
+ return;
+ }
+ final int uid = (int) msg.obj;
+ boolean isForeground = mUidObserver.isUidForeground(uid);
+ // store all sessions in a list and release the global lock
+ // we don't need to worry about stale data or racing as the session is synchronized
+ // itself and will perform its own closed status check in setThreads call
+ final List<AppHintSession> sessions;
+ synchronized (mLock) {
+ final ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap =
+ mActiveSessions.get(uid);
+ if (tokenMap == null || tokenMap.isEmpty()) {
+ return;
+ }
+ sessions = new ArrayList<>(tokenMap.size());
+ for (int i = tokenMap.size() - 1; i >= 0; i--) {
+ final ArraySet<AppHintSession> set = tokenMap.valueAt(i);
+ for (int j = set.size() - 1; j >= 0; j--) {
+ sessions.add(set.valueAt(j));
+ }
+ }
+ }
+ final long[] durationList = new long[sessions.size()];
+ final int[] invalidTidCntList = new int[sessions.size()];
+ final SparseIntArray checkedTids = new SparseIntArray();
+ int[] totalTidCnt = new int[1];
+ for (int i = sessions.size() - 1; i >= 0; i--) {
+ final AppHintSession session = sessions.get(i);
+ final long start = System.nanoTime();
+ try {
+ final int invalidCnt = cleanUpSession(session, checkedTids, totalTidCnt);
+ final long elapsed = System.nanoTime() - start;
+ invalidTidCntList[i] = invalidCnt;
+ durationList[i] = elapsed;
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to clean up session " + session.mHalSessionPtr
+ + " for UID " + session.mUid);
+ }
+ }
+ logCleanUpMetrics(uid, invalidTidCntList, durationList, sessions.size(),
+ totalTidCnt[0], isForeground);
+ }
+ }
+
+ private void logCleanUpMetrics(int uid, int[] count, long[] durationNsList, int sessionCnt,
+ int totalTidCnt, boolean isForeground) {
+ int maxInvalidTidCnt = Integer.MIN_VALUE;
+ int totalInvalidTidCnt = 0;
+ for (int i = 0; i < count.length; i++) {
+ totalInvalidTidCnt += count[i];
+ maxInvalidTidCnt = Math.max(maxInvalidTidCnt, count[i]);
+ }
+ if (DEBUG || totalInvalidTidCnt > 0) {
+ Arrays.sort(durationNsList);
+ long totalDurationNs = 0;
+ for (int i = 0; i < durationNsList.length; i++) {
+ totalDurationNs += durationNsList[i];
+ }
+ int totalDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(totalDurationNs);
+ int maxDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(
+ durationNsList[durationNsList.length - 1]);
+ int minDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(durationNsList[0]);
+ int avgDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(
+ totalDurationNs / durationNsList.length);
+ int th90DurationUs = (int) TimeUnit.NANOSECONDS.toMicros(
+ durationNsList[(int) (durationNsList.length * 0.9)]);
+ Slog.d(TAG,
+ "Invalid tid found for UID" + uid + " in " + totalDurationUs + "us:\n\t"
+ + "count("
+ + " session: " + sessionCnt
+ + " totalTid: " + totalTidCnt
+ + " maxInvalidTid: " + maxInvalidTidCnt
+ + " totalInvalidTid: " + totalInvalidTidCnt + ")\n\t"
+ + "time per session("
+ + " min: " + minDurationUs + "us"
+ + " max: " + maxDurationUs + "us"
+ + " avg: " + avgDurationUs + "us"
+ + " 90%: " + th90DurationUs + "us" + ")\n\t"
+ + "isForeground: " + isForeground);
+ }
+ }
+
+ // This will check if each TID currently linked to the session still exists. If it's
+ // previously registered as not an isolated process, then it will run tkill(pid, tid, 0) to
+ // verify that it's still running under the same pid. Otherwise, it will run
+ // kill(tid, 0) to only check if it exists. The result will be cached in checkedTids
+ // map with tid as the key and checked status as value.
+ public int cleanUpSession(AppHintSession session, SparseIntArray checkedTids, int[] total) {
+ if (session.isClosed()) {
+ return 0;
+ }
+ final int pid = session.mPid;
+ final int[] tids = session.getTidsInternal();
+ if (total != null && total.length == 1) {
+ total[0] += tids.length;
+ }
+ final IntArray filtered = new IntArray(tids.length);
+ for (int i = 0; i < tids.length; i++) {
+ int tid = tids[i];
+ if (checkedTids.get(tid, 0) != TID_NOT_CHECKED) {
+ if (checkedTids.get(tid) == TID_PASSED_CHECK) {
+ filtered.add(tid);
+ }
+ continue;
+ }
+ // if it was registered as a non-isolated then we perform more restricted check
+ final boolean isNotIsolated;
+ synchronized (mNonIsolatedTidsLock) {
+ isNotIsolated = mNonIsolatedTids.containsKey(tid);
+ }
+ try {
+ if (isNotIsolated) {
+ Process.checkTid(pid, tid);
+ } else {
+ Process.checkPid(tid);
+ }
+ checkedTids.put(tid, TID_PASSED_CHECK);
+ filtered.add(tid);
+ } catch (NoSuchElementException e) {
+ checkedTids.put(tid, TID_EXITED);
+ } catch (Exception e) {
+ Slog.w(TAG, "Unexpected exception when checking TID " + tid + " under PID "
+ + pid + "(isolated: " + !isNotIsolated + ")", e);
+ // if anything unexpected happens then we keep it, but don't store it as checked
+ filtered.add(tid);
+ }
+ }
+ final int diff = tids.length - filtered.size();
+ if (diff > 0) {
+ synchronized (session) {
+ // in case thread list is updated during the cleanup then we skip updating
+ // the session but just return the number for reporting purpose
+ final int[] newTids = session.getTidsInternal();
+ if (newTids.length != tids.length) {
+ Slog.d(TAG, "Skipped cleaning up the session as new tids are added");
+ return diff;
+ }
+ Arrays.sort(newTids);
+ Arrays.sort(tids);
+ if (!Arrays.equals(newTids, tids)) {
+ Slog.d(TAG, "Skipped cleaning up the session as new tids are updated");
+ return diff;
+ }
+ Slog.d(TAG, "Cleaned up " + diff + " invalid tids for session "
+ + session.mHalSessionPtr + " with UID " + session.mUid + "\n\t"
+ + "before: " + Arrays.toString(tids) + "\n\t"
+ + "after: " + filtered);
+ final int[] filteredTids = filtered.toArray();
+ if (filteredTids.length == 0) {
+ session.mShouldForcePause = true;
+ if (session.mUpdateAllowed) {
+ session.pause();
+ }
+ } else {
+ session.setThreadsInternal(filteredTids, false);
+ }
+ }
+ }
+ return diff;
+ }
+ }
+
@VisibleForTesting
IHintManager.Stub getBinderServiceInstance() {
return mService;
}
// returns the first invalid tid or null if not found
- private Integer checkTidValid(int uid, int tgid, int [] tids) {
+ private Integer checkTidValid(int uid, int tgid, int [] tids, IntArray nonIsolated) {
// Make sure all tids belongs to the same UID (including isolated UID),
// tids can belong to different application processes.
List<Integer> isolatedPids = null;
- for (int threadId : tids) {
+ for (int i = 0; i < tids.length; i++) {
+ int tid = tids[i];
final String[] procStatusKeys = new String[] {
"Uid:",
"Tgid:"
};
long[] output = new long[procStatusKeys.length];
- Process.readProcLines("/proc/" + threadId + "/status", procStatusKeys, output);
+ Process.readProcLines("/proc/" + tid + "/status", procStatusKeys, output);
int uidOfThreadId = (int) output[0];
int pidOfThreadId = (int) output[1];
- // use PID check for isolated processes, use UID check for non-isolated processes.
- if (pidOfThreadId == tgid || uidOfThreadId == uid) {
+ // use PID check for non-isolated processes
+ if (nonIsolated != null && pidOfThreadId == tgid) {
+ nonIsolated.add(tid);
+ continue;
+ }
+ // use UID check for isolated processes.
+ if (uidOfThreadId == uid) {
continue;
}
// 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) {
- return threadId;
+ return tid;
}
isolatedPids = mAmInternal.getIsolatedProcesses(uid);
if (isolatedPids == null) {
- return threadId;
+ return tid;
}
}
if (isolatedPids.contains(pidOfThreadId)) {
continue;
}
- return threadId;
+ return tid;
}
return null;
}
private String formatTidCheckErrMsg(int callingUid, int[] tids, Integer invalidTid) {
return "Tid" + invalidTid + " from list " + Arrays.toString(tids)
- + " doesn't belong to the calling application" + callingUid;
+ + " doesn't belong to the calling application " + callingUid;
}
@VisibleForTesting
@@ -387,7 +621,10 @@ public final class HintManagerService extends SystemService {
final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
final long identity = Binder.clearCallingIdentity();
try {
- final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids);
+ final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray(tids.length)
+ : null;
+ final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids,
+ nonIsolated);
if (invalidTid != null) {
final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid);
Slogf.w(TAG, errMsg);
@@ -396,6 +633,14 @@ public final class HintManagerService extends SystemService {
long halSessionPtr = mNativeWrapper.halCreateHintSession(callingTgid, callingUid,
tids, durationNanos);
+ if (powerhintThreadCleanup()) {
+ synchronized (mNonIsolatedTidsLock) {
+ for (int i = nonIsolated.size() - 1; i >= 0; i--) {
+ mNonIsolatedTids.putIfAbsent(nonIsolated.get(i), new ArraySet<>());
+ mNonIsolatedTids.get(nonIsolated.get(i)).add(halSessionPtr);
+ }
+ }
+ }
if (halSessionPtr == 0) {
return null;
}
@@ -482,6 +727,7 @@ public final class HintManagerService extends SystemService {
protected boolean mUpdateAllowed;
protected int[] mNewThreadIds;
protected boolean mPowerEfficient;
+ protected boolean mShouldForcePause;
private enum SessionModes {
POWER_EFFICIENCY,
@@ -498,6 +744,7 @@ public final class HintManagerService extends SystemService {
mTargetDurationNanos = durationNanos;
mUpdateAllowed = true;
mPowerEfficient = false;
+ mShouldForcePause = false;
final boolean allowed = mUidObserver.isUidForeground(mUid);
updateHintAllowed(allowed);
try {
@@ -511,7 +758,7 @@ public final class HintManagerService extends SystemService {
@VisibleForTesting
boolean updateHintAllowed(boolean allowed) {
synchronized (this) {
- if (allowed && !mUpdateAllowed) resume();
+ if (allowed && !mUpdateAllowed && !mShouldForcePause) resume();
if (!allowed && mUpdateAllowed) pause();
mUpdateAllowed = allowed;
return mUpdateAllowed;
@@ -521,7 +768,7 @@ public final class HintManagerService extends SystemService {
@Override
public void updateTargetWorkDuration(long targetDurationNanos) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(targetDurationNanos > 0, "Expected"
@@ -534,7 +781,7 @@ public final class HintManagerService extends SystemService {
@Override
public void reportActualWorkDuration(long[] actualDurationNanos, long[] timeStampNanos) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(actualDurationNanos.length != 0, "the count"
@@ -581,12 +828,25 @@ public final class HintManagerService extends SystemService {
if (sessionSet.isEmpty()) tokenMap.remove(mToken);
if (tokenMap.isEmpty()) mActiveSessions.remove(mUid);
}
+ if (powerhintThreadCleanup()) {
+ synchronized (mNonIsolatedTidsLock) {
+ final int[] tids = getTidsInternal();
+ for (int tid : tids) {
+ if (mNonIsolatedTids.containsKey(tid)) {
+ mNonIsolatedTids.get(tid).remove(mHalSessionPtr);
+ if (mNonIsolatedTids.get(tid).isEmpty()) {
+ mNonIsolatedTids.remove(tid);
+ }
+ }
+ }
+ }
+ }
}
@Override
public void sendHint(@PerformanceHintManager.Session.Hint int hint) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(hint >= 0, "the hint ID value should be"
@@ -596,33 +856,60 @@ public final class HintManagerService extends SystemService {
}
public void setThreads(@NonNull int[] tids) {
+ setThreadsInternal(tids, true);
+ }
+
+ private void setThreadsInternal(int[] tids, boolean checkTid) {
+ if (tids.length == 0) {
+ throw new IllegalArgumentException("Thread id list can't be empty.");
+ }
+
synchronized (this) {
if (mHalSessionPtr == 0) {
return;
}
- if (tids.length == 0) {
- throw new IllegalArgumentException("Thread id list can't be empty.");
- }
- final int callingUid = Binder.getCallingUid();
- final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
- final long identity = Binder.clearCallingIdentity();
- try {
- final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids);
- if (invalidTid != null) {
- final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid);
- Slogf.w(TAG, errMsg);
- throw new SecurityException(errMsg);
- }
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
if (!mUpdateAllowed) {
Slogf.v(TAG, "update hint not allowed, storing tids.");
mNewThreadIds = tids;
+ mShouldForcePause = false;
return;
}
+ if (checkTid) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
+ final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray() : null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids,
+ nonIsolated);
+ if (invalidTid != null) {
+ final String errMsg = formatTidCheckErrMsg(callingUid, tids,
+ invalidTid);
+ Slogf.w(TAG, errMsg);
+ throw new SecurityException(errMsg);
+ }
+ if (powerhintThreadCleanup()) {
+ synchronized (mNonIsolatedTidsLock) {
+ for (int i = nonIsolated.size() - 1; i >= 0; i--) {
+ mNonIsolatedTids.putIfAbsent(nonIsolated.get(i),
+ new ArraySet<>());
+ mNonIsolatedTids.get(nonIsolated.get(i)).add(mHalSessionPtr);
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
mNativeWrapper.halSetThreads(mHalSessionPtr, tids);
mThreadIds = tids;
+ mNewThreadIds = null;
+ // if the update is allowed but the session is force paused by tid clean up, then
+ // it's waiting for this tid update to resume
+ if (mShouldForcePause) {
+ resume();
+ mShouldForcePause = false;
+ }
}
}
@@ -632,10 +919,24 @@ public final class HintManagerService extends SystemService {
}
}
+ @VisibleForTesting
+ int[] getTidsInternal() {
+ synchronized (this) {
+ return mNewThreadIds != null ? Arrays.copyOf(mNewThreadIds, mNewThreadIds.length)
+ : Arrays.copyOf(mThreadIds, mThreadIds.length);
+ }
+ }
+
+ boolean isClosed() {
+ synchronized (this) {
+ return mHalSessionPtr == 0;
+ }
+ }
+
@Override
public void setMode(int mode, boolean enabled) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(mode >= 0, "the mode Id value should be"
@@ -650,13 +951,13 @@ public final class HintManagerService extends SystemService {
@Override
public void reportActualWorkDuration2(WorkDuration[] workDurations) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(workDurations.length != 0, "the count"
+ " of work durations shouldn't be 0.");
- for (WorkDuration workDuration : workDurations) {
- validateWorkDuration(workDuration);
+ for (int i = 0; i < workDurations.length; i++) {
+ validateWorkDuration(workDurations[i]);
}
mNativeWrapper.halReportActualWorkDuration(mHalSessionPtr, workDurations);
}
@@ -743,6 +1044,7 @@ public final class HintManagerService extends SystemService {
pw.println(prefix + "SessionTIDs: " + Arrays.toString(mThreadIds));
pw.println(prefix + "SessionTargetDurationNanos: " + mTargetDurationNanos);
pw.println(prefix + "SessionAllowed: " + mUpdateAllowed);
+ pw.println(prefix + "SessionForcePaused: " + mShouldForcePause);
pw.println(prefix + "PowerEfficient: " + (mPowerEfficient ? "true" : "false"));
}
}
diff --git a/services/core/java/com/android/server/power/hint/flags.aconfig b/services/core/java/com/android/server/power/hint/flags.aconfig
new file mode 100644
index 000000000000..f4afcb141b19
--- /dev/null
+++ b/services/core/java/com/android/server/power/hint/flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.power.hint"
+
+flag {
+ name: "powerhint_thread_cleanup"
+ namespace: "game"
+ description: "Feature flag for auto PowerHintSession dead thread cleanup"
+ bug: "296160319"
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 3b2a3dd9763a..e202bbf022bc 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1230,10 +1230,6 @@ 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");
@@ -1614,6 +1610,10 @@ public final class SystemServer implements Dumpable {
t.traceEnd();
}
+ t.traceBegin("StartHintManager");
+ mSystemServiceManager.startService(HintManagerService.class);
+ t.traceEnd();
+
// Grants default permissions and defines roles
t.traceBegin("StartRoleManagerService");
LocalManagerRegistry.addManager(RoleServicePlatformHelper.class,
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
index 66599e9e9125..510e7c42f12d 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -17,6 +17,8 @@
package com.android.server.power.hint;
+import static com.android.server.power.hint.HintManagerService.CLEAN_UP_UID_DELAY_MILLIS;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
@@ -45,6 +47,9 @@ import android.os.IBinder;
import android.os.IHintSession;
import android.os.PerformanceHintManager;
import android.os.Process;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.Log;
import com.android.server.FgThread;
@@ -54,11 +59,13 @@ import com.android.server.power.hint.HintManagerService.Injector;
import com.android.server.power.hint.HintManagerService.NativeWrapper;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -71,7 +78,7 @@ import java.util.concurrent.locks.LockSupport;
* Tests for {@link com.android.server.power.hint.HintManagerService}.
*
* Build/Install/Run:
- * atest FrameworksServicesTests:HintManagerServiceTest
+ * atest FrameworksServicesTests:HintManagerServiceTest
*/
public class HintManagerServiceTest {
private static final String TAG = "HintManagerServiceTest";
@@ -110,9 +117,15 @@ public class HintManagerServiceTest {
makeWorkDuration(2L, 13L, 2L, 8L, 0L),
};
- @Mock private Context mContext;
- @Mock private HintManagerService.NativeWrapper mNativeWrapperMock;
- @Mock private ActivityManagerInternal mAmInternalMock;
+ @Mock
+ private Context mContext;
+ @Mock
+ private HintManagerService.NativeWrapper mNativeWrapperMock;
+ @Mock
+ private ActivityManagerInternal mAmInternalMock;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
private HintManagerService mService;
@@ -122,12 +135,11 @@ public class HintManagerServiceTest {
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);
+ eq(DEFAULT_TARGET_DURATION))).thenReturn(1L);
when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_B),
- eq(DEFAULT_TARGET_DURATION))).thenReturn(2L);
+ eq(DEFAULT_TARGET_DURATION))).thenReturn(2L);
when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_C),
- eq(0L))).thenReturn(1L);
- when(mAmInternalMock.getIsolatedProcesses(anyInt())).thenReturn(null);
+ eq(0L))).thenReturn(1L);
LocalServices.removeServiceForTest(ActivityManagerInternal.class);
LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock);
}
@@ -434,6 +446,163 @@ public class HintManagerServiceTest {
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_POWERHINT_THREAD_CLEANUP)
+ public void testCleanupDeadThreads() throws Exception {
+ HintManagerService service = createService();
+ IBinder token = new Binder();
+ CountDownLatch stopLatch1 = new CountDownLatch(1);
+ int threadCount = 3;
+ int[] tids1 = createThreads(threadCount, stopLatch1);
+ long sessionPtr1 = 111;
+ when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids1),
+ eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr1);
+ AppHintSession session1 = (AppHintSession) service.getBinderServiceInstance()
+ .createHintSession(token, tids1, DEFAULT_TARGET_DURATION);
+ assertNotNull(session1);
+
+ // for test only to avoid conflicting with any real thread that exists on device
+ int isoProc1 = -100;
+ int isoProc2 = 9999;
+ when(mAmInternalMock.getIsolatedProcesses(eq(UID))).thenReturn(List.of(0));
+
+ CountDownLatch stopLatch2 = new CountDownLatch(1);
+ int[] tids2 = createThreads(threadCount, stopLatch2);
+ int[] tids2WithIsolated = Arrays.copyOf(tids2, tids2.length + 2);
+ int[] expectedTids2 = Arrays.copyOf(tids2, tids2.length + 1);
+ expectedTids2[tids2.length] = isoProc1;
+ tids2WithIsolated[threadCount] = isoProc1;
+ tids2WithIsolated[threadCount + 1] = isoProc2;
+ long sessionPtr2 = 222;
+ when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids2WithIsolated),
+ eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr2);
+ AppHintSession session2 = (AppHintSession) service.getBinderServiceInstance()
+ .createHintSession(token, tids2WithIsolated, DEFAULT_TARGET_DURATION);
+ assertNotNull(session2);
+
+ // trigger clean up through UID state change by making the process background
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
+ // the new TIDs pending list should be updated
+ assertArrayEquals(session2.getTidsInternal(), expectedTids2);
+ reset(mNativeWrapperMock);
+
+ // this should resume and update the threads so those never-existed invalid isolated
+ // processes should be cleaned up
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+ // wait for the async uid state change to trigger resume and setThreads
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+ verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2), eq(expectedTids2));
+ reset(mNativeWrapperMock);
+
+ // let all session 1 threads to exit and the cleanup should force pause the session
+ stopLatch1.countDown();
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
+ verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
+ // all hints will have no effect as the session is force paused while proc in foreground
+ verifyAllHintsEnabled(session1, false);
+ verifyAllHintsEnabled(session2, true);
+ reset(mNativeWrapperMock);
+
+ // in foreground, set new tids for session 1 then it should be resumed and all hints allowed
+ stopLatch1 = new CountDownLatch(1);
+ tids1 = createThreads(threadCount, stopLatch1);
+ session1.setThreads(tids1);
+ verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1), eq(tids1));
+ verify(mNativeWrapperMock, times(1)).halResumeHintSession(eq(sessionPtr1));
+ verifyAllHintsEnabled(session1, true);
+ reset(mNativeWrapperMock);
+
+ // let all session 1 and 2 non isolated threads to exit
+ stopLatch1.countDown();
+ stopLatch2.countDown();
+ expectedTids2 = new int[]{isoProc1};
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
+ verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
+ // in background, set threads for session 1 then it should not be force paused next time
+ session1.setThreads(SESSION_TIDS_A);
+ // the new TIDs pending list should be updated
+ assertArrayEquals(session1.getTidsInternal(), SESSION_TIDS_A);
+ assertArrayEquals(session2.getTidsInternal(), expectedTids2);
+ verifyAllHintsEnabled(session1, false);
+ verifyAllHintsEnabled(session2, false);
+ reset(mNativeWrapperMock);
+
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
+ verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1),
+ eq(SESSION_TIDS_A));
+ verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2),
+ eq(expectedTids2));
+ verifyAllHintsEnabled(session1, true);
+ verifyAllHintsEnabled(session2, true);
+ }
+
+ private void verifyAllHintsEnabled(AppHintSession session, boolean verifyEnabled) {
+ session.reportActualWorkDuration2(new WorkDuration[]{makeWorkDuration(1, 3, 2, 1, 1000)});
+ session.reportActualWorkDuration(new long[]{1}, new long[]{2});
+ session.updateTargetWorkDuration(3);
+ session.setMode(0, true);
+ session.sendHint(1);
+ if (verifyEnabled) {
+ verify(mNativeWrapperMock, times(1)).halReportActualWorkDuration(
+ eq(session.mHalSessionPtr), any());
+ verify(mNativeWrapperMock, times(1)).halSetMode(eq(session.mHalSessionPtr), anyInt(),
+ anyBoolean());
+ verify(mNativeWrapperMock, times(1)).halUpdateTargetWorkDuration(
+ eq(session.mHalSessionPtr), anyLong());
+ verify(mNativeWrapperMock, times(1)).halSendHint(eq(session.mHalSessionPtr), anyInt());
+ } else {
+ verify(mNativeWrapperMock, never()).halReportActualWorkDuration(
+ eq(session.mHalSessionPtr), any());
+ verify(mNativeWrapperMock, never()).halSetMode(eq(session.mHalSessionPtr), anyInt(),
+ anyBoolean());
+ verify(mNativeWrapperMock, never()).halUpdateTargetWorkDuration(
+ eq(session.mHalSessionPtr), anyLong());
+ verify(mNativeWrapperMock, never()).halSendHint(eq(session.mHalSessionPtr), anyInt());
+ }
+ }
+
+ private int[] createThreads(int threadCount, CountDownLatch stopLatch)
+ throws InterruptedException {
+ int[] tids = new int[threadCount];
+ AtomicInteger k = new AtomicInteger(0);
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ for (int j = 0; j < threadCount; j++) {
+ Thread thread = new Thread(() -> {
+ try {
+ tids[k.getAndIncrement()] = android.os.Process.myTid();
+ latch.countDown();
+ stopLatch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ thread.start();
+ }
+ latch.await();
+ return tids;
+ }
+
+ @Test
public void testSetMode() throws Exception {
HintManagerService service = createService();
IBinder token = new Binder();
@@ -457,7 +626,8 @@ public class HintManagerServiceTest {
// Set session to background, then the duration would not be updated.
service.mUidObserver.onUidStateChanged(
a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
- FgThread.getHandler().runWithScissors(() -> { }, 500);
+ FgThread.getHandler().runWithScissors(() -> {
+ }, 500);
assertFalse(service.mUidObserver.isUidForeground(a.mUid));
a.setMode(0, true);
verify(mNativeWrapperMock, never()).halSetMode(anyLong(), anyInt(), anyBoolean());
@@ -519,7 +689,10 @@ public class HintManagerServiceTest {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
service.mUidObserver.onUidStateChanged(UID,
ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
- LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+ // let the cleanup work proceed
+ LockSupport.parkNanos(
+ TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
}
Log.d(TAG, "notifier thread min " + min + " max " + max + " avg " + sum / count);
service.mUidObserver.onUidGone(UID, true);
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
new file mode 100644
index 000000000000..2d5df077b128
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+ "postsubmit": [
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.power.hint"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}