• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2016 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base;
6 
7 import android.os.Process;
8 import android.os.StrictMode;
9 import android.os.SystemClock;
10 
11 import androidx.annotation.VisibleForTesting;
12 
13 import org.jni_zero.CalledByNative;
14 import org.jni_zero.JNINamespace;
15 import org.jni_zero.NativeMethods;
16 
17 import java.io.File;
18 import java.util.ArrayList;
19 import java.util.List;
20 
21 import javax.annotation.concurrent.GuardedBy;
22 
23 /**
24  * Support for early tracing, before the native library is loaded.
25  *
26  * Note that arguments are not currently supported for early events, but could
27  * be added in the future.
28  *
29  * Events recorded here are buffered in Java until the native library is available, at which point
30  * they are flushed to the native side and regular java tracing (TraceEvent) takes over.
31  *
32  * Locking: This class is threadsafe. It is enabled when general tracing is, and then disabled when
33  *          tracing is enabled from the native side. At this point, buffered events are flushed to
34  *          the native side and then early tracing is permanently disabled after dumping the events.
35  *
36  * Like the TraceEvent, the event name of the trace events must be a string literal or a |static
37  * final String| class member. Otherwise NoDynamicStringsInTraceEventCheck error will be thrown.
38  */
39 @JNINamespace("base::android")
40 public class EarlyTraceEvent {
41     /** Single trace event. */
42     @VisibleForTesting
43     static final class Event {
44         final boolean mIsStart;
45         final boolean mIsToplevel;
46         final String mName;
47         final int mThreadId;
48         final long mTimeNanos;
49         final long mThreadTimeMillis;
50 
Event(String name, boolean isStart, boolean isToplevel)51         Event(String name, boolean isStart, boolean isToplevel) {
52             mIsStart = isStart;
53             mIsToplevel = isToplevel;
54             mName = name;
55             mThreadId = Process.myTid();
56             mTimeNanos = System.nanoTime(); // Same timebase as TimeTicks::Now().
57             mThreadTimeMillis = SystemClock.currentThreadTimeMillis();
58         }
59     }
60 
61     @VisibleForTesting
62     static final class AsyncEvent {
63         final boolean mIsStart;
64         final String mName;
65         final long mId;
66         final long mTimeNanos;
67 
AsyncEvent(String name, long id, boolean isStart)68         AsyncEvent(String name, long id, boolean isStart) {
69             mName = name;
70             mId = id;
71             mIsStart = isStart;
72             mTimeNanos = System.nanoTime(); // Same timebase as TimeTicks::Now().
73         }
74     }
75 
76     // State transitions are:
77     // - enable(): DISABLED -> ENABLED
78     // - disable(): ENABLED -> FINISHED
79     @VisibleForTesting static final int STATE_DISABLED = 0;
80     @VisibleForTesting static final int STATE_ENABLED = 1;
81     @VisibleForTesting static final int STATE_FINISHED = 2;
82     @VisibleForTesting static volatile int sState = STATE_DISABLED;
83 
84     // In child processes the CommandLine is not available immediately, so early tracing is enabled
85     // unconditionally in Chrome. This flag allows not to enable early tracing twice in this case.
86     private static volatile boolean sEnabledInChildProcessBeforeCommandLine;
87 
88     private static final String BACKGROUND_STARTUP_TRACING_ENABLED_KEY = "bg_startup_tracing";
89     private static boolean sCachedBackgroundStartupTracingFlag;
90 
91     // Early tracing can be enabled on browser start if the browser finds this file present. Must be
92     // kept in sync with the native kAndroidTraceConfigFile.
93     private static final String TRACE_CONFIG_FILENAME = "/data/local/chrome-trace-config.json";
94 
95     // Early tracing can be enabled on browser start if the browser finds this command line switch.
96     // Must be kept in sync with switches::kTraceStartup.
97     private static final String TRACE_STARTUP_SWITCH = "trace-startup";
98 
99     // Added to child process switches if tracing is enabled when the process is getting created.
100     // The flag is checked early in child process lifetime to have a solid guarantee that the early
101     // java tracing is not enabled forever. Native flags cannot be used for this purpose because the
102     // native library is not loaded at the moment. Cannot set --trace-startup for the child to avoid
103     // overriding the list of categories it may load from the config later. Also --trace-startup
104     // depends on other flags that early tracing should not know about. Public for use in
105     // ChildProcessLauncherHelperImpl.
106     public static final String TRACE_EARLY_JAVA_IN_CHILD_SWITCH = "trace-early-java-in-child";
107 
108     // Protects the fields below.
109     @VisibleForTesting static final Object sLock = new Object();
110 
111     // Not final because in many configurations these objects are not used.
112     @GuardedBy("sLock")
113     @VisibleForTesting
114     static List<Event> sEvents;
115 
116     @GuardedBy("sLock")
117     @VisibleForTesting
118     static List<AsyncEvent> sAsyncEvents;
119 
120     /** @see TraceEvent#maybeEnableEarlyTracing(boolean) */
maybeEnableInBrowserProcess()121     static void maybeEnableInBrowserProcess() {
122         ThreadUtils.assertOnUiThread();
123         assert !sEnabledInChildProcessBeforeCommandLine
124                 : "Should not have been initialized in a child process";
125         if (sState != STATE_DISABLED) return;
126         boolean shouldEnable = false;
127         // Checking for the trace config filename touches the disk.
128         StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
129         try {
130             if (CommandLine.getInstance().hasSwitch(TRACE_STARTUP_SWITCH)) {
131                 shouldEnable = true;
132             } else {
133                 try {
134                     shouldEnable = new File(TRACE_CONFIG_FILENAME).exists();
135                 } catch (SecurityException e) {
136                     // Access denied, not enabled.
137                 }
138             }
139             if (ContextUtils.getAppSharedPreferences()
140                     .getBoolean(BACKGROUND_STARTUP_TRACING_ENABLED_KEY, false)) {
141                 if (shouldEnable) {
142                     // If user has enabled tracing, then force disable background tracing for this
143                     // session.
144                     setBackgroundStartupTracingFlag(false);
145                     sCachedBackgroundStartupTracingFlag = false;
146                 } else {
147                     sCachedBackgroundStartupTracingFlag = true;
148                     shouldEnable = true;
149                 }
150             }
151         } finally {
152             StrictMode.setThreadPolicy(oldPolicy);
153         }
154         if (shouldEnable) enable();
155     }
156 
157     /** Enables early tracing in child processes before CommandLine arrives there. */
earlyEnableInChildWithoutCommandLine()158     public static void earlyEnableInChildWithoutCommandLine() {
159         sEnabledInChildProcessBeforeCommandLine = true;
160         assert sState == STATE_DISABLED;
161         enable();
162     }
163 
164     /**
165      * Based on a command line switch from the process launcher, enables or resets early tracing.
166      * Should be called only in child processes and as soon as possible after the CommandLine is
167      * initialized.
168      */
onCommandLineAvailableInChildProcess()169     public static void onCommandLineAvailableInChildProcess() {
170         // Ignore early Java tracing in WebView and other startup configurations that did not start
171         // collecting events before the command line was available.
172         if (!sEnabledInChildProcessBeforeCommandLine) return;
173         synchronized (sLock) {
174             // Remove early trace events if the child process launcher did not ask for early
175             // tracing.
176             if (!CommandLine.getInstance().hasSwitch(TRACE_EARLY_JAVA_IN_CHILD_SWITCH)) {
177                 reset();
178                 return;
179             }
180             // Otherwise continue with tracing enabled.
181             if (sState == STATE_DISABLED) enable();
182         }
183     }
184 
enable()185     static void enable() {
186         synchronized (sLock) {
187             if (sState != STATE_DISABLED) return;
188             sEvents = new ArrayList<Event>();
189             sAsyncEvents = new ArrayList<AsyncEvent>();
190             sState = STATE_ENABLED;
191         }
192     }
193 
194     /**
195      * Disables Early tracing and flushes buffered events to the native side.
196      *
197      * Once this is called, no new event will be registered.
198      */
disable()199     static void disable() {
200         synchronized (sLock) {
201             if (!enabled()) return;
202 
203             if (!sEvents.isEmpty()) {
204                 dumpEvents(sEvents);
205                 sEvents.clear();
206             }
207             if (!sAsyncEvents.isEmpty()) {
208                 dumpAsyncEvents(sAsyncEvents);
209                 sAsyncEvents.clear();
210             }
211 
212             sState = STATE_FINISHED;
213             sEvents = null;
214             sAsyncEvents = null;
215         }
216     }
217 
218     /** Stops early tracing without flushing the buffered events. */
219     @VisibleForTesting
reset()220     static void reset() {
221         synchronized (sLock) {
222             sState = STATE_DISABLED;
223             sEvents = null;
224             sAsyncEvents = null;
225         }
226     }
227 
enabled()228     static boolean enabled() {
229         return sState == STATE_ENABLED;
230     }
231 
232     /** Sets the background startup tracing enabled in app preferences for next startup. */
233     @CalledByNative
setBackgroundStartupTracingFlag(boolean enabled)234     static void setBackgroundStartupTracingFlag(boolean enabled) {
235         // Setting preferences might cause a disk write
236         try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
237             ContextUtils.getAppSharedPreferences()
238                     .edit()
239                     .putBoolean(BACKGROUND_STARTUP_TRACING_ENABLED_KEY, enabled)
240                     .apply();
241         }
242     }
243 
244     /**
245      * Returns true if the background startup tracing flag is set.
246      *
247      * This does not return the correct value if called before maybeEnable() was called. But that is
248      * called really early in startup.
249      */
250     @CalledByNative
getBackgroundStartupTracingFlag()251     public static boolean getBackgroundStartupTracingFlag() {
252         return sCachedBackgroundStartupTracingFlag;
253     }
254 
255     /** @see TraceEvent#begin */
begin(String name, boolean isToplevel)256     public static void begin(String name, boolean isToplevel) {
257         // begin() and end() are going to be called once per TraceEvent, this avoids entering a
258         // synchronized block at each and every call.
259         if (!enabled()) return;
260         Event event = new Event(name, /* isStart= */ true, isToplevel);
261         synchronized (sLock) {
262             if (!enabled()) return;
263             sEvents.add(event);
264         }
265     }
266 
267     /** @see TraceEvent#end */
end(String name, boolean isToplevel)268     public static void end(String name, boolean isToplevel) {
269         if (!enabled()) return;
270         Event event = new Event(name, /* isStart= */ false, isToplevel);
271         synchronized (sLock) {
272             if (!enabled()) return;
273             sEvents.add(event);
274         }
275     }
276 
277     /** @see TraceEvent#startAsync */
startAsync(String name, long id)278     public static void startAsync(String name, long id) {
279         if (!enabled()) return;
280         AsyncEvent event = new AsyncEvent(name, id, /* isStart= */ true);
281         synchronized (sLock) {
282             if (!enabled()) return;
283             sAsyncEvents.add(event);
284         }
285     }
286 
287     /** @see TraceEvent#finishAsync */
finishAsync(String name, long id)288     public static void finishAsync(String name, long id) {
289         if (!enabled()) return;
290         AsyncEvent event = new AsyncEvent(name, id, /* isStart= */ false);
291         synchronized (sLock) {
292             if (!enabled()) return;
293             sAsyncEvents.add(event);
294         }
295     }
296 
getMatchingCompletedEventsForTesting(String eventName)297     static List<Event> getMatchingCompletedEventsForTesting(String eventName) {
298         synchronized (sLock) {
299             List<Event> matchingEvents = new ArrayList<Event>();
300             for (Event evt : EarlyTraceEvent.sEvents) {
301                 if (evt.mName.equals(eventName)) {
302                     matchingEvents.add(evt);
303                 }
304             }
305             return matchingEvents;
306         }
307     }
308 
dumpEvents(List<Event> events)309     private static void dumpEvents(List<Event> events) {
310         for (Event e : events) {
311             if (e.mIsStart) {
312                 if (e.mIsToplevel) {
313                     EarlyTraceEventJni.get()
314                             .recordEarlyToplevelBeginEvent(
315                                     e.mName, e.mTimeNanos, e.mThreadId, e.mThreadTimeMillis);
316                 } else {
317                     EarlyTraceEventJni.get()
318                             .recordEarlyBeginEvent(
319                                     e.mName, e.mTimeNanos, e.mThreadId, e.mThreadTimeMillis);
320                 }
321             } else {
322                 if (e.mIsToplevel) {
323                     EarlyTraceEventJni.get()
324                             .recordEarlyToplevelEndEvent(
325                                     e.mName, e.mTimeNanos, e.mThreadId, e.mThreadTimeMillis);
326                 } else {
327                     EarlyTraceEventJni.get()
328                             .recordEarlyEndEvent(
329                                     e.mName, e.mTimeNanos, e.mThreadId, e.mThreadTimeMillis);
330                 }
331             }
332         }
333     }
334 
dumpAsyncEvents(List<AsyncEvent> events)335     private static void dumpAsyncEvents(List<AsyncEvent> events) {
336         for (AsyncEvent e : events) {
337             if (e.mIsStart) {
338                 EarlyTraceEventJni.get().recordEarlyAsyncBeginEvent(e.mName, e.mId, e.mTimeNanos);
339             } else {
340                 EarlyTraceEventJni.get().recordEarlyAsyncEndEvent(e.mId, e.mTimeNanos);
341             }
342         }
343     }
344 
345     @NativeMethods
346     interface Natives {
recordEarlyBeginEvent(String name, long timeNanos, int threadId, long threadMillis)347         void recordEarlyBeginEvent(String name, long timeNanos, int threadId, long threadMillis);
348 
recordEarlyEndEvent(String name, long timeNanos, int threadId, long threadMillis)349         void recordEarlyEndEvent(String name, long timeNanos, int threadId, long threadMillis);
350 
recordEarlyToplevelBeginEvent( String name, long timeNanos, int threadId, long threadMillis)351         void recordEarlyToplevelBeginEvent(
352                 String name, long timeNanos, int threadId, long threadMillis);
353 
recordEarlyToplevelEndEvent( String name, long timeNanos, int threadId, long threadMillis)354         void recordEarlyToplevelEndEvent(
355                 String name, long timeNanos, int threadId, long threadMillis);
356 
recordEarlyAsyncBeginEvent(String name, long id, long timeNanos)357         void recordEarlyAsyncBeginEvent(String name, long id, long timeNanos);
358 
recordEarlyAsyncEndEvent(long id, long timeNanos)359         void recordEarlyAsyncEndEvent(long id, long timeNanos);
360     }
361 }
362