/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/. */ #include "ProfileBufferEntry.h" #include "mozilla/ProfilerMarkers.h" #include "platform.h" #include "ProfileBuffer.h" #include "ProfiledThreadData.h" #include "ProfilerBacktrace.h" #include "ProfilerRustBindings.h" #include "js/ProfilingFrameIterator.h" #include "jsapi.h" #include "jsfriendapi.h" #include "mozilla/Logging.h" #include "mozilla/JSONStringWriteFuncs.h" #include "mozilla/ScopeExit.h" #include "mozilla/Sprintf.h" #include "mozilla/StackWalk.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" #include "ProfilerCodeAddressService.h" #include #include using namespace mozilla; using namespace mozilla::literals::ProportionValue_literals; //////////////////////////////////////////////////////////////////////// // BEGIN ProfileBufferEntry ProfileBufferEntry::ProfileBufferEntry() : mKind(Kind::INVALID), mStorage{0, 0, 0, 0, 0, 0, 0, 0} {} // aString must be a static string. ProfileBufferEntry::ProfileBufferEntry(Kind aKind, const char* aString) : mKind(aKind) { MOZ_ASSERT(aKind == Kind::Label); memcpy(mStorage, &aString, sizeof(aString)); } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, char aChars[kNumChars]) : mKind(aKind) { MOZ_ASSERT(aKind == Kind::DynamicStringFragment); memcpy(mStorage, aChars, kNumChars); } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, void* aPtr) : mKind(aKind) { memcpy(mStorage, &aPtr, sizeof(aPtr)); } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, double aDouble) : mKind(aKind) { memcpy(mStorage, &aDouble, sizeof(aDouble)); } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, int aInt) : mKind(aKind) { memcpy(mStorage, &aInt, sizeof(aInt)); } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, int64_t aInt64) : mKind(aKind) { memcpy(mStorage, &aInt64, sizeof(aInt64)); } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, uint64_t aUint64) : mKind(aKind) { memcpy(mStorage, &aUint64, sizeof(aUint64)); } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, ProfilerThreadId aThreadId) : mKind(aKind) { static_assert(std::is_trivially_copyable_v); static_assert(sizeof(aThreadId) <= sizeof(mStorage)); memcpy(mStorage, &aThreadId, sizeof(aThreadId)); } const char* ProfileBufferEntry::GetString() const { const char* result; memcpy(&result, mStorage, sizeof(result)); return result; } void* ProfileBufferEntry::GetPtr() const { void* result; memcpy(&result, mStorage, sizeof(result)); return result; } double ProfileBufferEntry::GetDouble() const { double result; memcpy(&result, mStorage, sizeof(result)); return result; } int ProfileBufferEntry::GetInt() const { int result; memcpy(&result, mStorage, sizeof(result)); return result; } int64_t ProfileBufferEntry::GetInt64() const { int64_t result; memcpy(&result, mStorage, sizeof(result)); return result; } uint64_t ProfileBufferEntry::GetUint64() const { uint64_t result; memcpy(&result, mStorage, sizeof(result)); return result; } ProfilerThreadId ProfileBufferEntry::GetThreadId() const { ProfilerThreadId result; static_assert(std::is_trivially_copyable_v); memcpy(&result, mStorage, sizeof(result)); return result; } void ProfileBufferEntry::CopyCharsInto(char (&aOutArray)[kNumChars]) const { memcpy(aOutArray, mStorage, kNumChars); } // END ProfileBufferEntry //////////////////////////////////////////////////////////////////////// struct TypeInfo { Maybe mKeyedBy; Maybe mName; Maybe mLocation; Maybe mLineNumber; }; // As mentioned in ProfileBufferEntry.h, the JSON format contains many // arrays whose elements are laid out according to various schemas to help // de-duplication. This RAII class helps write these arrays by keeping track of // the last non-null element written and adding the appropriate number of null // elements when writing new non-null elements. It also automatically opens and // closes an array element on the given JSON writer. // // You grant the AutoArraySchemaWriter exclusive access to the JSONWriter and // the UniqueJSONStrings objects for the lifetime of AutoArraySchemaWriter. Do // not access them independently while the AutoArraySchemaWriter is alive. // If you need to add complex objects, call FreeFormElement(), which will give // you temporary access to the writer. // // Example usage: // // // Define the schema of elements in this type of array: [FOO, BAR, BAZ] // enum Schema : uint32_t { // FOO = 0, // BAR = 1, // BAZ = 2 // }; // // AutoArraySchemaWriter writer(someJsonWriter, someUniqueStrings); // if (shouldWriteFoo) { // writer.IntElement(FOO, getFoo()); // } // ... etc ... // // The elements need to be added in-order. class MOZ_RAII AutoArraySchemaWriter { public: explicit AutoArraySchemaWriter(SpliceableJSONWriter& aWriter) : mJSONWriter(aWriter), mNextFreeIndex(0) { mJSONWriter.StartArrayElement(); } ~AutoArraySchemaWriter() { mJSONWriter.EndArray(); } template void IntElement(uint32_t aIndex, T aValue) { static_assert(!std::is_same_v, "Narrowing uint64 -> int64 conversion not allowed"); FillUpTo(aIndex); mJSONWriter.IntElement(static_cast(aValue)); } void DoubleElement(uint32_t aIndex, double aValue) { FillUpTo(aIndex); mJSONWriter.DoubleElement(aValue); } void TimeMsElement(uint32_t aIndex, double aTime_ms) { FillUpTo(aIndex); mJSONWriter.TimeDoubleMsElement(aTime_ms); } void BoolElement(uint32_t aIndex, bool aValue) { FillUpTo(aIndex); mJSONWriter.BoolElement(aValue); } protected: SpliceableJSONWriter& Writer() { return mJSONWriter; } void FillUpTo(uint32_t aIndex) { MOZ_ASSERT(aIndex >= mNextFreeIndex); mJSONWriter.NullElements(aIndex - mNextFreeIndex); mNextFreeIndex = aIndex + 1; } private: SpliceableJSONWriter& mJSONWriter; uint32_t mNextFreeIndex; }; // Same as AutoArraySchemaWriter, but this can also write strings (output as // indexes into the table of unique strings). class MOZ_RAII AutoArraySchemaWithStringsWriter : public AutoArraySchemaWriter { public: AutoArraySchemaWithStringsWriter(SpliceableJSONWriter& aWriter, UniqueJSONStrings& aStrings) : AutoArraySchemaWriter(aWriter), mStrings(aStrings) {} void StringElement(uint32_t aIndex, const Span& aValue) { FillUpTo(aIndex); mStrings.WriteElement(Writer(), aValue); } private: UniqueJSONStrings& mStrings; }; Maybe UniqueStacks::BeginStack(const FrameKey& aFrame) { if (Maybe frameIndex = GetOrAddFrameIndex(aFrame); frameIndex) { return Some(StackKey(*frameIndex)); } return Nothing{}; } Vector&& JITFrameInfo::MoveRangesWithNewFailureLatch(FailureLatch& aFailureLatch) && { aFailureLatch.SetFailureFrom(mLocalFailureLatchSource); return std::move(mRanges); } UniquePtr&& JITFrameInfo::MoveUniqueStringsWithNewFailureLatch( FailureLatch& aFailureLatch) && { if (mUniqueStrings) { mUniqueStrings->ChangeFailureLatchAndForwardState(aFailureLatch); } else { aFailureLatch.SetFailureFrom(mLocalFailureLatchSource); } return std::move(mUniqueStrings); } Maybe UniqueStacks::AppendFrame( const StackKey& aStack, const FrameKey& aFrame) { if (Maybe stackIndex = GetOrAddStackIndex(aStack); stackIndex) { if (Maybe frameIndex = GetOrAddFrameIndex(aFrame); frameIndex) { return Some(StackKey(aStack, *stackIndex, *frameIndex)); } } return Nothing{}; } JITFrameInfoForBufferRange JITFrameInfoForBufferRange::Clone() const { JITFrameInfoForBufferRange::JITAddressToJITFramesMap jitAddressToJITFramesMap; MOZ_RELEASE_ASSERT( jitAddressToJITFramesMap.reserve(mJITAddressToJITFramesMap.count())); for (auto iter = mJITAddressToJITFramesMap.iter(); !iter.done(); iter.next()) { const mozilla::Vector& srcKeys = iter.get().value(); mozilla::Vector destKeys; MOZ_RELEASE_ASSERT(destKeys.appendAll(srcKeys)); jitAddressToJITFramesMap.putNewInfallible(iter.get().key(), std::move(destKeys)); } JITFrameInfoForBufferRange::JITFrameToFrameJSONMap jitFrameToFrameJSONMap; MOZ_RELEASE_ASSERT( jitFrameToFrameJSONMap.reserve(mJITFrameToFrameJSONMap.count())); for (auto iter = mJITFrameToFrameJSONMap.iter(); !iter.done(); iter.next()) { jitFrameToFrameJSONMap.putNewInfallible(iter.get().key(), iter.get().value()); } return JITFrameInfoForBufferRange{mRangeStart, mRangeEnd, std::move(jitAddressToJITFramesMap), std::move(jitFrameToFrameJSONMap)}; } JITFrameInfo::JITFrameInfo(const JITFrameInfo& aOther, mozilla::ProgressLogger aProgressLogger) : mUniqueStrings(MakeUniqueFallible( mLocalFailureLatchSource, *aOther.mUniqueStrings, aProgressLogger.CreateSubLoggerFromTo( 0_pc, "Creating JIT frame info unique strings...", 49_pc, "Created JIT frame info unique strings"))) { if (!mUniqueStrings) { mLocalFailureLatchSource.SetFailure( "OOM in JITFrameInfo allocating mUniqueStrings"); return; } if (mRanges.reserve(aOther.mRanges.length())) { for (auto&& [i, progressLogger] : aProgressLogger.CreateLoopSubLoggersFromTo(50_pc, 100_pc, aOther.mRanges.length(), "Copying JIT frame info")) { mRanges.infallibleAppend(aOther.mRanges[i].Clone()); } } else { mLocalFailureLatchSource.SetFailure("OOM in JITFrameInfo resizing mRanges"); } } bool UniqueStacks::FrameKey::NormalFrameData::operator==( const NormalFrameData& aOther) const { return mLocation == aOther.mLocation && mRelevantForJS == aOther.mRelevantForJS && mBaselineInterp == aOther.mBaselineInterp && mInnerWindowID == aOther.mInnerWindowID && mLine == aOther.mLine && mColumn == aOther.mColumn && mCategoryPair == aOther.mCategoryPair; } bool UniqueStacks::FrameKey::JITFrameData::operator==( const JITFrameData& aOther) const { return mCanonicalAddress == aOther.mCanonicalAddress && mDepth == aOther.mDepth && mRangeIndex == aOther.mRangeIndex; } // Consume aJITFrameInfo by stealing its string table and its JIT frame info // ranges. The JIT frame info contains JSON which refers to strings from the // JIT frame info's string table, so our string table needs to have the same // strings at the same indices. UniqueStacks::UniqueStacks( FailureLatch& aFailureLatch, JITFrameInfo&& aJITFrameInfo, ProfilerCodeAddressService* aCodeAddressService /* = nullptr */) : mUniqueStrings(std::move(aJITFrameInfo) .MoveUniqueStringsWithNewFailureLatch(aFailureLatch)), mCodeAddressService(aCodeAddressService), mFrameTableWriter(aFailureLatch), mStackTableWriter(aFailureLatch), mJITInfoRanges(std::move(aJITFrameInfo) .MoveRangesWithNewFailureLatch(aFailureLatch)) { if (!mUniqueStrings) { SetFailure("Did not get mUniqueStrings from JITFrameInfo"); return; } mFrameTableWriter.StartBareList(); mStackTableWriter.StartBareList(); } Maybe UniqueStacks::GetOrAddStackIndex(const StackKey& aStack) { if (Failed()) { return Nothing{}; } uint32_t count = mStackToIndexMap.count(); auto entry = mStackToIndexMap.lookupForAdd(aStack); if (entry) { MOZ_ASSERT(entry->value() < count); return Some(entry->value()); } if (!mStackToIndexMap.add(entry, aStack, count)) { SetFailure("OOM in UniqueStacks::GetOrAddStackIndex"); return Nothing{}; } StreamStack(aStack); return Some(count); } Maybe> UniqueStacks::LookupFramesForJITAddressFromBufferPos(void* aJITAddress, uint64_t aBufferPos) { JITFrameInfoForBufferRange* rangeIter = std::lower_bound(mJITInfoRanges.begin(), mJITInfoRanges.end(), aBufferPos, [](const JITFrameInfoForBufferRange& aRange, uint64_t aPos) { return aRange.mRangeEnd < aPos; }); MOZ_RELEASE_ASSERT( rangeIter != mJITInfoRanges.end() && rangeIter->mRangeStart <= aBufferPos && aBufferPos < rangeIter->mRangeEnd, "Buffer position of jit address needs to be in one of the ranges"); using JITFrameKey = JITFrameInfoForBufferRange::JITFrameKey; const JITFrameInfoForBufferRange& jitFrameInfoRange = *rangeIter; auto jitFrameKeys = jitFrameInfoRange.mJITAddressToJITFramesMap.lookup(aJITAddress); if (!jitFrameKeys) { return Nothing(); } // Map the array of JITFrameKeys to an array of FrameKeys, and ensure that // each of the FrameKeys exists in mFrameToIndexMap. Vector frameKeys; MOZ_RELEASE_ASSERT(frameKeys.initCapacity(jitFrameKeys->value().length())); for (const JITFrameKey& jitFrameKey : jitFrameKeys->value()) { FrameKey frameKey(jitFrameKey.mCanonicalAddress, jitFrameKey.mDepth, rangeIter - mJITInfoRanges.begin()); uint32_t index = mFrameToIndexMap.count(); auto entry = mFrameToIndexMap.lookupForAdd(frameKey); if (!entry) { // We need to add this frame to our frame table. The JSON for this frame // already exists in jitFrameInfoRange, we just need to splice it into // the frame table and give it an index. auto frameJSON = jitFrameInfoRange.mJITFrameToFrameJSONMap.lookup(jitFrameKey); MOZ_RELEASE_ASSERT(frameJSON, "Should have cached JSON for this frame"); mFrameTableWriter.Splice(frameJSON->value()); MOZ_RELEASE_ASSERT(mFrameToIndexMap.add(entry, frameKey, index)); } MOZ_RELEASE_ASSERT(frameKeys.append(std::move(frameKey))); } return Some(std::move(frameKeys)); } Maybe UniqueStacks::GetOrAddFrameIndex(const FrameKey& aFrame) { if (Failed()) { return Nothing{}; } uint32_t count = mFrameToIndexMap.count(); auto entry = mFrameToIndexMap.lookupForAdd(aFrame); if (entry) { MOZ_ASSERT(entry->value() < count); return Some(entry->value()); } if (!mFrameToIndexMap.add(entry, aFrame, count)) { SetFailure("OOM in UniqueStacks::GetOrAddFrameIndex"); return Nothing{}; } StreamNonJITFrame(aFrame); return Some(count); } void UniqueStacks::SpliceFrameTableElements(SpliceableJSONWriter& aWriter) { mFrameTableWriter.EndBareList(); aWriter.TakeAndSplice(mFrameTableWriter.TakeChunkedWriteFunc()); } void UniqueStacks::SpliceStackTableElements(SpliceableJSONWriter& aWriter) { mStackTableWriter.EndBareList(); aWriter.TakeAndSplice(mStackTableWriter.TakeChunkedWriteFunc()); } [[nodiscard]] nsAutoCString UniqueStacks::FunctionNameOrAddress(void* aPC) { nsAutoCString nameOrAddress; if (!mCodeAddressService || !mCodeAddressService->GetFunction(aPC, nameOrAddress) || nameOrAddress.IsEmpty()) { nameOrAddress.AppendASCII("0x"); // `AppendInt` only knows `uint32_t` or `uint64_t`, but because these are // just aliases for *two* of (`unsigned`, `unsigned long`, and `unsigned // long long`), a call with `uintptr_t` could use the third type and // therefore would be ambiguous. // So we want to force using exactly `uint32_t` or `uint64_t`, whichever // matches the size of `uintptr_t`. // (The outer cast to `uint` should then be a no-op.) using uint = std::conditional_t; nameOrAddress.AppendInt(static_cast(reinterpret_cast(aPC)), 16); } return nameOrAddress; } void UniqueStacks::StreamStack(const StackKey& aStack) { enum Schema : uint32_t { PREFIX = 0, FRAME = 1 }; AutoArraySchemaWriter writer(mStackTableWriter); if (aStack.mPrefixStackIndex.isSome()) { writer.IntElement(PREFIX, *aStack.mPrefixStackIndex); } writer.IntElement(FRAME, aStack.mFrameIndex); } void UniqueStacks::StreamNonJITFrame(const FrameKey& aFrame) { if (Failed()) { return; } using NormalFrameData = FrameKey::NormalFrameData; enum Schema : uint32_t { LOCATION = 0, RELEVANT_FOR_JS = 1, INNER_WINDOW_ID = 2, IMPLEMENTATION = 3, OPTIMIZATIONS = 4, LINE = 5, COLUMN = 6, CATEGORY = 7, SUBCATEGORY = 8 }; AutoArraySchemaWithStringsWriter writer(mFrameTableWriter, *mUniqueStrings); const NormalFrameData& data = aFrame.mData.as(); writer.StringElement(LOCATION, data.mLocation); writer.BoolElement(RELEVANT_FOR_JS, data.mRelevantForJS); // It's okay to convert uint64_t to double here because DOM always creates IDs // that are convertible to double. writer.DoubleElement(INNER_WINDOW_ID, data.mInnerWindowID); // The C++ interpreter is the default implementation so we only emit element // for Baseline Interpreter frames. if (data.mBaselineInterp) { writer.StringElement(IMPLEMENTATION, MakeStringSpan("blinterp")); } if (data.mLine.isSome()) { writer.IntElement(LINE, *data.mLine); } if (data.mColumn.isSome()) { writer.IntElement(COLUMN, *data.mColumn); } if (data.mCategoryPair.isSome()) { const JS::ProfilingCategoryPairInfo& info = JS::GetProfilingCategoryPairInfo(*data.mCategoryPair); writer.IntElement(CATEGORY, uint32_t(info.mCategory)); writer.IntElement(SUBCATEGORY, info.mSubcategoryIndex); } } static void StreamJITFrame(JSContext* aContext, SpliceableJSONWriter& aWriter, UniqueJSONStrings& aUniqueStrings, const JS::ProfiledFrameHandle& aJITFrame) { enum Schema : uint32_t { LOCATION = 0, RELEVANT_FOR_JS = 1, INNER_WINDOW_ID = 2, IMPLEMENTATION = 3, OPTIMIZATIONS = 4, LINE = 5, COLUMN = 6, CATEGORY = 7, SUBCATEGORY = 8 }; AutoArraySchemaWithStringsWriter writer(aWriter, aUniqueStrings); writer.StringElement(LOCATION, MakeStringSpan(aJITFrame.label())); writer.BoolElement(RELEVANT_FOR_JS, false); // It's okay to convert uint64_t to double here because DOM always creates IDs // that are convertible to double. // Realm ID is the name of innerWindowID inside JS code. writer.DoubleElement(INNER_WINDOW_ID, aJITFrame.realmID()); JS::ProfilingFrameIterator::FrameKind frameKind = aJITFrame.frameKind(); MOZ_ASSERT(frameKind == JS::ProfilingFrameIterator::Frame_Ion || frameKind == JS::ProfilingFrameIterator::Frame_Baseline); writer.StringElement(IMPLEMENTATION, frameKind == JS::ProfilingFrameIterator::Frame_Ion ? MakeStringSpan("ion") : MakeStringSpan("baseline")); const JS::ProfilingCategoryPairInfo& info = JS::GetProfilingCategoryPairInfo( frameKind == JS::ProfilingFrameIterator::Frame_Ion ? JS::ProfilingCategoryPair::JS_IonMonkey : JS::ProfilingCategoryPair::JS_Baseline); writer.IntElement(CATEGORY, uint32_t(info.mCategory)); writer.IntElement(SUBCATEGORY, info.mSubcategoryIndex); } static nsCString JSONForJITFrame(JSContext* aContext, const JS::ProfiledFrameHandle& aJITFrame, UniqueJSONStrings& aUniqueStrings) { nsCString json; JSONStringRefWriteFunc jw(json); SpliceableJSONWriter writer(jw, aUniqueStrings.SourceFailureLatch()); StreamJITFrame(aContext, writer, aUniqueStrings, aJITFrame); return json; } void JITFrameInfo::AddInfoForRange( uint64_t aRangeStart, uint64_t aRangeEnd, JSContext* aCx, const std::function&)>& aJITAddressProvider) { if (mLocalFailureLatchSource.Failed()) { return; } if (aRangeStart == aRangeEnd) { return; } MOZ_RELEASE_ASSERT(aRangeStart < aRangeEnd); if (!mRanges.empty()) { const JITFrameInfoForBufferRange& prevRange = mRanges.back(); MOZ_RELEASE_ASSERT(prevRange.mRangeEnd <= aRangeStart, "Ranges must be non-overlapping and added in-order."); } using JITFrameKey = JITFrameInfoForBufferRange::JITFrameKey; JITFrameInfoForBufferRange::JITAddressToJITFramesMap jitAddressToJITFrameMap; JITFrameInfoForBufferRange::JITFrameToFrameJSONMap jitFrameToFrameJSONMap; aJITAddressProvider([&](void* aJITAddress) { // Make sure that we have cached data for aJITAddress. auto addressEntry = jitAddressToJITFrameMap.lookupForAdd(aJITAddress); if (!addressEntry) { Vector jitFrameKeys; for (JS::ProfiledFrameHandle handle : JS::GetProfiledFrames(aCx, aJITAddress)) { uint32_t depth = jitFrameKeys.length(); JITFrameKey jitFrameKey{handle.canonicalAddress(), depth}; auto frameEntry = jitFrameToFrameJSONMap.lookupForAdd(jitFrameKey); if (!frameEntry) { if (!jitFrameToFrameJSONMap.add( frameEntry, jitFrameKey, JSONForJITFrame(aCx, handle, *mUniqueStrings))) { mLocalFailureLatchSource.SetFailure( "OOM in JITFrameInfo::AddInfoForRange adding jit->frame map"); return; } } if (!jitFrameKeys.append(jitFrameKey)) { mLocalFailureLatchSource.SetFailure( "OOM in JITFrameInfo::AddInfoForRange adding jit frame key"); return; } } if (!jitAddressToJITFrameMap.add(addressEntry, aJITAddress, std::move(jitFrameKeys))) { mLocalFailureLatchSource.SetFailure( "OOM in JITFrameInfo::AddInfoForRange adding addr->jit map"); return; } } }); if (!mRanges.append(JITFrameInfoForBufferRange{ aRangeStart, aRangeEnd, std::move(jitAddressToJITFrameMap), std::move(jitFrameToFrameJSONMap)})) { mLocalFailureLatchSource.SetFailure( "OOM in JITFrameInfo::AddInfoForRange adding range"); return; } } struct ProfileSample { uint32_t mStack = 0; double mTime = 0.0; Maybe mResponsiveness; RunningTimes mRunningTimes; }; // Write CPU measurements with "Delta" unit, which is some amount of work that // happened since the previous sample. static void WriteDelta(AutoArraySchemaWriter& aSchemaWriter, uint32_t aProperty, uint64_t aDelta) { aSchemaWriter.IntElement(aProperty, int64_t(aDelta)); } static void WriteSample(SpliceableJSONWriter& aWriter, const ProfileSample& aSample) { enum Schema : uint32_t { STACK = 0, TIME = 1, EVENT_DELAY = 2 #define RUNNING_TIME_SCHEMA(index, name, unit, jsonProperty) , name PROFILER_FOR_EACH_RUNNING_TIME(RUNNING_TIME_SCHEMA) #undef RUNNING_TIME_SCHEMA }; AutoArraySchemaWriter writer(aWriter); writer.IntElement(STACK, aSample.mStack); writer.TimeMsElement(TIME, aSample.mTime); if (aSample.mResponsiveness.isSome()) { writer.DoubleElement(EVENT_DELAY, *aSample.mResponsiveness); } #define RUNNING_TIME_STREAM(index, name, unit, jsonProperty) \ aSample.mRunningTimes.GetJson##name##unit().apply( \ [&writer](const uint64_t& aValue) { \ Write##unit(writer, name, aValue); \ }); PROFILER_FOR_EACH_RUNNING_TIME(RUNNING_TIME_STREAM) #undef RUNNING_TIME_STREAM } static void StreamMarkerAfterKind( ProfileBufferEntryReader& aER, ProcessStreamingContext& aProcessStreamingContext) { ThreadStreamingContext* threadData = nullptr; mozilla::base_profiler_markers_detail::DeserializeAfterKindAndStream( aER, [&](ProfilerThreadId aThreadId) -> baseprofiler::SpliceableJSONWriter* { threadData = aProcessStreamingContext.GetThreadStreamingContext(aThreadId); return threadData ? &threadData->mMarkersDataWriter : nullptr; }, [&](ProfileChunkedBuffer& aChunkedBuffer) { ProfilerBacktrace backtrace("", &aChunkedBuffer); MOZ_ASSERT(threadData, "threadData should have been set before calling here"); backtrace.StreamJSON(threadData->mMarkersDataWriter, aProcessStreamingContext.ProcessStartTime(), *threadData->mUniqueStacks); }, [&](mozilla::base_profiler_markers_detail::Streaming::DeserializerTag aTag) { MOZ_ASSERT(threadData, "threadData should have been set before calling here"); size_t payloadSize = aER.RemainingBytes(); ProfileBufferEntryReader::DoubleSpanOfConstBytes spans = aER.ReadSpans(payloadSize); if (MOZ_LIKELY(spans.IsSingleSpan())) { // Only a single span, we can just refer to it directly // instead of copying it. profiler::ffi::gecko_profiler_serialize_marker_for_tag( aTag, spans.mFirstOrOnly.Elements(), payloadSize, &threadData->mMarkersDataWriter); } else { // Two spans, we need to concatenate them by copying. uint8_t* payloadBuffer = new uint8_t[payloadSize]; spans.CopyBytesTo(payloadBuffer); profiler::ffi::gecko_profiler_serialize_marker_for_tag( aTag, payloadBuffer, payloadSize, &threadData->mMarkersDataWriter); delete[] payloadBuffer; } }); } class EntryGetter { public: explicit EntryGetter( ProfileChunkedBuffer::Reader& aReader, mozilla::FailureLatch& aFailureLatch, mozilla::ProgressLogger aProgressLogger = {}, uint64_t aInitialReadPos = 0, ProcessStreamingContext* aStreamingContextForMarkers = nullptr) : mFailureLatch(aFailureLatch), mStreamingContextForMarkers(aStreamingContextForMarkers), mBlockIt( aReader.At(ProfileBufferBlockIndex::CreateFromProfileBufferIndex( aInitialReadPos))), mBlockItEnd(aReader.end()), mRangeStart(mBlockIt.BufferRangeStart().ConvertToProfileBufferIndex()), mRangeSize( double(mBlockIt.BufferRangeEnd().ConvertToProfileBufferIndex() - mRangeStart)), mProgressLogger(std::move(aProgressLogger)) { SetLocalProgress(ProgressLogger::NO_LOCATION_UPDATE); if (!ReadLegacyOrEnd()) { // Find and read the next non-legacy entry. Next(); } } bool Has() const { return (!mFailureLatch.Failed()) && (mBlockIt != mBlockItEnd); } const ProfileBufferEntry& Get() const { MOZ_ASSERT(Has() || mFailureLatch.Failed(), "Caller should have checked `Has()` before `Get()`"); return mEntry; } void Next() { MOZ_ASSERT(Has() || mFailureLatch.Failed(), "Caller should have checked `Has()` before `Next()`"); ++mBlockIt; ReadUntilLegacyOrEnd(); } // Hand off the current iterator to the caller, which may be used to read // any kind of entries (legacy or modern). ProfileChunkedBuffer::BlockIterator Iterator() const { return mBlockIt; } // After `Iterator()` was used, we can restart from *after* its updated // position. void RestartAfter(const ProfileChunkedBuffer::BlockIterator& it) { mBlockIt = it; if (!Has()) { return; } Next(); } ProfileBufferBlockIndex CurBlockIndex() const { return mBlockIt.CurrentBlockIndex(); } uint64_t CurPos() const { return CurBlockIndex().ConvertToProfileBufferIndex(); } void SetLocalProgress(const char* aLocation) { mProgressLogger.SetLocalProgress( ProportionValue{double(CurBlockIndex().ConvertToProfileBufferIndex() - mRangeStart) / mRangeSize}, aLocation); } private: // Try to read the entry at the current `mBlockIt` position. // * If we're at the end of the buffer, just return `true`. // * If there is a "legacy" entry (containing a real `ProfileBufferEntry`), // read it into `mEntry`, and return `true` as well. // * Otherwise the entry contains a "modern" type that cannot be read into // `mEntry`, return `false` (so `EntryGetter` can skip to another entry). bool ReadLegacyOrEnd() { if (!Has()) { return true; } // Read the entry "kind", which is always at the start of all entries. ProfileBufferEntryReader er = *mBlockIt; auto type = static_cast( er.ReadObject()); MOZ_ASSERT(static_cast(type) < static_cast( ProfileBufferEntry::Kind::MODERN_LIMIT)); if (type >= ProfileBufferEntry::Kind::LEGACY_LIMIT) { if (type == ProfileBufferEntry::Kind::Marker && mStreamingContextForMarkers) { StreamMarkerAfterKind(er, *mStreamingContextForMarkers); if (!Has()) { return true; } SetLocalProgress("Processed marker"); } er.SetRemainingBytes(0); return false; } // Here, we have a legacy item, we need to read it from the start. // Because the above `ReadObject` moved the reader, we ned to reset it to // the start of the entry before reading the whole entry. er = *mBlockIt; er.ReadBytes(&mEntry, er.RemainingBytes()); return true; } void ReadUntilLegacyOrEnd() { for (;;) { if (ReadLegacyOrEnd()) { // Either we're at the end, or we could read a legacy entry -> Done. break; } // Otherwise loop around until we hit a legacy entry or the end. ++mBlockIt; } SetLocalProgress(ProgressLogger::NO_LOCATION_UPDATE); } mozilla::FailureLatch& mFailureLatch; ProcessStreamingContext* const mStreamingContextForMarkers; ProfileBufferEntry mEntry; ProfileChunkedBuffer::BlockIterator mBlockIt; const ProfileChunkedBuffer::BlockIterator mBlockItEnd; // Progress logger, and the data needed to compute the current relative // position in the buffer. const mozilla::ProfileBufferIndex mRangeStart; const double mRangeSize; mozilla::ProgressLogger mProgressLogger; }; // The following grammar shows legal sequences of profile buffer entries. // The sequences beginning with a ThreadId entry are known as "samples". // // ( // ( /* Samples */ // ThreadId // TimeBeforeCompactStack // RunningTimes? // UnresponsivenessDurationMs? // CompactStack // /* internally including: // ( NativeLeafAddr // | Label FrameFlags? DynamicStringFragment* // LineNumber? CategoryPair? // | JitReturnAddr // )+ // */ // ) // | ( /* Reference to a previous identical sample */ // ThreadId // TimeBeforeSameSample // RunningTimes? // SameSample // ) // | Marker // | ( /* Counters */ // CounterId // Time // ( // CounterKey // Count // Number? // )* // ) // | CollectionStart // | CollectionEnd // | Pause // | Resume // | ( ProfilerOverheadTime /* Sampling start timestamp */ // ProfilerOverheadDuration /* Lock acquisition */ // ProfilerOverheadDuration /* Expired markers cleaning */ // ProfilerOverheadDuration /* Counters */ // ProfilerOverheadDuration /* Threads */ // ) // )* // // The most complicated part is the stack entry sequence that begins with // Label. Here are some examples. // // - ProfilingStack frames without a dynamic string: // // Label("js::RunScript") // CategoryPair(JS::ProfilingCategoryPair::JS) // // Label("XREMain::XRE_main") // LineNumber(4660) // CategoryPair(JS::ProfilingCategoryPair::OTHER) // // Label("ElementRestyler::ComputeStyleChangeFor") // LineNumber(3003) // CategoryPair(JS::ProfilingCategoryPair::CSS) // // - ProfilingStack frames with a dynamic string: // // Label("nsObserverService::NotifyObservers") // FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_LABEL_FRAME)) // DynamicStringFragment("domwindo") // DynamicStringFragment("wopened") // LineNumber(291) // CategoryPair(JS::ProfilingCategoryPair::OTHER) // // Label("") // FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_JS_FRAME)) // DynamicStringFragment("closeWin") // DynamicStringFragment("dow (chr") // DynamicStringFragment("ome://gl") // DynamicStringFragment("obal/con") // DynamicStringFragment("tent/glo") // DynamicStringFragment("balOverl") // DynamicStringFragment("ay.js:5)") // DynamicStringFragment("") # this string holds the closing '\0' // LineNumber(25) // CategoryPair(JS::ProfilingCategoryPair::JS) // // Label("") // FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_JS_FRAME)) // DynamicStringFragment("bound (s") // DynamicStringFragment("elf-host") // DynamicStringFragment("ed:914)") // LineNumber(945) // CategoryPair(JS::ProfilingCategoryPair::JS) // // - A profiling stack frame with an overly long dynamic string: // // Label("") // FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_LABEL_FRAME)) // DynamicStringFragment("(too lon") // DynamicStringFragment("g)") // LineNumber(100) // CategoryPair(JS::ProfilingCategoryPair::NETWORK) // // - A wasm JIT frame: // // Label("") // FrameFlags(uint64_t(0)) // DynamicStringFragment("wasm-fun") // DynamicStringFragment("ction[87") // DynamicStringFragment("36] (blo") // DynamicStringFragment("b:http:/") // DynamicStringFragment("/webasse") // DynamicStringFragment("mbly.org") // DynamicStringFragment("/3dc5759") // DynamicStringFragment("4-ce58-4") // DynamicStringFragment("626-975b") // DynamicStringFragment("-08ad116") // DynamicStringFragment("30bc1:38") // DynamicStringFragment("29856)") // // - A JS frame in a synchronous sample: // // Label("") // FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_LABEL_FRAME)) // DynamicStringFragment("u (https") // DynamicStringFragment("://perf-") // DynamicStringFragment("html.io/") // DynamicStringFragment("ac0da204") // DynamicStringFragment("aaa44d75") // DynamicStringFragment("a800.bun") // DynamicStringFragment("dle.js:2") // DynamicStringFragment("5)") // Because this is a format entirely internal to the Profiler, any parsing // error indicates a bug in the ProfileBuffer writing or the parser itself, // or possibly flaky hardware. #define ERROR_AND_CONTINUE(msg) \ { \ fprintf(stderr, "ProfileBuffer parse error: %s", msg); \ MOZ_ASSERT(false, msg); \ continue; \ } struct StreamingParametersForThread { SpliceableJSONWriter& mWriter; UniqueStacks& mUniqueStacks; ThreadStreamingContext::PreviousStackState& mPreviousStackState; uint32_t& mPreviousStack; StreamingParametersForThread( SpliceableJSONWriter& aWriter, UniqueStacks& aUniqueStacks, ThreadStreamingContext::PreviousStackState& aPreviousStackState, uint32_t& aPreviousStack) : mWriter(aWriter), mUniqueStacks(aUniqueStacks), mPreviousStackState(aPreviousStackState), mPreviousStack(aPreviousStack) {} }; // GetStreamingParametersForThreadCallback: // (ProfilerThreadId) -> Maybe template ProfilerThreadId ProfileBuffer::DoStreamSamplesAndMarkersToJSON( mozilla::FailureLatch& aFailureLatch, GetStreamingParametersForThreadCallback&& aGetStreamingParametersForThreadCallback, double aSinceTime, ProcessStreamingContext* aStreamingContextForMarkers, mozilla::ProgressLogger aProgressLogger) const { UniquePtr dynStrBuf = MakeUnique(kMaxFrameKeyLength); return mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) { MOZ_ASSERT(aReader, "ProfileChunkedBuffer cannot be out-of-session when sampler is " "running"); ProfilerThreadId processedThreadId; EntryGetter e(*aReader, aFailureLatch, std::move(aProgressLogger), /* aInitialReadPos */ 0, aStreamingContextForMarkers); for (;;) { // This block skips entries until we find the start of the next sample. // This is useful in three situations. // // - The circular buffer overwrites old entries, so when we start parsing // we might be in the middle of a sample, and we must skip forward to // the start of the next sample. // // - We skip samples that don't have an appropriate ThreadId or Time. // // - We skip range Pause, Resume, CollectionStart, Marker, Counter // and CollectionEnd entries between samples. while (e.Has()) { if (e.Get().IsThreadId()) { break; } e.Next(); } if (!e.Has()) { break; } // Due to the skip_to_next_sample block above, if we have an entry here it // must be a ThreadId entry. MOZ_ASSERT(e.Get().IsThreadId()); ProfilerThreadId threadId = e.Get().GetThreadId(); e.Next(); Maybe streamingParameters = std::forward( aGetStreamingParametersForThreadCallback)(threadId); // Ignore samples that are for the wrong thread. if (!streamingParameters) { continue; } SpliceableJSONWriter& writer = streamingParameters->mWriter; UniqueStacks& uniqueStacks = streamingParameters->mUniqueStacks; ThreadStreamingContext::PreviousStackState& previousStackState = streamingParameters->mPreviousStackState; uint32_t& previousStack = streamingParameters->mPreviousStack; auto ReadStack = [&](EntryGetter& e, double time, uint64_t entryPosition, const Maybe& unresponsiveDuration, const RunningTimes& runningTimes) { if (writer.Failed()) { return; } Maybe maybeStack = uniqueStacks.BeginStack(UniqueStacks::FrameKey("(root)")); if (!maybeStack) { writer.SetFailure("BeginStack failure"); return; } UniqueStacks::StackKey stack = *maybeStack; int numFrames = 0; while (e.Has()) { if (e.Get().IsNativeLeafAddr()) { numFrames++; void* pc = e.Get().GetPtr(); e.Next(); nsAutoCString functionNameOrAddress = uniqueStacks.FunctionNameOrAddress(pc); maybeStack = uniqueStacks.AppendFrame( stack, UniqueStacks::FrameKey(functionNameOrAddress.get())); if (!maybeStack) { writer.SetFailure("AppendFrame failure"); return; } stack = *maybeStack; } else if (e.Get().IsLabel()) { numFrames++; const char* label = e.Get().GetString(); e.Next(); using FrameFlags = js::ProfilingStackFrame::Flags; uint32_t frameFlags = 0; if (e.Has() && e.Get().IsFrameFlags()) { frameFlags = uint32_t(e.Get().GetUint64()); e.Next(); } bool relevantForJS = frameFlags & uint32_t(FrameFlags::RELEVANT_FOR_JS); bool isBaselineInterp = frameFlags & uint32_t(FrameFlags::IS_BLINTERP_FRAME); // Copy potential dynamic string fragments into dynStrBuf, so that // dynStrBuf will then contain the entire dynamic string. size_t i = 0; dynStrBuf[0] = '\0'; while (e.Has()) { if (e.Get().IsDynamicStringFragment()) { char chars[ProfileBufferEntry::kNumChars]; e.Get().CopyCharsInto(chars); for (char c : chars) { if (i < kMaxFrameKeyLength) { dynStrBuf[i] = c; i++; } } e.Next(); } else { break; } } dynStrBuf[kMaxFrameKeyLength - 1] = '\0'; bool hasDynamicString = (i != 0); nsAutoCStringN<1024> frameLabel; if (label[0] != '\0' && hasDynamicString) { if (frameFlags & uint32_t(FrameFlags::STRING_TEMPLATE_METHOD)) { frameLabel.AppendPrintf("%s.%s", label, dynStrBuf.get()); } else if (frameFlags & uint32_t(FrameFlags::STRING_TEMPLATE_GETTER)) { frameLabel.AppendPrintf("get %s.%s", label, dynStrBuf.get()); } else if (frameFlags & uint32_t(FrameFlags::STRING_TEMPLATE_SETTER)) { frameLabel.AppendPrintf("set %s.%s", label, dynStrBuf.get()); } else { frameLabel.AppendPrintf("%s %s", label, dynStrBuf.get()); } } else if (hasDynamicString) { frameLabel.Append(dynStrBuf.get()); } else { frameLabel.Append(label); } uint64_t innerWindowID = 0; if (e.Has() && e.Get().IsInnerWindowID()) { innerWindowID = uint64_t(e.Get().GetUint64()); e.Next(); } Maybe line; if (e.Has() && e.Get().IsLineNumber()) { line = Some(unsigned(e.Get().GetInt())); e.Next(); } Maybe column; if (e.Has() && e.Get().IsColumnNumber()) { column = Some(unsigned(e.Get().GetInt())); e.Next(); } Maybe categoryPair; if (e.Has() && e.Get().IsCategoryPair()) { categoryPair = Some(JS::ProfilingCategoryPair(uint32_t(e.Get().GetInt()))); e.Next(); } maybeStack = uniqueStacks.AppendFrame( stack, UniqueStacks::FrameKey(std::move(frameLabel), relevantForJS, isBaselineInterp, innerWindowID, line, column, categoryPair)); if (!maybeStack) { writer.SetFailure("AppendFrame failure"); return; } stack = *maybeStack; } else if (e.Get().IsJitReturnAddr()) { numFrames++; // A JIT frame may expand to multiple frames due to inlining. void* pc = e.Get().GetPtr(); const Maybe>& frameKeys = uniqueStacks.LookupFramesForJITAddressFromBufferPos( pc, entryPosition ? entryPosition : e.CurPos()); MOZ_RELEASE_ASSERT( frameKeys, "Attempting to stream samples for a buffer range " "for which we don't have JITFrameInfo?"); for (const UniqueStacks::FrameKey& frameKey : *frameKeys) { maybeStack = uniqueStacks.AppendFrame(stack, frameKey); if (!maybeStack) { writer.SetFailure("AppendFrame failure"); return; } stack = *maybeStack; } e.Next(); } else { break; } } // Even if this stack is considered empty, it contains the root frame, // which needs to be in the JSON output because following "same samples" // may refer to it when reusing this sample.mStack. const Maybe stackIndex = uniqueStacks.GetOrAddStackIndex(stack); if (!stackIndex) { writer.SetFailure("Can't add unique string for stack"); return; } // And store that possibly-empty stack in case it's followed by "same // sample" entries. previousStack = *stackIndex; previousStackState = (numFrames == 0) ? ThreadStreamingContext::eStackWasEmpty : ThreadStreamingContext::eStackWasNotEmpty; // Even if too old or empty, we did process a sample for this thread id. processedThreadId = threadId; // Discard samples that are too old. if (time < aSinceTime) { return; } if (numFrames == 0 && runningTimes.IsEmpty()) { // It is possible to have empty stacks if native stackwalking is // disabled. Skip samples with empty stacks, unless we have useful // running times. return; } WriteSample(writer, ProfileSample{*stackIndex, time, unresponsiveDuration, runningTimes}); }; // End of `ReadStack(EntryGetter&)` lambda. if (e.Has() && e.Get().IsTime()) { double time = e.Get().GetDouble(); e.Next(); // Note: Even if this sample is too old (before aSinceTime), we still // need to read it, so that its frames are in the tables, in case there // is a same-sample following it that would be after aSinceTime, which // would need these frames to be present. ReadStack(e, time, 0, Nothing{}, RunningTimes{}); e.SetLocalProgress("Processed sample"); } else if (e.Has() && e.Get().IsTimeBeforeCompactStack()) { double time = e.Get().GetDouble(); // Note: Even if this sample is too old (before aSinceTime), we still // need to read it, so that its frames are in the tables, in case there // is a same-sample following it that would be after aSinceTime, which // would need these frames to be present. RunningTimes runningTimes; Maybe unresponsiveDuration; ProfileChunkedBuffer::BlockIterator it = e.Iterator(); for (;;) { ++it; if (it.IsAtEnd()) { break; } ProfileBufferEntryReader er = *it; ProfileBufferEntry::Kind kind = er.ReadObject(); // There may be running times before the CompactStack. if (kind == ProfileBufferEntry::Kind::RunningTimes) { er.ReadIntoObject(runningTimes); continue; } // There may be an UnresponsiveDurationMs before the CompactStack. if (kind == ProfileBufferEntry::Kind::UnresponsiveDurationMs) { unresponsiveDuration = Some(er.ReadObject()); continue; } if (kind == ProfileBufferEntry::Kind::CompactStack) { ProfileChunkedBuffer tempBuffer( ProfileChunkedBuffer::ThreadSafety::WithoutMutex, WorkerChunkManager()); er.ReadIntoObject(tempBuffer); tempBuffer.Read([&](ProfileChunkedBuffer::Reader* aReader) { MOZ_ASSERT(aReader, "Local ProfileChunkedBuffer cannot be out-of-session"); // This is a compact stack, it should only contain one sample. EntryGetter stackEntryGetter(*aReader, aFailureLatch); ReadStack(stackEntryGetter, time, it.CurrentBlockIndex().ConvertToProfileBufferIndex(), unresponsiveDuration, runningTimes); }); WorkerChunkManager().Reset(tempBuffer.GetAllChunks()); break; } if (kind == ProfileBufferEntry::Kind::Marker && aStreamingContextForMarkers) { StreamMarkerAfterKind(er, *aStreamingContextForMarkers); continue; } MOZ_ASSERT(kind >= ProfileBufferEntry::Kind::LEGACY_LIMIT, "There should be no legacy entries between " "TimeBeforeCompactStack and CompactStack"); er.SetRemainingBytes(0); } e.RestartAfter(it); e.SetLocalProgress("Processed compact sample"); } else if (e.Has() && e.Get().IsTimeBeforeSameSample()) { if (previousStackState == ThreadStreamingContext::eNoStackYet) { // We don't have any full sample yet, we cannot duplicate a "previous" // one. This should only happen at most once per thread, for the very // first sample. continue; } ProfileSample sample; // Keep the same `mStack` as previously output. // Note that it may be empty, this is checked below before writing it. sample.mStack = previousStack; sample.mTime = e.Get().GetDouble(); // Ignore samples that are too old. if (sample.mTime < aSinceTime) { e.Next(); continue; } sample.mResponsiveness = Nothing{}; sample.mRunningTimes.Clear(); ProfileChunkedBuffer::BlockIterator it = e.Iterator(); for (;;) { ++it; if (it.IsAtEnd()) { break; } ProfileBufferEntryReader er = *it; ProfileBufferEntry::Kind kind = er.ReadObject(); // There may be running times before the SameSample. if (kind == ProfileBufferEntry::Kind::RunningTimes) { er.ReadIntoObject(sample.mRunningTimes); continue; } if (kind == ProfileBufferEntry::Kind::SameSample) { if (previousStackState == ThreadStreamingContext::eStackWasEmpty && sample.mRunningTimes.IsEmpty()) { // Skip samples with empty stacks, unless we have useful running // times. break; } WriteSample(writer, sample); break; } if (kind == ProfileBufferEntry::Kind::Marker && aStreamingContextForMarkers) { StreamMarkerAfterKind(er, *aStreamingContextForMarkers); continue; } MOZ_ASSERT(kind >= ProfileBufferEntry::Kind::LEGACY_LIMIT, "There should be no legacy entries between " "TimeBeforeSameSample and SameSample"); er.SetRemainingBytes(0); } e.RestartAfter(it); e.SetLocalProgress("Processed repeated sample"); } else { ERROR_AND_CONTINUE("expected a Time entry"); } } return processedThreadId; }); } ProfilerThreadId ProfileBuffer::StreamSamplesToJSON( SpliceableJSONWriter& aWriter, ProfilerThreadId aThreadId, double aSinceTime, UniqueStacks& aUniqueStacks, mozilla::ProgressLogger aProgressLogger) const { ThreadStreamingContext::PreviousStackState previousStackState = ThreadStreamingContext::eNoStackYet; uint32_t stack = 0u; #ifdef DEBUG int processedCount = 0; #endif // DEBUG return DoStreamSamplesAndMarkersToJSON( aWriter.SourceFailureLatch(), [&](ProfilerThreadId aReadThreadId) { Maybe streamingParameters; #ifdef DEBUG ++processedCount; MOZ_ASSERT( aThreadId.IsSpecified() || (processedCount == 1 && aReadThreadId.IsSpecified()), "Unspecified aThreadId should only be used with 1-sample buffer"); #endif // DEBUG if (!aThreadId.IsSpecified() || aThreadId == aReadThreadId) { streamingParameters.emplace(aWriter, aUniqueStacks, previousStackState, stack); } return streamingParameters; }, aSinceTime, /* aStreamingContextForMarkers */ nullptr, std::move(aProgressLogger)); } void ProfileBuffer::StreamSamplesAndMarkersToJSON( ProcessStreamingContext& aProcessStreamingContext, mozilla::ProgressLogger aProgressLogger) const { (void)DoStreamSamplesAndMarkersToJSON( aProcessStreamingContext.SourceFailureLatch(), [&](ProfilerThreadId aReadThreadId) { Maybe streamingParameters; ThreadStreamingContext* threadData = aProcessStreamingContext.GetThreadStreamingContext(aReadThreadId); if (threadData) { streamingParameters.emplace( threadData->mSamplesDataWriter, *threadData->mUniqueStacks, threadData->mPreviousStackState, threadData->mPreviousStack); } return streamingParameters; }, aProcessStreamingContext.GetSinceTime(), &aProcessStreamingContext, std::move(aProgressLogger)); } void ProfileBuffer::AddJITInfoForRange( uint64_t aRangeStart, ProfilerThreadId aThreadId, JSContext* aContext, JITFrameInfo& aJITFrameInfo, mozilla::ProgressLogger aProgressLogger) const { // We can only process JitReturnAddr entries if we have a JSContext. MOZ_RELEASE_ASSERT(aContext); aRangeStart = std::max(aRangeStart, BufferRangeStart()); aJITFrameInfo.AddInfoForRange( aRangeStart, BufferRangeEnd(), aContext, [&](const std::function& aJITAddressConsumer) { // Find all JitReturnAddr entries in the given range for the given // thread, and call aJITAddressConsumer with those addresses. mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) { MOZ_ASSERT(aReader, "ProfileChunkedBuffer cannot be out-of-session when " "sampler is running"); EntryGetter e(*aReader, aJITFrameInfo.LocalFailureLatchSource(), std::move(aProgressLogger), aRangeStart); while (true) { // Advance to the next ThreadId entry. while (e.Has() && !e.Get().IsThreadId()) { e.Next(); } if (!e.Has()) { break; } MOZ_ASSERT(e.Get().IsThreadId()); ProfilerThreadId threadId = e.Get().GetThreadId(); e.Next(); // Ignore samples that are for a different thread. if (threadId != aThreadId) { continue; } if (e.Has() && e.Get().IsTime()) { // Legacy stack. e.Next(); while (e.Has() && !e.Get().IsThreadId()) { if (e.Get().IsJitReturnAddr()) { aJITAddressConsumer(e.Get().GetPtr()); } e.Next(); } } else if (e.Has() && e.Get().IsTimeBeforeCompactStack()) { // Compact stack. ProfileChunkedBuffer::BlockIterator it = e.Iterator(); for (;;) { ++it; if (it.IsAtEnd()) { break; } ProfileBufferEntryReader er = *it; ProfileBufferEntry::Kind kind = er.ReadObject(); if (kind == ProfileBufferEntry::Kind::CompactStack) { ProfileChunkedBuffer tempBuffer( ProfileChunkedBuffer::ThreadSafety::WithoutMutex, WorkerChunkManager()); er.ReadIntoObject(tempBuffer); tempBuffer.Read([&](ProfileChunkedBuffer::Reader* aReader) { MOZ_ASSERT( aReader, "Local ProfileChunkedBuffer cannot be out-of-session"); EntryGetter stackEntryGetter( *aReader, aJITFrameInfo.LocalFailureLatchSource()); while (stackEntryGetter.Has()) { if (stackEntryGetter.Get().IsJitReturnAddr()) { aJITAddressConsumer(stackEntryGetter.Get().GetPtr()); } stackEntryGetter.Next(); } }); WorkerChunkManager().Reset(tempBuffer.GetAllChunks()); break; } MOZ_ASSERT(kind >= ProfileBufferEntry::Kind::LEGACY_LIMIT, "There should be no legacy entries between " "TimeBeforeCompactStack and CompactStack"); er.SetRemainingBytes(0); } e.Next(); } else if (e.Has() && e.Get().IsTimeBeforeSameSample()) { // Sample index, nothing to do. } else { ERROR_AND_CONTINUE("expected a Time entry"); } } }); }); } void ProfileBuffer::StreamMarkersToJSON( SpliceableJSONWriter& aWriter, ProfilerThreadId aThreadId, const TimeStamp& aProcessStartTime, double aSinceTime, UniqueStacks& aUniqueStacks, mozilla::ProgressLogger aProgressLogger) const { mEntries.ReadEach([&](ProfileBufferEntryReader& aER) { auto type = static_cast( aER.ReadObject()); MOZ_ASSERT(static_cast(type) < static_cast( ProfileBufferEntry::Kind::MODERN_LIMIT)); if (type == ProfileBufferEntry::Kind::Marker) { mozilla::base_profiler_markers_detail::DeserializeAfterKindAndStream( aER, [&](const ProfilerThreadId& aMarkerThreadId) { return (!aThreadId.IsSpecified() || aMarkerThreadId == aThreadId) ? &aWriter : nullptr; }, [&](ProfileChunkedBuffer& aChunkedBuffer) { ProfilerBacktrace backtrace("", &aChunkedBuffer); backtrace.StreamJSON(aWriter, aProcessStartTime, aUniqueStacks); }, [&](mozilla::base_profiler_markers_detail::Streaming::DeserializerTag aTag) { size_t payloadSize = aER.RemainingBytes(); ProfileBufferEntryReader::DoubleSpanOfConstBytes spans = aER.ReadSpans(payloadSize); if (MOZ_LIKELY(spans.IsSingleSpan())) { // Only a single span, we can just refer to it directly // instead of copying it. profiler::ffi::gecko_profiler_serialize_marker_for_tag( aTag, spans.mFirstOrOnly.Elements(), payloadSize, &aWriter); } else { // Two spans, we need to concatenate them by copying. uint8_t* payloadBuffer = new uint8_t[payloadSize]; spans.CopyBytesTo(payloadBuffer); profiler::ffi::gecko_profiler_serialize_marker_for_tag( aTag, payloadBuffer, payloadSize, &aWriter); delete[] payloadBuffer; } }); } else { // The entry was not a marker, we need to skip to the end. aER.SetRemainingBytes(0); } }); } void ProfileBuffer::StreamProfilerOverheadToJSON( SpliceableJSONWriter& aWriter, const TimeStamp& aProcessStartTime, double aSinceTime, mozilla::ProgressLogger aProgressLogger) const { mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) { MOZ_ASSERT(aReader, "ProfileChunkedBuffer cannot be out-of-session when sampler is " "running"); EntryGetter e(*aReader, aWriter.SourceFailureLatch(), std::move(aProgressLogger)); enum Schema : uint32_t { TIME = 0, LOCKING = 1, MARKER_CLEANING = 2, COUNTERS = 3, THREADS = 4 }; aWriter.StartObjectProperty("profilerOverhead"); aWriter.StartObjectProperty("samples"); // Stream all sampling overhead data. We skip other entries, because we // process them in StreamSamplesToJSON()/etc. { JSONSchemaWriter schema(aWriter); schema.WriteField("time"); schema.WriteField("locking"); schema.WriteField("expiredMarkerCleaning"); schema.WriteField("counters"); schema.WriteField("threads"); } aWriter.StartArrayProperty("data"); double firstTime = 0.0; double lastTime = 0.0; ProfilerStats intervals, overheads, lockings, cleanings, counters, threads; while (e.Has()) { // valid sequence: ProfilerOverheadTime, ProfilerOverheadDuration * 4 if (e.Get().IsProfilerOverheadTime()) { double time = e.Get().GetDouble(); if (time >= aSinceTime) { e.Next(); if (!e.Has() || !e.Get().IsProfilerOverheadDuration()) { ERROR_AND_CONTINUE( "expected a ProfilerOverheadDuration entry after " "ProfilerOverheadTime"); } double locking = e.Get().GetDouble(); e.Next(); if (!e.Has() || !e.Get().IsProfilerOverheadDuration()) { ERROR_AND_CONTINUE( "expected a ProfilerOverheadDuration entry after " "ProfilerOverheadTime,ProfilerOverheadDuration"); } double cleaning = e.Get().GetDouble(); e.Next(); if (!e.Has() || !e.Get().IsProfilerOverheadDuration()) { ERROR_AND_CONTINUE( "expected a ProfilerOverheadDuration entry after " "ProfilerOverheadTime,ProfilerOverheadDuration*2"); } double counter = e.Get().GetDouble(); e.Next(); if (!e.Has() || !e.Get().IsProfilerOverheadDuration()) { ERROR_AND_CONTINUE( "expected a ProfilerOverheadDuration entry after " "ProfilerOverheadTime,ProfilerOverheadDuration*3"); } double thread = e.Get().GetDouble(); if (firstTime == 0.0) { firstTime = time; } else { // Note that we'll have 1 fewer interval than other numbers (because // we need both ends of an interval to know its duration). The final // difference should be insignificant over the expected many // thousands of iterations. intervals.Count(time - lastTime); } lastTime = time; overheads.Count(locking + cleaning + counter + thread); lockings.Count(locking); cleanings.Count(cleaning); counters.Count(counter); threads.Count(thread); AutoArraySchemaWriter writer(aWriter); writer.TimeMsElement(TIME, time); writer.DoubleElement(LOCKING, locking); writer.DoubleElement(MARKER_CLEANING, cleaning); writer.DoubleElement(COUNTERS, counter); writer.DoubleElement(THREADS, thread); } } e.Next(); } aWriter.EndArray(); // data aWriter.EndObject(); // samples // Only output statistics if there is at least one full interval (and // therefore at least two samplings.) if (intervals.n > 0) { aWriter.StartObjectProperty("statistics"); aWriter.DoubleProperty("profiledDuration", lastTime - firstTime); aWriter.IntProperty("samplingCount", overheads.n); aWriter.DoubleProperty("overheadDurations", overheads.sum); aWriter.DoubleProperty("overheadPercentage", overheads.sum / (lastTime - firstTime)); #define PROFILER_STATS(name, var) \ aWriter.DoubleProperty("mean" name, (var).sum / (var).n); \ aWriter.DoubleProperty("min" name, (var).min); \ aWriter.DoubleProperty("max" name, (var).max); PROFILER_STATS("Interval", intervals); PROFILER_STATS("Overhead", overheads); PROFILER_STATS("Lockings", lockings); PROFILER_STATS("Cleaning", cleanings); PROFILER_STATS("Counter", counters); PROFILER_STATS("Thread", threads); #undef PROFILER_STATS aWriter.EndObject(); // statistics } aWriter.EndObject(); // profilerOverhead }); } struct CounterKeyedSample { double mTime; uint64_t mNumber; int64_t mCount; }; using CounterKeyedSamples = Vector; static LazyLogModule sFuzzyfoxLog("Fuzzyfox"); using CounterMap = HashMap; // HashMap lookup, if not found, a default value is inserted. // Returns reference to (existing or new) value inside the HashMap. template static auto& LookupOrAdd(HashM& aMap, Key&& aKey) { auto addPtr = aMap.lookupForAdd(aKey); if (!addPtr) { MOZ_RELEASE_ASSERT(aMap.add(addPtr, std::forward(aKey), typename HashM::Entry::ValueType{})); MOZ_ASSERT(!!addPtr); } return addPtr->value(); } void ProfileBuffer::StreamCountersToJSON( SpliceableJSONWriter& aWriter, const TimeStamp& aProcessStartTime, double aSinceTime, mozilla::ProgressLogger aProgressLogger) const { // Because this is a format entirely internal to the Profiler, any parsing // error indicates a bug in the ProfileBuffer writing or the parser itself, // or possibly flaky hardware. mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) { MOZ_ASSERT(aReader, "ProfileChunkedBuffer cannot be out-of-session when sampler is " "running"); EntryGetter e(*aReader, aWriter.SourceFailureLatch(), std::move(aProgressLogger)); enum Schema : uint32_t { TIME = 0, NUMBER = 1, COUNT = 2 }; // Stream all counters. We skip other entries, because we process them in // StreamSamplesToJSON()/etc. // // Valid sequence in the buffer: // CounterID // Time // ( CounterKey Count Number? )* // // And the JSON (example): // "counters": { // "name": "malloc", // "category": "Memory", // "description": "Amount of allocated memory", // "sample_groups": { // "id": 0, // "samples": { // "schema": {"time": 0, "number": 1, "count": 2}, // "data": [ // [ // 16117.033968000002, // 2446216, // 6801320 // ], // [ // 16118.037638, // 2446216, // 6801320 // ], // ], // } // } // }, // Build the map of counters and populate it HashMap counters; while (e.Has()) { // skip all non-Counters, including if we start in the middle of a counter if (e.Get().IsCounterId()) { void* id = e.Get().GetPtr(); CounterMap& counter = LookupOrAdd(counters, id); e.Next(); if (!e.Has() || !e.Get().IsTime()) { ERROR_AND_CONTINUE("expected a Time entry"); } double time = e.Get().GetDouble(); e.Next(); if (time >= aSinceTime) { while (e.Has() && e.Get().IsCounterKey()) { uint64_t key = e.Get().GetUint64(); CounterKeyedSamples& data = LookupOrAdd(counter, key); e.Next(); if (!e.Has() || !e.Get().IsCount()) { ERROR_AND_CONTINUE("expected a Count entry"); } int64_t count = e.Get().GetUint64(); e.Next(); uint64_t number; if (!e.Has() || !e.Get().IsNumber()) { number = 0; } else { number = e.Get().GetInt64(); e.Next(); } CounterKeyedSample sample = {time, number, count}; MOZ_RELEASE_ASSERT(data.append(sample)); } } else { // skip counter sample - only need to skip the initial counter // id, then let the loop at the top skip the rest } } else { e.Next(); } } // we have a map of a map of counter entries; dump them to JSON if (counters.count() == 0) { return; } aWriter.StartArrayProperty("counters"); for (auto iter = counters.iter(); !iter.done(); iter.next()) { CounterMap& counter = iter.get().value(); const BaseProfilerCount* base_counter = static_cast(iter.get().key()); aWriter.Start(); aWriter.StringProperty("name", MakeStringSpan(base_counter->mLabel)); aWriter.StringProperty("category", MakeStringSpan(base_counter->mCategory)); aWriter.StringProperty("description", MakeStringSpan(base_counter->mDescription)); aWriter.StartArrayProperty("sample_groups"); for (auto counter_iter = counter.iter(); !counter_iter.done(); counter_iter.next()) { CounterKeyedSamples& samples = counter_iter.get().value(); uint64_t key = counter_iter.get().key(); size_t size = samples.length(); if (size == 0) { continue; } aWriter.StartObjectElement(); { aWriter.IntProperty("id", static_cast(key)); aWriter.StartObjectProperty("samples"); { // XXX Can we assume a missing count means 0? JSONSchemaWriter schema(aWriter); schema.WriteField("time"); schema.WriteField("number"); schema.WriteField("count"); } aWriter.StartArrayProperty("data"); double previousSkippedTime = 0.0; uint64_t previousNumber = 0; int64_t previousCount = 0; for (size_t i = 0; i < size; i++) { // Encode as deltas, and only encode if different than the previous // or next sample; Always write the first and last samples. if (i == 0 || i == size - 1 || samples[i].mNumber != previousNumber || samples[i].mCount != previousCount || // Ensure we ouput the first 0 before skipping samples. (i >= 2 && (samples[i - 2].mNumber != previousNumber || samples[i - 2].mCount != previousCount))) { if (i != 0 && samples[i].mTime >= samples[i - 1].mTime) { MOZ_LOG(sFuzzyfoxLog, mozilla::LogLevel::Error, ("Fuzzyfox Profiler Assertion: %f >= %f", samples[i].mTime, samples[i - 1].mTime)); } MOZ_ASSERT(i == 0 || samples[i].mTime >= samples[i - 1].mTime); MOZ_ASSERT(samples[i].mNumber >= previousNumber); MOZ_ASSERT(samples[i].mNumber - previousNumber <= uint64_t(std::numeric_limits::max())); int64_t numberDelta = static_cast(samples[i].mNumber - previousNumber); int64_t countDelta = samples[i].mCount - previousCount; if (previousSkippedTime != 0.0 && (numberDelta != 0 || countDelta != 0)) { // Write the last skipped sample, unless the new one is all // zeroes (that'd be redundant) This is useful to know when a // certain value was last sampled, so that the front-end graph // will be more correct. AutoArraySchemaWriter writer(aWriter); writer.TimeMsElement(TIME, previousSkippedTime); // The deltas are effectively zeroes, since no change happened // between the last actually-written sample and the last skipped // one. writer.IntElement(NUMBER, 0); writer.IntElement(COUNT, 0); } AutoArraySchemaWriter writer(aWriter); writer.TimeMsElement(TIME, samples[i].mTime); writer.IntElement(NUMBER, numberDelta); writer.IntElement(COUNT, countDelta); previousSkippedTime = 0.0; previousNumber = samples[i].mNumber; previousCount = samples[i].mCount; } else { previousSkippedTime = samples[i].mTime; } } aWriter.EndArray(); // data aWriter.EndObject(); // samples } aWriter.EndObject(); // sample_groups item } aWriter.EndArray(); // sample groups aWriter.End(); // for each counter } aWriter.EndArray(); // counters }); } #undef ERROR_AND_CONTINUE static void AddPausedRange(SpliceableJSONWriter& aWriter, const char* aReason, const Maybe& aStartTime, const Maybe& aEndTime) { aWriter.Start(); if (aStartTime) { aWriter.TimeDoubleMsProperty("startTime", *aStartTime); } else { aWriter.NullProperty("startTime"); } if (aEndTime) { aWriter.TimeDoubleMsProperty("endTime", *aEndTime); } else { aWriter.NullProperty("endTime"); } aWriter.StringProperty("reason", MakeStringSpan(aReason)); aWriter.End(); } void ProfileBuffer::StreamPausedRangesToJSON( SpliceableJSONWriter& aWriter, double aSinceTime, mozilla::ProgressLogger aProgressLogger) const { mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) { MOZ_ASSERT(aReader, "ProfileChunkedBuffer cannot be out-of-session when sampler is " "running"); EntryGetter e(*aReader, aWriter.SourceFailureLatch(), aProgressLogger.CreateSubLoggerFromTo( 1_pc, "Streaming pauses...", 99_pc, "Streamed pauses")); Maybe currentPauseStartTime; Maybe currentCollectionStartTime; while (e.Has()) { if (e.Get().IsPause()) { currentPauseStartTime = Some(e.Get().GetDouble()); } else if (e.Get().IsResume()) { AddPausedRange(aWriter, "profiler-paused", currentPauseStartTime, Some(e.Get().GetDouble())); currentPauseStartTime = Nothing(); } else if (e.Get().IsCollectionStart()) { currentCollectionStartTime = Some(e.Get().GetDouble()); } else if (e.Get().IsCollectionEnd()) { AddPausedRange(aWriter, "collecting", currentCollectionStartTime, Some(e.Get().GetDouble())); currentCollectionStartTime = Nothing(); } e.Next(); } if (currentPauseStartTime) { AddPausedRange(aWriter, "profiler-paused", currentPauseStartTime, Nothing()); } if (currentCollectionStartTime) { AddPausedRange(aWriter, "collecting", currentCollectionStartTime, Nothing()); } }); } bool ProfileBuffer::DuplicateLastSample(ProfilerThreadId aThreadId, double aSampleTimeMs, Maybe& aLastSample, const RunningTimes& aRunningTimes) { if (!aLastSample) { return false; } if (mEntries.IsIndexInCurrentChunk(ProfileBufferIndex{*aLastSample})) { // The last (fully-written) sample is in this chunk, we can refer to it. // Note that between now and when we write the SameSample below, another // chunk could have been started, so the SameSample will in fact refer to a // block in a previous chunk. This is okay, because: // - When serializing to JSON, if that chunk is still there, we'll still be // able to find that old stack, so nothing will be lost. // - If unfortunately that chunk has been destroyed, we will lose this // sample. But this will only happen to the first sample (per thread) in // in the whole JSON output, because the next time we're here to duplicate // the same sample again, IsIndexInCurrentChunk will say `false` and we // will fall back to the normal copy or even re-sample. Losing the first // sample out of many in a whole recording is acceptable. // // |---| = chunk, S = Sample, D = Duplicate, s = same sample // |---S-s-s--| |s-D--s--s-| |s-D--s---s| // Later, the first chunk is destroyed/recycled: // |s-D--s--s-| |s-D--s---s| |-... // Output: ^ ^ ^ ^ // `-|--|-------|--- Same but no previous -> lost. // `--|-------|--- Full duplicate sample. // `-------|--- Same with previous -> okay. // `--- Same but now we have a previous -> okay! AUTO_PROFILER_STATS(DuplicateLastSample_SameSample); // Add the thread id first. We don't update `aLastSample` because we are not // writing a full sample. (void)AddThreadIdEntry(aThreadId); // Copy the new time, to be followed by a SameSample. AddEntry(ProfileBufferEntry::TimeBeforeSameSample(aSampleTimeMs)); // Add running times if they have data. if (!aRunningTimes.IsEmpty()) { mEntries.PutObjects(ProfileBufferEntry::Kind::RunningTimes, aRunningTimes); } // Finish with a SameSample entry. mEntries.PutObjects(ProfileBufferEntry::Kind::SameSample); return true; } AUTO_PROFILER_STATS(DuplicateLastSample_copy); ProfileChunkedBuffer tempBuffer( ProfileChunkedBuffer::ThreadSafety::WithoutMutex, WorkerChunkManager()); auto retrieveWorkerChunk = MakeScopeExit( [&]() { WorkerChunkManager().Reset(tempBuffer.GetAllChunks()); }); const bool ok = mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) { MOZ_ASSERT(aReader, "ProfileChunkedBuffer cannot be out-of-session when sampler is " "running"); // DuplicateLastSample is only called during profiling, so we don't need a // progress logger (only useful when capturing the final profile). EntryGetter e(*aReader, mozilla::FailureLatchInfallibleSource::Singleton(), ProgressLogger{}, *aLastSample); if (e.CurPos() != *aLastSample) { // The last sample is no longer within the buffer range, so we cannot // use it. Reset the stored buffer position to Nothing(). aLastSample.reset(); return false; } MOZ_RELEASE_ASSERT(e.Has() && e.Get().IsThreadId() && e.Get().GetThreadId() == aThreadId); e.Next(); // Go through the whole entry and duplicate it, until we find the next // one. while (e.Has()) { switch (e.Get().GetKind()) { case ProfileBufferEntry::Kind::Pause: case ProfileBufferEntry::Kind::Resume: case ProfileBufferEntry::Kind::PauseSampling: case ProfileBufferEntry::Kind::ResumeSampling: case ProfileBufferEntry::Kind::CollectionStart: case ProfileBufferEntry::Kind::CollectionEnd: case ProfileBufferEntry::Kind::ThreadId: case ProfileBufferEntry::Kind::TimeBeforeSameSample: // We're done. return true; case ProfileBufferEntry::Kind::Time: // Copy with new time AddEntry(tempBuffer, ProfileBufferEntry::Time(aSampleTimeMs)); break; case ProfileBufferEntry::Kind::TimeBeforeCompactStack: { // Copy with new time, followed by a compact stack. AddEntry(tempBuffer, ProfileBufferEntry::TimeBeforeCompactStack(aSampleTimeMs)); // Add running times if they have data. if (!aRunningTimes.IsEmpty()) { tempBuffer.PutObjects(ProfileBufferEntry::Kind::RunningTimes, aRunningTimes); } // The `CompactStack` *must* be present afterwards, but may not // immediately follow `TimeBeforeCompactStack` (e.g., some markers // could be written in-between), so we need to look for it in the // following entries. ProfileChunkedBuffer::BlockIterator it = e.Iterator(); for (;;) { ++it; if (it.IsAtEnd()) { break; } ProfileBufferEntryReader er = *it; auto kind = static_cast( er.ReadObject()); MOZ_ASSERT( static_cast(kind) < static_cast( ProfileBufferEntry::Kind::MODERN_LIMIT)); if (kind == ProfileBufferEntry::Kind::CompactStack) { // Found our CompactStack, just make a copy of the whole entry. er = *it; auto bytes = er.RemainingBytes(); MOZ_ASSERT(bytes < ProfileBufferChunkManager::scExpectedMaximumStackSize); tempBuffer.Put(bytes, [&](Maybe& aEW) { MOZ_ASSERT(aEW.isSome(), "tempBuffer cannot be out-of-session"); aEW->WriteFromReader(er, bytes); }); // CompactStack marks the end, we're done. break; } MOZ_ASSERT(kind >= ProfileBufferEntry::Kind::LEGACY_LIMIT, "There should be no legacy entries between " "TimeBeforeCompactStack and CompactStack"); er.SetRemainingBytes(0); // Here, we have encountered a non-legacy entry that was not the // CompactStack we're looking for; just continue the search... } // We're done. return true; } case ProfileBufferEntry::Kind::CounterKey: case ProfileBufferEntry::Kind::Number: case ProfileBufferEntry::Kind::Count: // Don't copy anything not part of a thread's stack sample break; case ProfileBufferEntry::Kind::CounterId: // CounterId is normally followed by Time - if so, we'd like // to skip it. If we duplicate Time, it won't hurt anything, just // waste buffer space (and this can happen if the CounterId has // fallen off the end of the buffer, but Time (and Number/Count) // are still in the buffer). e.Next(); if (e.Has() && e.Get().GetKind() != ProfileBufferEntry::Kind::Time) { // this would only happen if there was an invalid sequence // in the buffer. Don't skip it. continue; } // we've skipped Time break; case ProfileBufferEntry::Kind::ProfilerOverheadTime: // ProfilerOverheadTime is normally followed by // ProfilerOverheadDuration*4 - if so, we'd like to skip it. Don't // duplicate, as we are in the middle of a sampling and will soon // capture its own overhead. e.Next(); // A missing Time would only happen if there was an invalid // sequence in the buffer. Don't skip unexpected entry. if (e.Has() && e.Get().GetKind() != ProfileBufferEntry::Kind::ProfilerOverheadDuration) { continue; } e.Next(); if (e.Has() && e.Get().GetKind() != ProfileBufferEntry::Kind::ProfilerOverheadDuration) { continue; } e.Next(); if (e.Has() && e.Get().GetKind() != ProfileBufferEntry::Kind::ProfilerOverheadDuration) { continue; } e.Next(); if (e.Has() && e.Get().GetKind() != ProfileBufferEntry::Kind::ProfilerOverheadDuration) { continue; } // we've skipped ProfilerOverheadTime and // ProfilerOverheadDuration*4. break; default: { // Copy anything else we don't know about. AddEntry(tempBuffer, e.Get()); break; } } e.Next(); } return true; }); if (!ok) { return false; } // If the buffer was big enough, there won't be any cleared blocks. if (tempBuffer.GetState().mClearedBlockCount != 0) { // No need to try to read stack again as it won't fit. Reset the stored // buffer position to Nothing(). aLastSample.reset(); return false; } aLastSample = Some(AddThreadIdEntry(aThreadId)); mEntries.AppendContents(tempBuffer); return true; } void ProfileBuffer::DiscardSamplesBeforeTime(double aTime) { // This function does nothing! // The duration limit will be removed from Firefox, see bug 1632365. Unused << aTime; } // END ProfileBuffer ////////////////////////////////////////////////////////////////////////