• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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