Source code
Revision control
Copy as Markdown
Other Tools
/* 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
#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<TestNode> mNodes;
nsTArray<TestEdge> mEdges;
nsTArray<size_t> mCCRootIndices;
nsTArray<size_t> mCCSoftRootIndices;
nsTArray<size_t> mGCRootIndices;
nsTArray<size_t> mGCGrayRootIndices;
// To hold the string references alive
RefPtr<CollectorLogAnalyzerBackground> mBackground;
};
} // namespace CollectorLogAnalyzerTestUtils
class CollectorLogAnalyzerTestHelper {
public:
static RefPtr<CollectorLogAnalyzerBackground> InitBackground(
bool aHaveCC, const nsACString& aCCBuf, bool aHaveGC,
const nsACString& aGCBuf, nsCString* aOutError = nullptr) {
auto bg = MakeRefPtr<CollectorLogAnalyzerBackground>();
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<nsTArray<dom::CollectorLogNode>, LogError> QueryNodes(
CollectorLogAnalyzerBackground* aBg, const nsCString& aQuery) {
return aBg->QueryNodesImpl(aQuery);
}
static Result<nsTArray<dom::CollectorLogNode>, LogError> SampleNodes(
CollectorLogAnalyzerBackground* aBg) {
return aBg->SampleNodesImpl();
}
static Result<dom::CollectorLogNodeAdjacents, LogError> GetNodeAdjacents(
CollectorLogAnalyzerBackground* aBg, NodeTableIndex aIndex) {
return aBg->GetNodeAdjacentsImpl(aIndex);
}
static Result<dom::CollectorLogRootPath, LogError> 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<CollectorLogAnalyzerBackground>();
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<CollectorLogAnalyzerBackground> InitCCAnalyzer(
const nsACString& aLog) {
return CollectorLogAnalyzerTestHelper::InitBackground(
/* aHaveCC */ true, aLog, /* aHaveGC */ false, EmptyCString());
}
static RefPtr<CollectorLogAnalyzerBackground> InitBothAnalyzer(
const nsACString& aCCLog, const nsACString& aGCLog) {
return CollectorLogAnalyzerTestHelper::InitBackground(
/* aHaveCC */ true, aCCLog, /* aHaveGC */ true, aGCLog);
}
static const mozilla::dom::CollectorLogNode* FindResultNodeByLabel(
const nsTArray<mozilla::dom::CollectorLogNode>& 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);
}