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