summaryrefslogtreecommitdiffstats
path: root/devtools/shared/heapsnapshot/tests
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/heapsnapshot/tests')
-rw-r--r--devtools/shared/heapsnapshot/tests/browser/browser.ini8
-rw-r--r--devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js30
-rw-r--r--devtools/shared/heapsnapshot/tests/chrome/chrome.ini8
-rw-r--r--devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html40
-rw-r--r--devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html27
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp95
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp96
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp7
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DevTools.h214
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp67
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp58
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp49
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp34
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp27
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/moz.build32
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs176
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs218
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js54
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js550
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js52
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js49
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js48
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js50
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js55
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js141
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js49
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js119
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js37
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js124
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js150
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js24
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js41
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js21
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js26
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js97
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js62
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js22
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js20
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js56
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js19
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js60
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js110
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js57
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js87
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js28
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js91
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js15
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js62
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js64
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js24
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js26
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js53
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js133
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js50
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js114
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js57
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js87
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js50
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js33
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js87
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js43
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js57
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js33
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js61
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js35
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js43
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js31
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js127
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js124
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js85
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js111
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js74
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js131
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js60
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js22
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js51
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js27
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js41
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js121
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js77
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js139
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js97
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js160
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js150
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js199
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js206
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js143
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js44
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js51
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js75
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js26
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js87
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js64
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js35
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js139
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js111
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js125
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js67
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js105
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js77
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js37
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js87
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js60
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js135
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js9
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.ini100
107 files changed, 8228 insertions, 0 deletions
diff --git a/devtools/shared/heapsnapshot/tests/browser/browser.ini b/devtools/shared/heapsnapshot/tests/browser/browser.ini
new file mode 100644
index 0000000000..f3799a5ef8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/browser/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+
+[browser_saveHeapSnapshot_e10s_01.js]
diff --git a/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js b/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js
new file mode 100644
index 0000000000..1fc25341b8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1201597 - Test to verify that we can take a heap snapshot in an e10s child process.
+ */
+
+"use strict";
+
+add_task(async function () {
+ // Create a minimal browser
+ const browser = document.createXULElement("browser");
+ browser.setAttribute("type", "content");
+ document.body.appendChild(browser);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Save heap snapshot");
+ const result = await SpecialPowers.spawn(browser, [], () => {
+ try {
+ ChromeUtils.saveHeapSnapshot({ runtime: true });
+ } catch (err) {
+ return err.toString();
+ }
+
+ return "";
+ });
+ is(result, "", "result of saveHeapSnapshot");
+
+ browser.remove();
+});
diff --git a/devtools/shared/heapsnapshot/tests/chrome/chrome.ini b/devtools/shared/heapsnapshot/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..497b6fe37e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/chrome/chrome.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools devtools-memory
+skip-if = os == 'android'
+support-files =
+
+[test_DominatorTree_01.html]
+[test_SaveHeapSnapshot.html]
+
diff --git a/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html b/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html
new file mode 100644
index 0000000000..61e60ae209
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Sanity test that we can compute dominator trees from a heap snapshot in a web window.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>ChromeUtils.saveHeapSnapshot test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+/* global window, ChromeUtils, DominatorTree */
+
+SimpleTest.waitForExplicitFinish();
+window.onload = function() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ ok(dominatorTree);
+ ok(DominatorTree.isInstance(dominatorTree));
+
+ let threw = false;
+ try {
+ new DominatorTree();
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "Constructor shouldn't be usable");
+
+ SimpleTest.finish();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html b/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html
new file mode 100644
index 0000000000..53831bfaa2
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1024774 - Sanity test that we can take a heap snapshot in a web window.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>ChromeUtils.saveHeapSnapshot test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+window.onload = function() {
+ ok(ChromeUtils, "The ChromeUtils interface should be exposed in chrome windows.");
+ ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should save a heap snapshot and shouldn't throw.");
+ SimpleTest.finish();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
new file mode 100644
index 0000000000..dc24d13e98
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+// Test that the `JS::ubi::Node`s we create from
+// `mozilla::devtools::DeserializedNode` instances look and behave as we would
+// like.
+
+#include "DevTools.h"
+#include "js/TypeDecls.h"
+#include "mozilla/devtools/DeserializedNode.h"
+
+using testing::Field;
+using testing::ReturnRef;
+
+// A mock DeserializedNode for testing.
+struct MockDeserializedNode : public DeserializedNode {
+ MockDeserializedNode(NodeId id, const char16_t* typeName, uint64_t size)
+ : DeserializedNode(id, typeName, size) {}
+
+ bool addEdge(DeserializedEdge&& edge) {
+ return edges.append(std::move(edge));
+ }
+
+ MOCK_METHOD1(getEdgeReferent, JS::ubi::Node(const DeserializedEdge&));
+};
+
+size_t fakeMallocSizeOf(const void*) {
+ EXPECT_TRUE(false);
+ MOZ_ASSERT_UNREACHABLE(
+ "fakeMallocSizeOf should never be called because "
+ "DeserializedNodes report the deserialized size.");
+ return 0;
+}
+
+DEF_TEST(DeserializedNodeUbiNodes, {
+ const char16_t* typeName = u"TestTypeName";
+ const char* className = "MyObjectClassName";
+ const char* filename = "my-cool-filename.js";
+
+ NodeId id = uint64_t(1) << 33;
+ uint64_t size = uint64_t(1) << 60;
+ MockDeserializedNode mocked(id, typeName, size);
+ mocked.coarseType = JS::ubi::CoarseType::Script;
+ mocked.jsObjectClassName = className;
+ mocked.scriptFilename = filename;
+
+ DeserializedNode& deserialized = mocked;
+ JS::ubi::Node ubi(&deserialized);
+
+ // Test the ubi::Node accessors.
+
+ EXPECT_EQ(size, ubi.size(fakeMallocSizeOf));
+ EXPECT_EQ(typeName, ubi.typeName());
+ EXPECT_EQ(JS::ubi::CoarseType::Script, ubi.coarseType());
+ EXPECT_EQ(id, ubi.identifier());
+ EXPECT_FALSE(ubi.isLive());
+ EXPECT_EQ(ubi.jsObjectClassName(), className);
+ EXPECT_EQ(ubi.scriptFilename(), filename);
+
+ // Test the ubi::Node's edges.
+
+ UniquePtr<DeserializedNode> referent1(
+ new MockDeserializedNode(1, nullptr, 10));
+ DeserializedEdge edge1(referent1->id);
+ mocked.addEdge(std::move(edge1));
+ EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent1->id)))
+ .Times(1)
+ .WillOnce(Return(JS::ubi::Node(referent1.get())));
+
+ UniquePtr<DeserializedNode> referent2(
+ new MockDeserializedNode(2, nullptr, 20));
+ DeserializedEdge edge2(referent2->id);
+ mocked.addEdge(std::move(edge2));
+ EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent2->id)))
+ .Times(1)
+ .WillOnce(Return(JS::ubi::Node(referent2.get())));
+
+ UniquePtr<DeserializedNode> referent3(
+ new MockDeserializedNode(3, nullptr, 30));
+ DeserializedEdge edge3(referent3->id);
+ mocked.addEdge(std::move(edge3));
+ EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent3->id)))
+ .Times(1)
+ .WillOnce(Return(JS::ubi::Node(referent3.get())));
+
+ auto range = ubi.edges(cx);
+ ASSERT_TRUE(!!range);
+
+ for (; !range->empty(); range->popFront()) {
+ // Nothing to do here. This loop ensures that we get each edge referent
+ // that we expect above.
+ }
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp
new file mode 100644
index 0000000000..37e1d426f8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+// Test that the `JS::ubi::StackFrame`s we create from
+// `mozilla::devtools::DeserializedStackFrame` instances look and behave as we
+// would like.
+
+#include "DevTools.h"
+#include "js/SavedFrameAPI.h"
+#include "js/TypeDecls.h"
+#include "mozilla/devtools/DeserializedNode.h"
+
+using testing::Field;
+using testing::ReturnRef;
+
+// A mock DeserializedStackFrame for testing.
+struct MockDeserializedStackFrame : public DeserializedStackFrame {
+ MockDeserializedStackFrame() : DeserializedStackFrame() {}
+};
+
+DEF_TEST(DeserializedStackFrameUbiStackFrames, {
+ StackFrameId id = uint64_t(1) << 42;
+ uint32_t line = 1337;
+ uint32_t column = 9; // 3 space tabs!?
+ const char16_t* source = u"my-javascript-file.js";
+ const char16_t* functionDisplayName = u"myFunctionName";
+
+ MockDeserializedStackFrame mocked;
+ mocked.id = id;
+ mocked.line = line;
+ mocked.column = column;
+ mocked.source = source;
+ mocked.functionDisplayName = functionDisplayName;
+
+ DeserializedStackFrame& deserialized = mocked;
+ JS::ubi::StackFrame ubiFrame(&deserialized);
+
+ // Test the JS::ubi::StackFrame accessors.
+
+ EXPECT_EQ(id, ubiFrame.identifier());
+ EXPECT_EQ(JS::ubi::StackFrame(), ubiFrame.parent());
+ EXPECT_EQ(line, ubiFrame.line());
+ EXPECT_EQ(column, ubiFrame.column());
+ EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(source), ubiFrame.source());
+ EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(functionDisplayName),
+ ubiFrame.functionDisplayName());
+ EXPECT_FALSE(ubiFrame.isSelfHosted(cx));
+ EXPECT_FALSE(ubiFrame.isSystem());
+
+ JS::Rooted<JSObject*> savedFrame(cx);
+ EXPECT_TRUE(ubiFrame.constructSavedFrameStack(cx, &savedFrame));
+
+ JSPrincipals* principals = JS::GetRealmPrincipals(js::GetContextRealm(cx));
+
+ uint32_t frameLine;
+ ASSERT_EQ(JS::SavedFrameResult::Ok,
+ JS::GetSavedFrameLine(cx, principals, savedFrame, &frameLine));
+ EXPECT_EQ(line, frameLine);
+
+ uint32_t frameColumn;
+ ASSERT_EQ(JS::SavedFrameResult::Ok,
+ JS::GetSavedFrameColumn(cx, principals, savedFrame, &frameColumn));
+ EXPECT_EQ(column, frameColumn);
+
+ JS::Rooted<JSObject*> parent(cx);
+ ASSERT_EQ(JS::SavedFrameResult::Ok,
+ JS::GetSavedFrameParent(cx, principals, savedFrame, &parent));
+ EXPECT_EQ(nullptr, parent);
+
+ ASSERT_EQ(NS_strlen(source), 21U);
+ char16_t sourceBuf[21] = {};
+
+ // Test when the length is shorter than the string length.
+ auto written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 3);
+ EXPECT_EQ(written, 3U);
+ for (size_t i = 0; i < 3; i++) {
+ EXPECT_EQ(sourceBuf[i], source[i]);
+ }
+
+ written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 21);
+ EXPECT_EQ(written, 21U);
+ for (size_t i = 0; i < 21; i++) {
+ EXPECT_EQ(sourceBuf[i], source[i]);
+ }
+
+ ASSERT_EQ(NS_strlen(functionDisplayName), 14U);
+ char16_t nameBuf[14] = {};
+
+ written = ubiFrame.functionDisplayName(RangedPtr<char16_t>(nameBuf), 14);
+ EXPECT_EQ(written, 14U);
+ for (size_t i = 0; i < 14; i++) {
+ EXPECT_EQ(nameBuf[i], functionDisplayName[i]);
+ }
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp b/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp
new file mode 100644
index 0000000000..8e89d5ecd7
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp
@@ -0,0 +1,7 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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 "DevTools.h"
+const char16_t JS::ubi::Concrete<FakeNode>::concreteTypeName[] = u"FakeNode";
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DevTools.h b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h
new file mode 100644
index 0000000000..35a16c9182
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+#ifndef mozilla_devtools_gtest_DevTools__
+#define mozilla_devtools_gtest_DevTools__
+
+#include <utility>
+
+#include "CoreDump.pb.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "js/Principals.h"
+#include "js/UbiNode.h"
+#include "js/UniquePtr.h"
+#include "jsapi.h"
+#include "jspubtd.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/devtools/HeapSnapshot.h"
+#include "mozilla/dom/ChromeUtils.h"
+#include "nsCRTGlue.h"
+
+using namespace mozilla;
+using namespace mozilla::devtools;
+using namespace mozilla::dom;
+using namespace testing;
+
+// GTest fixture class that all of our tests derive from.
+struct DevTools : public ::testing::Test {
+ bool _initialized;
+ JSContext* cx;
+ JS::Compartment* compartment;
+ JS::Zone* zone;
+ JS::PersistentRooted<JSObject*> global;
+
+ DevTools() : _initialized(false), cx(nullptr) {}
+
+ virtual void SetUp() {
+ MOZ_ASSERT(!_initialized);
+
+ cx = getContext();
+ if (!cx) return;
+
+ global.init(cx, createGlobal());
+ if (!global) return;
+ JS::EnterRealm(cx, global);
+
+ compartment = js::GetContextCompartment(cx);
+ zone = js::GetContextZone(cx);
+
+ _initialized = true;
+ }
+
+ JSContext* getContext() { return CycleCollectedJSContext::Get()->Context(); }
+
+ static void reportError(JSContext* cx, const char* message,
+ JSErrorReport* report) {
+ fprintf(stderr, "%s:%u:%s\n",
+ report->filename ? report->filename : "<no filename>",
+ (unsigned int)report->lineno, message);
+ }
+
+ static const JSClass* getGlobalClass() {
+ static const JSClass globalClass = {"global", JSCLASS_GLOBAL_FLAGS,
+ &JS::DefaultGlobalClassOps};
+ return &globalClass;
+ }
+
+ JSObject* createGlobal() {
+ /* Create the global object. */
+ JS::RealmOptions options;
+ return JS_NewGlobalObject(cx, getGlobalClass(), nullptr,
+ JS::FireOnNewGlobalHook, options);
+ }
+
+ virtual void TearDown() {
+ _initialized = false;
+
+ if (global) {
+ JS::LeaveRealm(cx, nullptr);
+ global = nullptr;
+ }
+ }
+};
+
+// Helper to define a test and ensure that the fixture is initialized properly.
+#define DEF_TEST(name, body) \
+ TEST_F(DevTools, name) { \
+ ASSERT_TRUE(_initialized); \
+ body \
+ }
+
+// Fake JS::ubi::Node implementation
+class MOZ_STACK_CLASS FakeNode {
+ public:
+ JS::ubi::EdgeVector edges;
+ JS::Compartment* compartment;
+ JS::Zone* zone;
+ size_t size;
+
+ explicit FakeNode() : edges(), compartment(nullptr), zone(nullptr), size(1) {}
+};
+
+namespace JS {
+namespace ubi {
+
+template <>
+class Concrete<FakeNode> : public Base {
+ const char16_t* typeName() const override { return concreteTypeName; }
+
+ js::UniquePtr<EdgeRange> edges(JSContext*, bool) const override {
+ return js::UniquePtr<EdgeRange>(js_new<PreComputedEdgeRange>(get().edges));
+ }
+
+ Size size(mozilla::MallocSizeOf) const override { return get().size; }
+
+ JS::Zone* zone() const override { return get().zone; }
+
+ JS::Compartment* compartment() const override { return get().compartment; }
+
+ protected:
+ explicit Concrete(FakeNode* ptr) : Base(ptr) {}
+ FakeNode& get() const { return *static_cast<FakeNode*>(ptr); }
+
+ public:
+ static const char16_t concreteTypeName[];
+ static void construct(void* storage, FakeNode* ptr) {
+ new (storage) Concrete(ptr);
+ }
+};
+
+} // namespace ubi
+} // namespace JS
+
+inline void AddEdge(FakeNode& node, FakeNode& referent,
+ const char16_t* edgeName = nullptr) {
+ char16_t* ownedEdgeName = nullptr;
+ if (edgeName) {
+ ownedEdgeName = NS_xstrdup(edgeName);
+ }
+
+ JS::ubi::Edge edge(ownedEdgeName, &referent);
+ ASSERT_TRUE(node.edges.append(std::move(edge)));
+}
+
+// Custom GMock Matchers
+
+// Use the testing namespace to avoid static analysis failures in the gmock
+// matcher classes that get generated from MATCHER_P macros.
+namespace testing {
+
+// Ensure that given node has the expected number of edges.
+MATCHER_P2(EdgesLength, cx, expectedLength, "") {
+ auto edges = arg.edges(cx);
+ if (!edges) return false;
+
+ int actualLength = 0;
+ for (; !edges->empty(); edges->popFront()) actualLength++;
+
+ return Matcher<int>(Eq(expectedLength))
+ .MatchAndExplain(actualLength, result_listener);
+}
+
+// Get the nth edge and match it with the given matcher.
+MATCHER_P3(Edge, cx, n, matcher, "") {
+ auto edges = arg.edges(cx);
+ if (!edges) return false;
+
+ int i = 0;
+ for (; !edges->empty(); edges->popFront()) {
+ if (i == n) {
+ return Matcher<const JS::ubi::Edge&>(matcher).MatchAndExplain(
+ edges->front(), result_listener);
+ }
+
+ i++;
+ }
+
+ return false;
+}
+
+// Ensures that two char16_t* strings are equal.
+MATCHER_P(UTF16StrEq, str, "") { return NS_strcmp(arg, str) == 0; }
+
+MATCHER_P(UniqueUTF16StrEq, str, "") { return NS_strcmp(arg.get(), str) == 0; }
+
+MATCHER(UniqueIsNull, "") { return arg.get() == nullptr; }
+
+// Matches an edge whose referent is the node with the given id.
+MATCHER_P(EdgeTo, id, "") {
+ return Matcher<const DeserializedEdge&>(
+ Field(&DeserializedEdge::referent, id))
+ .MatchAndExplain(arg, result_listener);
+}
+
+} // namespace testing
+
+// A mock `Writer` class to be used with testing `WriteHeapGraph`.
+class MockWriter : public CoreDumpWriter {
+ public:
+ virtual ~MockWriter() override {}
+ MOCK_METHOD2(writeNode,
+ bool(const JS::ubi::Node&, CoreDumpWriter::EdgePolicy));
+ MOCK_METHOD1(writeMetadata, bool(uint64_t));
+};
+
+inline void ExpectWriteNode(MockWriter& writer, FakeNode& node) {
+ EXPECT_CALL(writer, writeNode(Eq(JS::ubi::Node(&node)), _))
+ .Times(1)
+ .WillOnce(Return(true));
+}
+
+#endif // mozilla_devtools_gtest_DevTools__
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp
new file mode 100644
index 0000000000..25c387d308
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+// Test that heap snapshots cross compartment boundaries when expected.
+
+#include "DevTools.h"
+
+DEF_TEST(DoesCrossCompartmentBoundaries, {
+ // Create a new global to get a new compartment.
+ JS::RealmOptions options;
+ JS::Rooted<JSObject*> newGlobal(
+ cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr,
+ JS::FireOnNewGlobalHook, options));
+ ASSERT_TRUE(newGlobal);
+ JS::Compartment* newCompartment = nullptr;
+ {
+ JSAutoRealm ar(cx, newGlobal);
+ ASSERT_TRUE(JS::InitRealmStandardClasses(cx));
+ newCompartment = js::GetContextCompartment(cx);
+ }
+ ASSERT_TRUE(newCompartment);
+ ASSERT_NE(newCompartment, compartment);
+
+ // Our set of target compartments is both the old and new compartments.
+ JS::CompartmentSet targetCompartments;
+ ASSERT_TRUE(targetCompartments.put(compartment));
+ ASSERT_TRUE(targetCompartments.put(newCompartment));
+
+ FakeNode nodeA;
+ FakeNode nodeB;
+ FakeNode nodeC;
+ FakeNode nodeD;
+
+ nodeA.compartment = compartment;
+ nodeB.compartment = nullptr;
+ nodeC.compartment = newCompartment;
+ nodeD.compartment = nullptr;
+
+ AddEdge(nodeA, nodeB);
+ AddEdge(nodeA, nodeC);
+ AddEdge(nodeB, nodeD);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should serialize nodeA, because it is in one of our target compartments.
+ ExpectWriteNode(writer, nodeA);
+
+ // Should serialize nodeB, because it doesn't belong to a compartment and is
+ // therefore assumed to be shared.
+ ExpectWriteNode(writer, nodeB);
+
+ // Should also serialize nodeC, which is in our target compartments, but a
+ // different compartment than A.
+ ExpectWriteNode(writer, nodeC);
+
+ // Should serialize nodeD because it's reachable via B and both nodes B and D
+ // don't belong to a specific compartment.
+ ExpectWriteNode(writer, nodeD);
+
+ JS::AutoCheckCannotGC noGC(cx);
+
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&nodeA), writer,
+ /* wantNames = */ false, &targetCompartments,
+ noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp
new file mode 100644
index 0000000000..f6c6b11619
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+// Test that heap snapshots walk the compartment boundaries correctly.
+
+#include "DevTools.h"
+
+DEF_TEST(DoesntCrossCompartmentBoundaries, {
+ // Create a new global to get a new compartment.
+ JS::RealmOptions options;
+ JS::Rooted<JSObject*> newGlobal(
+ cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr,
+ JS::FireOnNewGlobalHook, options));
+ ASSERT_TRUE(newGlobal);
+ JS::Compartment* newCompartment = nullptr;
+ {
+ JSAutoRealm ar(cx, newGlobal);
+ ASSERT_TRUE(JS::InitRealmStandardClasses(cx));
+ newCompartment = js::GetContextCompartment(cx);
+ }
+ ASSERT_TRUE(newCompartment);
+ ASSERT_NE(newCompartment, compartment);
+
+ // Our set of target compartments is only the pre-existing compartment and
+ // does not include the new compartment.
+ JS::CompartmentSet targetCompartments;
+ ASSERT_TRUE(targetCompartments.put(compartment));
+
+ FakeNode nodeA;
+ FakeNode nodeB;
+ FakeNode nodeC;
+
+ nodeA.compartment = compartment;
+ nodeB.compartment = nullptr;
+ nodeC.compartment = newCompartment;
+
+ AddEdge(nodeA, nodeB);
+ AddEdge(nodeB, nodeC);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should serialize nodeA, because it is in our target compartments.
+ ExpectWriteNode(writer, nodeA);
+
+ // Should serialize nodeB, because it doesn't belong to a compartment and is
+ // therefore assumed to be shared.
+ ExpectWriteNode(writer, nodeB);
+
+ // But we shouldn't ever serialize nodeC.
+
+ JS::AutoCheckCannotGC noGC(cx);
+
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&nodeA), writer,
+ /* wantNames = */ false, &targetCompartments,
+ noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp
new file mode 100644
index 0000000000..ab47941e39
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+// Test that edge names get serialized correctly.
+
+#include "DevTools.h"
+
+using testing::Field;
+using testing::IsNull;
+using testing::Property;
+using testing::Return;
+
+DEF_TEST(SerializesEdgeNames, {
+ FakeNode node;
+ FakeNode referent;
+
+ const char16_t edgeName[] = u"edge name";
+ const char16_t emptyStr[] = u"";
+
+ AddEdge(node, referent, edgeName);
+ AddEdge(node, referent, emptyStr);
+ AddEdge(node, referent, nullptr);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should get the node with edges once.
+ EXPECT_CALL(
+ writer,
+ writeNode(
+ AllOf(EdgesLength(cx, 3),
+ Edge(cx, 0,
+ Field(&JS::ubi::Edge::name, UniqueUTF16StrEq(edgeName))),
+ Edge(cx, 1,
+ Field(&JS::ubi::Edge::name, UniqueUTF16StrEq(emptyStr))),
+ Edge(cx, 2, Field(&JS::ubi::Edge::name, UniqueIsNull()))),
+ _))
+ .Times(1)
+ .WillOnce(Return(true));
+
+ // Should get the referent node that doesn't have any edges once.
+ ExpectWriteNode(writer, referent);
+
+ JS::AutoCheckCannotGC noGC(cx);
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&node), writer,
+ /* wantNames = */ true,
+ /* zones = */ nullptr, noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp
new file mode 100644
index 0000000000..d71c86703c
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+// Test that everything in the heap graph gets serialized once, and only once.
+
+#include "DevTools.h"
+
+DEF_TEST(SerializesEverythingInHeapGraphOnce, {
+ FakeNode nodeA;
+ FakeNode nodeB;
+ FakeNode nodeC;
+ FakeNode nodeD;
+
+ AddEdge(nodeA, nodeB);
+ AddEdge(nodeB, nodeC);
+ AddEdge(nodeC, nodeD);
+ AddEdge(nodeD, nodeA);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should serialize each node once.
+ ExpectWriteNode(writer, nodeA);
+ ExpectWriteNode(writer, nodeB);
+ ExpectWriteNode(writer, nodeC);
+ ExpectWriteNode(writer, nodeD);
+
+ JS::AutoCheckCannotGC noGC(cx);
+
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&nodeA), writer,
+ /* wantNames = */ false,
+ /* zones = */ nullptr, noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp
new file mode 100644
index 0000000000..4c29b28832
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+// Test that a ubi::Node's typeName gets properly serialized into a core dump.
+
+#include "DevTools.h"
+
+using testing::Property;
+using testing::Return;
+
+DEF_TEST(SerializesTypeNames, {
+ FakeNode node;
+
+ ::testing::NiceMock<MockWriter> writer;
+ EXPECT_CALL(
+ writer,
+ writeNode(Property(&JS::ubi::Node::typeName, UTF16StrEq(u"FakeNode")), _))
+ .Times(1)
+ .WillOnce(Return(true));
+
+ JS::AutoCheckCannotGC noGC(cx);
+ ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&node), writer,
+ /* wantNames = */ true,
+ /* zones = */ nullptr, noGC));
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/moz.build b/devtools/shared/heapsnapshot/tests/gtest/moz.build
new file mode 100644
index 0000000000..880d7b334e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/moz.build
@@ -0,0 +1,32 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Library("devtoolstests")
+
+LOCAL_INCLUDES += [
+ "../..",
+]
+
+DEFINES["GOOGLE_PROTOBUF_NO_RTTI"] = True
+DEFINES["GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER"] = True
+
+UNIFIED_SOURCES = [
+ "DeserializedNodeUbiNodes.cpp",
+ "DeserializedStackFrameUbiStackFrames.cpp",
+ "DevTools.cpp",
+ "DoesCrossCompartmentBoundaries.cpp",
+ "DoesntCrossCompartmentBoundaries.cpp",
+ "SerializesEdgeNames.cpp",
+ "SerializesEverythingInHeapGraphOnce.cpp",
+ "SerializesTypeNames.cpp",
+]
+
+# THE MOCK_METHOD2 macro from gtest triggers this clang warning and it's hard
+# to work around, so we just ignore it.
+if CONFIG["CC_TYPE"] == "clang":
+ CXXFLAGS += ["-Wno-inconsistent-missing-override"]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js b/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs b/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs
new file mode 100644
index 0000000000..0e86f8b055
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs
@@ -0,0 +1,176 @@
+// Functions for checking results returned by
+// Debugger.Memory.prototype.takeCensus and
+// HeapSnapshot.prototype.takeCensus. Adapted from js/src/jit-test/lib/census.js.
+
+export const Census = {};
+function dumpn(msg) {
+ dump("DBG-TEST: Census.jsm: " + msg + "\n");
+}
+
+// Census.walkCensus(subject, name, walker)
+//
+// Use |walker| to check |subject|, a census object of the sort returned by
+// Debugger.Memory.prototype.takeCensus: a tree of objects with integers at the
+// leaves. Use |name| as the name for |subject| in diagnostic messages. Return
+// the number of leaves of |subject| we visited.
+//
+// A walker is an object with three methods:
+//
+// - enter(prop): Return the walker we should use to check the property of the
+// subject census named |prop|. This is for recursing into the subobjects of
+// the subject.
+//
+// - done(): Called after we have called 'enter' on every property of the
+// subject.
+//
+// - check(value): Check |value|, a leaf in the subject.
+//
+// Walker methods are expected to simply throw if a node we visit doesn't look
+// right.
+Census.walkCensus = (subject, name, walker) => walk(subject, name, walker, 0);
+function walk(subject, name, walker, count) {
+ if (typeof subject === "object") {
+ dumpn(name);
+ for (const prop in subject) {
+ count = walk(
+ subject[prop],
+ name + "[" + uneval(prop) + "]",
+ walker.enter(prop),
+ count
+ );
+ }
+ walker.done();
+ } else {
+ dumpn(name + " = " + uneval(subject));
+ walker.check(subject);
+ count++;
+ }
+
+ return count;
+}
+
+// A walker that doesn't check anything.
+Census.walkAnything = {
+ enter: () => Census.walkAnything,
+ done: () => undefined,
+ check: () => undefined,
+};
+
+// A walker that requires all leaves to be zeros.
+Census.assertAllZeros = {
+ enter: () => Census.assertAllZeros,
+ done: () => undefined,
+ check: elt => {
+ if (elt !== 0) {
+ throw new Error("Census mismatch: expected zero, found " + elt);
+ }
+ },
+};
+
+function expectedObject() {
+ throw new Error(
+ "Census mismatch: subject has leaf where basis has nested object"
+ );
+}
+
+function expectedLeaf() {
+ throw new Error(
+ "Census mismatch: subject has nested object where basis has leaf"
+ );
+}
+
+// Return a function that, given a 'basis' census, returns a census walker that
+// compares the subject census against the basis. The returned walker calls the
+// given |compare|, |missing|, and |extra| functions as follows:
+//
+// - compare(subjectLeaf, basisLeaf): Check a leaf of the subject against the
+// corresponding leaf of the basis.
+//
+// - missing(prop, value): Called when the subject is missing a property named
+// |prop| which is present in the basis with value |value|.
+//
+// - extra(prop): Called when the subject has a property named |prop|, but the
+// basis has no such property. This should return a walker that can check
+// the subject's value.
+function makeBasisChecker({ compare, missing, extra }) {
+ return function makeWalker(basis) {
+ if (typeof basis === "object") {
+ const unvisited = new Set(Object.getOwnPropertyNames(basis));
+ return {
+ enter: prop => {
+ unvisited.delete(prop);
+ if (prop in basis) {
+ return makeWalker(basis[prop]);
+ }
+
+ return extra(prop);
+ },
+
+ done: () => unvisited.forEach(prop => missing(prop, basis[prop])),
+ check: expectedObject,
+ };
+ }
+
+ return {
+ enter: expectedLeaf,
+ done: expectedLeaf,
+ check: elt => compare(elt, basis),
+ };
+ };
+}
+
+function missingProp(prop) {
+ throw new Error(
+ "Census mismatch: subject lacks property present in basis: " + prop
+ );
+}
+
+function extraProp(prop) {
+ throw new Error(
+ "Census mismatch: subject has property not present in basis: " + prop
+ );
+}
+
+// Return a walker that checks that the subject census has counts all equal to
+// |basis|.
+Census.assertAllEqual = makeBasisChecker({
+ compare: (a, b) => {
+ if (a !== b) {
+ throw new Error("Census mismatch: expected " + a + " got " + b);
+ }
+ },
+ missing: missingProp,
+ extra: extraProp,
+});
+
+function ok(val) {
+ if (!val) {
+ throw new Error("Census mismatch: expected truthy, got " + val);
+ }
+}
+
+// Return a walker that checks that the subject census has at least as many
+// items of each category as |basis|.
+Census.assertAllNotLessThan = makeBasisChecker({
+ compare: (subject, basis) => ok(subject >= basis),
+ missing: missingProp,
+ extra: () => Census.walkAnything,
+});
+
+// Return a walker that checks that the subject census has at most as many
+// items of each category as |basis|.
+Census.assertAllNotMoreThan = makeBasisChecker({
+ compare: (subject, basis) => ok(subject <= basis),
+ missing: missingProp,
+ extra: () => Census.walkAnything,
+});
+
+// Return a walker that checks that the subject census has within |fudge|
+// items of each category of the count in |basis|.
+Census.assertAllWithin = function (fudge, basis) {
+ return makeBasisChecker({
+ compare: (subject, base) => ok(Math.abs(subject - base) <= fudge),
+ missing: missingProp,
+ extra: () => Census.walkAnything,
+ })(basis);
+};
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs b/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs
new file mode 100644
index 0000000000..76312db86a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs
@@ -0,0 +1,218 @@
+// A little pattern-matching library.
+//
+// Ported from js/src/tests/js1_8_5/reflect-parse/Match.js for use with devtools
+// server xpcshell tests.
+
+export const Match = (function () {
+ function Pattern(template) {
+ // act like a constructor even as a function
+ if (!(this instanceof Pattern)) {
+ return new Pattern(template);
+ }
+
+ this.template = template;
+ }
+
+ Pattern.prototype = {
+ match(act) {
+ return match(act, this.template);
+ },
+
+ matches(act) {
+ try {
+ return this.match(act);
+ } catch (e) {
+ if (e instanceof MatchError) {
+ return false;
+ }
+ }
+ return false;
+ },
+
+ assert(act, message) {
+ try {
+ return this.match(act);
+ } catch (e) {
+ if (e instanceof MatchError) {
+ throw new Error((message || "failed match") + ": " + e.message);
+ }
+ }
+ return false;
+ },
+
+ toString: () => "[object Pattern]",
+ };
+
+ Pattern.ANY = new Pattern();
+ Pattern.ANY.template = Pattern.ANY;
+
+ Pattern.NUMBER = new Pattern();
+ Pattern.NUMBER.match = function (act) {
+ if (typeof act !== "number") {
+ throw new MatchError("Expected number, got: " + quote(act));
+ }
+ };
+
+ Pattern.NATURAL = new Pattern();
+ Pattern.NATURAL.match = function (act) {
+ if (typeof act !== "number" || act !== Math.floor(act) || act < 0) {
+ throw new MatchError("Expected natural number, got: " + quote(act));
+ }
+ };
+
+ const quote = uneval;
+
+ function MatchError(msg) {
+ this.message = msg;
+ }
+
+ MatchError.prototype = {
+ toString() {
+ return "match error: " + this.message;
+ },
+ };
+
+ function isAtom(x) {
+ return (
+ typeof x === "number" ||
+ typeof x === "string" ||
+ typeof x === "boolean" ||
+ x === null ||
+ (typeof x === "object" && x instanceof RegExp)
+ );
+ }
+
+ function isObject(x) {
+ return x !== null && typeof x === "object";
+ }
+
+ function isFunction(x) {
+ return typeof x === "function";
+ }
+
+ function isArrayLike(x) {
+ return isObject(x) && "length" in x;
+ }
+
+ function matchAtom(act, exp) {
+ if (typeof exp === "number" && isNaN(exp)) {
+ if (typeof act !== "number" || !isNaN(act)) {
+ throw new MatchError("expected NaN, got: " + quote(act));
+ }
+ return true;
+ }
+
+ if (exp === null) {
+ if (act !== null) {
+ throw new MatchError("expected null, got: " + quote(act));
+ }
+ return true;
+ }
+
+ if (exp instanceof RegExp) {
+ if (!(act instanceof RegExp) || exp.source !== act.source) {
+ throw new MatchError("expected " + quote(exp) + ", got: " + quote(act));
+ }
+ return true;
+ }
+
+ switch (typeof exp) {
+ case "string":
+ if (act !== exp) {
+ throw new MatchError(
+ "expected " + quote(exp) + ", got " + quote(act)
+ );
+ }
+ return true;
+ case "boolean":
+ case "number":
+ if (exp !== act) {
+ throw new MatchError("expected " + exp + ", got " + quote(act));
+ }
+ return true;
+ }
+
+ throw new Error("bad pattern: " + exp.toSource());
+ }
+
+ function matchObject(act, exp) {
+ if (!isObject(act)) {
+ throw new MatchError("expected object, got " + quote(act));
+ }
+
+ for (const key in exp) {
+ if (!(key in act)) {
+ throw new MatchError(
+ "expected property " + quote(key) + " not found in " + quote(act)
+ );
+ }
+ match(act[key], exp[key]);
+ }
+
+ return true;
+ }
+
+ function matchFunction(act, exp) {
+ if (!isFunction(act)) {
+ throw new MatchError("expected function, got " + quote(act));
+ }
+
+ if (act !== exp) {
+ throw new MatchError(
+ "expected function: " + exp + "\nbut got different function: " + act
+ );
+ }
+ }
+
+ function matchArray(act, exp) {
+ if (!isObject(act) || !("length" in act)) {
+ throw new MatchError("expected array-like object, got " + quote(act));
+ }
+
+ const length = exp.length;
+ if (act.length !== exp.length) {
+ throw new MatchError(
+ "expected array-like object of length " + length + ", got " + quote(act)
+ );
+ }
+
+ for (let i = 0; i < length; i++) {
+ if (i in exp) {
+ if (!(i in act)) {
+ throw new MatchError(
+ "expected array property " + i + " not found in " + quote(act)
+ );
+ }
+ match(act[i], exp[i]);
+ }
+ }
+
+ return true;
+ }
+
+ function match(act, exp) {
+ if (exp === Pattern.ANY) {
+ return true;
+ }
+
+ if (exp instanceof Pattern) {
+ return exp.match(act);
+ }
+
+ if (isAtom(exp)) {
+ return matchAtom(act, exp);
+ }
+
+ if (isArrayLike(exp)) {
+ return matchArray(act, exp);
+ }
+
+ if (isFunction(exp)) {
+ return matchFunction(act, exp);
+ }
+
+ return matchObject(act, exp);
+ }
+
+ return { Pattern, MatchError };
+})();
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js
new file mode 100644
index 0000000000..e50bd80932
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env mozilla/chrome-worker */
+
+"use strict";
+
+console.log("Initializing worker.");
+
+self.onmessage = e => {
+ console.log("Starting test.");
+ try {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ ok(dominatorTree);
+ ok(DominatorTree.isInstance(dominatorTree));
+
+ let threw = false;
+ try {
+ new DominatorTree();
+ } catch (excp) {
+ threw = true;
+ }
+ ok(threw, "Constructor shouldn't be usable");
+ } catch (ex) {
+ ok(
+ false,
+ "Unexpected error inside worker:\n" + ex.toString() + "\n" + ex.stack
+ );
+ } finally {
+ done();
+ }
+};
+
+// Proxy assertions to the main thread.
+function ok(val, msg) {
+ console.log("ok(" + !!val + ', "' + msg + '")');
+ self.postMessage({
+ type: "assertion",
+ passed: !!val,
+ msg,
+ stack: Error().stack,
+ });
+}
+
+// Tell the main thread we are done with the tests.
+function done() {
+ console.log("done()");
+ self.postMessage({
+ type: "done",
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js
new file mode 100644
index 0000000000..6085a37f61
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js
@@ -0,0 +1,550 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+/* exported Cr, CC, Match, Census, Task, DevToolsUtils, HeapAnalysesClient,
+ assertThrows, getFilePath, saveHeapSnapshotAndTakeCensus,
+ saveHeapSnapshotAndComputeDominatorTree, compareCensusViewData, assertDiff,
+ assertLabelAndShallowSize, makeTestDominatorTreeNode,
+ assertDominatorTreeNodeInsertion, assertDeduplicatedPaths,
+ assertCountToBucketBreakdown, pathEntry */
+
+var CC = Components.Constructor;
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const { Match } = ChromeUtils.importESModule("resource://test/Match.sys.mjs");
+const { Census } = ChromeUtils.importESModule("resource://test/Census.sys.mjs");
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+);
+
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js");
+const {
+ censusReportToCensusTreeNode,
+} = require("resource://devtools/shared/heapsnapshot/census-tree-node.js");
+const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
+const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js");
+const {
+ deduplicatePaths,
+} = require("resource://devtools/shared/heapsnapshot/shortest-paths.js");
+const { LabelAndShallowSizeVisitor } = DominatorTreeNode;
+
+// Always log packets when running tests. runxpcshelltests.py will throw
+// the output away anyway, unless you give it the --verbose flag.
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Services.prefs.setBoolPref("devtools.debugger.log", true);
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.log");
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ });
+}
+
+const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+);
+
+function dumpn(msg) {
+ dump("HEAPSNAPSHOT-TEST: " + msg + "\n");
+}
+
+function addTestingFunctionsToGlobal(global) {
+ global.eval(
+ `
+ const testingFunctions = Components.utils.getJSTestingFunctions();
+ for (let k in testingFunctions) {
+ this[k] = testingFunctions[k];
+ }
+ `
+ );
+ if (!global.print) {
+ global.print = info;
+ }
+ if (!global.newGlobal) {
+ global.newGlobal = newGlobal;
+ }
+ if (!global.Debugger) {
+ addDebuggerToGlobal(global);
+ }
+}
+
+addTestingFunctionsToGlobal(this);
+
+/**
+ * Create a new global, with all the JS shell testing functions. Similar to the
+ * newGlobal function exposed to JS shells, and useful for porting JS shell
+ * tests to xpcshell tests.
+ */
+function newGlobal() {
+ const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
+ addTestingFunctionsToGlobal(global);
+ return global;
+}
+
+function assertThrows(f, val, msg) {
+ let fullmsg;
+ try {
+ f();
+ } catch (exc) {
+ if (exc === val && (val !== 0 || 1 / exc === 1 / val)) {
+ return;
+ } else if (exc instanceof Error && exc.message === val) {
+ return;
+ }
+ fullmsg = "Assertion failed: expected exception " + val + ", got " + exc;
+ }
+ if (fullmsg === undefined) {
+ fullmsg =
+ "Assertion failed: expected exception " + val + ", no exception thrown";
+ }
+ if (msg !== undefined) {
+ fullmsg += " - " + msg;
+ }
+ throw new Error(fullmsg);
+}
+
+/**
+ * Returns the full path of the file with the specified name in a
+ * platform-independent and URL-like form.
+ */
+function getFilePath(
+ name,
+ allowMissing = false,
+ usePlatformPathSeparator = false
+) {
+ const file = do_get_file(name, allowMissing);
+ let path = Services.io.newFileURI(file).spec;
+ let filePrePath = "file://";
+ if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) {
+ filePrePath += "/";
+ }
+
+ path = path.slice(filePrePath.length);
+
+ if (usePlatformPathSeparator && path.match(/^\w:/)) {
+ path = path.replace(/\//g, "\\");
+ }
+
+ return path;
+}
+
+function saveNewHeapSnapshot(opts = { runtime: true }) {
+ const filePath = ChromeUtils.saveHeapSnapshot(opts);
+ ok(filePath, "Should get a file path to save the core dump to.");
+ ok(true, "Saved a heap snapshot to " + filePath);
+ return filePath;
+}
+
+function readHeapSnapshot(filePath) {
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should have read a heap snapshot back from " + filePath);
+ ok(
+ HeapSnapshot.isInstance(snapshot),
+ "snapshot should be an instance of HeapSnapshot"
+ );
+ return snapshot;
+}
+
+/**
+ * Save a heap snapshot to the file with the given name in the current
+ * directory, read it back as a HeapSnapshot instance, and then take a census of
+ * the heap snapshot's serialized heap graph with the provided census options.
+ *
+ * @param {Object|undefined} censusOptions
+ * Options that should be passed through to the takeCensus method. See
+ * js/src/doc/Debugger/Debugger.Memory.md for details.
+ *
+ * @param {Debugger|null} dbg
+ * If a Debugger object is given, only serialize the subgraph covered by
+ * the Debugger's debuggees. If null, serialize the whole heap graph.
+ *
+ * @param {String} fileName
+ * The file name to save the heap snapshot's core dump file to, within
+ * the current directory.
+ *
+ * @returns Census
+ */
+function saveHeapSnapshotAndTakeCensus(dbg = null, censusOptions = undefined) {
+ const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
+ const filePath = saveNewHeapSnapshot(snapshotOptions);
+ const snapshot = readHeapSnapshot(filePath);
+
+ equal(
+ typeof snapshot.takeCensus,
+ "function",
+ "snapshot should have a takeCensus method"
+ );
+
+ return snapshot.takeCensus(censusOptions);
+}
+
+/**
+ * Save a heap snapshot to disk, read it back as a HeapSnapshot instance, and
+ * then compute its dominator tree.
+ *
+ * @param {Debugger|null} dbg
+ * If a Debugger object is given, only serialize the subgraph covered by
+ * the Debugger's debuggees. If null, serialize the whole heap graph.
+ *
+ * @returns {DominatorTree}
+ */
+function saveHeapSnapshotAndComputeDominatorTree(dbg = null) {
+ const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
+ const filePath = saveNewHeapSnapshot(snapshotOptions);
+ const snapshot = readHeapSnapshot(filePath);
+
+ equal(
+ typeof snapshot.computeDominatorTree,
+ "function",
+ "snapshot should have a `computeDominatorTree` method"
+ );
+
+ const dominatorTree = snapshot.computeDominatorTree();
+
+ ok(dominatorTree, "Should be able to compute a dominator tree");
+ ok(
+ DominatorTree.isInstance(dominatorTree),
+ "Should be an instance of DominatorTree"
+ );
+
+ return dominatorTree;
+}
+
+function isSavedFrame(obj) {
+ return Object.prototype.toString.call(obj) === "[object SavedFrame]";
+}
+
+function savedFrameReplacer(key, val) {
+ if (isSavedFrame(val)) {
+ return `<SavedFrame '${val.toString().split(/\n/g).shift()}'>`;
+ }
+ return val;
+}
+
+/**
+ * Assert that creating a CensusTreeNode from the given `report` with the
+ * specified `breakdown` creates the given `expected` CensusTreeNode.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} report
+ * The census report.
+ *
+ * @param {Object} expected
+ * The expected CensusTreeNode result.
+ *
+ * @param {Object} options
+ * The options to pass through to `censusReportToCensusTreeNode`.
+ */
+function compareCensusViewData(breakdown, report, expected, options) {
+ dumpn("Generating CensusTreeNode from report:");
+ dumpn("breakdown: " + JSON.stringify(breakdown, null, 4));
+ dumpn("report: " + JSON.stringify(report, null, 4));
+ dumpn("expected: " + JSON.stringify(expected, savedFrameReplacer, 4));
+
+ const actual = censusReportToCensusTreeNode(breakdown, report, options);
+ dumpn("actual: " + JSON.stringify(actual, savedFrameReplacer, 4));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+// Deep structural equivalence that can handle Map objects in addition to plain
+// objects.
+function assertStructurallyEquivalent(actual, expected, path = "root") {
+ if (actual === expected) {
+ equal(actual, expected, "actual and expected are the same");
+ return;
+ }
+
+ equal(typeof actual, typeof expected, `${path}: typeof should be the same`);
+
+ if (actual && typeof actual === "object") {
+ const actualProtoString = Object.prototype.toString.call(actual);
+ const expectedProtoString = Object.prototype.toString.call(expected);
+ equal(
+ actualProtoString,
+ expectedProtoString,
+ `${path}: Object.prototype.toString.call() should be the same`
+ );
+
+ if (actualProtoString === "[object Map]") {
+ const expectedKeys = new Set([...expected.keys()]);
+
+ for (const key of actual.keys()) {
+ ok(
+ expectedKeys.has(key),
+ `${path}: every key in actual is expected: ${String(key).slice(
+ 0,
+ 10
+ )}`
+ );
+ expectedKeys.delete(key);
+
+ assertStructurallyEquivalent(
+ actual.get(key),
+ expected.get(key),
+ path + ".get(" + String(key).slice(0, 20) + ")"
+ );
+ }
+
+ equal(
+ expectedKeys.size,
+ 0,
+ `${path}: every key in expected should also exist in actual,\
+ did not see ${[...expectedKeys]}`
+ );
+ } else if (actualProtoString === "[object Set]") {
+ const expectedItems = new Set([...expected]);
+
+ for (const item of actual) {
+ ok(
+ expectedItems.has(item),
+ `${path}: every set item in actual should exist in expected: ${item}`
+ );
+ expectedItems.delete(item);
+ }
+
+ equal(
+ expectedItems.size,
+ 0,
+ `${path}: every set item in expected should also exist in actual,\
+ did not see ${[...expectedItems]}`
+ );
+ } else {
+ const expectedKeys = new Set(Object.keys(expected));
+
+ for (const key of Object.keys(actual)) {
+ ok(
+ expectedKeys.has(key),
+ `${path}: every key in actual should exist in expected: ${key}`
+ );
+ expectedKeys.delete(key);
+
+ assertStructurallyEquivalent(
+ actual[key],
+ expected[key],
+ path + "." + key
+ );
+ }
+
+ equal(
+ expectedKeys.size,
+ 0,
+ `${path}: every key in expected should also exist in actual,\
+ did not see ${[...expectedKeys]}`
+ );
+ }
+ } else {
+ equal(actual, expected, `${path}: primitives should be equal`);
+ }
+}
+
+/**
+ * Assert that creating a diff of the `first` and `second` census reports
+ * creates the `expected` delta-report.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} first
+ * The first census report.
+ *
+ * @param {Object} second
+ * The second census report.
+ *
+ * @param {Object} expected
+ * The expected delta-report.
+ */
+function assertDiff(breakdown, first, second, expected) {
+ dumpn("Diffing census reports:");
+ dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
+ dumpn("First census report: " + JSON.stringify(first, null, 4));
+ dumpn("Second census report: " + JSON.stringify(second, null, 4));
+ dumpn("Expected delta-report: " + JSON.stringify(expected, null, 4));
+
+ const actual = CensusUtils.diff(breakdown, first, second);
+ dumpn("Actual delta-report: " + JSON.stringify(actual, null, 4));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+/**
+ * Assert that creating a label and getting a shallow size from the given node
+ * description with the specified breakdown is as expected.
+ *
+ * @param {Object} breakdown
+ * @param {Object} givenDescription
+ * @param {Number} expectedShallowSize
+ * @param {Object} expectedLabel
+ */
+function assertLabelAndShallowSize(
+ breakdown,
+ givenDescription,
+ expectedShallowSize,
+ expectedLabel
+) {
+ dumpn("Computing label and shallow size from node description:");
+ dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
+ dumpn("Given description: " + JSON.stringify(givenDescription, null, 4));
+
+ const visitor = new LabelAndShallowSizeVisitor();
+ CensusUtils.walk(breakdown, givenDescription, visitor);
+
+ dumpn("Expected shallow size: " + expectedShallowSize);
+ dumpn("Actual shallow size: " + visitor.shallowSize());
+ equal(
+ visitor.shallowSize(),
+ expectedShallowSize,
+ "Shallow size should be correct"
+ );
+
+ dumpn("Expected label: " + JSON.stringify(expectedLabel, null, 4));
+ dumpn("Actual label: " + JSON.stringify(visitor.label(), null, 4));
+ assertStructurallyEquivalent(visitor.label(), expectedLabel);
+}
+
+// Counter for mock DominatorTreeNode ids.
+let TEST_NODE_ID_COUNTER = 0;
+
+/**
+ * Create a mock DominatorTreeNode for testing, with sane defaults. Override any
+ * property by providing it on `opts`. Optionally pass child nodes as well.
+ *
+ * @param {Object} opts
+ * @param {Array<DominatorTreeNode>?} children
+ *
+ * @returns {DominatorTreeNode}
+ */
+function makeTestDominatorTreeNode(opts, children) {
+ const nodeId = TEST_NODE_ID_COUNTER++;
+
+ const node = Object.assign(
+ {
+ nodeId,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: (children || []).reduce(
+ (size, c) => size + c.retainedSize,
+ 1
+ ),
+ parentId: undefined,
+ children,
+ moreChildrenAvailable: true,
+ },
+ opts
+ );
+
+ if (children && children.length) {
+ children.map(c => (c.parentId = node.nodeId));
+ }
+
+ return node;
+}
+
+/**
+ * Insert `newChildren` into the given dominator `tree` as specified by the
+ * `path` from the root to the node the `newChildren` should be inserted
+ * beneath. Assert that the resulting tree matches `expected`.
+ */
+function assertDominatorTreeNodeInsertion(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable,
+ expected
+) {
+ dumpn("Inserting new children into a dominator tree:");
+ dumpn("Dominator tree: " + JSON.stringify(tree, null, 2));
+ dumpn("Path: " + JSON.stringify(path, null, 2));
+ dumpn("New children: " + JSON.stringify(newChildren, null, 2));
+ dumpn("Expected resulting tree: " + JSON.stringify(expected, null, 2));
+
+ const actual = DominatorTreeNode.insert(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable
+ );
+ dumpn("Actual resulting tree: " + JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+function assertDeduplicatedPaths({
+ target,
+ paths,
+ expectedNodes,
+ expectedEdges,
+}) {
+ dumpn("Deduplicating paths:");
+ dumpn("target = " + target);
+ dumpn("paths = " + JSON.stringify(paths, null, 2));
+ dumpn("expectedNodes = " + expectedNodes);
+ dumpn("expectedEdges = " + JSON.stringify(expectedEdges, null, 2));
+
+ const { nodes, edges } = deduplicatePaths(target, paths);
+
+ dumpn("Actual nodes = " + nodes);
+ dumpn("Actual edges = " + JSON.stringify(edges, null, 2));
+
+ equal(
+ nodes.length,
+ expectedNodes.length,
+ "actual number of nodes is equal to the expected number of nodes"
+ );
+
+ equal(
+ edges.length,
+ expectedEdges.length,
+ "actual number of edges is equal to the expected number of edges"
+ );
+
+ const expectedNodeSet = new Set(expectedNodes);
+ const nodeSet = new Set(nodes);
+ ok(nodeSet.size === nodes.length, "each returned node should be unique");
+
+ for (const node of nodes) {
+ ok(expectedNodeSet.has(node), `the ${node} node was expected`);
+ }
+
+ for (const expectedEdge of expectedEdges) {
+ let count = 0;
+ for (const edge of edges) {
+ if (
+ edge.from === expectedEdge.from &&
+ edge.to === expectedEdge.to &&
+ edge.name === expectedEdge.name
+ ) {
+ count++;
+ }
+ }
+ equal(
+ count,
+ 1,
+ "should have exactly one matching edge for the expected edge = " +
+ JSON.stringify(expectedEdge)
+ );
+ }
+}
+
+function assertCountToBucketBreakdown(breakdown, expected) {
+ dumpn("count => bucket breakdown");
+ dumpn("Initial breakdown = ", JSON.stringify(breakdown, null, 2));
+ dumpn("Expected results = ", JSON.stringify(expected, null, 2));
+
+ const actual = CensusUtils.countToBucketBreakdown(breakdown);
+ dumpn("Actual results = ", JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+/**
+ * Create a mock path entry for the given predecessor and edge.
+ */
+function pathEntry(predecessor, edge) {
+ return { predecessor, edge };
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js
new file mode 100644
index 0000000000..c80c83c633
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env mozilla/chrome-worker */
+
+"use strict";
+
+console.log("Initializing worker.");
+
+self.onmessage = ex => {
+ console.log("Starting test.");
+ try {
+ ok(ChromeUtils, "Should have access to ChromeUtils in a worker.");
+ ok(HeapSnapshot, "Should have access to HeapSnapshot in a worker.");
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(
+ HeapSnapshot.isInstance(snapshot),
+ "Should be an instanceof HeapSnapshot"
+ );
+ } catch (e) {
+ ok(
+ false,
+ "Unexpected error inside worker:\n" + e.toString() + "\n" + e.stack
+ );
+ } finally {
+ done();
+ }
+};
+
+// Proxy assertions to the main thread.
+function ok(val, msg) {
+ console.log("ok(" + !!val + ', "' + msg + '")');
+ self.postMessage({
+ type: "assertion",
+ passed: !!val,
+ msg,
+ stack: Error().stack,
+ });
+}
+
+// Tell the main thread we are done with the tests.
+function done() {
+ console.log("done()");
+ self.postMessage({
+ type: "done",
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js
new file mode 100644
index 0000000000..7d4560e6dc
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const description = {
+ objects: {
+ Function: { count: 1, bytes: 32 },
+ other: { count: 0, bytes: 0 },
+ },
+ strings: {},
+ scripts: {},
+ other: {},
+ domNode: {},
+};
+
+const expected = ["objects", "Function"];
+
+const shallowSize = 32;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js
new file mode 100644
index 0000000000..cd424afdd0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const description = {
+ objects: {
+ other: { count: 1, bytes: 10 },
+ },
+ strings: {},
+ scripts: {},
+ other: {},
+ domNode: {},
+};
+
+const expected = ["objects", "other"];
+
+const shallowSize = 10;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js
new file mode 100644
index 0000000000..098e3efc4f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const description = {
+ objects: {
+ other: { count: 0, bytes: 0 },
+ },
+ strings: {
+ JSString: { count: 1, bytes: 42 },
+ },
+ scripts: {},
+ other: {},
+ domNode: {},
+};
+
+const expected = ["strings", "JSString"];
+
+const shallowSize = 42;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js
new file mode 100644
index 0000000000..a087c39a2a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+ },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const stack = saveStack();
+
+const description = {
+ objects: {
+ Array: new Map([[stack, { count: 1, bytes: 512 }]]),
+ other: { count: 0, bytes: 0 },
+ },
+ strings: {},
+ scripts: {},
+ other: {},
+ domNode: {},
+};
+
+const expected = ["objects", "Array", stack];
+
+const shallowSize = 512;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js
new file mode 100644
index 0000000000..07894c67b1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the DominatorTreeNode.attachShortestPaths function can correctly
+// attach the deduplicated shortest retaining paths for each node it is given.
+
+const startNodeId = 9999;
+const maxNumPaths = 2;
+
+// Mock data mapping node id to shortest paths to that node id.
+const shortestPaths = new Map([
+ [
+ 1000,
+ [
+ [pathEntry(1100, "a"), pathEntry(1200, "b")],
+ [pathEntry(1100, "c"), pathEntry(1300, "d")],
+ ],
+ ],
+ [2000, [[pathEntry(2100, "e"), pathEntry(2200, "f"), pathEntry(2300, "g")]]],
+ [
+ 3000,
+ [
+ [pathEntry(3100, "h")],
+ [pathEntry(3100, "i")],
+ [pathEntry(3100, "j")],
+ [pathEntry(3200, "k")],
+ [pathEntry(3300, "l")],
+ [pathEntry(3400, "m")],
+ ],
+ ],
+]);
+
+const actual = [
+ makeTestDominatorTreeNode({ nodeId: 1000 }),
+ makeTestDominatorTreeNode({ nodeId: 2000 }),
+ makeTestDominatorTreeNode({ nodeId: 3000 }),
+];
+
+const expected = [
+ makeTestDominatorTreeNode({
+ nodeId: 1000,
+ shortestPaths: {
+ nodes: [
+ { id: 1000, label: ["SomeType-1000"] },
+ { id: 1100, label: ["SomeType-1100"] },
+ { id: 1200, label: ["SomeType-1200"] },
+ { id: 1300, label: ["SomeType-1300"] },
+ ],
+ edges: [
+ { from: 1100, to: 1200, name: "a" },
+ { from: 1100, to: 1300, name: "c" },
+ { from: 1200, to: 1000, name: "b" },
+ { from: 1300, to: 1000, name: "d" },
+ ],
+ },
+ }),
+
+ makeTestDominatorTreeNode({
+ nodeId: 2000,
+ shortestPaths: {
+ nodes: [
+ { id: 2000, label: ["SomeType-2000"] },
+ { id: 2100, label: ["SomeType-2100"] },
+ { id: 2200, label: ["SomeType-2200"] },
+ { id: 2300, label: ["SomeType-2300"] },
+ ],
+ edges: [
+ { from: 2100, to: 2200, name: "e" },
+ { from: 2200, to: 2300, name: "f" },
+ { from: 2300, to: 2000, name: "g" },
+ ],
+ },
+ }),
+
+ makeTestDominatorTreeNode({
+ nodeId: 3000,
+ shortestPaths: {
+ nodes: [
+ { id: 3000, label: ["SomeType-3000"] },
+ { id: 3100, label: ["SomeType-3100"] },
+ { id: 3200, label: ["SomeType-3200"] },
+ { id: 3300, label: ["SomeType-3300"] },
+ { id: 3400, label: ["SomeType-3400"] },
+ ],
+ edges: [
+ { from: 3100, to: 3000, name: "h" },
+ { from: 3100, to: 3000, name: "i" },
+ { from: 3100, to: 3000, name: "j" },
+ { from: 3200, to: 3000, name: "k" },
+ { from: 3300, to: 3000, name: "l" },
+ { from: 3400, to: 3000, name: "m" },
+ ],
+ },
+ }),
+];
+
+const breakdown = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+const mockSnapshot = {
+ computeShortestPaths: (start, nodeIds, max) => {
+ equal(start, startNodeId);
+ equal(max, maxNumPaths);
+
+ return new Map(
+ nodeIds.map(nodeId => {
+ const paths = shortestPaths.get(nodeId);
+ ok(paths, "Expected computeShortestPaths call for node id = " + nodeId);
+ return [nodeId, paths];
+ })
+ );
+ },
+
+ describeNode: (bd, nodeId) => {
+ equal(bd, breakdown);
+ return {
+ ["SomeType-" + nodeId]: {
+ count: 1,
+ bytes: 10,
+ },
+ };
+ },
+};
+
+function run_test() {
+ DominatorTreeNode.attachShortestPaths(
+ mockSnapshot,
+ breakdown,
+ startNodeId,
+ actual,
+ maxNumPaths
+ );
+
+ dumpn("Expected = " + JSON.stringify(expected, null, 2));
+ dumpn("Actual = " + JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(expected, actual);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js
new file mode 100644
index 0000000000..be1f210c3e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can find the node with the given id along the specified path.
+
+const node3000 = makeTestDominatorTreeNode({ nodeId: 3000 });
+
+const node2000 = makeTestDominatorTreeNode({ nodeId: 2000 }, [
+ makeTestDominatorTreeNode({}),
+ node3000,
+ makeTestDominatorTreeNode({}),
+]);
+
+const node1000 = makeTestDominatorTreeNode({ nodeId: 1000 }, [
+ makeTestDominatorTreeNode({}),
+ node2000,
+ makeTestDominatorTreeNode({}),
+]);
+
+const tree = node1000;
+
+const path = [1000, 2000, 3000];
+
+const tests = [
+ { id: 1000, expected: node1000 },
+ { id: 2000, expected: node2000 },
+ { id: 3000, expected: node3000 },
+];
+
+function run_test() {
+ for (const { id, expected } of tests) {
+ const actual = DominatorTreeNode.getNodeByIdAlongPath(id, tree, path);
+ equal(actual, expected, `We should have got the node with id = ${id}`);
+ }
+
+ equal(
+ null,
+ DominatorTreeNode.getNodeByIdAlongPath(999999999999, tree, path),
+ "null is returned for nodes that are not even in the tree"
+ );
+
+ const lastNodeId = tree.children[tree.children.length - 1].nodeId;
+ equal(
+ null,
+ DominatorTreeNode.getNodeByIdAlongPath(lastNodeId, tree, path),
+ "null is returned for nodes that are not along the path"
+ );
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js
new file mode 100644
index 0000000000..7567c473e0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can insert new children into an existing DominatorTreeNode tree.
+
+const tree = makeTestDominatorTreeNode({ nodeId: 1000 }, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({ nodeId: 2000 }, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({ nodeId: 3000 }),
+ makeTestDominatorTreeNode({}),
+ ]),
+ makeTestDominatorTreeNode({}),
+]);
+
+const path = [1000, 2000, 3000];
+
+const newChildren = [
+ makeTestDominatorTreeNode({ parentId: 3000 }),
+ makeTestDominatorTreeNode({ parentId: 3000 }),
+];
+
+const moreChildrenAvailable = false;
+
+const expected = {
+ nodeId: 1000,
+ parentId: undefined,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 7,
+ children: [
+ {
+ nodeId: 0,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 1000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 2000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 4,
+ parentId: 1000,
+ children: [
+ {
+ nodeId: 1,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 2000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 3000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 2000,
+ children: [
+ {
+ nodeId: 7,
+ parentId: 3000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 8,
+ parentId: 3000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: false,
+ },
+ {
+ nodeId: 3,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 2000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: true,
+ },
+ {
+ nodeId: 5,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 1000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: true,
+};
+
+function run_test() {
+ assertDominatorTreeNodeInsertion(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable,
+ expected
+ );
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js
new file mode 100644
index 0000000000..b0b80c3c95
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test attempting to insert new children into an existing DominatorTreeNode
+// tree with a bad path.
+
+const tree = makeTestDominatorTreeNode({}, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ ]),
+ makeTestDominatorTreeNode({}),
+]);
+
+const path = [111111, 222222, 333333];
+
+const newChildren = [
+ makeTestDominatorTreeNode({ parentId: 333333 }),
+ makeTestDominatorTreeNode({ parentId: 333333 }),
+];
+
+const moreChildrenAvailable = false;
+
+const expected = tree;
+
+function run_test() {
+ assertDominatorTreeNodeInsertion(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable,
+ expected
+ );
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js
new file mode 100644
index 0000000000..552ed72735
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test inserting new children into an existing DominatorTreeNode at the root.
+
+const tree = makeTestDominatorTreeNode({ nodeId: 666 }, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ ]),
+ makeTestDominatorTreeNode({}),
+]);
+
+const path = [666];
+
+const newChildren = [
+ makeTestDominatorTreeNode({
+ nodeId: 777,
+ parentId: 666,
+ }),
+ makeTestDominatorTreeNode({
+ nodeId: 888,
+ parentId: 666,
+ }),
+];
+
+const moreChildrenAvailable = false;
+
+const expected = {
+ nodeId: 666,
+ label: undefined,
+ parentId: undefined,
+ shallowSize: 1,
+ retainedSize: 7,
+ children: [
+ {
+ nodeId: 0,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 4,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 4,
+ parentId: 666,
+ children: [
+ {
+ nodeId: 1,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 4,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 2,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 4,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 3,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 4,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: true,
+ },
+ {
+ nodeId: 5,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 777,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 888,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: false,
+};
+
+function run_test() {
+ assertDominatorTreeNodeInsertion(
+ tree,
+ path,
+ newChildren,
+ moreChildrenAvailable,
+ expected
+ );
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js
new file mode 100644
index 0000000000..6da0587f57
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we correctly set `moreChildrenAvailable` when doing a partial
+// traversal of a dominator tree to create the initial incrementally loaded
+// `DominatorTreeNode` tree.
+
+// `tree` maps parent to children:
+//
+// 100
+// |- 200
+// | |- 500
+// | |- 600
+// | `- 700
+// |- 300
+// | |- 800
+// | |- 900
+// `- 400
+// |- 1000
+// |- 1100
+// `- 1200
+const tree = new Map([
+ [100, [200, 300, 400]],
+ [200, [500, 600, 700]],
+ [300, [800, 900]],
+ [400, [1000, 1100, 1200]],
+]);
+
+const mockDominatorTree = {
+ root: 100,
+ getRetainedSize: _ => 10,
+ getImmediatelyDominated: id => (tree.get(id) || []).slice(),
+};
+
+const mockSnapshot = {
+ describeNode: _ => ({
+ objects: { count: 0, bytes: 0 },
+ strings: { count: 0, bytes: 0 },
+ scripts: { count: 0, bytes: 0 },
+ other: { SomeType: { count: 1, bytes: 10 } },
+ domNode: { count: 0, bytes: 0 },
+ }),
+};
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+const expected = {
+ nodeId: 100,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ shortestPaths: undefined,
+ children: [
+ {
+ nodeId: 200,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 100,
+ shortestPaths: undefined,
+ children: [
+ {
+ nodeId: 500,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 200,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined,
+ },
+ {
+ nodeId: 600,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 200,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: true,
+ },
+ {
+ nodeId: 300,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 100,
+ shortestPaths: undefined,
+ children: [
+ {
+ nodeId: 800,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 300,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined,
+ },
+ {
+ nodeId: 900,
+ label: ["other", "SomeType"],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 300,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: false,
+ },
+ ],
+ moreChildrenAvailable: true,
+ parentId: undefined,
+};
+
+function run_test() {
+ // Traverse the whole depth of the test tree, but one short of the number of
+ // siblings. This will exercise the moreChildrenAvailable handling for
+ // siblings.
+ const actual = DominatorTreeNode.partialTraversal(
+ mockDominatorTree,
+ mockSnapshot,
+ breakdown,
+ // maxDepth
+ 4,
+ // siblings
+ 2
+ );
+
+ dumpn("Expected = " + JSON.stringify(expected, null, 2));
+ dumpn("Actual = " + JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(expected, actual);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js
new file mode 100644
index 0000000000..f98094fd32
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Sanity test that we can compute dominator trees.
+
+function run_test() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ ok(dominatorTree);
+ ok(DominatorTree.isInstance(dominatorTree));
+
+ let threw = false;
+ try {
+ new DominatorTree();
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "Constructor shouldn't be usable");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js
new file mode 100644
index 0000000000..ca8d0533ab
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can compute dominator trees from a snapshot in a worker.
+
+add_task(async function () {
+ const worker = new ChromeWorker("resource://test/dominator-tree-worker.js");
+ worker.postMessage({});
+
+ let assertionCount = 0;
+ worker.onmessage = e => {
+ if (e.data.type !== "assertion") {
+ return;
+ }
+
+ ok(e.data.passed, e.data.msg + "\n" + e.data.stack);
+ assertionCount++;
+ };
+
+ await waitForDone(worker);
+
+ ok(assertionCount > 0);
+ worker.terminate();
+});
+
+function waitForDone(w) {
+ return new Promise((resolve, reject) => {
+ w.onerror = e => {
+ reject();
+ ok(false, "Error in worker: " + e);
+ };
+
+ w.addEventListener("message", function listener(e) {
+ if (e.data.type === "done") {
+ w.removeEventListener("message", listener);
+ resolve();
+ }
+ });
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js
new file mode 100644
index 0000000000..013a93697a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can get the root of dominator trees.
+
+function run_test() {
+ const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
+ equal(typeof dominatorTree.root, "number", "root should be a number");
+ equal(
+ Math.floor(dominatorTree.root),
+ dominatorTree.root,
+ "root should be an integer"
+ );
+ ok(dominatorTree.root >= 0, "root should be positive");
+ ok(
+ dominatorTree.root <= Math.pow(2, 48),
+ "root should be less than or equal to 2^48"
+ );
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js
new file mode 100644
index 0000000000..a669c7b52b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can get the retained sizes of dominator trees.
+
+function run_test() {
+ const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
+ equal(
+ typeof dominatorTree.getRetainedSize,
+ "function",
+ "getRetainedSize should be a function"
+ );
+
+ const size = dominatorTree.getRetainedSize(dominatorTree.root);
+ ok(size, "should get a size for the root");
+ equal(typeof size, "number", "retained sizes should be a number");
+ equal(Math.floor(size), size, "size should be an integer");
+ ok(size > 0, "size should be positive");
+ ok(size <= Math.pow(2, 64), "size should be less than or equal to 2^64");
+
+ const bad = dominatorTree.getRetainedSize(1);
+ equal(bad, null, "null is returned for unknown node ids");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js
new file mode 100644
index 0000000000..23abf1bd74
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can get the set of immediately dominated nodes for any given
+// node and that this forms a tree.
+
+function run_test() {
+ const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
+ equal(
+ typeof dominatorTree.getImmediatelyDominated,
+ "function",
+ "getImmediatelyDominated should be a function"
+ );
+
+ // Do a traversal of the dominator tree.
+ //
+ // Note that we don't assert directly, only if we get an unexpected
+ // value. There are just way too many nodes in the heap graph to assert for
+ // every one. This test would constantly time out and assertion messages would
+ // overflow the log size.
+
+ const root = dominatorTree.root;
+ equal(
+ dominatorTree.getImmediateDominator(root),
+ null,
+ "The root should not have a parent"
+ );
+
+ const seen = new Set();
+ const stack = [root];
+ while (stack.length) {
+ const top = stack.pop();
+
+ if (seen.has(top)) {
+ ok(
+ false,
+ "This is a tree, not a graph: we shouldn't have " +
+ "multiple edges to the same node"
+ );
+ }
+ seen.add(top);
+ if (seen.size % 1000 === 0) {
+ dumpn("Progress update: seen size = " + seen.size);
+ }
+
+ const newNodes = dominatorTree.getImmediatelyDominated(top);
+ if (Object.prototype.toString.call(newNodes) !== "[object Array]") {
+ ok(
+ false,
+ "getImmediatelyDominated should return an array for known node ids"
+ );
+ }
+
+ const topSize = dominatorTree.getRetainedSize(top);
+
+ let lastSize = Infinity;
+ for (let i = 0; i < newNodes.length; i++) {
+ if (typeof newNodes[i] !== "number") {
+ ok(false, "Every dominated id should be a number");
+ }
+
+ if (dominatorTree.getImmediateDominator(newNodes[i]) !== top) {
+ ok(false, "child's parent should be the expected parent");
+ }
+
+ const thisSize = dominatorTree.getRetainedSize(newNodes[i]);
+
+ if (thisSize >= topSize) {
+ ok(
+ false,
+ "the size of children in the dominator tree should" +
+ " always be less than that of their parent"
+ );
+ }
+
+ if (thisSize > lastSize) {
+ ok(
+ false,
+ "children should be sorted by greatest to least retained size, " +
+ "lastSize = " +
+ lastSize +
+ ", thisSize = " +
+ thisSize
+ );
+ }
+
+ lastSize = thisSize;
+ stack.push(newNodes[i]);
+ }
+ }
+
+ ok(true, "Successfully walked the tree");
+ dumpn("Walked " + seen.size + " nodes");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js
new file mode 100644
index 0000000000..fdd4191c5f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the retained size of a node is the sum of its children retained
+// sizes plus its shallow size.
+
+// Note that we don't assert directly, only if we get an unexpected
+// value. There are just way too many nodes in the heap graph to assert for
+// every one. This test would constantly time out and assertion messages would
+// overflow the log size.
+function fastAssert(cond, msg) {
+ if (!cond) {
+ ok(false, msg);
+ }
+}
+
+const COUNT = { by: "count", count: false, bytes: true };
+
+function run_test() {
+ const path = saveNewHeapSnapshot();
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ const dominatorTree = snapshot.computeDominatorTree();
+
+ // Do a traversal of the dominator tree and assert the relationship between
+ // retained size, shallow size, and children's retained sizes.
+
+ const root = dominatorTree.root;
+ const stack = [root];
+ while (stack.length) {
+ const top = stack.pop();
+
+ const children = dominatorTree.getImmediatelyDominated(top);
+
+ const topRetainedSize = dominatorTree.getRetainedSize(top);
+ const topShallowSize = snapshot.describeNode(COUNT, top).bytes;
+ fastAssert(
+ topShallowSize <= topRetainedSize,
+ "The shallow size should be less than or equal to the " + "retained size"
+ );
+
+ let sumOfChildrensRetainedSizes = 0;
+ for (let i = 0; i < children.length; i++) {
+ sumOfChildrensRetainedSizes += dominatorTree.getRetainedSize(children[i]);
+ stack.push(children[i]);
+ }
+
+ fastAssert(
+ sumOfChildrensRetainedSizes <= topRetainedSize,
+ "The sum of the children's retained sizes should be less than " +
+ "or equal to the retained size"
+ );
+ fastAssert(
+ sumOfChildrensRetainedSizes + topShallowSize === topRetainedSize,
+ "The sum of the children's retained sizes plus the shallow " +
+ "size should be equal to the retained size"
+ );
+ }
+
+ ok(true, "Successfully walked the tree");
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js
new file mode 100644
index 0000000000..ba622f55df
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ equal(
+ typeof dominatorTreeId,
+ "number",
+ "should get a dominator tree id, and it should be a number"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js
new file mode 100644
index 0000000000..c3ee76be13
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request with bad
+// file paths.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ let threw = false;
+ try {
+ await client.computeDominatorTree("/etc/passwd");
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "should throw when given a bad path");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js
new file mode 100644
index 0000000000..b85e4b19fc
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can delete heap snapshots.
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ ok(true, "Should have computed the dominator tree");
+
+ await client.deleteHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have deleted the snapshot");
+
+ let threw = false;
+ try {
+ await client.getDominatorTree({
+ dominatorTreeId,
+ breakdown,
+ });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "getDominatorTree on deleted tree should throw an error");
+
+ threw = false;
+ try {
+ await client.computeDominatorTree(snapshotFilePath);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "computeDominatorTree on deleted snapshot should throw an error");
+
+ threw = false;
+ try {
+ await client.takeCensus(snapshotFilePath);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "takeCensus on deleted tree should throw an error");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js
new file mode 100644
index 0000000000..6960081afd
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test deleteHeapSnapshot is a noop if the provided path matches no snapshot
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ let threw = false;
+ try {
+ await client.deleteHeapSnapshot("path-does-not-exist");
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "deleteHeapSnapshot on non-existant path should throw an error");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js
new file mode 100644
index 0000000000..c84622f633
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test other dominatorTrees can still be retrieved after deleting a snapshot
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+async function createSnapshotAndDominatorTree(client) {
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ return { dominatorTreeId, snapshotFilePath };
+}
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const savedSnapshots = [
+ await createSnapshotAndDominatorTree(client),
+ await createSnapshotAndDominatorTree(client),
+ await createSnapshotAndDominatorTree(client),
+ ];
+ ok(true, "Create 3 snapshots and dominator trees");
+
+ await client.deleteHeapSnapshot(savedSnapshots[1].snapshotFilePath);
+ ok(true, "Snapshot deleted");
+
+ let tree = await client.getDominatorTree({
+ dominatorTreeId: savedSnapshots[0].dominatorTreeId,
+ breakdown,
+ });
+ ok(tree, "Should get a valid tree for first snapshot");
+
+ let threw = false;
+ try {
+ await client.getDominatorTree({
+ dominatorTreeId: savedSnapshots[1].dominatorTreeId,
+ breakdown,
+ });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "getDominatorTree on a deleted snapshot should throw an error");
+
+ tree = await client.getDominatorTree({
+ dominatorTreeId: savedSnapshots[2].dominatorTreeId,
+ breakdown,
+ });
+ ok(tree, "Should get a valid tree for third snapshot");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js
new file mode 100644
index 0000000000..2cbd9cc325
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can get census individuals.
+
+const COUNT = { by: "count", count: true, bytes: true };
+
+const CENSUS_BREAKDOWN = {
+ by: "coarseType",
+ objects: COUNT,
+ strings: COUNT,
+ scripts: COUNT,
+ other: COUNT,
+ domNode: COUNT,
+};
+
+const LABEL_BREAKDOWN = {
+ by: "internalType",
+ then: COUNT,
+};
+
+const MAX_INDIVIDUALS = 10;
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ ok(true, "Should have computed dominator tree");
+
+ const { report } = await client.takeCensus(
+ snapshotFilePath,
+ { breakdown: CENSUS_BREAKDOWN },
+ { asTreeNode: true }
+ );
+ ok(report, "Should get a report");
+
+ let nodesWithLeafIndicesFound = 0;
+
+ await (async function assertCanGetIndividuals(censusNode) {
+ if (censusNode.reportLeafIndex !== undefined) {
+ nodesWithLeafIndicesFound++;
+
+ const response = await client.getCensusIndividuals({
+ dominatorTreeId,
+ indices: DevToolsUtils.isSet(censusNode.reportLeafIndex)
+ ? censusNode.reportLeafIndex
+ : new Set([censusNode.reportLeafIndex]),
+ censusBreakdown: CENSUS_BREAKDOWN,
+ labelBreakdown: LABEL_BREAKDOWN,
+ maxRetainingPaths: 1,
+ maxIndividuals: MAX_INDIVIDUALS,
+ });
+
+ dumpn(`response = ${JSON.stringify(response, null, 4)}`);
+
+ equal(
+ response.nodes.length,
+ Math.min(MAX_INDIVIDUALS, censusNode.count),
+ "response.nodes.length === Math.min(MAX_INDIVIDUALS, censusNode.count)"
+ );
+
+ let lastRetainedSize = Infinity;
+ for (const individual of response.nodes) {
+ equal(
+ typeof individual.nodeId,
+ "number",
+ "individual.nodeId should be a number"
+ );
+ ok(
+ individual.retainedSize <= lastRetainedSize,
+ "individual.retainedSize <= lastRetainedSize"
+ );
+ lastRetainedSize = individual.retainedSize;
+ ok(
+ individual.shallowSize,
+ "individual.shallowSize should exist and be non-zero"
+ );
+ ok(individual.shortestPaths, "individual.shortestPaths should exist");
+ ok(
+ individual.shortestPaths.nodes,
+ "individual.shortestPaths.nodes should exist"
+ );
+ ok(
+ individual.shortestPaths.edges,
+ "individual.shortestPaths.edges should exist"
+ );
+ ok(individual.label, "individual.label should exist");
+ }
+ }
+
+ if (censusNode.children) {
+ for (const child of censusNode.children) {
+ await assertCanGetIndividuals(child);
+ }
+ }
+ })(report);
+
+ equal(
+ nodesWithLeafIndicesFound,
+ 4,
+ "Should have found a leaf for each coarse type"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js
new file mode 100644
index 0000000000..505e1a1dc6
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can get a HeapSnapshot's
+// creation time.
+
+function waitForThirtyMilliseconds() {
+ const start = Date.now();
+ while (Date.now() - start < 30) {
+ // do nothing
+ }
+}
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+ const start = Date.now() * 1000;
+
+ // Because Date.now() is less precise than the snapshot's time stamp, give it
+ // a little bit of head room. Additionally, WinXP's timers have a granularity
+ // of only +/-15 ms.
+ waitForThirtyMilliseconds();
+ const snapshotFilePath = saveNewHeapSnapshot();
+ waitForThirtyMilliseconds();
+ const end = Date.now() * 1000;
+
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ let threw = false;
+ try {
+ await client.getCreationTime("/not/a/real/path", {
+ breakdown: BREAKDOWN,
+ });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "getCreationTime should throw when snapshot does not exist");
+
+ const time = await client.getCreationTime(snapshotFilePath, {
+ breakdown: BREAKDOWN,
+ });
+
+ dumpn("Start = " + start);
+ dumpn("End = " + end);
+ dumpn("Time = " + time);
+
+ ok(time >= start, "creation time occurred after start");
+ ok(time <= end, "creation time occurred before end");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js
new file mode 100644
index 0000000000..9035624ca2
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request.
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+ equal(
+ typeof dominatorTreeId,
+ "number",
+ "should get a dominator tree id, and it should be a number"
+ );
+
+ const partialTree = await client.getDominatorTree({
+ dominatorTreeId,
+ breakdown,
+ });
+ ok(partialTree, "Should get a partial tree");
+ equal(typeof partialTree, "object", "partialTree should be an object");
+
+ function checkTree(node) {
+ equal(typeof node.nodeId, "number", "each node should have an id");
+
+ if (node === partialTree) {
+ equal(node.parentId, undefined, "the root has no parent");
+ } else {
+ equal(
+ typeof node.parentId,
+ "number",
+ "each node should have a parent id"
+ );
+ }
+
+ equal(
+ typeof node.retainedSize,
+ "number",
+ "each node should have a retained size"
+ );
+
+ ok(
+ node.children === undefined || Array.isArray(node.children),
+ "each node either has a list of children, " +
+ "or undefined meaning no children loaded"
+ );
+ equal(
+ typeof node.moreChildrenAvailable,
+ "boolean",
+ "each node should indicate if there are more children available or not"
+ );
+
+ equal(typeof node.shortestPaths, "object", "Should have shortest paths");
+ equal(
+ typeof node.shortestPaths.nodes,
+ "object",
+ "Should have shortest paths' nodes"
+ );
+ equal(
+ typeof node.shortestPaths.edges,
+ "object",
+ "Should have shortest paths' edges"
+ );
+
+ if (node.children) {
+ node.children.forEach(checkTree);
+ }
+ }
+
+ checkTree(partialTree);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js
new file mode 100644
index 0000000000..83a7cbbd3d
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request with bad
+// dominator tree ids.
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ let threw = false;
+ try {
+ await client.getDominatorTree({ dominatorTreeId: 42, breakdown });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "should throw when given a bad id");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js
new file mode 100644
index 0000000000..47860870cb
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the HeapAnalyses{Client,Worker} "getImmediatelyDominated" request.
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath);
+
+ const partialTree = await client.getDominatorTree({
+ dominatorTreeId,
+ breakdown,
+ });
+ ok(
+ !!partialTree.children.length,
+ "root should immediately dominate some nodes"
+ );
+
+ // First, test getting a subset of children available.
+ const response = await client.getImmediatelyDominated({
+ dominatorTreeId,
+ breakdown,
+ nodeId: partialTree.nodeId,
+ startIndex: 0,
+ maxCount: partialTree.children.length - 1,
+ });
+
+ ok(Array.isArray(response.nodes));
+ ok(response.nodes.every(node => node.parentId === partialTree.nodeId));
+ ok(response.moreChildrenAvailable);
+ equal(response.path.length, 1);
+ equal(response.path[0], partialTree.nodeId);
+
+ for (const node of response.nodes) {
+ equal(typeof node.shortestPaths, "object", "Should have shortest paths");
+ equal(
+ typeof node.shortestPaths.nodes,
+ "object",
+ "Should have shortest paths' nodes"
+ );
+ equal(
+ typeof node.shortestPaths.edges,
+ "object",
+ "Should have shortest paths' edges"
+ );
+ }
+
+ // Next, test getting a subset of children available.
+ const secondResponse = await client.getImmediatelyDominated({
+ dominatorTreeId,
+ breakdown,
+ nodeId: partialTree.nodeId,
+ startIndex: 0,
+ maxCount: Infinity,
+ });
+
+ ok(Array.isArray(secondResponse.nodes));
+ ok(secondResponse.nodes.every(node => node.parentId === partialTree.nodeId));
+ ok(!secondResponse.moreChildrenAvailable);
+ equal(secondResponse.path.length, 1);
+ equal(secondResponse.path[0], partialTree.nodeId);
+
+ for (const node of secondResponse.nodes) {
+ equal(typeof node.shortestPaths, "object", "Should have shortest paths");
+ equal(
+ typeof node.shortestPaths.nodes,
+ "object",
+ "Should have shortest paths' nodes"
+ );
+ equal(
+ typeof node.shortestPaths.edges,
+ "object",
+ "Should have shortest paths' edges"
+ );
+ }
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js
new file mode 100644
index 0000000000..dea8269a92
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can read heap snapshots.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js
new file mode 100644
index 0000000000..9703229438
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses.
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: false },
+ other: { by: "count", count: true, bytes: false },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const markers = [allocationMarker()];
+
+ const firstSnapshotFilePath = saveNewHeapSnapshot();
+
+ // Allocate and hold an additional AllocationMarker object so we can see it in
+ // the next heap snapshot.
+ markers.push(allocationMarker());
+
+ const secondSnapshotFilePath = saveNewHeapSnapshot();
+
+ await client.readHeapSnapshot(firstSnapshotFilePath);
+ await client.readHeapSnapshot(secondSnapshotFilePath);
+ ok(true, "Should have read both heap snapshot files");
+
+ const { delta } = await client.takeCensusDiff(
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN }
+ );
+
+ equal(
+ delta.AllocationMarker.count,
+ 1,
+ "There exists one new AllocationMarker in the second heap snapshot"
+ );
+
+ const { delta: deltaTreeNode } = await client.takeCensusDiff(
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN },
+ { asTreeNode: true }
+ );
+
+ // Have to manually set these because symbol properties aren't structured
+ // cloned.
+ delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes;
+ delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount;
+
+ compareCensusViewData(
+ BREAKDOWN,
+ delta,
+ deltaTreeNode,
+ "Returning delta-census as a tree node represents same data as the report"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js
new file mode 100644
index 0000000000..d5d988f78f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses as
+// inverted trees.
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+add_task(async function () {
+ const firstSnapshotFilePath = saveNewHeapSnapshot();
+ const secondSnapshotFilePath = saveNewHeapSnapshot();
+
+ const client = new HeapAnalysesClient();
+ await client.readHeapSnapshot(firstSnapshotFilePath);
+ await client.readHeapSnapshot(secondSnapshotFilePath);
+
+ ok(true, "Should have read both heap snapshot files");
+
+ const { delta } = await client.takeCensusDiff(
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN }
+ );
+
+ const { delta: deltaTreeNode } = await client.takeCensusDiff(
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN },
+ { asInvertedTreeNode: true }
+ );
+
+ // Have to manually set these because symbol properties aren't structured
+ // cloned.
+ delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes;
+ delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount;
+
+ compareCensusViewData(BREAKDOWN, delta, deltaTreeNode, { invert: true });
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js
new file mode 100644
index 0000000000..55da2bf4b8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath);
+ ok(report, "Should get a report");
+ equal(typeof report, "object", "report should be an object");
+
+ ok(report.objects);
+ ok(report.scripts);
+ ok(report.strings);
+ ok(report.other);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js
new file mode 100644
index 0000000000..5b1dcefe03
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses with breakdown
+// options.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: { by: "count", count: true, bytes: true },
+ });
+
+ ok(report, "Should get a report");
+ equal(typeof report, "object", "report should be an object");
+
+ ok(report.count);
+ ok(report.bytes);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js
new file mode 100644
index 0000000000..0dfda73f1f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} bubbles errors properly when things
+// go wrong.
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ // Snapshot file path to a file that doesn't exist.
+ let failed = false;
+ try {
+ await client.readHeapSnapshot(
+ getFilePath("foo-bar-baz" + Math.random(), true)
+ );
+ } catch (e) {
+ failed = true;
+ }
+ ok(failed, "should not read heap snapshots that do not exist");
+
+ // Snapshot file path to a file that is not a heap snapshot.
+ failed = false;
+ try {
+ await client.readHeapSnapshot(
+ getFilePath("test_HeapAnalyses_takeCensus_03.js")
+ );
+ } catch (e) {
+ failed = true;
+ }
+ ok(
+ failed,
+ "should not be able to read a file " +
+ "that is not a heap snapshot as a heap snapshot"
+ );
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ // Bad census breakdown options.
+ failed = false;
+ try {
+ await client.takeCensus(snapshotFilePath, {
+ breakdown: { by: "some classification that we do not have" },
+ });
+ } catch (e) {
+ failed = true;
+ }
+ ok(failed, "should not be able to breakdown by an unknown classification");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js
new file mode 100644
index 0000000000..26f78ad4f7
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can send SavedFrame stacks from
+// by-allocation-stack reports from the worker.
+
+add_task(async function test() {
+ const client = new HeapAnalysesClient();
+
+ // Track some allocation stacks.
+
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+ g.eval(` // 1
+ this.log = []; // 2
+ function f() { this.log.push(allocationMarker()); } // 3
+ function g() { this.log.push(allocationMarker()); } // 4
+ function h() { this.log.push(allocationMarker()); } // 5
+ `);
+
+ // Create one allocationMarker with tracking turned off,
+ // so it will have no associated stack.
+ g.f();
+
+ dbg.memory.allocationSamplingProbability = 1;
+
+ for (const [func, n] of [
+ [g.f, 20],
+ [g.g, 10],
+ [g.h, 5],
+ ]) {
+ for (let i = 0; i < n; i++) {
+ dbg.memory.trackingAllocationSites = true;
+ // All allocations of allocationMarker occur with this line as the oldest
+ // stack frame.
+ func();
+ dbg.memory.trackingAllocationSites = false;
+ }
+ }
+
+ // Take a heap snapshot.
+
+ const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg });
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ // Run a census broken down by class name -> allocation stack so we can grab
+ // only the AllocationMarker objects we have complete control over.
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: {
+ by: "count",
+ bytes: true,
+ count: true,
+ },
+ noStack: {
+ by: "count",
+ bytes: true,
+ count: true,
+ },
+ },
+ },
+ });
+
+ // Test the generated report.
+
+ ok(report, "Should get a report");
+
+ const map = report.AllocationMarker;
+ ok(map, "Should get AllocationMarkers in the report.");
+ // From a module with a different global, and therefore a different Map
+ // constructor, so we can't use instanceof.
+ equal(Object.getPrototypeOf(map).constructor.name, "Map");
+
+ equal(
+ map.size,
+ 4,
+ "Should have 4 allocation stacks (including the lack of a stack)"
+ );
+
+ // Gather the stacks we are expecting to appear as keys, and
+ // check that there are no unexpected keys.
+ const stacks = {};
+
+ map.forEach((v, k) => {
+ if (k === "noStack") {
+ // No need to save this key.
+ } else if (
+ k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "test"
+ ) {
+ stacks.f = k;
+ } else if (
+ k.functionDisplayName === "g" &&
+ k.parent.functionDisplayName === "test"
+ ) {
+ stacks.g = k;
+ } else if (
+ k.functionDisplayName === "h" &&
+ k.parent.functionDisplayName === "test"
+ ) {
+ stacks.h = k;
+ } else {
+ dumpn("Unexpected allocation stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+ ok(false);
+ }
+ });
+
+ ok(map.get("noStack"));
+ equal(map.get("noStack").count, 1);
+
+ ok(stacks.f);
+ ok(map.get(stacks.f));
+ equal(map.get(stacks.f).count, 20);
+
+ ok(stacks.g);
+ ok(map.get(stacks.g));
+ equal(map.get(stacks.g).count, 10);
+
+ ok(stacks.h);
+ ok(map.get(stacks.h));
+ equal(map.get(stacks.h).count, 5);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js
new file mode 100644
index 0000000000..951a3b3133
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses and return
+// a CensusTreeNode.
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN,
+ });
+
+ const { report: treeNode } = await client.takeCensus(
+ snapshotFilePath,
+ {
+ breakdown: BREAKDOWN,
+ },
+ {
+ asTreeNode: true,
+ }
+ );
+
+ ok(!!treeNode.children.length, "treeNode has children");
+ ok(
+ treeNode.children.every(type => {
+ return "name" in type && "bytes" in type && "count" in type;
+ }),
+ "all of tree node's children have name, bytes, count"
+ );
+
+ compareCensusViewData(
+ BREAKDOWN,
+ report,
+ treeNode,
+ "Returning census as a tree node represents same data as the report"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js
new file mode 100644
index 0000000000..d9fdaa3708
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses by
+// "allocationStack" and return a CensusTreeNode.
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+ },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(async function () {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ // 5 allocation markers with no stack.
+ g.eval(`
+ this.markers = [];
+ for (var i = 0; i < 5; i++) {
+ markers.push(allocationMarker());
+ }
+ `);
+
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+
+ // 5 allocation markers at 5 stacks.
+ g.eval(`
+ (function shouldHaveCountOfOne() {
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ }());
+ `);
+
+ // 5 allocation markers at 1 stack.
+ g.eval(`
+ (function shouldHaveCountOfFive() {
+ for (var i = 0; i < 5; i++) {
+ markers.push(allocationMarker());
+ }
+ }());
+ `);
+
+ const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg });
+
+ const client = new HeapAnalysesClient();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN,
+ });
+
+ const { report: treeNode } = await client.takeCensus(
+ snapshotFilePath,
+ {
+ breakdown: BREAKDOWN,
+ },
+ {
+ asTreeNode: true,
+ }
+ );
+
+ const markers = treeNode.children.find(c => c.name === "AllocationMarker");
+ ok(markers);
+
+ const noStack = markers.children.find(c => c.name === "noStack");
+ equal(noStack.count, 5);
+
+ let numShouldHaveFiveFound = 0;
+ let numShouldHaveOneFound = 0;
+
+ function walk(node) {
+ if (node.children) {
+ node.children.forEach(walk);
+ }
+
+ if (!isSavedFrame(node.name)) {
+ return;
+ }
+
+ if (node.name.functionDisplayName === "shouldHaveCountOfFive") {
+ equal(node.count, 5, "shouldHaveCountOfFive should have count of five");
+ numShouldHaveFiveFound++;
+ }
+
+ if (node.name.functionDisplayName === "shouldHaveCountOfOne") {
+ equal(node.count, 1, "shouldHaveCountOfOne should have count of one");
+ numShouldHaveOneFound++;
+ }
+ }
+ markers.children.forEach(walk);
+
+ equal(numShouldHaveFiveFound, 1);
+ equal(numShouldHaveOneFound, 5);
+
+ compareCensusViewData(
+ BREAKDOWN,
+ report,
+ treeNode,
+ "Returning census as a tree node represents same data as the report"
+ );
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js
new file mode 100644
index 0000000000..3f603122d3
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses and return
+// an inverted CensusTreeNode.
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+add_task(async function () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ await client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = await client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN,
+ });
+
+ const { report: treeNode } = await client.takeCensus(
+ snapshotFilePath,
+ {
+ breakdown: BREAKDOWN,
+ },
+ {
+ asInvertedTreeNode: true,
+ }
+ );
+
+ compareCensusViewData(BREAKDOWN, report, treeNode, { invert: true });
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js
new file mode 100644
index 0000000000..47d974050b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Sanity test that we can compute shortest paths.
+//
+// Because the actual heap graph is too unpredictable and likely to drastically
+// change as various implementation bits change, we don't test exact paths
+// here. See js/src/jsapi-tests/testUbiNode.cpp for such tests, where we can
+// control the specific graph shape and structure and so testing exact paths is
+// reliable.
+
+function run_test() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ const dominatedByRoot = dominatorTree
+ .getImmediatelyDominated(dominatorTree.root)
+ .slice(0, 10);
+ ok(dominatedByRoot);
+ ok(dominatedByRoot.length);
+
+ const targetSet = new Set(dominatedByRoot);
+
+ const shortestPaths = snapshot.computeShortestPaths(
+ dominatorTree.root,
+ dominatedByRoot,
+ 2
+ );
+ ok(shortestPaths);
+ ok(shortestPaths instanceof Map);
+ ok(shortestPaths.size === targetSet.size);
+
+ for (const [target, paths] of shortestPaths) {
+ ok(targetSet.has(target), "We should only get paths for our targets");
+ targetSet.delete(target);
+
+ ok(
+ !!paths.length,
+ "We must have at least one path, since the target is dominated by the root"
+ );
+ ok(
+ paths.length <= 2,
+ "Should not have recorded more paths than the max requested"
+ );
+
+ dumpn("---------------------");
+ dumpn("Shortest paths for 0x" + target.toString(16) + ":");
+ for (const pth of paths) {
+ dumpn(" path =");
+ for (const part of pth) {
+ dumpn(
+ " predecessor: 0x" +
+ part.predecessor.toString(16) +
+ "; edge: " +
+ part.edge
+ );
+ }
+ }
+ dumpn("---------------------");
+
+ for (const path2 of paths) {
+ ok(!!path2.length, "Cannot have zero length paths");
+ ok(
+ path2[0].predecessor === dominatorTree.root,
+ "The first predecessor is always our start node"
+ );
+
+ for (const part of path2) {
+ ok(part.predecessor, "Each part of a path has a predecessor");
+ ok(
+ !!snapshot.describeNode(
+ { by: "count", count: true, bytes: true },
+ part.predecessor
+ ),
+ "The predecessor is in the heap snapshot"
+ );
+ ok("edge" in part, "Each part has an (potentially null) edge property");
+ }
+ }
+ }
+
+ ok(targetSet.size === 0, "We found paths for all of our targets");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js
new file mode 100644
index 0000000000..714986c601
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test computing shortest paths with invalid arguments.
+
+function run_test() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ const target = dominatorTree
+ .getImmediatelyDominated(dominatorTree.root)
+ .pop();
+ ok(target);
+
+ let threw = false;
+ try {
+ snapshot.computeShortestPaths(0, [target], 2);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "invalid start node should throw");
+
+ threw = false;
+ try {
+ snapshot.computeShortestPaths(dominatorTree.root, [0], 2);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "invalid target nodes should throw");
+
+ threw = false;
+ try {
+ snapshot.computeShortestPaths(dominatorTree.root, [], 2);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "empty target nodes should throw");
+
+ threw = false;
+ try {
+ snapshot.computeShortestPaths(dominatorTree.root, [target], 0);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "0 max paths should throw");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js
new file mode 100644
index 0000000000..490ef1d6d0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.creationTime returns the expected time.
+
+function waitForThirtyMilliseconds() {
+ const start = Date.now();
+ while (Date.now() - start < 30) {
+ // do nothing
+ }
+}
+
+function run_test() {
+ const start = Date.now() * 1000;
+ info("start = " + start);
+
+ // Because Date.now() is less precise than the snapshot's time stamp, give it
+ // a little bit of head room. Additionally, WinXP's timer only has granularity
+ // of +/- 15ms.
+ waitForThirtyMilliseconds();
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ waitForThirtyMilliseconds();
+
+ const end = Date.now() * 1000;
+ info("end = " + end);
+
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ info("snapshot.creationTime = " + snapshot.creationTime);
+
+ ok(snapshot.creationTime >= start);
+ ok(snapshot.creationTime <= end);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js
new file mode 100644
index 0000000000..9c59d60fc8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can save a core dump with very deep allocation stacks and read
+// it back into a HeapSnapshot.
+
+function stackDepth(stack) {
+ return stack ? 1 + stackDepth(stack.parent) : 0;
+}
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ Services.prefs.setBoolPref("security.allow_eval_in_parent_process", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ Services.prefs.clearUserPref("security.allow_eval_in_parent_process");
+ });
+
+ // Create a Debugger observing a debuggee's allocations.
+ const debuggee = new Cu.Sandbox(null);
+ const dbg = new Debugger(debuggee);
+ dbg.memory.trackingAllocationSites = true;
+
+ // Allocate some objects in the debuggee that will have their allocation
+ // stacks recorded by the Debugger.
+
+ debuggee.eval("this.objects = []");
+ debuggee.eval(
+ function recursiveAllocate(n) {
+ if (n <= 0) {
+ return;
+ }
+
+ // Make sure to recurse before pushing the object so that when TCO is
+ // implemented sometime in the future, it doesn't invalidate this test.
+ recursiveAllocate(n - 1);
+ this.objects.push({});
+ }.toString()
+ );
+ debuggee.eval("recursiveAllocate = recursiveAllocate.bind(this);");
+ debuggee.eval("recursiveAllocate(200);");
+
+ // Now save a snapshot that will include the allocation stacks and read it
+ // back again.
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot");
+
+ const report = snapshot.takeCensus({
+ breakdown: {
+ by: "allocationStack",
+ then: { by: "count", bytes: true, count: true },
+ noStack: { by: "count", bytes: true, count: true },
+ },
+ });
+
+ // Keep this synchronized with `HeapSnapshot::MAX_STACK_DEPTH`!
+ const MAX_STACK_DEPTH = 60;
+
+ let foundStacks = false;
+ report.forEach((v, k) => {
+ if (k === "noStack") {
+ return;
+ }
+
+ foundStacks = true;
+ const depth = stackDepth(k);
+ dumpn("Stack depth is " + depth);
+ ok(
+ depth <= MAX_STACK_DEPTH,
+ "Every stack should have depth less than or equal to the maximum stack depth"
+ );
+ });
+ ok(foundStacks);
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js
new file mode 100644
index 0000000000..8597fb4edf
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can describe nodes with a breakdown.
+
+function run_test() {
+ const path = saveNewHeapSnapshot();
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ ok(snapshot.describeNode);
+ equal(typeof snapshot.describeNode, "function");
+
+ const dt = snapshot.computeDominatorTree();
+
+ let threw = false;
+ try {
+ snapshot.describeNode(undefined, dt.root);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "Should require a breakdown");
+
+ const breakdown = {
+ by: "coarseType",
+ objects: { by: "objectClass" },
+ scripts: { by: "internalType" },
+ strings: { by: "internalType" },
+ other: { by: "internalType" },
+ };
+
+ threw = false;
+ try {
+ snapshot.describeNode(breakdown, 0);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "Should throw when given an invalid node id");
+
+ const description = snapshot.describeNode(breakdown, dt.root);
+ ok(description);
+ ok(description.other);
+ ok(description.other["JS::ubi::RootList"]);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js
new file mode 100644
index 0000000000..058f8deba8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test ChromeUtils.getObjectNodeId()
+
+function run_test() {
+ // Create a test object, which we want to analyse
+ const testObject = {
+ foo: {
+ bar: {},
+ },
+ };
+
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ // Get the NodeId for our test object
+ const objectNodeIdRoot = ChromeUtils.getObjectNodeId(testObject);
+ const objectNodeIdFoo = ChromeUtils.getObjectNodeId(testObject.foo);
+ const objectNodeIdBar = ChromeUtils.getObjectNodeId(testObject.foo.bar);
+
+ // Also try to ensure that this is the right object via its retained path
+ const shortestPaths = snapshot.computeShortestPaths(
+ objectNodeIdRoot,
+ [objectNodeIdBar],
+ 50
+ );
+ ok(shortestPaths);
+ ok(shortestPaths instanceof Map);
+ ok(
+ shortestPaths.size == 1,
+ "We get only one path between the root object and bar object"
+ );
+
+ const paths = shortestPaths.get(objectNodeIdBar);
+ ok(paths.length == 1, "There is only one path between root and bar");
+ ok(
+ paths[0].length == 2,
+ "The shortest path is made of two edges: foo and bar"
+ );
+
+ const [path1, path2] = paths[0];
+ ok(
+ path1.predecessor == objectNodeIdRoot,
+ "The first edge goes from the root object"
+ );
+ ok(path1.edge == "foo", "The first edge is the foo attribute");
+
+ ok(
+ path2.predecessor == objectNodeIdFoo,
+ "The second edge goes from the foo object"
+ );
+ ok(path2.edge == "bar", "The first edge is the bar attribute");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js
new file mode 100644
index 0000000000..f4a7836be3
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus returns a value of an appropriate
+// shape. Ported from js/src/jit-tests/debug/Memory-takeCensus-01.js
+
+function run_test() {
+ const dbg = new Debugger();
+
+ function checkProperties(census) {
+ equal(typeof census, "object");
+ for (const prop of Object.getOwnPropertyNames(census)) {
+ const desc = Object.getOwnPropertyDescriptor(census, prop);
+ equal(desc.enumerable, true);
+ equal(desc.configurable, true);
+ equal(desc.writable, true);
+ if (typeof desc.value === "object") {
+ checkProperties(desc.value);
+ } else {
+ equal(typeof desc.value, "number");
+ }
+ }
+ }
+
+ checkProperties(saveHeapSnapshotAndTakeCensus(dbg));
+
+ const g = newGlobal();
+ dbg.addDebuggee(g);
+ checkProperties(saveHeapSnapshotAndTakeCensus(dbg));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js
new file mode 100644
index 0000000000..a26e9a96d8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus behaves plausibly as we allocate objects.
+//
+// Exact object counts vary in ways we can't predict. For example,
+// BaselineScripts can hold onto "template objects", which exist only to hold
+// the shape and type for newly created objects. When BaselineScripts are
+// discarded, these template objects go with them.
+//
+// So instead of expecting precise counts, we expect counts that are at least as
+// many as we would expect given the object graph we've built.
+//
+// Ported from js/src/jit-tests/debug/Memory-takeCensus-02.js
+
+function run_test() {
+ // A Debugger with no debuggees had better not find anything.
+ const dbg = new Debugger();
+ const census0 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census0, "census0", Census.assertAllZeros);
+
+ function newGlobalWithDefs() {
+ const g = newGlobal();
+ g.eval(`
+ function times(n, fn) {
+ var a=[];
+ for (var i = 0; i<n; i++)
+ a.push(fn());
+ return a;
+ }
+ `);
+ return g;
+ }
+
+ // Allocate a large number of various types of objects, and check that census
+ // finds them.
+ const g = newGlobalWithDefs();
+ dbg.addDebuggee(g);
+
+ g.eval("var objs = times(100, () => ({}));");
+ g.eval("var rxs = times(200, () => /foo/);");
+ g.eval("var ars = times(400, () => []);");
+ g.eval("var fns = times(800, () => () => {});");
+
+ const census1 = dbg.memory.takeCensus(dbg);
+ Census.walkCensus(
+ census1,
+ "census1",
+ Census.assertAllNotLessThan({
+ objects: {
+ Object: { count: 100 },
+ RegExp: { count: 200 },
+ Array: { count: 400 },
+ Function: { count: 800 },
+ },
+ })
+ );
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js
new file mode 100644
index 0000000000..06c574d4e0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus behaves plausibly as we add and remove
+// debuggees.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-03.js
+
+function run_test() {
+ const dbg = new Debugger();
+
+ const census0 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census0, "census0", Census.assertAllZeros);
+
+ const g1 = newGlobal();
+ dbg.addDebuggee(g1);
+ const census1 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census1, "census1", Census.assertAllNotLessThan(census0));
+
+ const g2 = newGlobal();
+ dbg.addDebuggee(g2);
+ const census2 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census2, "census2", Census.assertAllNotLessThan(census1));
+
+ dbg.removeDebuggee(g2);
+ const census3 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census3, "census3", Census.assertAllEqual(census1));
+
+ dbg.removeDebuggee(g1);
+ const census4 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census4, "census4", Census.assertAllEqual(census0));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js
new file mode 100644
index 0000000000..ac484c8815
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that HeapSnapshot.prototype.takeCensus finds GC roots that are on the
+// stack.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-04.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ g.eval(`
+function withAllocationMarkerOnStack(f) {
+ (function () {
+ var onStack = allocationMarker();
+ f();
+ }());
+}
+`);
+
+ equal(
+ "AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects,
+ false,
+ "There shouldn't exist any allocation markers in the census."
+ );
+
+ let allocationMarkerCount;
+ g.withAllocationMarkerOnStack(() => {
+ const census = saveHeapSnapshotAndTakeCensus(dbg);
+ allocationMarkerCount = census.objects.AllocationMarker.count;
+ });
+
+ equal(
+ allocationMarkerCount,
+ 1,
+ "Should have one allocation marker in the census, because there " +
+ "was one on the stack."
+ );
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js
new file mode 100644
index 0000000000..22c5324c68
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that HeapSnapshot.prototype.takeCensus finds cross compartment
+// wrapper GC roots.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-05.js
+
+/* eslint-disable strict */
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ equal(
+ "AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects,
+ false,
+ "No allocation markers should exist in the census."
+ );
+
+ this.ccw = g.allocationMarker();
+
+ const census = saveHeapSnapshotAndTakeCensus(dbg);
+ equal(
+ census.objects.AllocationMarker.count,
+ 1,
+ "Should have one allocation marker in the census, because there " +
+ "is one cross-compartment wrapper referring to it."
+ );
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js
new file mode 100644
index 0000000000..2b639411f9
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Check HeapSnapshot.prototype.takeCensus handling of 'breakdown' argument.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-06.js
+
+function run_test() {
+ const Pattern = Match.Pattern;
+
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ Pattern({ count: Pattern.NATURAL, bytes: Pattern.NATURAL }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count" } })
+ );
+
+ let census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", count: false, bytes: false },
+ });
+ equal("count" in census, false);
+ equal("bytes" in census, false);
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", count: true, bytes: false },
+ });
+ equal("count" in census, true);
+ equal("bytes" in census, false);
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", count: false, bytes: true },
+ });
+ equal("count" in census, false);
+ equal("bytes" in census, true);
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", count: true, bytes: true },
+ });
+ equal("count" in census, true);
+ equal("bytes" in census, true);
+
+ // Pattern doesn't mind objects with extra properties, so we'll restrict this
+ // list to the object classes we're pretty sure are going to stick around for
+ // the forseeable future.
+ Pattern({
+ Function: { count: Pattern.NATURAL },
+ Object: { count: Pattern.NATURAL },
+ DebuggerPrototype: { count: Pattern.NATURAL },
+ Sandbox: { count: Pattern.NATURAL },
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass" } })
+ );
+
+ Pattern({
+ objects: { count: Pattern.NATURAL },
+ scripts: { count: Pattern.NATURAL },
+ strings: { count: Pattern.NATURAL },
+ other: { count: Pattern.NATURAL },
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "coarseType" } })
+ );
+
+ // As for { by: 'objectClass' }, restrict our pattern to the types
+ // we predict will stick around for a long time.
+ Pattern({
+ JSString: { count: Pattern.NATURAL },
+ "js::Shape": { count: Pattern.NATURAL },
+ JSObject: { count: Pattern.NATURAL },
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "internalType" } })
+ );
+
+ // Nested breakdowns.
+
+ const coarseTypePattern = {
+ objects: { count: Pattern.NATURAL },
+ scripts: { count: Pattern.NATURAL },
+ strings: { count: Pattern.NATURAL },
+ other: { count: Pattern.NATURAL },
+ };
+
+ Pattern({
+ JSString: coarseTypePattern,
+ "js::Shape": coarseTypePattern,
+ JSObject: coarseTypePattern,
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "internalType", then: { by: "coarseType" } },
+ })
+ );
+
+ Pattern({
+ Function: { count: Pattern.NATURAL },
+ Object: { count: Pattern.NATURAL },
+ DebuggerPrototype: { count: Pattern.NATURAL },
+ Sandbox: { count: Pattern.NATURAL },
+ other: coarseTypePattern,
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "objectClass",
+ then: { by: "count" },
+ other: { by: "coarseType" },
+ },
+ })
+ );
+
+ Pattern({
+ objects: { count: Pattern.NATURAL, label: "object" },
+ scripts: { count: Pattern.NATURAL, label: "scripts" },
+ strings: { count: Pattern.NATURAL, label: "strings" },
+ other: { count: Pattern.NATURAL, label: "other" },
+ }).assert(
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ objects: { by: "count", label: "object" },
+ scripts: { by: "count", label: "scripts" },
+ strings: { by: "count", label: "strings" },
+ other: { by: "count", label: "other" },
+ },
+ })
+ );
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js
new file mode 100644
index 0000000000..56bec51074
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus breakdown: check error handling on property
+// gets.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-07.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ get by() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "count",
+ get count() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "count",
+ get bytes() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "objectClass",
+ get then() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "objectClass",
+ get other() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ get objects() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ get scripts() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ get strings() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ get other() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ assertThrows(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "internalType",
+ get then() {
+ throw Error("ಠ_ಠ");
+ },
+ },
+ });
+ }, "ಠ_ಠ");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js
new file mode 100644
index 0000000000..f40d367cd6
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus: test by: 'count' breakdown
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-08.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ g.eval(`
+ var stuff = [];
+ function add(n, c) {
+ for (let i = 0; i < n; i++)
+ stuff.push(c());
+ }
+
+ let count = 0;
+
+ function obj() { return { count: count++ }; }
+ obj.factor = 1;
+
+ // This creates a closure (a function JSObject) that has captured
+ // a Call object. So each call creates two items.
+ function fun() { let v = count; return () => { return v; } }
+ fun.factor = 2;
+
+ function str() { return 'perambulator' + count++; }
+ str.factor = 1;
+
+ // Eval a fresh text each time, allocating:
+ // - a fresh ScriptSourceObject
+ // - a new JSScripts, not an eval cache hits
+ // - a fresh prototype object
+ // - a fresh Call object, since the eval makes 'ev' heavyweight
+ // - the new function itself
+ function ev() {
+ return eval(\`(function () { return \${ count++ } })\`);
+ }
+ ev.factor = 5;
+
+ // A new object (1) with a new shape (2) with a new atom (3)
+ function shape() { return { [ 'theobroma' + count++ ]: count }; }
+ shape.factor = 3;
+ `);
+
+ let baseline = 0;
+ function countIncreasedByAtLeast(n) {
+ const oldBaseline = baseline;
+
+ // Since a census counts only reachable objects, one might assume that calling
+ // GC here would have no effect on the census results. But GC also throws away
+ // JIT code and any objects it might be holding (template objects, say);
+ // takeCensus reaches those. Shake everything loose that we can, to make the
+ // census approximate reachability a bit more closely, and make our results a
+ // bit more predictable.
+ gc(g, "shrinking");
+
+ baseline = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count" },
+ }).count;
+ return baseline >= oldBaseline + n;
+ }
+
+ countIncreasedByAtLeast(0);
+
+ g.add(100, g.obj);
+ ok(countIncreasedByAtLeast(g.obj.factor * 100));
+
+ g.add(100, g.fun);
+ ok(countIncreasedByAtLeast(g.fun.factor * 100));
+
+ g.add(100, g.str);
+ ok(countIncreasedByAtLeast(g.str.factor * 100));
+
+ g.add(100, g.ev);
+ ok(countIncreasedByAtLeast(g.ev.factor * 100));
+
+ g.add(100, g.shape);
+ ok(countIncreasedByAtLeast(g.shape.factor * 100));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js
new file mode 100644
index 0000000000..99830d75cb
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// HeapSnapshot.prototype.takeCensus: by: allocationStack breakdown
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-09.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ g.eval(` // 1
+ var log = []; // 2
+ function f() { log.push(allocationMarker()); } // 3
+ function g() { f(); } // 4
+ function h() { f(); } // 5
+ `);
+
+ // Create one allocationMarker with tracking turned off,
+ // so it will have no associated stack.
+ g.f();
+
+ dbg.memory.allocationSamplingProbability = 1;
+
+ for (const [func, n] of [
+ [g.f, 20],
+ [g.g, 10],
+ [g.h, 5],
+ ]) {
+ for (let i = 0; i < n; i++) {
+ dbg.memory.trackingAllocationSites = true;
+ // All allocations of allocationMarker occur with this line as the oldest
+ // stack frame.
+ func();
+ dbg.memory.trackingAllocationSites = false;
+ }
+ }
+
+ const census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: { by: "count", label: "haz stack" },
+ noStack: {
+ by: "count",
+ label: "no haz stack",
+ },
+ },
+ },
+ });
+
+ const map = census.AllocationMarker;
+ ok(map instanceof Map, "Should be a Map instance");
+ equal(
+ map.size,
+ 4,
+ "Should have 4 allocation stacks (including the lack of a stack)"
+ );
+
+ // Gather the stacks we are expecting to appear as keys, and
+ // check that there are no unexpected keys.
+ const stacks = {};
+
+ map.forEach((v, k) => {
+ if (k === "noStack") {
+ // No need to save this key.
+ } else if (
+ k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "run_test"
+ ) {
+ stacks.f = k;
+ } else if (
+ k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "g" &&
+ k.parent.parent.functionDisplayName === "run_test"
+ ) {
+ stacks.fg = k;
+ } else if (
+ k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "h" &&
+ k.parent.parent.functionDisplayName === "run_test"
+ ) {
+ stacks.fh = k;
+ } else {
+ dumpn("Unexpected allocation stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+ ok(false);
+ }
+ });
+
+ equal(map.get("noStack").label, "no haz stack");
+ equal(map.get("noStack").count, 1);
+
+ ok(stacks.f);
+ equal(map.get(stacks.f).label, "haz stack");
+ equal(map.get(stacks.f).count, 20);
+
+ ok(stacks.fg);
+ equal(map.get(stacks.fg).label, "haz stack");
+ equal(map.get(stacks.fg).count, 10);
+
+ ok(stacks.fh);
+ equal(map.get(stacks.fh).label, "haz stack");
+ equal(map.get(stacks.fh).count, 5);
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js
new file mode 100644
index 0000000000..9b0e0f8c74
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Check byte counts produced by takeCensus.
+//
+// Note that tracking allocation sites adds unique IDs to objects which
+// increases their size, making it hard to test reported sizes exactly.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-take Census-10.js
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ const sizeOfAM = byteSize(allocationMarker());
+
+ // Allocate a single allocation marker, and check that we can find it.
+ g.eval("var hold = allocationMarker();");
+ let census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "objectClass" },
+ });
+ equal(census.AllocationMarker.count, 1);
+ equal(census.AllocationMarker.bytes >= sizeOfAM, true);
+ g.hold = null;
+
+ g.eval(` // 1
+ var objs = []; // 2
+ function fnerd() { // 3
+ objs.push(allocationMarker()); // 4
+ for (let i = 0; i < 10; i++) // 5
+ objs.push(allocationMarker()); // 6
+ } // 7
+ `);
+
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+ g.fnerd();
+ dbg.memory.trackingAllocationSites = false;
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "objectClass", then: { by: "allocationStack" } },
+ });
+
+ let seen = 0;
+ census.AllocationMarker.forEach((v, k) => {
+ equal(k.functionDisplayName, "fnerd");
+ switch (k.line) {
+ case 4:
+ equal(v.count, 1);
+ equal(v.bytes >= sizeOfAM, true);
+ seen++;
+ break;
+
+ case 6:
+ equal(v.count, 10);
+ equal(v.bytes >= 10 * sizeOfAM, true);
+ seen++;
+ break;
+
+ default:
+ dumpn("Unexpected stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+ ok(false);
+ break;
+ }
+ });
+
+ equal(seen, 2);
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js
new file mode 100644
index 0000000000..151437bb41
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that Debugger.Memory.prototype.takeCensus and
+// HeapSnapshot.prototype.takeCensus return the same data for the same heap
+// graph.
+
+function doLiveAndOfflineCensus(g, dbg, opts) {
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+ g.eval(` // 1
+ (function unsafeAtAnySpeed() { // 2
+ for (var i = 0; i < 100; i++) { // 3
+ this.markers.push(allocationMarker()); // 4
+ } // 5
+ }()); // 6
+ `);
+ dbg.memory.trackingAllocationSites = false;
+
+ return {
+ live: dbg.memory.takeCensus(opts),
+ offline: saveHeapSnapshotAndTakeCensus(dbg, opts),
+ };
+}
+
+function getMarkerSize(g, dbg) {
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+ g.eval("var hold = allocationMarker();");
+ dbg.memory.trackingAllocationSites = false;
+ const live = dbg.memory.takeCensus({
+ breakdown: { by: "objectClass", then: { by: "count" } },
+ });
+ g.hold = null;
+ equal(live.AllocationMarker.count, 1);
+ return live.AllocationMarker.bytes;
+}
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ g.eval("this.markers = []");
+ const markerSize = getMarkerSize(g, dbg);
+
+ // First, test that we get the same counts and sizes as we allocate and retain
+ // more things.
+
+ let prevCount = 0;
+ let prevBytes = 0;
+
+ for (let i = 0; i < 10; i++) {
+ const { live, offline } = doLiveAndOfflineCensus(g, dbg, {
+ breakdown: { by: "objectClass", then: { by: "count" } },
+ });
+
+ equal(live.AllocationMarker.count, offline.AllocationMarker.count);
+ equal(live.AllocationMarker.bytes, offline.AllocationMarker.bytes);
+ equal(live.AllocationMarker.count, prevCount + 100);
+ equal(live.AllocationMarker.bytes, prevBytes + 100 * markerSize);
+
+ prevCount = live.AllocationMarker.count;
+ prevBytes = live.AllocationMarker.bytes;
+ }
+
+ // Second, test that the reported allocation stacks and counts and sizes at
+ // those allocation stacks match up.
+
+ const { live, offline } = doLiveAndOfflineCensus(g, dbg, {
+ breakdown: { by: "objectClass", then: { by: "allocationStack" } },
+ });
+
+ equal(live.AllocationMarker.size, offline.AllocationMarker.size);
+ // One stack with the loop further above, and another stack featuring the call
+ // right above.
+ equal(live.AllocationMarker.size, 2);
+
+ // Note that because SavedFrame stacks reconstructed from an offline heap
+ // snapshot don't have the same principals as SavedFrame stacks captured from
+ // a live stack, the live and offline allocation stacks won't be identity
+ // equal, but should be structurally the same.
+
+ const liveEntries = [];
+ live.AllocationMarker.forEach((v, k) => {
+ dumpn("Allocation stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+
+ equal(k.functionDisplayName, "unsafeAtAnySpeed");
+ equal(k.line, 4);
+
+ liveEntries.push([k.toString(), v]);
+ });
+
+ const offlineEntries = [];
+ offline.AllocationMarker.forEach((v, k) => {
+ dumpn("Allocation stack:");
+ k.toString()
+ .split(/\n/g)
+ .forEach(s => dumpn(s));
+
+ equal(k.functionDisplayName, "unsafeAtAnySpeed");
+ equal(k.line, 4);
+
+ offlineEntries.push([k.toString(), v]);
+ });
+
+ const sortEntries = (a, b) => {
+ if (a[0] < b[0]) {
+ return -1;
+ } else if (a[0] > b[0]) {
+ return 1;
+ }
+ return 0;
+ };
+ liveEntries.sort(sortEntries);
+ offlineEntries.sort(sortEntries);
+
+ equal(liveEntries.length, live.AllocationMarker.size);
+ equal(liveEntries.length, offlineEntries.length);
+
+ for (let i = 0; i < liveEntries.length; i++) {
+ equal(liveEntries[i][0], offlineEntries[i][0]);
+ equal(liveEntries[i][1].count, offlineEntries[i][1].count);
+ equal(liveEntries[i][1].bytes, offlineEntries[i][1].bytes);
+ }
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js
new file mode 100644
index 0000000000..f3a72102b0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that when we take a census and get a bucket list of ids that matched the
+// given category, that the returned ids are all in the snapshot and their
+// reported category.
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ const path = saveNewHeapSnapshot({ debugger: dbg });
+ const snapshot = readHeapSnapshot(path);
+
+ const bucket = { by: "bucket" };
+ const count = { by: "count", count: true, bytes: false };
+ const objectClassCount = { by: "objectClass", then: count, other: count };
+
+ const byClassName = snapshot.takeCensus({
+ breakdown: {
+ by: "objectClass",
+ then: bucket,
+ other: bucket,
+ },
+ });
+
+ const byClassNameCount = snapshot.takeCensus({
+ breakdown: objectClassCount,
+ });
+
+ const keys = new Set(Object.keys(byClassName));
+ equal(
+ keys.size,
+ Object.keys(byClassNameCount).length,
+ "Should have the same number of keys."
+ );
+ for (const k of Object.keys(byClassNameCount)) {
+ ok(keys.has(k), "Should not have any unexpected class names");
+ }
+
+ for (const key of Object.keys(byClassName)) {
+ equal(
+ byClassNameCount[key].count,
+ byClassName[key].length,
+ "Length of the bucket and count should be equal"
+ );
+
+ for (const id of byClassName[key]) {
+ const desc = snapshot.describeNode(objectClassCount, id);
+ equal(
+ desc[key].count,
+ 1,
+ "Describing the bucketed node confirms that it belongs to the category"
+ );
+ }
+ }
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js
new file mode 100644
index 0000000000..b7a3c9c2f8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can read core dumps into HeapSnapshot instances.
+/* eslint-disable strict */
+if (typeof Debugger != "function") {
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(globalThis);
+}
+
+function run_test() {
+ const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js
new file mode 100644
index 0000000000..273736abcd
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can save a core dump with allocation stacks and read it back
+// into a HeapSnapshot.
+
+if (typeof Debugger != "function") {
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(globalThis);
+}
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+ Services.prefs.setBoolPref("security.allow_eval_in_parent_process", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+ Services.prefs.clearUserPref("security.allow_eval_in_parent_process");
+ });
+
+ // Create a Debugger observing a debuggee's allocations.
+ const debuggee = new Cu.Sandbox(null);
+ const dbg = new Debugger(debuggee);
+ dbg.memory.trackingAllocationSites = true;
+
+ // Allocate some objects in the debuggee that will have their allocation
+ // stacks recorded by the Debugger.
+ debuggee.eval("this.objects = []");
+ for (let i = 0; i < 100; i++) {
+ debuggee.eval("this.objects.push({})");
+ }
+
+ // Now save a snapshot that will include the allocation stacks and read it
+ // back again.
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js
new file mode 100644
index 0000000000..aecbd9cdda
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can read core dumps with a UTF8 path into HeapSnapshot instances.
+/* eslint-disable strict */
+add_task(async function () {
+ const fileNameWithRussianCharacters =
+ "Снимок памяти Click.ru 08.06.2020 (Firefox dump).fxsnapshot";
+ const filePathWithRussianCharacters = PathUtils.join(
+ PathUtils.tempDir,
+ fileNameWithRussianCharacters
+ );
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot.");
+
+ await IOUtils.copy(filePath, filePathWithRussianCharacters);
+
+ ok(
+ await IOUtils.exists(filePathWithRussianCharacters),
+ `We could copy the file to the expected path ${filePathWithRussianCharacters}`
+ );
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePathWithRussianCharacters);
+ ok(snapshot, "Should be able to read a heap snapshot from an utf8 path");
+ ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot");
+});
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js
new file mode 100644
index 0000000000..52d4b31e29
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can read core dumps into HeapSnapshot instances in a worker.
+add_task(async function () {
+ const worker = new ChromeWorker("resource://test/heap-snapshot-worker.js");
+ worker.postMessage({});
+
+ let assertionCount = 0;
+ worker.onmessage = e => {
+ if (e.data.type !== "assertion") {
+ return;
+ }
+
+ ok(e.data.passed, e.data.msg + "\n" + e.data.stack);
+ assertionCount++;
+ };
+
+ await waitForDone(worker);
+
+ ok(assertionCount > 0);
+ worker.terminate();
+});
+
+function waitForDone(w) {
+ return new Promise((resolve, reject) => {
+ w.onerror = e => {
+ reject();
+ ok(false, "Error in worker: " + e);
+ };
+
+ w.addEventListener("message", function listener(e) {
+ if (e.data.type === "done") {
+ w.removeEventListener("message", listener);
+ resolve();
+ }
+ });
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js
new file mode 100644
index 0000000000..33367af7f7
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the ChromeUtils interface.
+// eslint-disable-next-line
+if (typeof Debugger != "function") {
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(globalThis);
+}
+
+function run_test() {
+ ok(ChromeUtils, "Should be able to get the ChromeUtils interface");
+
+ testBadParameters();
+ testGoodParameters();
+
+ do_test_finished();
+}
+
+function testBadParameters() {
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot(),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if arguments aren't passed in."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot(null),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if boundaries isn't an object."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({}),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if the boundaries object doesn't have any properties."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ runtime: true, globals: [this] }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if the boundaries object has more than one property."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ debugger: {} }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if the debuggees object is not a Debugger object"
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ globals: [{}] }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if the globals array contains non-global objects."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ runtime: false }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if runtime is supplied and is not true."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ globals: null }),
+ /TypeError:.*can't be converted to a sequence/,
+ "Should throw if globals is not an object."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ globals: {} }),
+ /TypeError:.*can't be converted to a sequence/,
+ "Should throw if globals is not an array."
+ );
+
+ Assert.throws(
+ () => ChromeUtils.saveHeapSnapshot({ debugger: Debugger.prototype }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if debugger is the Debugger.prototype object."
+ );
+
+ Assert.throws(
+ () =>
+ ChromeUtils.saveHeapSnapshot({
+ get globals() {
+ return [this];
+ },
+ }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should throw if boundaries property is a getter."
+ );
+}
+
+const makeNewSandbox = () =>
+ Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")());
+
+function testGoodParameters() {
+ const sandbox = makeNewSandbox();
+ let dbg = new Debugger(sandbox);
+
+ ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ ok(true, "Should be able to save a snapshot for a debuggee global.");
+
+ dbg = new Debugger();
+ const sandboxes = Array(10).fill(null).map(makeNewSandbox);
+ sandboxes.forEach(sb => dbg.addDebuggee(sb));
+
+ ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ ok(true, "Should be able to save a snapshot for many debuggee globals.");
+
+ dbg = new Debugger();
+ ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ ok(true, "Should be able to save a snapshot with no debuggee globals.");
+
+ ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot for a specific global.");
+
+ ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should be able to save a snapshot of the full runtime.");
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js
new file mode 100644
index 0000000000..625d359675
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `internalType` breakdown.
+ */
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+const REPORT = {
+ JSObject: {
+ bytes: 100,
+ count: 10,
+ },
+ "js::Shape": {
+ bytes: 500,
+ count: 50,
+ },
+ JSString: {
+ bytes: 10,
+ count: 1,
+ },
+};
+
+const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 610,
+ count: 0,
+ totalCount: 61,
+ children: [
+ {
+ name: "js::Shape",
+ bytes: 500,
+ totalBytes: 500,
+ count: 50,
+ totalCount: 50,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "JSObject",
+ bytes: 100,
+ totalBytes: 100,
+ count: 10,
+ totalCount: 10,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ reportLeafIndex: 1,
+ },
+ {
+ name: "JSString",
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 4,
+ parent: 1,
+ reportLeafIndex: 3,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+};
+
+function run_test() {
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js
new file mode 100644
index 0000000000..75ed87c25b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `coarseType` breakdown.
+ */
+
+const countBreakdown = { by: "count", count: true, bytes: true };
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: countBreakdown },
+ strings: countBreakdown,
+ scripts: countBreakdown,
+ other: { by: "internalType", then: countBreakdown },
+ domNode: countBreakdown,
+};
+
+const REPORT = {
+ objects: {
+ Function: { bytes: 10, count: 1 },
+ Array: { bytes: 20, count: 2 },
+ },
+ strings: { bytes: 10, count: 1 },
+ scripts: { bytes: 1, count: 1 },
+ other: {
+ "js::Shape": { bytes: 30, count: 3 },
+ "js::Shape2": { bytes: 40, count: 4 },
+ },
+ domNode: { bytes: 0, count: 0 },
+};
+
+const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 111,
+ count: 0,
+ totalCount: 12,
+ children: [
+ {
+ name: "other",
+ count: 0,
+ totalCount: 7,
+ bytes: 0,
+ totalBytes: 70,
+ children: [
+ {
+ name: "js::Shape2",
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 9,
+ parent: 7,
+ reportLeafIndex: 8,
+ },
+ {
+ name: "js::Shape",
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 8,
+ parent: 7,
+ reportLeafIndex: 7,
+ },
+ ],
+ id: 7,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "objects",
+ count: 0,
+ totalCount: 3,
+ bytes: 0,
+ totalBytes: 30,
+ children: [
+ {
+ name: "Array",
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 4,
+ parent: 2,
+ reportLeafIndex: 3,
+ },
+ {
+ name: "Function",
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 2,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "strings",
+ count: 1,
+ totalCount: 1,
+ bytes: 10,
+ totalBytes: 10,
+ children: undefined,
+ id: 6,
+ parent: 1,
+ reportLeafIndex: 5,
+ },
+ {
+ name: "scripts",
+ count: 1,
+ totalCount: 1,
+ bytes: 1,
+ totalBytes: 1,
+ children: undefined,
+ id: 5,
+ parent: 1,
+ reportLeafIndex: 4,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+};
+
+function run_test() {
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js
new file mode 100644
index 0000000000..e0ca7dc15d
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `objectClass` breakdown.
+ */
+
+const countBreakdown = { by: "count", count: true, bytes: true };
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: countBreakdown,
+ other: { by: "internalType", then: countBreakdown },
+};
+
+const REPORT = {
+ Function: { bytes: 10, count: 10 },
+ Array: { bytes: 100, count: 1 },
+ other: {
+ "JIT::CODE::NOW!!!": { bytes: 20, count: 2 },
+ "JIT::CODE::LATER!!!": { bytes: 40, count: 4 },
+ },
+};
+
+const EXPECTED = {
+ name: null,
+ count: 0,
+ totalCount: 17,
+ bytes: 0,
+ totalBytes: 170,
+ children: [
+ {
+ name: "Array",
+ bytes: 100,
+ totalBytes: 100,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "other",
+ count: 0,
+ totalCount: 6,
+ bytes: 0,
+ totalBytes: 60,
+ children: [
+ {
+ name: "JIT::CODE::LATER!!!",
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 6,
+ parent: 4,
+ reportLeafIndex: 5,
+ },
+ {
+ name: "JIT::CODE::NOW!!!",
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 5,
+ parent: 4,
+ reportLeafIndex: 4,
+ },
+ ],
+ id: 4,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "Function",
+ bytes: 10,
+ totalBytes: 10,
+ count: 10,
+ totalCount: 10,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ reportLeafIndex: 1,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+};
+
+function run_test() {
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js
new file mode 100644
index 0000000000..e1cc32d697
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `allocationStack` breakdown.
+ */
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: countBreakdown,
+ noStack: countBreakdown,
+ };
+
+ let stack1, stack2, stack3, stack4;
+
+ (function a() {
+ (function b() {
+ (function c() {
+ stack1 = saveStack(3);
+ })();
+ (function d() {
+ stack2 = saveStack(3);
+ stack3 = saveStack(3);
+ })();
+ stack4 = saveStack(2);
+ })();
+ })();
+
+ const stack5 = saveStack(1);
+
+ const REPORT = new Map([
+ [stack1, { bytes: 10, count: 1 }],
+ [stack2, { bytes: 20, count: 2 }],
+ [stack3, { bytes: 30, count: 3 }],
+ [stack4, { bytes: 40, count: 4 }],
+ [stack5, { bytes: 50, count: 5 }],
+ ["noStack", { bytes: 60, count: 6 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 210,
+ count: 0,
+ totalCount: 21,
+ children: [
+ {
+ name: stack4.parent,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: stack3.parent,
+ bytes: 0,
+ totalBytes: 50,
+ count: 0,
+ totalCount: 5,
+ children: [
+ {
+ name: stack3,
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 7,
+ parent: 5,
+ reportLeafIndex: 3,
+ },
+ {
+ name: stack2,
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 6,
+ parent: 5,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 5,
+ parent: 2,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: stack4,
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 8,
+ parent: 2,
+ reportLeafIndex: 4,
+ },
+ {
+ name: stack1.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: stack1,
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 4,
+ parent: 3,
+ reportLeafIndex: 1,
+ },
+ ],
+ id: 3,
+ parent: 2,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "noStack",
+ bytes: 60,
+ totalBytes: 60,
+ count: 6,
+ totalCount: 6,
+ children: undefined,
+ id: 10,
+ parent: 1,
+ reportLeafIndex: 6,
+ },
+ {
+ name: stack5,
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: undefined,
+ id: 9,
+ parent: 1,
+ reportLeafIndex: 5,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js
new file mode 100644
index 0000000000..fbd9944507
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests CensusTreeNode with `allocationStack` => `objectClass` breakdown.
+ */
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: {
+ by: "objectClass",
+ then: countBreakdown,
+ other: countBreakdown,
+ },
+ noStack: countBreakdown,
+ };
+
+ let stack;
+
+ (function a() {
+ (function b() {
+ (function c() {
+ stack = saveStack(3);
+ })();
+ })();
+ })();
+
+ const REPORT = new Map([
+ [
+ stack,
+ {
+ Foo: { bytes: 10, count: 1 },
+ Bar: { bytes: 20, count: 2 },
+ Baz: { bytes: 30, count: 3 },
+ other: { bytes: 40, count: 4 },
+ },
+ ],
+ ["noStack", { bytes: 50, count: 5 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 150,
+ count: 0,
+ totalCount: 15,
+ children: [
+ {
+ name: stack.parent.parent,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: stack.parent,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: stack,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: "other",
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 8,
+ parent: 4,
+ reportLeafIndex: 5,
+ },
+ {
+ name: "Baz",
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 7,
+ parent: 4,
+ reportLeafIndex: 4,
+ },
+ {
+ name: "Bar",
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 6,
+ parent: 4,
+ reportLeafIndex: 3,
+ },
+ {
+ name: "Foo",
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 5,
+ parent: 4,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 4,
+ parent: 3,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 3,
+ parent: 2,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "noStack",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: undefined,
+ id: 9,
+ parent: 1,
+ reportLeafIndex: 6,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js
new file mode 100644
index 0000000000..70cc4a2696
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test inverting CensusTreeNode with a by alloaction stack breakdown.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+ };
+
+ function a(n) {
+ return b(n);
+ }
+ function b(n) {
+ return c(n);
+ }
+ function c(n) {
+ return saveStack(n);
+ }
+ function d(n) {
+ return b(n);
+ }
+ function e(n) {
+ return c(n);
+ }
+
+ const abc_Stack = a(3);
+ const bc_Stack = b(2);
+ const c_Stack = c(1);
+ const dbc_Stack = d(3);
+ const ec_Stack = e(2);
+
+ const REPORT = new Map([
+ [abc_Stack, { bytes: 10, count: 1 }],
+ [bc_Stack, { bytes: 10, count: 1 }],
+ [c_Stack, { bytes: 10, count: 1 }],
+ [dbc_Stack, { bytes: 10, count: 1 }],
+ [ec_Stack, { bytes: 10, count: 1 }],
+ ["noStack", { bytes: 50, count: 5 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: "noStack",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 16,
+ parent: 15,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 15,
+ parent: 14,
+ reportLeafIndex: 6,
+ },
+ {
+ name: abc_Stack,
+ bytes: 50,
+ totalBytes: 10,
+ count: 5,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 18,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: abc_Stack.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 22,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: abc_Stack.parent.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 21,
+ parent: 20,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 20,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: dbc_Stack.parent.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 24,
+ parent: 23,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 23,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 19,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: ec_Stack.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 26,
+ parent: 25,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 25,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 17,
+ parent: 14,
+ reportLeafIndex: new Set([1, 2, 3, 4, 5]),
+ },
+ ],
+ id: 14,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js
new file mode 100644
index 0000000000..009848b9e6
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test inverting CensusTreeNode with a non-allocation stack breakdown.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { bytes: 50, count: 5 },
+ other: { bytes: 0, count: 0 },
+ },
+ scripts: {
+ "js::jit::JitScript": { bytes: 30, count: 3 },
+ },
+ strings: {
+ JSAtom: { bytes: 60, count: 6 },
+ },
+ other: {
+ "js::Shape": { bytes: 80, count: 8 },
+ },
+ domNode: {},
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: [
+ {
+ name: "js::Shape",
+ bytes: 80,
+ totalBytes: 80,
+ count: 8,
+ totalCount: 8,
+ children: [
+ {
+ name: "other",
+ bytes: 0,
+ totalBytes: 80,
+ count: 0,
+ totalCount: 8,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 15,
+ parent: 14,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 14,
+ parent: 13,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 13,
+ parent: 12,
+ reportLeafIndex: 9,
+ },
+ {
+ name: "JSAtom",
+ bytes: 60,
+ totalBytes: 60,
+ count: 6,
+ totalCount: 6,
+ children: [
+ {
+ name: "strings",
+ bytes: 0,
+ totalBytes: 60,
+ count: 0,
+ totalCount: 6,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 18,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 17,
+ parent: 16,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 16,
+ parent: 12,
+ reportLeafIndex: 7,
+ },
+ {
+ name: "Array",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 50,
+ count: 0,
+ totalCount: 5,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 21,
+ parent: 20,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 20,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 19,
+ parent: 12,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "js::jit::JitScript",
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: [
+ {
+ name: "scripts",
+ bytes: 0,
+ totalBytes: 30,
+ count: 0,
+ totalCount: 3,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 24,
+ parent: 23,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 23,
+ parent: 22,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 22,
+ parent: 12,
+ reportLeafIndex: 5,
+ },
+ ],
+ id: 12,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js
new file mode 100644
index 0000000000..0e8e2ebcc1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test inverting CensusTreeNode with a non-allocation stack breakdown.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "filename",
+ then: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ noFilename: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ "http://example.com/app.js": {
+ JSScript: { count: 10, bytes: 100 },
+ },
+ "http://example.com/ads.js": {
+ "js::LazyScript": { count: 20, bytes: 200 },
+ },
+ "http://example.com/trackers.js": {
+ JSScript: { count: 30, bytes: 300 },
+ },
+ noFilename: {
+ "js::jit::JitCode": { count: 40, bytes: 400 },
+ },
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 1000,
+ count: 0,
+ totalCount: 100,
+ children: [
+ {
+ name: "noFilename",
+ bytes: 0,
+ totalBytes: 400,
+ count: 0,
+ totalCount: 40,
+ children: [
+ {
+ name: "js::jit::JitCode",
+ bytes: 400,
+ totalBytes: 400,
+ count: 40,
+ totalCount: 40,
+ children: undefined,
+ id: 9,
+ parent: 8,
+ reportLeafIndex: 8,
+ },
+ ],
+ id: 8,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "http://example.com/trackers.js",
+ bytes: 0,
+ totalBytes: 300,
+ count: 0,
+ totalCount: 30,
+ children: [
+ {
+ name: "JSScript",
+ bytes: 300,
+ totalBytes: 300,
+ count: 30,
+ totalCount: 30,
+ children: undefined,
+ id: 7,
+ parent: 6,
+ reportLeafIndex: 6,
+ },
+ ],
+ id: 6,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "http://example.com/ads.js",
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 20,
+ children: [
+ {
+ name: "js::LazyScript",
+ bytes: 200,
+ totalBytes: 200,
+ count: 20,
+ totalCount: 20,
+ children: undefined,
+ id: 5,
+ parent: 4,
+ reportLeafIndex: 4,
+ },
+ ],
+ id: 4,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "http://example.com/app.js",
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: "JSScript",
+ bytes: 100,
+ totalBytes: 100,
+ count: 10,
+ totalCount: 10,
+ children: undefined,
+ id: 3,
+ parent: 2,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js
new file mode 100644
index 0000000000..5f91e5e67b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test that repeatedly converting the same census report to a CensusTreeNode
+ * tree results in the same CensusTreeNode tree.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "filename",
+ then: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ noFilename: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ "http://example.com/app.js": {
+ JSScript: { count: 10, bytes: 100 },
+ },
+ "http://example.com/ads.js": {
+ "js::LazyScript": { count: 20, bytes: 200 },
+ },
+ "http://example.com/trackers.js": {
+ JSScript: { count: 30, bytes: 300 },
+ },
+ noFilename: {
+ "js::jit::JitCode": { count: 40, bytes: 400 },
+ },
+ };
+
+ const first = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+ const second = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+ const third = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+
+ assertStructurallyEquivalent(first, second);
+ assertStructurallyEquivalent(second, third);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js
new file mode 100644
index 0000000000..ba783f8e47
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test when multiple leaves in the census report map to the same node in an
+ * inverted CensusReportTree.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { count: 1, bytes: 10 },
+ },
+ other: {
+ Array: { count: 1, bytes: 10 },
+ },
+ strings: { count: 0, bytes: 0 },
+ scripts: { count: 0, bytes: 0 },
+ domNode: { count: 0, bytes: 0 },
+ };
+
+ const node = censusReportToCensusTreeNode(BREAKDOWN, REPORT, {
+ invert: true,
+ });
+
+ equal(node.children[0].name, "Array");
+ equal(node.children[0].reportLeafIndex.size, 2);
+ dumpn(
+ `node.children[0].reportLeafIndex = ${[
+ ...node.children[0].reportLeafIndex,
+ ]}`
+ );
+ ok(node.children[0].reportLeafIndex.has(2));
+ ok(node.children[0].reportLeafIndex.has(6));
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js
new file mode 100644
index 0000000000..c90b34af70
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "internalType".
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+};
+
+const REPORT1 = {
+ JSObject: {
+ count: 10,
+ bytes: 100,
+ },
+ "js::Shape": {
+ count: 50,
+ bytes: 500,
+ },
+ JSString: {
+ count: 0,
+ bytes: 0,
+ },
+ "js::LazyScript": {
+ count: 1,
+ bytes: 10,
+ },
+};
+
+const REPORT2 = {
+ JSObject: {
+ count: 11,
+ bytes: 110,
+ },
+ "js::Shape": {
+ count: 51,
+ bytes: 510,
+ },
+ JSString: {
+ count: 1,
+ bytes: 1,
+ },
+ "js::BaseShape": {
+ count: 1,
+ bytes: 42,
+ },
+};
+
+const EXPECTED = {
+ JSObject: {
+ count: 1,
+ bytes: 10,
+ },
+ "js::Shape": {
+ count: 1,
+ bytes: 10,
+ },
+ JSString: {
+ count: 1,
+ bytes: 1,
+ },
+ "js::LazyScript": {
+ count: -1,
+ bytes: -10,
+ },
+ "js::BaseShape": {
+ count: 1,
+ bytes: 42,
+ },
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js
new file mode 100644
index 0000000000..cefe76abde
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "count".
+
+const BREAKDOWN = { by: "count", count: true, bytes: true };
+
+const REPORT1 = {
+ count: 10,
+ bytes: 100,
+};
+
+const REPORT2 = {
+ count: 11,
+ bytes: 110,
+};
+
+const EXPECTED = {
+ count: 1,
+ bytes: 10,
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js
new file mode 100644
index 0000000000..e625f4e0be
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "coarseType".
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ domNode: { by: "count", count: true, bytes: true },
+};
+
+const REPORT1 = {
+ objects: {
+ count: 1,
+ bytes: 10,
+ },
+ scripts: {
+ count: 1,
+ bytes: 10,
+ },
+ strings: {
+ count: 1,
+ bytes: 10,
+ },
+ other: {
+ count: 3,
+ bytes: 30,
+ },
+ domNode: {
+ count: 0,
+ bytes: 0,
+ },
+};
+
+const REPORT2 = {
+ objects: {
+ count: 1,
+ bytes: 10,
+ },
+ scripts: {
+ count: 0,
+ bytes: 0,
+ },
+ strings: {
+ count: 2,
+ bytes: 20,
+ },
+ other: {
+ count: 4,
+ bytes: 40,
+ },
+ domNode: {
+ count: 0,
+ bytes: 0,
+ },
+};
+
+const EXPECTED = {
+ objects: {
+ count: 0,
+ bytes: 0,
+ },
+ scripts: {
+ count: -1,
+ bytes: -10,
+ },
+ strings: {
+ count: 1,
+ bytes: 10,
+ },
+ other: {
+ count: 1,
+ bytes: 10,
+ },
+ domNode: {
+ count: 0,
+ bytes: 0,
+ },
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js
new file mode 100644
index 0000000000..036a33804f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "objectClass".
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+const REPORT1 = {
+ Array: {
+ count: 1,
+ bytes: 100,
+ },
+ Function: {
+ count: 10,
+ bytes: 10,
+ },
+ other: {
+ count: 10,
+ bytes: 100,
+ },
+};
+
+const REPORT2 = {
+ Object: {
+ count: 1,
+ bytes: 100,
+ },
+ Function: {
+ count: 20,
+ bytes: 20,
+ },
+ other: {
+ count: 10,
+ bytes: 100,
+ },
+};
+
+const EXPECTED = {
+ Array: {
+ count: -1,
+ bytes: -100,
+ },
+ Function: {
+ count: 10,
+ bytes: 10,
+ },
+ other: {
+ count: 0,
+ bytes: 0,
+ },
+ Object: {
+ count: 1,
+ bytes: 100,
+ },
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js
new file mode 100644
index 0000000000..1ce0cfeb5b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of breakdown by "allocationStack".
+
+const BREAKDOWN = {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+};
+
+const stack1 = saveStack();
+const stack2 = saveStack();
+const stack3 = saveStack();
+
+const REPORT1 = new Map([
+ [stack1, { count: 10, bytes: 100 }],
+ [stack2, { count: 1, bytes: 10 }],
+]);
+
+const REPORT2 = new Map([
+ [stack2, { count: 10, bytes: 100 }],
+ [stack3, { count: 1, bytes: 10 }],
+]);
+
+const EXPECTED = new Map([
+ [stack1, { count: -10, bytes: -100 }],
+ [stack2, { count: 9, bytes: 90 }],
+ [stack3, { count: 1, bytes: 10 }],
+]);
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js
new file mode 100644
index 0000000000..b805de48dd
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test diffing census reports of a "complex" and "realistic" breakdown.
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "allocationStack",
+ then: {
+ by: "objectClass",
+ then: { by: "count", count: false, bytes: true },
+ other: { by: "count", count: false, bytes: true },
+ },
+ noStack: {
+ by: "objectClass",
+ then: { by: "count", count: false, bytes: true },
+ other: { by: "count", count: false, bytes: true },
+ },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true },
+ },
+ domNode: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true },
+ },
+};
+
+const stack1 = saveStack();
+const stack2 = saveStack();
+const stack3 = saveStack();
+
+const REPORT1 = {
+ objects: new Map([
+ [
+ stack1,
+ { Function: { bytes: 1 }, Object: { bytes: 2 }, other: { bytes: 0 } },
+ ],
+ [stack2, { Array: { bytes: 3 }, Date: { bytes: 4 }, other: { bytes: 0 } }],
+ ["noStack", { Object: { bytes: 3 } }],
+ ]),
+ strings: {
+ JSAtom: { bytes: 10 },
+ JSLinearString: { bytes: 5 },
+ },
+ scripts: {
+ JSScript: { bytes: 1 },
+ "js::jit::JitCode": { bytes: 2 },
+ },
+ other: {
+ "mozilla::dom::Thing": { bytes: 1 },
+ },
+ domNode: {},
+};
+
+const REPORT2 = {
+ objects: new Map([
+ [stack2, { Array: { bytes: 1 }, Date: { bytes: 2 }, other: { bytes: 3 } }],
+ [
+ stack3,
+ { Function: { bytes: 1 }, Object: { bytes: 2 }, other: { bytes: 0 } },
+ ],
+ ["noStack", { Object: { bytes: 3 } }],
+ ]),
+ strings: {
+ JSAtom: { bytes: 5 },
+ JSLinearString: { bytes: 10 },
+ },
+ scripts: {
+ JSScript: { bytes: 2 },
+ "js::LazyScript": { bytes: 42 },
+ "js::jit::JitCode": { bytes: 1 },
+ },
+ other: {
+ "mozilla::dom::OtherThing": { bytes: 1 },
+ },
+ domNode: {},
+};
+
+const EXPECTED = {
+ objects: new Map([
+ [
+ stack1,
+ { Function: { bytes: -1 }, Object: { bytes: -2 }, other: { bytes: 0 } },
+ ],
+ [
+ stack2,
+ { Array: { bytes: -2 }, Date: { bytes: -2 }, other: { bytes: 3 } },
+ ],
+ [
+ stack3,
+ { Function: { bytes: 1 }, Object: { bytes: 2 }, other: { bytes: 0 } },
+ ],
+ ["noStack", { Object: { bytes: 0 } }],
+ ]),
+ scripts: {
+ JSScript: {
+ bytes: 1,
+ },
+ "js::jit::JitCode": {
+ bytes: -1,
+ },
+ "js::LazyScript": {
+ bytes: 42,
+ },
+ },
+ strings: {
+ JSAtom: {
+ bytes: -5,
+ },
+ JSLinearString: {
+ bytes: 5,
+ },
+ },
+ other: {
+ "mozilla::dom::Thing": {
+ bytes: -1,
+ },
+ "mozilla::dom::OtherThing": {
+ bytes: 1,
+ },
+ },
+ domNode: {},
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js
new file mode 100644
index 0000000000..961023b2c2
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test filtering basic CensusTreeNode trees.
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { bytes: 50, count: 5 },
+ UInt8Array: { bytes: 80, count: 8 },
+ Int32Array: { bytes: 320, count: 32 },
+ other: { bytes: 0, count: 0 },
+ },
+ scripts: {
+ "js::jit::JitScript": { bytes: 30, count: 3 },
+ },
+ strings: {
+ JSAtom: { bytes: 60, count: 6 },
+ },
+ other: {
+ "js::Shape": { bytes: 80, count: 8 },
+ },
+ domNode: {},
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 620,
+ count: 0,
+ totalCount: 62,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 450,
+ count: 0,
+ totalCount: 45,
+ children: [
+ {
+ name: "Int32Array",
+ bytes: 320,
+ totalBytes: 320,
+ count: 32,
+ totalCount: 32,
+ children: undefined,
+ id: 16,
+ parent: 15,
+ reportLeafIndex: 4,
+ },
+ {
+ name: "UInt8Array",
+ bytes: 80,
+ totalBytes: 80,
+ count: 8,
+ totalCount: 8,
+ children: undefined,
+ id: 17,
+ parent: 15,
+ reportLeafIndex: 3,
+ },
+ {
+ name: "Array",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: undefined,
+ id: 18,
+ parent: 15,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 15,
+ parent: 14,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 14,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "Array" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js
new file mode 100644
index 0000000000..9915acb678
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test filtering CensusTreeNode trees with an `allocationStack` breakdown.
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: countBreakdown,
+ noStack: countBreakdown,
+ };
+
+ let stack1, stack2, stack3, stack4;
+
+ (function foo() {
+ (function bar() {
+ (function baz() {
+ stack1 = saveStack(3);
+ })();
+ (function quux() {
+ stack2 = saveStack(3);
+ stack3 = saveStack(3);
+ })();
+ })();
+ stack4 = saveStack(2);
+ })();
+
+ const stack5 = saveStack(1);
+
+ const REPORT = new Map([
+ [stack1, { bytes: 10, count: 1 }],
+ [stack2, { bytes: 20, count: 2 }],
+ [stack3, { bytes: 30, count: 3 }],
+ [stack4, { bytes: 40, count: 4 }],
+ [stack5, { bytes: 50, count: 5 }],
+ ["noStack", { bytes: 60, count: 6 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 210,
+ count: 0,
+ totalCount: 21,
+ children: [
+ {
+ name: stack1.parent.parent,
+ bytes: 0,
+ totalBytes: 60,
+ count: 0,
+ totalCount: 6,
+ children: [
+ {
+ name: stack2.parent,
+ bytes: 0,
+ totalBytes: 50,
+ count: 0,
+ totalCount: 5,
+ children: [
+ {
+ name: stack3,
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 15,
+ parent: 14,
+ reportLeafIndex: 3,
+ },
+ {
+ name: stack2,
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 16,
+ parent: 14,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 14,
+ parent: 13,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: stack1.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: stack1,
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 18,
+ parent: 17,
+ reportLeafIndex: 1,
+ },
+ ],
+ id: 17,
+ parent: 13,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 13,
+ parent: 12,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 12,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "bar" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js
new file mode 100644
index 0000000000..dc144bac2e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test filtering with no matches.
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { bytes: 50, count: 5 },
+ UInt8Array: { bytes: 80, count: 8 },
+ Int32Array: { bytes: 320, count: 32 },
+ other: { bytes: 0, count: 0 },
+ },
+ scripts: {
+ "js::jit::JitScript": { bytes: 30, count: 3 },
+ },
+ strings: {
+ JSAtom: { bytes: 60, count: 6 },
+ },
+ other: {
+ "js::Shape": { bytes: 80, count: 8 },
+ },
+ domNode: {},
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 620,
+ count: 0,
+ totalCount: 62,
+ children: undefined,
+ id: 14,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, {
+ filter: "zzzzzzzzzzzzzzzzzzzz",
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js
new file mode 100644
index 0000000000..7e37b2c5da
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the filtered nodes' counts and bytes are the same as they were when
+// unfiltered.
+
+function run_test() {
+ const COUNT = { by: "count", count: true, bytes: true };
+ const INTERNAL_TYPE = { by: "internalType", then: COUNT };
+
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: COUNT, other: COUNT },
+ strings: COUNT,
+ scripts: {
+ by: "filename",
+ then: INTERNAL_TYPE,
+ noFilename: INTERNAL_TYPE,
+ },
+ other: INTERNAL_TYPE,
+ domNode: { by: "descriptiveType", then: COUNT, other: COUNT },
+ };
+
+ const REPORT = {
+ objects: {
+ Function: {
+ count: 7,
+ bytes: 70,
+ },
+ Array: {
+ count: 6,
+ bytes: 60,
+ },
+ },
+ scripts: {
+ "http://mozilla.github.io/pdf.js/build/pdf.js": {
+ "js::LazyScript": {
+ count: 4,
+ bytes: 40,
+ },
+ },
+ },
+ strings: {
+ count: 2,
+ bytes: 20,
+ },
+ other: {
+ "js::Shape": {
+ count: 1,
+ bytes: 10,
+ },
+ },
+ domNode: {},
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 20,
+ parent: undefined,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 130,
+ count: 0,
+ totalCount: 13,
+ children: [
+ {
+ name: "Function",
+ bytes: 70,
+ totalBytes: 70,
+ count: 7,
+ totalCount: 7,
+ id: 14,
+ parent: 13,
+ children: undefined,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "Array",
+ bytes: 60,
+ totalBytes: 60,
+ count: 6,
+ totalCount: 6,
+ id: 15,
+ parent: 13,
+ children: undefined,
+ reportLeafIndex: 3,
+ },
+ ],
+ id: 13,
+ parent: 12,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 12,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "objects" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js
new file mode 100644
index 0000000000..3202a4b39f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that filtered and inverted allocation stack census trees are sorted
+// properly.
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: countBreakdown,
+ noStack: countBreakdown,
+ };
+
+ const stacks = [];
+
+ function foo(depth = 1) {
+ stacks.push(saveStack(depth));
+ bar(depth + 1);
+ baz(depth + 1);
+ stacks.push(saveStack(depth));
+ }
+
+ function bar(depth = 1) {
+ stacks.push(saveStack(depth));
+ stacks.push(saveStack(depth));
+ }
+
+ function baz(depth = 1) {
+ stacks.push(saveStack(depth));
+ bang(depth + 1);
+ stacks.push(saveStack(depth));
+ }
+
+ function bang(depth = 1) {
+ stacks.push(saveStack(depth));
+ stacks.push(saveStack(depth));
+ stacks.push(saveStack(depth));
+ }
+
+ foo();
+ bar();
+ baz();
+ bang();
+
+ const REPORT = new Map(
+ stacks.map((s, i) => {
+ return [
+ s,
+ {
+ count: i + 1,
+ bytes: (i + 1) * 10,
+ },
+ ];
+ })
+ );
+
+ const tree = censusReportToCensusTreeNode(BREAKDOWN, REPORT, {
+ filter: "baz",
+ invert: true,
+ });
+
+ dumpn("tree = " + JSON.stringify(tree, savedFrameReplacer, 4));
+
+ (function assertSortedBySelf(node) {
+ if (node.children) {
+ let lastSelfBytes = Infinity;
+ for (const child of node.children) {
+ ok(child.bytes <= lastSelfBytes, `${child.bytes} <= ${lastSelfBytes}`);
+ lastSelfBytes = child.bytes;
+ assertSortedBySelf(child);
+ }
+ }
+ })(tree);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js
new file mode 100644
index 0000000000..e89048c333
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can turn a breakdown with { by: "count" } leaves into a
+// breakdown with { by: "bucket" } leaves.
+
+const COUNT = { by: "count", count: true, bytes: true };
+const BUCKET = { by: "bucket" };
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: COUNT, other: COUNT },
+ strings: COUNT,
+ scripts: {
+ by: "filename",
+ then: { by: "internalType", then: COUNT },
+ noFilename: { by: "internalType", then: COUNT },
+ },
+ other: { by: "internalType", then: COUNT },
+};
+
+const EXPECTED = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: BUCKET, other: BUCKET },
+ strings: BUCKET,
+ scripts: {
+ by: "filename",
+ then: { by: "internalType", then: BUCKET },
+ noFilename: { by: "internalType", then: BUCKET },
+ },
+ other: { by: "internalType", then: BUCKET },
+};
+
+function run_test() {
+ assertCountToBucketBreakdown(BREAKDOWN, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js
new file mode 100644
index 0000000000..fc2864f5f4
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the behavior of the deduplicatePaths utility function.
+
+function edge(from, to, name) {
+ return { from, to, name };
+}
+
+function run_test() {
+ const a = 1;
+ const b = 2;
+ const c = 3;
+ const d = 4;
+ const e = 5;
+ const f = 6;
+ const g = 7;
+
+ dumpn("Single long path");
+ assertDeduplicatedPaths({
+ target: g,
+ paths: [
+ [
+ pathEntry(a, "e1"),
+ pathEntry(b, "e2"),
+ pathEntry(c, "e3"),
+ pathEntry(d, "e4"),
+ pathEntry(e, "e5"),
+ pathEntry(f, "e6"),
+ ],
+ ],
+ expectedNodes: [a, b, c, d, e, f, g],
+ expectedEdges: [
+ edge(a, b, "e1"),
+ edge(b, c, "e2"),
+ edge(c, d, "e3"),
+ edge(d, e, "e4"),
+ edge(e, f, "e5"),
+ edge(f, g, "e6"),
+ ],
+ });
+
+ dumpn("Multiple edges from and to the same nodes");
+ assertDeduplicatedPaths({
+ target: a,
+ paths: [[pathEntry(b, "x")], [pathEntry(b, "y")], [pathEntry(b, "z")]],
+ expectedNodes: [a, b],
+ expectedEdges: [edge(b, a, "x"), edge(b, a, "y"), edge(b, a, "z")],
+ });
+
+ dumpn("Multiple paths sharing some nodes and edges");
+ assertDeduplicatedPaths({
+ target: g,
+ paths: [
+ [pathEntry(a, "a->b"), pathEntry(b, "b->c"), pathEntry(c, "foo")],
+ [pathEntry(a, "a->b"), pathEntry(b, "b->d"), pathEntry(d, "bar")],
+ [pathEntry(a, "a->b"), pathEntry(b, "b->e"), pathEntry(e, "baz")],
+ ],
+ expectedNodes: [a, b, c, d, e, g],
+ expectedEdges: [
+ edge(a, b, "a->b"),
+ edge(b, c, "b->c"),
+ edge(b, d, "b->d"),
+ edge(b, e, "b->e"),
+ edge(c, g, "foo"),
+ edge(d, g, "bar"),
+ edge(e, g, "baz"),
+ ],
+ });
+
+ dumpn("Second shortest path contains target itself");
+ assertDeduplicatedPaths({
+ target: g,
+ paths: [
+ [pathEntry(a, "a->b"), pathEntry(b, "b->g")],
+ [
+ pathEntry(a, "a->b"),
+ pathEntry(b, "b->g"),
+ pathEntry(g, "g->f"),
+ pathEntry(f, "f->g"),
+ ],
+ ],
+ expectedNodes: [a, b, g],
+ expectedEdges: [edge(a, b, "a->b"), edge(b, g, "b->g")],
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js
new file mode 100644
index 0000000000..6963f3f33a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test basic functionality of `CensusUtils.getCensusIndividuals`.
+
+function run_test() {
+ const stack1 = saveStack(1);
+ const stack2 = saveStack(1);
+ const stack3 = saveStack(1);
+
+ const COUNT = { by: "count", count: true, bytes: true };
+ const INTERNAL_TYPE = { by: "internalType", then: COUNT };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: INTERNAL_TYPE,
+ noStack: INTERNAL_TYPE,
+ };
+
+ const MOCK_SNAPSHOT = {
+ takeCensus: ({ breakdown }) => {
+ assertStructurallyEquivalent(
+ breakdown,
+ CensusUtils.countToBucketBreakdown(BREAKDOWN)
+ );
+
+ // DFS Index
+ // prettier-ignore
+ return new Map([ // 0
+ [stack1, { // 1
+ JSObject: [101, 102, 103], // 2
+ JSString: [111, 112, 113], // 3
+ }],
+ [stack2, { // 4
+ JSObject: [201, 202, 203], // 5
+ JSString: [211, 212, 213], // 6
+ }],
+ [stack3, { // 7
+ JSObject: [301, 302, 303], // 8
+ JSString: [311, 312, 313], // 9
+ }],
+ ["noStack", { // 10
+ JSObject: [401, 402, 403], // 11
+ JSString: [411, 412, 413], // 12
+ }],
+ ]);
+ },
+ };
+
+ const INDICES = new Set([3, 5, 9]);
+
+ const EXPECTED = new Set([111, 112, 113, 201, 202, 203, 311, 312, 313]);
+
+ const actual = new Set(
+ CensusUtils.getCensusIndividuals(INDICES, BREAKDOWN, MOCK_SNAPSHOT)
+ );
+
+ assertStructurallyEquivalent(EXPECTED, actual);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js
new file mode 100644
index 0000000000..04aae90e99
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test basic functionality of `CensusUtils.getReportLeaves`.
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: { by: "count", count: true, bytes: true },
+ scripts: {
+ by: "filename",
+ then: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ noFilename: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ domNode: {
+ by: "descriptiveType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { count: 6, bytes: 60 },
+ Function: { count: 1, bytes: 10 },
+ Object: { count: 1, bytes: 10 },
+ RegExp: { count: 1, bytes: 10 },
+ other: { count: 0, bytes: 0 },
+ },
+ strings: { count: 1, bytes: 10 },
+ scripts: {
+ "foo.js": {
+ JSScript: { count: 1, bytes: 10 },
+ "js::jit::IonScript": { count: 1, bytes: 10 },
+ },
+ noFilename: {
+ JSScript: { count: 1, bytes: 10 },
+ "js::jit::IonScript": { count: 1, bytes: 10 },
+ },
+ },
+ other: {
+ "js::Shape": { count: 7, bytes: 70 },
+ "js::BaseShape": { count: 1, bytes: 10 },
+ },
+ domNode: {},
+ };
+
+ const root = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+ dumpn("CensusTreeNode tree = " + JSON.stringify(root, null, 4));
+
+ (function assertEveryNodeCanFindItsLeaf(node) {
+ if (node.reportLeafIndex) {
+ const [leaf] = CensusUtils.getReportLeaves(
+ new Set([node.reportLeafIndex]),
+ BREAKDOWN,
+ REPORT
+ );
+ ok(
+ leaf,
+ "Should be able to find leaf " +
+ "for a node with a reportLeafIndex = " +
+ node.reportLeafIndex
+ );
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ assertEveryNodeCanFindItsLeaf(child);
+ }
+ }
+ })(root);
+
+ // Test finding multiple leaves at a time.
+
+ function find(name, node) {
+ if (node.name === name) {
+ return node;
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const found = find(name, child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return undefined;
+ }
+
+ const arrayNode = find("Array", root);
+ ok(arrayNode);
+ equal(typeof arrayNode.reportLeafIndex, "number");
+
+ const shapeNode = find("js::Shape", root);
+ ok(shapeNode);
+ equal(typeof shapeNode.reportLeafIndex, "number");
+
+ const indices = new Set([
+ arrayNode.reportLeafIndex,
+ shapeNode.reportLeafIndex,
+ ]);
+ const leaves = CensusUtils.getReportLeaves(indices, BREAKDOWN, REPORT);
+ equal(leaves.length, 2);
+
+ // `getReportLeaves` does not guarantee order of the results, so handle both
+ // cases.
+ ok(leaves.some(l => l === REPORT.objects.Array));
+ ok(leaves.some(l => l === REPORT.other["js::Shape"]));
+
+ // Test that bad indices do not yield results.
+
+ const none = CensusUtils.getReportLeaves(
+ new Set([999999999999]),
+ BREAKDOWN,
+ REPORT
+ );
+ equal(none.length, 0);
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js
new file mode 100644
index 0000000000..0a46618003
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test saving a heap snapshot in the sandboxed e10s child process.
+
+function run_test() {
+ run_test_in_child("test_SaveHeapSnapshot.js");
+}
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.ini b/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..fca04273ea
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,100 @@
+[DEFAULT]
+tags = devtools heapsnapshot devtools-memory
+head = head_heapsnapshot.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+support-files =
+ Census.sys.mjs
+ dominator-tree-worker.js
+ heap-snapshot-worker.js
+ Match.sys.mjs
+
+[test_census_diff_01.js]
+[test_census_diff_02.js]
+[test_census_diff_03.js]
+[test_census_diff_04.js]
+[test_census_diff_05.js]
+[test_census_diff_06.js]
+[test_census_filtering_01.js]
+[test_census_filtering_02.js]
+[test_census_filtering_03.js]
+[test_census_filtering_04.js]
+[test_census_filtering_05.js]
+[test_census-tree-node-01.js]
+[test_census-tree-node-02.js]
+[test_census-tree-node-03.js]
+[test_census-tree-node-04.js]
+[test_census-tree-node-05.js]
+[test_census-tree-node-06.js]
+[test_census-tree-node-07.js]
+[test_census-tree-node-08.js]
+[test_census-tree-node-09.js]
+[test_census-tree-node-10.js]
+[test_countToBucketBreakdown_01.js]
+[test_deduplicatePaths_01.js]
+[test_DominatorTree_01.js]
+[test_DominatorTree_02.js]
+[test_DominatorTree_03.js]
+[test_DominatorTree_04.js]
+[test_DominatorTree_05.js]
+[test_DominatorTree_06.js]
+[test_DominatorTreeNode_attachShortestPaths_01.js]
+[test_DominatorTreeNode_getNodeByIdAlongPath_01.js]
+[test_DominatorTreeNode_insert_01.js]
+[test_DominatorTreeNode_insert_02.js]
+[test_DominatorTreeNode_insert_03.js]
+[test_DominatorTreeNode_LabelAndShallowSize_01.js]
+[test_DominatorTreeNode_LabelAndShallowSize_02.js]
+[test_DominatorTreeNode_LabelAndShallowSize_03.js]
+[test_DominatorTreeNode_LabelAndShallowSize_04.js]
+[test_DominatorTreeNode_partialTraversal_01.js]
+[test_getCensusIndividuals_01.js]
+[test_getReportLeaves_01.js]
+[test_HeapAnalyses_computeDominatorTree_01.js]
+[test_HeapAnalyses_computeDominatorTree_02.js]
+[test_HeapAnalyses_deleteHeapSnapshot_01.js]
+[test_HeapAnalyses_deleteHeapSnapshot_02.js]
+[test_HeapAnalyses_deleteHeapSnapshot_03.js]
+[test_HeapAnalyses_getCensusIndividuals_01.js]
+[test_HeapAnalyses_getCreationTime_01.js]
+[test_HeapAnalyses_getDominatorTree_01.js]
+[test_HeapAnalyses_getDominatorTree_02.js]
+[test_HeapAnalyses_getImmediatelyDominated_01.js]
+skip-if = tsan # Unreasonably slow, bug 1612707
+[test_HeapAnalyses_readHeapSnapshot_01.js]
+[test_HeapAnalyses_takeCensusDiff_01.js]
+[test_HeapAnalyses_takeCensusDiff_02.js]
+[test_HeapAnalyses_takeCensus_01.js]
+[test_HeapAnalyses_takeCensus_02.js]
+[test_HeapAnalyses_takeCensus_03.js]
+[test_HeapAnalyses_takeCensus_04.js]
+[test_HeapAnalyses_takeCensus_05.js]
+[test_HeapAnalyses_takeCensus_06.js]
+[test_HeapAnalyses_takeCensus_07.js]
+[test_HeapSnapshot_creationTime_01.js]
+[test_HeapSnapshot_deepStack_01.js]
+[test_HeapSnapshot_describeNode_01.js]
+[test_HeapSnapshot_computeShortestPaths_01.js]
+[test_HeapSnapshot_computeShortestPaths_02.js]
+[test_HeapSnapshot_getObjectNodeId_01.js]
+[test_HeapSnapshot_takeCensus_01.js]
+[test_HeapSnapshot_takeCensus_02.js]
+[test_HeapSnapshot_takeCensus_03.js]
+[test_HeapSnapshot_takeCensus_04.js]
+[test_HeapSnapshot_takeCensus_05.js]
+[test_HeapSnapshot_takeCensus_06.js]
+[test_HeapSnapshot_takeCensus_07.js]
+[test_HeapSnapshot_takeCensus_08.js]
+[test_HeapSnapshot_takeCensus_09.js]
+[test_HeapSnapshot_takeCensus_10.js]
+[test_HeapSnapshot_takeCensus_11.js]
+[test_HeapSnapshot_takeCensus_12.js]
+[test_ReadHeapSnapshot.js]
+[test_ReadHeapSnapshot_with_utf8_paths.js]
+[test_ReadHeapSnapshot_with_allocations.js]
+skip-if = os == 'linux' # Bug 1176173
+[test_ReadHeapSnapshot_worker.js]
+skip-if = os == 'linux' # Bug 1176173
+[test_SaveHeapSnapshot.js]
+[test_saveHeapSnapshot_e10s_01.js]