diff options
| author | 2023-10-24 10:48:31 -0700 | |
|---|---|---|
| committer | 2024-03-19 11:32:53 -0700 | |
| commit | f63a6af05269eef76671e9533e578ac7a6e43e51 (patch) | |
| tree | 45e6c4aa5300f98abf91b3d71ddefb14942bab48 | |
| parent | 25530406d7584bb9997bb5469c421d5b9547bedc (diff) | |
Add support for ADPF hint session tid cleanup
Bug: 309701979
Test: atest HintManagerServiceTest
Change-Id: I94440e52efd54918d1e293e02aabb6e7820510df
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" + } + ] + } + ] +} |