/* 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 "gtest/gtest.h" #include "mozilla/CollectorLogAnalyzer.h" #include "mozilla/CollectorLogAnalyzerBackground.h" #include "mozilla/RefPtr.h" namespace mozilla { namespace CollectorLogAnalyzerTestUtils { // Mirrors CollectorNodeFlags from CollectorLogAnalyzer.webidl static constexpr uint8_t FLAG_GARBAGE = 0x01; static constexpr uint8_t FLAG_INCREMENTAL_ROOT = 0x02; static constexpr uint8_t FLAG_ROOT = 0x04; static constexpr uint8_t FLAG_SOFT_ROOT = 0x08; static constexpr uint8_t FLAG_CC_MANAGED = 0x10; static constexpr uint8_t FLAG_GC_MARKED = 0x20; static constexpr uint8_t FLAG_GC_GRAY = 0x40; struct TestNode { uint64_t mPtr; nsDependentCString mLabel; uint8_t mFlags; int32_t mReferenceCount; // -1 if not CC-managed size_t mIndex; }; struct TestEdge { size_t mFromIndex; size_t mToIndex; nsDependentCString mLabel; }; struct ParseResult { bool mSuccess = false; nsCString mError; nsTArray mNodes; nsTArray mEdges; nsTArray mCCRootIndices; nsTArray mCCSoftRootIndices; nsTArray mGCRootIndices; nsTArray mGCGrayRootIndices; // To hold the string references alive RefPtr mBackground; }; } // namespace CollectorLogAnalyzerTestUtils class CollectorLogAnalyzerTestHelper { public: static RefPtr InitBackground( bool aHaveCC, const nsACString& aCCBuf, bool aHaveGC, const nsACString& aGCBuf, nsCString* aOutError = nullptr) { auto bg = MakeRefPtr(); bg->mHaveCC = aHaveCC; bg->mHaveGC = aHaveGC; if (aHaveCC) { bg->mCCFileSize = aCCBuf.Length(); auto rv = bg->IngestCycleCollectorLog(aCCBuf, /* aContainsFileEnd */ true); if (rv.isErr()) { if (aOutError) *aOutError = rv.unwrapErr().Message(); return nullptr; } } if (aHaveGC) { bg->mGCFileSize = aGCBuf.Length(); auto rv = bg->IngestGarbageCollectorLog(aGCBuf, /* aContainsFileEnd */ true); if (rv.isErr()) { if (aOutError) *aOutError = rv.unwrapErr().Message(); return nullptr; } } auto rv = bg->FinishInitialization(); if (rv.isErr()) { if (aOutError) *aOutError = rv.unwrapErr().Message(); return nullptr; } return bg; } static Result, LogError> QueryNodes( CollectorLogAnalyzerBackground* aBg, const nsCString& aQuery) { return aBg->QueryNodesImpl(aQuery); } static Result, LogError> SampleNodes( CollectorLogAnalyzerBackground* aBg) { return aBg->SampleNodesImpl(); } static Result GetNodeAdjacents( CollectorLogAnalyzerBackground* aBg, NodeTableIndex aIndex) { return aBg->GetNodeAdjacentsImpl(aIndex); } static Result GetPathToRoot( CollectorLogAnalyzerBackground* aBg, NodeTableIndex aIndex) { return aBg->GetPathToRootImpl(aIndex); } static CollectorLogAnalyzerTestUtils::ParseResult ParseFromBuffers( bool aHaveCC, const nsACString& aCCBuf, bool aHaveGC, const nsACString& aGCBuf) { CollectorLogAnalyzerTestUtils::ParseResult result; auto bg = MakeRefPtr(); result.mBackground = bg; bg->mHaveCC = aHaveCC; bg->mHaveGC = aHaveGC; if (aHaveCC) { bg->mCCFileSize = aCCBuf.Length(); auto rv = bg->IngestCycleCollectorLog(aCCBuf, /* aContainsFileEnd */ true); if (rv.isErr()) { result.mError = rv.unwrapErr().Message(); return result; } } if (aHaveGC) { bg->mGCFileSize = aGCBuf.Length(); auto rv = bg->IngestGarbageCollectorLog(aGCBuf, /* aContainsFileEnd */ true); if (rv.isErr()) { result.mError = rv.unwrapErr().Message(); return result; } } auto rv = bg->FinishInitialization(); if (rv.isErr()) { result.mError = rv.unwrapErr().Message(); return result; } for (NodeTableIndex i = 0; i < bg->mNodeLabels.length(); ++i) { CollectorLogAnalyzerTestUtils::TestNode node; node.mPtr = bg->mNodeIds[i]; node.mFlags = bg->mNodeFlags[i]; auto rcPtr = bg->mCCReferenceCounts.lookup(i); node.mReferenceCount = rcPtr ? int32_t(rcPtr->value()) : -1; node.mIndex = i; if (bg->mNodeLabels[i] != INVALID_STRING) { node.mLabel.Rebind(&bg->mStrings[bg->mNodeLabels[i]]); } result.mNodes.AppendElement(std::move(node)); } for (NodeTableIndex i = 0; i < bg->mNodeEdges.length(); ++i) { const NodeEdgesDescriptor& edges = bg->mNodeEdges[i]; for (size_t j = 0; j < edges.mCCCount; j++) { CollectorLogAnalyzerTestUtils::TestEdge edge; edge.mFromIndex = i; edge.mToIndex = bg->mEdges[edges.mCC + j]; edge.mLabel.Rebind(&bg->mStrings[bg->mEdgeLabels[edges.mCC + j]]); result.mEdges.AppendElement(std::move(edge)); } for (size_t j = 0; j < edges.mGCCount; j++) { CollectorLogAnalyzerTestUtils::TestEdge edge; edge.mFromIndex = i; edge.mToIndex = bg->mEdges[edges.mGC + j]; edge.mLabel.Rebind(&bg->mStrings[bg->mEdgeLabels[edges.mGC + j]]); result.mEdges.AppendElement(std::move(edge)); } } for (NodeTableIndex idx : bg->mCCRoots) { result.mCCRootIndices.AppendElement(idx); } for (NodeTableIndex idx : bg->mCCSoftRoots) { result.mCCSoftRootIndices.AppendElement(idx); } for (NodeTableIndex idx : bg->mGCRoots) { result.mGCRootIndices.AppendElement(idx); } for (NodeTableIndex idx : bg->mGCGrayRoots) { result.mGCGrayRootIndices.AppendElement(idx); } result.mSuccess = true; return result; } }; namespace CollectorLogAnalyzerTestUtils { ParseResult ParseCCLog(const nsACString& aLog) { return CollectorLogAnalyzerTestHelper::ParseFromBuffers( /* aHaveCC */ true, aLog, /* aHaveGC */ false, EmptyCString()); } ParseResult ParseGCLog(const nsACString& aLog) { return CollectorLogAnalyzerTestHelper::ParseFromBuffers( /* aHaveCC */ false, EmptyCString(), /* aHaveGC */ true, aLog); } ParseResult ParseBothLogs(const nsACString& aCCLog, const nsACString& aGCLog) { return CollectorLogAnalyzerTestHelper::ParseFromBuffers( /* aHaveCC */ true, aCCLog, /* aHaveGC */ true, aGCLog); } } // namespace CollectorLogAnalyzerTestUtils } // namespace mozilla using namespace mozilla::CollectorLogAnalyzerTestUtils; using mozilla::CollectorLogAnalyzerBackground; using mozilla::CollectorLogAnalyzerTestHelper; using namespace mozilla::dom; static RefPtr InitCCAnalyzer( const nsACString& aLog) { return CollectorLogAnalyzerTestHelper::InitBackground( /* aHaveCC */ true, aLog, /* aHaveGC */ false, EmptyCString()); } static RefPtr InitBothAnalyzer( const nsACString& aCCLog, const nsACString& aGCLog) { return CollectorLogAnalyzerTestHelper::InitBackground( /* aHaveCC */ true, aCCLog, /* aHaveGC */ true, aGCLog); } static const mozilla::dom::CollectorLogNode* FindResultNodeByLabel( const nsTArray& aNodes, const nsACString& aLabel) { for (const auto& node : aNodes) { if (node.mLabel.Equals(aLabel)) { return &node; } } return nullptr; } static const TestNode* FindNodeByLabel(const ParseResult& aResult, const nsACString& aLabel) { for (const TestNode& node : aResult.mNodes) { if (node.mLabel.Equals(aLabel)) { return &node; } } return nullptr; } static const TestNode* FindNodeByPtr(const ParseResult& aResult, uint64_t aPtr) { for (const TestNode& node : aResult.mNodes) { if (node.mPtr == aPtr) { return &node; } } return nullptr; } static bool HasEdge(const ParseResult& aResult, size_t aFromIndex, size_t aToIndex) { for (const TestEdge& edge : aResult.mEdges) { if (edge.mFromIndex == aFromIndex && edge.mToIndex == aToIndex) { return true; } } return false; } static bool HasEdgeWithLabel(const ParseResult& aResult, size_t aFromIndex, size_t aToIndex, const nsACString& aLabel) { for (const TestEdge& edge : aResult.mEdges) { if (edge.mFromIndex == aFromIndex && edge.mToIndex == aToIndex && edge.mLabel.Equals(aLabel)) { return true; } } return false; } // --- CC log tests --- TEST(CollectorLogLoading, CCEmptyLog) { // A CC log with only the section separator is valid but produces no nodes. auto result = ParseCCLog("==========\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mNodes.Length(), 0u); EXPECT_EQ(result.mEdges.Length(), 0u); } TEST(CollectorLogLoading, CCCommentLinesSkipped) { auto result = ParseCCLog( "# This is a comment\n" "# Another comment\n" "==========\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mNodes.Length(), 0u); } TEST(CollectorLogLoading, CCSingleRCNode) { auto result = ParseCCLog( "0x1000 [rc=3] nsDocument\n" "==========\n" "0x1000 [known=2]\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); ASSERT_EQ(result.mNodes.Length(), 1u); const TestNode* node = FindNodeByLabel(result, "nsDocument"_ns); ASSERT_NE(node, nullptr); EXPECT_EQ(node->mPtr, uint64_t(0x1000)); EXPECT_EQ(node->mReferenceCount, 3); EXPECT_TRUE(node->mFlags & FLAG_CC_MANAGED); EXPECT_TRUE(node->mFlags & FLAG_ROOT); } TEST(CollectorLogLoading, CCGarbageNode) { auto result = ParseCCLog( "0x2000 [rc=1] nsWindow\n" "==========\n" "0x2000 [garbage]\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); ASSERT_EQ(result.mNodes.Length(), 1u); const TestNode* node = FindNodeByPtr(result, 0x2000); ASSERT_NE(node, nullptr); EXPECT_TRUE(node->mFlags & FLAG_GARBAGE); EXPECT_FALSE(node->mFlags & FLAG_ROOT); } TEST(CollectorLogLoading, CCNodeWithEdges) { auto result = ParseCCLog( "0x1000 [rc=2] nsDocument\n" "> 0xDEADBEEF mWindow\n" "> 0xDEADBEEF01010101 mChild\n" "0xDEADBEEF [rc=1] nsWindow\n" "0xDEADBEEF01010101 [rc=1] nsElement\n" "==========\n" "0x1000 [known=1]\n" "0xDEADBEEF [known=1]\n" "0xDEADBEEF01010101 [known=1]\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mNodes.Length(), 3u); EXPECT_EQ(result.mEdges.Length(), 2u); const TestNode* doc = FindNodeByLabel(result, "nsDocument"_ns); const TestNode* win = FindNodeByLabel(result, "nsWindow"_ns); const TestNode* elem = FindNodeByLabel(result, "nsElement"_ns); ASSERT_NE(doc, nullptr); ASSERT_NE(win, nullptr); ASSERT_NE(elem, nullptr); EXPECT_TRUE(HasEdgeWithLabel(result, doc->mIndex, win->mIndex, "mWindow"_ns)); EXPECT_TRUE(HasEdgeWithLabel(result, doc->mIndex, elem->mIndex, "mChild"_ns)); } TEST(CollectorLogLoading, CCGCThingMarked) { // GC things appear in CC log as [gc.marked] or [gc] auto result = ParseCCLog( "0x5000 [gc.marked] JSObject\n" "0x6000 [gc] JSFunction\n" "==========\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mNodes.Length(), 2u); const TestNode* obj = FindNodeByLabel(result, "JSObject"_ns); const TestNode* func = FindNodeByLabel(result, "JSFunction"_ns); ASSERT_NE(obj, nullptr); ASSERT_NE(func, nullptr); EXPECT_TRUE(obj->mFlags & FLAG_GC_MARKED); EXPECT_TRUE(func->mFlags & FLAG_GC_MARKED); EXPECT_TRUE(func->mFlags & FLAG_GC_GRAY); EXPECT_FALSE(obj->mFlags & FLAG_CC_MANAGED); } TEST(CollectorLogLoading, CCIncrementalRoot) { auto result = ParseCCLog( "IncrementalRoot 0xABCD\n" "==========\n"_ns); EXPECT_TRUE(result.mSuccess); ASSERT_EQ(result.mNodes.Length(), 1u); EXPECT_TRUE(result.mNodes[0].mFlags & FLAG_INCREMENTAL_ROOT); } TEST(CollectorLogLoading, CCWeakEdgeSkipped) { // Weak edges should not appear in the edge list auto result = ParseCCLog( "0x1000 [rc=1] nsDocument\n" "> 0x2000 [weak] mWeakRef\n" "0x2000 [rc=1] nsWindow\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [known=1]\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mEdges.Length(), 0u); } TEST(CollectorLogLoading, CCWeakMapEntry) { auto result = ParseCCLog( "WeakMapEntry map=0x1000 key=0x2000 keyDelegate=(nil) value=0x3000\n" "==========\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); // WeakMapEntry creates nodes for all non-nil addresses EXPECT_GE(result.mNodes.Length(), 3u); } TEST(CollectorLogLoading, SoftRootFromGCEdge) { // Soft roots arise when a GC edge provides the reference that makes observed // refcount match declared refcount. In CC-only mode, CC nodes with a known // entry always have rc > CC-internal edges, so soft roots require a GC log. // // nsDocument: rc=2, 1 CC edge from nsWindow, 1 GC edge from JSObject. // Observed = 2, declared = 2 -> soft root. auto result = ParseBothLogs( "0x1000 [rc=2] nsDocument\n" "0x2000 [rc=1] nsWindow\n" "> 0x1000 mDoc\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [known=1]\n"_ns, "==========\n" "0x5000 B JSObject\n" "> 0x1000 B mWrapped\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); const TestNode* doc = FindNodeByLabel(result, "nsDocument"_ns); ASSERT_NE(doc, nullptr); EXPECT_TRUE(doc->mFlags & FLAG_SOFT_ROOT); EXPECT_FALSE(doc->mFlags & FLAG_ROOT); EXPECT_TRUE(result.mCCSoftRootIndices.Contains(doc->mIndex)); } TEST(CollectorLogLoading, CCHardRoot) { // A node whose observed refcount < declared refcount retains ROOT flag auto result = ParseCCLog( "0x1000 [rc=2] nsDocument\n" "==========\n" "0x1000 [known=1]\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); const TestNode* doc = FindNodeByLabel(result, "nsDocument"_ns); ASSERT_NE(doc, nullptr); // rc=2, no incoming edges observed, so this is a hard root EXPECT_TRUE(doc->mFlags & FLAG_ROOT); EXPECT_FALSE(doc->mFlags & FLAG_SOFT_ROOT); EXPECT_TRUE(result.mCCRootIndices.Contains(doc->mIndex)); } TEST(CollectorLogLoading, CCMalformedLine) { auto result = ParseCCLog("this is not valid CC log content\n"_ns); EXPECT_FALSE(result.mSuccess); EXPECT_FALSE(result.mError.IsEmpty()); } TEST(CollectorLogLoading, CCNilPointer) { // (nil) pointers should parse as address 0 auto result = ParseCCLog( "(nil) [rc=0] NullObject\n" "==========\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); const TestNode* node = FindNodeByLabel(result, "NullObject"_ns); ASSERT_NE(node, nullptr); EXPECT_EQ(node->mPtr, uint64_t(0)); } TEST(CollectorLogLoading, CCDuplicateNodeError) { // The same node address appearing twice in the graph section is an error auto result = ParseCCLog( "0x1000 [rc=1] nsDocument\n" "0x1000 [rc=1] nsDocument\n" "==========\n"_ns); EXPECT_FALSE(result.mSuccess); } TEST(CollectorLogLoading, CCEdgeBeforeNodeError) { auto result = ParseCCLog( "> 0x2000 mWindow\n" "==========\n"_ns); EXPECT_FALSE(result.mSuccess); } TEST(CollectorLogLoading, CCCRLFLineEndings) { auto result = ParseCCLog( "0x1000 [rc=1] nsDocument\r\n" "==========\r\n" "0x1000 [known=1]\r\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mNodes.Length(), 1u); } TEST(CollectorLogLoading, CCStringDeduplication) { // Two nodes with the same label should share the interned string auto result = ParseCCLog( "0x1000 [rc=1] nsDocument\n" "0x2000 [rc=1] nsDocument\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [known=1]\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mNodes.Length(), 2u); EXPECT_EQ(result.mNodes[0].mLabel, "nsDocument"_ns); EXPECT_EQ(result.mNodes[1].mLabel, "nsDocument"_ns); // Both labels should point to the same interned string buffer location. EXPECT_EQ(result.mNodes[0].mLabel.get(), result.mNodes[1].mLabel.get()); } TEST(CollectorLogLoading, CCEdgeLabelDeduplication) { // Two edges with the same label should share the interned string auto result = ParseCCLog( "0x1000 [rc=1] nsDocument\n" "> 0x2000 mChild\n" "0x3000 [rc=1] nsWindow\n" "> 0x4000 mChild\n" "0x2000 [rc=1] nsElement\n" "0x4000 [rc=1] nsFrame\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [known=1]\n" "0x3000 [known=1]\n" "0x4000 [known=1]\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mEdges.Length(), 2u); EXPECT_EQ(result.mEdges[0].mLabel, "mChild"_ns); EXPECT_EQ(result.mEdges[1].mLabel, "mChild"_ns); // Both labels should point to the same interned string buffer location. EXPECT_EQ(result.mEdges[0].mLabel.get(), result.mEdges[1].mLabel.get()); } // --- GC log tests --- TEST(CollectorLogLoading, GCEmptyBlackRoots) { auto result = ParseGCLog("==========\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mNodes.Length(), 0u); } TEST(CollectorLogLoading, GCBlackRoot) { auto result = ParseGCLog( "0xA000 B\n" "==========\n" "0xA000 B JSScript\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); ASSERT_EQ(result.mNodes.Length(), 1u); const TestNode* node = FindNodeByLabel(result, "JSScript"_ns); ASSERT_NE(node, nullptr); EXPECT_EQ(node->mPtr, uint64_t(0xA000)); EXPECT_TRUE((node->mFlags & FLAG_ROOT) != 0); EXPECT_FALSE(node->mFlags & FLAG_SOFT_ROOT); EXPECT_TRUE(result.mGCRootIndices.Contains(node->mIndex)); } TEST(CollectorLogLoading, GCGrayRoot) { // Gray roots in the black roots section trigger a switch to gray root // mode when labeled with one of the known gray root labels. // Roots after the switch are soft roots. auto result = ParseGCLog( "0xA000 B\n" "0xB000 G mAnonymousGlobalScopes[i]\n" "0xC000 G\n" "==========\n" "0xA000 B JSScript\n" "0xB000 G mAnonymousGlobalScopes[i]\n" "0xC000 G JSObject\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); const TestNode* black = FindNodeByLabel(result, "JSScript"_ns); const TestNode* gray1 = FindNodeByLabel(result, "mAnonymousGlobalScopes[i]"_ns); const TestNode* gray2 = FindNodeByLabel(result, "JSObject"_ns); ASSERT_NE(black, nullptr); ASSERT_NE(gray1, nullptr); ASSERT_NE(gray2, nullptr); EXPECT_FALSE(black->mFlags & FLAG_SOFT_ROOT); EXPECT_TRUE(black->mFlags & FLAG_ROOT); EXPECT_TRUE(gray1->mFlags & FLAG_SOFT_ROOT); EXPECT_TRUE(gray2->mFlags & FLAG_SOFT_ROOT); EXPECT_TRUE(result.mGCGrayRootIndices.Contains(gray1->mIndex)); EXPECT_TRUE(result.mGCGrayRootIndices.Contains(gray2->mIndex)); } TEST(CollectorLogLoading, GCGraphSection) { auto result = ParseGCLog( "==========\n" "0xA000 B JSScript\n" "> 0xB000 B mParent\n" "0xB000 G JSObject\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mNodes.Length(), 2u); const TestNode* script = FindNodeByLabel(result, "JSScript"_ns); const TestNode* obj = FindNodeByLabel(result, "JSObject"_ns); ASSERT_NE(script, nullptr); ASSERT_NE(obj, nullptr); EXPECT_TRUE(script->mFlags & FLAG_GC_MARKED); EXPECT_FALSE(script->mFlags & FLAG_GC_GRAY); EXPECT_TRUE(obj->mFlags & FLAG_GC_MARKED); EXPECT_TRUE(obj->mFlags & FLAG_GC_GRAY); EXPECT_TRUE( HasEdgeWithLabel(result, script->mIndex, obj->mIndex, "mParent"_ns)); } TEST(CollectorLogLoading, GCWhiteNode) { auto result = ParseGCLog( "==========\n" "0xA000 W DeadObject\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); const TestNode* node = FindNodeByLabel(result, "DeadObject"_ns); ASSERT_NE(node, nullptr); EXPECT_FALSE(node->mFlags & FLAG_GC_MARKED); EXPECT_FALSE(node->mFlags & FLAG_GC_GRAY); } TEST(CollectorLogLoading, GCWeakEdgeSkipped) { auto result = ParseGCLog( "==========\n" "0xA000 B JSScript\n" "> 0xB000 B [weak] mWeak\n" "0xB000 B JSObject\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mEdges.Length(), 0u); } TEST(CollectorLogLoading, GCWeakMapEntry) { auto result = ParseGCLog( "WeakMapEntry map=0x1000 key=0x2000 keyDelegate=0x3000 value=0x4000\n" "==========\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_GE(result.mNodes.Length(), 4u); } TEST(CollectorLogLoading, GCDuplicateNodeError) { auto result = ParseGCLog( "==========\n" "0xA000 B JSScript\n" "0xA000 B JSScript\n"_ns); EXPECT_FALSE(result.mSuccess); } TEST(CollectorLogLoading, GCEdgeBeforeNodeError) { auto result = ParseGCLog( "==========\n" "> 0xB000 B mParent\n"_ns); EXPECT_FALSE(result.mSuccess); } TEST(CollectorLogLoading, GCMalformedLine) { auto result = ParseGCLog("not valid gc log content\n"_ns); EXPECT_FALSE(result.mSuccess); EXPECT_FALSE(result.mError.IsEmpty()); } TEST(CollectorLogLoading, GCCommentLines) { auto result = ParseGCLog( "# GC log header\n" "# timestamp: 2025-01-01\n" "==========\n" "0xA000 B JSScript\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); EXPECT_EQ(result.mNodes.Length(), 1u); } // --- Combined CC+GC log tests --- TEST(CollectorLogLoading, CombinedCCAndGCLogs) { // GC things in CC log get GC_MARKED flag set from GC log data when both // are loaded together. auto result = ParseBothLogs( "0x1000 [rc=1] nsDocument\n" "> 0x5000 mJSObj\n" "0x5000 [gc.marked] JSObject\n" "==========\n" "0x1000 [known=1]\n"_ns, "==========\n" "0x5000 B JSObject\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); const TestNode* doc = FindNodeByLabel(result, "nsDocument"_ns); const TestNode* obj = FindNodeByLabel(result, "JSObject"_ns); ASSERT_NE(doc, nullptr); ASSERT_NE(obj, nullptr); EXPECT_TRUE(doc->mFlags & FLAG_CC_MANAGED); EXPECT_TRUE(obj->mFlags & FLAG_GC_MARKED); EXPECT_TRUE(HasEdge(result, doc->mIndex, obj->mIndex)); } TEST(CollectorLogLoading, CCDuplicateEdgesToSameTarget) { // A node can have multiple references to the same target (different fields). auto result = ParseCCLog( "0x1000 [rc=2] nsWindow\n" "> 0x2000 mDoc\n" "> 0x2000 mOwner\n" "0x2000 [rc=2] nsDocument\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [known=1]\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); const TestNode* win = FindNodeByLabel(result, "nsWindow"_ns); const TestNode* doc = FindNodeByLabel(result, "nsDocument"_ns); ASSERT_NE(win, nullptr); ASSERT_NE(doc, nullptr); // Both edges should exist EXPECT_TRUE(HasEdgeWithLabel(result, win->mIndex, doc->mIndex, "mDoc"_ns)); EXPECT_TRUE(HasEdgeWithLabel(result, win->mIndex, doc->mIndex, "mOwner"_ns)); } TEST(CollectorLogLoading, CombinedNestingEdgesSkipped) { // When a GC log is present, CC edges with nesting != 0 should be skipped auto result = ParseBothLogs( "0x1000 [rc=1] nsDocument\n" "> 0x2000 [nesting=1] mNested\n" "> 0x3000 mDirect\n" "0x2000 [rc=1] nsElement\n" "0x3000 [rc=1] nsWindow\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [known=1]\n" "0x3000 [known=1]\n"_ns, "==========\n"_ns); EXPECT_TRUE(result.mSuccess) << result.mError.get(); const TestNode* doc = FindNodeByLabel(result, "nsDocument"_ns); const TestNode* nested = FindNodeByLabel(result, "nsElement"_ns); const TestNode* direct = FindNodeByLabel(result, "nsWindow"_ns); ASSERT_NE(doc, nullptr); ASSERT_NE(nested, nullptr); ASSERT_NE(direct, nullptr); EXPECT_FALSE(HasEdge(result, doc->mIndex, nested->mIndex)); EXPECT_TRUE(HasEdge(result, doc->mIndex, direct->mIndex)); } // --- Query tests --- TEST(CollectorLogLoading, QueryNodesByLabelSubstring) { auto bg = InitCCAnalyzer( "0x1000 [rc=1] nsDocument\n" "0x2000 [rc=1] nsDocumentViewer\n" "0x3000 [rc=1] nsWindow\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [known=1]\n" "0x3000 [known=1]\n"_ns); ASSERT_NE(bg, nullptr); auto rv = CollectorLogAnalyzerTestHelper::QueryNodes(bg, "nsDoc"_ns); ASSERT_TRUE(rv.isOk()); auto& nodes = rv.inspect(); EXPECT_EQ(nodes.Length(), 2u); EXPECT_NE(FindResultNodeByLabel(nodes, "nsDocument"_ns), nullptr); EXPECT_NE(FindResultNodeByLabel(nodes, "nsDocumentViewer"_ns), nullptr); } TEST(CollectorLogLoading, QueryNodesByExactPointer) { auto bg = InitCCAnalyzer( "0xABCD [rc=1] nsDocument\n" "0x2000 [rc=1] nsWindow\n" "==========\n" "0xABCD [known=1]\n" "0x2000 [known=1]\n"_ns); ASSERT_NE(bg, nullptr); auto rv = CollectorLogAnalyzerTestHelper::QueryNodes(bg, "0xABCD"_ns); ASSERT_TRUE(rv.isOk()); auto& nodes = rv.inspect(); EXPECT_EQ(nodes.Length(), 1u); EXPECT_EQ(nodes[0].mLabel, "nsDocument"_ns); } TEST(CollectorLogLoading, QueryNodesNoMatch) { auto bg = InitCCAnalyzer( "0x1000 [rc=1] nsDocument\n" "==========\n" "0x1000 [known=1]\n"_ns); ASSERT_NE(bg, nullptr); auto rv = CollectorLogAnalyzerTestHelper::QueryNodes(bg, "NoSuchLabel"_ns); ASSERT_TRUE(rv.isOk()); EXPECT_EQ(rv.inspect().Length(), 0u); } TEST(CollectorLogLoading, QueryNodesGarbageLast) { auto bg = InitCCAnalyzer( "0x1000 [rc=1] nsDocument\n" "0x2000 [rc=1] nsDocumentGarbage\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [garbage]\n"_ns); ASSERT_NE(bg, nullptr); auto rv = CollectorLogAnalyzerTestHelper::QueryNodes(bg, "nsDocument"_ns); ASSERT_TRUE(rv.isOk()); auto& nodes = rv.inspect(); EXPECT_EQ(nodes.Length(), 2u); // Non-garbage node should come first EXPECT_FALSE(nodes[0].mFlags & mozilla::dom::CollectorNodeFlags_Binding::GARBAGE); EXPECT_TRUE(nodes[1].mFlags & mozilla::dom::CollectorNodeFlags_Binding::GARBAGE); } TEST(CollectorLogLoading, SampleNodesReturnsNonGarbage) { // Build a graph with enough nodes and some garbage nsAutoCString log; for (int i = 0; i < 30; i++) { log.AppendPrintf("0x%x [rc=1] Node%d\n", 0x1000 + i * 0x10, i); } log.Append("==========\n"_ns); for (int i = 0; i < 30; i++) { if (i < 5) { log.AppendPrintf("0x%x [garbage]\n", 0x1000 + i * 0x10); } else { log.AppendPrintf("0x%x [known=1]\n", 0x1000 + i * 0x10); } } auto bg = InitCCAnalyzer(log); ASSERT_NE(bg, nullptr); auto rv = CollectorLogAnalyzerTestHelper::SampleNodes(bg); ASSERT_TRUE(rv.isOk()); auto& nodes = rv.inspect(); EXPECT_EQ(nodes.Length(), 20u); for (const auto& node : nodes) { EXPECT_FALSE(node.mFlags & mozilla::dom::CollectorNodeFlags_Binding::GARBAGE); } } // --- Adjacents tests --- TEST(CollectorLogLoading, GetNodeAdjacentsBasic) { auto bg = InitCCAnalyzer( "0x1000 [rc=2] nsDocument\n" "> 0x2000 mWindow\n" "0x2000 [rc=1] nsWindow\n" "> 0x1000 mDoc\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [known=1]\n"_ns); ASSERT_NE(bg, nullptr); // Query adjacents for nsDocument (node index 0) auto rv = CollectorLogAnalyzerTestHelper::GetNodeAdjacents(bg, 0); ASSERT_TRUE(rv.isOk()); auto& adj = rv.inspect(); // nsDocument has one outgoing edge (mWindow -> nsWindow) EXPECT_EQ(adj.mFromSelf.Length(), 1u); EXPECT_EQ(adj.mFromSelf[0].mLabel, "mWindow"_ns); EXPECT_EQ(adj.mFromSelf[0].mOther.mLabel, "nsWindow"_ns); // nsDocument has one incoming edge (nsWindow -> mDoc) EXPECT_EQ(adj.mToSelf.Length(), 1u); EXPECT_EQ(adj.mToSelf[0].mLabel, "mDoc"_ns); EXPECT_EQ(adj.mToSelf[0].mOther.mLabel, "nsWindow"_ns); } TEST(CollectorLogLoading, GetNodeAdjacentsDeduplicatesGCEdges) { // When both CC and GC logs have an edge to the same target, GC edges should // be deduplicated in fromSelf. auto bg = InitBothAnalyzer( "0x1000 [rc=1] nsDocument\n" "> 0x2000 mJSObj\n" "0x2000 [gc.marked] JSObject\n" "==========\n" "0x1000 [known=1]\n"_ns, "==========\n" "0x2000 B JSObject\n" "> 0x1000 B mDoc\n"_ns); ASSERT_NE(bg, nullptr); // Find nsDocument's index auto qrv = CollectorLogAnalyzerTestHelper::QueryNodes(bg, "nsDocument"_ns); ASSERT_TRUE(qrv.isOk()); ASSERT_GE(qrv.inspect().Length(), 1u); uint64_t docIndex = qrv.inspect()[0].mIndex; auto rv = CollectorLogAnalyzerTestHelper::GetNodeAdjacents(bg, docIndex); ASSERT_TRUE(rv.isOk()); auto& adj = rv.inspect(); // nsDocument -> JSObject via CC edge mJSObj (only one, not duplicated) EXPECT_EQ(adj.mFromSelf.Length(), 1u); EXPECT_EQ(adj.mFromSelf[0].mLabel, "mJSObj"_ns); } // --- Path to root tests --- TEST(CollectorLogLoading, PathToRootHard) { // Root -> A -> B, where Root has unexplained references (hard root) auto bg = InitCCAnalyzer( "0x1000 [rc=2] Root\n" "> 0x2000 mChild\n" "0x2000 [rc=1] ChildA\n" "> 0x3000 mChild\n" "0x3000 [rc=1] ChildB\n" "==========\n" "0x1000 [known=1]\n" "0x2000 [known=1]\n" "0x3000 [known=1]\n"_ns); ASSERT_NE(bg, nullptr); // Find ChildB auto qrv = CollectorLogAnalyzerTestHelper::QueryNodes(bg, "ChildB"_ns); ASSERT_TRUE(qrv.isOk()); ASSERT_EQ(qrv.inspect().Length(), 1u); uint64_t childBIndex = qrv.inspect()[0].mIndex; auto rv = CollectorLogAnalyzerTestHelper::GetPathToRoot(bg, childBIndex); ASSERT_TRUE(rv.isOk()); auto& pathResult = rv.inspect(); EXPECT_EQ(pathResult.mKind, mozilla::dom::CollectorLogRootKind::Hard); // Path: Root -> ChildA -> ChildB (3 nodes in the path) ASSERT_EQ(pathResult.mPath.Length(), 3u); EXPECT_EQ(pathResult.mPath[0].mOther.mLabel, "Root"_ns); EXPECT_EQ(pathResult.mPath[1].mOther.mLabel, "ChildA"_ns); EXPECT_EQ(pathResult.mPath[2].mOther.mLabel, "ChildB"_ns); } TEST(CollectorLogLoading, PathToRootSoft) { // SoftRoot has all references accounted for (soft root). // No hard roots exist, so path-to-root falls back to soft roots. auto bg = InitBothAnalyzer( "0x1000 [rc=1] SoftRoot\n" "> 0x2000 mChild\n" "0x2000 [rc=1] Child\n" "==========\n" "0x1000 [known=0]\n"_ns, "0xB000 B\n" "==========\n" "0xB000 B Map\n" "0xA000 B Map\n" "> 0x1000 B mSlot\n"_ns); ASSERT_NE(bg, nullptr); auto qrv = CollectorLogAnalyzerTestHelper::QueryNodes(bg, "Child"_ns); ASSERT_TRUE(qrv.isOk()); ASSERT_EQ(qrv.inspect().Length(), 1u); uint64_t childIndex = qrv.inspect()[0].mIndex; auto rv = CollectorLogAnalyzerTestHelper::GetPathToRoot(bg, childIndex); ASSERT_TRUE(rv.isOk()); EXPECT_EQ(rv.inspect().mKind, mozilla::dom::CollectorLogRootKind::Soft); EXPECT_GE(rv.inspect().mPath.Length(), 2u); } TEST(CollectorLogLoading, PathToRootNone) { // A garbage node with no root path should return kind=None. auto bg = InitCCAnalyzer( "0x1000 [rc=1] Garbage\n" "==========\n" "0x1000 [garbage]\n"_ns); ASSERT_NE(bg, nullptr); auto qrv = CollectorLogAnalyzerTestHelper::QueryNodes(bg, "Garbage"_ns); ASSERT_TRUE(qrv.isOk()); ASSERT_EQ(qrv.inspect().Length(), 1u); uint64_t garbageIndex = qrv.inspect()[0].mIndex; auto rv = CollectorLogAnalyzerTestHelper::GetPathToRoot(bg, garbageIndex); ASSERT_TRUE(rv.isOk()); EXPECT_EQ(rv.inspect().mKind, mozilla::dom::CollectorLogRootKind::None); } TEST(CollectorLogLoading, PathToRootViaWeakMap) { // Test that path-to-root can traverse WeakMap edges. // WeakMap semantics: value is reachable if both map and key are reachable. // // Graph: // GC root 0xA000 (map) --edge--> 0xB000 // GC root 0xC000 (key) // WeakMapEntry map=0xA000 key=0xC000 value=0xD000 // 0xD000 -> 0xE000 (the node we query) // // 0xE000 is reachable: root->0xA000 (map alive), root->0xC000 (key alive), // so weakmap value 0xD000 is alive, then 0xD000->0xE000. auto bg = InitBothAnalyzer( "WeakMapEntry map=0xA000 key=0xC000 keyDelegate=(nil) value=0xD000\n" "==========\n"_ns, "0xA000 B\n" "0xC000 B\n" "==========\n" "0xA000 B Map\n" "> 0xB000 B mSlot\n" "0xB000 B MapChild\n" "0xC000 B Key\n" "0xD000 B Value\n" "> 0xE000 B mTarget\n" "0xE000 B Target\n"_ns); ASSERT_NE(bg, nullptr); auto qrv = CollectorLogAnalyzerTestHelper::QueryNodes(bg, "Target"_ns); ASSERT_TRUE(qrv.isOk()); ASSERT_EQ(qrv.inspect().Length(), 1u); uint64_t targetIndex = qrv.inspect()[0].mIndex; auto rv = CollectorLogAnalyzerTestHelper::GetPathToRoot(bg, targetIndex); ASSERT_TRUE(rv.isOk()); auto& pathResult = rv.inspect(); EXPECT_EQ(pathResult.mKind, mozilla::dom::CollectorLogRootKind::Hard); // The path should exist and reach Target ASSERT_GE(pathResult.mPath.Length(), 1u); EXPECT_EQ(pathResult.mPath.LastElement().mOther.mLabel, "Target"_ns); // There should be a WeakMap path for the key EXPECT_GE(pathResult.mWeakMapPaths.Length(), 1u); }