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