// 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: *
{@code
 * try (TraceEvent e = TraceEvent.scoped("MyTraceEvent")) {
 *   // code.
 * }
 * }
* * 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: *

*

  1. * When using chrometrace, there will be a near-continuous line of * measurements showing both event dispatches as well as idles; *
  2. * Logging messages are output for events that run too long on the * event dispatcher, making it easy to identify problematic areas; *
  3. * 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; *
*

* 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 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 snapshotViewHierarchyState() { if (!ApplicationStatus.isInitialized()) { return new ArrayList(); } // In local testing we generally just have one activity. ArrayList 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 activities = (ArrayList) 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(125); } public String mActivityName; public ArrayList 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; } } } }