summaryrefslogtreecommitdiffstats
path: root/tools/profiler/core
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
commita90a5cba08fdf6c0ceb95101c275108a152a3aed (patch)
tree532507288f3defd7f4dcf1af49698bcb76034855 /tools/profiler/core
parentAdding debian version 126.0.1-1. (diff)
downloadfirefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.tar.xz
firefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tools/profiler/core')
-rw-r--r--tools/profiler/core/memory_hooks.cpp7
-rw-r--r--tools/profiler/core/memory_hooks.h1
-rw-r--r--tools/profiler/core/platform.cpp320
3 files changed, 280 insertions, 48 deletions
diff --git a/tools/profiler/core/memory_hooks.cpp b/tools/profiler/core/memory_hooks.cpp
index e13a109aab..5167d15d46 100644
--- a/tools/profiler/core/memory_hooks.cpp
+++ b/tools/profiler/core/memory_hooks.cpp
@@ -584,6 +584,7 @@ BaseProfilerCount* install_memory_hooks() {
ThreadIntercept::Init();
} else {
sCounter->Clear();
+ sCounter->Register();
}
jemalloc_replace_dynamic(replace_init);
return sCounter;
@@ -635,4 +636,10 @@ void disable_native_allocations() {
}
}
+void unregister_memory_counter() {
+ if (sCounter) {
+ sCounter->Unregister();
+ }
+}
+
} // namespace mozilla::profiler
diff --git a/tools/profiler/core/memory_hooks.h b/tools/profiler/core/memory_hooks.h
index a6ace771dd..4f59b21136 100644
--- a/tools/profiler/core/memory_hooks.h
+++ b/tools/profiler/core/memory_hooks.h
@@ -17,6 +17,7 @@ BaseProfilerCount* install_memory_hooks();
void remove_memory_hooks();
void enable_native_allocations();
void disable_native_allocations();
+void unregister_memory_counter();
} // namespace profiler
} // namespace mozilla
diff --git a/tools/profiler/core/platform.cpp b/tools/profiler/core/platform.cpp
index 6b7e318f80..4641587a9a 100644
--- a/tools/profiler/core/platform.cpp
+++ b/tools/profiler/core/platform.cpp
@@ -114,18 +114,32 @@
#include <string_view>
#include <type_traits>
+// To simplify other code in this file, define a helper definition to avoid
+// repeating the same preprocessor checks.
+
// The signals that we use to control the profiler conflict with the signals
-// used to control the code coverage tool. Therefore, if coverage is enabled, we
-// need to disable our own signal handling mechanisms.
+// used to control the code coverage tool. Therefore, if coverage is enabled,
+// we need to disable our own signal handling mechanisms.
#ifndef MOZ_CODE_COVERAGE
# ifdef XP_WIN
// TODO: Add support for windows "signal"-like behaviour. See Bug 1867328.
+# elif defined(GP_OS_darwin) || defined(GP_OS_linux) || \
+ defined(GP_OS_android) || defined(GP_OS_freebsd)
+// Specify the specific platforms that we want to support
+# define GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL 1
# else
-# include <signal.h>
-# include <unistd.h>
+// No support on this unknown platform!
# endif
#endif
+// We need some extra includes if we're supporting async posix signals
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
+# include <signal.h>
+# include <fcntl.h>
+# include <unistd.h>
+# include <errno.h>
+#endif
+
#if defined(GP_OS_android)
# include "JavaExceptions.h"
# include "mozilla/java/GeckoJavaSamplerNatives.h"
@@ -250,11 +264,28 @@ ProfileChunkedBuffer& profiler_get_core_buffer() {
return sProfileChunkedBuffer;
}
-mozilla::Atomic<int, mozilla::MemoryOrdering::Relaxed> gSkipSampling;
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
+// Control character to start the profiler ('g' for "go"!)
+static const char sAsyncSignalControlCharStart = 'g';
+// Control character to stop the profiler ('s' for "stop"!)
+static const char sAsyncSignalControlCharStop = 's';
+
+// This is a file descriptor that is the "write" end of the POSIX pipe that we
+// use to start the profiler. It is written to in profiler_start_signal_handler
+// and read from in AsyncSignalControlThread
+static mozilla::Atomic<int, mozilla::MemoryOrdering::Relaxed>
+ sAsyncSignalControlWriteFd(-1);
// Atomic flag to stop the profiler from within the sampling loop
mozilla::Atomic<bool, mozilla::MemoryOrdering::Relaxed> gStopAndDumpFromSignal(
false);
+#endif
+
+// Forward declare the function to call when we need to dump + stop from within
+// the async control thread
+void profiler_dump_and_stop();
+
+mozilla::Atomic<int, mozilla::MemoryOrdering::Relaxed> gSkipSampling;
#if defined(GP_OS_android)
class GeckoJavaSampler
@@ -380,6 +411,7 @@ static uint32_t AvailableFeatures() {
}
#else
// The memory hooks are not available.
+ ProfilerFeature::ClearMemory(features);
ProfilerFeature::ClearNativeAllocations(features);
#endif
@@ -506,6 +538,143 @@ static constexpr size_t MAX_JS_FRAMES =
using JsFrame = mozilla::profiler::ThreadRegistrationData::JsFrame;
using JsFrameBuffer = mozilla::profiler::ThreadRegistrationData::JsFrameBuffer;
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
+// Forward declare this, so we can call it from the constructor.
+static void* AsyncSignalControlThreadEntry(void* aArg);
+
+// Define our platform specific async (posix) signal control thread here.
+class AsyncSignalControlThread {
+ public:
+ AsyncSignalControlThread() : mThread() {
+ // Try to open a pipe for this to communicate with. If we can't do this,
+ // then we give up and return, as there's no point continuing without
+ // being able to communicate
+ int pipeFds[2];
+ if (pipe(pipeFds)) {
+ LOG("Profiler AsyncSignalControlThread failed to create a pipe.");
+ return;
+ }
+
+ // Close this pipe on calls to exec().
+ fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC);
+ fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC);
+
+ // Write the reading side to mFd, and the writing side to the global atomic
+ mFd = pipeFds[0];
+ sAsyncSignalControlWriteFd = pipeFds[1];
+
+ // We don't really care about stack size, as it should be minimal, so
+ // leave the pthread attributes as a nullptr, i.e. choose the default.
+ pthread_attr_t* attr_ptr = nullptr;
+ if (pthread_create(&mThread, attr_ptr, AsyncSignalControlThreadEntry,
+ this) != 0) {
+ MOZ_CRASH("pthread_create failed");
+ }
+ };
+
+ ~AsyncSignalControlThread() {
+ // Derived from code in nsDumpUtils.cpp. Comment reproduced here for
+ // poisterity: Close sAsyncSignalControlWriteFd /after/ setting the fd to
+ // -1. Otherwise we have the (admittedly far-fetched) race where we
+ //
+ // 1) close sAsyncSignalControlWriteFd
+ // 2) open a new fd with the same number as sAsyncSignalControlWriteFd
+ // had.
+ // 3) receive a signal, then write to the fd.
+ int asyncSignalControlWriteFd = sAsyncSignalControlWriteFd.exchange(-1);
+ // This will unblock the "read" in StartWatching.
+ close(asyncSignalControlWriteFd);
+ // Finally, exit the thread.
+ pthread_join(mThread, nullptr);
+ };
+
+ void Watch() {
+ char msg[1];
+ ssize_t nread;
+ while (true) {
+ // Try reading from the pipe. This will block until something is written:
+ nread = read(mFd, msg, sizeof(msg));
+
+ if (nread == -1 && errno == EINTR) {
+ // nread == -1 and errno == EINTR means that `read` was interrupted
+ // by a signal before reading any data. This is likely because the
+ // profiling thread interrupted us (with SIGPROF). We can safely ignore
+ // this and "go around" the loop again to try and read.
+ continue;
+ }
+
+ if (nread == -1 && errno != EINTR) {
+ // nread == -1 and errno != EINTR means that `read` has failed in some
+ // way that we can't recover from. In this case, all we can do is give
+ // up, and quit the watcher, as the pipe is likely broken.
+ LOG("Error (%d) when reading in AsyncSignalControlThread", errno);
+ return;
+ }
+
+ if (nread == 0) {
+ // nread == 0 signals that the other end of the pipe has been cleanly
+ // closed. Close our end, and exit the reading loop.
+ close(mFd);
+ return;
+ }
+
+ // If we reach here, nread != 0 and nread != -1. This means that we
+ // should have read at least one byte, which should be a control byte
+ // for the profiler.
+ // It *might* happen that `read` is interrupted by the sampler thread
+ // after successfully reading. If this occurs, read returns the number
+ // of bytes read. As anything other than 1 is wrong for us, we can
+ // always assume that we can read whatever `read` read.
+ MOZ_RELEASE_ASSERT(nread == 1);
+
+ if (msg[0] == sAsyncSignalControlCharStart) {
+ // Start the profiler here directly, as we're on a background thread.
+ // set of preferences, configuration of them is TODO, see Bug 1866007
+ uint32_t features = ProfilerFeature::JS | ProfilerFeature::StackWalk |
+ ProfilerFeature::CPUUtilization;
+ // as we often don't know what threads we'll care about, tell the
+ // profiler to profile all threads.
+ const char* filters[] = {"*"};
+ profiler_start(PROFILER_DEFAULT_SIGHANDLE_ENTRIES,
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+ } else if (msg[0] == sAsyncSignalControlCharStop) {
+ // Check to see whether the profiler is even running before trying to
+ // stop the profiler. Most other methods of stopping the profiler (i.e.
+ // those through nsProfiler etc) already know whether or not the
+ // profiler is running, so don't try and stop it if it's already
+ // running. Signal-stopping doesn't have this constraint, so we should
+ // check just in case there is a codepath followed by
+ // `profiler_dump_and_stop` that breaks if we stop while stopped.
+ if (profiler_is_active()) {
+ profiler_dump_and_stop();
+ }
+ } else {
+ LOG("AsyncSignalControlThread recieved unknown control signal: %c",
+ msg[0]);
+ }
+ }
+ };
+
+ private:
+ // The read side of the pipe that we use to communicate from a signal handler
+ // to the AsyncSignalControlThread
+ int mFd;
+
+ // The thread handle for the async signal control thread
+ // Note, that unlike the sampler thread, this is currently a posix-only
+ // feature. Therefore, we don't bother to have a windows equivalent - we
+ // just use a pthread_t
+ pthread_t mThread;
+};
+
+static void* AsyncSignalControlThreadEntry(void* aArg) {
+ auto* thread = static_cast<AsyncSignalControlThread*>(aArg);
+ thread->Watch();
+ return nullptr;
+}
+#endif
+
// All functions in this file can run on multiple threads unless they have an
// NS_IsMainThread() assertion.
@@ -529,6 +698,10 @@ class CorePS {
CorePS()
: mProcessStartTime(TimeStamp::ProcessCreation()),
mMaybeBandwidthCounter(nullptr)
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
+ ,
+ mAsyncSignalControlThread(nullptr)
+#endif
#ifdef USE_LUL_STACKWALK
,
mLul(nullptr)
@@ -539,6 +712,9 @@ class CorePS {
}
~CorePS() {
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
+ delete mAsyncSignalControlThread;
+#endif
#ifdef USE_LUL_STACKWALK
delete sInstance->mLul;
delete mMaybeBandwidthCounter;
@@ -684,6 +860,14 @@ class CorePS {
return sInstance->mMaybeBandwidthCounter;
}
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
+ static void SetAsyncSignalControlThread(
+ AsyncSignalControlThread* aAsyncSignalControlThread) {
+ MOZ_ASSERT(sInstance);
+ sInstance->mAsyncSignalControlThread = aAsyncSignalControlThread;
+ }
+#endif
+
private:
// The singleton instance
static CorePS* sInstance;
@@ -701,6 +885,11 @@ class CorePS {
// Non-owning pointers to all active counters
Vector<BaseProfilerCount*> mCounters;
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
+ // Background thread for communicating with async signal handlers
+ AsyncSignalControlThread* mAsyncSignalControlThread;
+#endif
+
#ifdef USE_LUL_STACKWALK
// LUL's state. Null prior to the first activation, non-null thereafter.
// Owned by this CorePS.
@@ -1139,6 +1328,11 @@ class ActivePS {
#undef PS_GET_FEATURE
+ static bool ShouldInstallMemoryHooks(PSLockRef) {
+ MOZ_ASSERT(sInstance);
+ return ProfilerFeature::ShouldInstallMemoryHooks(sInstance->mFeatures);
+ }
+
static uint32_t JSFlags(PSLockRef aLock) {
uint32_t Flags = 0;
Flags |=
@@ -4106,10 +4300,6 @@ static SamplerThread* NewSamplerThread(PSLockRef aLock, uint32_t aGeneration,
return new SamplerThread(aLock, aGeneration, aInterval, aFeatures);
}
-// Forward declare the function to call when we need to dump + stop from within
-// the sampler thread
-void profiler_dump_and_stop();
-
// This function is the sampler thread. This implementation is used for all
// targets.
void SamplerThread::Run() {
@@ -4765,27 +4955,6 @@ void SamplerThread::Run() {
scheduledSampleStart = beforeSleep + sampleInterval;
SleepMicro(static_cast<uint32_t>(sampleInterval.ToMicroseconds()));
}
-
- // Check to see if the hard-reset flag has been set to stop the profiler.
- // This should only be used on the worst occasions when we need to stop the
- // profiler from within the sampling thread (e.g. if the main thread is
- // stuck) We need to do this here as it is outside of the scope of the lock.
- // Otherwise we'll encounter a race condition where `profiler_stop` tries to
- // get the lock that we already hold. We also need to wait until after we
- // have carried out post sampling callbacks, as otherwise we may reach a
- // situation where another part of the program is waiting for us to finish
- // sampling, but we have ended early!
- if (gStopAndDumpFromSignal) {
- // Reset the flag in case we restart the profiler at a later point
- gStopAndDumpFromSignal = false;
- // dump the profile, and stop the profiler
- profiler_dump_and_stop();
- // profiler_stop will try to destroy the active sampling thread. This will
- // also destroy some data structures that are used further down this
- // function, leading to invalid accesses. We therefore exit the function
- // directly, rather than breaking from the loop.
- return;
- }
}
// End of `while` loop. We can only be here from a `break` inside the loop.
@@ -5069,9 +5238,9 @@ void SamplerThread::SpyOnUnregisteredThreads() {
MOZ_DEFINE_MALLOC_SIZE_OF(GeckoProfilerMallocSizeOf)
-NS_IMETHODIMP
-GeckoProfilerReporter::CollectReports(nsIHandleReportCallback* aHandleReport,
- nsISupports* aData, bool aAnonymize) {
+NS_IMETHODIMP GeckoProfilerReporter::CollectReports(
+ nsIHandleReportCallback* aHandleReport, nsISupports* aData,
+ bool aAnonymize) {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
size_t profSize = 0;
@@ -5294,16 +5463,41 @@ static const char* get_size_suffix(const char* str) {
return ptr;
}
-#if !defined(XP_WIN) && !defined(MOZ_CODE_COVERAGE)
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
+static void profiler_start_signal_handler(int signal, siginfo_t* info,
+ void* context) {
+ // Starting the profiler from a signal handler is a risky business: Both of
+ // the main tasks that we would like to accomplish (allocating memory, and
+ // starting a thread) are illegal within a UNIX signal handler. Conversely,
+ // we cannot dispatch to the main thread, as this may be "stuck" (why else
+ // would we be using a signal handler to start the profiler?).
+ // Instead, we have a background thread running that watches a pipe for a
+ // given "control" character. In this handler, we can simply write to that
+ // pipe to get the background thread to start the profiler for us!
+ // Note that `write` is async-signal safe (see signal-safety(7)):
+ // https://www.man7.org/linux/man-pages/man7/signal-safety.7.html
+ // This means that it's safe for us to call within a signal handler.
+ if (sAsyncSignalControlWriteFd != -1) {
+ char signalControlCharacter = sAsyncSignalControlCharStart;
+ Unused << write(sAsyncSignalControlWriteFd, &signalControlCharacter,
+ sizeof(signalControlCharacter));
+ }
+}
+
static void profiler_stop_signal_handler(int signal, siginfo_t* info,
void* context) {
- // We cannot really do any logging here, as this is a signal handler.
// Signal handlers are limited in what functions they can call, for more
// details see: https://man7.org/linux/man-pages/man7/signal-safety.7.html
- // Writing to a file is allowed, but as signal handlers are also limited in
- // how long they can run, we instead set an atomic variable to true to trigger
- // the sampling thread to stop and dump the data in the profiler.
- gStopAndDumpFromSignal = true;
+ // As we have a background thread already running for checking whether or
+ // not we want to start the profiler, we can re-use the same machinery to
+ // stop the profiler. We use the same mechanism of writing to a pipe/file
+ // descriptor, but with a different control character. Note that `write` is
+ // signal safe.
+ if (sAsyncSignalControlWriteFd != -1) {
+ char signalControlCharacter = sAsyncSignalControlCharStop;
+ Unused << write(sAsyncSignalControlWriteFd, &signalControlCharacter,
+ sizeof(signalControlCharacter));
+ }
}
#endif
@@ -5379,8 +5573,17 @@ void profiler_dump_and_stop() {
profiler_stop();
}
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
void profiler_init_signal_handlers() {
-#if !defined(XP_WIN) && !defined(MOZ_CODE_COVERAGE)
+ // Set a handler to start the profiler
+ struct sigaction prof_start_sa {};
+ memset(&prof_start_sa, 0, sizeof(struct sigaction));
+ prof_start_sa.sa_sigaction = profiler_start_signal_handler;
+ prof_start_sa.sa_flags = SA_RESTART | SA_SIGINFO;
+ sigemptyset(&prof_start_sa.sa_mask);
+ DebugOnly<int> rstart = sigaction(SIGUSR1, &prof_start_sa, nullptr);
+ MOZ_ASSERT(rstart == 0, "Failed to install Profiler SIGUSR1 handler");
+
// Set a handler to stop the profiler
struct sigaction prof_stop_sa {};
memset(&prof_stop_sa, 0, sizeof(struct sigaction));
@@ -5389,8 +5592,8 @@ void profiler_init_signal_handlers() {
sigemptyset(&prof_stop_sa.sa_mask);
DebugOnly<int> rstop = sigaction(SIGUSR2, &prof_stop_sa, nullptr);
MOZ_ASSERT(rstop == 0, "Failed to install Profiler SIGUSR2 handler");
-#endif
}
+#endif
void profiler_init(void* aStackTop) {
LOG("profiler_init");
@@ -5444,8 +5647,14 @@ void profiler_init(void* aStackTop) {
// Platform-specific initialization.
PlatformInit(lock);
+#if defined(GECKO_PROFILER_ASYNC_POSIX_SIGNAL_CONTROL)
+ // Initialise the background thread to listen for signal handler
+ // communication
+ CorePS::SetAsyncSignalControlThread(new AsyncSignalControlThread);
+
// Initialise the signal handlers needed to start/stop the profiler
profiler_init_signal_handlers();
+#endif
#if defined(GP_OS_android)
if (jni::IsAvailable()) {
@@ -5611,9 +5820,16 @@ void profiler_init(void* aStackTop) {
profiler_mark_thread_awake();
#if defined(MOZ_REPLACE_MALLOC) && defined(MOZ_PROFILER_MEMORY)
- // Start counting memory allocations (outside of lock because this may call
- // profiler_add_sampled_counter which would attempt to take the lock.)
- ActivePS::SetMemoryCounter(mozilla::profiler::install_memory_hooks());
+ if (ProfilerFeature::ShouldInstallMemoryHooks(features)) {
+ // Start counting memory allocations (outside of lock because this may call
+ // profiler_add_sampled_counter which would attempt to take the lock.)
+ ActivePS::SetMemoryCounter(mozilla::profiler::install_memory_hooks());
+ } else {
+ // Unregister the memory counter in case it was registered before. This will
+ // make sure that the empty memory counter from the previous profiler run is
+ // removed completely and we don't serialize the memory counters.
+ mozilla::profiler::unregister_memory_counter();
+ }
#endif
invoke_profiler_state_change_callbacks(ProfilingState::Started);
@@ -6240,9 +6456,16 @@ RefPtr<GenericPromise> profiler_start(PowerOfTwo32 aCapacity, double aInterval,
}
#if defined(MOZ_REPLACE_MALLOC) && defined(MOZ_PROFILER_MEMORY)
- // Start counting memory allocations (outside of lock because this may call
- // profiler_add_sampled_counter which would attempt to take the lock.)
- ActivePS::SetMemoryCounter(mozilla::profiler::install_memory_hooks());
+ if (ProfilerFeature::ShouldInstallMemoryHooks(aFeatures)) {
+ // Start counting memory allocations (outside of lock because this may call
+ // profiler_add_sampled_counter which would attempt to take the lock.)
+ ActivePS::SetMemoryCounter(mozilla::profiler::install_memory_hooks());
+ } else {
+ // Unregister the memory counter in case it was registered before. This will
+ // make sure that the empty memory counter from the previous profiler run is
+ // removed completely and we don't serialize the memory counters.
+ mozilla::profiler::unregister_memory_counter();
+ }
#endif
invoke_profiler_state_change_callbacks(ProfilingState::Started);
@@ -6366,7 +6589,8 @@ void profiler_ensure_started(PowerOfTwo32 aCapacity, double aInterval,
}
#if defined(MOZ_REPLACE_MALLOC) && defined(MOZ_PROFILER_MEMORY)
- if (ActivePS::FeatureNativeAllocations(aLock)) {
+ if (ActivePS::FeatureNativeAllocations(aLock) &&
+ ActivePS::ShouldInstallMemoryHooks(aLock)) {
mozilla::profiler::disable_native_allocations();
}
#endif