diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java | 807 |
1 files changed, 807 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java new file mode 100644 index 0000000000..0e18cec515 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java @@ -0,0 +1,807 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import android.os.Build; +import android.os.Looper; +import android.os.Process; +import android.os.SystemClock; +import android.util.Log; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.mozglue.JNIObject; +import org.mozilla.geckoview.GeckoResult; + +/** + * Takes samples and adds markers for Java threads for the Gecko profiler. + * + * <p>This class is thread safe because it uses synchronized on accesses to its mutable state. One + * exception is {@link #isProfilerActive()}: see the javadoc for details. + */ +public class GeckoJavaSampler { + private static final String LOGTAG = "GeckoJavaSampler"; + + /** + * The thread ID to use for the main thread instead of its true thread ID. + * + * <p>The main thread is sampled twice: once for native code and once on the JVM. The native + * version uses the thread's id so we replace it to avoid a collision. We use this thread ID + * because it's unlikely any other thread currently has it. We can't use 0 because 0 is considered + * "unspecified" in native code: + * https://searchfox.org/mozilla-central/rev/d4ebb53e719b913afdbcf7c00e162f0e96574701/mozglue/baseprofiler/public/BaseProfilerUtils.h#194 + */ + private static final long REPLACEMENT_MAIN_THREAD_ID = 1; + + /** + * The thread name to use for the main thread instead of its true thread name. The name is "main", + * which is ambiguous with the JS main thread, so we rename it to match the C++ replacement. We + * expect our code to later add a suffix to avoid a collision with the C++ thread name. See {@link + * #REPLACEMENT_MAIN_THREAD_ID} for related details. + */ + private static final String REPLACEMENT_MAIN_THREAD_NAME = "AndroidUI"; + + @GuardedBy("GeckoJavaSampler.class") + private static SamplingRunnable sSamplingRunnable; + + @GuardedBy("GeckoJavaSampler.class") + private static ScheduledExecutorService sSamplingScheduler; + + // See isProfilerActive for details on the AtomicReference. + @GuardedBy("GeckoJavaSampler.class") + private static final AtomicReference<ScheduledFuture<?>> sSamplingFuture = + new AtomicReference<>(); + + private static final MarkerStorage sMarkerStorage = new MarkerStorage(); + + /** + * Returns true if profiler is running and unpaused at the moment which means it's allowed to add + * a marker. + * + * <p>Thread policy: we want this method to be inexpensive (i.e. non-blocking) because we want to + * be able to use it in performance-sensitive code. That's why we rely on an AtomicReference. If + * this requirement didn't exist, the AtomicReference could be removed because the class thread + * policy is to call synchronized on mutable state access. + */ + public static boolean isProfilerActive() { + // This value will only be present if the profiler is started and not paused. + return sSamplingFuture.get() != null; + } + + // Use the same timer primitive as the profiler + // to get a perfect sample syncing. + @WrapForJNI + private static native double getProfilerTime(); + + /** Try to get the profiler time. Returns null if profiler is not running. */ + public static @Nullable Double tryToGetProfilerTime() { + if (!isProfilerActive()) { + // Android profiler hasn't started yet. + return null; + } + if (!GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) { + // getProfilerTime is not available yet; either libs are not loaded, + // or profiling hasn't started on the Gecko side yet + return null; + } + + return getProfilerTime(); + } + + /** + * A data container for a profiler sample. This class is effectively immutable (i.e. technically + * mutable but never mutated after construction) so is thread safe *if it is safely published* + * (see Java Concurrency in Practice, 2nd Ed., Section 3.5.3 for safe publication idioms). + */ + private static class Sample { + public final long mThreadId; + public final Frame[] mFrames; + public final double mTime; + public final long mJavaTime; // non-zero if Android system time is used + + public Sample(final long aThreadId, final StackTraceElement[] aStack) { + mThreadId = aThreadId; + mFrames = new Frame[aStack.length]; + mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0; + + // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded, + // or profiling hasn't started on the Gecko side yet + mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0; + + for (int i = 0; i < aStack.length; i++) { + mFrames[aStack.length - 1 - i] = + new Frame(aStack[i].getMethodName(), aStack[i].getClassName()); + } + } + } + + /** + * A container for the metadata around a call in a stack. This class is thread safe by being + * immutable. + */ + private static class Frame { + public final String methodName; + public final String className; + + private Frame(final String methodName, final String className) { + this.methodName = methodName; + this.className = className; + } + } + + /** A data container for thread metadata. */ + private static class ThreadInfo { + private final long mId; + private final String mName; + + public ThreadInfo(final long mId, final String mName) { + this.mId = mId; + this.mName = mName; + } + + @WrapForJNI + public long getId() { + return mId; + } + + @WrapForJNI + public String getName() { + return mName; + } + } + + /** + * A data container for metadata around a marker. This class is thread safe by being immutable. + */ + private static class Marker extends JNIObject { + /** The id of the thread this marker was captured on. */ + private final long mThreadId; + + /** Name of the marker */ + private final String mMarkerName; + + /** Either start time for the duration markers or time for a point-in-time markers. */ + private final double mTime; + + /** + * A fallback field of {@link #mTime} but it only exists when {@link #getProfilerTime()} is + * failed. It is non-zero if Android time is used. + */ + private final long mJavaTime; + + /** End time for the duration markers. It's zero for point-in-time markers. */ + private final double mEndTime; + + /** + * A fallback field of {@link #mEndTime} but it only exists when {@link #getProfilerTime()} is + * failed. It is non-zero if Android time is used. + */ + private final long mEndJavaTime; + + /** A nullable additional information field for the marker. */ + private @Nullable final String mText; + + /** + * Constructor for the Marker class. It initializes different kinds of markers depending on the + * parameters. Here are some combinations to create different kinds of markers: + * + * <p>If you want to create a marker that points a single point in time: <code> + * new Marker("name", null, null, null)</code> to implicitly get the time when this marker is + * added, or <code>new Marker("name", null, endTime, null)</code> to use an explicit time as an + * end time retrieved from {@link #tryToGetProfilerTime()}. + * + * <p>If you want to create a marker that has a start and end time: <code> + * new Marker("name", startTime, null, null)</code> to implicitly get the end time when this + * marker is added, or <code>new Marker("name", startTime, endTime, null)</code> to explicitly + * give the marker start and end time retrieved from {@link #tryToGetProfilerTime()}. + * + * <p>Last parameter is optional and can be given with any combination. This gives users the + * ability to add more context into a marker. + * + * @param aThreadId The id of the thread this marker was captured on. + * @param aMarkerName Identifier of the marker as a string. + * @param aStartTime Start time as Double. It can be null if you want to mark a point of time. + * @param aEndTime End time as Double. If it's null, this function implicitly gets the end time. + * @param aText An optional string field for more information about the marker. + */ + public Marker( + final long aThreadId, + @NonNull final String aMarkerName, + @Nullable final Double aStartTime, + @Nullable final Double aEndTime, + @Nullable final String aText) { + mThreadId = getAdjustedThreadId(aThreadId); + mMarkerName = aMarkerName; + mText = aText; + + if (aStartTime != null) { + // Start time is provided. This is an interval marker. + mTime = aStartTime; + mJavaTime = 0; + if (aEndTime != null) { + // End time is also provided. + mEndTime = aEndTime; + mEndJavaTime = 0; + } else { + // End time is not provided. Get the profiler time now and use it. + mEndTime = + GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0; + + // if mEndTime == 0, getProfilerTime is not available yet; either libs are not loaded, + // or profiling hasn't started on the Gecko side yet + mEndJavaTime = mEndTime == 0.0d ? SystemClock.elapsedRealtime() : 0; + } + + } else { + // Start time is not provided. This is point-in-time marker. + mEndTime = 0; + mEndJavaTime = 0; + + if (aEndTime != null) { + // End time is also provided. Use that to point the time. + mTime = aEndTime; + mJavaTime = 0; + } else { + mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0; + + // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded, + // or profiling hasn't started on the Gecko side yet + mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0; + } + } + } + + @WrapForJNI + @Override // JNIObject + protected native void disposeNative(); + + @WrapForJNI + public double getStartTime() { + if (mJavaTime != 0) { + return (mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime(); + } + return mTime; + } + + @WrapForJNI + public double getEndTime() { + if (mEndJavaTime != 0) { + return (mEndJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime(); + } + return mEndTime; + } + + @WrapForJNI + public long getThreadId() { + return mThreadId; + } + + @WrapForJNI + public @NonNull String getMarkerName() { + return mMarkerName; + } + + @WrapForJNI + public @Nullable String getMarkerText() { + return mText; + } + } + + /** + * Public method to add a new marker to Gecko profiler. This can be used to add a marker *inside* + * the geckoview code, but ideally ProfilerController methods should be used instead. + * + * @see Marker#Marker(long, String, Double, Double, String) for information about the parameter + * options. + */ + public static void addMarker( + @NonNull final String aMarkerName, + @Nullable final Double aStartTime, + @Nullable final Double aEndTime, + @Nullable final String aText) { + sMarkerStorage.addMarker(aMarkerName, aStartTime, aEndTime, aText); + } + + /** + * A routine to store profiler samples. This class is thread safe because it synchronizes access + * to its mutable state. + */ + private static class SamplingRunnable implements Runnable { + private final long mMainThreadId = Looper.getMainLooper().getThread().getId(); + + // Sampling interval that is used by start and unpause + public final int mInterval; + private final int mSampleCount; + + @GuardedBy("GeckoJavaSampler.class") + private boolean mBufferOverflowed = false; + + @GuardedBy("GeckoJavaSampler.class") + private @NonNull final List<Thread> mThreadsToProfile; + + @GuardedBy("GeckoJavaSampler.class") + private final Sample[] mSamples; + + @GuardedBy("GeckoJavaSampler.class") + private int mSamplePos; + + public SamplingRunnable( + @NonNull final List<Thread> aThreadsToProfile, + final int aInterval, + final int aSampleCount) { + mThreadsToProfile = aThreadsToProfile; + // Sanity check of sampling interval. + mInterval = Math.max(1, aInterval); + mSampleCount = aSampleCount; + mSamples = new Sample[mSampleCount]; + mSamplePos = 0; + } + + @Override + public void run() { + synchronized (GeckoJavaSampler.class) { + // To minimize allocation in the critical section, we use a traditional for loop instead of + // a for each (i.e. `elem : coll`) loop because that allocates an iterator. + // + // We won't capture threads that are started during profiling because we iterate through an + // unchanging list of threads (bug 1759550). + for (int i = 0; i < mThreadsToProfile.size(); i++) { + final Thread thread = mThreadsToProfile.get(i); + + // getStackTrace will return an empty trace if the thread is not alive: we call continue + // to avoid wasting space in the buffer for an empty sample. + final StackTraceElement[] stackTrace = thread.getStackTrace(); + if (stackTrace.length == 0) { + continue; + } + + mSamples[mSamplePos] = new Sample(thread.getId(), stackTrace); + mSamplePos += 1; + if (mSamplePos == mSampleCount) { + // Sample array is full now, go back to start of + // the array and override old samples + mSamplePos = 0; + mBufferOverflowed = true; + } + } + } + } + + private Sample getSample(final int aSampleId) { + synchronized (GeckoJavaSampler.class) { + if (aSampleId >= mSampleCount) { + // Return early because there is no more sample left. + return null; + } + + int samplePos = aSampleId; + if (mBufferOverflowed) { + // This is a circular buffer and the buffer is overflowed. Start + // of the buffer is mSamplePos now. Calculate the real index. + samplePos = (samplePos + mSamplePos) % mSampleCount; + } + + // Since the array elements are initialized to null, it will return + // null whenever we access to an element that's not been written yet. + // We want it to return null in that case, so it's okay. + return mSamples[samplePos]; + } + } + } + + /** + * Returns the sample with the given sample ID. + * + * <p>Thread safety code smell: this method call is synchronized but this class returns a + * reference to an effectively immutable object so that the reference is accessible after + * synchronization ends. It's unclear if this is thread safe. However, this is safe with the + * current callers (because they are all synchronized and don't leak the Sample) so we don't + * investigate it further. + */ + private static synchronized Sample getSample(final int aSampleId) { + return sSamplingRunnable.getSample(aSampleId); + } + + @WrapForJNI + public static Marker pollNextMarker() { + return sMarkerStorage.pollNextMarker(); + } + + @WrapForJNI + public static synchronized int getRegisteredThreadCount() { + return sSamplingRunnable.mThreadsToProfile.size(); + } + + @WrapForJNI + public static synchronized ThreadInfo getRegisteredThreadInfo(final int aIndex) { + final Thread thread = sSamplingRunnable.mThreadsToProfile.get(aIndex); + + // See REPLACEMENT_MAIN_THREAD_NAME for why we do this. + String adjustedThreadName = + thread.getId() == sSamplingRunnable.mMainThreadId + ? REPLACEMENT_MAIN_THREAD_NAME + : thread.getName(); + + // To distinguish JVM threads from native threads, we append a JVM-specific suffix. + adjustedThreadName += " (JVM)"; + return new ThreadInfo(getAdjustedThreadId(thread.getId()), adjustedThreadName); + } + + @WrapForJNI + public static synchronized long getThreadId(final int aSampleId) { + final Sample sample = getSample(aSampleId); + return getAdjustedThreadId(sample != null ? sample.mThreadId : 0); + } + + private static synchronized long getAdjustedThreadId(final long threadId) { + // See REPLACEMENT_MAIN_THREAD_ID for why we do this. + return threadId == sSamplingRunnable.mMainThreadId ? REPLACEMENT_MAIN_THREAD_ID : threadId; + } + + @WrapForJNI + public static synchronized double getSampleTime(final int aSampleId) { + final Sample sample = getSample(aSampleId); + if (sample != null) { + if (sample.mJavaTime != 0) { + return (sample.mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime(); + } + return sample.mTime; + } + return 0; + } + + @WrapForJNI + public static synchronized String getFrameName(final int aSampleId, final int aFrameId) { + final Sample sample = getSample(aSampleId); + if (sample != null && aFrameId < sample.mFrames.length) { + final Frame frame = sample.mFrames[aFrameId]; + if (frame == null) { + return null; + } + return frame.className + "." + frame.methodName + "()"; + } + return null; + } + + /** + * A start/stop-aware container for storing profiler markers. + * + * <p>This class is thread safe: see {@link #mMarkers} and other member variables for the + * threading policy. Start/stop are guaranteed to execute in the order they are called but other + * methods do not have such ordering guarantees. + */ + private static class MarkerStorage { + /** + * The underlying storage for the markers. This field maintains thread safety without using + * synchronized everywhere by: + * <li>- using volatile to allow non-blocking reads + * <li>- leveraging a thread safe collection when accessing the underlying data + * <li>- looping until success for compound read-write operations + */ + private volatile Queue<Marker> mMarkers; + + /** + * The thread ids of the threads we're profiling. This field maintains thread safety by writing + * a read-only value to this volatile field before concurrency begins and only reading it during + * concurrent sections. + */ + private volatile Set<Long> mProfiledThreadIds = Collections.emptySet(); + + MarkerStorage() {} + + public synchronized void start(final int aMarkerCount, final List<Thread> aProfiledThreads) { + if (this.mMarkers != null) { + return; + } + this.mMarkers = new LinkedBlockingQueue<>(aMarkerCount); + + final Set<Long> profiledThreadIds = new HashSet<>(aProfiledThreads.size()); + for (final Thread thread : aProfiledThreads) { + profiledThreadIds.add(thread.getId()); + } + + // We use a temporary collection, rather than mutating the collection within the member + // variable, to ensure the collection is fully written before the state is made available to + // all threads via the volatile write into the member variable. This collection must be + // read-only for it to remain thread safe. + mProfiledThreadIds = Collections.unmodifiableSet(profiledThreadIds); + } + + public synchronized void stop() { + if (this.mMarkers == null) { + return; + } + this.mMarkers = null; + mProfiledThreadIds = Collections.emptySet(); + } + + private void addMarker( + @NonNull final String aMarkerName, + @Nullable final Double aStartTime, + @Nullable final Double aEndTime, + @Nullable final String aText) { + final Queue<Marker> markersQueue = this.mMarkers; + if (markersQueue == null) { + // Profiler is not active. + return; + } + + final long threadId = Thread.currentThread().getId(); + if (!mProfiledThreadIds.contains(threadId)) { + return; + } + + final Marker newMarker = new Marker(threadId, aMarkerName, aStartTime, aEndTime, aText); + boolean successful = markersQueue.offer(newMarker); + while (!successful) { + // Marker storage is full, remove the head and add again. + markersQueue.poll(); + successful = markersQueue.offer(newMarker); + } + } + + private Marker pollNextMarker() { + final Queue<Marker> markersQueue = this.mMarkers; + if (markersQueue == null) { + // Profiler is not active. + return null; + } + // Retrieve and return the head of this queue. + // Returns null if the queue is empty. + return markersQueue.poll(); + } + } + + @WrapForJNI + public static void start( + @NonNull final Object[] aFilters, final int aInterval, final int aEntryCount) { + synchronized (GeckoJavaSampler.class) { + if (sSamplingRunnable != null) { + return; + } + + final ScheduledFuture<?> future = sSamplingFuture.get(); + if (future != null && !future.isDone()) { + return; + } + + Log.i(LOGTAG, "Profiler starting. Calling thread: " + Thread.currentThread().getName()); + + // Setting a limit of 120000 (2 mins with 1ms interval) for samples and markers for now + // to make sure we are not allocating too much. + final int limitedEntryCount = Math.min(aEntryCount, 120000); + + final List<Thread> threadsToProfile = getThreadsToProfile(aFilters); + if (threadsToProfile.size() < 1) { + throw new IllegalStateException("Expected >= 1 thread to profile (main thread)."); + } + Log.i(LOGTAG, "Number of threads to profile: " + threadsToProfile.size()); + + sSamplingRunnable = new SamplingRunnable(threadsToProfile, aInterval, limitedEntryCount); + sMarkerStorage.start(limitedEntryCount, threadsToProfile); + sSamplingScheduler = Executors.newSingleThreadScheduledExecutor(); + sSamplingFuture.set( + sSamplingScheduler.scheduleAtFixedRate( + sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS)); + } + } + + private static @NonNull List<Thread> getThreadsToProfile(final Object[] aFilters) { + // Clean up filters. + final List<String> cleanedFilters = new ArrayList<>(); + for (final Object rawFilter : aFilters) { + // aFilters is a String[] but jni can only accept Object[] so we're forced to cast. + // + // We could pass the lowercased filters from native code but it may not handle lowercasing the + // same way Java does so we lower case here so it's consistent later when we lower case the + // thread name and compare against it. + final String filter = ((String) rawFilter).trim().toLowerCase(Locale.US); + + // If the filter is empty, it's not meaningful: skip. + if (filter.isEmpty()) { + continue; + } + + cleanedFilters.add(filter); + } + + final ThreadGroup rootThreadGroup = getRootThreadGroup(); + final Thread[] activeThreads = getActiveThreads(rootThreadGroup); + final Thread mainThread = Looper.getMainLooper().getThread(); + + // We model these catch-all filters after the C++ code (which we should eventually deduplicate): + // https://searchfox.org/mozilla-central/rev/b0779bcc485dc1c04334dfb9ea024cbfff7b961a/tools/profiler/core/platform.cpp#778-801 + if (cleanedFilters.contains("*") || doAnyFiltersMatchPid(cleanedFilters, Process.myPid())) { + final List<Thread> activeThreadList = new ArrayList<>(); + Collections.addAll(activeThreadList, activeThreads); + if (!activeThreadList.contains(mainThread)) { + activeThreadList.add(mainThread); // see below for why this is necessary. + } + return activeThreadList; + } + + // We always want to profile the main thread. We're not certain getActiveThreads returns + // all active threads since we've observed that getActiveThreads doesn't include the main thread + // during xpcshell tests even though it's alive (bug 1760716). We intentionally don't rely on + // that method to add the main thread here. + final List<Thread> threadsToProfile = new ArrayList<>(); + threadsToProfile.add(mainThread); + + for (final Thread thread : activeThreads) { + if (shouldProfileThread(thread, cleanedFilters, mainThread)) { + threadsToProfile.add(thread); + } + } + return threadsToProfile; + } + + private static boolean shouldProfileThread( + final Thread aThread, final List<String> aFilters, final Thread aMainThread) { + final String threadName = aThread.getName().trim().toLowerCase(Locale.US); + if (threadName.isEmpty()) { + return false; // We can't match against a thread with no name: skip. + } + + if (aThread.equals(aMainThread)) { + return false; // We've already added the main thread outside of this method. + } + + for (final String filter : aFilters) { + // In order to generically support thread pools with thread names like "arch_disk_io_0" (the + // kotlin IO dispatcher), we check if the filter is inside the thread name (e.g. a filter of + // "io" will match all of the threads in that pool) rather than an equality check. + if (threadName.contains(filter)) { + return true; + } + } + + return false; + } + + private static boolean doAnyFiltersMatchPid( + @NonNull final List<String> aFilters, final long aPid) { + final String prefix = "pid:"; + for (final String filter : aFilters) { + if (!filter.startsWith(prefix)) { + continue; + } + + try { + final long filterPid = Long.parseLong(filter.substring(prefix.length())); + if (filterPid == aPid) { + return true; + } + } catch (final NumberFormatException e) { + /* do nothing. */ + } + } + + return false; + } + + private static @NonNull Thread[] getActiveThreads(final @NonNull ThreadGroup rootThreadGroup) { + // We need the root thread group to get all of the active threads because of how + // ThreadGroup.enumerate works. + // + // ThreadGroup.enumerate is inherently racey so we loop until we capture all of the active + // threads. We can only detect if we didn't capture all of the threads if the number of threads + // found (the value returned by enumerate) is smaller than the array we're capturing them in. + // Therefore, we make the array slightly larger than the known number of threads. + Thread[] allThreads; + int threadsFound; + do { + allThreads = new Thread[rootThreadGroup.activeCount() + 15]; + threadsFound = rootThreadGroup.enumerate(allThreads, /* recurse */ true); + } while (threadsFound >= allThreads.length); + + // There will be more indices in the array than threads and these will be set to null. We remove + // the null values to minimize bugs. + return Arrays.copyOfRange(allThreads, 0, threadsFound); + } + + private static @NonNull ThreadGroup getRootThreadGroup() { + // Assert non-null: getThreadGroup only returns null for dead threads but the current thread + // can't be dead. + ThreadGroup parentGroup = Objects.requireNonNull(Thread.currentThread().getThreadGroup()); + + ThreadGroup group = null; + while (parentGroup != null) { + group = parentGroup; + parentGroup = group.getParent(); + } + return group; + } + + @WrapForJNI + public static void pauseSampling() { + synchronized (GeckoJavaSampler.class) { + final ScheduledFuture<?> future = sSamplingFuture.getAndSet(null); + future.cancel(false /* mayInterruptIfRunning */); + } + } + + @WrapForJNI + public static void unpauseSampling() { + synchronized (GeckoJavaSampler.class) { + if (sSamplingFuture.get() != null) { + return; + } + sSamplingFuture.set( + sSamplingScheduler.scheduleAtFixedRate( + sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS)); + } + } + + @WrapForJNI + public static void stop() { + synchronized (GeckoJavaSampler.class) { + if (sSamplingRunnable == null) { + return; + } + + Log.i( + LOGTAG, + "Profiler stopping. Sample array position: " + + sSamplingRunnable.mSamplePos + + ". Overflowed? " + + sSamplingRunnable.mBufferOverflowed); + + try { + sSamplingScheduler.shutdown(); + // 1s is enough to wait shutdown. + sSamplingScheduler.awaitTermination(1000, TimeUnit.MILLISECONDS); + } catch (final InterruptedException e) { + Log.e(LOGTAG, "Sampling scheduler isn't terminated. Last sampling data might be broken."); + sSamplingScheduler.shutdownNow(); + } + sSamplingScheduler = null; + sSamplingRunnable = null; + sSamplingFuture.set(null); + sMarkerStorage.stop(); + } + } + + @WrapForJNI(dispatchTo = "gecko", stubName = "StartProfiler") + private static native void startProfilerNative(String[] aFilters, String[] aFeaturesArr); + + @WrapForJNI(dispatchTo = "gecko", stubName = "StopProfiler") + private static native void stopProfilerNative(GeckoResult<byte[]> aResult); + + public static void startProfiler(final String[] aFilters, final String[] aFeaturesArr) { + startProfilerNative(aFilters, aFeaturesArr); + } + + public static GeckoResult<byte[]> stopProfiler() { + final GeckoResult<byte[]> result = new GeckoResult<byte[]>(); + stopProfilerNative(result); + return result; + } + + /** Returns the device brand and model as a string. */ + @WrapForJNI + public static String getDeviceInformation() { + final StringBuilder sb = new StringBuilder(Build.BRAND); + sb.append(" "); + sb.append(Build.MODEL); + return sb.toString(); + } +} |