1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.power.hint;
18 
19 import android.app.ActivityManager;
20 import android.app.ActivityManagerInternal;
21 import android.app.IUidObserver;
22 import android.content.Context;
23 import android.os.Binder;
24 import android.os.IBinder;
25 import android.os.IHintManager;
26 import android.os.IHintSession;
27 import android.os.Process;
28 import android.os.RemoteException;
29 import android.util.ArrayMap;
30 import android.util.SparseArray;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.internal.util.DumpUtils;
35 import com.android.internal.util.Preconditions;
36 import com.android.server.FgThread;
37 import com.android.server.LocalServices;
38 import com.android.server.SystemService;
39 import com.android.server.utils.Slogf;
40 
41 import java.io.FileDescriptor;
42 import java.io.PrintWriter;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.List;
46 import java.util.Objects;
47 
48 /** An hint service implementation that runs in System Server process. */
49 public final class HintManagerService extends SystemService {
50     private static final String TAG = "HintManagerService";
51     private static final boolean DEBUG = false;
52     @VisibleForTesting final long mHintSessionPreferredRate;
53 
54     @GuardedBy("mLock")
55     private final ArrayMap<Integer, ArrayMap<IBinder, AppHintSession>> mActiveSessions;
56 
57     /** Lock to protect HAL handles and listen list. */
58     private final Object mLock = new Object();
59 
60     @VisibleForTesting final UidObserver mUidObserver;
61 
62     private final NativeWrapper mNativeWrapper;
63 
64     private final ActivityManagerInternal mAmInternal;
65 
66     @VisibleForTesting final IHintManager.Stub mService = new BinderService();
67 
HintManagerService(Context context)68     public HintManagerService(Context context) {
69         this(context, new Injector());
70     }
71 
72     @VisibleForTesting
HintManagerService(Context context, Injector injector)73     HintManagerService(Context context, Injector injector) {
74         super(context);
75         mActiveSessions = new ArrayMap<>();
76         mNativeWrapper = injector.createNativeWrapper();
77         mNativeWrapper.halInit();
78         mHintSessionPreferredRate = mNativeWrapper.halGetHintSessionPreferredRate();
79         mUidObserver = new UidObserver();
80         mAmInternal = Objects.requireNonNull(
81                 LocalServices.getService(ActivityManagerInternal.class));
82     }
83 
84     @VisibleForTesting
85     static class Injector {
createNativeWrapper()86         NativeWrapper createNativeWrapper() {
87             return new NativeWrapper();
88         }
89     }
90 
isHalSupported()91     private boolean isHalSupported() {
92         return mHintSessionPreferredRate != -1;
93     }
94 
95     @Override
onStart()96     public void onStart() {
97         publishBinderService(Context.PERFORMANCE_HINT_SERVICE, mService);
98     }
99 
100     @Override
onBootPhase(int phase)101     public void onBootPhase(int phase) {
102         if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
103             systemReady();
104         }
105     }
106 
systemReady()107     private void systemReady() {
108         Slogf.v(TAG, "Initializing HintManager service...");
109         try {
110             ActivityManager.getService().registerUidObserver(mUidObserver,
111                     ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE,
112                     ActivityManager.PROCESS_STATE_UNKNOWN, null);
113         } catch (RemoteException e) {
114             // ignored; both services live in system_server
115         }
116 
117     }
118 
119     /**
120      * Wrapper around the static-native methods from native.
121      *
122      * This class exists to allow us to mock static native methods in our tests. If mocking static
123      * methods becomes easier than this in the future, we can delete this class.
124      */
125     @VisibleForTesting
126     public static class NativeWrapper {
nativeInit()127         private native void nativeInit();
128 
nativeCreateHintSession(int tgid, int uid, int[] tids, long durationNanos)129         private static native long nativeCreateHintSession(int tgid, int uid, int[] tids,
130                 long durationNanos);
131 
nativePauseHintSession(long halPtr)132         private static native void nativePauseHintSession(long halPtr);
133 
nativeResumeHintSession(long halPtr)134         private static native void nativeResumeHintSession(long halPtr);
135 
nativeCloseHintSession(long halPtr)136         private static native void nativeCloseHintSession(long halPtr);
137 
nativeUpdateTargetWorkDuration( long halPtr, long targetDurationNanos)138         private static native void nativeUpdateTargetWorkDuration(
139                 long halPtr, long targetDurationNanos);
140 
nativeReportActualWorkDuration( long halPtr, long[] actualDurationNanos, long[] timeStampNanos)141         private static native void nativeReportActualWorkDuration(
142                 long halPtr, long[] actualDurationNanos, long[] timeStampNanos);
143 
nativeGetHintSessionPreferredRate()144         private static native long nativeGetHintSessionPreferredRate();
145 
146         /** Wrapper for HintManager.nativeInit */
halInit()147         public void halInit() {
148             nativeInit();
149         }
150 
151         /** Wrapper for HintManager.nativeCreateHintSession */
halCreateHintSession(int tgid, int uid, int[] tids, long durationNanos)152         public long halCreateHintSession(int tgid, int uid, int[] tids, long durationNanos) {
153             return nativeCreateHintSession(tgid, uid, tids, durationNanos);
154         }
155 
156         /** Wrapper for HintManager.nativePauseHintSession */
halPauseHintSession(long halPtr)157         public void halPauseHintSession(long halPtr) {
158             nativePauseHintSession(halPtr);
159         }
160 
161         /** Wrapper for HintManager.nativeResumeHintSession */
halResumeHintSession(long halPtr)162         public void halResumeHintSession(long halPtr) {
163             nativeResumeHintSession(halPtr);
164         }
165 
166         /** Wrapper for HintManager.nativeCloseHintSession */
halCloseHintSession(long halPtr)167         public void halCloseHintSession(long halPtr) {
168             nativeCloseHintSession(halPtr);
169         }
170 
171         /** Wrapper for HintManager.nativeUpdateTargetWorkDuration */
halUpdateTargetWorkDuration(long halPtr, long targetDurationNanos)172         public void halUpdateTargetWorkDuration(long halPtr, long targetDurationNanos) {
173             nativeUpdateTargetWorkDuration(halPtr, targetDurationNanos);
174         }
175 
176         /** Wrapper for HintManager.nativeReportActualWorkDuration */
halReportActualWorkDuration( long halPtr, long[] actualDurationNanos, long[] timeStampNanos)177         public void halReportActualWorkDuration(
178                 long halPtr, long[] actualDurationNanos, long[] timeStampNanos) {
179             nativeReportActualWorkDuration(halPtr, actualDurationNanos,
180                     timeStampNanos);
181         }
182 
183         /** Wrapper for HintManager.nativeGetHintSessionPreferredRate */
halGetHintSessionPreferredRate()184         public long halGetHintSessionPreferredRate() {
185             return nativeGetHintSessionPreferredRate();
186         }
187     }
188 
189     @VisibleForTesting
190     final class UidObserver extends IUidObserver.Stub {
191         private final SparseArray<Integer> mProcStatesCache = new SparseArray<>();
192 
isUidForeground(int uid)193         public boolean isUidForeground(int uid) {
194             synchronized (mLock) {
195                 return mProcStatesCache.get(uid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND)
196                         <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
197             }
198         }
199 
200         @Override
onUidGone(int uid, boolean disabled)201         public void onUidGone(int uid, boolean disabled) {
202             FgThread.getHandler().post(() -> {
203                 synchronized (mLock) {
204                     ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(uid);
205                     if (tokenMap == null) {
206                         return;
207                     }
208                     for (int i = tokenMap.size() - 1; i >= 0; i--) {
209                         // Will remove the session from tokenMap
210                         tokenMap.valueAt(i).close();
211                     }
212                     mProcStatesCache.delete(uid);
213                 }
214             });
215         }
216 
217         @Override
onUidActive(int uid)218         public void onUidActive(int uid) {
219         }
220 
221         @Override
onUidIdle(int uid, boolean disabled)222         public void onUidIdle(int uid, boolean disabled) {
223         }
224 
225         /**
226          * The IUidObserver callback is called from the system_server, so it'll be a direct function
227          * call from ActivityManagerService. Do not do heavy logic here.
228          */
229         @Override
onUidStateChanged(int uid, int procState, long procStateSeq, int capability)230         public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
231             FgThread.getHandler().post(() -> {
232                 synchronized (mLock) {
233                     mProcStatesCache.put(uid, procState);
234                     ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(uid);
235                     if (tokenMap == null) {
236                         return;
237                     }
238                     for (AppHintSession s : tokenMap.values()) {
239                         s.onProcStateChanged();
240                     }
241                 }
242             });
243         }
244 
245         @Override
onUidCachedChanged(int uid, boolean cached)246         public void onUidCachedChanged(int uid, boolean cached) {
247         }
248     }
249 
250     @VisibleForTesting
getBinderServiceInstance()251     IHintManager.Stub getBinderServiceInstance() {
252         return mService;
253     }
254 
checkTidValid(int uid, int tgid, int [] tids)255     private boolean checkTidValid(int uid, int tgid, int [] tids) {
256         // Make sure all tids belongs to the same UID (including isolated UID),
257         // tids can belong to different application processes.
258         List<Integer> eligiblePids = null;
259         // To avoid deadlock, do not call into AMS if the call is from system.
260         if (uid != Process.SYSTEM_UID) {
261             eligiblePids = mAmInternal.getIsolatedProcesses(uid);
262         }
263         if (eligiblePids == null) {
264             eligiblePids = new ArrayList<>();
265         }
266         eligiblePids.add(tgid);
267 
268         for (int threadId : tids) {
269             final String[] procStatusKeys = new String[] {
270                     "Uid:",
271                     "Tgid:"
272             };
273             long[] output = new long[procStatusKeys.length];
274             Process.readProcLines("/proc/" + threadId + "/status", procStatusKeys, output);
275             int uidOfThreadId = (int) output[0];
276             int pidOfThreadId = (int) output[1];
277 
278             // use PID check for isolated processes, use UID check for non-isolated processes.
279             if (eligiblePids.contains(pidOfThreadId) || uidOfThreadId == uid) {
280                 continue;
281             }
282             return false;
283         }
284         return true;
285     }
286 
287     @VisibleForTesting
288     final class BinderService extends IHintManager.Stub {
289         @Override
createHintSession(IBinder token, int[] tids, long durationNanos)290         public IHintSession createHintSession(IBinder token, int[] tids, long durationNanos) {
291             if (!isHalSupported()) return null;
292 
293             java.util.Objects.requireNonNull(token);
294             java.util.Objects.requireNonNull(tids);
295             Preconditions.checkArgument(tids.length != 0, "tids should"
296                     + " not be empty.");
297 
298             final int callingUid = Binder.getCallingUid();
299             final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
300             final long identity = Binder.clearCallingIdentity();
301             try {
302                 if (!checkTidValid(callingUid, callingTgid, tids)) {
303                     throw new SecurityException("Some tid doesn't belong to the application");
304                 }
305 
306                 long halSessionPtr = mNativeWrapper.halCreateHintSession(callingTgid, callingUid,
307                         tids, durationNanos);
308                 if (halSessionPtr == 0) return null;
309 
310                 AppHintSession hs = new AppHintSession(callingUid, callingTgid, tids, token,
311                         halSessionPtr, durationNanos);
312                 synchronized (mLock) {
313                     ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(callingUid);
314                     if (tokenMap == null) {
315                         tokenMap = new ArrayMap<>(1);
316                         mActiveSessions.put(callingUid, tokenMap);
317                     }
318                     tokenMap.put(token, hs);
319                     return hs;
320                 }
321             } finally {
322                 Binder.restoreCallingIdentity(identity);
323             }
324         }
325 
326         @Override
getHintSessionPreferredRate()327         public long getHintSessionPreferredRate() {
328             return mHintSessionPreferredRate;
329         }
330 
331         @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)332         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
333             if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
334                 return;
335             }
336             synchronized (mLock) {
337                 pw.println("HintSessionPreferredRate: " + mHintSessionPreferredRate);
338                 pw.println("HAL Support: " + isHalSupported());
339                 pw.println("Active Sessions:");
340                 for (int i = 0; i < mActiveSessions.size(); i++) {
341                     pw.println("Uid " + mActiveSessions.keyAt(i).toString() + ":");
342                     ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.valueAt(i);
343                     for (int j = 0; j < tokenMap.size(); j++) {
344                         pw.println("  Session " + j + ":");
345                         tokenMap.valueAt(j).dump(pw, "    ");
346                     }
347                 }
348             }
349         }
350     }
351 
352     @VisibleForTesting
353     final class AppHintSession extends IHintSession.Stub implements IBinder.DeathRecipient {
354         protected final int mUid;
355         protected final int mPid;
356         protected final int[] mThreadIds;
357         protected final IBinder mToken;
358         protected long mHalSessionPtr;
359         protected long mTargetDurationNanos;
360         protected boolean mUpdateAllowed;
361 
AppHintSession( int uid, int pid, int[] threadIds, IBinder token, long halSessionPtr, long durationNanos)362         protected AppHintSession(
363                 int uid, int pid, int[] threadIds, IBinder token,
364                 long halSessionPtr, long durationNanos) {
365             mUid = uid;
366             mPid = pid;
367             mToken = token;
368             mThreadIds = threadIds;
369             mHalSessionPtr = halSessionPtr;
370             mTargetDurationNanos = durationNanos;
371             mUpdateAllowed = true;
372             updateHintAllowed();
373             try {
374                 token.linkToDeath(this, 0);
375             } catch (RemoteException e) {
376                 mNativeWrapper.halCloseHintSession(mHalSessionPtr);
377                 throw new IllegalStateException("Client already dead", e);
378             }
379         }
380 
381         @VisibleForTesting
updateHintAllowed()382         boolean updateHintAllowed() {
383             synchronized (mLock) {
384                 final boolean allowed = mUidObserver.isUidForeground(mUid);
385                 if (allowed && !mUpdateAllowed) resume();
386                 if (!allowed && mUpdateAllowed) pause();
387                 mUpdateAllowed = allowed;
388                 return mUpdateAllowed;
389             }
390         }
391 
392         @Override
updateTargetWorkDuration(long targetDurationNanos)393         public void updateTargetWorkDuration(long targetDurationNanos) {
394             synchronized (mLock) {
395                 if (mHalSessionPtr == 0 || !updateHintAllowed()) {
396                     return;
397                 }
398                 Preconditions.checkArgument(targetDurationNanos > 0, "Expected"
399                         + " the target duration to be greater than 0.");
400                 mNativeWrapper.halUpdateTargetWorkDuration(mHalSessionPtr, targetDurationNanos);
401                 mTargetDurationNanos = targetDurationNanos;
402             }
403         }
404 
405         @Override
reportActualWorkDuration(long[] actualDurationNanos, long[] timeStampNanos)406         public void reportActualWorkDuration(long[] actualDurationNanos, long[] timeStampNanos) {
407             synchronized (mLock) {
408                 if (mHalSessionPtr == 0 || !updateHintAllowed()) {
409                     return;
410                 }
411                 Preconditions.checkArgument(actualDurationNanos.length != 0, "the count"
412                         + " of hint durations shouldn't be 0.");
413                 Preconditions.checkArgument(actualDurationNanos.length == timeStampNanos.length,
414                         "The length of durations and timestamps should be the same.");
415                 for (int i = 0; i < actualDurationNanos.length; i++) {
416                     if (actualDurationNanos[i] <= 0) {
417                         throw new IllegalArgumentException(
418                                 String.format("durations[%d]=%d should be greater than 0",
419                                         i, actualDurationNanos[i]));
420                     }
421                 }
422                 mNativeWrapper.halReportActualWorkDuration(mHalSessionPtr, actualDurationNanos,
423                         timeStampNanos);
424             }
425         }
426 
427         /** TODO: consider monitor session threads and close session if any thread is dead. */
428         @Override
close()429         public void close() {
430             synchronized (mLock) {
431                 if (mHalSessionPtr == 0) return;
432                 mNativeWrapper.halCloseHintSession(mHalSessionPtr);
433                 mHalSessionPtr = 0;
434                 mToken.unlinkToDeath(this, 0);
435                 ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(mUid);
436                 if (tokenMap == null) {
437                     Slogf.w(TAG, "UID %d is note present in active session map", mUid);
438                 }
439                 tokenMap.remove(mToken);
440                 if (tokenMap.isEmpty()) mActiveSessions.remove(mUid);
441             }
442         }
443 
onProcStateChanged()444         private void onProcStateChanged() {
445             updateHintAllowed();
446         }
447 
pause()448         private void pause() {
449             synchronized (mLock) {
450                 if (mHalSessionPtr == 0) return;
451                 mNativeWrapper.halPauseHintSession(mHalSessionPtr);
452             }
453         }
454 
resume()455         private void resume() {
456             synchronized (mLock) {
457                 if (mHalSessionPtr == 0) return;
458                 mNativeWrapper.halResumeHintSession(mHalSessionPtr);
459             }
460         }
461 
dump(PrintWriter pw, String prefix)462         private void dump(PrintWriter pw, String prefix) {
463             synchronized (mLock) {
464                 pw.println(prefix + "SessionPID: " + mPid);
465                 pw.println(prefix + "SessionUID: " + mUid);
466                 pw.println(prefix + "SessionTIDs: " + Arrays.toString(mThreadIds));
467                 pw.println(prefix + "SessionTargetDurationNanos: " + mTargetDurationNanos);
468                 pw.println(prefix + "SessionAllowed: " + updateHintAllowed());
469             }
470         }
471 
472         @Override
binderDied()473         public void binderDied() {
474             close();
475         }
476 
477     }
478 }
479