1 /*
2  * Copyright (C) 2017 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.internal.os;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.Binder;
22 import android.os.Process;
23 import android.os.SystemClock;
24 import android.text.format.DateFormat;
25 import android.util.ArrayMap;
26 import android.util.Pair;
27 import android.util.Slog;
28 import android.util.SparseArray;
29 
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.internal.os.BinderInternal.CallSession;
33 
34 import java.io.PrintWriter;
35 import java.lang.reflect.InvocationTargetException;
36 import java.lang.reflect.Method;
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.Comparator;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Queue;
43 import java.util.Random;
44 import java.util.concurrent.ConcurrentLinkedQueue;
45 import java.util.function.ToDoubleFunction;
46 
47 /**
48  * Collects statistics about CPU time spent per binder call across multiple dimensions, e.g.
49  * per thread, uid or call description.
50  */
51 public class BinderCallsStats implements BinderInternal.Observer {
52     public static final boolean ENABLED_DEFAULT = true;
53     public static final boolean DETAILED_TRACKING_DEFAULT = true;
54     public static final int PERIODIC_SAMPLING_INTERVAL_DEFAULT = 1000;
55     public static final boolean DEFAULT_TRACK_SCREEN_INTERACTIVE = false;
56     public static final boolean DEFAULT_TRACK_DIRECT_CALLING_UID = true;
57     public static final int MAX_BINDER_CALL_STATS_COUNT_DEFAULT = 1500;
58     private static final String DEBUG_ENTRY_PREFIX = "__DEBUG_";
59 
60     private static class OverflowBinder extends Binder {}
61 
62     private static final String TAG = "BinderCallsStats";
63     private static final int CALL_SESSIONS_POOL_SIZE = 100;
64     private static final int MAX_EXCEPTION_COUNT_SIZE = 50;
65     private static final String EXCEPTION_COUNT_OVERFLOW_NAME = "overflow";
66     // Default values for overflow entry. The work source uid does not use a default value in order
67     // to have on overflow entry per work source uid.
68     private static final Class<? extends Binder> OVERFLOW_BINDER = OverflowBinder.class;
69     private static final boolean OVERFLOW_SCREEN_INTERACTIVE = false;
70     private static final int OVERFLOW_DIRECT_CALLING_UID = -1;
71     private static final int OVERFLOW_TRANSACTION_CODE = -1;
72 
73     // Whether to collect all the data: cpu + exceptions + reply/request sizes.
74     private boolean mDetailedTracking = DETAILED_TRACKING_DEFAULT;
75     // Sampling period to control how often to track CPU usage. 1 means all calls, 100 means ~1 out
76     // of 100 requests.
77     private int mPeriodicSamplingInterval = PERIODIC_SAMPLING_INTERVAL_DEFAULT;
78     private int mMaxBinderCallStatsCount = MAX_BINDER_CALL_STATS_COUNT_DEFAULT;
79     @GuardedBy("mLock")
80     private final SparseArray<UidEntry> mUidEntries = new SparseArray<>();
81     @GuardedBy("mLock")
82     private final ArrayMap<String, Integer> mExceptionCounts = new ArrayMap<>();
83     private final Queue<CallSession> mCallSessionsPool = new ConcurrentLinkedQueue<>();
84     private final Object mLock = new Object();
85     private final Random mRandom;
86     private long mStartCurrentTime = System.currentTimeMillis();
87     private long mStartElapsedTime = SystemClock.elapsedRealtime();
88     private long mCallStatsCount = 0;
89     private boolean mAddDebugEntries = false;
90     private boolean mTrackDirectCallingUid = DEFAULT_TRACK_DIRECT_CALLING_UID;
91     private boolean mTrackScreenInteractive = DEFAULT_TRACK_SCREEN_INTERACTIVE;
92 
93     private CachedDeviceState.Readonly mDeviceState;
94     private CachedDeviceState.TimeInStateStopwatch mBatteryStopwatch;
95 
96     /** Injector for {@link BinderCallsStats}. */
97     public static class Injector {
getRandomGenerator()98         public Random getRandomGenerator() {
99             return new Random();
100         }
101     }
102 
BinderCallsStats(Injector injector)103     public BinderCallsStats(Injector injector) {
104         this.mRandom = injector.getRandomGenerator();
105     }
106 
setDeviceState(@onNull CachedDeviceState.Readonly deviceState)107     public void setDeviceState(@NonNull CachedDeviceState.Readonly deviceState) {
108         if (mBatteryStopwatch != null) {
109             mBatteryStopwatch.close();
110         }
111         mDeviceState = deviceState;
112         mBatteryStopwatch = deviceState.createTimeOnBatteryStopwatch();
113     }
114 
115     @Override
116     @Nullable
callStarted(Binder binder, int code, int workSourceUid)117     public CallSession callStarted(Binder binder, int code, int workSourceUid) {
118         if (mDeviceState == null || mDeviceState.isCharging()) {
119             return null;
120         }
121 
122         final CallSession s = obtainCallSession();
123         s.binderClass = binder.getClass();
124         s.transactionCode = code;
125         s.exceptionThrown = false;
126         s.cpuTimeStarted = -1;
127         s.timeStarted = -1;
128         if (shouldRecordDetailedData()) {
129             s.cpuTimeStarted = getThreadTimeMicro();
130             s.timeStarted = getElapsedRealtimeMicro();
131         }
132         return s;
133     }
134 
obtainCallSession()135     private CallSession obtainCallSession() {
136         CallSession s = mCallSessionsPool.poll();
137         return s == null ? new CallSession() : s;
138     }
139 
140     @Override
callEnded(@ullable CallSession s, int parcelRequestSize, int parcelReplySize, int workSourceUid)141     public void callEnded(@Nullable CallSession s, int parcelRequestSize,
142             int parcelReplySize, int workSourceUid) {
143         if (s == null) {
144             return;
145         }
146 
147         processCallEnded(s, parcelRequestSize, parcelReplySize, workSourceUid);
148 
149         if (mCallSessionsPool.size() < CALL_SESSIONS_POOL_SIZE) {
150             mCallSessionsPool.add(s);
151         }
152     }
153 
processCallEnded(CallSession s, int parcelRequestSize, int parcelReplySize, int workSourceUid)154     private void processCallEnded(CallSession s,
155             int parcelRequestSize, int parcelReplySize, int workSourceUid) {
156         // Non-negative time signals we need to record data for this call.
157         final boolean recordCall = s.cpuTimeStarted >= 0;
158         final long duration;
159         final long latencyDuration;
160         if (recordCall) {
161             duration = getThreadTimeMicro() - s.cpuTimeStarted;
162             latencyDuration = getElapsedRealtimeMicro() - s.timeStarted;
163         } else {
164             duration = 0;
165             latencyDuration = 0;
166         }
167         final boolean screenInteractive = mTrackScreenInteractive
168                 ? mDeviceState.isScreenInteractive()
169                 : OVERFLOW_SCREEN_INTERACTIVE;
170         final int callingUid = mTrackDirectCallingUid
171                 ? getCallingUid()
172                 : OVERFLOW_DIRECT_CALLING_UID;
173 
174         synchronized (mLock) {
175             // This was already checked in #callStart but check again while synchronized.
176             if (mDeviceState == null || mDeviceState.isCharging()) {
177                 return;
178             }
179 
180             final UidEntry uidEntry = getUidEntry(workSourceUid);
181             uidEntry.callCount++;
182 
183             if (recordCall) {
184                 uidEntry.cpuTimeMicros += duration;
185                 uidEntry.recordedCallCount++;
186 
187                 final CallStat callStat = uidEntry.getOrCreate(
188                         callingUid, s.binderClass, s.transactionCode,
189                         screenInteractive,
190                         mCallStatsCount >= mMaxBinderCallStatsCount);
191                 final boolean isNewCallStat = callStat.callCount == 0;
192                 if (isNewCallStat) {
193                     mCallStatsCount++;
194                 }
195 
196                 callStat.callCount++;
197                 callStat.recordedCallCount++;
198                 callStat.cpuTimeMicros += duration;
199                 callStat.maxCpuTimeMicros = Math.max(callStat.maxCpuTimeMicros, duration);
200                 callStat.latencyMicros += latencyDuration;
201                 callStat.maxLatencyMicros =
202                         Math.max(callStat.maxLatencyMicros, latencyDuration);
203                 if (mDetailedTracking) {
204                     callStat.exceptionCount += s.exceptionThrown ? 1 : 0;
205                     callStat.maxRequestSizeBytes =
206                             Math.max(callStat.maxRequestSizeBytes, parcelRequestSize);
207                     callStat.maxReplySizeBytes =
208                             Math.max(callStat.maxReplySizeBytes, parcelReplySize);
209                 }
210             } else {
211                 // Only record the total call count if we already track data for this key.
212                 // It helps to keep the memory usage down when sampling is enabled.
213                 final CallStat callStat = uidEntry.get(
214                         callingUid, s.binderClass, s.transactionCode,
215                         screenInteractive);
216                 if (callStat != null) {
217                     callStat.callCount++;
218                 }
219             }
220         }
221     }
222 
getUidEntry(int uid)223     private UidEntry getUidEntry(int uid) {
224         UidEntry uidEntry = mUidEntries.get(uid);
225         if (uidEntry == null) {
226             uidEntry = new UidEntry(uid);
227             mUidEntries.put(uid, uidEntry);
228         }
229         return uidEntry;
230     }
231 
232     @Override
callThrewException(@ullable CallSession s, Exception exception)233     public void callThrewException(@Nullable CallSession s, Exception exception) {
234         if (s == null) {
235             return;
236         }
237         s.exceptionThrown = true;
238         try {
239             String className = exception.getClass().getName();
240             synchronized (mLock) {
241                 if (mExceptionCounts.size() >= MAX_EXCEPTION_COUNT_SIZE) {
242                     className = EXCEPTION_COUNT_OVERFLOW_NAME;
243                 }
244                 final Integer count = mExceptionCounts.get(className);
245                 mExceptionCounts.put(className, count == null ? 1 : count + 1);
246             }
247         } catch (RuntimeException e) {
248             // Do not propagate the exception. We do not want to swallow original exception.
249             Slog.wtf(TAG, "Unexpected exception while updating mExceptionCounts");
250         }
251     }
252 
253     @Nullable
getDefaultTransactionNameMethod(Class<? extends Binder> binder)254     private Method getDefaultTransactionNameMethod(Class<? extends Binder> binder) {
255         try {
256             return binder.getMethod("getDefaultTransactionName", int.class);
257         } catch (NoSuchMethodException e) {
258             // The method might not be present for stubs not generated with AIDL.
259             return null;
260         }
261     }
262 
263     @Nullable
resolveTransactionCode(Method getDefaultTransactionName, int transactionCode)264     private String resolveTransactionCode(Method getDefaultTransactionName, int transactionCode) {
265         if (getDefaultTransactionName == null) {
266             return null;
267         }
268 
269         try {
270             return (String) getDefaultTransactionName.invoke(null, transactionCode);
271         } catch (IllegalAccessException | InvocationTargetException | ClassCastException e) {
272             throw new RuntimeException(e);
273         }
274     }
275 
276     /**
277      * This method is expensive to call.
278      */
getExportedCallStats()279     public ArrayList<ExportedCallStat> getExportedCallStats() {
280         // We do not collect all the data if detailed tracking is off.
281         if (!mDetailedTracking) {
282             return new ArrayList<>();
283         }
284 
285         ArrayList<ExportedCallStat> resultCallStats = new ArrayList<>();
286         synchronized (mLock) {
287             final int uidEntriesSize = mUidEntries.size();
288             for (int entryIdx = 0; entryIdx < uidEntriesSize; entryIdx++) {
289                 final UidEntry entry = mUidEntries.valueAt(entryIdx);
290                 for (CallStat stat : entry.getCallStatsList()) {
291                     ExportedCallStat exported = new ExportedCallStat();
292                     exported.workSourceUid = entry.workSourceUid;
293                     exported.callingUid = stat.callingUid;
294                     exported.className = stat.binderClass.getName();
295                     exported.binderClass = stat.binderClass;
296                     exported.transactionCode = stat.transactionCode;
297                     exported.screenInteractive = stat.screenInteractive;
298                     exported.cpuTimeMicros = stat.cpuTimeMicros;
299                     exported.maxCpuTimeMicros = stat.maxCpuTimeMicros;
300                     exported.latencyMicros = stat.latencyMicros;
301                     exported.maxLatencyMicros = stat.maxLatencyMicros;
302                     exported.recordedCallCount = stat.recordedCallCount;
303                     exported.callCount = stat.callCount;
304                     exported.maxRequestSizeBytes = stat.maxRequestSizeBytes;
305                     exported.maxReplySizeBytes = stat.maxReplySizeBytes;
306                     exported.exceptionCount = stat.exceptionCount;
307                     resultCallStats.add(exported);
308                 }
309             }
310         }
311 
312         // Resolve codes outside of the lock since it can be slow.
313         ExportedCallStat previous = null;
314         // Cache the previous method/transaction code.
315         Method getDefaultTransactionName = null;
316         String previousMethodName = null;
317         resultCallStats.sort(BinderCallsStats::compareByBinderClassAndCode);
318         for (ExportedCallStat exported : resultCallStats) {
319             final boolean isClassDifferent = previous == null
320                     || !previous.className.equals(exported.className);
321             if (isClassDifferent) {
322                 getDefaultTransactionName = getDefaultTransactionNameMethod(exported.binderClass);
323             }
324 
325             final boolean isCodeDifferent = previous == null
326                     || previous.transactionCode != exported.transactionCode;
327             final String methodName;
328             if (isClassDifferent || isCodeDifferent) {
329                 String resolvedCode = resolveTransactionCode(
330                         getDefaultTransactionName, exported.transactionCode);
331                 methodName = resolvedCode == null
332                         ? String.valueOf(exported.transactionCode)
333                         : resolvedCode;
334             } else {
335                 methodName = previousMethodName;
336             }
337             previousMethodName = methodName;
338             exported.methodName = methodName;
339         }
340 
341         // Debug entries added to help validate the data.
342         if (mAddDebugEntries && mBatteryStopwatch != null) {
343             resultCallStats.add(createDebugEntry("start_time_millis", mStartElapsedTime));
344             resultCallStats.add(createDebugEntry("end_time_millis", SystemClock.elapsedRealtime()));
345             resultCallStats.add(
346                     createDebugEntry("battery_time_millis", mBatteryStopwatch.getMillis()));
347             resultCallStats.add(createDebugEntry("sampling_interval", mPeriodicSamplingInterval));
348         }
349 
350         return resultCallStats;
351     }
352 
createDebugEntry(String variableName, long value)353     private ExportedCallStat createDebugEntry(String variableName, long value) {
354         final int uid = Process.myUid();
355         final ExportedCallStat callStat = new ExportedCallStat();
356         callStat.className = "";
357         callStat.workSourceUid = uid;
358         callStat.callingUid = uid;
359         callStat.recordedCallCount = 1;
360         callStat.callCount = 1;
361         callStat.methodName = DEBUG_ENTRY_PREFIX + variableName;
362         callStat.latencyMicros = value;
363         return callStat;
364     }
365 
366     /** @hide */
getExportedExceptionStats()367     public ArrayMap<String, Integer> getExportedExceptionStats() {
368         synchronized (mLock) {
369             return new ArrayMap(mExceptionCounts);
370         }
371     }
372 
373     /** Writes the collected statistics to the supplied {@link PrintWriter}.*/
dump(PrintWriter pw, AppIdToPackageMap packageMap, boolean verbose)374     public void dump(PrintWriter pw, AppIdToPackageMap packageMap, boolean verbose) {
375         synchronized (mLock) {
376             dumpLocked(pw, packageMap, verbose);
377         }
378     }
379 
dumpLocked(PrintWriter pw, AppIdToPackageMap packageMap, boolean verbose)380     private void dumpLocked(PrintWriter pw, AppIdToPackageMap packageMap, boolean verbose) {
381         long totalCallsCount = 0;
382         long totalRecordedCallsCount = 0;
383         long totalCpuTime = 0;
384         pw.print("Start time: ");
385         pw.println(DateFormat.format("yyyy-MM-dd HH:mm:ss", mStartCurrentTime));
386         pw.print("On battery time (ms): ");
387         pw.println(mBatteryStopwatch != null ? mBatteryStopwatch.getMillis() : 0);
388         pw.println("Sampling interval period: " + mPeriodicSamplingInterval);
389         final List<UidEntry> entries = new ArrayList<>();
390 
391         final int uidEntriesSize = mUidEntries.size();
392         for (int i = 0; i < uidEntriesSize; i++) {
393             UidEntry e = mUidEntries.valueAt(i);
394             entries.add(e);
395             totalCpuTime += e.cpuTimeMicros;
396             totalRecordedCallsCount += e.recordedCallCount;
397             totalCallsCount += e.callCount;
398         }
399 
400         entries.sort(Comparator.<UidEntry>comparingDouble(value -> value.cpuTimeMicros).reversed());
401         final String datasetSizeDesc = verbose ? "" : "(top 90% by cpu time) ";
402         final StringBuilder sb = new StringBuilder();
403         pw.println("Per-UID raw data " + datasetSizeDesc
404                 + "(package/uid, worksource, call_desc, screen_interactive, "
405                 + "cpu_time_micros, max_cpu_time_micros, "
406                 + "latency_time_micros, max_latency_time_micros, exception_count, "
407                 + "max_request_size_bytes, max_reply_size_bytes, recorded_call_count, "
408                 + "call_count):");
409         final List<ExportedCallStat> exportedCallStats = getExportedCallStats();
410         exportedCallStats.sort(BinderCallsStats::compareByCpuDesc);
411         for (ExportedCallStat e : exportedCallStats) {
412             if (e.methodName.startsWith(DEBUG_ENTRY_PREFIX)) {
413                 // Do not dump debug entries.
414                 continue;
415             }
416             sb.setLength(0);
417             sb.append("    ")
418                     .append(packageMap.mapUid(e.callingUid))
419                     .append(',')
420                     .append(packageMap.mapUid(e.workSourceUid))
421                     .append(',').append(e.className)
422                     .append('#').append(e.methodName)
423                     .append(',').append(e.screenInteractive)
424                     .append(',').append(e.cpuTimeMicros)
425                     .append(',').append(e.maxCpuTimeMicros)
426                     .append(',').append(e.latencyMicros)
427                     .append(',').append(e.maxLatencyMicros)
428                     .append(',').append(mDetailedTracking ? e.exceptionCount : '_')
429                     .append(',').append(mDetailedTracking ? e.maxRequestSizeBytes : '_')
430                     .append(',').append(mDetailedTracking ? e.maxReplySizeBytes : '_')
431                     .append(',').append(e.recordedCallCount)
432                     .append(',').append(e.callCount);
433             pw.println(sb);
434         }
435         pw.println();
436         pw.println("Per-UID Summary " + datasetSizeDesc
437                 + "(cpu_time, % of total cpu_time, recorded_call_count, call_count, package/uid):");
438         final List<UidEntry> summaryEntries = verbose ? entries
439                 : getHighestValues(entries, value -> value.cpuTimeMicros, 0.9);
440         for (UidEntry entry : summaryEntries) {
441             String uidStr = packageMap.mapUid(entry.workSourceUid);
442             pw.println(String.format("  %10d %3.0f%% %8d %8d %s",
443                     entry.cpuTimeMicros, 100d * entry.cpuTimeMicros / totalCpuTime,
444                     entry.recordedCallCount, entry.callCount, uidStr));
445         }
446         pw.println();
447         pw.println(String.format("  Summary: total_cpu_time=%d, "
448                         + "calls_count=%d, avg_call_cpu_time=%.0f",
449                 totalCpuTime, totalCallsCount, (double) totalCpuTime / totalRecordedCallsCount));
450         pw.println();
451 
452         pw.println("Exceptions thrown (exception_count, class_name):");
453         final List<Pair<String, Integer>> exceptionEntries = new ArrayList<>();
454         // We cannot use new ArrayList(Collection) constructor because MapCollections does not
455         // implement toArray method.
456         mExceptionCounts.entrySet().iterator().forEachRemaining(
457                 (e) -> exceptionEntries.add(Pair.create(e.getKey(), e.getValue())));
458         exceptionEntries.sort((e1, e2) -> Integer.compare(e2.second, e1.second));
459         for (Pair<String, Integer> entry : exceptionEntries) {
460             pw.println(String.format("  %6d %s", entry.second, entry.first));
461         }
462 
463         if (mPeriodicSamplingInterval != 1) {
464             pw.println("");
465             pw.println("/!\\ Displayed data is sampled. See sampling interval at the top.");
466         }
467     }
468 
getThreadTimeMicro()469     protected long getThreadTimeMicro() {
470         return SystemClock.currentThreadTimeMicro();
471     }
472 
getCallingUid()473     protected int getCallingUid() {
474         return Binder.getCallingUid();
475     }
476 
getElapsedRealtimeMicro()477     protected long getElapsedRealtimeMicro() {
478         return SystemClock.elapsedRealtimeNanos() / 1000;
479     }
480 
shouldRecordDetailedData()481     protected boolean shouldRecordDetailedData() {
482         return mRandom.nextInt() % mPeriodicSamplingInterval == 0;
483     }
484 
485     /**
486      * Sets to true to collect all the data.
487      */
setDetailedTracking(boolean enabled)488     public void setDetailedTracking(boolean enabled) {
489         synchronized (mLock) {
490             if (enabled != mDetailedTracking) {
491                 mDetailedTracking = enabled;
492                 reset();
493             }
494         }
495     }
496 
497     /**
498      * Whether to track the screen state.
499      */
setTrackScreenInteractive(boolean enabled)500     public void setTrackScreenInteractive(boolean enabled) {
501         synchronized (mLock) {
502             if (enabled != mTrackScreenInteractive) {
503                 mTrackScreenInteractive = enabled;
504                 reset();
505             }
506         }
507     }
508 
509     /**
510      * Whether to track direct caller uid.
511      */
setTrackDirectCallerUid(boolean enabled)512     public void setTrackDirectCallerUid(boolean enabled) {
513         synchronized (mLock) {
514             if (enabled != mTrackDirectCallingUid) {
515                 mTrackDirectCallingUid = enabled;
516                 reset();
517             }
518         }
519     }
520 
setAddDebugEntries(boolean addDebugEntries)521     public void setAddDebugEntries(boolean addDebugEntries) {
522         mAddDebugEntries = addDebugEntries;
523     }
524 
525     /**
526      * Sets the maximum number of items to track.
527      */
setMaxBinderCallStats(int maxKeys)528     public void setMaxBinderCallStats(int maxKeys) {
529         if (maxKeys <= 0) {
530             Slog.w(TAG, "Ignored invalid max value (value must be positive): "
531                     + maxKeys);
532             return;
533         }
534 
535         synchronized (mLock) {
536             if (maxKeys != mMaxBinderCallStatsCount) {
537                 mMaxBinderCallStatsCount = maxKeys;
538                 reset();
539             }
540         }
541     }
542 
setSamplingInterval(int samplingInterval)543     public void setSamplingInterval(int samplingInterval) {
544         if (samplingInterval <= 0) {
545             Slog.w(TAG, "Ignored invalid sampling interval (value must be positive): "
546                     + samplingInterval);
547             return;
548         }
549 
550         synchronized (mLock) {
551             if (samplingInterval != mPeriodicSamplingInterval) {
552                 mPeriodicSamplingInterval = samplingInterval;
553                 reset();
554             }
555         }
556     }
557 
reset()558     public void reset() {
559         synchronized (mLock) {
560             mCallStatsCount = 0;
561             mUidEntries.clear();
562             mExceptionCounts.clear();
563             mStartCurrentTime = System.currentTimeMillis();
564             mStartElapsedTime = SystemClock.elapsedRealtime();
565             if (mBatteryStopwatch != null) {
566                 mBatteryStopwatch.reset();
567             }
568         }
569     }
570 
571     /**
572      * Aggregated data by uid/class/method to be sent through statsd.
573      */
574     public static class ExportedCallStat {
575         public int callingUid;
576         public int workSourceUid;
577         public String className;
578         public String methodName;
579         public boolean screenInteractive;
580         public long cpuTimeMicros;
581         public long maxCpuTimeMicros;
582         public long latencyMicros;
583         public long maxLatencyMicros;
584         public long callCount;
585         public long recordedCallCount;
586         public long maxRequestSizeBytes;
587         public long maxReplySizeBytes;
588         public long exceptionCount;
589 
590         // Used internally.
591         Class<? extends Binder> binderClass;
592         int transactionCode;
593     }
594 
595     @VisibleForTesting
596     public static class CallStat {
597         // The UID who executed the transaction (i.e. Binder#getCallingUid).
598         public final int callingUid;
599         public final Class<? extends Binder> binderClass;
600         public final int transactionCode;
601         // True if the screen was interactive when the call ended.
602         public final boolean screenInteractive;
603         // Number of calls for which we collected data for. We do not record data for all the calls
604         // when sampling is on.
605         public long recordedCallCount;
606         // Roughly the real number of total calls. We only track only track the API call count once
607         // at least one non-sampled count happened.
608         public long callCount;
609         // Total CPU of all for all the recorded calls.
610         // Approximate total CPU usage can be computed by
611         // cpuTimeMicros * callCount / recordedCallCount
612         public long cpuTimeMicros;
613         public long maxCpuTimeMicros;
614         // Total latency of all for all the recorded calls.
615         // Approximate average latency can be computed by
616         // latencyMicros * callCount / recordedCallCount
617         public long latencyMicros;
618         public long maxLatencyMicros;
619         // The following fields are only computed if mDetailedTracking is set.
620         public long maxRequestSizeBytes;
621         public long maxReplySizeBytes;
622         public long exceptionCount;
623 
CallStat(int callingUid, Class<? extends Binder> binderClass, int transactionCode, boolean screenInteractive)624         CallStat(int callingUid, Class<? extends Binder> binderClass, int transactionCode,
625                 boolean screenInteractive) {
626             this.callingUid = callingUid;
627             this.binderClass = binderClass;
628             this.transactionCode = transactionCode;
629             this.screenInteractive = screenInteractive;
630         }
631     }
632 
633     /** Key used to store CallStat object in a Map. */
634     public static class CallStatKey {
635         public int callingUid;
636         public Class<? extends Binder> binderClass;
637         public int transactionCode;
638         private boolean screenInteractive;
639 
640         @Override
equals(Object o)641         public boolean equals(Object o) {
642             if (this == o) {
643                 return true;
644             }
645 
646             final CallStatKey key = (CallStatKey) o;
647             return callingUid == key.callingUid
648                     && transactionCode == key.transactionCode
649                     && screenInteractive == key.screenInteractive
650                     && (binderClass.equals(key.binderClass));
651         }
652 
653         @Override
hashCode()654         public int hashCode() {
655             int result = binderClass.hashCode();
656             result = 31 * result + transactionCode;
657             result = 31 * result + callingUid;
658             result = 31 * result + (screenInteractive ? 1231 : 1237);
659             return result;
660         }
661     }
662 
663 
664     @VisibleForTesting
665     public static class UidEntry {
666         // The UID who is responsible for the binder transaction. If the bluetooth process execute a
667         // transaction on behalf of app foo, the workSourceUid will be the uid of app foo.
668         public int workSourceUid;
669         // Number of calls for which we collected data for. We do not record data for all the calls
670         // when sampling is on.
671         public long recordedCallCount;
672         // Real number of total calls.
673         public long callCount;
674         // Total CPU of all for all the recorded calls.
675         // Approximate total CPU usage can be computed by
676         // cpuTimeMicros * callCount / recordedCallCount
677         public long cpuTimeMicros;
678 
UidEntry(int uid)679         UidEntry(int uid) {
680             this.workSourceUid = uid;
681         }
682 
683         // Aggregate time spent per each call name: call_desc -> cpu_time_micros
684         private Map<CallStatKey, CallStat> mCallStats = new ArrayMap<>();
685         private CallStatKey mTempKey = new CallStatKey();
686 
687         @Nullable
get(int callingUid, Class<? extends Binder> binderClass, int transactionCode, boolean screenInteractive)688         CallStat get(int callingUid, Class<? extends Binder> binderClass, int transactionCode,
689                 boolean screenInteractive) {
690             // Use a global temporary key to avoid creating new objects for every lookup.
691             mTempKey.callingUid = callingUid;
692             mTempKey.binderClass = binderClass;
693             mTempKey.transactionCode = transactionCode;
694             mTempKey.screenInteractive = screenInteractive;
695             return mCallStats.get(mTempKey);
696         }
697 
getOrCreate(int callingUid, Class<? extends Binder> binderClass, int transactionCode, boolean screenInteractive, boolean maxCallStatsReached)698         CallStat getOrCreate(int callingUid, Class<? extends Binder> binderClass,
699                 int transactionCode, boolean screenInteractive, boolean maxCallStatsReached) {
700             CallStat mapCallStat = get(callingUid, binderClass, transactionCode, screenInteractive);
701             // Only create CallStat if it's a new entry, otherwise update existing instance.
702             if (mapCallStat == null) {
703                 if (maxCallStatsReached) {
704                     mapCallStat = get(OVERFLOW_DIRECT_CALLING_UID, OVERFLOW_BINDER,
705                             OVERFLOW_TRANSACTION_CODE, OVERFLOW_SCREEN_INTERACTIVE);
706                     if (mapCallStat != null) {
707                         return mapCallStat;
708                     }
709 
710                     callingUid = OVERFLOW_DIRECT_CALLING_UID;
711                     binderClass = OVERFLOW_BINDER;
712                     transactionCode = OVERFLOW_TRANSACTION_CODE;
713                     screenInteractive = OVERFLOW_SCREEN_INTERACTIVE;
714                 }
715 
716                 mapCallStat = new CallStat(callingUid, binderClass, transactionCode,
717                         screenInteractive);
718                 CallStatKey key = new CallStatKey();
719                 key.callingUid = callingUid;
720                 key.binderClass = binderClass;
721                 key.transactionCode = transactionCode;
722                 key.screenInteractive = screenInteractive;
723                 mCallStats.put(key, mapCallStat);
724             }
725             return mapCallStat;
726         }
727 
728         /**
729          * Returns list of calls sorted by CPU time
730          */
getCallStatsList()731         public Collection<CallStat> getCallStatsList() {
732             return mCallStats.values();
733         }
734 
735         @Override
toString()736         public String toString() {
737             return "UidEntry{" +
738                     "cpuTimeMicros=" + cpuTimeMicros +
739                     ", callCount=" + callCount +
740                     ", mCallStats=" + mCallStats +
741                     '}';
742         }
743 
744         @Override
equals(Object o)745         public boolean equals(Object o) {
746             if (this == o) {
747                 return true;
748             }
749 
750             UidEntry uidEntry = (UidEntry) o;
751             return workSourceUid == uidEntry.workSourceUid;
752         }
753 
754         @Override
hashCode()755         public int hashCode() {
756             return workSourceUid;
757         }
758     }
759 
760     @VisibleForTesting
getUidEntries()761     public SparseArray<UidEntry> getUidEntries() {
762         return mUidEntries;
763     }
764 
765     @VisibleForTesting
getExceptionCounts()766     public ArrayMap<String, Integer> getExceptionCounts() {
767         return mExceptionCounts;
768     }
769 
770     @VisibleForTesting
getHighestValues(List<T> list, ToDoubleFunction<T> toDouble, double percentile)771     public static <T> List<T> getHighestValues(List<T> list, ToDoubleFunction<T> toDouble,
772             double percentile) {
773         List<T> sortedList = new ArrayList<>(list);
774         sortedList.sort(Comparator.comparingDouble(toDouble).reversed());
775         double total = 0;
776         for (T item : list) {
777             total += toDouble.applyAsDouble(item);
778         }
779         List<T> result = new ArrayList<>();
780         double runningSum = 0;
781         for (T item : sortedList) {
782             if (runningSum > percentile * total) {
783                 break;
784             }
785             result.add(item);
786             runningSum += toDouble.applyAsDouble(item);
787         }
788         return result;
789     }
790 
compareByCpuDesc( ExportedCallStat a, ExportedCallStat b)791     private static int compareByCpuDesc(
792             ExportedCallStat a, ExportedCallStat b) {
793         return Long.compare(b.cpuTimeMicros, a.cpuTimeMicros);
794     }
795 
compareByBinderClassAndCode( ExportedCallStat a, ExportedCallStat b)796     private static int compareByBinderClassAndCode(
797             ExportedCallStat a, ExportedCallStat b) {
798         int result = a.className.compareTo(b.className);
799         return result != 0
800                 ? result
801                 : Integer.compare(a.transactionCode, b.transactionCode);
802     }
803 }
804