// 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;
            }
        }
    }
}
