// Copyright 2016 The Chromium Authors. All rights reserved.
// 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.os.Process;
import android.os.StrictMode;
import android.os.SystemClock;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.annotations.NativeMethods;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.concurrent.GuardedBy;

/**
 * Support for early tracing, before the native library is loaded.
 *
 * Note that arguments are not currently supported for early events, but could
 * be added in the future.
 *
 * Events recorded here are buffered in Java until the native library is available, at which point
 * they are flushed to the native side and regular java tracing (TraceEvent) takes over.
 *
 * Locking: This class is threadsafe. It is enabled when general tracing is, and then disabled when
 *          tracing is enabled from the native side. At this point, buffered events are flushed to
 *          the native side and then early tracing is permanently disabled after dumping the events.
 *
 * Like the TraceEvent, 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.
 */
@JNINamespace("base::android")
@MainDex
public class EarlyTraceEvent {
    /** Single trace event. */
    @VisibleForTesting
    static final class Event {
        final boolean mIsStart;
        final boolean mIsToplevel;
        final String mName;
        final int mThreadId;
        final long mTimeNanos;
        final long mThreadTimeMillis;

        Event(String name, boolean isStart, boolean isToplevel) {
            mIsStart = isStart;
            mIsToplevel = isToplevel;
            mName = name;
            mThreadId = Process.myTid();
            mTimeNanos = SystemClock.elapsedRealtimeNanos();
            mThreadTimeMillis = SystemClock.currentThreadTimeMillis();
        }
    }

    @VisibleForTesting
    static final class AsyncEvent {
        final boolean mIsStart;
        final String mName;
        final long mId;
        final long mTimestampNanos;

        AsyncEvent(String name, long id, boolean isStart) {
            mName = name;
            mId = id;
            mIsStart = isStart;
            mTimestampNanos = SystemClock.elapsedRealtimeNanos();
        }
    }

    // State transitions are:
    // - enable(): DISABLED -> ENABLED
    // - disable(): ENABLED -> FINISHED
    @VisibleForTesting static final int STATE_DISABLED = 0;
    @VisibleForTesting static final int STATE_ENABLED = 1;
    @VisibleForTesting
    static final int STATE_FINISHED = 2;
    @VisibleForTesting
    static volatile int sState = STATE_DISABLED;

    // In child processes the CommandLine is not available immediately, so early tracing is enabled
    // unconditionally in Chrome. This flag allows not to enable early tracing twice in this case.
    private static volatile boolean sEnabledInChildProcessBeforeCommandLine;

    private static final String BACKGROUND_STARTUP_TRACING_ENABLED_KEY = "bg_startup_tracing";
    private static boolean sCachedBackgroundStartupTracingFlag;

    // Early tracing can be enabled on browser start if the browser finds this file present. Must be
    // kept in sync with the native kAndroidTraceConfigFile.
    private static final String TRACE_CONFIG_FILENAME = "/data/local/chrome-trace-config.json";

    // Early tracing can be enabled on browser start if the browser finds this command line switch.
    // Must be kept in sync with switches::kTraceStartup.
    private static final String TRACE_STARTUP_SWITCH = "trace-startup";

    // Added to child process switches if tracing is enabled when the process is getting created.
    // The flag is checked early in child process lifetime to have a solid guarantee that the early
    // java tracing is not enabled forever. Native flags cannot be used for this purpose because the
    // native library is not loaded at the moment. Cannot set --trace-startup for the child to avoid
    // overriding the list of categories it may load from the config later. Also --trace-startup
    // depends on other flags that early tracing should not know about. Public for use in
    // ChildProcessLauncherHelperImpl.
    public static final String TRACE_EARLY_JAVA_IN_CHILD_SWITCH = "trace-early-java-in-child";

    // Protects the fields below.
    private static final Object sLock = new Object();

    // Not final because in many configurations these objects are not used.
    @GuardedBy("sLock")
    @VisibleForTesting
    static List<Event> sEvents;
    @GuardedBy("sLock")
    @VisibleForTesting
    static List<AsyncEvent> sAsyncEvents;

    /** @see TraceEvent#maybeEnableEarlyTracing(long, boolean) */
    static void maybeEnableInBrowserProcess() {
        ThreadUtils.assertOnUiThread();
        assert !sEnabledInChildProcessBeforeCommandLine
            : "Should not have been initialized in a child process";
        if (sState != STATE_DISABLED) return;
        boolean shouldEnable = false;
        // Checking for the trace config filename touches the disk.
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
        try {
            if (CommandLine.getInstance().hasSwitch(TRACE_STARTUP_SWITCH)) {
                shouldEnable = true;
            } else {
                try {
                    shouldEnable = new File(TRACE_CONFIG_FILENAME).exists();
                } catch (SecurityException e) {
                    // Access denied, not enabled.
                }
            }
            if (ContextUtils.getAppSharedPreferences().getBoolean(
                        BACKGROUND_STARTUP_TRACING_ENABLED_KEY, false)) {
                if (shouldEnable) {
                    // If user has enabled tracing, then force disable background tracing for this
                    // session.
                    setBackgroundStartupTracingFlag(false);
                    sCachedBackgroundStartupTracingFlag = false;
                } else {
                    sCachedBackgroundStartupTracingFlag = true;
                    shouldEnable = true;
                }
            }
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }
        if (shouldEnable) enable();
    }

    /**
     * Enables early tracing in child processes before CommandLine arrives there.
     */
    public static void earlyEnableInChildWithoutCommandLine() {
        sEnabledInChildProcessBeforeCommandLine = true;
        assert sState == STATE_DISABLED;
        enable();
    }

    /**
     * Based on a command line switch from the process launcher, enables or resets early tracing.
     * Should be called only in child processes and as soon as possible after the CommandLine is
     * initialized.
     */
    public static void onCommandLineAvailableInChildProcess() {
        // Ignore early Java tracing in WebView and other startup configurations that did not start
        // collecting events before the command line was available.
        if (!sEnabledInChildProcessBeforeCommandLine) return;
        synchronized (sLock) {
            // Remove early trace events if the child process launcher did not ask for early
            // tracing.
            if (!CommandLine.getInstance().hasSwitch(TRACE_EARLY_JAVA_IN_CHILD_SWITCH)) {
                reset();
                return;
            }
            // Otherwise continue with tracing enabled.
            if (sState == STATE_DISABLED) enable();
        }
    }

    static void enable() {
        synchronized (sLock) {
            if (sState != STATE_DISABLED) return;
            sEvents = new ArrayList<Event>();
            sAsyncEvents = new ArrayList<AsyncEvent>();
            sState = STATE_ENABLED;
        }
    }

    /**
     * Disables Early tracing and flushes buffered events to the native side.
     *
     * Once this is called, no new event will be registered.
     */
    static void disable() {
        synchronized (sLock) {
            if (!enabled()) return;

            if (!sEvents.isEmpty()) {
                dumpEvents(sEvents);
                sEvents.clear();
            }
            if (!sAsyncEvents.isEmpty()) {
                dumpAsyncEvents(sAsyncEvents);
                sAsyncEvents.clear();
            }

            sState = STATE_FINISHED;
            sEvents = null;
            sAsyncEvents = null;
        }
    }

    /**
     * Stops early tracing without flushing the buffered events.
     */
    @VisibleForTesting
    static void reset() {
        synchronized (sLock) {
            sState = STATE_DISABLED;
            sEvents = null;
            sAsyncEvents = null;
        }
    }

    static boolean enabled() {
        return sState == STATE_ENABLED;
    }

    /**
     * Sets the background startup tracing enabled in app preferences for next startup.
     */
    @CalledByNative
    static void setBackgroundStartupTracingFlag(boolean enabled) {
        ContextUtils.getAppSharedPreferences()
                .edit()
                .putBoolean(BACKGROUND_STARTUP_TRACING_ENABLED_KEY, enabled)
                .apply();
    }

    /**
     * Returns true if the background startup tracing flag is set.
     *
     * This does not return the correct value if called before maybeEnable() was called. But that is
     * called really early in startup.
     */
    @CalledByNative
    public static boolean getBackgroundStartupTracingFlag() {
        return sCachedBackgroundStartupTracingFlag;
    }

    /** @see TraceEvent#begin */
    public static void begin(String name, boolean isToplevel) {
        // begin() and end() are going to be called once per TraceEvent, this avoids entering a
        // synchronized block at each and every call.
        if (!enabled()) return;
        Event event = new Event(name, true /*isStart*/, isToplevel);
        synchronized (sLock) {
            if (!enabled()) return;
            sEvents.add(event);
        }
    }

    /** @see TraceEvent#end */
    public static void end(String name, boolean isToplevel) {
        if (!enabled()) return;
        Event event = new Event(name, false /*isStart*/, isToplevel);
        synchronized (sLock) {
            if (!enabled()) return;
            sEvents.add(event);
        }
    }

    /** @see TraceEvent#startAsync */
    public static void startAsync(String name, long id) {
        if (!enabled()) return;
        AsyncEvent event = new AsyncEvent(name, id, true /*isStart*/);
        synchronized (sLock) {
            if (!enabled()) return;
            sAsyncEvents.add(event);
        }
    }

    /** @see TraceEvent#finishAsync */
    public static void finishAsync(String name, long id) {
        if (!enabled()) return;
        AsyncEvent event = new AsyncEvent(name, id, false /*isStart*/);
        synchronized (sLock) {
            if (!enabled()) return;
            sAsyncEvents.add(event);
        }
    }

    @VisibleForTesting
    static List<Event> getMatchingCompletedEventsForTesting(String eventName) {
        synchronized (sLock) {
            List<Event> matchingEvents = new ArrayList<Event>();
            for (Event evt : EarlyTraceEvent.sEvents) {
                if (evt.mName.equals(eventName)) {
                    matchingEvents.add(evt);
                }
            }
            return matchingEvents;
        }
    }

    private static void dumpEvents(List<Event> events) {
        long offsetNanos = getOffsetNanos();
        for (Event e : events) {
            if (e.mIsStart) {
                if (e.mIsToplevel) {
                    EarlyTraceEventJni.get().recordEarlyToplevelBeginEvent(
                            e.mName, e.mTimeNanos + offsetNanos, e.mThreadId, e.mThreadTimeMillis);
                } else {
                    EarlyTraceEventJni.get().recordEarlyBeginEvent(
                            e.mName, e.mTimeNanos + offsetNanos, e.mThreadId, e.mThreadTimeMillis);
                }
            } else {
                if (e.mIsToplevel) {
                    EarlyTraceEventJni.get().recordEarlyToplevelEndEvent(
                            e.mName, e.mTimeNanos + offsetNanos, e.mThreadId, e.mThreadTimeMillis);
                } else {
                    EarlyTraceEventJni.get().recordEarlyEndEvent(
                            e.mName, e.mTimeNanos + offsetNanos, e.mThreadId, e.mThreadTimeMillis);
                }
            }
        }
    }
    private static void dumpAsyncEvents(List<AsyncEvent> events) {
        long offsetNanos = getOffsetNanos();
        for (AsyncEvent e : events) {
            if (e.mIsStart) {
                EarlyTraceEventJni.get().recordEarlyAsyncBeginEvent(
                        e.mName, e.mId, e.mTimestampNanos + offsetNanos);
            } else {
                EarlyTraceEventJni.get().recordEarlyAsyncEndEvent(
                        e.mName, e.mId, e.mTimestampNanos + offsetNanos);
            }
        }
    }

    private static long getOffsetNanos() {
        long nativeNowNanos = TimeUtilsJni.get().getTimeTicksNowUs() * 1000;
        long javaNowNanos = SystemClock.elapsedRealtimeNanos();
        return nativeNowNanos - javaNowNanos;
    }

    @NativeMethods
    interface Natives {
        void recordEarlyBeginEvent(String name, long timeNanos, int threadId, long threadMillis);
        void recordEarlyEndEvent(String name, long timeNanos, int threadId, long threadMillis);
        void recordEarlyToplevelBeginEvent(
                String name, long timeNanos, int threadId, long threadMillis);
        void recordEarlyToplevelEndEvent(
                String name, long timeNanos, int threadId, long threadMillis);
        void recordEarlyAsyncBeginEvent(String name, long id, long timestamp);
        void recordEarlyAsyncEndEvent(String name, long id, long timestamp);
    }
}
