/* 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 "CollectorLogAnalyzer.h" #include #include #include "CollectorLogAnalyzerBackground.h" #include "mozilla/ErrorResult.h" #include "mozilla/HashTable.h" #include "mozilla/SIMD.h" #include "mozilla/Vector.h" #include "mozilla/dom/ChromeUtils.h" #include "mozilla/dom/FileCreatorHelper.h" #include "mozilla/dom/Promise.h" #include "nsFileStreams.h" #include "nsIFile.h" #include "nsIGlobalObject.h" #include "nsXULAppAPI.h" #include "prio.h" namespace mozilla { static constexpr auto MAX_QUERY_RESULTS = dom::CollectorLogAnalyzer_Binding::MAX_QUERY_RESULTS; static constexpr auto SAMPLE_COUNT = dom::CollectorLogAnalyzer_Binding::SAMPLE_COUNT; NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CollectorLogAnalyzer, mGlobal) NS_IMPL_CYCLE_COLLECTING_ADDREF(CollectorLogAnalyzer) NS_IMPL_CYCLE_COLLECTING_RELEASE(CollectorLogAnalyzer) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CollectorLogAnalyzer) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END CollectorLogAnalyzer::CollectorLogAnalyzer(nsIGlobalObject* aGlobal, const nsAString& aCCLogPath, const nsAString& aGCLogPath) : mGlobal(aGlobal), mCCLogPath(aCCLogPath), mGCLogPath(aGCLogPath) { MOZ_ALWAYS_SUCCEEDS( NS_CreateBackgroundTaskQueue("CollectorLogAnalyzer::BackgroundTaskQueue", getter_AddRefs(mBackgroundEventTarget))); MOZ_RELEASE_ASSERT(mBackgroundEventTarget); mBackground = MakeRefPtr(); } JSObject* CollectorLogAnalyzer::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return dom::CollectorLogAnalyzer_Binding::Wrap(aCx, this, aGivenProto); } template static void ResolveJSPromise(dom::Promise* aPromise, T&& aValue) { if constexpr (std::is_same_v) { aPromise->MaybeResolveWithUndefined(); } else { aPromise->MaybeResolve(std::forward(aValue)); } } static nsCString FormatErrorMessage(nsresult aError, const nsCString& aMessage) { nsAutoCString errorName; GetErrorName(aError, errorName); nsCString msg(aMessage); msg.AppendPrintf(" (%s)", errorName.get()); return msg; } static void RejectJSPromise(dom::Promise* aPromise, const LogError& aError) { const auto errMsg = FormatErrorMessage(aError.Code(), aError.Message()); aPromise->MaybeRejectWithUnknownError(errMsg); } template already_AddRefed CollectorLogAnalyzer::DispatchToBackground( ErrorResult& aError, Fn aFunc) { MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); MOZ_RELEASE_ASSERT(mBackgroundEventTarget); RefPtr jsPromise = dom::Promise::Create(mGlobal, aError); if (aError.Failed()) { return nullptr; } MOZ_ASSERT(jsPromise); auto nativePromise = MakeRefPtr::Private>(__func__); mBackgroundEventTarget->Dispatch( NS_NewRunnableFunction( "CollectorLogAnalyzer::BackgroundTaskQueue::Dispatch", [nativePromise, func = std::move(aFunc)] { Result result = func(); if (result.isErr()) { nativePromise->Reject(result.unwrapErr(), __func__); } else { nativePromise->Resolve(result.unwrap(), __func__); } }), NS_DISPATCH_EVENT_MAY_BLOCK); nativePromise->Then( GetCurrentSerialEventTarget(), __func__, [promise = RefPtr(jsPromise)](OkT&& ok) { ResolveJSPromise(promise, std::forward(ok)); }, [promise = RefPtr(jsPromise)](const LogError& err) { RejectJSPromise(promise, err); }); return jsPromise.forget(); } enum class GCThingKind { None, Marked, Unmarked, }; Result CollectorLogAnalyzerBackground::EnsureInitialized() { if (!mInitialized) { return Err(LogError(NS_ERROR_NOT_INITIALIZED, "The CollectorLogAnalyzer is not initialized")); } return Ok(); } template Result PutInMap(TMap& aMap, TKey&& aKey, TValue&& aValue) { if (!aMap.put(std::forward(aKey), std::forward(aValue))) { return Err( LogError(NS_ERROR_OUT_OF_MEMORY, "Out of memory growing hash map")); } return Ok(); } template Result AddToMap(TMap& aMap, typename TMap::AddPtr& aAddPtr, TKey&& aKey, TValue&& aValue) { if (!aMap.add(aAddPtr, std::forward(aKey), std::forward(aValue))) { return Err( LogError(NS_ERROR_OUT_OF_MEMORY, "Out of memory growing hash map")); } return Ok(); } template Result PutInSet(TSet& set, TValue&& aValue) { if (!set.put(std::forward(aValue))) { return Err( LogError(NS_ERROR_OUT_OF_MEMORY, "Out of memory growing hash set")); } return Ok(); } template Result AppendToVector(TVector& vec, TValue&& aValue) { if (!vec.append(std::forward(aValue))) { return Err(LogError(NS_ERROR_OUT_OF_MEMORY, "Out of memory growing vector. Length: %zu", vec.length())); } return Ok(); } [[nodiscard]] static bool SkipCommentLine(const nsACString& aBuf, size_t* aCurrChar) { if (aBuf[*aCurrChar] != '#') { return false; } const char* start = aBuf.BeginReading(); const char* newline = SIMD::memchr8(start + *aCurrChar, '\n', aBuf.Length() - *aCurrChar); if (!newline) { return false; } *aCurrChar = newline - start + 1; return true; } [[nodiscard]] static bool MatchLiteral(const nsACString& aBuf, size_t* aCurrChar, const nsLiteralCString& aLiteral) { size_t currChar = *aCurrChar; if (aLiteral.Length() >= aBuf.Length() - currChar) { return false; } if (memcmp(aBuf.BeginReading() + currChar, aLiteral.get(), aLiteral.Length()) != 0) { return false; } *aCurrChar += aLiteral.Length(); return true; } [[nodiscard]] static bool ParsePointer(const nsACString& aBuf, size_t* aCurrChar, uint64_t* aPtr) { size_t currChar = *aCurrChar; if (MatchLiteral(aBuf, &currChar, "(nil)"_ns)) { *aPtr = 0; *aCurrChar = currChar; return true; } char* end = nullptr; const char* begin = aBuf.BeginReading() + currChar; *aPtr = strtoull(begin, &end, 16); if (end == begin) { return false; } *aCurrChar = currChar + (end - begin); return true; } [[nodiscard]] static bool ParseDecimal(const nsACString& aBuf, size_t* aCurrChar, int32_t* aValue) { size_t currChar = *aCurrChar; char* end = nullptr; const char* begin = aBuf.BeginReading() + currChar; *aValue = strtol(begin, &end, 10); if (end == begin) { return false; } *aCurrChar = currChar + (end - begin); return true; } static void SkipTabsAndSpaces(const nsACString& aBuf, size_t* aCurrChar) { size_t currChar = *aCurrChar; while (currChar < aBuf.Length() && (aBuf[currChar] == ' ' || aBuf[currChar] == '\t')) { currChar++; } *aCurrChar = currChar; } [[nodiscard]] static bool MatchLineEnd(const nsACString& aBuf, size_t* aCurrChar) { size_t currChar = *aCurrChar; if (currChar < aBuf.Length() && aBuf[currChar] == '\r') { currChar++; } if (currChar >= aBuf.Length() || aBuf[currChar] != '\n') { return false; } currChar++; *aCurrChar = currChar; return true; } // If we have a newline character after the current character in the // buffer, this will collect the contents of the buffer starting at // *aCurrChar up until the first newline. [[nodiscard]] static bool MatchRemainingLine(const nsACString& aBuf, size_t* aCurrChar, nsACString* aResult) { size_t currChar = *aCurrChar; size_t resultStart = currChar; while (currChar < aBuf.Length() && aBuf[currChar] != '\r' && aBuf[currChar] != '\n') { currChar++; } size_t resultEnd = currChar; if (!MatchLineEnd(aBuf, &currChar)) { return false; } aResult->Assign(Substring(aBuf, resultStart, resultEnd - resultStart)); *aCurrChar = currChar; return true; } [[nodiscard]] static bool MatchEndSection(const nsACString& aBuf, size_t* aCurrChar) { size_t currChar = *aCurrChar; if (!MatchLiteral(aBuf, &currChar, "=========="_ns)) { return false; } if (!MatchLineEnd(aBuf, &currChar)) { return false; } *aCurrChar = currChar; return true; } [[nodiscard]] static bool ParseCCNode(const nsACString& aBuf, size_t* aCurrChar, uint64_t* aPtr, int32_t* aRefCnt, GCThingKind* aGCKind, nsACString* aLabel) { size_t currChar = *aCurrChar; if (!ParsePointer(aBuf, &currChar, aPtr)) { return false; } if (MatchLiteral(aBuf, &currChar, " [gc"_ns)) { *aRefCnt = -1; if (MatchLiteral(aBuf, &currChar, ".marked]"_ns)) { *aGCKind = GCThingKind::Marked; } else if (MatchLiteral(aBuf, &currChar, "]"_ns)) { *aGCKind = GCThingKind::Unmarked; } else { return false; } } else if (MatchLiteral(aBuf, &currChar, " [rc="_ns)) { *aGCKind = GCThingKind::None; if (!ParseDecimal(aBuf, &currChar, aRefCnt)) { return false; } if (!MatchLiteral(aBuf, &currChar, "]"_ns)) { return false; } } else { return false; } SkipTabsAndSpaces(aBuf, &currChar); if (!MatchRemainingLine(aBuf, &currChar, aLabel)) { return false; } *aCurrChar = currChar; return true; } enum class GCColor { Black, Gray, White, }; [[nodiscard]] static bool ParseGCNode(const nsACString& aBuf, size_t* aCurrChar, uint64_t* aPtr, GCColor* aColor, nsACString* aLabel) { size_t currChar = *aCurrChar; if (!ParsePointer(aBuf, &currChar, aPtr)) { return false; } if (MatchLiteral(aBuf, &currChar, " B"_ns)) { *aColor = GCColor::Black; } else if (MatchLiteral(aBuf, &currChar, " G"_ns)) { *aColor = GCColor::Gray; } else if (MatchLiteral(aBuf, &currChar, " W"_ns)) { *aColor = GCColor::White; } else { return false; } SkipTabsAndSpaces(aBuf, &currChar); if (!MatchRemainingLine(aBuf, &currChar, aLabel)) { return false; } *aCurrChar = currChar; return true; } [[nodiscard]] static bool ParseCCEdge(const nsACString& aBuf, size_t* aCurrChar, uint64_t* aPtr, int32_t* aNesting, bool* aWeak, nsACString* aLabel) { size_t currChar = *aCurrChar; if (!MatchLiteral(aBuf, &currChar, "> "_ns)) { return false; } if (!ParsePointer(aBuf, &currChar, aPtr)) { return false; } if (MatchLiteral(aBuf, &currChar, " [weak]"_ns)) { *aWeak = true; } if (MatchLiteral(aBuf, &currChar, " [nesting="_ns)) { if (!ParseDecimal(aBuf, &currChar, aNesting)) { return false; } if (!MatchLiteral(aBuf, &currChar, "]"_ns)) { return false; } } SkipTabsAndSpaces(aBuf, &currChar); if (!MatchRemainingLine(aBuf, &currChar, aLabel)) { return false; } *aCurrChar = currChar; return true; } [[nodiscard]] static bool ParseGCEdge(const nsACString& aBuf, size_t* aCurrChar, uint64_t* aPtr, bool* aWeak, nsACString* aLabel) { size_t currChar = *aCurrChar; if (!MatchLiteral(aBuf, &currChar, "> "_ns)) { return false; } if (!ParsePointer(aBuf, &currChar, aPtr)) { return false; } if (!MatchLiteral(aBuf, &currChar, " B"_ns) && !MatchLiteral(aBuf, &currChar, " G"_ns) && !MatchLiteral(aBuf, &currChar, " W"_ns)) { return false; } if (MatchLiteral(aBuf, &currChar, " [weak]"_ns)) { *aWeak = true; } SkipTabsAndSpaces(aBuf, &currChar); if (!MatchRemainingLine(aBuf, &currChar, aLabel)) { return false; } *aCurrChar = currChar; return true; } [[nodiscard]] static bool ParseIncrementalRoot(const nsACString& aBuf, size_t* aCurrChar, uint64_t* aPtr) { size_t currChar = *aCurrChar; if (!MatchLiteral(aBuf, &currChar, "IncrementalRoot "_ns)) { return false; } if (!ParsePointer(aBuf, &currChar, aPtr)) { return false; } if (!MatchLineEnd(aBuf, &currChar)) { return false; } *aCurrChar = currChar; return true; } [[nodiscard]] static bool ParseWeakMapEntry(const nsACString& aBuf, size_t* aCurrChar, WeakMapEntry* aEntry) { size_t currChar = *aCurrChar; if (!MatchLiteral(aBuf, &currChar, "WeakMapEntry map="_ns)) { return false; } if (!ParsePointer(aBuf, &currChar, &aEntry->mMap)) { return false; } if (!MatchLiteral(aBuf, &currChar, " key="_ns)) { return false; } if (!ParsePointer(aBuf, &currChar, &aEntry->mKey)) { return false; } if (!MatchLiteral(aBuf, &currChar, " keyDelegate="_ns)) { return false; } if (!ParsePointer(aBuf, &currChar, &aEntry->mKeyDelegate)) { return false; } if (!MatchLiteral(aBuf, &currChar, " value="_ns)) { return false; } if (!ParsePointer(aBuf, &currChar, &aEntry->mValue)) { return false; } if (!MatchLineEnd(aBuf, &currChar)) { return false; } *aCurrChar = currChar; return true; } [[nodiscard]] static bool ParseResult(const nsACString& aBuf, size_t* aCurrChar, uint64_t* aPtr, int32_t* aKnown) { size_t currChar = *aCurrChar; if (!ParsePointer(aBuf, &currChar, aPtr)) { return false; } if (MatchLiteral(aBuf, &currChar, " [garbage]"_ns)) { *aKnown = -1; } else if (MatchLiteral(aBuf, &currChar, " [known="_ns)) { if (!ParseDecimal(aBuf, &currChar, aKnown)) { return false; } if (!MatchLiteral(aBuf, &currChar, "]"_ns)) { return false; } } else { return false; } if (!MatchLineEnd(aBuf, &currChar)) { return false; } *aCurrChar = currChar; return true; } // Find and return the index of aNeedle within aHaystack. If aNeedle can't be // found, this returns -1 static int64_t FindMatch(mozilla::Span aHaystack, const nsCString& aNeedle) { MOZ_ASSERT(aNeedle.Length() > 0); if (aNeedle.Length() == 1) { const char* pos = SIMD::memchr8(aHaystack.Elements(), aNeedle[0], aHaystack.Length()); if (pos) { return pos - aHaystack.Elements(); } return -1; } size_t index = 0; while (aHaystack.Length() >= aNeedle.Length()) { const char* pos; const size_t inlineLookaheadChars = 2; size_t searchLen = aHaystack.Length() - aNeedle.Length() + inlineLookaheadChars; pos = SIMD::memchr2x8(aHaystack.Elements(), aNeedle[0], aNeedle[1], searchLen); if (!pos) { return -1; } size_t localIndex = pos - aHaystack.Elements(); aHaystack = aHaystack.Subspan(localIndex); index += localIndex; if (memcmp(aNeedle.get() + inlineLookaheadChars, aHaystack.Elements() + inlineLookaheadChars, aNeedle.Length() - inlineLookaheadChars) == 0) { return int64_t(index); } aHaystack = aHaystack.Subspan(1); index += 1; } return -1; } // Since we store all the strings in one contiguous buffer for search purposes // (i.e., aHaystack), we expose this helper to get the boundaries of a string // based on the start index (aInnerIndex) of a substring within it. We return // the start and end index of the string in aStart and aEnd respectively. The // boundaries of a string are determined either by the presence of a null // character or the limits of the buffer. static void GetStringBoundaries(mozilla::Span aHaystack, size_t aInnerIndex, size_t* aStart, size_t* aEnd) { MOZ_ASSERT(aInnerIndex < aHaystack.Length()); MOZ_ASSERT(aHaystack[aInnerIndex] != '\0'); MOZ_ASSERT(uint64_t(aHaystack.Length()) <= uint64_t(std::numeric_limits::max())); *aStart = 0; for (int64_t i = int64_t(aInnerIndex) - 1; i >= 0; --i) { if (aHaystack[i] == '\0') { *aStart = i + 1; break; } } const char* endPtr = SIMD::memchr8(aHaystack.Elements() + aInnerIndex, '\0', aHaystack.Length() - aInnerIndex); if (endPtr == nullptr) { *aEnd = aHaystack.Length(); } else { *aEnd = endPtr - aHaystack.Elements(); } } // This finds every occurrence of aQuery in a contiguous buffer of strings // delimited by null characters. For example, if aQuery is "Global", then in a // typical log's processed string buffer this should find the index of // "nsGlobalWindowInner" and the index of "nsGlobalWindowOuter", among others. static Result, LogError> FindStringIndices( const Vector& aStrings, const nsCString& aQuery) { HashSet result; mozilla::Span haystack = aStrings; size_t offset = 0; int64_t match = -1; while (offset < haystack.Length() && (match = FindMatch(haystack.subspan(offset), aQuery)) != -1) { size_t start; size_t end; GetStringBoundaries(haystack, match + offset, &start, &end); MOZ_TRY(PutInSet(result, start)); offset = end; } return result; } // There are typically a lot of duplicated strings within a GC/CC log, so we // intern strings into one contiguous buffer and represent node/edge labels // as indexes into this buffer. If two nodes have the same label, they will // have the same index. This function takes aStr and returns the index at // which the string can be found in the contiguous buffer (mStrings). If aStr // does not already exist within mStrings, this will add it. Result CollectorLogAnalyzerBackground::InternString(const nsCString& aStr) { auto p = mStringTable.lookupForAdd(aStr); if (!p) { size_t res = mStrings.length(); size_t reserveLength = aStr.Length() + 1; if (!mStrings.growByUninitialized(reserveLength)) { return Err(LogError(NS_ERROR_OUT_OF_MEMORY, "Out of memory growing string buffer")); } char* writePtr = mStrings.end() - reserveLength; memcpy(writePtr, aStr.get(), aStr.Length()); writePtr[reserveLength - 1] = '\0'; MOZ_TRY(AddToMap(mStringTable, p, aStr, res)); } return p->value(); } Result CollectorLogAnalyzerBackground::EnsureNode( NodeId aNodeId) { auto p = mNodeIdsToIndices.lookupForAdd(aNodeId); if (!p) { NodeTableIndex index = mNodeIds.length(); MOZ_TRY(AppendToVector(mNodeIds, aNodeId)); MOZ_TRY(AddToMap(mNodeIdsToIndices, p, aNodeId, index)); MOZ_TRY(AppendToVector(mNodeLabels, INVALID_STRING)); MOZ_TRY(AppendToVector(mNodeFlags, 0)); MOZ_TRY(AppendToVector(mObservedReferenceCounts, 0)); NodeEdgesDescriptor edges = {}; edges.mCC = INVALID_NODE; edges.mGC = INVALID_NODE; edges.mWeakMap = INVALID_NODE; MOZ_TRY(AppendToVector(mNodeEdges, edges)); } return p->value(); } Result CollectorLogAnalyzerBackground::AddWeakMapEdge( NodeTableIndex aKey, NodeTableIndex aMap, NodeTableIndex aValue, bool aKeyDelegate, bool aKeyIsSource) { WeakMapEdge edge = {}; edge.mKey = aKey; edge.mMap = aMap; edge.mValue = aValue; if (aKeyDelegate) { edge.mKind = WeakMapEdgeKind::WeakMapKeyDelegate; } else { edge.mKind = WeakMapEdgeKind::WeakMapKey; } edge.mKeyIsSource = aKeyIsSource; MOZ_TRY(AppendToVector(mWeakMapEdges, edge)); mNodeEdges[edge.source()].mWeakMapCount++; return Ok(); } Result CollectorLogAnalyzerBackground::AddWeakMapEntry( const WeakMapEntry& aEntry) { NodeTableIndex keyIndex = MOZ_TRY(EnsureNode(aEntry.mKey)); NodeTableIndex mapIndex = MOZ_TRY(EnsureNode(aEntry.mMap)); NodeTableIndex valueIndex = MOZ_TRY(EnsureNode(aEntry.mValue)); MOZ_TRY(AddWeakMapEdge(keyIndex, mapIndex, valueIndex, /* aKeyDelegate = */ false, /* aKeyIsSource = */ true)); MOZ_TRY(AddWeakMapEdge(keyIndex, mapIndex, valueIndex, /* aKeyDelegate = */ false, /* aKeyIsSource = */ false)); if (aEntry.mKeyDelegate != 0) { NodeTableIndex keyDelegateIndex = MOZ_TRY(EnsureNode(aEntry.mKeyDelegate)); MOZ_TRY(AddWeakMapEdge(keyDelegateIndex, mapIndex, keyIndex, /* aKeyDelegate = */ true, /* aKeyIsSource = */ true)); MOZ_TRY(AddWeakMapEdge(keyDelegateIndex, mapIndex, keyIndex, /* aKeyDelegate = */ true, /* aKeyIsSource = */ false)); } return Ok(); } Result CollectorLogAnalyzerBackground::AddCCEdge( NodeTableIndex aCurrentNode, NodeTableIndex aEdge, StringBufferIndex aLabel) { NodeEdgesDescriptor& edges = mNodeEdges[aCurrentNode]; MOZ_ASSERT(edges.mCC + edges.mCCCount == mEdges.length()); MOZ_TRY(AppendToVector(mEdges, aEdge)); MOZ_TRY(AppendToVector(mEdgeLabels, aLabel)); edges.mCCCount++; return Ok(); } Result CollectorLogAnalyzerBackground::AddGCEdge( NodeTableIndex aCurrentNode, NodeTableIndex aEdge, StringBufferIndex aLabel) { NodeEdgesDescriptor& edges = mNodeEdges[aCurrentNode]; MOZ_ASSERT(edges.mGC + edges.mGCCount == mEdges.length()); MOZ_TRY(AppendToVector(mEdges, aEdge)); MOZ_TRY(AppendToVector(mEdgeLabels, aLabel)); edges.mGCCount++; return Ok(); } Result CollectorLogAnalyzerBackground::IngestCycleCollectorLog(const nsACString& aBuf, bool aContainsFileEnd) { size_t currChar = 0; size_t prevChar = 0; while (currChar < aBuf.Length()) { mCCFileProgress += currChar - prevChar; prevChar = currChar; mCCLineNumber++; if (SkipCommentLine(aBuf, &currChar)) { continue; } if (mCurrentCCSection == CCLogSection::Graph) { if (MatchEndSection(aBuf, &currChar)) { mCurrentCCSection = CCLogSection::Results; continue; } uint64_t ptr; int32_t refCount; // -1 if does not exist GCThingKind gcKind; nsAutoCString label; if (ParseCCNode(aBuf, &currChar, &ptr, &refCount, &gcKind, &label)) { mCurrentCCNode = MOZ_TRY(EnsureNode(ptr)); StringBufferIndex labelIndex = MOZ_TRY(InternString(label)); mNodeLabels[mCurrentCCNode] = labelIndex; if (gcKind == GCThingKind::None) { mNodeFlags[mCurrentCCNode] |= dom::CollectorNodeFlags_Binding::CC_MANAGED; if (refCount < 0) { return Err( LogError(NS_ERROR_FILE_CORRUPTED, "Encountered a negative refcount in the CC log")); } MOZ_TRY( PutInMap(mCCReferenceCounts, mCurrentCCNode, uint32_t(refCount))); } else if (!mHaveGC) { mNodeFlags[mCurrentCCNode] |= dom::CollectorNodeFlags_Binding::GC_MARKED; if (gcKind == GCThingKind::Unmarked) { mNodeFlags[mCurrentCCNode] |= dom::CollectorNodeFlags_Binding::GC_GRAY; } } if (mNodeEdges[mCurrentCCNode].mCC != INVALID_NODE) { return Err(LogError( NS_ERROR_FILE_CORRUPTED, "Encountered node 0x%" PRIx64 " twice in the CC log", ptr)); } mNodeEdges[mCurrentCCNode].mCC = mEdges.length(); continue; } int32_t nesting = 0; bool weak = false; if (ParseCCEdge(aBuf, &currChar, &ptr, &nesting, &weak, &label)) { if (mCurrentCCNode >= mNodeEdges.length()) { return Err( LogError(NS_ERROR_FILE_CORRUPTED, "Invalid log format: edge encountered before a node")); } if (mHaveGC && nesting != 0) { continue; } if (weak) { continue; } StringBufferIndex labelIndex = MOZ_TRY(InternString(label)); NodeTableIndex edge = MOZ_TRY(EnsureNode(ptr)); MOZ_TRY(AddCCEdge(mCurrentCCNode, edge, labelIndex)); continue; } if (ParseIncrementalRoot(aBuf, &currChar, &ptr)) { NodeTableIndex node = MOZ_TRY(EnsureNode(ptr)); mNodeFlags[node] |= dom::CollectorNodeFlags_Binding::INCREMENTAL_ROOT; MOZ_TRY(AppendToVector(mIncrementalRoots, node)); continue; } WeakMapEntry entry = {}; if (ParseWeakMapEntry(aBuf, &currChar, &entry)) { MOZ_TRY(AddWeakMapEntry(entry)); continue; } } else if (mCurrentCCSection == CCLogSection::Results) { uint64_t ptr; int32_t known; if (ParseResult(aBuf, &currChar, &ptr, &known)) { NodeTableIndex node = MOZ_TRY(EnsureNode(ptr)); mNodeFlags[node] |= dom::CollectorNodeFlags_Binding::CC_MANAGED; if (known == -1) { mNodeFlags[node] |= dom::CollectorNodeFlags_Binding::GARBAGE; } else { mNodeFlags[node] |= dom::CollectorNodeFlags_Binding::ROOT; } continue; } } else { MOZ_CRASH("Unexpected section"); } nsAutoCString line; bool matchedLine = MatchRemainingLine(aBuf, &currChar, &line); if (matchedLine || aContainsFileEnd) { return Err(LogError(NS_ERROR_FILE_CORRUPTED, "Failed to parse CC line %zu: `%s`", mCCLineNumber, line.get())); } // We didn't actually complete reading the line mCCLineNumber--; MOZ_ASSERT(currChar == prevChar); return currChar; } mCCFileProgress += currChar - prevChar; return currChar; } // A bit of a hack, lifted from https://github.com/amccreight/heapgraph/ // Up-to-date as of Jan 15, 2025. static bool SwitchToGrayRoots(const nsCString& aLabel) { if (aLabel.Equals("mAnonymousGlobalScopes[i]"_ns)) { return true; } if (aLabel.Equals("active window global"_ns)) { return true; } if (aLabel.Equals("mCallback"_ns)) { return true; } if (aLabel.Equals("DOM expando object"_ns)) { return true; } if (StringBeginsWith(aLabel, "XPCNativeInterface"_ns)) { return true; } if (StringBeginsWith(aLabel, "XPCWrappedNative"_ns)) { return true; } if (StringBeginsWith(aLabel, "XPCVariant"_ns)) { return true; } if (StringBeginsWith(aLabel, "nsXPCWrappedJS"_ns)) { return true; } return false; } Result CollectorLogAnalyzerBackground::IngestGarbageCollectorLog( const nsACString& aBuf, bool aContainsFileEnd) { size_t currChar = 0; size_t prevChar = 0; while (currChar < aBuf.Length()) { mGCFileProgress += currChar - prevChar; prevChar = currChar; mGCLineNumber++; if (SkipCommentLine(aBuf, &currChar)) { continue; } if (mCurrentGCSection == GCLogSection::BlackRoots || mCurrentGCSection == GCLogSection::GrayRoots) { if (MatchEndSection(aBuf, &currChar)) { mCurrentGCSection = GCLogSection::Graph; continue; } uint64_t ptr; nsAutoCString label; GCColor color; if (ParseGCNode(aBuf, &currChar, &ptr, &color, &label)) { NodeTableIndex node = MOZ_TRY(EnsureNode(ptr)); if (mCurrentGCSection == GCLogSection::BlackRoots && SwitchToGrayRoots(label)) { mCurrentGCSection = GCLogSection::GrayRoots; } mNodeFlags[node] |= dom::CollectorNodeFlags_Binding::ROOT; if (mCurrentGCSection == GCLogSection::GrayRoots) { mNodeFlags[node] |= dom::CollectorNodeFlags_Binding::SOFT_ROOT; MOZ_TRY(AppendToVector(mGCGrayRoots, node)); } else { MOZ_TRY(AppendToVector(mGCRoots, node)); } continue; } WeakMapEntry entry = {}; if (ParseWeakMapEntry(aBuf, &currChar, &entry)) { MOZ_TRY(AddWeakMapEntry(entry)); continue; } } else if (mCurrentGCSection == GCLogSection::Graph) { uint64_t ptr; nsAutoCString label; GCColor color; if (ParseGCNode(aBuf, &currChar, &ptr, &color, &label)) { mCurrentGCNode = MOZ_TRY(EnsureNode(ptr)); StringBufferIndex labelIndex = MOZ_TRY(InternString(label)); mNodeLabels[mCurrentGCNode] = labelIndex; if (color == GCColor::Gray) { mNodeFlags[mCurrentGCNode] |= dom::CollectorNodeFlags_Binding::GC_MARKED; mNodeFlags[mCurrentGCNode] |= dom::CollectorNodeFlags_Binding::GC_GRAY; } else if (color == GCColor::Black) { mNodeFlags[mCurrentGCNode] |= dom::CollectorNodeFlags_Binding::GC_MARKED; } if (mNodeEdges[mCurrentGCNode].mGC != INVALID_NODE) { return Err(LogError( NS_ERROR_FILE_CORRUPTED, "Encountered node 0x%" PRIx64 " twice in the logs", ptr)); } mNodeEdges[mCurrentGCNode].mGC = mEdges.length(); continue; } bool weak = false; if (ParseGCEdge(aBuf, &currChar, &ptr, &weak, &label)) { if (mCurrentGCNode >= mNodeEdges.length()) { return Err( LogError(NS_ERROR_FILE_CORRUPTED, "Invalid log format: edge encountered before a node")); } if (weak) { continue; } StringBufferIndex labelIndex = MOZ_TRY(InternString(label)); NodeTableIndex edge = MOZ_TRY(EnsureNode(ptr)); MOZ_TRY(AddGCEdge(mCurrentGCNode, edge, labelIndex)); continue; } } else { MOZ_CRASH("Unexpected section"); } nsAutoCString line; if (MatchRemainingLine(aBuf, &currChar, &line) || aContainsFileEnd) { return Err(LogError(NS_ERROR_FILE_CORRUPTED, "Failed to parse GC line %zu: `%s`", mGCLineNumber, line.get())); } // We didn't actually complete reading the line mGCLineNumber--; MOZ_ASSERT(currChar == prevChar); return currChar; } mGCFileProgress += currChar - prevChar; return currChar; } dom::CollectorLogNode CollectorLogAnalyzerBackground::MakeResultNode( NodeTableIndex aIndex) { dom::CollectorLogNode result; NodeId id = mNodeIds[aIndex]; result.mIndex = aIndex; result.mPtr = nsPrintfCString("0x%" PRIx64, id); if (mNodeLabels[aIndex] != INVALID_STRING) { result.mLabel = nsCString(&mStrings[mNodeLabels[aIndex]]); } auto ccRefCountPtr = mCCReferenceCounts.lookup(aIndex); if (ccRefCountPtr) { result.mReferenceCount = ccRefCountPtr->value(); } else { result.mReferenceCount = -1; } result.mFlags = mNodeFlags[aIndex]; return result; } Result, LogError> CollectorLogAnalyzerBackground::QueryNodesImpl(const nsCString& aQuery) { MOZ_TRY(EnsureInitialized()); HashSet stringIndices = MOZ_TRY(FindStringIndices(mStrings, aQuery)); uint64_t bytePattern = 0; size_t matchEnd = 0; bool matchPtrs = false; if (ParsePointer(aQuery, &matchEnd, &bytePattern) && matchEnd == aQuery.Length()) { matchPtrs = true; } nsTArray result; nsTArray garbage; if (!matchPtrs && stringIndices.count() == 0) { return result; } for (NodeTableIndex i = 0; i < mNodeLabels.length(); ++i) { nsTArray* dest = &result; if (mNodeFlags[i] & dom::CollectorNodeFlags_Binding::GARBAGE) { dest = &garbage; } NodeId id = mNodeIds[i]; StringBufferIndex label = mNodeLabels[i]; if (matchPtrs && id == bytePattern) { dest->AppendElement(MakeResultNode(i)); continue; } if (stringIndices.has(label)) { dest->AppendElement(MakeResultNode(i)); continue; } if (result.Length() >= MAX_QUERY_RESULTS) { break; } } size_t garbageIndex = 0; while (result.Length() < MAX_QUERY_RESULTS && garbageIndex < garbage.Length()) { result.AppendElement(garbage.ElementAt(garbageIndex++)); } return result; } Result, LogError> CollectorLogAnalyzerBackground::SampleNodesImpl() { MOZ_TRY(EnsureInitialized()); AutoTArray result; // Reservoir sampling. It's maybe a little overkill but if we ever want // to sample with a filter this is nicely extendable. for (NodeTableIndex i = 0; i < mNodeLabels.length(); ++i) { if (mNodeFlags[i] & dom::CollectorNodeFlags_Binding::GARBAGE) { continue; } if (result.Length() < SAMPLE_COUNT) { result.AppendElement(MakeResultNode(i)); } else { NodeId id = mNodeIds[i]; size_t randomEnough = HashGeneric(id); size_t randomIndex = randomEnough % i; if (randomIndex < SAMPLE_COUNT) { result[randomIndex] = MakeResultNode(i); } } } return result; } Result CollectorLogAnalyzerBackground::InitImpl( const nsAString& aCCLogPath, const nsAString& aGCLogPath) { mHaveCC = aCCLogPath.Length() > 0; mHaveGC = aGCLogPath.Length() > 0; mCCFileSize = 0; mGCFileSize = 0; int64_t totalCCFileSize = 0; int64_t totalGCFileSize = 0; nsCOMPtr ccLogFile; nsCOMPtr gcLogFile; nsresult rv = NS_OK; if (mHaveCC) { rv = NS_NewLocalFile(aCCLogPath, getter_AddRefs(ccLogFile)); if (NS_FAILED(rv)) { return Err(LogError(rv, "NS_NewLocalFile failed for CC log")); } int64_t fileSize; rv = ccLogFile->GetFileSize(&fileSize); if (NS_FAILED(rv)) { return Err(LogError(rv, "GetFileSize failed for CC log")); } mCCFileSize = totalCCFileSize = fileSize; } if (mHaveGC) { rv = NS_NewLocalFile(aGCLogPath, getter_AddRefs(gcLogFile)); if (NS_FAILED(rv)) { return Err(LogError(rv, "NS_NewLocalFile failed for GC log")); } int64_t fileSize; rv = gcLogFile->GetFileSize(&fileSize); if (NS_FAILED(rv)) { return Err(LogError(rv, "GetFileSize failed for GC log")); } mGCFileSize = totalGCFileSize = fileSize; } int64_t maxChunkSize = 128 * 1024 * 1024; size_t bufferSize = size_t( std::min(maxChunkSize, std::max(totalCCFileSize, totalGCFileSize))); auto buffer = MakeUnique(bufferSize); if (mHaveCC) { RefPtr ccStream = new nsFileRandomAccessStream(); rv = ccStream->Init(ccLogFile, PR_RDONLY, 0, 0); if (NS_FAILED(rv)) { return Err(LogError(rv, "Failed to open CC log file at `%s`", ccLogFile->HumanReadablePath().get())); } int64_t offset = 0; while (offset < totalCCFileSize) { rv = ccStream->Seek(PR_SEEK_SET, offset); if (NS_FAILED(rv)) { return Err(LogError(rv, "Failed to seek in CC log file")); } uint32_t toRead = uint32_t(std::min(int64_t(bufferSize - 1), totalCCFileSize - offset)); uint32_t bytesRead = 0; rv = ccStream->Read(buffer.get(), toRead, &bytesRead); if (NS_FAILED(rv)) { return Err(LogError(rv, "Failed to read CC log file")); } if (bytesRead == 0) { return Err( LogError(NS_ERROR_FILE_CORRUPTED, "Unexpected end of CC file")); } buffer.get()[bytesRead] = '\0'; bool containsFileEnd = offset + int64_t(bytesRead) == totalCCFileSize; size_t processed = MOZ_TRY(IngestCycleCollectorLog( nsDependentCString(buffer.get(), bytesRead), containsFileEnd)); if (processed == 0) { return Err( LogError(NS_ERROR_FILE_CORRUPTED, "Failed to parse CC file")); } offset += int64_t(processed); } } if (mHaveGC) { RefPtr gcStream = new nsFileRandomAccessStream(); rv = gcStream->Init(gcLogFile, PR_RDONLY, 0, 0); if (NS_FAILED(rv)) { return Err(LogError(rv, "Failed to open GC log file at `%s`", gcLogFile->HumanReadablePath().get())); } int64_t offset = 0; while (offset < totalGCFileSize) { rv = gcStream->Seek(PR_SEEK_SET, offset); if (NS_FAILED(rv)) { return Err(LogError(rv, "Failed to seek in GC log file")); } uint32_t toRead = uint32_t(std::min(int64_t(bufferSize - 1), totalGCFileSize - offset)); uint32_t bytesRead = 0; rv = gcStream->Read(buffer.get(), toRead, &bytesRead); if (NS_FAILED(rv)) { return Err(LogError(rv, "Failed to read GC log file")); } if (bytesRead == 0) { return Err( LogError(NS_ERROR_FILE_CORRUPTED, "Unexpected end of GC file")); } buffer.get()[bytesRead] = '\0'; bool containsFileEnd = offset + int64_t(bytesRead) == totalGCFileSize; size_t processed = MOZ_TRY(IngestGarbageCollectorLog( nsDependentCString(buffer.get(), bytesRead), containsFileEnd)); if (processed == 0) { return Err( LogError(NS_ERROR_FILE_CORRUPTED, "Failed to parse GC file")); } offset += int64_t(processed); } } return FinishInitialization(); } Result CollectorLogAnalyzerBackground::FinishInitialization() { // Free ingestion-only lookup tables now that parsing is complete. mStringTable.clearAndCompact(); mNodeIdsToIndices.clearAndCompact(); mCCReferenceCounts.compact(); // Shrink all data vectors to their final sizes. mNodeIds.shrinkStorageToFit(); mNodeLabels.shrinkStorageToFit(); mNodeFlags.shrinkStorageToFit(); mNodeEdges.shrinkStorageToFit(); mObservedReferenceCounts.shrinkStorageToFit(); mStrings.shrinkStorageToFit(); mGCRoots.shrinkStorageToFit(); mGCGrayRoots.shrinkStorageToFit(); mIncrementalRoots.shrinkStorageToFit(); // Sort weak map edges by source and record each source's first entry index. // Ensuring they are contiguous for each source will let us efficiently search // through weak map edges when traversing the graph. std::sort(mWeakMapEdges.begin(), mWeakMapEdges.end(), [](const WeakMapEdge& a, const WeakMapEdge& b) { return a.source() < b.source(); }); for (size_t i = 0; i < mWeakMapEdges.length(); ++i) { if (i == 0 || mWeakMapEdges[i].source() != mWeakMapEdges[i - 1].source()) { MOZ_ASSERT(mNodeEdges[mWeakMapEdges[i].source()].mWeakMap == INVALID_NODE); MOZ_ASSERT(mNodeEdges[mWeakMapEdges[i].source()].mWeakMapCount > 0); mNodeEdges[mWeakMapEdges[i].source()].mWeakMap = i; } } mQuerySize = mNodeEdges.length(); for (NodeTableIndex current = 0; current < mNodeEdges.length(); ++current) { mQueryProgress++; HashSet ccSeen; const NodeEdgesDescriptor& edges = mNodeEdges[current]; for (size_t i = 0; i < edges.mCCCount; i++) { NodeTableIndex edge = mEdges[edges.mCC + i]; if (ccSeen.has(edge)) { continue; } MOZ_TRY(PutInSet(ccSeen, edge)); mObservedReferenceCounts[edge]++; } for (size_t i = 0; i < edges.mGCCount; i++) { NodeTableIndex edge = mEdges[edges.mGC + i]; if (ccSeen.has(edge)) { continue; } mObservedReferenceCounts[edge]++; } } // Classify CC-managed roots as hard or soft based on observed vs declared RC. for (NodeTableIndex current = 0; current < mNodeFlags.length(); ++current) { if (mNodeFlags[current] & dom::CollectorNodeFlags_Binding::CC_MANAGED && mNodeFlags[current] & dom::CollectorNodeFlags_Binding::ROOT) { auto rcPtr = mCCReferenceCounts.lookup(current); uint32_t declaredRefCount = rcPtr ? rcPtr->value() : 0; uint32_t observedRefCount = mObservedReferenceCounts[current]; if (observedRefCount >= declaredRefCount) { mNodeFlags[current] &= ~dom::CollectorNodeFlags_Binding::ROOT; mNodeFlags[current] |= dom::CollectorNodeFlags_Binding::SOFT_ROOT; MOZ_TRY(AppendToVector(mCCSoftRoots, current)); } else { MOZ_TRY(AppendToVector(mCCRoots, current)); } } } mCCRoots.shrinkStorageToFit(); mCCSoftRoots.shrinkStorageToFit(); mInitialized = true; return Ok(); } struct PathNode { NodeTableIndex mFrom; NodeTableIndex mWeakMap; uint64_t mDistance; WeakMapEdgeKind mKind; }; // BFS from roots to aNodeIndex, returning the shortest path. // When aOnlyUseSoftRoots is false, searches from hard roots (GC black roots and // CC roots with unexplained references) first. If no path is found, retries // with aOnlyUseSoftRoots = true. When aOnlyUseSoftRoots is true, searches from // soft roots (GC gray roots and CC roots whose references are fully accounted // for) and returns None if no path is found. Result CollectorLogAnalyzerBackground::GetPathToRootInner(NodeTableIndex aNodeIndex, bool aOnlyUseSoftRoots) { MOZ_TRY(EnsureInitialized()); dom::CollectorLogRootPath result; HashMap paths; std::queue worklist; if (aOnlyUseSoftRoots) { for (NodeTableIndex root : mGCGrayRoots) { PathNode node = {INVALID_NODE, 0, 0, WeakMapEdgeKind::None}; MOZ_TRY(PutInMap(paths, root, node)); worklist.push(root); } for (NodeTableIndex root : mCCSoftRoots) { PathNode node = {INVALID_NODE, 0, 0, WeakMapEdgeKind::None}; MOZ_TRY(PutInMap(paths, root, node)); worklist.push(root); } } else { for (NodeTableIndex root : mGCRoots) { PathNode node = {INVALID_NODE, 0, 0, WeakMapEdgeKind::None}; MOZ_TRY(PutInMap(paths, root, node)); worklist.push(root); } for (NodeTableIndex root : mCCRoots) { PathNode node = {INVALID_NODE, 0, 0, WeakMapEdgeKind::None}; MOZ_TRY(PutInMap(paths, root, node)); worklist.push(root); } } // Incremental roots are objects touched during an incremental CC that must // be treated as live; include them regardless of soft-root mode. for (NodeTableIndex root : mIncrementalRoots) { auto p = paths.lookupForAdd(root); if (!p) { PathNode node = {INVALID_NODE, 0, 0, WeakMapEdgeKind::None}; MOZ_TRY(AddToMap(paths, p, root, node)); worklist.push(root); } } bool found = false; while (!worklist.empty()) { mQueryProgress++; mQuerySize = mQueryProgress + worklist.size(); NodeTableIndex current = worklist.front(); worklist.pop(); auto pathLookup = paths.lookup(current); if (!pathLookup) { continue; } const PathNode& path = pathLookup->value(); uint64_t distance = path.mDistance; uint64_t nextDistance = distance + 1; if (current == aNodeIndex) { found = true; break; } const NodeEdgesDescriptor& edges = mNodeEdges[current]; for (size_t i = 0; i < edges.mCCCount; i++) { NodeTableIndex edge = mEdges[edges.mCC + i]; auto p = paths.lookupForAdd(edge); if (!p) { PathNode edgeNode = {current, 0, nextDistance, WeakMapEdgeKind::None}; MOZ_TRY(AddToMap(paths, p, edge, edgeNode)); worklist.push(edge); } } for (size_t i = 0; i < edges.mGCCount; i++) { NodeTableIndex edge = mEdges[edges.mGC + i]; auto p = paths.lookupForAdd(edge); if (!p) { PathNode edgeNode = {current, 0, nextDistance, WeakMapEdgeKind::None}; MOZ_TRY(AddToMap(paths, p, edge, edgeNode)); worklist.push(edge); } } for (size_t i = 0; i < edges.mWeakMapCount; i++) { WeakMapEdge edge = mWeakMapEdges[edges.mWeakMap + i]; MOZ_ASSERT(edge.source() == current); if (!paths.has(edge.other())) { continue; } auto p = paths.lookupForAdd(edge.mValue); if (!p) { PathNode edgeNode = {edge.mKey, edge.mMap, nextDistance, edge.mKind}; MOZ_TRY(AddToMap(paths, p, edge.mValue, edgeNode)); worklist.push(edge.mValue); } } } if (aOnlyUseSoftRoots) { if (found) { result.mKind = dom::CollectorLogRootKind::Soft; } else { result.mKind = dom::CollectorLogRootKind::None; } } else { if (found) { result.mKind = dom::CollectorLogRootKind::Hard; } else { return GetPathToRootInner(aNodeIndex, /* aOnlyUseSoftRoots = */ true); } } std::queue resultWorklist; std::queue weakMapKeysToProcess; resultWorklist.push(aNodeIndex); HashSet processed; MOZ_TRY(PutInSet(processed, aNodeIndex)); bool firstPath = true; while (!resultWorklist.empty()) { NodeTableIndex current = resultWorklist.front(); resultWorklist.pop(); nsTArray path; HashMap::Ptr pathLookup; while ((pathLookup = paths.lookup(current))) { const PathNode& pathNode = pathLookup->value(); dom::CollectorLogEdge edge; edge.mOther = MakeResultNode(current); if (pathNode.mFrom != INVALID_NODE) { const NodeEdgesDescriptor& edges = mNodeEdges[pathNode.mFrom]; int64_t edgeIndex = -1; for (size_t i = 0; i < edges.mCCCount; i++) { if (mEdges[edges.mCC + i] == current) { edgeIndex = int64_t(edges.mCC) + int64_t(i); break; } } if (edgeIndex == -1) { for (size_t i = 0; i < edges.mGCCount; i++) { if (mEdges[edges.mGC + i] == current) { edgeIndex = int64_t(edges.mGC) + int64_t(i); break; } } } if (edgeIndex != -1) { edge.mLabel = nsCString(&mStrings[mEdgeLabels[edgeIndex]]); } } path.AppendElement(edge); current = pathNode.mFrom; if (pathNode.mWeakMap && !processed.has(pathNode.mWeakMap)) { MOZ_TRY(PutInSet(processed, pathNode.mWeakMap)); resultWorklist.push(pathNode.mWeakMap); weakMapKeysToProcess.push(pathNode.mFrom); } } std::reverse(path.begin(), path.end()); if (firstPath) { result.mPath = std::move(path); firstPath = false; } else { dom::CollectorLogWeakMapPath wmPath; NodeTableIndex key = weakMapKeysToProcess.front(); weakMapKeysToProcess.pop(); wmPath.mKey = MakeResultNode(key); wmPath.mPath = std::move(path); if (!result.mWeakMapPaths.AppendElement(std::move(wmPath), mozilla::fallible)) { return Err(LogError(NS_ERROR_OUT_OF_MEMORY, "Out of memory growing weak map paths")); } } } return result; } Result CollectorLogAnalyzerBackground::GetNodeAdjacentsImpl( NodeTableIndex aNodeIndex) { MOZ_TRY(EnsureInitialized()); if (aNodeIndex >= mNodeEdges.length()) { return Err(LogError(NS_ERROR_ILLEGAL_INPUT, "Invalid node supplied to getNodeAdjacents")); } mQuerySize = mNodeEdges.length() + mNodeEdges[aNodeIndex].mCCCount + mNodeEdges[aNodeIndex].mGCCount; mQueryProgress = 0; nsTArray toSelf; nsTArray fromSelf; for (NodeTableIndex i = 0; i < mNodeEdges.length(); ++i) { mQueryProgress++; const NodeEdgesDescriptor& edges = mNodeEdges[i]; bool foundCC = false; for (EdgeTableIndex j = 0; j < edges.mCCCount; ++j) { if (mEdges[edges.mCC + j] == aNodeIndex) { dom::CollectorLogEdge edge; edge.mOther = MakeResultNode(i); edge.mLabel = &mStrings[mEdgeLabels[edges.mCC + j]]; toSelf.AppendElement(edge); foundCC = true; } } // The GC log can contain edges to nodes which effectively // are just duplicates of the edges already present in the // CC log. As a best effort attempt to not confuse, we // simply exclude GC edges from A -> B if we already have // a CC-reported edge from A -> B if (!foundCC) { for (EdgeTableIndex j = 0; j < edges.mGCCount; ++j) { if (mEdges[edges.mGC + j] == aNodeIndex) { dom::CollectorLogEdge edge; edge.mOther = MakeResultNode(i); edge.mLabel = &mStrings[mEdgeLabels[edges.mGC + j]]; toSelf.AppendElement(edge); break; } } } if (toSelf.Length() >= MAX_QUERY_RESULTS) { break; } } { HashSet seen; const NodeEdgesDescriptor& edges = mNodeEdges[aNodeIndex]; for (NodeTableIndex i = 0; i < edges.mCCCount; i++) { if (fromSelf.Length() >= MAX_QUERY_RESULTS) { break; } mQueryProgress++; dom::CollectorLogEdge edge; edge.mOther = MakeResultNode(mEdges[edges.mCC + i]); edge.mLabel = &mStrings[mEdgeLabels[edges.mCC + i]]; fromSelf.AppendElement(edge); MOZ_TRY(PutInSet(seen, mEdges[edges.mCC + i])); } for (NodeTableIndex i = 0; i < edges.mGCCount; i++) { if (fromSelf.Length() >= MAX_QUERY_RESULTS) { break; } mQueryProgress++; if (!seen.has(mEdges[edges.mGC + i])) { dom::CollectorLogEdge edge; edge.mOther = MakeResultNode(mEdges[edges.mGC + i]); edge.mLabel = &mStrings[mEdgeLabels[edges.mGC + i]]; fromSelf.AppendElement(edge); } } } dom::CollectorLogNodeAdjacents result; result.mToSelf = std::move(toSelf); result.mFromSelf = std::move(fromSelf); return result; } already_AddRefed CollectorLogAnalyzer::Init(ErrorResult& aRv) { return DispatchToBackground( aRv, [background = mBackground, ccLogPath = nsString(mCCLogPath), gcLogPath = nsString(mGCLogPath)]() { return background->InitImpl(ccLogPath, gcLogPath); }); } double CollectorLogAnalyzer::GetInitProgress() { return mBackground->GetInitProgress(); } already_AddRefed CollectorLogAnalyzer::QueryNodes( const nsACString& aQuery, ErrorResult& aRv) { return DispatchToBackground>( aRv, [background = mBackground, query = nsCString(aQuery)]() { return background->QueryNodesImpl(query); }); } already_AddRefed CollectorLogAnalyzer::GetPathToRoot( const dom::CollectorLogNode& aNode, ErrorResult& aRv) { return DispatchToBackground( aRv, [background = mBackground, nodeIndex = aNode.mIndex]() { return background->GetPathToRootImpl(nodeIndex); }); } already_AddRefed CollectorLogAnalyzer::GetNodeAdjacents( const dom::CollectorLogNode& aNode, ErrorResult& aRv) { return DispatchToBackground( aRv, [background = mBackground, nodeIndex = aNode.mIndex]() { return background->GetNodeAdjacentsImpl(nodeIndex); }); } double CollectorLogAnalyzer::GetQueryProgress() { return mBackground->GetQueryProgress(); } already_AddRefed CollectorLogAnalyzer::SampleNodes( ErrorResult& aRv) { return DispatchToBackground>( aRv, [background = mBackground]() { return background->SampleNodesImpl(); }); } /* static */ already_AddRefed CollectorLogAnalyzer::Constructor( dom::GlobalObject& aGlobal, const nsAString& aCCLogPath, const nsAString& aGCLogPath) { nsCOMPtr globalSupports = do_QueryInterface(aGlobal.GetAsSupports()); RefPtr analyzer = new CollectorLogAnalyzer(globalSupports, aCCLogPath, aGCLogPath); return analyzer.forget(); } } // namespace mozilla