blob: 264f307770c1d0a4c4b058b8f042c07a691f8e2e [file] [log] [blame]
// 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.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.build.annotations.MainDex;
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 = 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<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) {
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<AsyncEvent> events) {
for (AsyncEvent e : events) {
if (e.mIsStart) {
EarlyTraceEventJni.get().recordEarlyAsyncBeginEvent(e.mName, e.mId, e.mTimeNanos);
} else {
EarlyTraceEventJni.get().recordEarlyAsyncEndEvent(e.mName, 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(String name, long id, long timeNanos);
}
}