From a90a5cba08fdf6c0ceb95101c275108a152a3aed Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:35:37 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- tools/profiler/core/memory_hooks.cpp | 7 + tools/profiler/core/memory_hooks.h | 1 + tools/profiler/core/platform.cpp | 320 +++++++++++++++++++++++++++++------ 3 files changed, 280 insertions(+), 48 deletions(-) (limited to 'tools/profiler/core') 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 #include +// 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 -# include +// 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 +# include +# include +# include +#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 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 + sAsyncSignalControlWriteFd(-1); // Atomic flag to stop the profiler from within the sampling loop mozilla::Atomic 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 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(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 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(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 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 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 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 -- cgit v1.2.3