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