tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

GeckoJavaSampler.java (31599B)


      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.content.ComponentName;
      9 import android.content.Intent;
     10 import android.os.Build;
     11 import android.os.Looper;
     12 import android.os.Process;
     13 import android.os.SystemClock;
     14 import android.util.Log;
     15 import androidx.annotation.GuardedBy;
     16 import androidx.annotation.NonNull;
     17 import androidx.annotation.Nullable;
     18 import androidx.core.content.ContextCompat;
     19 import java.util.ArrayList;
     20 import java.util.Arrays;
     21 import java.util.Collections;
     22 import java.util.HashSet;
     23 import java.util.List;
     24 import java.util.Locale;
     25 import java.util.Objects;
     26 import java.util.Queue;
     27 import java.util.Set;
     28 import java.util.concurrent.Executors;
     29 import java.util.concurrent.LinkedBlockingQueue;
     30 import java.util.concurrent.ScheduledExecutorService;
     31 import java.util.concurrent.ScheduledFuture;
     32 import java.util.concurrent.TimeUnit;
     33 import java.util.concurrent.atomic.AtomicReference;
     34 import org.mozilla.gecko.annotation.WrapForJNI;
     35 import org.mozilla.gecko.mozglue.JNIObject;
     36 import org.mozilla.geckoview.GeckoResult;
     37 
     38 /**
     39 * Takes samples and adds markers for Java threads for the Gecko profiler.
     40 *
     41 * <p>This class is thread safe because it uses synchronized on accesses to its mutable state. One
     42 * exception is {@link #isProfilerActive()}: see the javadoc for details.
     43 */
     44 public class GeckoJavaSampler {
     45 
     46  private static final String LOGTAG = "GeckoJavaSampler";
     47 
     48  private static final String PROFILER_SERVICE_CLASS_NAME =
     49      "org.mozilla.fenix.perf.ProfilerService";
     50  private static final String PROFILER_SERVICE_ACTION = "mozilla.perf.action.START_PROFILING";
     51  public static final String INTENT_PROFILER_STATE_CHANGED =
     52      "org.mozilla.fenix.PROFILER_STATE_CHANGED";
     53 
     54  /**
     55   * The thread ID to use for the main thread instead of its true thread ID.
     56   *
     57   * <p>The main thread is sampled twice: once for native code and once on the JVM. The native
     58   * version uses the thread's id so we replace it to avoid a collision. We use this thread ID
     59   * because it's unlikely any other thread currently has it. We can't use 0 because 0 is considered
     60   * "unspecified" in native code:
     61   * https://searchfox.org/mozilla-central/rev/d4ebb53e719b913afdbcf7c00e162f0e96574701/mozglue/baseprofiler/public/BaseProfilerUtils.h#194
     62   */
     63  private static final long REPLACEMENT_MAIN_THREAD_ID = 1;
     64 
     65  /**
     66   * The thread name to use for the main thread instead of its true thread name. The name is "main",
     67   * which is ambiguous with the JS main thread, so we rename it to match the C++ replacement. We
     68   * expect our code to later add a suffix to avoid a collision with the C++ thread name. See {@link
     69   * #REPLACEMENT_MAIN_THREAD_ID} for related details.
     70   */
     71  private static final String REPLACEMENT_MAIN_THREAD_NAME = "AndroidUI";
     72 
     73  @GuardedBy("GeckoJavaSampler.class")
     74  private static SamplingRunnable sSamplingRunnable;
     75 
     76  @GuardedBy("GeckoJavaSampler.class")
     77  private static ScheduledExecutorService sSamplingScheduler;
     78 
     79  // See isProfilerActive for details on the AtomicReference.
     80  @GuardedBy("GeckoJavaSampler.class")
     81  private static final AtomicReference<ScheduledFuture<?>> sSamplingFuture =
     82      new AtomicReference<>();
     83 
     84  private static final MarkerStorage sMarkerStorage = new MarkerStorage();
     85 
     86  /**
     87   * Returns true if profiler is running and unpaused at the moment which means it's allowed to add
     88   * a marker.
     89   *
     90   * <p>Thread policy: we want this method to be inexpensive (i.e. non-blocking) because we want to
     91   * be able to use it in performance-sensitive code. That's why we rely on an AtomicReference. If
     92   * this requirement didn't exist, the AtomicReference could be removed because the class thread
     93   * policy is to call synchronized on mutable state access.
     94   */
     95  public static boolean isProfilerActive() {
     96    // This value will only be present if the profiler is started and not paused.
     97    return sSamplingFuture.get() != null;
     98  }
     99 
    100  // Use the same timer primitive as the profiler
    101  // to get a perfect sample syncing.
    102  @WrapForJNI
    103  private static native double getProfilerTime();
    104 
    105  /** Try to get the profiler time. Returns null if profiler is not running. */
    106  public static @Nullable Double tryToGetProfilerTime() {
    107    if (!isProfilerActive()) {
    108      // Android profiler hasn't started yet.
    109      return null;
    110    }
    111    if (!GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) {
    112      // getProfilerTime is not available yet; either libs are not loaded,
    113      // or profiling hasn't started on the Gecko side yet
    114      return null;
    115    }
    116 
    117    return getProfilerTime();
    118  }
    119 
    120  /**
    121   * A data container for a profiler sample. This class is effectively immutable (i.e. technically
    122   * mutable but never mutated after construction) so is thread safe *if it is safely published*
    123   * (see Java Concurrency in Practice, 2nd Ed., Section 3.5.3 for safe publication idioms).
    124   */
    125  private static class Sample {
    126    public final long mThreadId;
    127    public final Frame[] mFrames;
    128    public final double mTime;
    129    public final long mJavaTime; // non-zero if Android system time is used
    130 
    131    public Sample(final long aThreadId, final StackTraceElement[] aStack) {
    132      mThreadId = aThreadId;
    133      mFrames = new Frame[aStack.length];
    134      mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0;
    135 
    136      // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded,
    137      // or profiling hasn't started on the Gecko side yet
    138      mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0;
    139 
    140      for (int i = 0; i < aStack.length; i++) {
    141        mFrames[aStack.length - 1 - i] =
    142            new Frame(aStack[i].getMethodName(), aStack[i].getClassName());
    143      }
    144    }
    145  }
    146 
    147  /**
    148   * A container for the metadata around a call in a stack. This class is thread safe by being
    149   * immutable.
    150   */
    151  private static class Frame {
    152    public final String methodName;
    153    public final String className;
    154 
    155    private Frame(final String methodName, final String className) {
    156      this.methodName = methodName;
    157      this.className = className;
    158    }
    159  }
    160 
    161  /** A data container for thread metadata. */
    162  private static class ThreadInfo {
    163    private final long mId;
    164    private final String mName;
    165 
    166    public ThreadInfo(final long mId, final String mName) {
    167      this.mId = mId;
    168      this.mName = mName;
    169    }
    170 
    171    @WrapForJNI
    172    public long getId() {
    173      return mId;
    174    }
    175 
    176    @WrapForJNI
    177    public String getName() {
    178      return mName;
    179    }
    180  }
    181 
    182  /**
    183   * A data container for metadata around a marker. This class is thread safe by being immutable.
    184   */
    185  private static class Marker extends JNIObject {
    186    /** The id of the thread this marker was captured on. */
    187    private final long mThreadId;
    188 
    189    /** Name of the marker */
    190    private final String mMarkerName;
    191 
    192    /** Either start time for the duration markers or time for a point-in-time markers. */
    193    private final double mTime;
    194 
    195    /**
    196     * A fallback field of {@link #mTime} but it only exists when {@link #getProfilerTime()} is
    197     * failed. It is non-zero if Android time is used.
    198     */
    199    private final long mJavaTime;
    200 
    201    /** End time for the duration markers. It's zero for point-in-time markers. */
    202    private final double mEndTime;
    203 
    204    /**
    205     * A fallback field of {@link #mEndTime} but it only exists when {@link #getProfilerTime()} is
    206     * failed. It is non-zero if Android time is used.
    207     */
    208    private final long mEndJavaTime;
    209 
    210    /** A nullable additional information field for the marker. */
    211    private @Nullable final String mText;
    212 
    213    /**
    214     * Constructor for the Marker class. It initializes different kinds of markers depending on the
    215     * parameters. Here are some combinations to create different kinds of markers:
    216     *
    217     * <p>If you want to create a marker that points a single point in time: <code>
    218     * new Marker("name", null, null, null)</code> to implicitly get the time when this marker is
    219     * added, or <code>new Marker("name", null, endTime, null)</code> to use an explicit time as an
    220     * end time retrieved from {@link #tryToGetProfilerTime()}.
    221     *
    222     * <p>If you want to create a marker that has a start and end time: <code>
    223     * new Marker("name", startTime, null, null)</code> to implicitly get the end time when this
    224     * marker is added, or <code>new Marker("name", startTime, endTime, null)</code> to explicitly
    225     * give the marker start and end time retrieved from {@link #tryToGetProfilerTime()}.
    226     *
    227     * <p>Last parameter is optional and can be given with any combination. This gives users the
    228     * ability to add more context into a marker.
    229     *
    230     * @param aThreadId The id of the thread this marker was captured on.
    231     * @param aMarkerName Identifier of the marker as a string.
    232     * @param aStartTime Start time as Double. It can be null if you want to mark a point of time.
    233     * @param aEndTime End time as Double. If it's null, this function implicitly gets the end time.
    234     * @param aText An optional string field for more information about the marker.
    235     */
    236    public Marker(
    237        final long aThreadId,
    238        @NonNull final String aMarkerName,
    239        @Nullable final Double aStartTime,
    240        @Nullable final Double aEndTime,
    241        @Nullable final String aText) {
    242      mThreadId = getAdjustedThreadId(aThreadId);
    243      mMarkerName = aMarkerName;
    244      mText = aText;
    245 
    246      if (aStartTime != null) {
    247        // Start time is provided. This is an interval marker.
    248        mTime = aStartTime;
    249        mJavaTime = 0;
    250        if (aEndTime != null) {
    251          // End time is also provided.
    252          mEndTime = aEndTime;
    253          mEndJavaTime = 0;
    254        } else {
    255          // End time is not provided. Get the profiler time now and use it.
    256          mEndTime =
    257              GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0;
    258 
    259          // if mEndTime == 0, getProfilerTime is not available yet; either libs are not loaded,
    260          // or profiling hasn't started on the Gecko side yet
    261          mEndJavaTime = mEndTime == 0.0d ? SystemClock.elapsedRealtime() : 0;
    262        }
    263 
    264      } else {
    265        // Start time is not provided. This is point-in-time marker.
    266        mEndTime = 0;
    267        mEndJavaTime = 0;
    268 
    269        if (aEndTime != null) {
    270          // End time is also provided. Use that to point the time.
    271          mTime = aEndTime;
    272          mJavaTime = 0;
    273        } else {
    274          mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0;
    275 
    276          // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded,
    277          // or profiling hasn't started on the Gecko side yet
    278          mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0;
    279        }
    280      }
    281    }
    282 
    283    @WrapForJNI
    284    @Override // JNIObject
    285    protected native void disposeNative();
    286 
    287    @WrapForJNI
    288    public double getStartTime() {
    289      if (mJavaTime != 0) {
    290        return (mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime();
    291      }
    292      return mTime;
    293    }
    294 
    295    @WrapForJNI
    296    public double getEndTime() {
    297      if (mEndJavaTime != 0) {
    298        return (mEndJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime();
    299      }
    300      return mEndTime;
    301    }
    302 
    303    @WrapForJNI
    304    public long getThreadId() {
    305      return mThreadId;
    306    }
    307 
    308    @WrapForJNI
    309    public @NonNull String getMarkerName() {
    310      return mMarkerName;
    311    }
    312 
    313    @WrapForJNI
    314    public @Nullable String getMarkerText() {
    315      return mText;
    316    }
    317  }
    318 
    319  /**
    320   * Public method to add a new marker to Gecko profiler. This can be used to add a marker *inside*
    321   * the geckoview code, but ideally ProfilerController methods should be used instead.
    322   *
    323   * @see Marker#Marker(long, String, Double, Double, String) for information about the parameter
    324   *     options.
    325   */
    326  public static void addMarker(
    327      @NonNull final String aMarkerName,
    328      @Nullable final Double aStartTime,
    329      @Nullable final Double aEndTime,
    330      @Nullable final String aText) {
    331    sMarkerStorage.addMarker(aMarkerName, aStartTime, aEndTime, aText);
    332  }
    333 
    334  /**
    335   * A routine to store profiler samples. This class is thread safe because it synchronizes access
    336   * to its mutable state.
    337   */
    338  private static class SamplingRunnable implements Runnable {
    339    private final long mMainThreadId = Looper.getMainLooper().getThread().getId();
    340 
    341    // Sampling interval that is used by start and unpause
    342    public final int mInterval;
    343    private final int mSampleCount;
    344 
    345    @GuardedBy("GeckoJavaSampler.class")
    346    private boolean mBufferOverflowed = false;
    347 
    348    @GuardedBy("GeckoJavaSampler.class")
    349    private @NonNull final List<Thread> mThreadsToProfile;
    350 
    351    @GuardedBy("GeckoJavaSampler.class")
    352    private final Sample[] mSamples;
    353 
    354    @GuardedBy("GeckoJavaSampler.class")
    355    private int mSamplePos;
    356 
    357    public SamplingRunnable(
    358        @NonNull final List<Thread> aThreadsToProfile,
    359        final int aInterval,
    360        final int aSampleCount) {
    361      mThreadsToProfile = aThreadsToProfile;
    362      // Sanity check of sampling interval.
    363      mInterval = Math.max(1, aInterval);
    364      mSampleCount = aSampleCount;
    365      mSamples = new Sample[mSampleCount];
    366      mSamplePos = 0;
    367    }
    368 
    369    @Override
    370    public void run() {
    371      synchronized (GeckoJavaSampler.class) {
    372        // To minimize allocation in the critical section, we use a traditional for loop instead of
    373        // a for each (i.e. `elem : coll`) loop because that allocates an iterator.
    374        //
    375        // We won't capture threads that are started during profiling because we iterate through an
    376        // unchanging list of threads (bug 1759550).
    377        for (int i = 0; i < mThreadsToProfile.size(); i++) {
    378          final Thread thread = mThreadsToProfile.get(i);
    379 
    380          // getStackTrace will return an empty trace if the thread is not alive: we call continue
    381          // to avoid wasting space in the buffer for an empty sample.
    382          final StackTraceElement[] stackTrace = thread.getStackTrace();
    383          if (stackTrace.length == 0) {
    384            continue;
    385          }
    386 
    387          mSamples[mSamplePos] = new Sample(thread.getId(), stackTrace);
    388          mSamplePos += 1;
    389          if (mSamplePos == mSampleCount) {
    390            // Sample array is full now, go back to start of
    391            // the array and override old samples
    392            mSamplePos = 0;
    393            mBufferOverflowed = true;
    394          }
    395        }
    396      }
    397    }
    398 
    399    private Sample getSample(final int aSampleId) {
    400      synchronized (GeckoJavaSampler.class) {
    401        if (aSampleId >= mSampleCount) {
    402          // Return early because there is no more sample left.
    403          return null;
    404        }
    405 
    406        int samplePos = aSampleId;
    407        if (mBufferOverflowed) {
    408          // This is a circular buffer and the buffer is overflowed. Start
    409          // of the buffer is mSamplePos now. Calculate the real index.
    410          samplePos = (samplePos + mSamplePos) % mSampleCount;
    411        }
    412 
    413        // Since the array elements are initialized to null, it will return
    414        // null whenever we access to an element that's not been written yet.
    415        // We want it to return null in that case, so it's okay.
    416        return mSamples[samplePos];
    417      }
    418    }
    419  }
    420 
    421  /**
    422   * Returns the sample with the given sample ID.
    423   *
    424   * <p>Thread safety code smell: this method call is synchronized but this class returns a
    425   * reference to an effectively immutable object so that the reference is accessible after
    426   * synchronization ends. It's unclear if this is thread safe. However, this is safe with the
    427   * current callers (because they are all synchronized and don't leak the Sample) so we don't
    428   * investigate it further.
    429   */
    430  private static synchronized Sample getSample(final int aSampleId) {
    431    return sSamplingRunnable.getSample(aSampleId);
    432  }
    433 
    434  @WrapForJNI
    435  public static Marker pollNextMarker() {
    436    return sMarkerStorage.pollNextMarker();
    437  }
    438 
    439  @WrapForJNI
    440  public static synchronized int getRegisteredThreadCount() {
    441    return sSamplingRunnable.mThreadsToProfile.size();
    442  }
    443 
    444  @WrapForJNI
    445  public static synchronized ThreadInfo getRegisteredThreadInfo(final int aIndex) {
    446    final Thread thread = sSamplingRunnable.mThreadsToProfile.get(aIndex);
    447 
    448    // See REPLACEMENT_MAIN_THREAD_NAME for why we do this.
    449    String adjustedThreadName =
    450        thread.getId() == sSamplingRunnable.mMainThreadId
    451            ? REPLACEMENT_MAIN_THREAD_NAME
    452            : thread.getName();
    453 
    454    // To distinguish JVM threads from native threads, we append a JVM-specific suffix.
    455    adjustedThreadName += " (JVM)";
    456    return new ThreadInfo(getAdjustedThreadId(thread.getId()), adjustedThreadName);
    457  }
    458 
    459  @WrapForJNI
    460  public static synchronized long getThreadId(final int aSampleId) {
    461    final Sample sample = getSample(aSampleId);
    462    return getAdjustedThreadId(sample != null ? sample.mThreadId : 0);
    463  }
    464 
    465  private static synchronized long getAdjustedThreadId(final long threadId) {
    466    // See REPLACEMENT_MAIN_THREAD_ID for why we do this.
    467    return threadId == sSamplingRunnable.mMainThreadId ? REPLACEMENT_MAIN_THREAD_ID : threadId;
    468  }
    469 
    470  @WrapForJNI
    471  public static synchronized double getSampleTime(final int aSampleId) {
    472    final Sample sample = getSample(aSampleId);
    473    if (sample != null) {
    474      if (sample.mJavaTime != 0) {
    475        return (sample.mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime();
    476      }
    477      return sample.mTime;
    478    }
    479    return 0;
    480  }
    481 
    482  @WrapForJNI
    483  public static synchronized String getFrameName(final int aSampleId, final int aFrameId) {
    484    final Sample sample = getSample(aSampleId);
    485    if (sample != null && aFrameId < sample.mFrames.length) {
    486      final Frame frame = sample.mFrames[aFrameId];
    487      if (frame == null) {
    488        return null;
    489      }
    490      return frame.className + "." + frame.methodName + "()";
    491    }
    492    return null;
    493  }
    494 
    495  /**
    496   * A start/stop-aware container for storing profiler markers.
    497   *
    498   * <p>This class is thread safe: see {@link #mMarkers} and other member variables for the
    499   * threading policy. Start/stop are guaranteed to execute in the order they are called but other
    500   * methods do not have such ordering guarantees.
    501   */
    502  private static class MarkerStorage {
    503    /**
    504     * The underlying storage for the markers. This field maintains thread safety without using
    505     * synchronized everywhere by:
    506     * <li>- using volatile to allow non-blocking reads
    507     * <li>- leveraging a thread safe collection when accessing the underlying data
    508     * <li>- looping until success for compound read-write operations
    509     */
    510    private volatile Queue<Marker> mMarkers;
    511 
    512    /**
    513     * The thread ids of the threads we're profiling. This field maintains thread safety by writing
    514     * a read-only value to this volatile field before concurrency begins and only reading it during
    515     * concurrent sections.
    516     */
    517    private volatile Set<Long> mProfiledThreadIds = Collections.emptySet();
    518 
    519    MarkerStorage() {}
    520 
    521    public synchronized void start(final int aMarkerCount, final List<Thread> aProfiledThreads) {
    522      if (this.mMarkers != null) {
    523        return;
    524      }
    525      this.mMarkers = new LinkedBlockingQueue<>(aMarkerCount);
    526 
    527      final Set<Long> profiledThreadIds = new HashSet<>(aProfiledThreads.size());
    528      for (final Thread thread : aProfiledThreads) {
    529        profiledThreadIds.add(thread.getId());
    530      }
    531 
    532      // We use a temporary collection, rather than mutating the collection within the member
    533      // variable, to ensure the collection is fully written before the state is made available to
    534      // all threads via the volatile write into the member variable. This collection must be
    535      // read-only for it to remain thread safe.
    536      mProfiledThreadIds = Collections.unmodifiableSet(profiledThreadIds);
    537    }
    538 
    539    public synchronized void stop() {
    540      if (this.mMarkers == null) {
    541        return;
    542      }
    543      this.mMarkers = null;
    544      mProfiledThreadIds = Collections.emptySet();
    545    }
    546 
    547    private void addMarker(
    548        @NonNull final String aMarkerName,
    549        @Nullable final Double aStartTime,
    550        @Nullable final Double aEndTime,
    551        @Nullable final String aText) {
    552      final Queue<Marker> markersQueue = this.mMarkers;
    553      if (markersQueue == null) {
    554        // Profiler is not active.
    555        return;
    556      }
    557 
    558      final long threadId = Thread.currentThread().getId();
    559      if (!mProfiledThreadIds.contains(threadId)) {
    560        return;
    561      }
    562 
    563      final Marker newMarker = new Marker(threadId, aMarkerName, aStartTime, aEndTime, aText);
    564      boolean successful = markersQueue.offer(newMarker);
    565      while (!successful) {
    566        // Marker storage is full, remove the head and add again.
    567        markersQueue.poll();
    568        successful = markersQueue.offer(newMarker);
    569      }
    570    }
    571 
    572    private Marker pollNextMarker() {
    573      final Queue<Marker> markersQueue = this.mMarkers;
    574      if (markersQueue == null) {
    575        // Profiler is not active.
    576        return null;
    577      }
    578      // Retrieve and return the head of this queue.
    579      // Returns null if the queue is empty.
    580      return markersQueue.poll();
    581    }
    582  }
    583 
    584  @WrapForJNI
    585  public static void start(
    586      @NonNull final Object[] aFilters, final int aInterval, final int aEntryCount) {
    587    synchronized (GeckoJavaSampler.class) {
    588      if (sSamplingRunnable != null) {
    589        return;
    590      }
    591 
    592      final ScheduledFuture<?> future = sSamplingFuture.get();
    593      if (future != null && !future.isDone()) {
    594        return;
    595      }
    596 
    597      // Setting a limit of 120000 (2 mins with 1ms interval) for samples and markers for now
    598      // to make sure we are not allocating too much.
    599      final int limitedEntryCount = Math.min(aEntryCount, 120000);
    600 
    601      final List<Thread> threadsToProfile = getThreadsToProfile(aFilters);
    602      if (threadsToProfile.size() < 1) {
    603        throw new IllegalStateException("Expected >= 1 thread to profile (main thread).");
    604      }
    605      Log.i(LOGTAG, "Number of threads to profile: " + threadsToProfile.size());
    606 
    607      sSamplingRunnable = new SamplingRunnable(threadsToProfile, aInterval, limitedEntryCount);
    608      sMarkerStorage.start(limitedEntryCount, threadsToProfile);
    609      sSamplingScheduler = Executors.newSingleThreadScheduledExecutor();
    610      sSamplingFuture.set(
    611          sSamplingScheduler.scheduleAtFixedRate(
    612              sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS));
    613    }
    614  }
    615 
    616  private static @NonNull List<Thread> getThreadsToProfile(final Object[] aFilters) {
    617    // Clean up filters.
    618    final List<String> cleanedFilters = new ArrayList<>();
    619    for (final Object rawFilter : aFilters) {
    620      // aFilters is a String[] but jni can only accept Object[] so we're forced to cast.
    621      //
    622      // We could pass the lowercased filters from native code but it may not handle lowercasing the
    623      // same way Java does so we lower case here so it's consistent later when we lower case the
    624      // thread name and compare against it.
    625      final String filter = ((String) rawFilter).trim().toLowerCase(Locale.US);
    626 
    627      // If the filter is empty, it's not meaningful: skip.
    628      if (filter.isEmpty()) {
    629        continue;
    630      }
    631 
    632      cleanedFilters.add(filter);
    633    }
    634 
    635    final ThreadGroup rootThreadGroup = getRootThreadGroup();
    636    final Thread[] activeThreads = getActiveThreads(rootThreadGroup);
    637    final Thread mainThread = Looper.getMainLooper().getThread();
    638 
    639    // We model these catch-all filters after the C++ code (which we should eventually deduplicate):
    640    // https://searchfox.org/mozilla-central/rev/b0779bcc485dc1c04334dfb9ea024cbfff7b961a/tools/profiler/core/platform.cpp#778-801
    641    if (cleanedFilters.contains("*") || doAnyFiltersMatchPid(cleanedFilters, Process.myPid())) {
    642      final List<Thread> activeThreadList = new ArrayList<>();
    643      Collections.addAll(activeThreadList, activeThreads);
    644      if (!activeThreadList.contains(mainThread)) {
    645        activeThreadList.add(mainThread); // see below for why this is necessary.
    646      }
    647      return activeThreadList;
    648    }
    649 
    650    // We always want to profile the main thread. We're not certain getActiveThreads returns
    651    // all active threads since we've observed that getActiveThreads doesn't include the main thread
    652    // during xpcshell tests even though it's alive (bug 1760716). We intentionally don't rely on
    653    // that method to add the main thread here.
    654    final List<Thread> threadsToProfile = new ArrayList<>();
    655    threadsToProfile.add(mainThread);
    656 
    657    for (final Thread thread : activeThreads) {
    658      if (shouldProfileThread(thread, cleanedFilters, mainThread)) {
    659        threadsToProfile.add(thread);
    660      }
    661    }
    662    return threadsToProfile;
    663  }
    664 
    665  private static boolean shouldProfileThread(
    666      final Thread aThread, final List<String> aFilters, final Thread aMainThread) {
    667    final String threadName = aThread.getName().trim().toLowerCase(Locale.US);
    668    if (threadName.isEmpty()) {
    669      return false; // We can't match against a thread with no name: skip.
    670    }
    671 
    672    if (aThread.equals(aMainThread)) {
    673      return false; // We've already added the main thread outside of this method.
    674    }
    675 
    676    for (final String filter : aFilters) {
    677      // In order to generically support thread pools with thread names like "arch_disk_io_0" (the
    678      // kotlin IO dispatcher), we check if the filter is inside the thread name (e.g. a filter of
    679      // "io" will match all of the threads in that pool) rather than an equality check.
    680      if (threadName.contains(filter)) {
    681        return true;
    682      }
    683    }
    684 
    685    return false;
    686  }
    687 
    688  private static boolean doAnyFiltersMatchPid(
    689      @NonNull final List<String> aFilters, final long aPid) {
    690    final String prefix = "pid:";
    691    for (final String filter : aFilters) {
    692      if (!filter.startsWith(prefix)) {
    693        continue;
    694      }
    695 
    696      try {
    697        final long filterPid = Long.parseLong(filter.substring(prefix.length()));
    698        if (filterPid == aPid) {
    699          return true;
    700        }
    701      } catch (final NumberFormatException e) {
    702        /* do nothing. */
    703      }
    704    }
    705 
    706    return false;
    707  }
    708 
    709  private static @NonNull Thread[] getActiveThreads(final @NonNull ThreadGroup rootThreadGroup) {
    710    // We need the root thread group to get all of the active threads because of how
    711    // ThreadGroup.enumerate works.
    712    //
    713    // ThreadGroup.enumerate is inherently racey so we loop until we capture all of the active
    714    // threads. We can only detect if we didn't capture all of the threads if the number of threads
    715    // found (the value returned by enumerate) is smaller than the array we're capturing them in.
    716    // Therefore, we make the array slightly larger than the known number of threads.
    717    Thread[] allThreads;
    718    int threadsFound;
    719    do {
    720      allThreads = new Thread[rootThreadGroup.activeCount() + 15];
    721      threadsFound = rootThreadGroup.enumerate(allThreads, /* recurse */ true);
    722    } while (threadsFound >= allThreads.length);
    723 
    724    // There will be more indices in the array than threads and these will be set to null. We remove
    725    // the null values to minimize bugs.
    726    return Arrays.copyOfRange(allThreads, 0, threadsFound);
    727  }
    728 
    729  private static @NonNull ThreadGroup getRootThreadGroup() {
    730    // Assert non-null: getThreadGroup only returns null for dead threads but the current thread
    731    // can't be dead.
    732    ThreadGroup parentGroup = Objects.requireNonNull(Thread.currentThread().getThreadGroup());
    733 
    734    ThreadGroup group = null;
    735    while (parentGroup != null) {
    736      group = parentGroup;
    737      parentGroup = group.getParent();
    738    }
    739    return group;
    740  }
    741 
    742  @WrapForJNI
    743  public static void pauseSampling() {
    744    synchronized (GeckoJavaSampler.class) {
    745      final ScheduledFuture<?> future = sSamplingFuture.getAndSet(null);
    746      future.cancel(false /* mayInterruptIfRunning */);
    747    }
    748  }
    749 
    750  @WrapForJNI
    751  public static void unpauseSampling() {
    752    synchronized (GeckoJavaSampler.class) {
    753      if (sSamplingFuture.get() != null) {
    754        return;
    755      }
    756      sSamplingFuture.set(
    757          sSamplingScheduler.scheduleAtFixedRate(
    758              sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS));
    759    }
    760  }
    761 
    762  @WrapForJNI
    763  public static void stop() {
    764    synchronized (GeckoJavaSampler.class) {
    765      if (sSamplingRunnable == null) {
    766        return;
    767      }
    768 
    769      Log.i(
    770          LOGTAG,
    771          "Profiler stopping. Sample array position: "
    772              + sSamplingRunnable.mSamplePos
    773              + ". Overflowed? "
    774              + sSamplingRunnable.mBufferOverflowed);
    775 
    776      try {
    777        sSamplingScheduler.shutdown();
    778        // 1s is enough to wait shutdown.
    779        sSamplingScheduler.awaitTermination(1000, TimeUnit.MILLISECONDS);
    780      } catch (final InterruptedException e) {
    781        Log.e(LOGTAG, "Sampling scheduler isn't terminated. Last sampling data might be broken.");
    782        sSamplingScheduler.shutdownNow();
    783      }
    784      sSamplingScheduler = null;
    785      sSamplingRunnable = null;
    786      sSamplingFuture.set(null);
    787      sMarkerStorage.stop();
    788    }
    789  }
    790 
    791  /**
    792   * Notifies Fenix layer about profiler state changes by broadcasting the new state. This is called
    793   * from native code whenever the profiler starts or stops, ensuring that the Fenix repository is
    794   * always synchronized with the actual native profiler state.
    795   *
    796   * @param isActive true if the profiler is now active, false if it stopped
    797   */
    798  @WrapForJNI
    799  public static void notifyProfilerStateChanged(final boolean isActive) {
    800    if (isActive) {
    801      final ComponentName componentName =
    802          new ComponentName(GeckoAppShell.getApplicationContext(), PROFILER_SERVICE_CLASS_NAME);
    803      final Intent serviceIntent = new Intent();
    804      serviceIntent.setComponent(componentName);
    805      serviceIntent.setAction(PROFILER_SERVICE_ACTION);
    806      ContextCompat.startForegroundService(GeckoAppShell.getApplicationContext(), serviceIntent);
    807    }
    808 
    809    final Intent intent = new Intent(INTENT_PROFILER_STATE_CHANGED);
    810    intent.putExtra("isActive", isActive);
    811    intent.setPackage(GeckoAppShell.getApplicationContext().getPackageName());
    812    final String permission =
    813        GeckoAppShell.getApplicationContext().getPackageName() + ".permission.PROFILER_INTERNAL";
    814    GeckoAppShell.getApplicationContext().sendBroadcast(intent, permission);
    815  }
    816 
    817  @WrapForJNI(dispatchTo = "gecko", stubName = "StartProfiler")
    818  private static native void startProfilerNative(String[] aFilters, String[] aFeaturesArr);
    819 
    820  @WrapForJNI(dispatchTo = "gecko", stubName = "StopProfiler")
    821  private static native void stopProfilerNative(GeckoResult<byte[]> aResult);
    822 
    823  public static void startProfiler(final String[] aFilters, final String[] aFeaturesArr) {
    824    startProfilerNative(aFilters, aFeaturesArr);
    825  }
    826 
    827  public static GeckoResult<byte[]> stopProfiler() {
    828    final GeckoResult<byte[]> result = new GeckoResult<byte[]>();
    829    stopProfilerNative(result);
    830    return result;
    831  }
    832 
    833  /** Returns the device brand and model as a string. */
    834  @WrapForJNI
    835  public static String getDeviceInformation() {
    836    final StringBuilder sb = new StringBuilder(Build.BRAND);
    837    sb.append(" ");
    838    sb.append(Build.MODEL);
    839    return sb.toString();
    840  }
    841 }