blob: 2cea0230f5cd6b4040a998d7fb0425a91f67042b [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base;
import android.app.Activity;
import android.content.res.Resources.NotFoundException;
import android.os.Looper;
import android.os.MessageQueue;
import android.util.Log;
import android.util.Printer;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.VisibleForTesting;
import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import java.util.ArrayList;
/**
* Java mirror of Chrome trace event API. See base/trace_event/trace_event.h.
*
* To get scoped trace events, use the "try with resource" construct, for instance:
* <pre>{@code
* try (TraceEvent e = TraceEvent.scoped("MyTraceEvent")) {
* // code.
* }
* }</pre>
*
* The event name of the trace events must be a string literal or a |static final String| class
* member. Otherwise NoDynamicStringsInTraceEventCheck error will be thrown.
*
* It is OK to use tracing before the native library has loaded, in a slightly restricted fashion.
* @see EarlyTraceEvent for details.
*/
@JNINamespace("base::android")
public class TraceEvent implements AutoCloseable {
private static volatile boolean sEnabled; // True when tracing into Chrome's tracing service.
private static volatile boolean sUiThreadReady;
private static boolean sEventNameFilteringEnabled;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static class BasicLooperMonitor implements Printer {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static final String LOOPER_TASK_PREFIX = "Looper.dispatch: ";
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static final String FILTERED_EVENT_NAME = LOOPER_TASK_PREFIX + "EVENT_NAME_FILTERED";
private static final int SHORTEST_LOG_PREFIX_LENGTH = "<<<<< Finished to ".length();
private String mCurrentTarget;
@Override
public void println(final String line) {
if (line.startsWith(">")) {
beginHandling(line);
} else {
assert line.startsWith("<");
endHandling(line);
}
}
void beginHandling(final String line) {
// May return an out-of-date value. this is not an issue as EarlyTraceEvent#begin()
// will filter the event in this case.
boolean earlyTracingActive = EarlyTraceEvent.enabled();
if (sEnabled || earlyTracingActive) {
// Note that we don't need to log ATrace events here because the
// framework does that for us (M+).
mCurrentTarget = getTraceEventName(line);
if (sEnabled) {
TraceEventJni.get().beginToplevel(mCurrentTarget);
} else {
EarlyTraceEvent.begin(mCurrentTarget, /* isToplevel= */ true);
}
}
}
void endHandling(final String line) {
boolean earlyTracingActive = EarlyTraceEvent.enabled();
if ((sEnabled || earlyTracingActive) && mCurrentTarget != null) {
if (sEnabled) {
TraceEventJni.get().endToplevel(mCurrentTarget);
} else {
EarlyTraceEvent.end(mCurrentTarget, /* isToplevel= */ true);
}
}
mCurrentTarget = null;
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static String getTraceEventName(String line) {
if (sEventNameFilteringEnabled) {
return FILTERED_EVENT_NAME;
}
return LOOPER_TASK_PREFIX + getTarget(line) + "(" + getTargetName(line) + ")";
}
/**
* Android Looper formats |logLine| as
*
* ">>>>> Dispatching to (TARGET) {HASH_CODE} TARGET_NAME: WHAT"
*
* and
*
* "<<<<< Finished to (TARGET) {HASH_CODE} TARGET_NAME".
*
* This has been the case since at least 2009 (Donut). This function extracts the
* TARGET part of the message.
*/
private static String getTarget(String logLine) {
int start = logLine.indexOf('(', SHORTEST_LOG_PREFIX_LENGTH);
int end = start == -1 ? -1 : logLine.indexOf(')', start);
return end != -1 ? logLine.substring(start + 1, end) : "";
}
// Extracts the TARGET_NAME part of the log message (see above).
private static String getTargetName(String logLine) {
int start = logLine.indexOf('}', SHORTEST_LOG_PREFIX_LENGTH);
int end = start == -1 ? -1 : logLine.indexOf(':', start);
if (end == -1) {
end = logLine.length();
}
return start != -1 ? logLine.substring(start + 2, end) : "";
}
}
/**
* A class that records, traces and logs statistics about the UI thead's Looper.
* The output of this class can be used in a number of interesting ways:
* <p>
* <ol><li>
* When using chrometrace, there will be a near-continuous line of
* measurements showing both event dispatches as well as idles;
* </li><li>
* Logging messages are output for events that run too long on the
* event dispatcher, making it easy to identify problematic areas;
* </li><li>
* Statistics are output whenever there is an idle after a non-trivial
* amount of activity, allowing information to be gathered about task
* density and execution cadence on the Looper;
* </li></ol>
* <p>
* The class attaches itself as an idle handler to the main Looper, and
* monitors the execution of events and idle notifications. Task counters
* accumulate between idle notifications and get reset when a new idle
* notification is received.
*/
private static final class IdleTracingLooperMonitor extends BasicLooperMonitor
implements MessageQueue.IdleHandler {
// Tags for dumping to logcat or TraceEvent
private static final String TAG = "TraceEvt_LooperMonitor";
private static final String IDLE_EVENT_NAME = "Looper.queueIdle";
// Calculation constants
private static final long FRAME_DURATION_MILLIS = 1000L / 60L; // 60 FPS
// A reasonable threshold for defining a Looper event as "long running"
private static final long MIN_INTERESTING_DURATION_MILLIS = FRAME_DURATION_MILLIS;
// A reasonable threshold for a "burst" of tasks on the Looper
private static final long MIN_INTERESTING_BURST_DURATION_MILLIS =
MIN_INTERESTING_DURATION_MILLIS * 3;
// Stats tracking
private long mLastIdleStartedAt;
private long mLastWorkStartedAt;
private int mNumTasksSeen;
private int mNumIdlesSeen;
private int mNumTasksSinceLastIdle;
// State
private boolean mIdleMonitorAttached;
// Called from within the begin/end methods only.
// This method can only execute on the looper thread, because that is
// the only thread that is permitted to call Looper.myqueue().
private final void syncIdleMonitoring() {
if (sEnabled && !mIdleMonitorAttached) {
// approximate start time for computational purposes
mLastIdleStartedAt = TimeUtils.elapsedRealtimeMillis();
Looper.myQueue().addIdleHandler(this);
mIdleMonitorAttached = true;
Log.v(TAG, "attached idle handler");
} else if (mIdleMonitorAttached && !sEnabled) {
Looper.myQueue().removeIdleHandler(this);
mIdleMonitorAttached = false;
Log.v(TAG, "detached idle handler");
}
}
@Override
final void beginHandling(final String line) {
// Close-out any prior 'idle' period before starting new task.
if (mNumTasksSinceLastIdle == 0) {
TraceEvent.end(IDLE_EVENT_NAME);
}
mLastWorkStartedAt = TimeUtils.elapsedRealtimeMillis();
syncIdleMonitoring();
super.beginHandling(line);
}
@Override
final void endHandling(final String line) {
final long elapsed = TimeUtils.elapsedRealtimeMillis() - mLastWorkStartedAt;
if (elapsed > MIN_INTERESTING_DURATION_MILLIS) {
traceAndLog(Log.WARN, "observed a task that took " + elapsed + "ms: " + line);
}
super.endHandling(line);
syncIdleMonitoring();
mNumTasksSeen++;
mNumTasksSinceLastIdle++;
}
private static void traceAndLog(int level, String message) {
TraceEvent.instant("TraceEvent.LooperMonitor:IdleStats", message);
Log.println(level, TAG, message);
}
@Override
public final boolean queueIdle() {
final long now = TimeUtils.elapsedRealtimeMillis();
if (mLastIdleStartedAt == 0) mLastIdleStartedAt = now;
final long elapsed = now - mLastIdleStartedAt;
mNumIdlesSeen++;
TraceEvent.begin(IDLE_EVENT_NAME, mNumTasksSinceLastIdle + " tasks since last idle.");
if (elapsed > MIN_INTERESTING_BURST_DURATION_MILLIS) {
// Dump stats
String statsString =
mNumTasksSeen
+ " tasks and "
+ mNumIdlesSeen
+ " idles processed so far, "
+ mNumTasksSinceLastIdle
+ " tasks bursted and "
+ elapsed
+ "ms elapsed since last idle";
traceAndLog(Log.DEBUG, statsString);
}
mLastIdleStartedAt = now;
mNumTasksSinceLastIdle = 0;
return true; // stay installed
}
}
// Holder for monitor avoids unnecessary construction on non-debug runs
private static final class LooperMonitorHolder {
private static final BasicLooperMonitor sInstance =
CommandLine.getInstance().hasSwitch(BaseSwitches.ENABLE_IDLE_TRACING)
? new IdleTracingLooperMonitor()
: new BasicLooperMonitor();
}
private final String mName;
/** Constructor used to support the "try with resource" construct. */
private TraceEvent(String name, String arg) {
mName = name;
begin(name, arg);
}
/** Constructor used to support the "try with resource" construct. */
private TraceEvent(String name, int arg) {
mName = name;
begin(name, arg);
}
@Override
public void close() {
end(mName);
}
/**
* Factory used to support the "try with resource" construct.
*
* Note that if tracing is not enabled, this will not result in allocating an object.
*
* @param name Trace event name.
* @param arg The arguments of the event.
* @return a TraceEvent, or null if tracing is not enabled.
*/
public static TraceEvent scoped(String name, String arg) {
if (!(EarlyTraceEvent.enabled() || enabled())) return null;
return new TraceEvent(name, arg);
}
/**
* Factory used to support the "try with resource" construct.
*
* Note that if tracing is not enabled, this will not result in allocating an object.
*
* @param name Trace event name.
* @param arg An integer argument of the event.
* @return a TraceEvent, or null if tracing is not enabled.
*/
public static TraceEvent scoped(String name, int arg) {
if (!(EarlyTraceEvent.enabled() || enabled())) return null;
return new TraceEvent(name, arg);
}
/** Similar to {@link #scoped(String, String arg)}, but uses null for |arg|. */
public static TraceEvent scoped(String name) {
return scoped(name, null);
}
/** Notification from native that tracing is enabled/disabled. */
@CalledByNative
public static void setEnabled(boolean enabled) {
if (enabled) EarlyTraceEvent.disable();
// Only disable logging if Chromium enabled it originally, so as to not disrupt logging done
// by other applications
if (sEnabled != enabled) {
sEnabled = enabled;
ThreadUtils.getUiThreadLooper()
.setMessageLogging(enabled ? LooperMonitorHolder.sInstance : null);
}
if (sUiThreadReady) {
ViewHierarchyDumper.updateEnabledState();
}
}
@CalledByNative
public static void setEventNameFilteringEnabled(boolean enabled) {
sEventNameFilteringEnabled = enabled;
}
public static boolean eventNameFilteringEnabled() {
return sEventNameFilteringEnabled;
}
/**
* May enable early tracing depending on the environment.
*
* @param readCommandLine If true, also check command line flags to see
* whether tracing should be turned on.
*/
public static void maybeEnableEarlyTracing(boolean readCommandLine) {
// Enable early trace events based on command line flags. This is only
// done for Chrome since WebView tracing isn't controlled with command
// line flags.
if (readCommandLine) {
EarlyTraceEvent.maybeEnableInBrowserProcess();
}
if (EarlyTraceEvent.enabled()) {
ThreadUtils.getUiThreadLooper().setMessageLogging(LooperMonitorHolder.sInstance);
}
}
public static void onNativeTracingReady() {
TraceEventJni.get().registerEnabledObserver();
}
// Called by ThreadUtils.
static void onUiThreadReady() {
sUiThreadReady = true;
if (sEnabled) {
ViewHierarchyDumper.updateEnabledState();
}
}
/**
* @return True if tracing is enabled, false otherwise.
* It is safe to call trace methods without checking if TraceEvent
* is enabled.
*/
public static boolean enabled() {
return sEnabled;
}
/**
* Triggers the 'instant' native trace event with no arguments.
* @param name The name of the event.
*/
public static void instant(String name) {
if (sEnabled) TraceEventJni.get().instant(name, null);
}
/**
* Triggers the 'instant' native trace event.
* @param name The name of the event.
* @param arg The arguments of the event.
*/
public static void instant(String name, String arg) {
if (sEnabled) TraceEventJni.get().instant(name, arg);
}
/**
* Triggers a 'instant' native "AndroidIPC" event.
* @param name The name of the IPC.
* @param durMs The duration the IPC took in milliseconds.
*/
public static void instantAndroidIPC(String name, long durMs) {
if (sEnabled) TraceEventJni.get().instantAndroidIPC(name, durMs);
}
/**
* Triggers a 'instant' native "AndroidToolbar" event.
* @param blockReason the enum TopToolbarBlockCapture (-1 if not blocked).
* @param allowReason the enum TopToolbarAllowCapture (-1 if not allowed).
* @param snapshotDiff the enum ToolbarSnapshotDifference (-1 if no diff).
*/
public static void instantAndroidToolbar(int blockReason, int allowReason, int snapshotDiff) {
if (sEnabled) {
TraceEventJni.get().instantAndroidToolbar(blockReason, allowReason, snapshotDiff);
}
}
/**
* Records a 'WebView.Startup.CreationTime.TotalFactoryInitTime' event with the
* 'android_webview.timeline' category starting at `startTimeMs` with the duration of
* `durationMs`.
*/
public static void webViewStartupTotalFactoryInit(long startTimeMs, long durationMs) {
if (sEnabled) {
TraceEventJni.get().webViewStartupTotalFactoryInit(startTimeMs, durationMs);
}
}
/**
* Records a 'WebView.Startup.CreationTime.Stage1.FactoryInit' event with the
* 'android_webview.timeline' category starting at `startTimeMs` with the duration of
* `durationMs`.
*/
public static void webViewStartupStage1(long startTimeMs, long durationMs) {
if (sEnabled) {
TraceEventJni.get().webViewStartupStage1(startTimeMs, durationMs);
}
}
/**
* Records 'WebView.Startup.CreationTime.Stage2.ProviderInit.Warm' and
* 'WebView.Startup.CreationTime.Stage2.ProviderInit.Cold' events depending on the value of
* `isColdStartup` with the 'android_webview.timeline' category starting at `startTimeMs` with
* the duration of `durationMs`.
*/
public static void webViewStartupStage2(
long startTimeMs, long durationMs, boolean isColdStartup) {
if (sEnabled) {
TraceEventJni.get().webViewStartupStage2(startTimeMs, durationMs, isColdStartup);
}
}
/** Records 'Startup.LaunchCause' event with the 'interactions' category. */
public static void startupLaunchCause(long activityId, int launchCause) {
if (!sEnabled) return;
TraceEventJni.get().startupLaunchCause(activityId, launchCause);
}
/** Records 'Startup.TimeToFirstVisibleContent2' event with the 'interactions' category. */
public static void startupTimeToFirstVisibleContent2(
long activityId, long startTimeMs, long durationMs) {
if (!sEnabled) return;
TraceEventJni.get().startupTimeToFirstVisibleContent2(activityId, startTimeMs, durationMs);
}
/**
* Snapshots the view hierarchy state on the main thread and then finishes emitting a trace
* event on the threadpool.
*/
public static void snapshotViewHierarchy() {
if (sEnabled && TraceEventJni.get().viewHierarchyDumpEnabled()) {
// Emit separate begin and end so we can set the flow id at the end.
TraceEvent.begin("instantAndroidViewHierarchy");
// If we have no views don't bother to emit any TraceEvents for efficiency.
ArrayList<ActivityInfo> views = snapshotViewHierarchyState();
if (views.isEmpty()) {
TraceEvent.end("instantAndroidViewHierarchy");
return;
}
// Use the correct snapshot object as a processed scoped flow id. This connects the
// mainthread work with the result emitted on the threadpool. We do this because
// resolving resource names can trigger exceptions (NotFoundException) which can be
// quite slow.
long flow = views.hashCode();
PostTask.postTask(
TaskTraits.BEST_EFFORT,
() -> {
// Actually output the dump as a trace event on a thread pool.
TraceEventJni.get().initViewHierarchyDump(flow, views);
});
TraceEvent.end("instantAndroidViewHierarchy", null, flow);
}
}
/**
* Triggers the 'start' native trace event with no arguments.
* @param name The name of the event.
* @param id The id of the asynchronous event.
*/
public static void startAsync(String name, long id) {
EarlyTraceEvent.startAsync(name, id);
if (sEnabled) {
TraceEventJni.get().startAsync(name, id);
}
}
/**
* Triggers the 'finish' native trace event with no arguments.
* @param name The name of the event.
* @param id The id of the asynchronous event.
*/
public static void finishAsync(String name, long id) {
EarlyTraceEvent.finishAsync(name, id);
if (sEnabled) {
TraceEventJni.get().finishAsync(name, id);
}
}
/**
* Triggers the 'begin' native trace event with no arguments.
* @param name The name of the event.
*/
public static void begin(String name) {
begin(name, null);
}
/**
* Triggers the 'begin' native trace event.
* @param name The name of the event.
* @param arg The arguments of the event.
*/
public static void begin(String name, String arg) {
EarlyTraceEvent.begin(name, /* isToplevel= */ false);
if (sEnabled) {
TraceEventJni.get().begin(name, arg);
}
}
/**
* Triggers the 'begin' native trace event.
* @param name The name of the event.
* @param arg An integer argument of the event.
*/
public static void begin(String name, int arg) {
EarlyTraceEvent.begin(name, /* isToplevel= */ false);
if (sEnabled) {
TraceEventJni.get().beginWithIntArg(name, arg);
}
}
/**
* Triggers the 'end' native trace event with no arguments.
* @param name The name of the event.
*/
public static void end(String name) {
end(name, null);
}
/**
* Triggers the 'end' native trace event.
* @param name The name of the event.
* @param arg The arguments of the event.
*/
public static void end(String name, String arg) {
end(name, arg, 0);
}
/**
* Triggers the 'end' native trace event.
* @param name The name of the event.
* @param arg The arguments of the event.
* @param flow The flow ID to associate with this event (0 is treated as invalid).
*/
public static void end(String name, String arg, long flow) {
EarlyTraceEvent.end(name, /* isToplevel= */ false);
if (sEnabled) {
TraceEventJni.get().end(name, arg, flow);
}
}
public static ArrayList<ActivityInfo> snapshotViewHierarchyState() {
if (!ApplicationStatus.isInitialized()) {
return new ArrayList<ActivityInfo>();
}
// In local testing we generally just have one activity.
ArrayList<ActivityInfo> views = new ArrayList<>(2);
for (Activity a : ApplicationStatus.getRunningActivities()) {
views.add(new ActivityInfo(a.getClass().getName()));
ViewHierarchyDumper.dumpView(
views.get(views.size() - 1),
/* parentId= */ 0,
a.getWindow().getDecorView().getRootView());
}
return views;
}
@NativeMethods
interface Natives {
void registerEnabledObserver();
void instant(String name, String arg);
void begin(String name, String arg);
void beginWithIntArg(String name, int arg);
void end(String name, String arg, long flow);
void beginToplevel(String target);
void endToplevel(String target);
void startAsync(String name, long id);
void finishAsync(String name, long id);
boolean viewHierarchyDumpEnabled();
void initViewHierarchyDump(long id, Object list);
long startActivityDump(String name, long dumpProtoPtr);
void addViewDump(
int id,
int parentId,
boolean isShown,
boolean isDirty,
String className,
String resourceName,
long activityProtoPtr);
void instantAndroidIPC(String name, long durMs);
void instantAndroidToolbar(int blockReason, int allowReason, int snapshotDiff);
void webViewStartupTotalFactoryInit(long startTimeMs, long durationMs);
void webViewStartupStage1(long startTimeMs, long durationMs);
void webViewStartupStage2(long startTimeMs, long durationMs, boolean isColdStartup);
void startupLaunchCause(long activityId, int launchCause);
void startupTimeToFirstVisibleContent2(long activityId, long startTimeMs, long durationMs);
}
/**
* A method to be called by native code that uses the ViewHierarchyDumper class to emit a trace
* event with views of all running activities of the app.
*/
@CalledByNative
public static void dumpViewHierarchy(long dumpProtoPtr, Object list) {
if (!ApplicationStatus.isInitialized()) {
return;
}
// Convert the Object back into the ArrayList of ActivityInfo, lifetime of this object is
// maintained by the Runnable that we are running in currently.
ArrayList<ActivityInfo> activities = (ArrayList<ActivityInfo>) list;
for (ActivityInfo activity : activities) {
long activityProtoPtr =
TraceEventJni.get().startActivityDump(activity.mActivityName, dumpProtoPtr);
for (ViewInfo view : activity.mViews) {
// We need to resolve the resource, take care as NotFoundException can be common and
// java exceptions aren't he fastest thing ever.
String resource;
try {
resource =
view.mRes != null
? (view.mId == 0 || view.mId == -1
? "__no_id__"
: view.mRes.getResourceName(view.mId))
: "__no_resources__";
} catch (NotFoundException e) {
resource = "__name_not_found__";
}
TraceEventJni.get()
.addViewDump(
view.mId,
view.mParentId,
view.mIsShown,
view.mIsDirty,
view.mClassName,
resource,
activityProtoPtr);
}
}
}
/**
* This class contains the minimum information to represent a view that the {@link
* #ViewHierarchyDumper} needs, so that in {@link #snapshotViewHierarchy} we can output a trace
* event off the main thread.
*/
public static class ViewInfo {
public ViewInfo(
int id,
int parentId,
boolean isShown,
boolean isDirty,
String className,
android.content.res.Resources res) {
mId = id;
mParentId = parentId;
mIsShown = isShown;
mIsDirty = isDirty;
mClassName = className;
mRes = res;
}
private int mId;
private int mParentId;
private boolean mIsShown;
private boolean mIsDirty;
private String mClassName;
// One can use mRes to resolve mId to a resource name.
private android.content.res.Resources mRes;
}
/**
* This class contains the minimum information to represent an Activity that the {@link
* #ViewHierarchyDumper} needs, so that in {@link #snapshotViewHierarchy} we can output a trace
* event off the main thread.
*/
public static class ActivityInfo {
public ActivityInfo(String activityName) {
mActivityName = activityName;
// Local testing found about 115ish views in the ChromeTabbedActivity.
mViews = new ArrayList<ViewInfo>(125);
}
public String mActivityName;
public ArrayList<ViewInfo> mViews;
}
/**
* A class that periodically dumps the view hierarchy of all running activities of the app to
* the trace. Enabled/disabled via the disabled-by-default-android_view_hierarchy trace
* category.
*
* The class registers itself as an idle handler, so that it can run when there are no other
* tasks in the queue (but not more often than once a second). When the queue is idle,
* it calls the initViewHierarchyDump() native function which in turn calls the
* TraceEvent.dumpViewHierarchy() with a pointer to the proto buffer to fill in. The
* TraceEvent.dumpViewHierarchy() traverses all activities and dumps view hierarchy for every
* activity. Altogether, the call sequence is as follows:
* ViewHierarchyDumper.queueIdle()
* -> JNI#initViewHierarchyDump()
* -> TraceEvent.dumpViewHierarchy()
* -> JNI#startActivityDump()
* -> ViewHierarchyDumper.dumpView()
* -> JNI#addViewDump()
*/
private static final class ViewHierarchyDumper implements MessageQueue.IdleHandler {
private static final String EVENT_NAME = "TraceEvent.ViewHierarchyDumper";
private static final long MIN_VIEW_DUMP_INTERVAL_MILLIS = 1000L;
private static boolean sEnabled;
private static ViewHierarchyDumper sInstance;
private long mLastDumpTs;
@Override
public final boolean queueIdle() {
final long now = TimeUtils.elapsedRealtimeMillis();
if (mLastDumpTs == 0 || (now - mLastDumpTs) > MIN_VIEW_DUMP_INTERVAL_MILLIS) {
mLastDumpTs = now;
snapshotViewHierarchy();
}
// Returning true to keep IdleHandler alive.
return true;
}
public static void updateEnabledState() {
PostTask.runOrPostTask(
TaskTraits.UI_DEFAULT,
() -> {
if (TraceEventJni.get().viewHierarchyDumpEnabled()) {
if (sInstance == null) {
sInstance = new ViewHierarchyDumper();
}
enable();
} else {
if (sInstance != null) {
disable();
}
}
});
}
private static void dumpView(ActivityInfo collection, int parentId, View v) {
ThreadUtils.assertOnUiThread();
int id = v.getId();
collection.mViews.add(
new ViewInfo(
id,
parentId,
v.isShown(),
v.isDirty(),
v.getClass().getSimpleName(),
v.getResources()));
if (v instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) v;
for (int i = 0; i < vg.getChildCount(); i++) {
dumpView(collection, id, vg.getChildAt(i));
}
}
}
private static void enable() {
ThreadUtils.assertOnUiThread();
if (!sEnabled) {
Looper.myQueue().addIdleHandler(sInstance);
sEnabled = true;
}
}
private static void disable() {
ThreadUtils.assertOnUiThread();
if (sEnabled) {
Looper.myQueue().removeIdleHandler(sInstance);
sEnabled = false;
}
}
}
}