/* -*- 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 #include "platform.h" #include "mozilla/HashFunctions.h" #include "mozilla/Sprintf.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" // JS #include "jsapi.h" #include "jsfriendapi.h" #include "js/TrackedOptimizationInfo.h" // Self #include "ProfileBufferEntry.h" using namespace mozilla; //////////////////////////////////////////////////////////////////////// // BEGIN ProfileBufferEntry ProfileBufferEntry::ProfileBufferEntry() : mKind(Kind::INVALID) { u.mString = nullptr; } // aString must be a static string. ProfileBufferEntry::ProfileBufferEntry(Kind aKind, const char *aString) : mKind(aKind) { u.mString = aString; } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, char aChars[kNumChars]) : mKind(aKind) { memcpy(u.mChars, aChars, kNumChars); } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, void* aPtr) : mKind(aKind) { u.mPtr = aPtr; } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, ProfilerMarker* aMarker) : mKind(aKind) { u.mMarker = aMarker; } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, double aDouble) : mKind(aKind) { u.mDouble = aDouble; } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, int aInt) : mKind(aKind) { u.mInt = aInt; } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, int64_t aInt64) : mKind(aKind) { u.mInt64 = aInt64; } ProfileBufferEntry::ProfileBufferEntry(Kind aKind, uint64_t aUint64) : mKind(aKind) { u.mUint64 = aUint64; } // END ProfileBufferEntry //////////////////////////////////////////////////////////////////////// class JSONSchemaWriter { JSONWriter& mWriter; uint32_t mIndex; public: explicit JSONSchemaWriter(JSONWriter& aWriter) : mWriter(aWriter) , mIndex(0) { aWriter.StartObjectProperty("schema", SpliceableJSONWriter::SingleLineStyle); } void WriteField(const char* aName) { mWriter.IntProperty(aName, mIndex++); } ~JSONSchemaWriter() { mWriter.EndObject(); } }; struct TypeInfo { Maybe mKeyedBy; Maybe mName; Maybe mLocation; Maybe mLineNumber; }; template class ForEachTrackedOptimizationTypeInfoLambdaOp : public JS::ForEachTrackedOptimizationTypeInfoOp { public: // aLambda needs to be a function with the following signature: // void lambda(JS::TrackedTypeSite site, const char* mirType, // const nsTArray& typeset) // aLambda will be called once per entry. explicit ForEachTrackedOptimizationTypeInfoLambdaOp(LambdaT&& aLambda) : mLambda(aLambda) {} // This is called 0 or more times per entry, *before* operator() is called // for that entry. void readType(const char* keyedBy, const char* name, const char* location, const Maybe& lineno) override { TypeInfo info = { keyedBy ? Some(nsCString(keyedBy)) : Nothing(), name ? Some(nsCString(name)) : Nothing(), location ? Some(nsCString(location)) : Nothing(), lineno }; mTypesetForUpcomingEntry.AppendElement(std::move(info)); } void operator()(JS::TrackedTypeSite site, const char* mirType) override { nsTArray typeset(std::move(mTypesetForUpcomingEntry)); mLambda(site, mirType, typeset); } private: nsTArray mTypesetForUpcomingEntry; LambdaT mLambda; }; template ForEachTrackedOptimizationTypeInfoLambdaOp MakeForEachTrackedOptimizationTypeInfoLambdaOp(LambdaT&& aLambda) { return ForEachTrackedOptimizationTypeInfoLambdaOp(std::move(aLambda)); } // 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: AutoArraySchemaWriter(SpliceableJSONWriter& aWriter, UniqueJSONStrings& aStrings) : mJSONWriter(aWriter) , mStrings(&aStrings) , mNextFreeIndex(0) { mJSONWriter.StartArrayElement(SpliceableJSONWriter::SingleLineStyle); } explicit AutoArraySchemaWriter(SpliceableJSONWriter& aWriter) : mJSONWriter(aWriter) , mStrings(nullptr) , mNextFreeIndex(0) { mJSONWriter.StartArrayElement(SpliceableJSONWriter::SingleLineStyle); } ~AutoArraySchemaWriter() { mJSONWriter.EndArray(); } void IntElement(uint64_t aIndex, uint64_t aValue) { FillUpTo(aIndex); mJSONWriter.IntElement(aValue); } void DoubleElement(uint32_t aIndex, double aValue) { FillUpTo(aIndex); mJSONWriter.DoubleElement(aValue); } void StringElement(uint32_t aIndex, const char* aValue) { MOZ_RELEASE_ASSERT(mStrings); FillUpTo(aIndex); mStrings->WriteElement(mJSONWriter, aValue); } // Write an element using a callback that takes a JSONWriter& and a // UniqueJSONStrings&. template void FreeFormElement(uint32_t aIndex, LambdaT aCallback) { MOZ_RELEASE_ASSERT(mStrings); FillUpTo(aIndex); aCallback(mJSONWriter, *mStrings); } private: void FillUpTo(uint32_t aIndex) { MOZ_ASSERT(aIndex >= mNextFreeIndex); mJSONWriter.NullElements(aIndex - mNextFreeIndex); mNextFreeIndex = aIndex + 1; } SpliceableJSONWriter& mJSONWriter; UniqueJSONStrings* mStrings; uint32_t mNextFreeIndex; }; template class ForEachTrackedOptimizationAttemptsLambdaOp : public JS::ForEachTrackedOptimizationAttemptOp { public: explicit ForEachTrackedOptimizationAttemptsLambdaOp(LambdaT&& aLambda) : mLambda(std::move(aLambda)) {} void operator()(JS::TrackedStrategy aStrategy, JS::TrackedOutcome aOutcome) override { mLambda(aStrategy, aOutcome); } private: LambdaT mLambda; }; template ForEachTrackedOptimizationAttemptsLambdaOp MakeForEachTrackedOptimizationAttemptsLambdaOp(LambdaT&& aLambda) { return ForEachTrackedOptimizationAttemptsLambdaOp(std::move(aLambda)); } UniqueJSONStrings::UniqueJSONStrings() { mStringTableWriter.StartBareList(); } UniqueJSONStrings::UniqueJSONStrings(const UniqueJSONStrings& aOther) { mStringTableWriter.StartBareList(); if (aOther.mStringToIndexMap.Count() > 0) { for (auto iter = aOther.mStringToIndexMap.ConstIter(); !iter.Done(); iter.Next()) { mStringToIndexMap.Put(iter.Key(), iter.Data()); } UniquePtr stringTableJSON = aOther.mStringTableWriter.WriteFunc()->CopyData(); mStringTableWriter.Splice(stringTableJSON.get()); } } uint32_t UniqueJSONStrings::GetOrAddIndex(const char* aStr) { nsDependentCString str(aStr); uint32_t count = mStringToIndexMap.Count(); auto entry = mStringToIndexMap.LookupForAdd(str); if (entry) { MOZ_ASSERT(entry.Data() < count); return entry.Data(); } entry.OrInsert([&]{ return count; }); mStringTableWriter.StringElement(aStr); return count; } UniqueStacks::StackKey UniqueStacks::BeginStack(const FrameKey& aFrame) { return StackKey(GetOrAddFrameIndex(aFrame)); } UniqueStacks::StackKey UniqueStacks::AppendFrame(const StackKey& aStack, const FrameKey& aFrame) { return StackKey(aStack, GetOrAddStackIndex(aStack), GetOrAddFrameIndex(aFrame)); } uint32_t JITFrameInfoForBufferRange::JITFrameKey::Hash() const { uint32_t hash = 0; hash = AddToHash(hash, mCanonicalAddress); hash = AddToHash(hash, mDepth); return hash; } bool JITFrameInfoForBufferRange::JITFrameKey::operator==(const JITFrameKey& aOther) const { return mCanonicalAddress == aOther.mCanonicalAddress && mDepth == aOther.mDepth; } template void CopyClassHashtable(nsClassHashtable& aDest, const nsClassHashtable& aSrc) { for (auto iter = aSrc.ConstIter(); !iter.Done(); iter.Next()) { const T& objRef = *iter.Data(); aDest.LookupOrAdd(iter.Key(), objRef); } } JITFrameInfoForBufferRange JITFrameInfoForBufferRange::Clone() const { nsClassHashtable, nsTArray> jitAddressToJITFramesMap; nsClassHashtable, nsCString> jitFrameToFrameJSONMap; CopyClassHashtable(jitAddressToJITFramesMap, mJITAddressToJITFramesMap); CopyClassHashtable(jitFrameToFrameJSONMap, mJITFrameToFrameJSONMap); return JITFrameInfoForBufferRange{ mRangeStart, mRangeEnd, std::move(jitAddressToJITFramesMap), std::move(jitFrameToFrameJSONMap) }; } JITFrameInfo::JITFrameInfo(const JITFrameInfo& aOther) : mUniqueStrings(MakeUnique(*aOther.mUniqueStrings)) { for (const JITFrameInfoForBufferRange& range : aOther.mRanges) { mRanges.AppendElement(range.Clone()); } } bool UniqueStacks::FrameKey::NormalFrameData::operator==(const NormalFrameData& aOther) const { return mLocation == aOther.mLocation && mLine == aOther.mLine && mColumn == aOther.mColumn && mCategory == aOther.mCategory; } bool UniqueStacks::FrameKey::JITFrameData::operator==(const JITFrameData& aOther) const { return mCanonicalAddress == aOther.mCanonicalAddress && mDepth == aOther.mDepth && mRangeIndex == aOther.mRangeIndex; } uint32_t UniqueStacks::FrameKey::Hash() const { uint32_t hash = 0; if (mData.is()) { const NormalFrameData& data = mData.as(); if (!data.mLocation.IsEmpty()) { hash = AddToHash(hash, HashString(data.mLocation.get())); } if (data.mLine.isSome()) { hash = AddToHash(hash, *data.mLine); } if (data.mColumn.isSome()) { hash = AddToHash(hash, *data.mColumn); } if (data.mCategory.isSome()) { hash = AddToHash(hash, *data.mCategory); } } else { const JITFrameData& data = mData.as(); hash = AddToHash(hash, data.mCanonicalAddress); hash = AddToHash(hash, data.mDepth); hash = AddToHash(hash, data.mRangeIndex); } return hash; } // 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(JITFrameInfo&& aJITFrameInfo) : mUniqueStrings(std::move(aJITFrameInfo.mUniqueStrings)) , mJITInfoRanges(std::move(aJITFrameInfo.mRanges)) { mFrameTableWriter.StartBareList(); mStackTableWriter.StartBareList(); } uint32_t UniqueStacks::GetOrAddStackIndex(const StackKey& aStack) { uint32_t count = mStackToIndexMap.Count(); auto entry = mStackToIndexMap.LookupForAdd(aStack); if (entry) { MOZ_ASSERT(entry.Data() < count); return entry.Data(); } entry.OrInsert([&]{ return count; }); StreamStack(aStack); return count; } template struct PositionInRangeComparator final { bool Equals(const RangeT& aRange, PosT aPos) const { return aRange.mRangeStart <= aPos && aPos < aRange.mRangeEnd; } bool LessThan(const RangeT& aRange, PosT aPos) const { return aRange.mRangeEnd <= aPos; } }; Maybe> UniqueStacks::LookupFramesForJITAddressFromBufferPos(void* aJITAddress, uint64_t aBufferPos) { size_t rangeIndex = mJITInfoRanges.BinaryIndexOf(aBufferPos, PositionInRangeComparator()); MOZ_RELEASE_ASSERT(rangeIndex != mJITInfoRanges.NoIndex, "Buffer position of jit address needs to be in one of the ranges"); using JITFrameKey = JITFrameInfoForBufferRange::JITFrameKey; const JITFrameInfoForBufferRange& jitFrameInfoRange = mJITInfoRanges[rangeIndex]; const nsTArray* jitFrameKeys = jitFrameInfoRange.mJITAddressToJITFramesMap.Get(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. nsTArray frameKeys; for (const JITFrameKey& jitFrameKey : *jitFrameKeys) { FrameKey frameKey(jitFrameKey.mCanonicalAddress, jitFrameKey.mDepth, rangeIndex); 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. const nsCString* frameJSON = jitFrameInfoRange.mJITFrameToFrameJSONMap.Get(jitFrameKey); MOZ_RELEASE_ASSERT(frameJSON, "Should have cached JSON for this frame"); mFrameTableWriter.Splice(frameJSON->get()); entry.OrInsert([&] { return index; }); } frameKeys.AppendElement(std::move(frameKey)); } return Some(std::move(frameKeys)); } uint32_t UniqueStacks::GetOrAddFrameIndex(const FrameKey& aFrame) { uint32_t count = mFrameToIndexMap.Count(); auto entry = mFrameToIndexMap.LookupForAdd(aFrame); if (entry) { MOZ_ASSERT(entry.Data() < count); return entry.Data(); } entry.OrInsert([&]{ return count; }); StreamNonJITFrame(aFrame); return count; } void UniqueStacks::SpliceFrameTableElements(SpliceableJSONWriter& aWriter) { mFrameTableWriter.EndBareList(); aWriter.TakeAndSplice(mFrameTableWriter.WriteFunc()); } void UniqueStacks::SpliceStackTableElements(SpliceableJSONWriter& aWriter) { mStackTableWriter.EndBareList(); aWriter.TakeAndSplice(mStackTableWriter.WriteFunc()); } void UniqueStacks::StreamStack(const StackKey& aStack) { enum Schema : uint32_t { PREFIX = 0, FRAME = 1 }; AutoArraySchemaWriter writer(mStackTableWriter, *mUniqueStrings); if (aStack.mPrefixStackIndex.isSome()) { writer.IntElement(PREFIX, *aStack.mPrefixStackIndex); } writer.IntElement(FRAME, aStack.mFrameIndex); } void UniqueStacks::StreamNonJITFrame(const FrameKey& aFrame) { using NormalFrameData = FrameKey::NormalFrameData; enum Schema : uint32_t { LOCATION = 0, IMPLEMENTATION = 1, OPTIMIZATIONS = 2, LINE = 3, COLUMN = 4, CATEGORY = 5 }; AutoArraySchemaWriter writer(mFrameTableWriter, *mUniqueStrings); const NormalFrameData& data = aFrame.mData.as(); writer.StringElement(LOCATION, data.mLocation.get()); if (data.mLine.isSome()) { writer.IntElement(LINE, *data.mLine); } if (data.mColumn.isSome()) { writer.IntElement(COLUMN, *data.mColumn); } if (data.mCategory.isSome()) { writer.IntElement(CATEGORY, *data.mCategory); } } static void StreamJITFrameOptimizations(SpliceableJSONWriter& aWriter, UniqueJSONStrings& aUniqueStrings, JSContext* aContext, const JS::ProfiledFrameHandle& aJITFrame) { aWriter.StartObjectElement(); { aWriter.StartArrayProperty("types"); { auto op = MakeForEachTrackedOptimizationTypeInfoLambdaOp( [&](JS::TrackedTypeSite site, const char* mirType, const nsTArray& typeset) { aWriter.StartObjectElement(); { aUniqueStrings.WriteProperty(aWriter, "site", JS::TrackedTypeSiteString(site)); aUniqueStrings.WriteProperty(aWriter, "mirType", mirType); if (!typeset.IsEmpty()) { aWriter.StartArrayProperty("typeset"); for (const TypeInfo& typeInfo : typeset) { aWriter.StartObjectElement(); { aUniqueStrings.WriteProperty(aWriter, "keyedBy", typeInfo.mKeyedBy->get()); if (typeInfo.mName) { aUniqueStrings.WriteProperty(aWriter, "name", typeInfo.mName->get()); } if (typeInfo.mLocation) { aUniqueStrings.WriteProperty(aWriter, "location", typeInfo.mLocation->get()); } if (typeInfo.mLineNumber.isSome()) { aWriter.IntProperty("line", *typeInfo.mLineNumber); } } aWriter.EndObject(); } aWriter.EndArray(); } } aWriter.EndObject(); }); aJITFrame.forEachOptimizationTypeInfo(op); } aWriter.EndArray(); JS::Rooted script(aContext); jsbytecode* pc; aWriter.StartObjectProperty("attempts"); { { JSONSchemaWriter schema(aWriter); schema.WriteField("strategy"); schema.WriteField("outcome"); } aWriter.StartArrayProperty("data"); { auto op = MakeForEachTrackedOptimizationAttemptsLambdaOp( [&](JS::TrackedStrategy strategy, JS::TrackedOutcome outcome) { enum Schema : uint32_t { STRATEGY = 0, OUTCOME = 1 }; AutoArraySchemaWriter writer(aWriter, aUniqueStrings); writer.StringElement(STRATEGY, JS::TrackedStrategyString(strategy)); writer.StringElement(OUTCOME, JS::TrackedOutcomeString(outcome)); }); aJITFrame.forEachOptimizationAttempt(op, script.address(), &pc); } aWriter.EndArray(); } aWriter.EndObject(); if (JSAtom* name = js::GetPropertyNameFromPC(script, pc)) { char buf[512]; JS_PutEscapedFlatString(buf, ArrayLength(buf), js::AtomToFlatString(name), 0); aUniqueStrings.WriteProperty(aWriter, "propertyName", buf); } unsigned line, column; line = JS_PCToLineNumber(script, pc, &column); aWriter.IntProperty("line", line); aWriter.IntProperty("column", column); } aWriter.EndObject(); } static void StreamJITFrame(JSContext* aContext, SpliceableJSONWriter& aWriter, UniqueJSONStrings& aUniqueStrings, const JS::ProfiledFrameHandle& aJITFrame) { enum Schema : uint32_t { LOCATION = 0, IMPLEMENTATION = 1, OPTIMIZATIONS = 2, LINE = 3, COLUMN = 4, CATEGORY = 5 }; AutoArraySchemaWriter writer(aWriter, aUniqueStrings); writer.StringElement(LOCATION, aJITFrame.label()); 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 ? "ion" : "baseline"); if (aJITFrame.hasTrackedOptimizations()) { writer.FreeFormElement(OPTIMIZATIONS, [&](SpliceableJSONWriter& aWriter, UniqueJSONStrings& aUniqueStrings) { StreamJITFrameOptimizations(aWriter, aUniqueStrings, aContext, aJITFrame); }); } } struct CStringWriteFunc : public JSONWriteFunc { nsACString& mBuffer; // The struct must not outlive this buffer explicit CStringWriteFunc(nsACString& aBuffer) : mBuffer(aBuffer) {} void Write(const char* aStr) override { mBuffer.Append(aStr); } }; static nsCString JSONForJITFrame(JSContext* aContext, const JS::ProfiledFrameHandle& aJITFrame, UniqueJSONStrings& aUniqueStrings) { nsCString json; SpliceableJSONWriter writer(MakeUnique(json)); StreamJITFrame(aContext, writer, aUniqueStrings, aJITFrame); return json; } void JITFrameInfo::AddInfoForRange(uint64_t aRangeStart, uint64_t aRangeEnd, JSContext* aCx, const std::function&)>& aJITAddressProvider) { if (aRangeStart == aRangeEnd) { return; } MOZ_RELEASE_ASSERT(aRangeStart < aRangeEnd); if (!mRanges.IsEmpty()) { const JITFrameInfoForBufferRange& prevRange = mRanges.LastElement(); MOZ_RELEASE_ASSERT(prevRange.mRangeEnd <= aRangeStart, "Ranges must be non-overlapping and added in-order."); } using JITFrameKey = JITFrameInfoForBufferRange::JITFrameKey; nsClassHashtable, nsTArray> jitAddressToJITFrameMap; nsClassHashtable, nsCString> jitFrameToFrameJSONMap; aJITAddressProvider([&](void* aJITAddress) { // Make sure that we have cached data for aJITAddress. if (!jitAddressToJITFrameMap.Contains(aJITAddress)) { nsTArray& jitFrameKeys = *jitAddressToJITFrameMap.LookupOrAdd(aJITAddress); for (JS::ProfiledFrameHandle handle : JS::GetProfiledFrames(aCx, aJITAddress)) { uint32_t depth = jitFrameKeys.Length(); JITFrameKey jitFrameKey{ handle.canonicalAddress(), depth }; if (!jitFrameToFrameJSONMap.Contains(jitFrameKey)) { nsCString& json = *jitFrameToFrameJSONMap.LookupOrAdd(jitFrameKey); json = JSONForJITFrame(aCx, handle, *mUniqueStrings); } jitFrameKeys.AppendElement(jitFrameKey); } } }); mRanges.AppendElement(JITFrameInfoForBufferRange{ aRangeStart, aRangeEnd, std::move(jitAddressToJITFrameMap), std::move(jitFrameToFrameJSONMap) }); } struct ProfileSample { uint32_t mStack; double mTime; Maybe mResponsiveness; Maybe mRSS; Maybe mUSS; }; static void WriteSample(SpliceableJSONWriter& aWriter, UniqueJSONStrings& aUniqueStrings, const ProfileSample& aSample) { enum Schema : uint32_t { STACK = 0, TIME = 1, RESPONSIVENESS = 2, RSS = 3, USS = 4 }; AutoArraySchemaWriter writer(aWriter, aUniqueStrings); writer.IntElement(STACK, aSample.mStack); writer.DoubleElement(TIME, aSample.mTime); if (aSample.mResponsiveness.isSome()) { writer.DoubleElement(RESPONSIVENESS, *aSample.mResponsiveness); } if (aSample.mRSS.isSome()) { writer.DoubleElement(RSS, *aSample.mRSS); } if (aSample.mUSS.isSome()) { writer.DoubleElement(USS, *aSample.mUSS); } } class EntryGetter { public: explicit EntryGetter(const ProfileBuffer& aBuffer, uint64_t aInitialReadPos = 0) : mBuffer(aBuffer) , mReadPos(aBuffer.mRangeStart) { if (aInitialReadPos != 0) { MOZ_RELEASE_ASSERT(aInitialReadPos >= aBuffer.mRangeStart && aInitialReadPos <= aBuffer.mRangeEnd); mReadPos = aInitialReadPos; } } bool Has() const { return mReadPos != mBuffer.mRangeEnd; } const ProfileBufferEntry& Get() const { return mBuffer.GetEntry(mReadPos); } void Next() { mReadPos++; } uint64_t CurPos() { return mReadPos; } private: const ProfileBuffer& mBuffer; uint64_t mReadPos; }; // The following grammar shows legal sequences of profile buffer entries. // The sequences beginning with a ThreadId entry are known as "samples". // // ( // ( /* Samples */ // ThreadId // Time // ( NativeLeafAddr // | Label DynamicStringFragment* LineNumber? Category? // | JitReturnAddr // )+ // Marker* // Responsiveness? // ResidentMemory? // UnsharedMemory? // ) // | ( ResidentMemory UnsharedMemory? Time) /* Memory */ // | ( /* Counters */ // CounterId // Time // ( // CounterKey // Count // Number? // )* // ) // | CollectionStart // | CollectionEnd // | Pause // | Resume // )* // // 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") // Category(ProfilingStackFrame::Category::JS) // // Label("XREMain::XRE_main") // LineNumber(4660) // Category(ProfilingStackFrame::Category::OTHER) // // Label("ElementRestyler::ComputeStyleChangeFor") // LineNumber(3003) // Category(ProfilingStackFrame::Category::CSS) // // - ProfilingStack frames with a dynamic string: // // Label("nsObserverService::NotifyObservers") // DynamicStringFragment("domwindo") // DynamicStringFragment("wopened") // LineNumber(291) // Category(ProfilingStackFrame::Category::OTHER) // // Label("") // 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) // Category(ProfilingStackFrame::Category::JS) // // Label("") // DynamicStringFragment("bound (s") // DynamicStringFragment("elf-host") // DynamicStringFragment("ed:914)") // LineNumber(945) // Category(ProfilingStackFrame::Category::JS) // // - A profiling stack frame with a dynamic string, but with privacy enabled: // // Label("nsObserverService::NotifyObservers") // DynamicStringFragment("(private") // DynamicStringFragment(")") // LineNumber(291) // Category(ProfilingStackFrame::Category::OTHER) // // - A profiling stack frame with an overly long dynamic string: // // Label("") // DynamicStringFragment("(too lon") // DynamicStringFragment("g)") // LineNumber(100) // Category(ProfilingStackFrame::Category::NETWORK) // // - A wasm JIT frame: // // Label("") // 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("") // 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; \ } void ProfileBuffer::StreamSamplesToJSON(SpliceableJSONWriter& aWriter, int aThreadId, double aSinceTime, UniqueStacks& aUniqueStacks) const { UniquePtr strbuf = MakeUnique(kMaxFrameKeyLength); EntryGetter e(*this); 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; } else { e.Next(); } } if (!e.Has()) { break; } if (e.Get().IsThreadId()) { int threadId = e.Get().u.mInt; e.Next(); // Ignore samples that are for the wrong thread. if (threadId != aThreadId) { continue; } } else { // Due to the skip_to_next_sample block above, if we have an entry here // it must be a ThreadId entry. MOZ_CRASH(); } ProfileSample sample; if (e.Has() && e.Get().IsTime()) { sample.mTime = e.Get().u.mDouble; e.Next(); // Ignore samples that are too old. if (sample.mTime < aSinceTime) { continue; } } else { ERROR_AND_CONTINUE("expected a Time entry"); } UniqueStacks::StackKey stack = aUniqueStacks.BeginStack(UniqueStacks::FrameKey("(root)")); int numFrames = 0; while (e.Has()) { if (e.Get().IsNativeLeafAddr()) { numFrames++; // Bug 753041: We need a double cast here to tell GCC that we don't // want to sign extend 32-bit addresses starting with 0xFXXXXXX. unsigned long long pc = (unsigned long long)(uintptr_t)e.Get().u.mPtr; char buf[20]; SprintfLiteral(buf, "%#llx", pc); stack = aUniqueStacks.AppendFrame(stack, UniqueStacks::FrameKey(buf)); e.Next(); } else if (e.Get().IsLabel()) { numFrames++; // Copy the label into strbuf. const char* label = e.Get().u.mString; strncpy(strbuf.get(), label, kMaxFrameKeyLength); size_t i = strlen(label); e.Next(); bool seenFirstDynamicStringFragment = false; while (e.Has()) { if (e.Get().IsDynamicStringFragment()) { // If this is the first dynamic string fragment and we have a // non-empty label, insert a ' ' after the label and before the // dynamic string. if (!seenFirstDynamicStringFragment) { if (i > 0 && i < kMaxFrameKeyLength) { strbuf[i] = ' '; i++; } seenFirstDynamicStringFragment = true; } for (size_t j = 0; j < ProfileBufferEntry::kNumChars; j++) { const char* chars = e.Get().u.mChars; if (i < kMaxFrameKeyLength) { strbuf[i] = chars[j]; i++; } } e.Next(); } else { break; } } strbuf[kMaxFrameKeyLength - 1] = '\0'; Maybe line; if (e.Has() && e.Get().IsLineNumber()) { line = Some(unsigned(e.Get().u.mInt)); e.Next(); } Maybe column; if (e.Has() && e.Get().IsColumnNumber()) { column = Some(unsigned(e.Get().u.mInt)); e.Next(); } Maybe category; if (e.Has() && e.Get().IsCategory()) { category = Some(unsigned(e.Get().u.mInt)); e.Next(); } stack = aUniqueStacks.AppendFrame( stack, UniqueStacks::FrameKey(strbuf.get(), line, column, category)); } else if (e.Get().IsJitReturnAddr()) { numFrames++; // A JIT frame may expand to multiple frames due to inlining. void* pc = e.Get().u.mPtr; const Maybe>& frameKeys = aUniqueStacks.LookupFramesForJITAddressFromBufferPos(pc, 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) { stack = aUniqueStacks.AppendFrame(stack, frameKey); } e.Next(); } else { break; } } if (numFrames == 0) { ERROR_AND_CONTINUE("expected one or more frame entries"); } sample.mStack = aUniqueStacks.GetOrAddStackIndex(stack); // Skip over the markers. We process them in StreamMarkersToJSON(). while (e.Has()) { if (e.Get().IsMarker()) { e.Next(); } else { break; } } if (e.Has() && e.Get().IsResponsiveness()) { sample.mResponsiveness = Some(e.Get().u.mDouble); e.Next(); } if (e.Has() && e.Get().IsResidentMemory()) { sample.mRSS = Some(e.Get().u.mDouble); e.Next(); } if (e.Has() && e.Get().IsUnsharedMemory()) { sample.mUSS = Some(e.Get().u.mDouble); e.Next(); } WriteSample(aWriter, *aUniqueStacks.mUniqueStrings, sample); } } void ProfileBuffer::AddJITInfoForRange(uint64_t aRangeStart, int aThreadId, JSContext* aContext, JITFrameInfo& aJITFrameInfo) const { // We can only process JitReturnAddr entries if we have a JSContext. MOZ_RELEASE_ASSERT(aContext); aRangeStart = std::max(aRangeStart, mRangeStart); aJITFrameInfo.AddInfoForRange(aRangeStart, mRangeEnd, aContext, [&](const std::function& aJITAddressConsumer) { // Find all JitReturnAddr entries in the given range for the given thread, // and call aJITAddressConsumer with those addresses. EntryGetter e(*this, 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()); int threadId = e.Get().u.mInt; e.Next(); // Ignore samples that are for a different thread. if (threadId != aThreadId) { continue; } while (e.Has() && !e.Get().IsThreadId()) { if (e.Get().IsJitReturnAddr()) { aJITAddressConsumer(e.Get().u.mPtr); } e.Next(); } } }); } void ProfileBuffer::StreamMarkersToJSON(SpliceableJSONWriter& aWriter, int aThreadId, const TimeStamp& aProcessStartTime, double aSinceTime, UniqueStacks& aUniqueStacks) const { EntryGetter e(*this); // Stream all markers whose threadId matches aThreadId. We skip other entries, // because we process them in StreamSamplesToJSON(). // // NOTE: The ThreadId of a marker is determined by its GetThreadId() method, // rather than ThreadId buffer entries, as markers can be added outside of // samples. while (e.Has()) { if (e.Get().IsMarker()) { const ProfilerMarker* marker = e.Get().u.mMarker; if (marker->GetTime() >= aSinceTime && marker->GetThreadId() == aThreadId) { marker->StreamJSON(aWriter, aProcessStartTime, aUniqueStacks); } } e.Next(); } } struct CounterKeyedSample { double mTime; uint64_t mNumber; int64_t mCount; }; typedef nsTArray CounterKeyedSamples; typedef nsDataHashtable CounterMap; void ProfileBuffer::StreamCountersToJSON(SpliceableJSONWriter& aWriter, const TimeStamp& aProcessStartTime, double aSinceTime) 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. EntryGetter e(*this); 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 nsDataHashtable 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().u.mPtr; CounterMap& counter = counters.GetOrInsert(id); e.Next(); if (!e.Has() || !e.Get().IsTime()) { ERROR_AND_CONTINUE("expected a Time entry"); } double time = e.Get().u.mDouble; if (time >= aSinceTime) { e.Next(); while (e.Has() && e.Get().IsCounterKey()) { uint64_t key = e.Get().u.mUint64; CounterKeyedSamples& data = counter.GetOrInsert(key); e.Next(); if (!e.Has() || !e.Get().IsCount()) { ERROR_AND_CONTINUE("expected a Count entry"); } int64_t count = e.Get().u.mUint64; e.Next(); uint64_t number; if (!e.Has() || !e.Get().IsNumber()) { number = 0; } else { number = e.Get().u.mInt64; } CounterKeyedSample sample = {time, number, count}; data.AppendElement(sample); } } else { // skip counter sample - only need to skip the initial counter // id, then let the loop at the top skip the rest } } 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.Data(); const BaseProfilerCount* base_counter = static_cast(iter.Key()); aWriter.Start(); aWriter.StringProperty("name", base_counter->mLabel); aWriter.StringProperty("category", base_counter->mCategory); aWriter.StringProperty("description", base_counter->mDescription); aWriter.StartObjectProperty("sample_groups"); for (auto counter_iter = counter.Iter(); !counter_iter.Done(); counter_iter.Next()) { CounterKeyedSamples& samples = counter_iter.Data(); uint64_t key = counter_iter.Key(); size_t size = samples.Length(); if (size == 0) { continue; } 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"); 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 last sample if (i == 0 || samples[i].mNumber != previousNumber || samples[i].mCount != previousCount) { MOZ_ASSERT(i == 0 || samples[i].mTime >= samples[i - 1].mTime); MOZ_ASSERT(samples[i].mNumber >= previousNumber); aWriter.StartArrayElement(SpliceableJSONWriter::SingleLineStyle); aWriter.DoubleElement(samples[i].mTime); aWriter.IntElement(samples[i].mNumber - previousNumber); // uint64_t aWriter.IntElement(samples[i].mCount - previousCount); // int64_t aWriter.EndArray(); previousNumber = samples[i].mNumber; previousCount = samples[i].mCount; } } aWriter.EndArray(); // data aWriter.EndObject(); // samples } aWriter.EndObject(); // sample groups aWriter.End(); // for each counter } aWriter.EndArray(); // counters } void ProfileBuffer::StreamMemoryToJSON(SpliceableJSONWriter& aWriter, const TimeStamp& aProcessStartTime, double aSinceTime) const { enum Schema : uint32_t { TIME = 0, RSS = 1, USS = 2 }; EntryGetter e(*this); aWriter.StartObjectProperty("memory"); // Stream all memory (rss/uss) data. We skip other entries, because we // process them in StreamSamplesToJSON()/etc. aWriter.IntProperty("initial_heap", 0); // XXX FIX aWriter.StartObjectProperty("samples"); { JSONSchemaWriter schema(aWriter); schema.WriteField("time"); schema.WriteField("rss"); schema.WriteField("uss"); } aWriter.StartArrayProperty("data"); int64_t previous_rss = 0; int64_t previous_uss = 0; while (e.Has()) { // valid sequence: Resident, Unshared?, Time if (e.Get().IsResidentMemory()) { int64_t rss = e.Get().u.mInt64; int64_t uss = 0; e.Next(); if (e.Has()) { if (e.Get().IsUnsharedMemory()) { uss = e.Get().u.mDouble; e.Next(); if (!e.Has()) { break; } } if (e.Get().IsTime()) { double time = e.Get().u.mDouble; if (time >= aSinceTime && (previous_rss != rss || previous_uss != uss)) { aWriter.StartArrayElement(SpliceableJSONWriter::SingleLineStyle); aWriter.DoubleElement(time); aWriter.IntElement(rss); // int64_t if (uss != 0) { aWriter.IntElement(uss); // int64_t } aWriter.EndArray(); previous_rss = rss; previous_uss = uss; } } else { ERROR_AND_CONTINUE("expected a Time entry"); } } } e.Next(); } aWriter.EndArray(); // data aWriter.EndObject(); // samples aWriter.EndObject(); // memory } #undef ERROR_AND_CONTINUE static void AddPausedRange(SpliceableJSONWriter& aWriter, const char* aReason, const Maybe& aStartTime, const Maybe& aEndTime) { aWriter.Start(); if (aStartTime) { aWriter.DoubleProperty("startTime", *aStartTime); } else { aWriter.NullProperty("startTime"); } if (aEndTime) { aWriter.DoubleProperty("endTime", *aEndTime); } else { aWriter.NullProperty("endTime"); } aWriter.StringProperty("reason", aReason); aWriter.End(); } void ProfileBuffer::StreamPausedRangesToJSON(SpliceableJSONWriter& aWriter, double aSinceTime) const { EntryGetter e(*this); Maybe currentPauseStartTime; Maybe currentCollectionStartTime; while (e.Has()) { if (e.Get().IsPause()) { currentPauseStartTime = Some(e.Get().u.mDouble); } else if (e.Get().IsResume()) { AddPausedRange(aWriter, "profiler-paused", currentPauseStartTime, Some(e.Get().u.mDouble)); currentPauseStartTime = Nothing(); } else if (e.Get().IsCollectionStart()) { currentCollectionStartTime = Some(e.Get().u.mDouble); } else if (e.Get().IsCollectionEnd()) { AddPausedRange(aWriter, "collecting", currentCollectionStartTime, Some(e.Get().u.mDouble)); currentCollectionStartTime = Nothing(); } e.Next(); } if (currentPauseStartTime) { AddPausedRange(aWriter, "profiler-paused", currentPauseStartTime, Nothing()); } if (currentCollectionStartTime) { AddPausedRange(aWriter, "collecting", currentCollectionStartTime, Nothing()); } } bool ProfileBuffer::DuplicateLastSample(int aThreadId, const TimeStamp& aProcessStartTime, Maybe& aLastSample) { if (aLastSample && *aLastSample < mRangeStart) { // The last sample is no longer within the buffer range, so we cannot use // it. Reset the stored buffer position to Nothing(). aLastSample.reset(); } if (!aLastSample) { return false; } uint64_t lastSampleStartPos = *aLastSample; MOZ_RELEASE_ASSERT(GetEntry(lastSampleStartPos).IsThreadId() && GetEntry(lastSampleStartPos).u.mInt == aThreadId); aLastSample = Some(AddThreadIdEntry(aThreadId)); EntryGetter e(*this, lastSampleStartPos + 1); // 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::CollectionStart: case ProfileBufferEntry::Kind::CollectionEnd: case ProfileBufferEntry::Kind::ThreadId: // We're done. return true; case ProfileBufferEntry::Kind::Time: // Copy with new time AddEntry(ProfileBufferEntry::Time( (TimeStamp::Now() - aProcessStartTime).ToMilliseconds())); break; case ProfileBufferEntry::Kind::Marker: case ProfileBufferEntry::Kind::ResidentMemory: case ProfileBufferEntry::Kind::UnsharedMemory: case ProfileBufferEntry::Kind::CounterKey: case ProfileBufferEntry::Kind::Number: case ProfileBufferEntry::Kind::Count: case ProfileBufferEntry::Kind::Responsiveness: // 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; default: { // Copy anything else we don't know about. ProfileBufferEntry entry = e.Get(); AddEntry(entry); break; } } e.Next(); } return true; } // END ProfileBuffer ////////////////////////////////////////////////////////////////////////