1 // Copyright 2014 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base; 6 7 import android.app.Activity; 8 import android.content.res.Resources.NotFoundException; 9 import android.os.Looper; 10 import android.os.MessageQueue; 11 import android.util.Log; 12 import android.util.Printer; 13 import android.view.View; 14 import android.view.ViewGroup; 15 16 import androidx.annotation.VisibleForTesting; 17 18 import org.chromium.base.annotations.CalledByNative; 19 import org.chromium.base.annotations.JNINamespace; 20 import org.chromium.base.annotations.NativeMethods; 21 import org.chromium.base.task.PostTask; 22 import org.chromium.base.task.TaskTraits; 23 import org.chromium.build.annotations.MainDex; 24 25 import java.util.ArrayList; 26 27 /** 28 * Java mirror of Chrome trace event API. See base/trace_event/trace_event.h. 29 * 30 * To get scoped trace events, use the "try with resource" construct, for instance: 31 * <pre>{@code 32 * try (TraceEvent e = TraceEvent.scoped("MyTraceEvent")) { 33 * // code. 34 * } 35 * }</pre> 36 * 37 * The event name of the trace events must be a string literal or a |static final String| class 38 * member. Otherwise NoDynamicStringsInTraceEventCheck error will be thrown. 39 * 40 * It is OK to use tracing before the native library has loaded, in a slightly restricted fashion. 41 * @see EarlyTraceEvent for details. 42 */ 43 @JNINamespace("base::android") 44 @MainDex 45 public class TraceEvent implements AutoCloseable { 46 private static volatile boolean sEnabled; // True when tracing into Chrome's tracing service. 47 private static volatile boolean sUiThreadReady; 48 private static boolean sEventNameFilteringEnabled; 49 50 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 51 static class BasicLooperMonitor implements Printer { 52 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 53 static final String LOOPER_TASK_PREFIX = "Looper.dispatch: "; 54 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 55 static final String FILTERED_EVENT_NAME = LOOPER_TASK_PREFIX + "EVENT_NAME_FILTERED"; 56 private static final int SHORTEST_LOG_PREFIX_LENGTH = "<<<<< Finished to ".length(); 57 private String mCurrentTarget; 58 59 @Override println(final String line)60 public void println(final String line) { 61 if (line.startsWith(">")) { 62 beginHandling(line); 63 } else { 64 assert line.startsWith("<"); 65 endHandling(line); 66 } 67 } 68 beginHandling(final String line)69 void beginHandling(final String line) { 70 // May return an out-of-date value. this is not an issue as EarlyTraceEvent#begin() 71 // will filter the event in this case. 72 boolean earlyTracingActive = EarlyTraceEvent.enabled(); 73 if (sEnabled || earlyTracingActive) { 74 // Note that we don't need to log ATrace events here because the 75 // framework does that for us (M+). 76 mCurrentTarget = getTraceEventName(line); 77 if (sEnabled) { 78 TraceEventJni.get().beginToplevel(mCurrentTarget); 79 } else { 80 EarlyTraceEvent.begin(mCurrentTarget, true /*isToplevel*/); 81 } 82 } 83 } 84 endHandling(final String line)85 void endHandling(final String line) { 86 boolean earlyTracingActive = EarlyTraceEvent.enabled(); 87 if ((sEnabled || earlyTracingActive) && mCurrentTarget != null) { 88 if (sEnabled) { 89 TraceEventJni.get().endToplevel(mCurrentTarget); 90 } else { 91 EarlyTraceEvent.end(mCurrentTarget, true /*isToplevel*/); 92 } 93 } 94 mCurrentTarget = null; 95 } 96 97 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) getTraceEventName(String line)98 static String getTraceEventName(String line) { 99 if (sEventNameFilteringEnabled) { 100 return FILTERED_EVENT_NAME; 101 } 102 return LOOPER_TASK_PREFIX + getTarget(line) + "(" + getTargetName(line) + ")"; 103 } 104 105 /** 106 * Android Looper formats |logLine| as 107 * 108 * ">>>>> Dispatching to (TARGET) {HASH_CODE} TARGET_NAME: WHAT" 109 * 110 * and 111 * 112 * "<<<<< Finished to (TARGET) {HASH_CODE} TARGET_NAME". 113 * 114 * This has been the case since at least 2009 (Donut). This function extracts the 115 * TARGET part of the message. 116 */ getTarget(String logLine)117 private static String getTarget(String logLine) { 118 int start = logLine.indexOf('(', SHORTEST_LOG_PREFIX_LENGTH); 119 int end = start == -1 ? -1 : logLine.indexOf(')', start); 120 return end != -1 ? logLine.substring(start + 1, end) : ""; 121 } 122 123 // Extracts the TARGET_NAME part of the log message (see above). getTargetName(String logLine)124 private static String getTargetName(String logLine) { 125 int start = logLine.indexOf('}', SHORTEST_LOG_PREFIX_LENGTH); 126 int end = start == -1 ? -1 : logLine.indexOf(':', start); 127 if (end == -1) { 128 end = logLine.length(); 129 } 130 return start != -1 ? logLine.substring(start + 2, end) : ""; 131 } 132 } 133 134 /** 135 * A class that records, traces and logs statistics about the UI thead's Looper. 136 * The output of this class can be used in a number of interesting ways: 137 * <p> 138 * <ol><li> 139 * When using chrometrace, there will be a near-continuous line of 140 * measurements showing both event dispatches as well as idles; 141 * </li><li> 142 * Logging messages are output for events that run too long on the 143 * event dispatcher, making it easy to identify problematic areas; 144 * </li><li> 145 * Statistics are output whenever there is an idle after a non-trivial 146 * amount of activity, allowing information to be gathered about task 147 * density and execution cadence on the Looper; 148 * </li></ol> 149 * <p> 150 * The class attaches itself as an idle handler to the main Looper, and 151 * monitors the execution of events and idle notifications. Task counters 152 * accumulate between idle notifications and get reset when a new idle 153 * notification is received. 154 */ 155 private static final class IdleTracingLooperMonitor extends BasicLooperMonitor 156 implements MessageQueue.IdleHandler { 157 // Tags for dumping to logcat or TraceEvent 158 private static final String TAG = "TraceEvt_LooperMonitor"; 159 private static final String IDLE_EVENT_NAME = "Looper.queueIdle"; 160 161 // Calculation constants 162 private static final long FRAME_DURATION_MILLIS = 1000L / 60L; // 60 FPS 163 // A reasonable threshold for defining a Looper event as "long running" 164 private static final long MIN_INTERESTING_DURATION_MILLIS = 165 FRAME_DURATION_MILLIS; 166 // A reasonable threshold for a "burst" of tasks on the Looper 167 private static final long MIN_INTERESTING_BURST_DURATION_MILLIS = 168 MIN_INTERESTING_DURATION_MILLIS * 3; 169 170 // Stats tracking 171 private long mLastIdleStartedAt; 172 private long mLastWorkStartedAt; 173 private int mNumTasksSeen; 174 private int mNumIdlesSeen; 175 private int mNumTasksSinceLastIdle; 176 177 // State 178 private boolean mIdleMonitorAttached; 179 180 // Called from within the begin/end methods only. 181 // This method can only execute on the looper thread, because that is 182 // the only thread that is permitted to call Looper.myqueue(). syncIdleMonitoring()183 private final void syncIdleMonitoring() { 184 if (sEnabled && !mIdleMonitorAttached) { 185 // approximate start time for computational purposes 186 mLastIdleStartedAt = TimeUtils.elapsedRealtimeMillis(); 187 Looper.myQueue().addIdleHandler(this); 188 mIdleMonitorAttached = true; 189 Log.v(TAG, "attached idle handler"); 190 } else if (mIdleMonitorAttached && !sEnabled) { 191 Looper.myQueue().removeIdleHandler(this); 192 mIdleMonitorAttached = false; 193 Log.v(TAG, "detached idle handler"); 194 } 195 } 196 197 @Override beginHandling(final String line)198 final void beginHandling(final String line) { 199 // Close-out any prior 'idle' period before starting new task. 200 if (mNumTasksSinceLastIdle == 0) { 201 TraceEvent.end(IDLE_EVENT_NAME); 202 } 203 mLastWorkStartedAt = TimeUtils.elapsedRealtimeMillis(); 204 syncIdleMonitoring(); 205 super.beginHandling(line); 206 } 207 208 @Override endHandling(final String line)209 final void endHandling(final String line) { 210 final long elapsed = TimeUtils.elapsedRealtimeMillis() - mLastWorkStartedAt; 211 if (elapsed > MIN_INTERESTING_DURATION_MILLIS) { 212 traceAndLog(Log.WARN, "observed a task that took " 213 + elapsed + "ms: " + line); 214 } 215 super.endHandling(line); 216 syncIdleMonitoring(); 217 mNumTasksSeen++; 218 mNumTasksSinceLastIdle++; 219 } 220 traceAndLog(int level, String message)221 private static void traceAndLog(int level, String message) { 222 TraceEvent.instant("TraceEvent.LooperMonitor:IdleStats", message); 223 Log.println(level, TAG, message); 224 } 225 226 @Override queueIdle()227 public final boolean queueIdle() { 228 final long now = TimeUtils.elapsedRealtimeMillis(); 229 if (mLastIdleStartedAt == 0) mLastIdleStartedAt = now; 230 final long elapsed = now - mLastIdleStartedAt; 231 mNumIdlesSeen++; 232 TraceEvent.begin(IDLE_EVENT_NAME, mNumTasksSinceLastIdle + " tasks since last idle."); 233 if (elapsed > MIN_INTERESTING_BURST_DURATION_MILLIS) { 234 // Dump stats 235 String statsString = mNumTasksSeen + " tasks and " 236 + mNumIdlesSeen + " idles processed so far, " 237 + mNumTasksSinceLastIdle + " tasks bursted and " 238 + elapsed + "ms elapsed since last idle"; 239 traceAndLog(Log.DEBUG, statsString); 240 } 241 mLastIdleStartedAt = now; 242 mNumTasksSinceLastIdle = 0; 243 return true; // stay installed 244 } 245 } 246 247 // Holder for monitor avoids unnecessary construction on non-debug runs 248 private static final class LooperMonitorHolder { 249 private static final BasicLooperMonitor sInstance = 250 CommandLine.getInstance().hasSwitch(BaseSwitches.ENABLE_IDLE_TRACING) 251 ? new IdleTracingLooperMonitor() : new BasicLooperMonitor(); 252 } 253 254 private final String mName; 255 256 /** 257 * Constructor used to support the "try with resource" construct. 258 */ TraceEvent(String name, String arg)259 private TraceEvent(String name, String arg) { 260 mName = name; 261 begin(name, arg); 262 } 263 264 @Override close()265 public void close() { 266 end(mName); 267 } 268 269 /** 270 * Factory used to support the "try with resource" construct. 271 * 272 * Note that if tracing is not enabled, this will not result in allocating an object. 273 * 274 * @param name Trace event name. 275 * @param arg The arguments of the event. 276 * @return a TraceEvent, or null if tracing is not enabled. 277 */ scoped(String name, String arg)278 public static TraceEvent scoped(String name, String arg) { 279 if (!(EarlyTraceEvent.enabled() || enabled())) return null; 280 return new TraceEvent(name, arg); 281 } 282 283 /** 284 * Similar to {@link #scoped(String, String arg)}, but uses null for |arg|. 285 */ scoped(String name)286 public static TraceEvent scoped(String name) { 287 return scoped(name, null); 288 } 289 290 /** 291 * Notification from native that tracing is enabled/disabled. 292 */ 293 @CalledByNative setEnabled(boolean enabled)294 public static void setEnabled(boolean enabled) { 295 if (enabled) EarlyTraceEvent.disable(); 296 // Only disable logging if Chromium enabled it originally, so as to not disrupt logging done 297 // by other applications 298 if (sEnabled != enabled) { 299 sEnabled = enabled; 300 ThreadUtils.getUiThreadLooper().setMessageLogging( 301 enabled ? LooperMonitorHolder.sInstance : null); 302 } 303 if (sUiThreadReady) { 304 ViewHierarchyDumper.updateEnabledState(); 305 } 306 } 307 308 @CalledByNative setEventNameFilteringEnabled(boolean enabled)309 public static void setEventNameFilteringEnabled(boolean enabled) { 310 sEventNameFilteringEnabled = enabled; 311 } 312 eventNameFilteringEnabled()313 public static boolean eventNameFilteringEnabled() { 314 return sEventNameFilteringEnabled; 315 } 316 317 /** 318 * May enable early tracing depending on the environment. 319 * 320 * @param readCommandLine If true, also check command line flags to see 321 * whether tracing should be turned on. 322 */ maybeEnableEarlyTracing(boolean readCommandLine)323 public static void maybeEnableEarlyTracing(boolean readCommandLine) { 324 // Enable early trace events based on command line flags. This is only 325 // done for Chrome since WebView tracing isn't controlled with command 326 // line flags. 327 if (readCommandLine) { 328 EarlyTraceEvent.maybeEnableInBrowserProcess(); 329 } 330 if (EarlyTraceEvent.enabled()) { 331 ThreadUtils.getUiThreadLooper().setMessageLogging(LooperMonitorHolder.sInstance); 332 } 333 } 334 onNativeTracingReady()335 public static void onNativeTracingReady() { 336 TraceEventJni.get().registerEnabledObserver(); 337 } 338 339 // Called by ThreadUtils. onUiThreadReady()340 static void onUiThreadReady() { 341 sUiThreadReady = true; 342 if (sEnabled) { 343 ViewHierarchyDumper.updateEnabledState(); 344 } 345 } 346 347 /** 348 * @return True if tracing is enabled, false otherwise. 349 * It is safe to call trace methods without checking if TraceEvent 350 * is enabled. 351 */ enabled()352 public static boolean enabled() { 353 return sEnabled; 354 } 355 356 /** 357 * Triggers the 'instant' native trace event with no arguments. 358 * @param name The name of the event. 359 */ instant(String name)360 public static void instant(String name) { 361 if (sEnabled) TraceEventJni.get().instant(name, null); 362 } 363 364 /** 365 * Triggers the 'instant' native trace event. 366 * @param name The name of the event. 367 * @param arg The arguments of the event. 368 */ instant(String name, String arg)369 public static void instant(String name, String arg) { 370 if (sEnabled) TraceEventJni.get().instant(name, arg); 371 } 372 373 /** 374 * Triggers a 'instant' native "AndroidIPC" event. 375 * @param name The name of the IPC. 376 * @param durMs The duration the IPC took in milliseconds. 377 */ instantAndroidIPC(String name, long durMs)378 public static void instantAndroidIPC(String name, long durMs) { 379 if (sEnabled) TraceEventJni.get().instantAndroidIPC(name, durMs); 380 } 381 382 /** 383 * Triggers a 'instant' native "AndroidToolbar" event. 384 * @param blockReason the enum TopToolbarBlockCapture (-1 if not blocked). 385 * @param allowReason the enum TopToolbarAllowCapture (-1 if not allowed). 386 * @param snapshotDiff the enum ToolbarSnapshotDifference (-1 if no diff). 387 */ instantAndroidToolbar(int blockReason, int allowReason, int snapshotDiff)388 public static void instantAndroidToolbar(int blockReason, int allowReason, int snapshotDiff) { 389 if (sEnabled) { 390 TraceEventJni.get().instantAndroidToolbar(blockReason, allowReason, snapshotDiff); 391 } 392 } 393 394 /** 395 * Snapshots the view hierarchy state on the main thread and then finishes emitting a trace 396 * event on the threadpool. 397 */ snapshotViewHierarchy()398 public static void snapshotViewHierarchy() { 399 if (sEnabled && TraceEventJni.get().viewHierarchyDumpEnabled()) { 400 // Emit separate begin and end so we can set the flow id at the end. 401 TraceEvent.begin("instantAndroidViewHierarchy"); 402 403 // If we have no views don't bother to emit any TraceEvents for efficiency. 404 ArrayList<ActivityInfo> views = snapshotViewHierarchyState(); 405 if (views.isEmpty()) { 406 TraceEvent.end("instantAndroidViewHierarchy"); 407 return; 408 } 409 410 // Use the correct snapshot object as a processed scoped flow id. This connects the 411 // mainthread work with the result emitted on the threadpool. We do this because 412 // resolving resource names can trigger exceptions (NotFoundException) which can be 413 // quite slow. 414 long flow = views.hashCode(); 415 416 PostTask.postTask(TaskTraits.BEST_EFFORT, () -> { 417 // Actually output the dump as a trace event on a thread pool. 418 TraceEventJni.get().initViewHierarchyDump(flow, views); 419 }); 420 TraceEvent.end("instantAndroidViewHierarchy", null, flow); 421 } 422 } 423 424 /** 425 * Triggers the 'start' native trace event with no arguments. 426 * @param name The name of the event. 427 * @param id The id of the asynchronous event. 428 */ startAsync(String name, long id)429 public static void startAsync(String name, long id) { 430 EarlyTraceEvent.startAsync(name, id); 431 if (sEnabled) { 432 TraceEventJni.get().startAsync(name, id); 433 } 434 } 435 436 /** 437 * Triggers the 'finish' native trace event with no arguments. 438 * @param name The name of the event. 439 * @param id The id of the asynchronous event. 440 */ finishAsync(String name, long id)441 public static void finishAsync(String name, long id) { 442 EarlyTraceEvent.finishAsync(name, id); 443 if (sEnabled) { 444 TraceEventJni.get().finishAsync(name, id); 445 } 446 } 447 448 /** 449 * Triggers the 'begin' native trace event with no arguments. 450 * @param name The name of the event. 451 */ begin(String name)452 public static void begin(String name) { 453 begin(name, null); 454 } 455 456 /** 457 * Triggers the 'begin' native trace event. 458 * @param name The name of the event. 459 * @param arg The arguments of the event. 460 */ begin(String name, String arg)461 public static void begin(String name, String arg) { 462 EarlyTraceEvent.begin(name, false /*isToplevel*/); 463 if (sEnabled) { 464 TraceEventJni.get().begin(name, arg); 465 } 466 } 467 468 /** 469 * Triggers the 'end' native trace event with no arguments. 470 * @param name The name of the event. 471 */ end(String name)472 public static void end(String name) { 473 end(name, null); 474 } 475 476 /** 477 * Triggers the 'end' native trace event. 478 * @param name The name of the event. 479 * @param arg The arguments of the event. 480 */ end(String name, String arg)481 public static void end(String name, String arg) { 482 end(name, arg, 0); 483 } 484 485 /** 486 * Triggers the 'end' native trace event. 487 * @param name The name of the event. 488 * @param arg The arguments of the event. 489 * @param flow The flow ID to associate with this event (0 is treated as invalid). 490 */ end(String name, String arg, long flow)491 public static void end(String name, String arg, long flow) { 492 EarlyTraceEvent.end(name, false /*isToplevel*/); 493 if (sEnabled) { 494 TraceEventJni.get().end(name, arg, flow); 495 } 496 } 497 snapshotViewHierarchyState()498 public static ArrayList<ActivityInfo> snapshotViewHierarchyState() { 499 if (!ApplicationStatus.isInitialized()) { 500 return new ArrayList<ActivityInfo>(); 501 } 502 503 // In local testing we generally just have one activity. 504 ArrayList<ActivityInfo> views = new ArrayList<>(2); 505 for (Activity a : ApplicationStatus.getRunningActivities()) { 506 views.add(new ActivityInfo(a.getClass().getName())); 507 ViewHierarchyDumper.dumpView(views.get(views.size() - 1), 508 /*parentId=*/0, a.getWindow().getDecorView().getRootView()); 509 } 510 return views; 511 } 512 513 @NativeMethods 514 interface Natives { registerEnabledObserver()515 void registerEnabledObserver(); instant(String name, String arg)516 void instant(String name, String arg); begin(String name, String arg)517 void begin(String name, String arg); end(String name, String arg, long flow)518 void end(String name, String arg, long flow); beginToplevel(String target)519 void beginToplevel(String target); endToplevel(String target)520 void endToplevel(String target); startAsync(String name, long id)521 void startAsync(String name, long id); finishAsync(String name, long id)522 void finishAsync(String name, long id); viewHierarchyDumpEnabled()523 boolean viewHierarchyDumpEnabled(); initViewHierarchyDump(long id, Object list)524 void initViewHierarchyDump(long id, Object list); startActivityDump(String name, long dumpProtoPtr)525 long startActivityDump(String name, long dumpProtoPtr); addViewDump(int id, int parentId, boolean isShown, boolean isDirty, String className, String resourceName, long activityProtoPtr)526 void addViewDump(int id, int parentId, boolean isShown, boolean isDirty, String className, 527 String resourceName, long activityProtoPtr); instantAndroidIPC(String name, long durMs)528 void instantAndroidIPC(String name, long durMs); instantAndroidToolbar(int blockReason, int allowReason, int snapshotDiff)529 void instantAndroidToolbar(int blockReason, int allowReason, int snapshotDiff); 530 } 531 532 /** 533 * A method to be called by native code that uses the ViewHierarchyDumper class to emit a trace 534 * event with views of all running activities of the app. 535 */ 536 @CalledByNative dumpViewHierarchy(long dumpProtoPtr, Object list)537 public static void dumpViewHierarchy(long dumpProtoPtr, Object list) { 538 if (!ApplicationStatus.isInitialized()) { 539 return; 540 } 541 542 // Convert the Object back into the ArrayList of ActivityInfo, lifetime of this object is 543 // maintained by the Runnable that we are running in currently. 544 ArrayList<ActivityInfo> activities = (ArrayList<ActivityInfo>) list; 545 546 for (ActivityInfo activity : activities) { 547 long activityProtoPtr = 548 TraceEventJni.get().startActivityDump(activity.mActivityName, dumpProtoPtr); 549 for (ViewInfo view : activity.mViews) { 550 // We need to resolve the resource, take care as NotFoundException can be common and 551 // java exceptions aren't he fastest thing ever. 552 String resource; 553 try { 554 resource = view.mRes != null ? (view.mId == 0 || view.mId == -1 555 ? "__no_id__" 556 : view.mRes.getResourceName(view.mId)) 557 : "__no_resources__"; 558 } catch (NotFoundException e) { 559 resource = "__name_not_found__"; 560 } 561 TraceEventJni.get().addViewDump(view.mId, view.mParentId, view.mIsShown, 562 view.mIsDirty, view.mClassName, resource, activityProtoPtr); 563 } 564 } 565 } 566 567 /** 568 * This class contains the minimum information to represent a view that the {@link 569 * #ViewHierarchyDumper} needs, so that in {@link #snapshotViewHierarchy} we can output a trace 570 * event off the main thread. 571 */ 572 public static class ViewInfo { ViewInfo(int id, int parentId, boolean isShown, boolean isDirty, String className, android.content.res.Resources res)573 public ViewInfo(int id, int parentId, boolean isShown, boolean isDirty, String className, 574 android.content.res.Resources res) { 575 mId = id; 576 mParentId = parentId; 577 mIsShown = isShown; 578 mIsDirty = isDirty; 579 mClassName = className; 580 mRes = res; 581 } 582 583 private int mId; 584 private int mParentId; 585 private boolean mIsShown; 586 private boolean mIsDirty; 587 private String mClassName; 588 // One can use mRes to resolve mId to a resource name. 589 private android.content.res.Resources mRes; 590 } 591 592 /** 593 * This class contains the minimum information to represent an Activity that the {@link 594 * #ViewHierarchyDumper} needs, so that in {@link #snapshotViewHierarchy} we can output a trace 595 * event off the main thread. 596 */ 597 public static class ActivityInfo { ActivityInfo(String activityName)598 public ActivityInfo(String activityName) { 599 mActivityName = activityName; 600 // Local testing found about 115ish views in the ChromeTabbedActivity. 601 mViews = new ArrayList<ViewInfo>(125); 602 } 603 604 public String mActivityName; 605 public ArrayList<ViewInfo> mViews; 606 } 607 608 /** 609 * A class that periodically dumps the view hierarchy of all running activities of the app to 610 * the trace. Enabled/disabled via the disabled-by-default-android_view_hierarchy trace 611 * category. 612 * 613 * The class registers itself as an idle handler, so that it can run when there are no other 614 * tasks in the queue (but not more often than once a second). When the queue is idle, 615 * it calls the initViewHierarchyDump() native function which in turn calls the 616 * TraceEvent.dumpViewHierarchy() with a pointer to the proto buffer to fill in. The 617 * TraceEvent.dumpViewHierarchy() traverses all activities and dumps view hierarchy for every 618 * activity. Altogether, the call sequence is as follows: 619 * ViewHierarchyDumper.queueIdle() 620 * -> JNI#initViewHierarchyDump() 621 * -> TraceEvent.dumpViewHierarchy() 622 * -> JNI#startActivityDump() 623 * -> ViewHierarchyDumper.dumpView() 624 * -> JNI#addViewDump() 625 */ 626 private static final class ViewHierarchyDumper implements MessageQueue.IdleHandler { 627 private static final String EVENT_NAME = "TraceEvent.ViewHierarchyDumper"; 628 private static final long MIN_VIEW_DUMP_INTERVAL_MILLIS = 1000L; 629 private static boolean sEnabled; 630 private static ViewHierarchyDumper sInstance; 631 private long mLastDumpTs; 632 633 @Override queueIdle()634 public final boolean queueIdle() { 635 final long now = TimeUtils.elapsedRealtimeMillis(); 636 if (mLastDumpTs == 0 || (now - mLastDumpTs) > MIN_VIEW_DUMP_INTERVAL_MILLIS) { 637 mLastDumpTs = now; 638 snapshotViewHierarchy(); 639 } 640 641 // Returning true to keep IdleHandler alive. 642 return true; 643 } 644 updateEnabledState()645 public static void updateEnabledState() { 646 PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> { 647 if (TraceEventJni.get().viewHierarchyDumpEnabled()) { 648 if (sInstance == null) { 649 sInstance = new ViewHierarchyDumper(); 650 } 651 enable(); 652 } else { 653 if (sInstance != null) { 654 disable(); 655 } 656 } 657 }); 658 } 659 dumpView(ActivityInfo collection, int parentId, View v)660 private static void dumpView(ActivityInfo collection, int parentId, View v) { 661 ThreadUtils.assertOnUiThread(); 662 int id = v.getId(); 663 collection.mViews.add(new ViewInfo(id, parentId, v.isShown(), v.isDirty(), 664 v.getClass().getSimpleName(), v.getResources())); 665 666 if (v instanceof ViewGroup) { 667 ViewGroup vg = (ViewGroup) v; 668 for (int i = 0; i < vg.getChildCount(); i++) { 669 dumpView(collection, id, vg.getChildAt(i)); 670 } 671 } 672 } 673 enable()674 private static void enable() { 675 ThreadUtils.assertOnUiThread(); 676 if (!sEnabled) { 677 Looper.myQueue().addIdleHandler(sInstance); 678 sEnabled = true; 679 } 680 } 681 disable()682 private static void disable() { 683 ThreadUtils.assertOnUiThread(); 684 if (sEnabled) { 685 Looper.myQueue().removeIdleHandler(sInstance); 686 sEnabled = false; 687 } 688 } 689 } 690 } 691