// Copyright 2016 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.os.Process; import android.os.StrictMode; import android.os.SystemClock; import androidx.annotation.VisibleForTesting; import org.jni_zero.CalledByNative; import org.jni_zero.JNINamespace; import org.jni_zero.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") 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 = System.nanoTime(); // Same timebase as TimeTicks::Now(). mThreadTimeMillis = SystemClock.currentThreadTimeMillis(); } } @VisibleForTesting static final class AsyncEvent { final boolean mIsStart; final String mName; final long mId; final long mTimeNanos; AsyncEvent(String name, long id, boolean isStart) { mName = name; mId = id; mIsStart = isStart; mTimeNanos = System.nanoTime(); // Same timebase as TimeTicks::Now(). } } // 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. @VisibleForTesting static final Object sLock = new Object(); // Not final because in many configurations these objects are not used. @GuardedBy("sLock") @VisibleForTesting static List sEvents; @GuardedBy("sLock") @VisibleForTesting static List sAsyncEvents; /** @see TraceEvent#maybeEnableEarlyTracing(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(); sAsyncEvents = new ArrayList(); 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) { // Setting preferences might cause a disk write try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) { 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, /* isStart= */ true, 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, /* isStart= */ false, 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, /* isStart= */ true); 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, /* isStart= */ false); synchronized (sLock) { if (!enabled()) return; sAsyncEvents.add(event); } } static List getMatchingCompletedEventsForTesting(String eventName) { synchronized (sLock) { List matchingEvents = new ArrayList(); for (Event evt : EarlyTraceEvent.sEvents) { if (evt.mName.equals(eventName)) { matchingEvents.add(evt); } } return matchingEvents; } } private static void dumpEvents(List events) { for (Event e : events) { if (e.mIsStart) { if (e.mIsToplevel) { EarlyTraceEventJni.get() .recordEarlyToplevelBeginEvent( e.mName, e.mTimeNanos, e.mThreadId, e.mThreadTimeMillis); } else { EarlyTraceEventJni.get() .recordEarlyBeginEvent( e.mName, e.mTimeNanos, e.mThreadId, e.mThreadTimeMillis); } } else { if (e.mIsToplevel) { EarlyTraceEventJni.get() .recordEarlyToplevelEndEvent( e.mName, e.mTimeNanos, e.mThreadId, e.mThreadTimeMillis); } else { EarlyTraceEventJni.get() .recordEarlyEndEvent( e.mName, e.mTimeNanos, e.mThreadId, e.mThreadTimeMillis); } } } } private static void dumpAsyncEvents(List events) { for (AsyncEvent e : events) { if (e.mIsStart) { EarlyTraceEventJni.get().recordEarlyAsyncBeginEvent(e.mName, e.mId, e.mTimeNanos); } else { EarlyTraceEventJni.get().recordEarlyAsyncEndEvent(e.mId, e.mTimeNanos); } } } @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 timeNanos); void recordEarlyAsyncEndEvent(long id, long timeNanos); } }