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