2460 lines
88 KiB
C++
2460 lines
88 KiB
C++
/* -*- 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/CycleCollectedJSContext.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 <ostream>
|
|
#include <type_traits>
|
|
|
|
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<ProfilerThreadId>);
|
|
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<ProfilerThreadId>);
|
|
memcpy(&result, mStorage, sizeof(result));
|
|
return result;
|
|
}
|
|
|
|
void ProfileBufferEntry::CopyCharsInto(char (&aOutArray)[kNumChars]) const {
|
|
memcpy(aOutArray, mStorage, kNumChars);
|
|
}
|
|
|
|
// END ProfileBufferEntry
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
struct TypeInfo {
|
|
Maybe<nsCString> mKeyedBy;
|
|
Maybe<nsCString> mName;
|
|
Maybe<nsCString> mLocation;
|
|
Maybe<unsigned> 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 <typename T>
|
|
void IntElement(uint32_t aIndex, T aValue) {
|
|
static_assert(!std::is_same_v<T, uint64_t>,
|
|
"Narrowing uint64 -> int64 conversion not allowed");
|
|
FillUpTo(aIndex);
|
|
mJSONWriter.IntElement(static_cast<int64_t>(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<const char>& aValue) {
|
|
FillUpTo(aIndex);
|
|
mStrings.WriteElement(Writer(), aValue);
|
|
}
|
|
|
|
private:
|
|
UniqueJSONStrings& mStrings;
|
|
};
|
|
|
|
Maybe<UniqueStacks::StackKey> UniqueStacks::BeginStack(const FrameKey& aFrame) {
|
|
if (Maybe<uint32_t> frameIndex = GetOrAddFrameIndex(aFrame); frameIndex) {
|
|
return Some(StackKey(*frameIndex));
|
|
}
|
|
return Nothing{};
|
|
}
|
|
|
|
Vector<JITFrameInfoForBufferRange>&&
|
|
JITFrameInfo::MoveRangesWithNewFailureLatch(FailureLatch& aFailureLatch) && {
|
|
aFailureLatch.SetFailureFrom(mLocalFailureLatchSource);
|
|
return std::move(mRanges);
|
|
}
|
|
|
|
UniquePtr<UniqueJSONStrings>&&
|
|
JITFrameInfo::MoveUniqueStringsWithNewFailureLatch(
|
|
FailureLatch& aFailureLatch) && {
|
|
if (mUniqueStrings) {
|
|
mUniqueStrings->ChangeFailureLatchAndForwardState(aFailureLatch);
|
|
} else {
|
|
aFailureLatch.SetFailureFrom(mLocalFailureLatchSource);
|
|
}
|
|
return std::move(mUniqueStrings);
|
|
}
|
|
|
|
Maybe<UniqueStacks::StackKey> UniqueStacks::AppendFrame(
|
|
const StackKey& aStack, const FrameKey& aFrame) {
|
|
if (Maybe<uint32_t> stackIndex = GetOrAddStackIndex(aStack); stackIndex) {
|
|
if (Maybe<uint32_t> 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<JITFrameKey>& srcKeys = iter.get().value();
|
|
mozilla::Vector<JITFrameKey> 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<UniqueJSONStrings>(
|
|
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<uint32_t> 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<Vector<UniqueStacks::FrameKey>>
|
|
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<FrameKey> 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<uint32_t> 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<sizeof(uintptr_t) <= sizeof(uint32_t),
|
|
uint32_t, uint64_t>;
|
|
nameOrAddress.AppendInt(static_cast<uint>(reinterpret_cast<uintptr_t>(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,
|
|
LINE = 4,
|
|
COLUMN = 5,
|
|
CATEGORY = 6,
|
|
SUBCATEGORY = 7
|
|
};
|
|
|
|
AutoArraySchemaWithStringsWriter writer(mFrameTableWriter, *mUniqueStrings);
|
|
|
|
const NormalFrameData& data = aFrame.mData.as<NormalFrameData>();
|
|
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,
|
|
LINE = 4,
|
|
COLUMN = 5,
|
|
CATEGORY = 6,
|
|
SUBCATEGORY = 7
|
|
};
|
|
|
|
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<void(const std::function<void(void*)>&)>&
|
|
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<JITFrameKey> 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<double> 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<ProfileBufferEntry::Kind>(
|
|
er.ReadObject<ProfileBufferEntry::KindUnderlyingType>());
|
|
MOZ_ASSERT(static_cast<ProfileBufferEntry::KindUnderlyingType>(type) <
|
|
static_cast<ProfileBufferEntry::KindUnderlyingType>(
|
|
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) {}
|
|
};
|
|
|
|
#ifdef MOZ_EXECUTION_TRACING
|
|
|
|
template <typename GetStreamingParametersForThreadCallback>
|
|
void ProfileBuffer::MaybeStreamExecutionTraceToJSON(
|
|
GetStreamingParametersForThreadCallback&&
|
|
aGetStreamingParametersForThreadCallback,
|
|
double aSinceTime) const {
|
|
JS::ExecutionTrace trace;
|
|
if (!JS_TracerSnapshotTrace(trace)) {
|
|
return;
|
|
}
|
|
|
|
for (const JS::ExecutionTrace::TracedJSContext& context : trace.contexts) {
|
|
Maybe<StreamingParametersForThread> streamingParameters =
|
|
std::forward<GetStreamingParametersForThreadCallback>(
|
|
aGetStreamingParametersForThreadCallback)(context.id);
|
|
|
|
// Ignore samples that are for the wrong thread.
|
|
if (!streamingParameters) {
|
|
continue;
|
|
}
|
|
|
|
SpliceableJSONWriter& writer = streamingParameters->mWriter;
|
|
UniqueStacks& uniqueStacks = streamingParameters->mUniqueStacks;
|
|
|
|
mozilla::Vector<UniqueStacks::StackKey> frameStack;
|
|
|
|
Maybe<UniqueStacks::StackKey> maybeStack =
|
|
uniqueStacks.BeginStack(UniqueStacks::FrameKey("(root)"));
|
|
if (!maybeStack) {
|
|
writer.SetFailure("BeginStack failure");
|
|
continue;
|
|
}
|
|
|
|
UniqueStacks::StackKey stack = *maybeStack;
|
|
if (!frameStack.append(stack)) {
|
|
writer.SetFailure("frameStack append failure");
|
|
continue;
|
|
}
|
|
|
|
for (const JS::ExecutionTrace::TracedEvent& event : context.events) {
|
|
if (event.time < aSinceTime) {
|
|
continue;
|
|
}
|
|
|
|
if (event.kind == JS::ExecutionTrace::EventKind::Error) {
|
|
writer.SetFailure("Error during tracing (likely OOM)");
|
|
continue;
|
|
}
|
|
|
|
if (event.kind == JS::ExecutionTrace::EventKind::FunctionEnter) {
|
|
HashMap<uint32_t, size_t>::Ptr functionName =
|
|
context.atoms.lookup(event.functionEvent.functionNameId);
|
|
// This is uncommon, but if one of our ring buffers wraps around, we
|
|
// can end up with missing function name entries
|
|
const char* functionNameStr = "<expired>";
|
|
if (functionName) {
|
|
functionNameStr = &trace.stringBuffer[functionName->value()];
|
|
}
|
|
HashMap<uint32_t, size_t>::Ptr scriptUrl =
|
|
context.scriptUrls.lookup(event.functionEvent.scriptId);
|
|
// See the comment above functionNameStr
|
|
const char* scriptUrlStr = "<expired>";
|
|
if (scriptUrl) {
|
|
scriptUrlStr = &trace.stringBuffer[scriptUrl->value()];
|
|
}
|
|
nsAutoCStringN<1024> name(functionNameStr);
|
|
name.AppendPrintf(" (%s:%u:%u)", scriptUrlStr,
|
|
event.functionEvent.lineNumber,
|
|
event.functionEvent.column);
|
|
JS::ProfilingCategoryPair categoryPair;
|
|
switch (event.functionEvent.implementation) {
|
|
case JS::ExecutionTrace::ImplementationType::Interpreter:
|
|
categoryPair = JS::ProfilingCategoryPair::JS;
|
|
break;
|
|
case JS::ExecutionTrace::ImplementationType::Baseline:
|
|
categoryPair = JS::ProfilingCategoryPair::JS_Baseline;
|
|
break;
|
|
case JS::ExecutionTrace::ImplementationType::Ion:
|
|
categoryPair = JS::ProfilingCategoryPair::JS_IonMonkey;
|
|
break;
|
|
case JS::ExecutionTrace::ImplementationType::Wasm:
|
|
categoryPair = JS::ProfilingCategoryPair::JS_WasmOther;
|
|
break;
|
|
}
|
|
|
|
UniqueStacks::FrameKey newFrame(nsCString(name.get()), true, false,
|
|
event.functionEvent.realmID, Nothing{},
|
|
Nothing{}, Some(categoryPair));
|
|
maybeStack = uniqueStacks.AppendFrame(stack, newFrame);
|
|
if (!maybeStack) {
|
|
writer.SetFailure("AppendFrame failure");
|
|
continue;
|
|
}
|
|
stack = *maybeStack;
|
|
if (!frameStack.append(stack)) {
|
|
writer.SetFailure("frameStack append failure");
|
|
continue;
|
|
}
|
|
|
|
} else if (event.kind == JS::ExecutionTrace::EventKind::LabelEnter) {
|
|
UniqueStacks::FrameKey newFrame(
|
|
nsCString(&trace.stringBuffer[event.labelEvent.label]), true, false,
|
|
0, Nothing{}, Nothing{}, Some(JS::ProfilingCategoryPair::DOM));
|
|
maybeStack = uniqueStacks.AppendFrame(stack, newFrame);
|
|
if (!maybeStack) {
|
|
writer.SetFailure("AppendFrame failure");
|
|
continue;
|
|
}
|
|
stack = *maybeStack;
|
|
if (!frameStack.append(stack)) {
|
|
writer.SetFailure("frameStack append failure");
|
|
continue;
|
|
}
|
|
|
|
} else {
|
|
MOZ_ASSERT(event.kind == JS::ExecutionTrace::EventKind::LabelLeave ||
|
|
event.kind == JS::ExecutionTrace::EventKind::FunctionLeave);
|
|
if (frameStack.length() > 0) {
|
|
frameStack.popBack();
|
|
}
|
|
if (frameStack.length() > 0) {
|
|
stack = frameStack[frameStack.length() - 1];
|
|
} else {
|
|
maybeStack =
|
|
uniqueStacks.BeginStack(UniqueStacks::FrameKey("(root)"));
|
|
if (!maybeStack) {
|
|
writer.SetFailure("BeginStack failure");
|
|
continue;
|
|
}
|
|
|
|
stack = *maybeStack;
|
|
if (!frameStack.append(stack)) {
|
|
writer.SetFailure("frameStack append failure");
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
const Maybe<uint32_t> stackIndex = uniqueStacks.GetOrAddStackIndex(stack);
|
|
if (!stackIndex) {
|
|
writer.SetFailure("Can't add unique string for stack");
|
|
continue;
|
|
}
|
|
|
|
WriteSample(writer, ProfileSample{*stackIndex, event.time, Nothing{},
|
|
RunningTimes{}});
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// GetStreamingParametersForThreadCallback:
|
|
// (ProfilerThreadId) -> Maybe<StreamingParametersForThread>
|
|
template <typename GetStreamingParametersForThreadCallback>
|
|
ProfilerThreadId ProfileBuffer::DoStreamSamplesAndMarkersToJSON(
|
|
mozilla::FailureLatch& aFailureLatch,
|
|
GetStreamingParametersForThreadCallback&&
|
|
aGetStreamingParametersForThreadCallback,
|
|
double aSinceTime, ProcessStreamingContext* aStreamingContextForMarkers,
|
|
mozilla::ProgressLogger aProgressLogger) const {
|
|
UniquePtr<char[]> dynStrBuf = MakeUnique<char[]>(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<StreamingParametersForThread> streamingParameters =
|
|
std::forward<GetStreamingParametersForThreadCallback>(
|
|
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<double>& unresponsiveDuration,
|
|
const RunningTimes& runningTimes) {
|
|
if (writer.Failed()) {
|
|
return;
|
|
}
|
|
|
|
Maybe<UniqueStacks::StackKey> 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<unsigned> line;
|
|
if (e.Has() && e.Get().IsLineNumber()) {
|
|
line = Some(unsigned(e.Get().GetInt()));
|
|
e.Next();
|
|
}
|
|
|
|
Maybe<unsigned> column;
|
|
if (e.Has() && e.Get().IsColumnNumber()) {
|
|
column = Some(unsigned(e.Get().GetInt()));
|
|
e.Next();
|
|
}
|
|
|
|
Maybe<JS::ProfilingCategoryPair> 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<Vector<UniqueStacks::FrameKey>>& 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<uint32_t> 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<double> unresponsiveDuration;
|
|
|
|
ProfileChunkedBuffer::BlockIterator it = e.Iterator();
|
|
for (;;) {
|
|
++it;
|
|
if (it.IsAtEnd()) {
|
|
break;
|
|
}
|
|
ProfileBufferEntryReader er = *it;
|
|
ProfileBufferEntry::Kind kind =
|
|
er.ReadObject<ProfileBufferEntry::Kind>();
|
|
|
|
// 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<double>());
|
|
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<ProfileBufferEntry::Kind>();
|
|
|
|
// 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<StreamingParametersForThread> 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 {
|
|
auto getStreamingParamsCallback = [&](ProfilerThreadId aReadThreadId) {
|
|
Maybe<StreamingParametersForThread> streamingParameters;
|
|
ThreadStreamingContext* threadData =
|
|
aProcessStreamingContext.GetThreadStreamingContext(aReadThreadId);
|
|
if (threadData) {
|
|
streamingParameters.emplace(
|
|
threadData->mSamplesDataWriter, *threadData->mUniqueStacks,
|
|
threadData->mPreviousStackState, threadData->mPreviousStack);
|
|
}
|
|
return streamingParameters;
|
|
};
|
|
|
|
#ifdef MOZ_EXECUTION_TRACING
|
|
MaybeStreamExecutionTraceToJSON(getStreamingParamsCallback,
|
|
aProcessStreamingContext.GetSinceTime());
|
|
#endif
|
|
|
|
(void)DoStreamSamplesAndMarkersToJSON(
|
|
aProcessStreamingContext.SourceFailureLatch(), getStreamingParamsCallback,
|
|
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<void(void*)>& 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<ProfileBufferEntry::Kind>();
|
|
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<ProfileBufferEntry::Kind>(
|
|
aER.ReadObject<ProfileBufferEntry::KindUnderlyingType>());
|
|
MOZ_ASSERT(static_cast<ProfileBufferEntry::KindUnderlyingType>(type) <
|
|
static_cast<ProfileBufferEntry::KindUnderlyingType>(
|
|
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 {
|
|
const char* recordOverheads = getenv("MOZ_PROFILER_RECORD_OVERHEADS");
|
|
if (!recordOverheads || recordOverheads[0] == '\0') {
|
|
// Overheads were not recorded, return early.
|
|
return;
|
|
}
|
|
|
|
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 CounterSample {
|
|
double mTime;
|
|
uint64_t mNumber;
|
|
int64_t mCount;
|
|
};
|
|
|
|
using CounterSamples = Vector<CounterSample>;
|
|
|
|
static LazyLogModule sFuzzyfoxLog("Fuzzyfox");
|
|
|
|
// HashMap lookup, if not found, a default value is inserted.
|
|
// Returns reference to (existing or new) value inside the HashMap.
|
|
template <typename HashM, typename Key>
|
|
static auto& LookupOrAdd(HashM& aMap, Key&& aKey) {
|
|
auto addPtr = aMap.lookupForAdd(aKey);
|
|
if (!addPtr) {
|
|
MOZ_RELEASE_ASSERT(aMap.add(addPtr, std::forward<Key>(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, COUNT = 1, NUMBER = 2 };
|
|
|
|
// Stream all counters. We skip other entries, because we process them in
|
|
// StreamSamplesToJSON()/etc.
|
|
//
|
|
// Valid sequence in the buffer:
|
|
// CounterID
|
|
// Time
|
|
// ( Count Number? )*
|
|
//
|
|
// And the JSON (example):
|
|
// "counters": {
|
|
// "name": "malloc",
|
|
// "category": "Memory",
|
|
// "description": "Amount of allocated memory",
|
|
// "samples": {
|
|
// "schema": {"time": 0, "count": 1, "number": 2},
|
|
// "data": [
|
|
// [
|
|
// 16117.033968000002,
|
|
// 2446216,
|
|
// 6801320
|
|
// ],
|
|
// [
|
|
// 16118.037638,
|
|
// 2446216,
|
|
// 6801320
|
|
// ],
|
|
// ],
|
|
// },
|
|
// }
|
|
|
|
// Build the map of counters and populate it
|
|
HashMap<void*, CounterSamples> 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();
|
|
CounterSamples& data = 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) {
|
|
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();
|
|
}
|
|
CounterSample 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 counter entries; dump them to JSON
|
|
if (counters.count() == 0) {
|
|
return;
|
|
}
|
|
|
|
aWriter.StartArrayProperty("counters");
|
|
for (auto iter = counters.iter(); !iter.done(); iter.next()) {
|
|
CounterSamples& samples = iter.get().value();
|
|
size_t size = samples.length();
|
|
if (size == 0) {
|
|
continue;
|
|
}
|
|
const BaseProfilerCount* base_counter =
|
|
static_cast<const BaseProfilerCount*>(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));
|
|
|
|
bool hasNumber = false;
|
|
for (size_t i = 0; i < size; i++) {
|
|
if (samples[i].mNumber != 0) {
|
|
hasNumber = true;
|
|
break;
|
|
}
|
|
}
|
|
aWriter.StartObjectProperty("samples");
|
|
{
|
|
JSONSchemaWriter schema(aWriter);
|
|
schema.WriteField("time");
|
|
schema.WriteField("count");
|
|
if (hasNumber) {
|
|
schema.WriteField("number");
|
|
}
|
|
}
|
|
|
|
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<int64_t>::max()));
|
|
|
|
int64_t numberDelta =
|
|
static_cast<int64_t>(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(COUNT, 0);
|
|
if (hasNumber) {
|
|
writer.IntElement(NUMBER, 0);
|
|
}
|
|
}
|
|
|
|
AutoArraySchemaWriter writer(aWriter);
|
|
writer.TimeMsElement(TIME, samples[i].mTime);
|
|
writer.IntElement(COUNT, countDelta);
|
|
if (hasNumber) {
|
|
writer.IntElement(NUMBER, numberDelta);
|
|
}
|
|
|
|
previousSkippedTime = 0.0;
|
|
previousNumber = samples[i].mNumber;
|
|
previousCount = samples[i].mCount;
|
|
} else {
|
|
previousSkippedTime = samples[i].mTime;
|
|
}
|
|
}
|
|
aWriter.EndArray(); // data
|
|
aWriter.EndObject(); // samples
|
|
aWriter.End(); // for each counter
|
|
}
|
|
aWriter.EndArray(); // counters
|
|
});
|
|
}
|
|
|
|
#undef ERROR_AND_CONTINUE
|
|
|
|
static void AddPausedRange(SpliceableJSONWriter& aWriter, const char* aReason,
|
|
const Maybe<double>& aStartTime,
|
|
const Maybe<double>& 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<double> currentPauseStartTime;
|
|
Maybe<double> 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<uint64_t>& 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<ProfileBufferEntry::Kind>(
|
|
er.ReadObject<ProfileBufferEntry::KindUnderlyingType>());
|
|
MOZ_ASSERT(
|
|
static_cast<ProfileBufferEntry::KindUnderlyingType>(kind) <
|
|
static_cast<ProfileBufferEntry::KindUnderlyingType>(
|
|
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<ProfileBufferEntryWriter>& 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::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 IceCat, see bug 1632365.
|
|
Unused << aTime;
|
|
}
|
|
|
|
// END ProfileBuffer
|
|
////////////////////////////////////////////////////////////////////////
|