1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.gecko; 7 8 import android.os.Build; 9 import android.os.Looper; 10 import android.os.SystemClock; 11 import android.util.Log; 12 import androidx.annotation.GuardedBy; 13 import androidx.annotation.NonNull; 14 import androidx.annotation.Nullable; 15 import java.util.Queue; 16 import java.util.concurrent.Executors; 17 import java.util.concurrent.LinkedBlockingQueue; 18 import java.util.concurrent.ScheduledExecutorService; 19 import java.util.concurrent.ScheduledFuture; 20 import java.util.concurrent.TimeUnit; 21 import java.util.concurrent.atomic.AtomicReference; 22 import org.mozilla.gecko.annotation.WrapForJNI; 23 import org.mozilla.gecko.mozglue.JNIObject; 24 25 /** 26 * Takes samples and adds markers for Java threads for the Gecko profiler. 27 * 28 * <p>This class is thread safe because it uses synchronized on accesses to its mutable state. One 29 * exception is {@link #isProfilerActive()}: see the javadoc for details. 30 * 31 * <p>Bug 1618560: Currently we only profile the Android UI thread. Ideally we should be able to 32 * profile multiple threads. 33 */ 34 public class GeckoJavaSampler { 35 private static final String LOGTAG = "GeckoJavaSampler"; 36 37 @GuardedBy("GeckoJavaSampler.class") 38 private static SamplingRunnable sSamplingRunnable; 39 40 @GuardedBy("GeckoJavaSampler.class") 41 private static ScheduledExecutorService sSamplingScheduler; 42 43 // See isProfilerActive for details on the AtomicReference. 44 @GuardedBy("GeckoJavaSampler.class") 45 private static AtomicReference<ScheduledFuture<?>> sSamplingFuture = new AtomicReference<>(); 46 47 private static final MarkerStorage sMarkerStorage = new MarkerStorage(); 48 49 /** 50 * Returns true if profiler is running and unpaused at the moment which means it's allowed to add 51 * a marker. 52 * 53 * <p>Thread policy: we want this method to be inexpensive (i.e. non-blocking) because we want to 54 * be able to use it in performance-sensitive code. That's why we rely on an AtomicReference. If 55 * this requirement didn't exist, the AtomicReference could be removed because the class thread 56 * policy is to call synchronized on mutable state access. 57 */ isProfilerActive()58 public static boolean isProfilerActive() { 59 // This value will only be present if the profiler is started and not paused. 60 return sSamplingFuture.get() != null; 61 } 62 63 // Use the same timer primitive as the profiler 64 // to get a perfect sample syncing. 65 @WrapForJNI getProfilerTime()66 private static native double getProfilerTime(); 67 68 /** Try to get the profiler time. Returns null if profiler is not running. */ tryToGetProfilerTime()69 public static @Nullable Double tryToGetProfilerTime() { 70 if (!isProfilerActive()) { 71 // Android profiler hasn't started yet. 72 return null; 73 } 74 if (!GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) { 75 // getProfilerTime is not available yet; either libs are not loaded, 76 // or profiling hasn't started on the Gecko side yet 77 return null; 78 } 79 80 return getProfilerTime(); 81 } 82 83 /** 84 * A data container for a profiler sample. This class is effectively immutable (i.e. technically 85 * mutable but never mutated after construction) so is thread safe *if it is safely published* 86 * (see Java Concurrency in Practice, 2nd Ed., Section 3.5.3 for safe publication idioms). 87 */ 88 private static class Sample { 89 public final Frame[] mFrames; 90 public final double mTime; 91 public final long mJavaTime; // non-zero if Android system time is used 92 Sample(final StackTraceElement[] aStack)93 public Sample(final StackTraceElement[] aStack) { 94 mFrames = new Frame[aStack.length]; 95 mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0; 96 97 // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded, 98 // or profiling hasn't started on the Gecko side yet 99 mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0; 100 101 for (int i = 0; i < aStack.length; i++) { 102 mFrames[aStack.length - 1 - i] = 103 new Frame(aStack[i].getMethodName(), aStack[i].getClassName()); 104 } 105 } 106 } 107 108 /** 109 * A container for the metadata around a call in a stack. This class is thread safe by being 110 * immutable. 111 */ 112 private static class Frame { 113 public final String methodName; 114 public final String className; 115 Frame(final String methodName, final String className)116 private Frame(final String methodName, final String className) { 117 this.methodName = methodName; 118 this.className = className; 119 } 120 } 121 122 /** 123 * A data container for metadata around a marker. This class is thread safe by being immutable. 124 */ 125 private static class Marker extends JNIObject { 126 /** Name of the marker */ 127 private final String mMarkerName; 128 /** Either start time for the duration markers or time for a point-in-time markers. */ 129 private final double mTime; 130 /** 131 * A fallback field of {@link #mTime} but it only exists when {@link #getProfilerTime()} is 132 * failed. It is non-zero if Android time is used. 133 */ 134 private final long mJavaTime; 135 /** End time for the duration markers. It's zero for point-in-time markers. */ 136 private final double mEndTime; 137 /** 138 * A fallback field of {@link #mEndTime} but it only exists when {@link #getProfilerTime()} is 139 * failed. It is non-zero if Android time is used. 140 */ 141 private final long mEndJavaTime; 142 /** A nullable additional information field for the marker. */ 143 private @Nullable final String mText; 144 145 /** 146 * Constructor for the Marker class. It initializes different kinds of markers depending on the 147 * parameters. Here are some combinations to create different kinds of markers: 148 * 149 * <p>If you want to create a marker that points a single point in time: <code> 150 * new Marker("name", null, null, null)</code> to implicitly get the time when this marker is 151 * added, or <code>new Marker("name", null, endTime, null)</code> to use an explicit time as an 152 * end time retrieved from {@link #tryToGetProfilerTime()}. 153 * 154 * <p>If you want to create a marker that has a start and end time: <code> 155 * new Marker("name", startTime, null, null)</code> to implicitly get the end time when this 156 * marker is added, or <code>new Marker("name", startTime, endTime, null)</code> to explicitly 157 * give the marker start and end time retrieved from {@link #tryToGetProfilerTime()}. 158 * 159 * <p>Last parameter is optional and can be given with any combination. This gives users the 160 * ability to add more context into a marker. 161 * 162 * @param aMarkerName Identifier of the marker as a string. 163 * @param aStartTime Start time as Double. It can be null if you want to mark a point of time. 164 * @param aEndTime End time as Double. If it's null, this function implicitly gets the end time. 165 * @param aText An optional string field for more information about the marker. 166 */ Marker( @onNull final String aMarkerName, @Nullable final Double aStartTime, @Nullable final Double aEndTime, @Nullable final String aText)167 public Marker( 168 @NonNull final String aMarkerName, 169 @Nullable final Double aStartTime, 170 @Nullable final Double aEndTime, 171 @Nullable final String aText) { 172 mMarkerName = aMarkerName; 173 mText = aText; 174 175 if (aStartTime != null) { 176 // Start time is provided. This is an interval marker. 177 mTime = aStartTime; 178 mJavaTime = 0; 179 if (aEndTime != null) { 180 // End time is also provided. 181 mEndTime = aEndTime; 182 mEndJavaTime = 0; 183 } else { 184 // End time is not provided. Get the profiler time now and use it. 185 mEndTime = 186 GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0; 187 188 // if mEndTime == 0, getProfilerTime is not available yet; either libs are not loaded, 189 // or profiling hasn't started on the Gecko side yet 190 mEndJavaTime = mEndTime == 0.0d ? SystemClock.elapsedRealtime() : 0; 191 } 192 193 } else { 194 // Start time is not provided. This is point-in-time marker. 195 mEndTime = 0; 196 mEndJavaTime = 0; 197 198 if (aEndTime != null) { 199 // End time is also provided. Use that to point the time. 200 mTime = aEndTime; 201 mJavaTime = 0; 202 } else { 203 mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0; 204 205 // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded, 206 // or profiling hasn't started on the Gecko side yet 207 mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0; 208 } 209 } 210 } 211 212 @WrapForJNI 213 @Override // JNIObject disposeNative()214 protected native void disposeNative(); 215 216 @WrapForJNI getStartTime()217 public double getStartTime() { 218 if (mJavaTime != 0) { 219 return (mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime(); 220 } 221 return mTime; 222 } 223 224 @WrapForJNI getEndTime()225 public double getEndTime() { 226 if (mEndJavaTime != 0) { 227 return (mEndJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime(); 228 } 229 return mEndTime; 230 } 231 232 @WrapForJNI getMarkerName()233 public @NonNull String getMarkerName() { 234 return mMarkerName; 235 } 236 237 @WrapForJNI getMarkerText()238 public @Nullable String getMarkerText() { 239 return mText; 240 } 241 } 242 243 /** 244 * Public method to add a new marker to Gecko profiler. This can be used to add a marker *inside* 245 * the geckoview code, but ideally ProfilerController methods should be used instead. 246 * 247 * @see Marker#Marker(String, Double, Double, String) for information about the parameter options. 248 */ addMarker( @onNull final String aMarkerName, @Nullable final Double aStartTime, @Nullable final Double aEndTime, @Nullable final String aText)249 public static void addMarker( 250 @NonNull final String aMarkerName, 251 @Nullable final Double aStartTime, 252 @Nullable final Double aEndTime, 253 @Nullable final String aText) { 254 sMarkerStorage.addMarker(aMarkerName, aStartTime, aEndTime, aText); 255 } 256 257 /** 258 * A routine to store profiler samples. This class is thread safe because it synchronizes access 259 * to its mutable state. 260 */ 261 private static class SamplingRunnable implements Runnable { 262 // Sampling interval that is used by start and unpause 263 public final int mInterval; 264 private final int mSampleCount; 265 266 @GuardedBy("GeckoJavaSampler.class") 267 private boolean mBufferOverflowed = false; 268 269 private final Thread mMainThread; 270 271 @GuardedBy("GeckoJavaSampler.class") 272 private final Sample[] mSamples; 273 274 @GuardedBy("GeckoJavaSampler.class") 275 private int mSamplePos; 276 SamplingRunnable(final int aInterval, final int aSampleCount)277 public SamplingRunnable(final int aInterval, final int aSampleCount) { 278 // Sanity check of sampling interval. 279 mInterval = Math.max(1, aInterval); 280 mSampleCount = aSampleCount; 281 mSamples = new Sample[mSampleCount]; 282 mSamplePos = 0; 283 284 // Find the main thread 285 mMainThread = Looper.getMainLooper().getThread(); 286 if (mMainThread == null) { 287 Log.e(LOGTAG, "Main thread not found"); 288 } 289 } 290 291 @Override run()292 public void run() { 293 synchronized (GeckoJavaSampler.class) { 294 if (mMainThread == null) { 295 return; 296 } 297 final StackTraceElement[] bt = mMainThread.getStackTrace(); 298 mSamples[mSamplePos] = new Sample(bt); 299 mSamplePos += 1; 300 if (mSamplePos == mSampleCount) { 301 // Sample array is full now, go back to start of 302 // the array and override old samples 303 mSamplePos = 0; 304 mBufferOverflowed = true; 305 } 306 } 307 } 308 getSample(final int aSampleId)309 private Sample getSample(final int aSampleId) { 310 synchronized (GeckoJavaSampler.class) { 311 if (aSampleId >= mSampleCount) { 312 // Return early because there is no more sample left. 313 return null; 314 } 315 316 int samplePos = aSampleId; 317 if (mBufferOverflowed) { 318 // This is a circular buffer and the buffer is overflowed. Start 319 // of the buffer is mSamplePos now. Calculate the real index. 320 samplePos = (samplePos + mSamplePos) % mSampleCount; 321 } 322 323 // Since the array elements are initialized to null, it will return 324 // null whenever we access to an element that's not been written yet. 325 // We want it to return null in that case, so it's okay. 326 return mSamples[samplePos]; 327 } 328 } 329 } 330 331 /** 332 * Returns the sample with the given sample ID. 333 * 334 * <p>Thread safety code smell: this method call is synchronized but this class returns a 335 * reference to an effectively immutable object so that the reference is accessible after 336 * synchronization ends. It's unclear if this is thread safe. However, this is safe with the 337 * current callers (because they are all synchronized and don't leak the Sample) so we don't 338 * investigate it further. 339 */ getSample(final int aSampleId)340 private static synchronized Sample getSample(final int aSampleId) { 341 return sSamplingRunnable.getSample(aSampleId); 342 } 343 344 @WrapForJNI pollNextMarker()345 public static Marker pollNextMarker() { 346 return sMarkerStorage.pollNextMarker(); 347 } 348 349 @WrapForJNI getSampleTime(final int aSampleId)350 public static synchronized double getSampleTime(final int aSampleId) { 351 final Sample sample = getSample(aSampleId); 352 if (sample != null) { 353 if (sample.mJavaTime != 0) { 354 return (sample.mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime(); 355 } 356 return sample.mTime; 357 } 358 return 0; 359 } 360 361 @WrapForJNI getFrameName(final int aSampleId, final int aFrameId)362 public static synchronized String getFrameName(final int aSampleId, final int aFrameId) { 363 final Sample sample = getSample(aSampleId); 364 if (sample != null && aFrameId < sample.mFrames.length) { 365 final Frame frame = sample.mFrames[aFrameId]; 366 if (frame == null) { 367 return null; 368 } 369 return frame.className + "." + frame.methodName + "()"; 370 } 371 return null; 372 } 373 374 /** 375 * A start/stop-aware container for storing profiler markers. 376 * 377 * <p>This class is thread safe: see {@link #mMarkers} for the threading policy. Start/stop are 378 * guaranteed to execute in the order they are called but other methods do not have such ordering 379 * guarantees. 380 */ 381 private static class MarkerStorage { 382 /** 383 * The underlying storage for the markers. This field maintains thread safety without using 384 * synchronized everywhere by: 385 * <li>- using volatile to allow non-blocking reads 386 * <li>- leveraging a thread safe collection when accessing the underlying data 387 * <li>- looping until success for compound read-write operations 388 */ 389 private volatile Queue<Marker> mMarkers; 390 MarkerStorage()391 MarkerStorage() {} 392 start(final int aMarkerCount)393 public synchronized void start(final int aMarkerCount) { 394 if (this.mMarkers != null) { 395 return; 396 } 397 this.mMarkers = new LinkedBlockingQueue<>(aMarkerCount); 398 } 399 stop()400 public synchronized void stop() { 401 if (this.mMarkers == null) { 402 return; 403 } 404 this.mMarkers = null; 405 } 406 addMarker( @onNull final String aMarkerName, @Nullable final Double aStartTime, @Nullable final Double aEndTime, @Nullable final String aText)407 private void addMarker( 408 @NonNull final String aMarkerName, 409 @Nullable final Double aStartTime, 410 @Nullable final Double aEndTime, 411 @Nullable final String aText) { 412 final Queue<Marker> markersQueue = this.mMarkers; 413 if (markersQueue == null) { 414 // Profiler is not active. 415 return; 416 } 417 418 // It would be good to use `Looper.getMainLooper().isCurrentThread()` 419 // instead but it requires API level 23 and current min is 16. 420 if (Looper.myLooper() != Looper.getMainLooper()) { 421 // Bug 1618560: Currently only main thread is being profiled and 422 // this marker doesn't belong to the main thread. 423 throw new AssertionError("Currently only main thread is supported for markers."); 424 } 425 426 final Marker newMarker = new Marker(aMarkerName, aStartTime, aEndTime, aText); 427 428 boolean successful = markersQueue.offer(newMarker); 429 while (!successful) { 430 // Marker storage is full, remove the head and add again. 431 markersQueue.poll(); 432 successful = markersQueue.offer(newMarker); 433 } 434 } 435 pollNextMarker()436 private Marker pollNextMarker() { 437 final Queue<Marker> markersQueue = this.mMarkers; 438 if (markersQueue == null) { 439 // Profiler is not active. 440 return null; 441 } 442 // Retrieve and return the head of this queue. 443 // Returns null if the queue is empty. 444 return markersQueue.poll(); 445 } 446 } 447 448 @WrapForJNI start(final int aInterval, final int aEntryCount)449 public static void start(final int aInterval, final int aEntryCount) { 450 synchronized (GeckoJavaSampler.class) { 451 if (sSamplingRunnable != null) { 452 return; 453 } 454 455 final ScheduledFuture<?> future = sSamplingFuture.get(); 456 if (future != null && !future.isDone()) { 457 return; 458 } 459 460 // Setting a limit of 120000 (2 mins with 1ms interval) for samples and markers for now 461 // to make sure we are not allocating too much. 462 final int limitedEntryCount = Math.min(aEntryCount, 120000); 463 sSamplingRunnable = new SamplingRunnable(aInterval, limitedEntryCount); 464 sMarkerStorage.start(limitedEntryCount); 465 sSamplingScheduler = Executors.newSingleThreadScheduledExecutor(); 466 sSamplingFuture.set( 467 sSamplingScheduler.scheduleAtFixedRate( 468 sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS)); 469 } 470 } 471 472 @WrapForJNI pauseSampling()473 public static void pauseSampling() { 474 synchronized (GeckoJavaSampler.class) { 475 final ScheduledFuture<?> future = sSamplingFuture.getAndSet(null); 476 future.cancel(false /* mayInterruptIfRunning */); 477 } 478 } 479 480 @WrapForJNI unpauseSampling()481 public static void unpauseSampling() { 482 synchronized (GeckoJavaSampler.class) { 483 if (sSamplingFuture.get() != null) { 484 return; 485 } 486 sSamplingFuture.set( 487 sSamplingScheduler.scheduleAtFixedRate( 488 sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS)); 489 } 490 } 491 492 @WrapForJNI stop()493 public static void stop() { 494 synchronized (GeckoJavaSampler.class) { 495 if (sSamplingRunnable == null) { 496 return; 497 } 498 499 try { 500 sSamplingScheduler.shutdown(); 501 // 1s is enough to wait shutdown. 502 sSamplingScheduler.awaitTermination(1000, TimeUnit.MILLISECONDS); 503 } catch (final InterruptedException e) { 504 Log.e(LOGTAG, "Sampling scheduler isn't terminated. Last sampling data might be broken."); 505 sSamplingScheduler.shutdownNow(); 506 } 507 sSamplingScheduler = null; 508 sSamplingRunnable = null; 509 sSamplingFuture.set(null); 510 sMarkerStorage.stop(); 511 } 512 } 513 514 /** Returns the device brand and model as a string. */ 515 @WrapForJNI getDeviceInformation()516 public static String getDeviceInformation() { 517 final StringBuilder sb = new StringBuilder(Build.BRAND); 518 sb.append(" "); 519 sb.append(Build.MODEL); 520 return sb.toString(); 521 } 522 } 523