diff --git a/tools/profiler/core/ProfileBuffer.cpp b/tools/profiler/core/ProfileBuffer.cpp index 5a77da904347..d0b29062afdd 100644 --- a/tools/profiler/core/ProfileBuffer.cpp +++ b/tools/profiler/core/ProfileBuffer.cpp @@ -8,6 +8,8 @@ #include "ProfilerMarker.h" +using namespace mozilla; + ProfileBuffer::ProfileBuffer(int aEntrySize) : mEntries(mozilla::MakeUnique(aEntrySize)) , mWritePos(0) @@ -55,24 +57,6 @@ ProfileBuffer::AddThreadIdEntry(int aThreadId, LastSample* aLS) AddEntry(ProfileBufferEntry::ThreadId(aThreadId)); } -void -ProfileBuffer::AddDynamicStringEntry(const char* aStr) -{ - size_t strLen = strlen(aStr) + 1; // +1 for the null terminator - for (size_t j = 0; j < strLen; ) { - // Store up to kNumChars characters in the entry. - char chars[ProfileBufferEntry::kNumChars]; - size_t len = ProfileBufferEntry::kNumChars; - if (j + len >= strLen) { - len = strLen - j; - } - memcpy(chars, &aStr[j], len); - j += ProfileBufferEntry::kNumChars; - - AddEntry(ProfileBufferEntry::DynamicStringFragment(chars)); - } -} - void ProfileBuffer::AddStoredMarker(ProfilerMarker *aStoredMarker) { @@ -80,6 +64,51 @@ ProfileBuffer::AddStoredMarker(ProfilerMarker *aStoredMarker) mStoredMarkers.insert(aStoredMarker); } +void +ProfileBuffer::CollectNativeLeafAddr(void* aAddr) +{ + AddEntry(ProfileBufferEntry::NativeLeafAddr(aAddr)); +} + +void +ProfileBuffer::CollectJitReturnAddr(void* aAddr) +{ + AddEntry(ProfileBufferEntry::JitReturnAddr(aAddr)); +} + +void +ProfileBuffer::CollectCodeLocation( + const char* aLabel, const char* aStr, int aLineNumber, + const Maybe& aCategory) +{ + AddEntry(ProfileBufferEntry::Label(aLabel)); + + if (aStr) { + // Store the string using one or more DynamicStringFragment entries. + size_t strLen = strlen(aStr) + 1; // +1 for the null terminator + for (size_t j = 0; j < strLen; ) { + // Store up to kNumChars characters in the entry. + char chars[ProfileBufferEntry::kNumChars]; + size_t len = ProfileBufferEntry::kNumChars; + if (j + len >= strLen) { + len = strLen - j; + } + memcpy(chars, &aStr[j], len); + j += ProfileBufferEntry::kNumChars; + + AddEntry(ProfileBufferEntry::DynamicStringFragment(chars)); + } + } + + if (aLineNumber != -1) { + AddEntry(ProfileBufferEntry::LineNumber(aLineNumber)); + } + + if (aCategory.isSome()) { + AddEntry(ProfileBufferEntry::Category(int(*aCategory))); + } +} + void ProfileBuffer::DeleteExpiredStoredMarkers() { diff --git a/tools/profiler/core/ProfileBuffer.h b/tools/profiler/core/ProfileBuffer.h index 201447258012..d07f80b3aadb 100644 --- a/tools/profiler/core/ProfileBuffer.h +++ b/tools/profiler/core/ProfileBuffer.h @@ -13,7 +13,7 @@ #include "mozilla/RefPtr.h" #include "mozilla/RefCounted.h" -class ProfileBuffer final +class ProfileBuffer final : public ProfilerStackCollector { public: explicit ProfileBuffer(int aEntrySize); @@ -42,9 +42,16 @@ public: // record the resulting generation and index in |aLS| if it's non-null. void AddThreadIdEntry(int aThreadId, LastSample* aLS = nullptr); - // Add to the buffer a dynamic string. It'll be spread across one or more - // DynamicStringFragment entries. - void AddDynamicStringEntry(const char* aStr); + virtual mozilla::Maybe Generation() override + { + return mozilla::Some(mGeneration); + } + + virtual void CollectNativeLeafAddr(void* aAddr) override; + virtual void CollectJitReturnAddr(void* aAddr) override; + virtual void CollectCodeLocation( + const char* aLabel, const char* aStr, int aLineNumber, + const mozilla::Maybe& aCategory) override; // Maximum size of a frameKey string that we'll handle. static const size_t kMaxFrameKeyLength = 512; diff --git a/tools/profiler/core/platform.cpp b/tools/profiler/core/platform.cpp index 53ceb1bd606a..fabc7d6ecd7d 100644 --- a/tools/profiler/core/platform.cpp +++ b/tools/profiler/core/platform.cpp @@ -22,7 +22,7 @@ // // - A "backtrace" sample is the simplest kind. It is done in response to an // API call (profiler_suspend_and_sample_thread()). It involves getting a -// stack trace and passing it to a callback function; it does not write to a +// stack trace via a ProfilerStackCollector; it does not write to a // ProfileBuffer. The sampling is done from off-thread, and so uses // SuspendAndSampleAndResumeThread() to get the register values. @@ -54,7 +54,9 @@ #include "nsIXULRuntime.h" #include "nsDirectoryServiceUtils.h" #include "nsDirectoryServiceDefs.h" +#include "nsJSPrincipals.h" #include "nsMemoryReporterManager.h" +#include "nsScriptSecurityManager.h" #include "nsXULAppAPI.h" #include "nsProfilerStartParams.h" #include "ProfilerParent.h" @@ -657,17 +659,31 @@ public: #endif }; -static void -AddPseudoEntry(PSLockRef aLock, NotNull aRacyInfo, - const js::ProfileEntry& entry, ProfileBuffer& aBuffer) +static bool +IsChromeJSScript(JSScript* aScript) { // WARNING: this function runs within the profiler's "critical section". + nsIScriptSecurityManager* const secman = + nsScriptSecurityManager::GetScriptSecurityManager(); + NS_ENSURE_TRUE(secman, false); + + JSPrincipals* const principals = JS_GetScriptPrincipals(aScript); + return secman->IsSystemPrincipal(nsJSPrincipals::get(principals)); +} + +static void +AddPseudoEntry(uint32_t aFeatures, NotNull aRacyInfo, + const js::ProfileEntry& entry, + ProfilerStackCollector& aCollector) +{ + // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. + MOZ_ASSERT(entry.kind() == js::ProfileEntry::Kind::CPP_NORMAL || entry.kind() == js::ProfileEntry::Kind::JS_NORMAL); - aBuffer.AddEntry(ProfileBufferEntry::Label(entry.label())); - const char* dynamicString = entry.dynamicString(); int lineno = -1; @@ -675,18 +691,11 @@ AddPseudoEntry(PSLockRef aLock, NotNull aRacyInfo, // |dynamicString|. Perhaps it shouldn't? if (dynamicString) { - // Adjust the dynamic string as necessary. - if (ActivePS::FeaturePrivacy(aLock)) { - dynamicString = "(private)"; - } else if (strlen(dynamicString) >= ProfileBuffer::kMaxFrameKeyLength) { - dynamicString = "(too long)"; - } - - // Store the string using one or more DynamicStringFragment entries. - aBuffer.AddDynamicStringEntry(dynamicString); + bool isChromeJSEntry = false; if (entry.isJs()) { JSScript* script = entry.script(); if (script) { + isChromeJSEntry = IsChromeJSScript(script); if (!entry.pc()) { // The JIT only allows the top-most entry to have a nullptr pc. MOZ_ASSERT(&entry == &aRacyInfo->entries[aRacyInfo->stackSize() - 1]); @@ -697,6 +706,14 @@ AddPseudoEntry(PSLockRef aLock, NotNull aRacyInfo, } else { lineno = entry.line(); } + + // Adjust the dynamic string as necessary. + if (ProfilerFeature::HasPrivacy(aFeatures) && !isChromeJSEntry) { + dynamicString = "(private)"; + } else if (strlen(dynamicString) >= ProfileBuffer::kMaxFrameKeyLength) { + dynamicString = "(too long)"; + } + } else { // XXX: Bug 1010578. Don't assume a CPP entry and try to get the line for // js entries as well. @@ -705,11 +722,8 @@ AddPseudoEntry(PSLockRef aLock, NotNull aRacyInfo, } } - if (lineno != -1) { - aBuffer.AddEntry(ProfileBufferEntry::LineNumber(lineno)); - } - - aBuffer.AddEntry(ProfileBufferEntry::Category(int(entry.category()))); + aCollector.CollectCodeLocation(entry.label(), dynamicString, lineno, + Some(entry.category())); } // Setting MAX_NATIVE_FRAMES too high risks the unwinder wasting a lot of time @@ -747,12 +761,17 @@ struct AutoWalkJSStack } }; +// Merges the pseudo-stack, native stack, and JS stack, outputting the details +// to aCollector. static void -MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, - const ThreadInfo& aThreadInfo, const Registers& aRegs, - const NativeStack& aNativeStack, ProfileBuffer& aBuffer) +MergeStacks(uint32_t aFeatures, bool aIsSynchronous, + const ThreadInfo& aThreadInfo, const Registers& aRegs, + const NativeStack& aNativeStack, + ProfilerStackCollector& aCollector) { // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. NotNull racyInfo = aThreadInfo.RacyInfo(); js::ProfileEntry* pseudoEntries = racyInfo->entries; @@ -767,10 +786,10 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, // ProfilingFrameIterator to avoid incorrectly resetting the generation of // sampled JIT entries inside the JS engine. See note below concerning 'J' // entries. - uint32_t startBufferGen; - startBufferGen = aIsSynchronous - ? UINT32_MAX - : aBuffer.mGeneration; + uint32_t startBufferGen = UINT32_MAX; + if (!aIsSynchronous && aCollector.Generation().isSome()) { + startBufferGen = *aCollector.Generation(); + } uint32_t jsCount = 0; JS::ProfilingFrameIterator::Frame jsFrames[MAX_JS_FRAMES]; @@ -882,7 +901,7 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, // Pseudo-frames with the CPP_MARKER_FOR_JS kind are just annotations and // should not be recorded in the profile. if (pseudoEntry.kind() != js::ProfileEntry::Kind::CPP_MARKER_FOR_JS) { - AddPseudoEntry(aLock, racyInfo, pseudoEntry, aBuffer); + AddPseudoEntry(aFeatures, racyInfo, pseudoEntry, aCollector); } pseudoIndex++; continue; @@ -908,13 +927,11 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, // with stale JIT code return addresses. if (aIsSynchronous || jsFrame.kind == JS::ProfilingFrameIterator::Frame_Wasm) { - aBuffer.AddEntry(ProfileBufferEntry::Label("")); - aBuffer.AddDynamicStringEntry(jsFrame.label); + aCollector.CollectCodeLocation("", jsFrame.label, -1, Nothing()); } else { MOZ_ASSERT(jsFrame.kind == JS::ProfilingFrameIterator::Frame_Ion || jsFrame.kind == JS::ProfilingFrameIterator::Frame_Baseline); - aBuffer.AddEntry( - ProfileBufferEntry::JitReturnAddr(jsFrames[jsIndex].returnAddress)); + aCollector.CollectJitReturnAddr(jsFrames[jsIndex].returnAddress); } jsIndex--; @@ -926,7 +943,7 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, if (nativeStackAddr) { MOZ_ASSERT(nativeIndex >= 0); void* addr = (void*)aNativeStack.mPCs[nativeIndex]; - aBuffer.AddEntry(ProfileBufferEntry::NativeLeafAddr(addr)); + aCollector.CollectNativeLeafAddr(addr); } if (nativeIndex >= 0) { nativeIndex--; @@ -937,10 +954,11 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, // // Do not do this for synchronous samples, which use their own // ProfileBuffers instead of the global one in CorePS. - if (!aIsSynchronous && context) { - MOZ_ASSERT(aBuffer.mGeneration >= startBufferGen); - uint32_t lapCount = aBuffer.mGeneration - startBufferGen; - JS::UpdateJSContextProfilerSampleBufferGen(context, aBuffer.mGeneration, + if (!aIsSynchronous && context && aCollector.Generation().isSome()) { + MOZ_ASSERT(*aCollector.Generation() >= startBufferGen); + uint32_t lapCount = *aCollector.Generation() - startBufferGen; + JS::UpdateJSContextProfilerSampleBufferGen(context, + *aCollector.Generation(), lapCount); } } @@ -965,6 +983,8 @@ DoNativeBacktrace(PSLockRef aLock, const ThreadInfo& aThreadInfo, const Registers& aRegs, NativeStack& aNativeStack) { // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. // Start with the current function. We use 0 as the frame number here because // the FramePointerStackWalk() and MozStackWalk() calls below will use 1..N. @@ -998,6 +1018,8 @@ DoNativeBacktrace(PSLockRef aLock, const ThreadInfo& aThreadInfo, const Registers& aRegs, NativeStack& aNativeStack) { // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. const mcontext_t* mcontext = &aRegs.mContext->uc_mcontext; mcontext_t savedContext; @@ -1077,6 +1099,8 @@ DoNativeBacktrace(PSLockRef aLock, const ThreadInfo& aThreadInfo, const Registers& aRegs, NativeStack& aNativeStack) { // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. const mcontext_t* mc = &aRegs.mContext->uc_mcontext; @@ -1222,13 +1246,13 @@ DoSharedSample(PSLockRef aLock, bool aIsSynchronous, if (ActivePS::FeatureStackWalk(aLock)) { DoNativeBacktrace(aLock, aThreadInfo, aRegs, nativeStack); - MergeStacksIntoProfile(aLock, aIsSynchronous, aThreadInfo, aRegs, - nativeStack, aBuffer); + MergeStacks(ActivePS::Features(aLock), aIsSynchronous, aThreadInfo, aRegs, + nativeStack, aBuffer); } else #endif { - MergeStacksIntoProfile(aLock, aIsSynchronous, aThreadInfo, aRegs, - nativeStack, aBuffer); + MergeStacks(ActivePS::Features(aLock), aIsSynchronous, aThreadInfo, aRegs, + nativeStack, aBuffer); if (ActivePS::FeatureLeaf(aLock)) { aBuffer.AddEntry(ProfileBufferEntry::NativeLeafAddr((void*)aRegs.mPC)); @@ -3046,5 +3070,62 @@ profiler_suspend_and_sample_thread( } } +// NOTE: aCollector's methods will be called while the target thread is paused. +// Doing things in those methods like allocating -- which may try to claim +// locks -- is a surefire way to deadlock. +void +profiler_suspend_and_sample_thread(int aThreadId, + uint32_t aFeatures, + ProfilerStackCollector& aCollector, + bool aSampleNative /* = true */) +{ + // Lock the profiler mutex + PSAutoLock lock(gPSMutex); + + const CorePS::ThreadVector& liveThreads = CorePS::LiveThreads(lock); + for (uint32_t i = 0; i < liveThreads.size(); i++) { + ThreadInfo* info = liveThreads.at(i); + + if (info->ThreadId() == aThreadId) { + if (info->IsMainThread()) { + aCollector.SetIsMainThread(); + } + + // Allocate the space for the native stack + NativeStack nativeStack; + + // Suspend, sample, and then resume the target thread. + Sampler sampler(lock); + sampler.SuspendAndSampleAndResumeThread(lock, *info, + [&](const Registers& aRegs) { + // The target thread is now suspended. Collect a native backtrace, and + // call the callback. + bool isSynchronous = false; +#if defined(HAVE_NATIVE_UNWIND) + if (aSampleNative) { + DoNativeBacktrace(lock, *info, aRegs, nativeStack); + + MergeStacks(aFeatures, isSynchronous, *info, aRegs, nativeStack, + aCollector); + } else +#endif + { + MergeStacks(aFeatures, isSynchronous, *info, aRegs, nativeStack, + aCollector); + + if (ProfilerFeature::HasLeaf(aFeatures)) { + aCollector.CollectNativeLeafAddr((void*)aRegs.mPC); + } + } + }); + + // NOTE: Make sure to disable the sampler before it is destroyed, in case + // the profiler is running at the same time. + sampler.Disable(lock); + break; + } + } +} + // END externally visible functions //////////////////////////////////////////////////////////////////////// diff --git a/tools/profiler/moz.build b/tools/profiler/moz.build index 54c971791c96..e5a2f95006f9 100644 --- a/tools/profiler/moz.build +++ b/tools/profiler/moz.build @@ -81,6 +81,7 @@ if CONFIG['MOZ_GECKO_PROFILER']: ] LOCAL_INCLUDES += [ + '/caps', '/docshell/base', '/ipc/chromium/src', '/mozglue/linker', diff --git a/tools/profiler/public/GeckoProfiler.h b/tools/profiler/public/GeckoProfiler.h index e42654a079d1..bbf6fedc5649 100644 --- a/tools/profiler/public/GeckoProfiler.h +++ b/tools/profiler/public/GeckoProfiler.h @@ -25,6 +25,7 @@ #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/GuardObjects.h" +#include "mozilla/Maybe.h" #include "mozilla/Sprintf.h" #include "mozilla/ThreadLocal.h" #include "mozilla/UniquePtr.h" @@ -267,11 +268,52 @@ typedef void ProfilerStackCallback(void** aPCs, size_t aCount, bool aIsMainThrea // WARNING: The target thread is suspended during the callback. Do not try to // allocate or acquire any locks, or you could deadlock. The target thread will // have resumed by the time this function returns. +// +// XXX: this function is in the process of being replaced with the other profiler_suspend_and_sample_thread() function. PROFILER_FUNC_VOID( profiler_suspend_and_sample_thread(int aThreadId, const std::function& aCallback, bool aSampleNative = true)) +// An object of this class is passed to profiler_suspend_and_sample_thread(). +// For each stack frame, one of the Collect methods will be called. +class ProfilerStackCollector +{ +public: + // Some collectors need to worry about possibly overwriting previous + // generations of data. If that's not an issue, this can return Nothing, + // which is the default behaviour. + virtual mozilla::Maybe Generation() { return mozilla::Nothing(); } + + // This method will be called once if the thread being suspended is the main + // thread. Default behaviour is to do nothing. + virtual void SetIsMainThread() {} + + // WARNING: The target thread is suspended when the Collect methods are + // called. Do not try to allocate or acquire any locks, or you could + // deadlock. The target thread will have resumed by the time this function + // returns. + + virtual void CollectNativeLeafAddr(void* aAddr) = 0; + + virtual void CollectJitReturnAddr(void* aAddr) = 0; + + // aLabel is static and never null. aStr may be null. aLineNumber may be -1. + virtual void CollectCodeLocation( + const char* aLabel, const char* aStr, int aLineNumber, + const mozilla::Maybe& aCategory) = 0; +}; + +// This method suspends the thread identified by aThreadId, samples its +// pseudo-stack, JS stack, and (optionally) native stack, passing the collected +// frames into aCollector. aFeatures dictates which compiler features are used. +// |Privacy| and |Leaf| are the only relevant ones. +PROFILER_FUNC_VOID( + profiler_suspend_and_sample_thread(int aThreadId, + uint32_t aFeatures, + ProfilerStackCollector& aCollector, + bool aSampleNative = true)) + struct ProfilerBacktraceDestructor { #ifdef MOZ_GECKO_PROFILER diff --git a/tools/profiler/tests/gtest/GeckoProfiler.cpp b/tools/profiler/tests/gtest/GeckoProfiler.cpp index 03825c94bee7..fa06002c8705 100644 --- a/tools/profiler/tests/gtest/GeckoProfiler.cpp +++ b/tools/profiler/tests/gtest/GeckoProfiler.cpp @@ -664,3 +664,70 @@ TEST(GeckoProfiler, Bug1355807) profiler_stop(); } + +class GTestStackCollector final : public ProfilerStackCollector +{ +public: + GTestStackCollector() + : mSetIsMainThread(0) + , mFrames(0) + {} + + virtual void SetIsMainThread() { mSetIsMainThread++; } + + virtual void CollectNativeLeafAddr(void* aAddr) { mFrames++; } + virtual void CollectJitReturnAddr(void* aAddr) { mFrames++; } + virtual void CollectCodeLocation( + const char* aLabel, const char* aStr, int aLineNumber, + const mozilla::Maybe& aCategory) { mFrames++; } + + int mSetIsMainThread; + int mFrames; +}; + +void DoSuspendAndSample(int aTid, nsIThread* aThread) +{ + aThread->Dispatch( + NS_NewRunnableFunction( + "GeckoProfiler_SuspendAndSample_Test::TestBody", + [&]() { + uint32_t features = ProfilerFeature::Leaf; + GTestStackCollector collector; + profiler_suspend_and_sample_thread(aTid, features, collector, + /* sampleNative = */ true); + + ASSERT_TRUE(collector.mSetIsMainThread == 1); + ASSERT_TRUE(collector.mFrames > 5); // approximate; must be > 0 + }), + NS_DISPATCH_SYNC); +} + +TEST(GeckoProfiler, SuspendAndSample) +{ + nsCOMPtr thread; + nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + + int tid = Thread::GetCurrentId(); + + ASSERT_TRUE(!profiler_is_active()); + + // Suspend and sample while the profiler is inactive. + DoSuspendAndSample(tid, thread); + + uint32_t features = ProfilerFeature::JS | ProfilerFeature::Threads; + const char* filters[] = { "GeckoMain", "Compositor" }; + + profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, + features, filters, MOZ_ARRAY_LENGTH(filters)); + + ASSERT_TRUE(profiler_is_active()); + + // Suspend and sample while the profiler is active. + DoSuspendAndSample(tid, thread); + + profiler_stop(); + + ASSERT_TRUE(!profiler_is_active()); +} +