Backed out changeset 416c97246140 (bug 1618560) for causing xpcshell failures in test_feature_java.js CLOSED TREE

This commit is contained in:
Cristian Tuns 2022-03-16 14:52:13 -04:00
Родитель 9507d3ff3c
Коммит e8224e0ed6
3 изменённых файлов: 54 добавлений и 284 удалений

Просмотреть файл

@ -7,17 +7,11 @@ 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.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
@ -33,28 +27,13 @@ import org.mozilla.gecko.mozglue.JNIObject;
*
* <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.
*
* <p>Bug 1618560: Currently we only profile the Android UI thread. Ideally we should be able to
* profile multiple threads.
*/
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;
@ -63,8 +42,7 @@ public class GeckoJavaSampler {
// See isProfilerActive for details on the AtomicReference.
@GuardedBy("GeckoJavaSampler.class")
private static final AtomicReference<ScheduledFuture<?>> sSamplingFuture =
new AtomicReference<>();
private static AtomicReference<ScheduledFuture<?>> sSamplingFuture = new AtomicReference<>();
private static final MarkerStorage sMarkerStorage = new MarkerStorage();
@ -108,13 +86,11 @@ public class GeckoJavaSampler {
* (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;
public Sample(final StackTraceElement[] aStack) {
mFrames = new Frame[aStack.length];
mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0;
@ -143,27 +119,6 @@ public class GeckoJavaSampler {
}
}
/** 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.
*/
@ -304,8 +259,6 @@ public class GeckoJavaSampler {
* 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;
@ -313,8 +266,7 @@ public class GeckoJavaSampler {
@GuardedBy("GeckoJavaSampler.class")
private boolean mBufferOverflowed = false;
@GuardedBy("GeckoJavaSampler.class")
private @NonNull final List<Thread> mThreadsToProfile;
private final Thread mMainThread;
@GuardedBy("GeckoJavaSampler.class")
private final Sample[] mSamples;
@ -322,44 +274,34 @@ public class GeckoJavaSampler {
@GuardedBy("GeckoJavaSampler.class")
private int mSamplePos;
public SamplingRunnable(
@NonNull final List<Thread> aThreadsToProfile,
final int aInterval,
final int aSampleCount) {
mThreadsToProfile = aThreadsToProfile;
public SamplingRunnable(final int aInterval, final int aSampleCount) {
// Sanity check of sampling interval.
mInterval = Math.max(1, aInterval);
mSampleCount = aSampleCount;
mSamples = new Sample[mSampleCount];
mSamplePos = 0;
// Find the main thread
mMainThread = Looper.getMainLooper().getThread();
if (mMainThread == null) {
Log.e(LOGTAG, "Main thread not found");
}
}
@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;
}
if (mMainThread == null) {
return;
}
final StackTraceElement[] bt = mMainThread.getStackTrace();
mSamples[mSamplePos] = new Sample(bt);
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;
}
}
}
@ -404,37 +346,6 @@ public class GeckoJavaSampler {
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);
@ -535,8 +446,7 @@ public class GeckoJavaSampler {
}
@WrapForJNI
public static void start(
@NonNull final Object[] aFilters, final int aInterval, final int aEntryCount) {
public static void start(final int aInterval, final int aEntryCount) {
synchronized (GeckoJavaSampler.class) {
if (sSamplingRunnable != null) {
return;
@ -550,8 +460,7 @@ public class GeckoJavaSampler {
// 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);
sSamplingRunnable =
new SamplingRunnable(getThreadsToProfile(aFilters), aInterval, limitedEntryCount);
sSamplingRunnable = new SamplingRunnable(aInterval, limitedEntryCount);
sMarkerStorage.start(limitedEntryCount);
sSamplingScheduler = Executors.newSingleThreadScheduledExecutor();
sSamplingFuture.set(
@ -560,119 +469,6 @@ public class GeckoJavaSampler {
}
}
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);
// 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())) {
return Arrays.asList(activeThreads);
}
final Thread mainThread = Looper.getMainLooper().getThread();
final List<Thread> threadsToProfile = new ArrayList<>();
threads:
for (final Thread thread : activeThreads) {
final String threadName = thread.getName().trim().toLowerCase(Locale.US);
// We can't match against a thread with no name: skip.
if (threadName.isEmpty()) {
continue;
}
// We always want to profile the main thread.
if (thread.equals(mainThread)) {
threadsToProfile.add(thread);
continue;
}
for (final String filter : cleanedFilters) {
// 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)) {
threadsToProfile.add(thread);
continue threads;
}
}
}
return threadsToProfile;
}
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) {

Просмотреть файл

@ -709,7 +709,6 @@ public class GeckoThread extends Thread {
final String startupEnv = "MOZ_PROFILER_STARTUP=";
final String intervalEnv = "MOZ_PROFILER_STARTUP_INTERVAL=";
final String capacityEnv = "MOZ_PROFILER_STARTUP_ENTRIES=";
final String filtersEnv = "MOZ_PROFILER_STARTUP_FILTERS=";
boolean isStartupProfiling = false;
// Putting default values for now, but they can be overwritten.
// Keep these values in sync with profiler defaults.
@ -729,10 +728,6 @@ public class GeckoThread extends Thread {
// and this is: 4 * 2 * 64 * 1024 / 8 = 65536 (~512 kb)
final int minCapacity = 65536;
// Set the default value of no filters - an empty array - which is safer than using null.
// If we find a user provided value, this will be overwritten.
String[] filters = new String[0];
// Looping the environment variable list to check known variable names.
for (final String envItem : env) {
if (envItem == null) {
@ -771,13 +766,11 @@ public class GeckoThread extends Thread {
} catch (final NumberFormatException err) {
// Failed to parse. Do nothing and just use the default value.
}
} else if (envItem.startsWith(filtersEnv)) {
filters = envItem.substring(filtersEnv.length()).split(",");
}
}
if (isStartupProfiling) {
GeckoJavaSampler.start(filters, interval, capacity);
GeckoJavaSampler.start(interval, capacity);
}
}

Просмотреть файл

@ -101,7 +101,6 @@
#if defined(GP_OS_android)
# include "mozilla/java/GeckoJavaSamplerNatives.h"
# include "mozilla/jni/Refs.h"
#endif
#if defined(GP_OS_darwin)
@ -2883,29 +2882,23 @@ struct JavaMarkerWithDetails {
}
};
static void CollectJavaThreadProfileData(
nsTArray<java::GeckoJavaSampler::ThreadInfo::LocalRef>& javaThreads,
ProfileBuffer& aProfileBuffer) {
// Retrieve metadata about the threads.
const auto threadCount = java::GeckoJavaSampler::GetRegisteredThreadCount();
for (int i = 0; i < threadCount; i++) {
javaThreads.AppendElement(
java::GeckoJavaSampler::GetRegisteredThreadInfo(i));
}
static void CollectJavaThreadProfileData(ProfileBuffer& aProfileBuffer) {
// locked_profiler_start uses sample count is 1000 for Java thread.
// This entry size is enough now, but we might have to estimate it
// if we can customize it
// Pass the samples
// FIXME(bug 1618560): We are currently only profiling the Android UI thread.
constexpr ProfilerThreadId threadId = ProfilerThreadId::FromNumber(1);
int sampleId = 0;
while (true) {
const auto threadId = java::GeckoJavaSampler::GetThreadId(sampleId);
// Gets the data from the Android UI thread only.
double sampleTime = java::GeckoJavaSampler::GetSampleTime(sampleId);
if (threadId == 0 || sampleTime == 0.0) {
if (sampleTime == 0.0) {
break;
}
aProfileBuffer.AddThreadIdEntry(ProfilerThreadId::FromNumber(threadId));
aProfileBuffer.AddThreadIdEntry(threadId);
aProfileBuffer.AddEntry(ProfileBufferEntry::Time(sampleTime));
int frameId = 0;
while (true) {
@ -2926,8 +2919,6 @@ static void CollectJavaThreadProfileData(
// Pass the markers now
while (true) {
constexpr auto threadId = ProfilerThreadId::FromNumber(1);
// Gets the data from the Android UI thread only.
java::GeckoJavaSampler::Marker::LocalRef marker =
java::GeckoJavaSampler::PollNextMarker();
@ -3055,11 +3046,8 @@ static void locked_profiler_stream_json_for_this_process(
ProfileChunkedBuffer javaBufferManager(
ProfileChunkedBuffer::ThreadSafety::WithoutMutex, javaChunkManager);
ProfileBuffer javaBuffer(javaBufferManager);
nsTArray<java::GeckoJavaSampler::ThreadInfo::LocalRef> javaThreads;
if (ActivePS::FeatureJava(aLock)) {
CollectJavaThreadProfileData(javaThreads, javaBuffer);
CollectJavaThreadProfileData(javaBuffer);
aProgressLogger.SetLocalProgress(3_pc, "Collected Java thread");
}
#endif
@ -3155,20 +3143,23 @@ static void locked_profiler_stream_json_for_this_process(
#if defined(GP_OS_android)
if (ActivePS::FeatureJava(aLock)) {
for (java::GeckoJavaSampler::ThreadInfo::LocalRef& threadInfo :
javaThreads) {
ProfiledThreadData threadData(ThreadRegistrationInfo{
threadInfo->GetName()->ToCString().BeginReading(),
ProfilerThreadId::FromNumber(threadInfo->GetId()), false,
CorePS::ProcessStartTime()});
threadData.StreamJSON(
javaBuffer, nullptr, aWriter, CorePS::ProcessName(aLock),
CorePS::ETLDplus1(aLock), CorePS::ProcessStartTime(), aSinceTime,
ActivePS::FeatureJSTracer(aLock), nullptr,
aProgressLogger.CreateSubLoggerTo("Streaming Java thread...", 96_pc,
"Streamed Java thread"));
}
// Set the thread id of the Android UI thread to be 0.
// We are profiling the Android UI thread twice: Both from the C++ side
// (as a regular C++ profiled thread with the name "AndroidUI"), and from
// the Java side. The thread's actual ID is mozilla::jni::GetUIThreadId(),
// but since we're using that ID for the C++ side, we need to pick another
// tid that doesn't conflict with it for the Java side. So we just use 0.
// Once we add support for profiling of other java threads, we'll have to
// get their thread id and name via JNI.
ProfiledThreadData profiledThreadData(ThreadRegistrationInfo{
"AndroidUI (JVM)", ProfilerThreadId::FromNumber(1), false,
CorePS::ProcessStartTime()});
profiledThreadData.StreamJSON(
javaBuffer, nullptr, aWriter, CorePS::ProcessName(aLock),
CorePS::ETLDplus1(aLock), CorePS::ProcessStartTime(), aSinceTime,
ActivePS::FeatureJSTracer(aLock), nullptr,
aProgressLogger.CreateSubLoggerTo("Streaming Java thread...", 96_pc,
"Streamed Java thread"));
} else {
aProgressLogger.SetLocalProgress(96_pc, "No Java thread");
}
@ -5602,21 +5593,11 @@ static void locked_profiler_start(PSLockRef aLock, PowerOfTwo32 aCapacity,
if (javaInterval < 1) {
javaInterval = 1;
}
JNIEnv* env = jni::GetEnvForThread();
const auto& filters = ActivePS::Filters(aLock);
jni::ObjectArray::LocalRef javaFilters =
jni::ObjectArray::New<jni::String>(filters.length());
for (size_t i = 0; i < filters.length(); i++) {
javaFilters->SetElement(i, jni::StringParam(filters[i].data(), env));
}
// Send the interval-relative entry count, but we have 100000 hard cap in
// the java code, it can't be more than that.
java::GeckoJavaSampler::Start(
javaFilters, javaInterval,
std::round((double)(capacity.Value()) * interval /
(double)(javaInterval)));
javaInterval, std::round((double)(capacity.Value()) * interval /
(double)(javaInterval)));
}
#endif