blob: e0fd1c740c5c8e6c852ed09bbf0cc0044b693835 [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.AnyThread;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.build.annotations.MainDex;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 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")
@MainDex
public class TraceEvent implements AutoCloseable {
private static volatile boolean sEnabled; // True when tracing into Chrome's tracing service.
private static AtomicBoolean sNativeTracingReady = new AtomicBoolean();
private static AtomicBoolean sUiThreadReady = new AtomicBoolean();
private static boolean sEventNameFilteringEnabled;
// Trace tags replicated from android.os.Trace.
public static final long ATRACE_TAG_WEBVIEW = 1L << 4;
public static final long ATRACE_TAG_APP = 1L << 12;
/**
* Watches for active ATrace sessions and accordingly enables or disables
* tracing in Chrome/WebView.
*/
@SuppressWarnings("PrivateApi")
private static class ATrace implements MessageQueue.IdleHandler {
private static final String TAG = "ATrace";
private Class<?> mTraceClass;
private Method mIsTraceTagEnabledMethod;
private Method mTraceBeginMethod;
private Method mTraceEndMethod;
private Method mAsyncTraceBeginMethod;
private Method mAsyncTraceEndMethod;
private Class<?> mSystemPropertiesClass;
private Method mGetSystemPropertyMethod;
private final AtomicBoolean mNativeTracingReady = new AtomicBoolean();
private final AtomicBoolean mUiThreadReady = new AtomicBoolean();
private final AtomicBoolean mTraceTagActive = new AtomicBoolean();
private final long mTraceTag;
private boolean mShouldWriteToSystemTrace;
private boolean mIdleHandlerRegistered;
private static class CategoryConfig {
public String filter = "";
public boolean shouldWriteToATrace = true;
}
public ATrace(long traceTag) {
// Look up hidden ATrace APIs.
try {
mTraceClass = Class.forName("android.os.Trace");
mIsTraceTagEnabledMethod = mTraceClass.getMethod("isTagEnabled", long.class);
mTraceBeginMethod = mTraceClass.getMethod("traceBegin", long.class, String.class);
mTraceEndMethod = mTraceClass.getMethod("traceEnd", long.class);
mAsyncTraceBeginMethod = mTraceClass.getMethod(
"asyncTraceBegin", long.class, String.class, int.class);
mAsyncTraceEndMethod =
mTraceClass.getMethod("asyncTraceEnd", long.class, String.class, int.class);
mSystemPropertiesClass = Class.forName("android.os.SystemProperties");
mGetSystemPropertyMethod = mSystemPropertiesClass.getMethod("get", String.class);
} catch (Exception e) {
// If we hit reflection errors, just disable atrace support.
org.chromium.base.Log.w(TAG, "Reflection error", e);
mIsTraceTagEnabledMethod = null;
}
// If there's an active atrace session, also start collecting early trace events.
mTraceTag = traceTag;
pollConfig();
}
/**
* Reads a system property and returns its string value.
*
* @param name the name of the system property
* @return the result string or null if an exception occurred
*/
@Nullable
private String getSystemProperty(String name) {
try {
return (String) mGetSystemPropertyMethod.invoke(mSystemPropertiesClass, name);
} catch (Exception e) {
return null;
}
}
/**
* Reads a system property and returns its value as an integer.
*
* @param name the name of the system property
* @return the result integer or null if an exception occurred
*/
private Integer getIntegerSystemProperty(String name) {
String property = getSystemProperty(name);
if (property == null) return null;
try {
return Integer.decode(property);
} catch (NumberFormatException e) {
return null;
}
}
private boolean isTraceTagEnabled(long traceTag) {
try {
return (boolean) mIsTraceTagEnabledMethod.invoke(mTraceClass, traceTag);
} catch (Exception e) {
return false;
}
}
/**
* @return true if Chrome/WebView is part of an active ATrace session.
*/
public boolean hasActiveSession() {
return mTraceTagActive.get();
}
/**
* Checks whether ATrace has started or stopped tracing since the last
* call to this function and parses the changed config if necessary.
*
* @return true if a session has started or stopped.
*/
@UiThread
private boolean pollConfig() {
// ATrace's tracing configuration consists of the following system
// properties:
// - debug.atrace.tags.enableflags: A hex mask of the enabled system
// tracing categories (e.g, "0x10").
// - debug.atrace.app_number: The number of per-app config entries
// (e.g., "1").
// - debug.atrace.app_0: Config for app 0 (up to
// app_number-1).
//
// Normally the per-app config entry is just the package name, but we
// also support setting the trace config with additional parameters,
// e.g., assuming "com.android.chrome" as the package name:
//
// - Enable default categories: "com.android.chrome"
// - Enable specific categories: "com.android.chrome/cat1:cat2"
// - Disable specific categories: "com.android.chrome/*:-cat1"
//
// Since each app-specific config is limited to 91 characters, multiple
// entries can be used to work around the limit.
//
// If either the "webview" trace tag (0x10) is enabled (for WebView)
// or our package name is found in the list of configs, trace events
// will be written into ATrace. However, if "-atrace" appears as a
// category in any of the app-specific configs, events will only be
// written into Chrome's own startup tracing buffer to avoid
// duplicate events.
boolean traceTagWasActive = mTraceTagActive.get();
boolean traceTagIsActive = isTraceTagEnabled(mTraceTag);
if (traceTagWasActive == traceTagIsActive) return false;
mTraceTagActive.set(traceTagIsActive);
if (!traceTagIsActive) {
// A previously active atrace session ended.
EarlyTraceEvent.disable();
disableNativeATrace();
mShouldWriteToSystemTrace = false;
ThreadUtils.getUiThreadLooper().setMessageLogging(null);
return true;
}
CategoryConfig config = getCategoryConfigFromATrace();
// There is an active atrace session. We can output events into one
// of the following sinks:
//
// - To ATrace:
// ...via TraceLog if native has finished loading.
// ...via android.os.Trace otherwise.
// - To Chrome's own tracing service (for startup tracing):
// ...via TraceLog if native has finished loading.
// ...via EarlyTraceEvent otherwise.
mShouldWriteToSystemTrace = false;
if (mNativeTracingReady.get()) {
// Native is loaded; start writing to atrace via TraceLog, or in
// the case of a Chrome-only trace, setup a startup tracing
// session.
if (config.shouldWriteToATrace) {
enableNativeATrace(config.filter);
} else {
setupATraceStartupTrace(config.filter);
}
} else {
// Native isn't there yet; fall back to android.os.Trace or
// EarlyTraceEvent. We can't use the category filter in this
// case because Java events don't have categories.
if (config.shouldWriteToATrace) {
mShouldWriteToSystemTrace = true;
} else {
EarlyTraceEvent.enable();
}
}
// For Chrome-only traces, also capture Looper messages. In other
// cases, they are logged by the system.
if (!config.shouldWriteToATrace) {
ThreadUtils.getUiThreadLooper().setMessageLogging(LooperMonitorHolder.sInstance);
}
return true;
}
private CategoryConfig getCategoryConfigFromATrace() {
CategoryConfig config = new CategoryConfig();
boolean shouldWriteToATrace = true;
Integer appCount = getIntegerSystemProperty("debug.atrace.app_number");
// In the case of WebView, the application context may not have been
// attached yet. Ignore per-app category settings in that case; they
// will be applied when the native library finishes loading.
if (appCount != null && appCount > 0 && ContextUtils.getApplicationContext() != null) {
// Look for tracing category settings meant for this activity.
// For Chrome this is the package name of the browser, while for
// WebView this is the package name of the hosting application
// (e.g., GMail).
String packageName = ContextUtils.getApplicationContext().getPackageName();
for (int i = 0; i < appCount; i++) {
String appConfig = getSystemProperty("debug.atrace.app_" + i);
if (appConfig == null || !appConfig.startsWith(packageName)) continue;
String extra = appConfig.substring(packageName.length());
if (!extra.startsWith("/")) continue;
for (String category : extra.substring(1).split(":")) {
if (category.equals("-atrace")) {
config.shouldWriteToATrace = false;
continue;
}
if (config.filter.length() > 0) config.filter += ",";
config.filter += category;
}
}
}
return config;
}
@AnyThread
public void onNativeTracingReady() {
mNativeTracingReady.set(true);
// If there already was an active atrace session, we should transfer
// it over to native. If the UI thread was already registered, post
// a task to move the session over as soon as possible. Otherwise
// we'll wait until the UI thread activates.
mTraceTagActive.set(false);
if (mUiThreadReady.get()) {
ThreadUtils.postOnUiThread(() -> { pollConfig(); });
}
}
@AnyThread
public void onUiThreadReady() {
mUiThreadReady.set(true);
if (!ThreadUtils.runningOnUiThread()) {
ThreadUtils.postOnUiThread(() -> { startPolling(); });
return;
}
startPolling();
}
private void startPolling() {
ThreadUtils.assertOnUiThread();
// Since Android R there's no way for an app to be notified of
// atrace activations. To work around this, we poll for the latest
// state whenever the main run loop becomes idle. Since the check
// amounts to one JNI call, the overhead of doing this is
// negligible. See queueIdle().
if (!mIdleHandlerRegistered) {
Looper.myQueue().addIdleHandler(this);
mIdleHandlerRegistered = true;
}
pollConfig();
}
@Override
public final boolean queueIdle() {
pollConfig();
return true;
}
/**
* Instructs Chrome's tracing service to start tracing.
*
* @param categoryFilter Set of trace categories to enable.
*/
private void enableNativeATrace(String categoryFilter) {
assert mNativeTracingReady.get();
TraceEventJni.get().startATrace(categoryFilter);
}
/**
* Stop a previously started tracing session and flush remaining events
* to ATrace (if enabled).
*/
private void disableNativeATrace() {
assert mNativeTracingReady.get();
TraceEventJni.get().stopATrace();
}
/**
* Begins a startup tracing session which will be later taken over by a
* system tracing session.
*
* @param categoryFilter Set of trace categories to enable.
*/
private void setupATraceStartupTrace(String categoryFilter) {
assert mNativeTracingReady.get();
TraceEventJni.get().setupATraceStartupTrace(categoryFilter);
}
public void traceBegin(String name) {
if (!mShouldWriteToSystemTrace) return;
try {
mTraceBeginMethod.invoke(mTraceClass, mTraceTag, name);
} catch (Exception e) {
// No-op.
}
}
public void traceEnd() {
if (!mShouldWriteToSystemTrace) return;
try {
mTraceEndMethod.invoke(mTraceClass, mTraceTag);
} catch (Exception e) {
// No-op.
}
}
public void asyncTraceBegin(String name, int cookie) {
if (!mShouldWriteToSystemTrace) return;
try {
mAsyncTraceBeginMethod.invoke(mTraceClass, mTraceTag, name, cookie);
} catch (Exception e) {
// No-op.
}
}
public void asyncTraceEnd(String name, int cookie) {
if (!mShouldWriteToSystemTrace) return;
try {
mAsyncTraceEndMethod.invoke(mTraceClass, mTraceTag, name, cookie);
} catch (Exception e) {
// No-op.
}
}
}
private static ATrace sATrace;
@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, true /*isToplevel*/);
}
}
}
void endHandling(final String line) {
boolean earlyTracingActive = EarlyTraceEvent.enabled();
if ((sEnabled || earlyTracingActive) && mCurrentTarget != null) {
if (sEnabled) {
TraceEventJni.get().endToplevel(mCurrentTarget);
} else {
EarlyTraceEvent.end(mCurrentTarget, true /*isToplevel*/);
}
}
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 = "TraceEvent_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);
}
@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);
}
/**
* 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;
// Android M+ systrace logs this on its own. Only log it if not writing to Android
// systrace.
if (sATrace == null || !sATrace.hasActiveSession()) {
ThreadUtils.getUiThreadLooper().setMessageLogging(
enabled ? LooperMonitorHolder.sInstance : null);
}
}
if (sUiThreadReady.get()) {
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 traceTag If non-zero, start watching for ATrace sessions on the given tag.
* @param readCommandLine If true, also check command line flags to see
* whether tracing should be turned on.
*/
public static void maybeEnableEarlyTracing(long traceTag, 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 (traceTag != 0) {
sATrace = new ATrace(traceTag);
if (sNativeTracingReady.get()) {
sATrace.onNativeTracingReady();
}
if (sUiThreadReady.get()) {
sATrace.onUiThreadReady();
}
}
if (EarlyTraceEvent.enabled() && (sATrace == null || !sATrace.hasActiveSession())) {
ThreadUtils.getUiThreadLooper().setMessageLogging(LooperMonitorHolder.sInstance);
}
}
public static void onNativeTracingReady() {
// Register an enabled observer, such that java traces are always
// enabled with native.
sNativeTracingReady.set(true);
TraceEventJni.get().registerEnabledObserver();
if (sATrace != null) {
sATrace.onNativeTracingReady();
}
}
// Called by ThreadUtils.
static void onUiThreadReady() {
sUiThreadReady.set(true);
if (sATrace != null) {
sATrace.onUiThreadReady();
}
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);
}
}
/**
* 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);
} else if (sATrace != null) {
sATrace.asyncTraceBegin(name, (int) 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);
} else if (sATrace != null) {
sATrace.asyncTraceEnd(name, (int) 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, false /*isToplevel*/);
if (sEnabled) {
TraceEventJni.get().begin(name, arg);
} else if (sATrace != null) {
sATrace.traceBegin(name);
}
}
/**
* 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, false /*isToplevel*/);
if (sEnabled) {
TraceEventJni.get().end(name, arg, flow);
} else if (sATrace != null) {
sATrace.traceEnd();
}
}
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 startATrace(String categoryFilter);
void stopATrace();
void setupATraceStartupTrace(String categoryFilter);
void instant(String name, String arg);
void begin(String name, String 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);
}
/**
* 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() {
if (!ThreadUtils.runningOnUiThread()) {
ThreadUtils.postOnUiThread(() -> { updateEnabledState(); });
return;
}
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;
}
}
}
}