/* -*- 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. * *
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. * *
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 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:
*
* If you want to create a marker that points a single point in time: If you want to create a marker that has a start and end time: 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 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.
*
* 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:
*
* new Marker("name", null, null, null)
to implicitly get the time when this marker is
* added, or new Marker("name", null, endTime, null)
to use an explicit time as an
* end time retrieved from {@link #tryToGetProfilerTime()}.
*
*
* new Marker("name", startTime, null, null)
to implicitly get the end time when this
* marker is added, or new Marker("name", startTime, endTime, null)
to explicitly
* give the marker start and end time retrieved from {@link #tryToGetProfilerTime()}.
*
*