summaryrefslogtreecommitdiffstats
path: root/devtools/client/memory/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/memory/test')
-rw-r--r--devtools/client/memory/test/browser/browser.toml63
-rw-r--r--devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js52
-rw-r--r--devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_02.js51
-rw-r--r--devtools/client/memory/test/browser/browser_memory_clear_snapshots.js78
-rw-r--r--devtools/client/memory/test/browser/browser_memory_diff_01.js86
-rw-r--r--devtools/client/memory/test/browser/browser_memory_displays_01.js49
-rw-r--r--devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js178
-rw-r--r--devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js81
-rw-r--r--devtools/client/memory/test/browser/browser_memory_filter_01.js107
-rw-r--r--devtools/client/memory/test/browser/browser_memory_fission_switch_target.js79
-rw-r--r--devtools/client/memory/test/browser/browser_memory_individuals_01.js74
-rw-r--r--devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js112
-rw-r--r--devtools/client/memory/test/browser/browser_memory_keyboard.js111
-rw-r--r--devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js54
-rw-r--r--devtools/client/memory/test/browser/browser_memory_no_auto_expand.js49
-rw-r--r--devtools/client/memory/test/browser/browser_memory_percents_01.js62
-rw-r--r--devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js139
-rw-r--r--devtools/client/memory/test/browser/browser_memory_simple_01.js60
-rw-r--r--devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js34
-rw-r--r--devtools/client/memory/test/browser/browser_memory_tree_map-01.js136
-rw-r--r--devtools/client/memory/test/browser/browser_memory_tree_map-02.js199
-rw-r--r--devtools/client/memory/test/browser/doc_big_tree.html20
-rw-r--r--devtools/client/memory/test/browser/doc_empty.html9
-rw-r--r--devtools/client/memory/test/browser/doc_steady_allocation.html21
-rw-r--r--devtools/client/memory/test/browser/head.js269
-rw-r--r--devtools/client/memory/test/chrome/chrome.toml34
-rw-r--r--devtools/client/memory/test/chrome/head.js354
-rw-r--r--devtools/client/memory/test/chrome/test_CensusTreeItem_01.html65
-rw-r--r--devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html46
-rw-r--r--devtools/client/memory/test/chrome/test_DominatorTree_01.html52
-rw-r--r--devtools/client/memory/test/chrome/test_DominatorTree_02.html52
-rw-r--r--devtools/client/memory/test/chrome/test_DominatorTree_03.html77
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_01.html51
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_02.html78
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_03.html79
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_04.html122
-rw-r--r--devtools/client/memory/test/chrome/test_Heap_05.html133
-rw-r--r--devtools/client/memory/test/chrome/test_List_01.html73
-rw-r--r--devtools/client/memory/test/chrome/test_ShortestPaths_01.html112
-rw-r--r--devtools/client/memory/test/chrome/test_ShortestPaths_02.html46
-rw-r--r--devtools/client/memory/test/chrome/test_SnapshotListItem_01.html53
-rw-r--r--devtools/client/memory/test/chrome/test_Toolbar_01.html48
-rw-r--r--devtools/client/memory/test/chrome/test_TreeMap_01.html44
-rw-r--r--devtools/client/memory/test/xpcshell/.eslintrc.js14
-rw-r--r--devtools/client/memory/test/xpcshell/head.js189
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-clear-snapshots_01.js41
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-clear-snapshots_02.js61
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-clear-snapshots_03.js52
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-clear-snapshots_04.js55
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-clear-snapshots_05.js53
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-clear-snapshots_06.js68
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-export-snapshot.js45
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-filter-01.js23
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-filter-02.js88
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-filter-03.js69
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-import-snapshot-and-census.js135
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-import-snapshot-dominator-tree.js100
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-select-snapshot.js46
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-01.js186
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-02.js74
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-set-display.js74
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-take-census.js70
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-take-snapshot-and-census.js65
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-take-snapshot.js61
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-01.js98
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-02.js82
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-toggle-inverted.js31
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-toggle-recording-allocations.js53
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_01.js29
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_02.js59
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_03.js147
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_04.js101
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_05.js134
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_01.js79
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_02.js80
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_03.js77
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_04.js91
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_05.js66
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_06.js161
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_07.js181
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_08.js100
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_09.js92
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_10.js95
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_01.js77
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_02.js91
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_03.js114
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_04.js93
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_05.js84
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_06.js85
-rw-r--r--devtools/client/memory/test/xpcshell/test_pop_view_01.js80
-rw-r--r--devtools/client/memory/test/xpcshell/test_tree-map-01.js76
-rw-r--r--devtools/client/memory/test/xpcshell/test_tree-map-02.js105
-rw-r--r--devtools/client/memory/test/xpcshell/test_utils-get-snapshot-totals.js103
-rw-r--r--devtools/client/memory/test/xpcshell/test_utils.js117
-rw-r--r--devtools/client/memory/test/xpcshell/xpcshell.toml106
95 files changed, 8148 insertions, 0 deletions
diff --git a/devtools/client/memory/test/browser/browser.toml b/devtools/client/memory/test/browser/browser.toml
new file mode 100644
index 0000000000..e748c3c1c5
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser.toml
@@ -0,0 +1,63 @@
+[DEFAULT]
+tags = "devtools devtools-memory"
+subsuite = "devtools"
+support-files = [
+ "head.js",
+ "doc_big_tree.html",
+ "doc_empty.html",
+ "doc_steady_allocation.html",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+]
+
+["browser_memory_allocationStackDisplay_01.js"]
+skip-if = ["debug"] # bug 1219554
+
+["browser_memory_allocationStackDisplay_02.js"]
+skip-if = ["debug"] # bug 1219554
+
+["browser_memory_clear_snapshots.js"]
+
+["browser_memory_diff_01.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_memory_displays_01.js"]
+
+["browser_memory_dominator_trees_01.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["ccov"] # bug 1347244
+
+["browser_memory_dominator_trees_02.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["ccov"] # bug 1347244
+
+["browser_memory_filter_01.js"]
+skip-if = ["ccov"] # bug 1347244
+
+["browser_memory_fission_switch_target.js"]
+
+["browser_memory_individuals_01.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_memory_keyboard-snapshot-list.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_memory_keyboard.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_memory_no_allocation_stacks.js"]
+
+["browser_memory_no_auto_expand.js"]
+skip-if = ["debug"] # bug 1219554
+
+["browser_memory_percents_01.js"]
+
+["browser_memory_refresh_does_not_leak.js"]
+
+["browser_memory_simple_01.js"]
+
+["browser_memory_transferHeapSnapshot_e10s_01.js"]
+
+["browser_memory_tree_map-01.js"]
+
+["browser_memory_tree_map-02.js"]
diff --git a/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js
new file mode 100644
index 0000000000..b526025b17
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can show allocation stack displays in the tree.
+
+"use strict";
+
+const {
+ toggleRecordingAllocationStacks,
+} = require("resource://devtools/client/memory/actions/allocations.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const censusDisplayActions = require("resource://devtools/client/memory/actions/census-display.js");
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const { getState, dispatch } = panel.panelWin.gStore;
+ const front = getState().front;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ dispatch(
+ censusDisplayActions.setCensusDisplay(
+ censusDisplays.invertedAllocationStack
+ )
+ );
+ is(getState().censusDisplay.breakdown.by, "allocationStack");
+
+ await dispatch(toggleRecordingAllocationStacks(panel._commands));
+ ok(getState().allocations.recording);
+
+ // Let some allocations build up.
+ await waitForTime(500);
+
+ await dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ const names = [...doc.querySelectorAll(".frame-link-function-display-name")];
+ ok(names.length, "Should have rendered some allocation stack tree items");
+ ok(
+ names.some(e => !!e.textContent.trim()),
+ "And at least some of them should have functionDisplayNames"
+ );
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_02.js b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_02.js
new file mode 100644
index 0000000000..34492187a2
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_02.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can show allocation stack work when loading a new page
+
+"use strict";
+
+const {
+ toggleRecordingAllocationStacks,
+} = require("resource://devtools/client/memory/actions/allocations.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const censusDisplayActions = require("resource://devtools/client/memory/actions/census-display.js");
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "https://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest("about:blank", async function ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const { getState, dispatch } = panel.panelWin.gStore;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ dispatch(
+ censusDisplayActions.setCensusDisplay(
+ censusDisplays.invertedAllocationStack
+ )
+ );
+ is(getState().censusDisplay.breakdown.by, "allocationStack");
+
+ await dispatch(toggleRecordingAllocationStacks(panel._commands));
+ ok(getState().allocations.recording);
+
+ await navigateTo(TEST_URL);
+
+ const front = getState().front;
+ await dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ const names = [...doc.querySelectorAll(".frame-link-function-display-name")];
+ ok(names.length, "Should have rendered some allocation stack tree items");
+ ok(
+ names.some(e => !!e.textContent.trim()),
+ "And at least some of them should have functionDisplayNames"
+ );
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
new file mode 100644
index 0000000000..52eecd2838
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests taking and then clearing snapshots.
+ */
+
+const {
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const { gStore, document } = panel.panelWin;
+ const { getState } = gStore;
+
+ let snapshotEls = document.querySelectorAll(
+ "#memory-tool-container .list li"
+ );
+ is(getState().snapshots.length, 0, "Starts with no snapshots in store");
+ is(snapshotEls.length, 0, "No snapshots visible");
+
+ info("Take two snapshots");
+ takeSnapshot(panel.panelWin);
+ takeSnapshot(panel.panelWin);
+ takeSnapshot(panel.panelWin);
+ await waitUntilState(
+ gStore,
+ state =>
+ state.snapshots.length === 3 &&
+ state.snapshots[0].treeMap &&
+ state.snapshots[1].treeMap &&
+ state.snapshots[2].treeMap &&
+ state.snapshots[0].treeMap.state === treeMapState.SAVED &&
+ state.snapshots[1].treeMap.state === treeMapState.SAVED &&
+ state.snapshots[2].treeMap.state === treeMapState.SAVED
+ );
+
+ snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(snapshotEls.length, 3, "Three snapshots visible");
+ is(
+ document.querySelectorAll(".selected").length,
+ 1,
+ "One selected snapshot visible"
+ );
+ ok(snapshotEls[2].classList.contains("selected"), "Third snapshot selected");
+
+ info("Clicking on first snapshot delete button");
+ document.querySelectorAll(".delete")[0].click();
+
+ await waitUntilState(
+ gStore,
+ state =>
+ state.snapshots.length === 2 &&
+ state.snapshots[0].treeMap &&
+ state.snapshots[1].treeMap &&
+ state.snapshots[0].treeMap.state === treeMapState.SAVED &&
+ state.snapshots[1].treeMap.state === treeMapState.SAVED
+ );
+
+ snapshotEls = document.querySelectorAll(".snapshot-list-item");
+ is(snapshotEls.length, 2, "Two snapshots visible");
+ // Bug 1476289
+ ok(
+ !snapshotEls[0].classList.contains("selected"),
+ "First snapshot not selected"
+ );
+ ok(snapshotEls[1].classList.contains("selected"), "Second snapshot selected");
+
+ info("Click on Clear Snapshots");
+ await clearSnapshots(panel.panelWin);
+ is(getState().snapshots.length, 0, "No snapshots in store");
+ snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(snapshotEls.length, 0, "No snapshot visible");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_diff_01.js b/devtools/client/memory/test/browser/browser_memory_diff_01.js
new file mode 100644
index 0000000000..7b6487565f
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_diff_01.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test diffing.
+
+"use strict";
+
+const {
+ diffingState,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const store = panel.panelWin.gStore;
+ const { getState } = store;
+ const doc = panel.panelWin.document;
+
+ ok(!getState().diffing, "Not diffing by default.");
+
+ // Take two snapshots.
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+ await waitForTime(1000);
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ // Enable diffing mode.
+ const diffButton = doc.getElementById("diff-snapshots");
+ EventUtils.synthesizeMouseAtCenter(diffButton, {}, panel.panelWin);
+ await waitUntilState(
+ store,
+ state => !!state.diffing && state.diffing.state === diffingState.SELECTING
+ );
+ ok(true, "Clicking the diffing button put us into the diffing state.");
+ is(getDisplayedSnapshotStatus(doc), "Select the baseline snapshot");
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots.length === 2 &&
+ state.snapshots[0].treeMap &&
+ state.snapshots[1].treeMap &&
+ state.snapshots[0].treeMap.state === treeMapState.SAVED &&
+ state.snapshots[1].treeMap.state === treeMapState.SAVED
+ );
+
+ const listItems = [...doc.querySelectorAll(".snapshot-list-item")];
+ is(listItems.length, 2, "Should have two snapshot list items");
+
+ // Select the first snapshot.
+ EventUtils.synthesizeMouseAtCenter(listItems[0], {}, panel.panelWin);
+ await waitUntilState(
+ store,
+ state =>
+ state.diffing.state === diffingState.SELECTING &&
+ state.diffing.firstSnapshotId
+ );
+ is(
+ getDisplayedSnapshotStatus(doc),
+ "Select the snapshot to compare to the baseline"
+ );
+
+ // Select the second snapshot.
+ EventUtils.synthesizeMouseAtCenter(listItems[1], {}, panel.panelWin);
+ await waitUntilState(
+ store,
+ state => state.diffing.state === diffingState.TAKING_DIFF
+ );
+ ok(true, "Selecting two snapshots for diffing triggers computing the diff");
+
+ // .startsWith because the ellipsis is lost in translation.
+ ok(getDisplayedSnapshotStatus(doc).startsWith("Computing difference"));
+
+ await waitUntilState(
+ store,
+ state => state.diffing.state === diffingState.TOOK_DIFF
+ );
+ ok(true, "And that diff is computed successfully");
+ is(getDisplayedSnapshotStatus(doc), null, "No status text anymore");
+ ok(
+ doc.querySelector(".heap-tree-item"),
+ "And instead we should be showing the tree"
+ );
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_displays_01.js b/devtools/client/memory/test/browser/browser_memory_displays_01.js
new file mode 100644
index 0000000000..89296b77f4
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_displays_01.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the heap tree renders rows based on the display
+ */
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const { gStore, document } = panel.panelWin;
+
+ const { dispatch } = panel.panelWin.gStore;
+
+ function $$(selector) {
+ return [...document.querySelectorAll(selector)];
+ }
+ dispatch(changeView(viewState.CENSUS));
+
+ await takeSnapshot(panel.panelWin);
+
+ await waitUntilState(
+ gStore,
+ state =>
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED
+ );
+
+ info("Check coarse type heap view");
+
+ ["Function", "js::PropMap", "Object", "strings"].forEach(findNameCell);
+
+ await setCensusDisplay(panel.panelWin, censusDisplays.allocationStack);
+ info("Check allocation stack heap view");
+ [L10N.getStr("tree-item.nostack")].forEach(findNameCell);
+
+ function findNameCell(name) {
+ const el = $$(".tree .heap-tree-item-name").find(
+ e => e.textContent === name
+ );
+ ok(el, `Found heap tree item cell for ${name}.`);
+ }
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js b/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js
new file mode 100644
index 0000000000..1faaf365a1
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test for dominator trees, their focused nodes, and keyboard navigating
+// through nodes across incrementally fetching subtrees.
+
+"use strict";
+
+const {
+ dominatorTreeState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ expandDominatorTreeNode,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_big_tree.html";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ // Taking snapshots and computing dominator trees is slow :-/
+ requestLongerTimeout(4);
+
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ // Take a snapshot.
+
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ // Wait for the dominator tree to be computed and fetched.
+
+ await waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]);
+ ok(true, "Computed and fetched the dominator tree.");
+
+ // Expand all the dominator tree nodes that are eagerly fetched, except for
+ // the leaves which will trigger fetching their lazily loaded subtrees.
+
+ const id = getState().snapshots[0].id;
+ const root = getState().snapshots[0].dominatorTree.root;
+ (function expandAllEagerlyFetched(node = root) {
+ if (!node.moreChildrenAvailable || node.children) {
+ dispatch(expandDominatorTreeNode(id, node));
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ expandAllEagerlyFetched(child);
+ }
+ }
+ })();
+
+ // Find the deepest eagerly loaded node: one which has more children but none
+ // of them are loaded.
+
+ const deepest = (function findDeepest(node = root) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const found = findDeepest(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ })();
+
+ ok(deepest, "Found the deepest node");
+ ok(
+ !getState().snapshots[0].dominatorTree.expanded.has(deepest.nodeId),
+ "The deepest node should not be expanded"
+ );
+
+ // Select the deepest node.
+
+ EventUtils.synthesizeMouseAtCenter(
+ doc.querySelector(`.node-${deepest.nodeId}`),
+ {},
+ panel.panelWin
+ );
+ await waitUntilState(
+ store,
+ state => state.snapshots[0].dominatorTree.focused.nodeId === deepest.nodeId
+ );
+ ok(
+ doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
+ "The deepest node should be focused now"
+ );
+
+ // Expand the deepest node, which triggers an incremental fetch of its lazily
+ // loaded subtree.
+
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ await waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.expanded.has(deepest.nodeId)
+ );
+ is(
+ getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Expanding the deepest node should start an incremental fetch of its subtree"
+ );
+ ok(
+ doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
+ "The deepest node should still be focused after expansion"
+ );
+
+ // Wait for the incremental fetch to complete.
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "And the incremental fetch completes.");
+ ok(
+ doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
+ "The deepest node should still be focused after we have loaded its children"
+ );
+
+ // Find the most up-to-date version of the node whose children we just
+ // incrementally fetched.
+
+ const newDeepest = (function findNewDeepest(
+ node = getState().snapshots[0].dominatorTree.root
+ ) {
+ if (node.nodeId === deepest.nodeId) {
+ return node;
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const found = findNewDeepest(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ })();
+
+ ok(newDeepest, "We found the up-to-date version of deepest");
+ ok(newDeepest.children, "And its children are loaded");
+ ok(newDeepest.children.length, "And there are more than 0 children");
+
+ const firstChild = newDeepest.children[0];
+ ok(firstChild, "deepest should have a first child");
+ ok(
+ doc.querySelector(`.node-${firstChild.nodeId}`),
+ "and the first child should exist in the dom"
+ );
+
+ // Select the newly loaded first child by pressing the right arrow once more.
+
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ await waitUntilState(
+ store,
+ state => state.snapshots[0].dominatorTree.focused === firstChild
+ );
+ ok(
+ doc
+ .querySelector(`.node-${firstChild.nodeId}`)
+ .classList.contains("focused"),
+ "The first child should now be focused"
+ );
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js b/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js
new file mode 100644
index 0000000000..fea3603c52
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Integration test for mouse interaction in the dominator tree
+
+"use strict";
+
+const {
+ dominatorTreeState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+function clickOnNodeArrow(node, panel) {
+ EventUtils.synthesizeMouseAtCenter(
+ node.querySelector(".arrow"),
+ {},
+ panel.panelWin
+ );
+}
+
+this.test = makeMemoryTest(TEST_URL, async function ({ panel }) {
+ // Taking snapshots and computing dominator trees is slow :-/
+ requestLongerTimeout(4);
+
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ // Take a snapshot.
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ // Wait for the dominator tree to be computed and fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "Computed and fetched the dominator tree.");
+
+ const root = getState().snapshots[0].dominatorTree.root;
+ ok(
+ getState().snapshots[0].dominatorTree.expanded.has(root.nodeId),
+ "Root node is expanded by default"
+ );
+
+ // Click on root arrow to collapse the root element
+ const rootNode = doc.querySelector(`.node-${root.nodeId}`);
+ clickOnNodeArrow(rootNode, panel);
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ !state.snapshots[0].dominatorTree.expanded.has(root.nodeId)
+ );
+ ok(true, "Root node collapsed");
+
+ // Click on root arrow to expand it again
+ clickOnNodeArrow(rootNode, panel);
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.expanded.has(root.nodeId)
+ );
+ ok(true, "Root node is expanded again");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_filter_01.js b/devtools/client/memory/test/browser/browser_memory_filter_01.js
new file mode 100644
index 0000000000..00dcfdb951
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_filter_01.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can show allocation stack displays in the tree.
+
+"use strict";
+
+const {
+ dominatorTreeState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ changeViewAndRefresh,
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const store = panel.panelWin.gStore;
+ const { dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVING
+ );
+
+ let filterInput = doc.getElementById("filter");
+ EventUtils.synthesizeMouseAtCenter(filterInput, {}, panel.panelWin);
+ EventUtils.sendString("js::Shape", panel.panelWin);
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVING
+ );
+ ok(true, "adding a filter string should trigger census recompute");
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED
+ );
+
+ let nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
+ ok(nameElem, "Should get a tree item row with a name");
+ is(
+ nameElem.textContent.trim(),
+ "js::Shape",
+ "the tree item should be the one we filtered for"
+ );
+ is(
+ filterInput.value,
+ "js::Shape",
+ "and filter input contains the user value"
+ );
+
+ // Now switch the dominator view, then switch back to census view
+ // and check that the filter word is still correctly applied
+ dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
+ ok(true, "change view to dominator tree");
+
+ // Wait for the dominator tree to be computed and fetched.
+ await waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]);
+ ok(true, "computed and fetched the dominator tree.");
+
+ dispatch(changeViewAndRefresh(viewState.CENSUS, heapWorker));
+ ok(true, "change view back to census");
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED
+ );
+
+ nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
+ filterInput = doc.getElementById("filter");
+
+ ok(nameElem, "Should still get a tree item row with a name");
+ is(
+ nameElem.textContent.trim(),
+ "js::Shape",
+ "the tree item should still be the one we filtered for"
+ );
+ is(
+ filterInput.value,
+ "js::Shape",
+ "and filter input still contains the user value"
+ );
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_fission_switch_target.js b/devtools/client/memory/test/browser/browser_memory_fission_switch_target.js
new file mode 100644
index 0000000000..5d0d474d76
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_fission_switch_target.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test top-level target switching for memory panel.
+
+const {
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const PARENT_PROCESS_URI = "about:robots";
+const CONTENT_PROCESS_URI =
+ "data:text/html,<section>content process page</section>";
+const EXPECTED_ELEMENT_IN_PARENT_PROCESS = "button";
+const EXPECTED_ELEMENT_IN_CONTENT_PROCESS = "section";
+
+add_task(async () => {
+ info("Open the memory panel with empty page");
+ const tab = await addTab();
+ const { panel } = await openMemoryPanel(tab);
+ const { gStore: store } = panel.panelWin;
+
+ info("Open a page running on the content process");
+ await navigateTo(CONTENT_PROCESS_URI);
+ await takeAndWaitSnapshot(
+ panel.panelWin,
+ store,
+ EXPECTED_ELEMENT_IN_CONTENT_PROCESS
+ );
+ ok(true, "Can take a snapshot for content process page correctly");
+
+ info("Navigate to a page running on parent process");
+ await navigateTo(PARENT_PROCESS_URI);
+ await takeAndWaitSnapshot(
+ panel.panelWin,
+ store,
+ EXPECTED_ELEMENT_IN_PARENT_PROCESS
+ );
+ ok(true, "Can take a snapshot for parent process page correctly");
+
+ info("Return to a page running on content process again");
+ await navigateTo(CONTENT_PROCESS_URI);
+ await takeAndWaitSnapshot(
+ panel.panelWin,
+ store,
+ EXPECTED_ELEMENT_IN_CONTENT_PROCESS
+ );
+ ok(
+ true,
+ "Can take a snapshot for content process page correctly after switching targets twice"
+ );
+});
+
+async function takeAndWaitSnapshot(window, store, expectedElement) {
+ await asyncWaitUntil(async () => {
+ await takeSnapshot(window);
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].treeMap &&
+ state.snapshots[0].treeMap.state === treeMapState.SAVED
+ );
+
+ const snapshot = store.getState().snapshots[0];
+ const nodeNames = getNodeNames(snapshot);
+
+ await clearSnapshots(window);
+
+ return nodeNames.includes(expectedElement);
+ });
+}
+
+function getNodeNames(snapshot) {
+ const domNodePart = snapshot.treeMap.report.children.find(
+ child => child.name === "domNode"
+ );
+ return domNodePart.children.map(child => child.name.toLowerCase());
+}
diff --git a/devtools/client/memory/test/browser/browser_memory_individuals_01.js b/devtools/client/memory/test/browser/browser_memory_individuals_01.js
new file mode 100644
index 0000000000..f54154b949
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_individuals_01.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can show census group individuals, and then go back to
+// the previous view.
+
+"use strict";
+
+const {
+ individualsState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const store = panel.panelWin.gStore;
+ const { dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Take a snapshot and wait for the census to finish.
+
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ await waitUntilState(store, state => {
+ return (
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED
+ );
+ });
+
+ // Click on the first individuals button found, and wait for the individuals
+ // to be fetched.
+
+ const individualsButton = doc.querySelector(".individuals-button");
+ EventUtils.synthesizeMouseAtCenter(individualsButton, {}, panel.panelWin);
+
+ await waitUntilState(store, state => {
+ return (
+ state.view.state === viewState.INDIVIDUALS &&
+ state.individuals &&
+ state.individuals.state === individualsState.FETCHED
+ );
+ });
+
+ ok(
+ doc.getElementById("shortest-paths"),
+ "Should be showing the shortest paths component"
+ );
+ ok(doc.querySelector(".heap-tree-item"), "Should be showing the individuals");
+
+ // Go back to the previous view.
+
+ const popViewButton = doc.getElementById("pop-view-button");
+ ok(popViewButton, "Should be showing the #pop-view-button");
+ EventUtils.synthesizeMouseAtCenter(popViewButton, {}, panel.panelWin);
+
+ await waitUntilState(store, state => {
+ return state.view.state === viewState.CENSUS;
+ });
+
+ ok(
+ !doc.getElementById("shortest-paths"),
+ "Should not be showing the shortest paths component anymore"
+ );
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js
new file mode 100644
index 0000000000..7a2e2e12f7
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that using ACCEL+UP/DOWN, the user can navigate between snapshots.
+
+"use strict";
+
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ panel }) {
+ // Creating snapshots already takes ~25 seconds on linux 32 debug machines
+ // which makes the test very likely to go over the allowed timeout
+ requestLongerTimeout(2);
+
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const store = panel.panelWin.gStore;
+ const { dispatch } = store;
+ const front = store.getState().front;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ info("Take 3 snapshots");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots.length == 3 &&
+ state.snapshots.every(
+ s => s.census && s.census.state === censusState.SAVED
+ )
+ );
+ ok(true, "All snapshots censuses are in SAVED state");
+
+ await waitUntilSnapshotSelected(store, 2);
+ ok(true, "Third snapshot selected after creating all snapshots.");
+
+ info("Press ACCEL+UP key, expect second snapshot selected.");
+ EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
+ await waitUntilSnapshotSelected(store, 1);
+ ok(true, "Second snapshot selected after alt+UP.");
+
+ info("Press ACCEL+UP key, expect first snapshot selected.");
+ EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
+ await waitUntilSnapshotSelected(store, 0);
+ ok(true, "First snapshot is selected after ACCEL+UP");
+
+ info("Check ACCEL+UP is a noop when the first snapshot is selected.");
+ EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
+ // We assume the snapshot selection should be synchronous here.
+ is(getSelectedSnapshotIndex(store), 0, "First snapshot is still selected");
+
+ info("Press ACCEL+DOWN key, expect second snapshot selected.");
+ EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin);
+ await waitUntilSnapshotSelected(store, 1);
+ ok(true, "Second snapshot is selected after ACCEL+DOWN");
+
+ info("Click on first node.");
+ const firstNode = doc.querySelector(".tree .heap-tree-item-name");
+ EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin);
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[1].census.focused ===
+ state.snapshots[1].census.report.children[0]
+ );
+ ok(true, "First root is selected after click.");
+
+ info("Press DOWN key, expect second root focused.");
+ EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[1].census.focused ===
+ state.snapshots[1].census.report.children[1]
+ );
+ ok(true, "Second root is selected after pressing DOWN.");
+ is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected");
+
+ info("Press UP key, expect second root focused.");
+ EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[1].census.focused ===
+ state.snapshots[1].census.report.children[0]
+ );
+ ok(true, "First root is selected after pressing UP.");
+ is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected");
+
+ info("Press ACCEL+DOWN key, expect third snapshot selected.");
+ EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin);
+ await waitUntilSnapshotSelected(store, 2);
+ ok(true, "Thirdˆ snapshot is selected after ACCEL+DOWN");
+
+ info("Check ACCEL+DOWN is a noop when the last snapshot is selected.");
+ EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin);
+ // We assume the snapshot selection should be synchronous here.
+ is(getSelectedSnapshotIndex(store), 2, "Third snapshot is still selected");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_keyboard.js b/devtools/client/memory/test/browser/browser_memory_keyboard.js
new file mode 100644
index 0000000000..0fba33c456
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Bug 1246570 - Check that when pressing on LEFT arrow, the parent tree node
+// gets focused.
+
+"use strict";
+
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+function waitUntilFocused(store, node) {
+ return waitUntilState(
+ store,
+ state =>
+ state.snapshots.length === 1 &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED &&
+ state.snapshots[0].census.focused &&
+ state.snapshots[0].census.focused === node
+ );
+}
+
+function waitUntilExpanded(store, node) {
+ return waitUntilState(
+ store,
+ state =>
+ state.snapshots[0] &&
+ state.snapshots[0].census &&
+ state.snapshots[0].census.expanded.has(node.id)
+ );
+}
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const front = getState().front;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ is(getState().censusDisplay.breakdown.by, "coarseType");
+
+ await dispatch(takeSnapshotAndCensus(front, heapWorker));
+ const census = getState().snapshots[0].census;
+ const root1 = census.report.children[0];
+ const root2 = census.report.children[0];
+ const root3 = census.report.children[0];
+ const root4 = census.report.children[0];
+ const child1 = root1.children[0];
+
+ info("Click on first node.");
+ const firstNode = doc.querySelector(".tree .heap-tree-item-name");
+ EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin);
+ await waitUntilFocused(store, root1);
+ ok(true, "First root is selected after click.");
+
+ info("Press DOWN key, expect second root focused.");
+ EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
+ await waitUntilFocused(store, root2);
+ ok(true, "Second root is selected after pressing DOWN arrow.");
+
+ info("Press DOWN key, expect third root focused.");
+ EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
+ await waitUntilFocused(store, root3);
+ ok(true, "Third root is selected after pressing DOWN arrow.");
+
+ info("Press DOWN key, expect fourth root focused.");
+ EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
+ await waitUntilFocused(store, root4);
+ ok(true, "Fourth root is selected after pressing DOWN arrow.");
+
+ info("Press UP key, expect third root focused.");
+ EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
+ await waitUntilFocused(store, root3);
+ ok(true, "Third root is selected after pressing UP arrow.");
+
+ info("Press UP key, expect second root focused.");
+ EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
+ await waitUntilFocused(store, root2);
+ ok(true, "Second root is selected after pressing UP arrow.");
+
+ info("Press UP key, expect first root focused.");
+ EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
+ await waitUntilFocused(store, root1);
+ ok(true, "First root is selected after pressing UP arrow.");
+
+ info("Press RIGHT key");
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ await waitUntilExpanded(store, root1);
+ ok(true, "Root node is expanded.");
+
+ info("Press RIGHT key, expect first child focused.");
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ await waitUntilFocused(store, child1);
+ ok(true, "First child is selected after pressing RIGHT arrow.");
+
+ info("Press LEFT key, expect first root focused.");
+ EventUtils.synthesizeKey("VK_LEFT", {}, panel.panelWin);
+ await waitUntilFocused(store, root1);
+ ok(true, "First root is selected after pressing LEFT arrow.");
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js
new file mode 100644
index 0000000000..b8b09b35d0
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can show allocation stack displays in the tree.
+
+"use strict";
+
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const censusDisplayActions = require("resource://devtools/client/memory/actions/census-display.js");
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const { getState, dispatch } = panel.panelWin.gStore;
+ const front = getState().front;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ ok(!getState().allocations.recording, "Should not be recording allocagtions");
+
+ await dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await dispatch(
+ censusDisplayActions.setCensusDisplayAndRefresh(
+ heapWorker,
+ censusDisplays.allocationStack
+ )
+ );
+
+ is(
+ getState().censusDisplay.breakdown.by,
+ "allocationStack",
+ "Should be using allocation stack breakdown"
+ );
+
+ ok(
+ !getState().allocations.recording,
+ "Should still not be recording allocagtions"
+ );
+
+ ok(
+ doc.querySelector(".no-allocation-stacks"),
+ "Because we did not record allocations, " +
+ "the no-allocation-stack warning should be visible"
+ );
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
new file mode 100644
index 0000000000..9704d925d1
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Bug 1221150 - Ensure that census trees do not accidentally auto expand
+// when clicking on the allocation stacks checkbox.
+
+"use strict";
+
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const { getState, dispatch } = panel.panelWin.gStore;
+ const front = getState().front;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ await dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ is(getState().allocations.recording, false);
+ const recordingCheckbox = doc.getElementById(
+ "record-allocation-stacks-checkbox"
+ );
+ EventUtils.synthesizeMouseAtCenter(recordingCheckbox, {}, panel.panelWin);
+ is(getState().allocations.recording, true);
+
+ const nameElems = [
+ ...doc.querySelectorAll(".heap-tree-item-field.heap-tree-item-name"),
+ ];
+
+ for (const el of nameElems) {
+ dumpn(`Found ${el.textContent.trim()}`);
+ is(
+ el.style.marginInlineStart,
+ "0px",
+ "None of the elements should be an indented/expanded child"
+ );
+ }
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_percents_01.js b/devtools/client/memory/test/browser/browser_memory_percents_01.js
new file mode 100644
index 0000000000..c4bb254b07
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_percents_01.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we calculate percentages in the tree.
+
+"use strict";
+
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+function checkCells(cells) {
+ Assert.greater(cells.length, 1, "Should have found some");
+ // Ignore the first header cell.
+ for (const cell of cells.slice(1)) {
+ const percent = cell.querySelector(".heap-tree-percent");
+ ok(percent, "should have a percent cell");
+ ok(
+ percent.textContent.match(/^\d?\d%$/),
+ "should be of the form nn% or n%"
+ );
+ }
+}
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const { getState, dispatch } = panel.panelWin.gStore;
+ const front = getState().front;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ await dispatch(takeSnapshotAndCensus(front, heapWorker));
+ is(
+ getState().censusDisplay.breakdown.by,
+ "coarseType",
+ "Should be using coarse type breakdown"
+ );
+
+ const bytesCells = [...doc.querySelectorAll(".heap-tree-item-bytes")];
+ checkCells(bytesCells);
+
+ const totalBytesCells = [
+ ...doc.querySelectorAll(".heap-tree-item-total-bytes"),
+ ];
+ checkCells(totalBytesCells);
+
+ const countCells = [...doc.querySelectorAll(".heap-tree-item-count")];
+ checkCells(countCells);
+
+ const totalCountCells = [
+ ...doc.querySelectorAll(".heap-tree-item-total-count"),
+ ];
+ checkCells(totalCountCells);
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js b/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js
new file mode 100644
index 0000000000..e0ee1eee41
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global ChromeUtils */
+
+// Test that refreshing the page with devtools open does not leak the old
+// windows from previous navigations.
+//
+// IF THIS TEST STARTS FAILING, YOU ARE LEAKING EVERY WINDOW EVER NAVIGATED TO
+// WHILE DEVTOOLS ARE OPEN! THIS IS NOT SPECIFIC TO THE MEMORY TOOL ONLY!
+
+"use strict";
+
+const {
+ getLabelAndShallowSize,
+} = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_empty.html";
+
+async function getWindowsInSnapshot(front) {
+ dumpn("Taking snapshot.");
+ const path = await front.saveHeapSnapshot();
+ dumpn("Took snapshot with path = " + path);
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ dumpn("Read snapshot into memory, taking census.");
+ const report = snapshot.takeCensus({
+ breakdown: {
+ by: "objectClass",
+ then: { by: "bucket" },
+ other: { by: "count", count: true, bytes: false },
+ },
+ });
+ dumpn("Took census, window count = " + report.Window.count);
+ return report.Window;
+}
+
+const DESCRIPTION = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: false },
+ other: { by: "count", count: true, bytes: false },
+ },
+ strings: { by: "count", count: true, bytes: false },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: false },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: false },
+ },
+};
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ let front = panel.panelWin.gStore.getState().front;
+
+ const startWindows = await getWindowsInSnapshot(front);
+ dumpn(
+ "Initial windows found = " +
+ startWindows.map(w => "0x" + w.toString(16)).join(", ")
+ );
+ is(startWindows.length, 1);
+
+ await reloadBrowser();
+
+ // Update the front as we may have switched to a new target and a new memory front
+ front = panel.panelWin.gStore.getState().front;
+
+ const endWindows = await getWindowsInSnapshot(front);
+ is(endWindows.length, 1);
+
+ if (endWindows.length === 1) {
+ return;
+ }
+
+ dumpn("Test failed, diagnosing leaking windows.");
+ dumpn(
+ "(This may fail if a moving GC has relocated the initial Window objects.)"
+ );
+
+ dumpn("Taking full runtime snapshot.");
+ const path = await front.saveHeapSnapshot({ boundaries: { runtime: true } });
+ dumpn("Full runtime's snapshot path = " + path);
+
+ dumpn("Reading full runtime heap snapshot.");
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ dumpn("Done reading full runtime heap snapshot.");
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ const paths = snapshot.computeShortestPaths(
+ dominatorTree.root,
+ startWindows,
+ 50
+ );
+
+ for (let i = 0; i < startWindows.length; i++) {
+ dumpn(
+ "Shortest retaining paths for leaking Window 0x" +
+ startWindows[i].toString(16) +
+ " ========================="
+ );
+ let j = 0;
+ for (const retainingPath of paths.get(startWindows[i])) {
+ if (retainingPath.find(part => part.predecessor === startWindows[i])) {
+ // Skip paths that loop out from the target window and back to it again.
+ continue;
+ }
+
+ dumpn(
+ " Path #" +
+ ++j +
+ ": --------------------------------------------------------------------"
+ );
+ for (const part of retainingPath) {
+ const { label } = getLabelAndShallowSize(
+ part.predecessor,
+ snapshot,
+ DESCRIPTION
+ );
+ dumpn(
+ " 0x" +
+ part.predecessor.toString(16) +
+ " (" +
+ label.join(" > ") +
+ ")"
+ );
+ dumpn(" |");
+ dumpn(" " + part.edge);
+ dumpn(" |");
+ dumpn(" V");
+ }
+ dumpn(
+ " 0x" + startWindows[i].toString(16) + " (objects > Window)"
+ );
+ }
+ }
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_simple_01.js b/devtools/client/memory/test/browser/browser_memory_simple_01.js
new file mode 100644
index 0000000000..a983e23395
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_simple_01.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests taking snapshots and default states.
+ */
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const { gStore, document } = panel.panelWin;
+ const { getState, dispatch } = gStore;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ let snapshotEls = document.querySelectorAll(
+ "#memory-tool-container .list li"
+ );
+ is(getState().snapshots.length, 0, "Starts with no snapshots in store");
+ is(snapshotEls.length, 0, "No snapshots rendered");
+
+ await takeSnapshot(panel.panelWin);
+ snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(getState().snapshots.length, 1, "One snapshot was created in store");
+ is(snapshotEls.length, 1, "One snapshot was rendered");
+ ok(
+ snapshotEls[0].classList.contains("selected"),
+ "Only snapshot has `selected` class"
+ );
+
+ await takeSnapshot(panel.panelWin);
+ snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
+ is(getState().snapshots.length, 2, "Two snapshots created in store");
+ is(snapshotEls.length, 2, "Two snapshots rendered");
+ ok(
+ !snapshotEls[0].classList.contains("selected"),
+ "First snapshot no longer has `selected` class"
+ );
+ ok(
+ snapshotEls[1].classList.contains("selected"),
+ "Second snapshot has `selected` class"
+ );
+
+ await waitUntilCensusState(gStore, s => s.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+
+ ok(
+ document.querySelector(".heap-tree-item-name"),
+ "Should have rendered some tree items"
+ );
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js b/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js
new file mode 100644
index 0000000000..c647836332
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global ChromeUtils, HeapSnapshot */
+
+// Test that we can save a heap snapshot and transfer it over the RDP in e10s
+// where the child process is sandboxed and so we have to use
+// HeapSnapshotFileActor to get the heap snapshot file.
+
+"use strict";
+
+const TEST_URL = "data:text/html,<html><body></body></html>";
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const memoryFront = panel.panelWin.gStore.getState().front;
+ ok(memoryFront, "Should get the MemoryFront");
+
+ const snapshotFilePath = await memoryFront.saveHeapSnapshot({
+ // Force a copy so that we go through the HeapSnapshotFileActor's
+ // transferHeapSnapshot request and exercise this code path on e10s.
+ forceCopy: true,
+ });
+
+ ok(
+ !!(await IOUtils.stat(snapshotFilePath)),
+ "Should have the heap snapshot file"
+ );
+
+ const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath);
+ ok(
+ HeapSnapshot.isInstance(snapshot),
+ "And we should be able to read a HeapSnapshot instance from the file"
+ );
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_tree_map-01.js b/devtools/client/memory/test/browser/browser_memory_tree_map-01.js
new file mode 100644
index 0000000000..c65b7fc079
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_tree_map-01.js
@@ -0,0 +1,136 @@
+/* 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/. */
+
+// Make sure the canvases are created correctly
+
+"use strict";
+
+const CanvasUtils = require("resource://devtools/client/memory/components/tree-map/canvas-utils.js");
+const D3_SCRIPT =
+ '<script type="application/javascript" ' +
+ 'src="chrome://global/content/third_party/d3/d3.js">';
+const TEST_URL = `data:text/html,<html><body>${D3_SCRIPT}</body></html>`;
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const document = panel.panelWin.document;
+ const window = panel.panelWin;
+ const div = document.createElement("div");
+
+ Object.assign(div.style, {
+ width: "100px",
+ height: "200px",
+ position: "absolute",
+ });
+
+ document.body.appendChild(div);
+
+ info("Create the canvases");
+
+ const canvases = new CanvasUtils(div, 0);
+
+ info("Test the shape of the returned object");
+
+ is(typeof canvases, "object", "Canvases create an object");
+ is(typeof canvases.emit, "function", "Decorated with an EventEmitter");
+ is(typeof canvases.on, "function", "Decorated with an EventEmitter");
+ is(div.children[0], canvases.container, "Div has the container");
+ ok(
+ canvases.main.canvas instanceof window.HTMLCanvasElement,
+ "Creates the main canvas"
+ );
+ ok(
+ canvases.zoom.canvas instanceof window.HTMLCanvasElement,
+ "Creates the zoom canvas"
+ );
+ ok(
+ canvases.main.ctx instanceof window.CanvasRenderingContext2D,
+ "Creates the main canvas context"
+ );
+ ok(
+ canvases.zoom.ctx instanceof window.CanvasRenderingContext2D,
+ "Creates the zoom canvas context"
+ );
+
+ info("Test resizing");
+
+ let timesResizeCalled = 0;
+ canvases.on("resize", function () {
+ timesResizeCalled++;
+ });
+
+ const main = canvases.main.canvas;
+ const zoom = canvases.zoom.canvas;
+ const ratio = window.devicePixelRatio;
+
+ is(
+ main.width,
+ 100 * ratio,
+ "Main canvas width is the same as the parent div"
+ );
+ is(
+ main.height,
+ 200 * ratio,
+ "Main canvas height is the same as the parent div"
+ );
+ is(
+ zoom.width,
+ 100 * ratio,
+ "Zoom canvas width is the same as the parent div"
+ );
+ is(
+ zoom.height,
+ 200 * ratio,
+ "Zoom canvas height is the same as the parent div"
+ );
+ is(timesResizeCalled, 0, "Resize was not emitted");
+
+ div.style.width = "500px";
+ div.style.height = "700px";
+
+ window.dispatchEvent(new Event("resize"));
+
+ is(
+ main.width,
+ 500 * ratio,
+ "Main canvas width is resized to be the same as the parent div"
+ );
+ is(
+ main.height,
+ 700 * ratio,
+ "Main canvas height is resized to be the same as the parent div"
+ );
+ is(
+ zoom.width,
+ 500 * ratio,
+ "Zoom canvas width is resized to be the same as the parent div"
+ );
+ is(
+ zoom.height,
+ 700 * ratio,
+ "Zoom canvas height is resized to be the same as the parent div"
+ );
+ is(timesResizeCalled, 1, "'resize' was emitted was emitted");
+
+ div.style.width = "1100px";
+ div.style.height = "1300px";
+
+ canvases.destroy();
+ window.dispatchEvent(new Event("resize"));
+
+ is(main.width, 500 * ratio, "Main canvas width is not resized after destroy");
+ is(
+ main.height,
+ 700 * ratio,
+ "Main canvas height is not resized after destroy"
+ );
+ is(zoom.width, 500 * ratio, "Zoom canvas width is not resized after destroy");
+ is(
+ zoom.height,
+ 700 * ratio,
+ "Zoom canvas height is not resized after destroy"
+ );
+ is(timesResizeCalled, 1, "onResize was not called again");
+
+ document.body.removeChild(div);
+});
diff --git a/devtools/client/memory/test/browser/browser_memory_tree_map-02.js b/devtools/client/memory/test/browser/browser_memory_tree_map-02.js
new file mode 100644
index 0000000000..890ede23b5
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_tree_map-02.js
@@ -0,0 +1,199 @@
+/* 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 the drag and zooming behavior
+
+"use strict";
+
+const CanvasUtils = require("resource://devtools/client/memory/components/tree-map/canvas-utils.js");
+const DragZoom = require("resource://devtools/client/memory/components/tree-map/drag-zoom.js");
+
+const TEST_URL = "data:text/html,<html><body></body></html>";
+const PIXEL_SCROLL_MODE = 0;
+const PIXEL_DELTA = 10;
+const MAX_RAF_LOOP = 1000;
+
+this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ const panelWin = panel.panelWin;
+ const panelDoc = panelWin.document;
+ const div = panelDoc.createElement("div");
+
+ Object.assign(div.style, {
+ width: "100px",
+ height: "200px",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ });
+
+ const rafMock = createRAFMock();
+
+ panelDoc.body.appendChild(div);
+
+ const canvases = new CanvasUtils(div, 0);
+ const dragZoom = new DragZoom(canvases.container, 0, rafMock.raf);
+ const style = canvases.container.style;
+
+ info("Check initial state of dragZoom");
+ {
+ is(dragZoom.zoom, 0, "Zooming starts at 0");
+ is(dragZoom.smoothZoom, 0, "Smoothed zooming starts at 0");
+ is(rafMock.timesCalled, 0, "No RAFs have been queued");
+ is(
+ style.transform,
+ "translate(0px) scale(1)",
+ "No transforms have been done."
+ );
+
+ canvases.container.dispatchEvent(
+ new WheelEvent("wheel", {
+ deltaY: -PIXEL_DELTA,
+ deltaMode: PIXEL_SCROLL_MODE,
+ })
+ );
+
+ is(
+ style.transform,
+ "translate(0px) scale(1.05)",
+ "The div has been slightly scaled."
+ );
+ is(
+ dragZoom.zoom,
+ PIXEL_DELTA * dragZoom.ZOOM_SPEED,
+ "The zoom was increased"
+ );
+ ok(
+ floatEquality(dragZoom.smoothZoom, 0.05),
+ "The smooth zoom is between the initial value and the target"
+ );
+ is(rafMock.timesCalled, 1, "A RAF has been queued");
+ }
+
+ info("RAF will eventually stop once the smooth values approach the target");
+ {
+ let i;
+ let lastCallCount;
+ for (i = 0; i < MAX_RAF_LOOP; i++) {
+ if (lastCallCount === rafMock.timesCalled) {
+ break;
+ }
+ lastCallCount = rafMock.timesCalled;
+ rafMock.nextFrame();
+ }
+ is(
+ style.transform,
+ "translate(0px) scale(1.1)",
+ "The scale has been fully applied"
+ );
+ is(
+ dragZoom.zoom,
+ dragZoom.smoothZoom,
+ "The smooth and target zoom values match"
+ );
+ isnot(MAX_RAF_LOOP, i, "The RAF loop correctly stopped");
+ }
+
+ info("Dragging correctly translates the div");
+ {
+ div.dispatchEvent(
+ new MouseEvent("mousemove", {
+ clientX: 10,
+ clientY: 10,
+ })
+ );
+ div.dispatchEvent(new MouseEvent("mousedown"));
+ div.dispatchEvent(
+ new MouseEvent("mousemove", {
+ clientX: 20,
+ clientY: 20,
+ })
+ );
+ div.dispatchEvent(new MouseEvent("mouseup"));
+
+ is(
+ style.transform,
+ "translate(2.5px, 5px) scale(1.1)",
+ "The style is correctly translated"
+ );
+ ok(
+ floatEquality(dragZoom.translateX, 5),
+ "Translate X moved by some pixel amount"
+ );
+ ok(
+ floatEquality(dragZoom.translateY, 10),
+ "Translate Y moved by some pixel amount"
+ );
+ }
+
+ info("Zooming centers around the mouse");
+ {
+ canvases.container.dispatchEvent(
+ new WheelEvent("wheel", {
+ deltaY: -PIXEL_DELTA,
+ deltaMode: PIXEL_SCROLL_MODE,
+ })
+ );
+ // Run through the RAF loop to zoom in towards that value.
+ let lastCallCount;
+ for (let i = 0; i < MAX_RAF_LOOP; i++) {
+ if (lastCallCount === rafMock.timesCalled) {
+ break;
+ }
+ lastCallCount = rafMock.timesCalled;
+ rafMock.nextFrame();
+ }
+ is(
+ style.transform,
+ "translate(8.18182px, 18.1818px) scale(1.2)",
+ "Zooming affects the translation to keep the mouse centered"
+ );
+ ok(
+ floatEquality(dragZoom.translateX, 8.181818181818185),
+ "Translate X was affected by the mouse position"
+ );
+ ok(
+ floatEquality(dragZoom.translateY, 18.18181818181817),
+ "Translate Y was affected by the mouse position"
+ );
+ is(dragZoom.zoom, 0.2, "Zooming starts at 0");
+ }
+
+ dragZoom.destroy();
+
+ info("Scroll isn't tracked after destruction");
+ {
+ const previousZoom = dragZoom.zoom;
+ const previousSmoothZoom = dragZoom.smoothZoom;
+
+ canvases.container.dispatchEvent(
+ new WheelEvent("wheel", {
+ deltaY: -PIXEL_DELTA,
+ deltaMode: PIXEL_SCROLL_MODE,
+ })
+ );
+
+ is(dragZoom.zoom, previousZoom, "The zoom stayed the same");
+ is(
+ dragZoom.smoothZoom,
+ previousSmoothZoom,
+ "The smooth zoom stayed the same"
+ );
+ }
+
+ info("Translation isn't tracked after destruction");
+ {
+ const initialX = dragZoom.translateX;
+ const initialY = dragZoom.translateY;
+
+ div.dispatchEvent(new MouseEvent("mousedown"));
+ div.dispatchEvent(new MouseEvent("mousemove"), {
+ clientX: 40,
+ clientY: 40,
+ });
+ div.dispatchEvent(new MouseEvent("mouseup"));
+ is(dragZoom.translateX, initialX, "The translationX didn't change");
+ is(dragZoom.translateY, initialY, "The translationY didn't change");
+ }
+ panelDoc.body.removeChild(div);
+});
diff --git a/devtools/client/memory/test/browser/doc_big_tree.html b/devtools/client/memory/test/browser/doc_big_tree.html
new file mode 100644
index 0000000000..9fe74cd28b
--- /dev/null
+++ b/devtools/client/memory/test/browser/doc_big_tree.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ <script>
+ "use strict";
+
+ window.big = (function makeBig(depth = 0) {
+ let big = Array(5);
+ big.fill(undefined);
+ if (depth < 5) {
+ big = big.map(_ => makeBig(depth + 1));
+ }
+ return big;
+ }());
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/memory/test/browser/doc_empty.html b/devtools/client/memory/test/browser/doc_empty.html
new file mode 100644
index 0000000000..ef123d8d20
--- /dev/null
+++ b/devtools/client/memory/test/browser/doc_empty.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ This is an empty window.
+ </body>
+</html>
diff --git a/devtools/client/memory/test/browser/doc_steady_allocation.html b/devtools/client/memory/test/browser/doc_steady_allocation.html
new file mode 100644
index 0000000000..3e168507fa
--- /dev/null
+++ b/devtools/client/memory/test/browser/doc_steady_allocation.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ </head>
+ <body>
+ <script>
+ "use strict";
+
+ var objects = window.objects = [];
+ var allocate = this.allocate = function allocate() {
+ for (let i = 0; i < 100; i++) {
+ objects.push({});
+ }
+ setTimeout(allocate, 10);
+ };
+
+ allocate();
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/memory/test/browser/head.js b/devtools/client/memory/test/browser/head.js
new file mode 100644
index 0000000000..19a42c5494
--- /dev/null
+++ b/devtools/client/memory/test/browser/head.js
@@ -0,0 +1,269 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Load the shared test helpers into this compartment.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+var {
+ censusDisplays,
+ censusState,
+ snapshotState: states,
+} = require("resource://devtools/client/memory/constants.js");
+var { L10N } = require("resource://devtools/client/memory/utils.js");
+
+Services.prefs.setBoolPref("devtools.memory.enabled", true);
+
+/**
+ * Open the memory panel for the given tab.
+ */
+this.openMemoryPanel = async function (tab) {
+ info("Opening memory panel.");
+ const toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "memory" });
+ info("Memory panel shown successfully.");
+ const panel = toolbox.getCurrentPanel();
+ return { tab, panel };
+};
+
+/**
+ * Close the memory panel for the given tab.
+ */
+this.closeMemoryPanel = async function (tab) {
+ info("Closing memory panel.");
+ const toolbox = gDevTools.getToolboxForTab(tab);
+ await toolbox.destroy();
+ info("Closed memory panel successfully.");
+};
+
+/**
+ * Return a test function that adds a tab with the given url, opens the memory
+ * panel, runs the given generator, closes the memory panel, removes the tab,
+ * and finishes.
+ *
+ * Example usage:
+ *
+ * this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) {
+ * // Your tests go here...
+ * });
+ */
+function makeMemoryTest(url, generator) {
+ return async function () {
+ waitForExplicitFinish();
+
+ // It can take a long time to save a snapshot to disk, read the snapshots
+ // back from disk, and finally perform analyses on them.
+ requestLongerTimeout(2);
+
+ const tab = await addTab(url);
+ const results = await openMemoryPanel(tab);
+
+ try {
+ await generator(results);
+ } catch (err) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
+ }
+
+ await closeMemoryPanel(tab);
+ await removeTab(tab);
+
+ finish();
+ };
+}
+
+function dumpn(msg) {
+ dump(`MEMORY-TEST: ${msg}\n`);
+}
+
+/**
+ * Returns a promise that will resolve when the provided store matches
+ * the expected array. expectedStates is an array of dominatorTree states.
+ * Expectations :
+ * - store.getState().snapshots.length == expected.length
+ * - snapshots[i].dominatorTree.state == expected[i]
+ *
+ * @param {Store} store
+ * @param {Array<string>} expectedStates [description]
+ * @return {Promise}
+ */
+function waitUntilDominatorTreeState(store, expected) {
+ const predicate = () => {
+ const snapshots = store.getState().snapshots;
+ return (
+ snapshots.length === expected.length &&
+ expected.every((state, i) => {
+ return (
+ snapshots[i].dominatorTree &&
+ snapshots[i].dominatorTree.state === state
+ );
+ })
+ );
+ };
+ info(`Waiting for dominator trees to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+
+function takeSnapshot(window) {
+ const { gStore, document } = window;
+ const snapshotCount = gStore.getState().snapshots.length;
+ info("Taking snapshot...");
+ document.querySelector(".devtools-toolbar .take-snapshot").click();
+ return waitUntilState(
+ gStore,
+ () => gStore.getState().snapshots.length === snapshotCount + 1
+ );
+}
+
+function clearSnapshots(window) {
+ const { gStore, document } = window;
+ document.querySelector(".devtools-toolbar .clear-snapshots").click();
+ return waitUntilState(gStore, () =>
+ gStore
+ .getState()
+ .snapshots.every(snapshot => snapshot.state !== states.READ)
+ );
+}
+
+/**
+ * Sets the current requested display and waits for the selected snapshot to use
+ * it and complete the new census that entails.
+ */
+function setCensusDisplay(window, display) {
+ info(`Setting census display to ${display}...`);
+ const { gStore, gHeapAnalysesClient } = window;
+ // XXX: Should handle this via clicking the DOM, but React doesn't
+ // fire the onChange event, so just change it in the store.
+ // window.document.querySelector(`.select-display`).value = type;
+ gStore.dispatch(
+ require("resource://devtools/client/memory/actions/census-display.js").setCensusDisplayAndRefresh(
+ gHeapAnalysesClient,
+ display
+ )
+ );
+
+ return waitUntilState(window.gStore, () => {
+ const selected = window.gStore.getState().snapshots.find(s => s.selected);
+ return (
+ selected.state === states.READ &&
+ selected.census &&
+ selected.census.state === censusState.SAVED &&
+ selected.census.display === display
+ );
+ });
+}
+
+/**
+ * Get the snapshot tatus text currently displayed, or null if none is
+ * displayed.
+ *
+ * @param {Document} document
+ */
+function getDisplayedSnapshotStatus(document) {
+ const status = document.querySelector(".snapshot-status");
+ return status ? status.textContent.trim() : null;
+}
+
+/**
+ * Get the index of the currently selected snapshot.
+ *
+ * @return {Number}
+ */
+function getSelectedSnapshotIndex(store) {
+ const snapshots = store.getState().snapshots;
+ const selectedSnapshot = snapshots.find(s => s.selected);
+ return snapshots.indexOf(selectedSnapshot);
+}
+
+/**
+ * Returns a promise that will resolve when the snapshot with provided index
+ * becomes selected.
+ *
+ * @return {Promise}
+ */
+function waitUntilSnapshotSelected(store, snapshotIndex) {
+ return waitUntilState(
+ store,
+ state =>
+ state.snapshots[snapshotIndex] &&
+ state.snapshots[snapshotIndex].selected === true
+ );
+}
+
+/**
+ * Wait until the state has censuses in a certain state.
+ *
+ * @return {Promise}
+ */
+function waitUntilCensusState(store, getCensus, expected) {
+ const predicate = () => {
+ const snapshots = store.getState().snapshots;
+
+ info(
+ "Current census state:" +
+ snapshots.map(x => (getCensus(x) ? getCensus(x).state : null))
+ );
+
+ return (
+ snapshots.length === expected.length &&
+ expected.every((state, i) => {
+ const census = getCensus(snapshots[i]);
+ return (
+ state === "*" ||
+ (!census && !state) ||
+ (census && census.state === state)
+ );
+ })
+ );
+ };
+ info(`Waiting for snapshot censuses to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+
+/**
+ * Mock out the requestAnimationFrame.
+ *
+ * @return {Object}
+ * @function nextFrame
+ * Call the last queued function
+ * @function raf
+ * The mocked raf function
+ * @function timesCalled
+ * How many times the RAF has been called
+ */
+function createRAFMock() {
+ let queuedFns = [];
+ const mock = { timesCalled: 0 };
+
+ mock.nextFrame = function () {
+ const thisQueue = queuedFns;
+ queuedFns = [];
+ for (let i = 0; i < thisQueue.length; i++) {
+ thisQueue[i]();
+ }
+ };
+
+ mock.raf = function (fn) {
+ mock.timesCalled++;
+ queuedFns.push(fn);
+ };
+ return mock;
+}
+
+/**
+ * Test to see if two floats are equivalent.
+ *
+ * @param {Float} a
+ * @param {Float} b
+ * @return {Boolean}
+ */
+function floatEquality(a, b) {
+ const EPSILON = 0.00000000001;
+ const equals = Math.abs(a - b) < EPSILON;
+ if (!equals) {
+ info(`${a} not equal to ${b}`);
+ }
+ return equals;
+}
diff --git a/devtools/client/memory/test/chrome/chrome.toml b/devtools/client/memory/test/chrome/chrome.toml
new file mode 100644
index 0000000000..0414f9d74b
--- /dev/null
+++ b/devtools/client/memory/test/chrome/chrome.toml
@@ -0,0 +1,34 @@
+[DEFAULT]
+support-files = ["head.js"]
+
+["test_CensusTreeItem_01.html"]
+
+["test_DominatorTreeItem_01.html"]
+
+["test_DominatorTree_01.html"]
+
+["test_DominatorTree_02.html"]
+
+["test_DominatorTree_03.html"]
+
+["test_Heap_01.html"]
+
+["test_Heap_02.html"]
+
+["test_Heap_03.html"]
+
+["test_Heap_04.html"]
+
+["test_Heap_05.html"]
+
+["test_List_01.html"]
+
+["test_ShortestPaths_01.html"]
+
+["test_ShortestPaths_02.html"]
+
+["test_SnapshotListItem_01.html"]
+
+["test_Toolbar_01.html"]
+
+["test_TreeMap_01.html"]
diff --git a/devtools/client/memory/test/chrome/head.js b/devtools/client/memory/test/chrome/head.js
new file mode 100644
index 0000000000..685fa62928
--- /dev/null
+++ b/devtools/client/memory/test/chrome/head.js
@@ -0,0 +1,354 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/browser-loader.js"
+);
+var { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/memory/",
+ window,
+});
+var { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+);
+
+var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+
+SimpleTest.registerCleanupFunction(function () {
+ if (
+ DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT
+ ) {
+ ok(
+ false,
+ "Should have had the expected number of DevToolsUtils.assert() failures." +
+ "Expected " +
+ EXPECTED_DTU_ASSERT_FAILURE_COUNT +
+ ", got " +
+ DevToolsUtils.assertionFailureCount
+ );
+ }
+});
+
+var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+var { immutableUpdate } = DevToolsUtils;
+
+var constants = require("resource://devtools/client/memory/constants.js");
+var {
+ censusDisplays,
+ diffingState,
+ labelDisplays,
+ dominatorTreeState,
+ snapshotState,
+ viewState,
+ censusState,
+} = constants;
+
+const { L10N } = require("resource://devtools/client/memory/utils.js");
+
+var models = require("resource://devtools/client/memory/models.js");
+
+var Immutable = require("resource://devtools/client/shared/vendor/immutable.js");
+var React = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+var ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js");
+var { createFactory } = React;
+var Heap = createFactory(
+ require("resource://devtools/client/memory/components/Heap.js")
+);
+var CensusTreeItem = createFactory(
+ require("resource://devtools/client/memory/components/CensusTreeItem.js")
+);
+var DominatorTreeComponent = createFactory(
+ require("resource://devtools/client/memory/components/DominatorTree.js")
+);
+var DominatorTreeItem = createFactory(
+ require("resource://devtools/client/memory/components/DominatorTreeItem.js")
+);
+var ShortestPaths = createFactory(
+ require("resource://devtools/client/memory/components/ShortestPaths.js")
+);
+var TreeMap = createFactory(
+ require("resource://devtools/client/memory/components/TreeMap.js")
+);
+var SnapshotListItem = createFactory(
+ require("resource://devtools/client/memory/components/SnapshotListItem.js")
+);
+var List = createFactory(
+ require("resource://devtools/client/memory/components/List.js")
+);
+var Toolbar = createFactory(
+ require("resource://devtools/client/memory/components/Toolbar.js")
+);
+
+// All tests are asynchronous.
+SimpleTest.waitForExplicitFinish();
+
+var noop = () => {};
+
+var TEST_CENSUS_TREE_ITEM_PROPS = Object.freeze({
+ item: Object.freeze({
+ bytes: 10,
+ count: 1,
+ totalBytes: 10,
+ totalCount: 1,
+ name: "foo",
+ children: [
+ Object.freeze({
+ bytes: 10,
+ count: 1,
+ totalBytes: 10,
+ totalCount: 1,
+ name: "bar",
+ }),
+ ],
+ }),
+ depth: 0,
+ arrow: ">",
+ focused: true,
+ getPercentBytes: () => 50,
+ getPercentCount: () => 50,
+ showSign: false,
+ onViewSourceInDebugger: noop,
+ inverted: false,
+});
+
+// Counter for mock DominatorTreeNode ids.
+var 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: ["other", "SomeType"],
+ 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;
+}
+
+var TEST_DOMINATOR_TREE = Object.freeze({
+ dominatorTreeId: 666,
+ root: (function makeTree(depth = 0) {
+ let children;
+ if (depth <= 3) {
+ children = [
+ makeTree(depth + 1),
+ makeTree(depth + 1),
+ makeTree(depth + 1),
+ ];
+ }
+ return makeTestDominatorTreeNode({}, children);
+ })(),
+ expanded: new Set(),
+ focused: null,
+ error: null,
+ display: labelDisplays.coarseType,
+ activeFetchRequestCount: null,
+ state: dominatorTreeState.LOADED,
+});
+
+var TEST_DOMINATOR_TREE_PROPS = Object.freeze({
+ dominatorTree: TEST_DOMINATOR_TREE,
+ onLoadMoreSiblings: noop,
+ onViewSourceInDebugger: noop,
+ onExpand: noop,
+ onCollapse: noop,
+});
+
+var TEST_SHORTEST_PATHS_PROPS = Object.freeze({
+ graph: Object.freeze({
+ nodes: [
+ { id: 1, label: ["other", "SomeType"] },
+ { id: 2, label: ["other", "SomeType"] },
+ { id: 3, label: ["other", "SomeType"] },
+ ],
+ edges: [
+ { from: 1, to: 2, name: "1->2" },
+ { from: 1, to: 3, name: "1->3" },
+ { from: 2, to: 3, name: "2->3" },
+ ],
+ }),
+});
+
+var TEST_SNAPSHOT = Object.freeze({
+ id: 1337,
+ selected: true,
+ path: "/fake/path/to/snapshot",
+ census: Object.freeze({
+ report: Object.freeze({
+ objects: Object.freeze({ count: 4, bytes: 400 }),
+ scripts: Object.freeze({ count: 3, bytes: 300 }),
+ strings: Object.freeze({ count: 2, bytes: 200 }),
+ other: Object.freeze({ count: 1, bytes: 100 }),
+ }),
+ display: Object.freeze({
+ displayName: "Test Display",
+ tooltip: "Test display tooltup",
+ inverted: false,
+ breakdown: Object.freeze({
+ by: "coarseType",
+ objects: Object.freeze({ by: "count", count: true, bytes: true }),
+ scripts: Object.freeze({ by: "count", count: true, bytes: true }),
+ strings: Object.freeze({ by: "count", count: true, bytes: true }),
+ other: Object.freeze({ by: "count", count: true, bytes: true }),
+ }),
+ }),
+ state: censusState.SAVED,
+ inverted: false,
+ filter: null,
+ expanded: new Set(),
+ focused: null,
+ parentMap: Object.freeze(Object.create(null)),
+ }),
+ dominatorTree: TEST_DOMINATOR_TREE,
+ error: null,
+ imported: false,
+ creationTime: 0,
+ state: snapshotState.READ,
+});
+
+var TEST_HEAP_PROPS = Object.freeze({
+ onSnapshotClick: noop,
+ onLoadMoreSiblings: noop,
+ onCensusExpand: noop,
+ onCensusCollapse: noop,
+ onDominatorTreeExpand: noop,
+ onDominatorTreeCollapse: noop,
+ onCensusFocus: noop,
+ onDominatorTreeFocus: noop,
+ onViewSourceInDebugger: noop,
+ diffing: null,
+ view: { state: viewState.CENSUS },
+ snapshot: TEST_SNAPSHOT,
+ sizes: Object.freeze({ shortestPathsSize: 0.5 }),
+ onShortestPathsResize: noop,
+});
+
+var TEST_TOOLBAR_PROPS = Object.freeze({
+ censusDisplays: [
+ censusDisplays.coarseType,
+ censusDisplays.allocationStack,
+ censusDisplays.invertedAllocationStack,
+ ],
+ censusDisplay: censusDisplays.coarseType,
+ onTakeSnapshotClick: noop,
+ onImportClick: noop,
+ onCensusDisplayChange: noop,
+ onToggleRecordAllocationStacks: noop,
+ allocations: models.allocations,
+ onToggleInverted: noop,
+ inverted: false,
+ filterString: null,
+ setFilterString: noop,
+ diffing: null,
+ onToggleDiffing: noop,
+ view: { state: viewState.CENSUS },
+ onViewChange: noop,
+ labelDisplays: [labelDisplays.coarseType, labelDisplays.allocationStack],
+ labelDisplay: labelDisplays.coarseType,
+ onLabelDisplayChange: noop,
+ snapshots: [],
+});
+
+function makeTestCensusNode() {
+ return {
+ name: "Function",
+ bytes: 100,
+ totalBytes: 100,
+ count: 100,
+ totalCount: 100,
+ children: [],
+ };
+}
+
+var TEST_TREE_MAP_PROPS = Object.freeze({
+ treeMap: Object.freeze({
+ report: {
+ name: null,
+ bytes: 0,
+ totalBytes: 400,
+ count: 0,
+ totalCount: 400,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 200,
+ children: [makeTestCensusNode(), makeTestCensusNode()],
+ },
+ {
+ name: "other",
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 200,
+ children: [makeTestCensusNode(), makeTestCensusNode()],
+ },
+ ],
+ },
+ }),
+});
+
+var TEST_SNAPSHOT_LIST_ITEM_PROPS = Object.freeze({
+ onClick: noop,
+ onSave: noop,
+ onDelete: noop,
+ item: TEST_SNAPSHOT,
+ index: 1234,
+});
+
+function onNextAnimationFrame(fn) {
+ return () => requestAnimationFrame(() => requestAnimationFrame(fn));
+}
+
+/**
+ * Render the provided ReactElement in the provided HTML container.
+ * Returns a Promise that will resolve the rendered element as a React
+ * component.
+ */
+function renderComponent(element, container) {
+ return new Promise(resolve => {
+ const component = ReactDOM.render(
+ element,
+ container,
+ onNextAnimationFrame(() => {
+ dumpn("Rendered = " + container.innerHTML);
+ resolve(component);
+ })
+ );
+ });
+}
+
+function dumpn(msg) {
+ dump(`MEMORY-TEST: ${msg}\n`);
+}
diff --git a/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html b/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html
new file mode 100644
index 0000000000..21d900b668
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that children pointers show up at the correct times.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ await renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+ inverted: true,
+ depth: 0,
+ })), container);
+
+ ok(!container.querySelector(".children-pointer"),
+ "Don't show children pointer for roots when we are inverted");
+
+ await renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+ inverted: true,
+ depth: 1,
+ })), container);
+
+ ok(container.querySelector(".children-pointer"),
+ "Do show children pointer for non-roots when we are inverted");
+
+ await renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+ inverted: false,
+ item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: undefined }),
+ })), container);
+
+ ok(!container.querySelector(".children-pointer"),
+ "Don't show children pointer when non-inverted and no children");
+
+ await renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+ inverted: false,
+ depth: 0,
+ item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: [{}] }),
+ })), container);
+
+ ok(container.querySelector(".children-pointer"),
+ "Do show children pointer when non-inverted and have children");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html b/devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html
new file mode 100644
index 0000000000..c8cb6b6036
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we don't display `JS::ubi::RootList` for the root, and instead show "GC Roots".
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ await renderComponent(DominatorTreeItem({
+ item: makeTestDominatorTreeNode({ label: ["other", "JS::ubi::RootList"] }),
+ depth: 0,
+ arrow: dom.div(),
+ focused: true,
+ getPercentSize: _ => 50,
+ onViewSourceInDebugger: _ => { },
+ }), container);
+
+ ok(!container.textContent.includes("JS::ubi::RootList"),
+ "Should not display `JS::ubi::RootList`");
+ ok(container.textContent.includes("GC Roots"),
+ "Should display `GC Roots` instead");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_DominatorTree_01.html b/devtools/client/memory/test/chrome/test_DominatorTree_01.html
new file mode 100644
index 0000000000..0067a0f412
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTree_01.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a place holder for a subtree we are lazily fetching.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true});
+ ok(!root.children);
+
+ const expanded = new Set();
+ expanded.add(root.nodeId);
+
+ await renderComponent(DominatorTreeComponent(immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS, {
+ dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, {
+ expanded,
+ root,
+ state: dominatorTreeState.INCREMENTAL_FETCHING,
+ activeFetchRequestCount: 1,
+ }),
+ })), container);
+
+ ok(container.querySelector(".subtree-fetching"),
+ "Expanded nodes with more children available, but no children " +
+ "loaded, should get a placeholder");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_DominatorTree_02.html b/devtools/client/memory/test/chrome/test_DominatorTree_02.html
new file mode 100644
index 0000000000..66aad95296
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTree_02.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a link to load more children when some (but not all) are loaded.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true }, [
+ makeTestDominatorTreeNode({}),
+ ]);
+ ok(root.children);
+ ok(root.moreChildrenAvailable);
+
+ const expanded = new Set();
+ expanded.add(root.nodeId);
+
+ await renderComponent(DominatorTreeComponent(immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS, {
+ dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, {
+ expanded,
+ root,
+ }),
+ })), container);
+
+ ok(container.querySelector(".more-children"),
+ "Should get a link to load more children");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_DominatorTree_03.html b/devtools/client/memory/test/chrome/test_DominatorTree_03.html
new file mode 100644
index 0000000000..6fcfa3d577
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTree_03.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that expanded DominatorTreeItems are correctly rendered and updated
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ // simple tree with one root and one child
+ const root = makeTestDominatorTreeNode(
+ { moreChildrenAvailable: false },
+ [
+ makeTestDominatorTreeNode({ moreChildrenAvailable: false }),
+ ]);
+ ok(root.children);
+
+ // root node is expanded
+ const expanded = new Set();
+ expanded.add(root.nodeId);
+
+ await renderComponent(
+ DominatorTreeComponent(immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS,
+ {
+ dominatorTree: immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS.dominatorTree,
+ { expanded, root }
+ ),
+ })), container);
+ ok(true, "Dominator tree rendered");
+
+ is(container.querySelectorAll(".tree-node").length, 2,
+ "Should display two rows");
+ is(container.querySelectorAll(".arrow.open").length, 1,
+ "Should display one expanded arrow");
+
+ await renderComponent(
+ DominatorTreeComponent(immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS,
+ {
+ dominatorTree: immutableUpdate(
+ TEST_DOMINATOR_TREE_PROPS.dominatorTree,
+ { expanded: new Set(), root }
+ ),
+ })), container);
+
+ ok(true, "Dominator tree props updated to collapse all nodes");
+
+ is(container.querySelectorAll(".tree-node").length, 1,
+ "Should display only one row");
+ is(container.querySelectorAll(".arrow.open").length, 0,
+ "Should display no expanded arrow");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_01.html b/devtools/client/memory/test/chrome/test_Heap_01.html
new file mode 100644
index 0000000000..2facc3d221
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_01.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that rendering a dominator tree error is handled correctly.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ ok(React, "Should get React");
+ ok(Heap, "Should get Heap");
+
+ const errorMessage = "Something went wrong!";
+ const container = document.getElementById("container");
+
+ const props = immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE },
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: {
+ error: new Error(errorMessage),
+ state: dominatorTreeState.ERROR,
+ },
+ }),
+ });
+
+ await renderComponent(Heap(props), container);
+
+ ok(container.querySelector(".error"), "Should render an error view");
+ ok(container.textContent.includes(errorMessage),
+ "Should see our error message");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_02.html b/devtools/client/memory/test/chrome/test_Heap_02.html
new file mode 100644
index 0000000000..343ee66714
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_02.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the currently selected view is rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ ok(React, "Should get React");
+ ok(Heap, "Should get Heap");
+
+ const container = document.getElementById("container");
+
+ // Dominator tree view.
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE },
+ })), container);
+
+ ok(container.querySelector(`[data-state=${dominatorTreeState.LOADED}]`),
+ "Should render the dominator tree.");
+
+ // Census view.
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.CENSUS },
+ })), container);
+
+ ok(container.querySelector(`[data-state=${censusState.SAVED}]`),
+ "Should render the census.");
+
+ // Diffing view.
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DIFFING },
+ snapshot: null,
+ diffing: {
+ firstSnapshotId: null,
+ secondSnapshotId: null,
+ census: null,
+ error: null,
+ state: diffingState.SELECTING,
+ },
+ })), container);
+
+ ok(container.querySelector(`[data-state=${diffingState.SELECTING}]`),
+ "Should render the diffing.");
+
+ // Initial view.
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: null,
+ diffing: null,
+ })), container);
+
+ ok(container.querySelector("[data-state=initial]"),
+ "With no snapshot, nor a diffing, should render initial prompt.");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_03.html b/devtools/client/memory/test/chrome/test_Heap_03.html
new file mode 100644
index 0000000000..69f95f4275
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_03.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a throbber while computing and fetching dominator trees,
+but not in other dominator tree states.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ for (const state of [dominatorTreeState.COMPUTING, dominatorTreeState.FETCHING]) {
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE },
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
+ state,
+ root: null,
+ dominatorTreeId: state === dominatorTreeState.FETCHING ? 1 : null,
+ }),
+ }),
+ })), container);
+
+ ok(container.querySelector(".devtools-throbber"),
+ `Should show a throbber for state = ${state}`);
+ }
+
+ for (
+ const state of [
+ dominatorTreeState.LOADED, dominatorTreeState.INCREMENTAL_FETCHING,
+ ]) {
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE },
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
+ state,
+ activeFetchRequestCount:
+ state === dominatorTreeState.INCREMENTAL_FETCHING ? 1 : undefined,
+ }),
+ }),
+ })), container);
+
+ ok(!container.querySelector(".devtools-throbber"),
+ `Should not show a throbber for state = ${state}`);
+ }
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DOMINATOR_TREE },
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: {
+ state: dominatorTreeState.ERROR,
+ error: new Error("example error for testing"),
+ },
+ }),
+ })), container);
+
+ ok(!container.querySelector(".devtools-throbber"),
+ `Should not show a throbber for ERROR state`);
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_04.html b/devtools/client/memory/test/chrome/test_Heap_04.html
new file mode 100644
index 0000000000..76de5e4826
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_04.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show the "hey you're not recording allocation stacks" message at the appropriate times.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
+ report: {
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ id: 1,
+ parent: undefined,
+ children: [
+ {
+ name: "noStack",
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ },
+ ],
+ },
+ display: censusDisplays.allocationStack,
+ }),
+ }),
+ })), container);
+
+ ok(container.querySelector(".no-allocation-stacks"),
+ "When there are no allocation stacks, we should show the message");
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
+ report: {
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ id: 1,
+ parent: undefined,
+ children: [
+ {
+ name: Cu.getJSTestingFunctions().saveStack(),
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ },
+ {
+ name: "noStack",
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ },
+ ],
+ },
+ display: censusDisplays.allocationStack,
+ }),
+ }),
+ })), container);
+
+ ok(!container.querySelector(".no-allocation-stacks"),
+ "When there are allocation stacks, we should not show the message");
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
+ report: {
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ id: 1,
+ parent: undefined,
+ children: undefined,
+ },
+ display: censusDisplays.allocationStack,
+ }),
+ }),
+ })), container);
+
+ ok(!container.querySelector(".no-allocation-stacks"),
+ "When there isn't census data, we should not show the message");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Heap_05.html b/devtools/client/memory/test/chrome/test_Heap_05.html
new file mode 100644
index 0000000000..bd3fc393cc
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_05.html
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a message when the census results are empty.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
+ report: {
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ id: 1,
+ parent: undefined,
+ children: [
+ {
+ name: Cu.getJSTestingFunctions().saveStack(),
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ },
+ {
+ name: "noStack",
+ bytes: 1,
+ totalBytes: 1,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ },
+ ],
+ },
+ display: censusDisplays.allocationStack,
+ }),
+ }),
+ })), container);
+
+ ok(!container.querySelector(".empty"),
+ "When the report is not empty, we should not show the empty message");
+
+ // Empty Census Report
+
+ const emptyCensus = {
+ report: {
+ bytes: 0,
+ totalBytes: 0,
+ count: 0,
+ totalCount: 0,
+ id: 1,
+ parent: undefined,
+ children: undefined,
+ },
+ parentMap: Object.create(null),
+ display: censusDisplays.allocationStack,
+ filter: null,
+ expanded: new Immutable.Set(),
+ focused: null,
+ state: censusState.SAVED,
+ };
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, emptyCensus),
+ }),
+ })), container);
+
+ ok(container.querySelector(".empty"),
+ "When the report is empty in census view, we show the empty message");
+ ok(container.textContent.includes(L10N.getStr("heapview.empty")));
+
+ // Empty Diffing Report
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: { state: viewState.DIFFING },
+ diffing: {
+ firstSnapshotId: 1,
+ secondSnapshotId: 2,
+ census: emptyCensus,
+ state: diffingState.TOOK_DIFF,
+ },
+ snapshot: null,
+ })), container);
+
+ ok(container.querySelector(".empty"),
+ "When the report is empty in diffing view, the empty message is shown");
+ ok(container.textContent.includes(L10N.getStr("heapview.no-difference")));
+
+ // Empty Filtered Census
+
+ await renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ census: immutableUpdate(
+ TEST_HEAP_PROPS.snapshot.census, immutableUpdate(emptyCensus, {
+ filter: "zzzz",
+ })),
+ }),
+ })), container);
+
+ ok(container.querySelector(".empty"),
+ "When the report is empty in census view w/ filter, we show the empty message");
+ ok(container.textContent.includes(L10N.getStr("heapview.none-match")));
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_List_01.html b/devtools/client/memory/test/chrome/test_List_01.html
new file mode 100644
index 0000000000..4ffab49620
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_List_01.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test to verify the delete button calls the onDelete handler for an item
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <div id="container"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ const deletedSnapshots = [];
+
+ const snapshots = [ TEST_SNAPSHOT, TEST_SNAPSHOT, TEST_SNAPSHOT ]
+ .map((snapshot, index) => immutableUpdate(snapshot, {
+ index: snapshot.index + index,
+ }));
+
+ await renderComponent(
+ List({
+ itemComponent: SnapshotListItem,
+ onClick: noop,
+ onDelete: (item) => deletedSnapshots.push(item),
+ items: snapshots,
+ }),
+ container
+ );
+
+ const deleteButtons = container.querySelectorAll(".delete");
+
+ is(container.querySelectorAll(".snapshot-list-item").length, 3,
+ "There are 3 list items\n");
+ is(deletedSnapshots.length, 0,
+ "Not snapshots have been deleted\n");
+
+ deleteButtons[1].click();
+
+ is(deletedSnapshots.length, 1, "One snapshot was deleted\n");
+ is(deletedSnapshots[0], snapshots[1],
+ "Deleted snapshot was added to the deleted list\n");
+
+ deleteButtons[0].click();
+
+ is(deletedSnapshots.length, 2, "Two snapshots were deleted\n");
+ is(deletedSnapshots[1], snapshots[0],
+ "Deleted snapshot was added to the deleted list\n");
+
+ deleteButtons[2].click();
+
+ is(deletedSnapshots.length, 3, "Three snapshots were deleted\n");
+ is(deletedSnapshots[2], snapshots[2],
+ "Deleted snapshot was added to the deleted list\n");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_ShortestPaths_01.html b/devtools/client/memory/test/chrome/test_ShortestPaths_01.html
new file mode 100644
index 0000000000..29905bfaca
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_ShortestPaths_01.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the ShortestPaths component properly renders a graph of the merged shortest paths.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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">
+
+ <script type="application/javascript"
+ src="chrome://global/content/third_party/d3/d3.js">
+ </script>
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/dagre-d3.js">
+ </script>
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ await renderComponent(ShortestPaths(TEST_SHORTEST_PATHS_PROPS), container);
+
+ let found1 = false;
+ let found2 = false;
+ let found3 = false;
+
+ let found1to2 = false;
+ let found1to3 = false;
+ let found2to3 = false;
+
+ const tspans = [...container.querySelectorAll("tspan")];
+ for (const el of tspans) {
+ const text = el.textContent.trim();
+ dumpn("tspan's text = " + text);
+
+ switch (text) {
+ // Nodes
+
+ case "other › SomeType @ 0x1": {
+ ok(!found1, "Should only find node 1 once");
+ found1 = true;
+ break;
+ }
+
+ case "other › SomeType @ 0x2": {
+ ok(!found2, "Should only find node 2 once");
+ found2 = true;
+ break;
+ }
+
+ case "other › SomeType @ 0x3": {
+ ok(!found3, "Should only find node 3 once");
+ found3 = true;
+ break;
+ }
+
+ // Edges
+
+ case "1->2": {
+ ok(!found1to2, "Should only find edge 1->2 once");
+ found1to2 = true;
+ break;
+ }
+
+ case "1->3": {
+ ok(!found1to3, "Should only find edge 1->3 once");
+ found1to3 = true;
+ break;
+ }
+
+ case "2->3": {
+ ok(!found2to3, "Should only find edge 2->3 once");
+ found2to3 = true;
+ break;
+ }
+
+ // Unexpected
+
+ default: {
+ ok(false, `Unexpected tspan: ${text}`);
+ break;
+ }
+ }
+ }
+
+ ok(found1, "Should have rendered node 1");
+ ok(found2, "Should have rendered node 2");
+ ok(found3, "Should have rendered node 3");
+
+ ok(found1to2, "Should have rendered edge 1->2");
+ ok(found1to3, "Should have rendered edge 1->3");
+ ok(found2to3, "Should have rendered edge 2->3");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_ShortestPaths_02.html b/devtools/client/memory/test/chrome/test_ShortestPaths_02.html
new file mode 100644
index 0000000000..cbf0370d22
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_ShortestPaths_02.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the ShortestPaths component renders a suggestion to select a node when there is no graph.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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">
+
+ <script type="application/javascript"
+ src="chrome://global/content/third_party/d3/d3.js">
+ </script>
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/dagre-d3.js">
+ </script>
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ await renderComponent(ShortestPaths(immutableUpdate(TEST_SHORTEST_PATHS_PROPS,
+ { graph: null })),
+ container);
+
+ ok(container.textContent.includes(L10N.getStr("shortest-paths.select-node")),
+ "The node selection prompt is displayed");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html b/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html
new file mode 100644
index 0000000000..20fc137479
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test to verify that the delete button only shows up for a snapshot when it has a
+path.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <div id="container"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ await renderComponent(
+ SnapshotListItem(TEST_SNAPSHOT_LIST_ITEM_PROPS),
+ container
+ );
+
+ ok(container.querySelector(".delete"),
+ "Should have delete button when there is a path");
+
+ const pathlessProps = immutableUpdate(
+ TEST_SNAPSHOT_LIST_ITEM_PROPS,
+ {item: immutableUpdate(TEST_SNAPSHOT, {path: null})}
+ );
+
+ await renderComponent(
+ SnapshotListItem(pathlessProps),
+ container
+ );
+
+ ok(!container.querySelector(".delete"),
+ "No delete button should be found if there is no path\n");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_Toolbar_01.html b/devtools/client/memory/test/chrome/test_Toolbar_01.html
new file mode 100644
index 0000000000..3bf934c77c
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Toolbar_01.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the Toolbar component shows the view switcher only at the appropriate times.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ // Census and dominator tree views.
+
+ for (const view of [viewState.CENSUS, viewState.DOMINATOR_TREE]) {
+ await renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
+ view: { state: view },
+ })), container);
+
+ ok(container.querySelector("#select-view"),
+ `The view selector is shown in view = ${view}`);
+ }
+
+ await renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
+ view: { state: viewState.DIFFING },
+ })), container);
+
+ ok(!container.querySelector("#select-view"),
+ "The view selector is NOT shown in the DIFFING view");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/chrome/test_TreeMap_01.html b/devtools/client/memory/test/chrome/test_TreeMap_01.html
new file mode 100644
index 0000000000..7b8b98b9a7
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_TreeMap_01.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the Tree Map correctly renders onto 2 managed canvases.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component 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">
+
+ <script type="application/javascript"
+ src="chrome://global/content/third_party/d3/d3.js">
+ </script>
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+ window.onload = async function() {
+ try {
+ const container = document.getElementById("container");
+
+ await renderComponent(TreeMap(TEST_TREE_MAP_PROPS), container);
+
+ const treeMapContainer = container.querySelector(".tree-map-container");
+ ok(treeMapContainer, "Component creates a container");
+
+ const canvases = treeMapContainer.querySelectorAll("canvas");
+ is(canvases.length, 2, "Creates 2 canvases");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ };
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/devtools/client/memory/test/xpcshell/.eslintrc.js b/devtools/client/memory/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..f1618e83c2
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/.eslintrc.js
@@ -0,0 +1,14 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../../.eslintrc.xpcshell.js",
+ rules: {
+ "no-unused-vars": [
+ "error",
+ {
+ vars: "local",
+ },
+ ],
+ },
+};
diff --git a/devtools/client/memory/test/xpcshell/head.js b/devtools/client/memory/test/xpcshell/head.js
new file mode 100644
index 0000000000..94788a60fb
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/head.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// via xpcshell.ini
+/* import-globals-from ../../../shared/test/shared-head.js */
+
+Services.prefs.setBoolPref("devtools.testing", true);
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.testing");
+ Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { expectState } = require("resource://devtools/server/actors/common.js");
+var HeapSnapshotFileUtils = require("resource://devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js");
+var HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js");
+var { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+);
+var Store = require("resource://devtools/client/memory/store.js");
+var { L10N } = require("resource://devtools/client/memory/utils.js");
+var SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+);
+
+var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+
+registerCleanupFunction(function () {
+ equal(
+ DevToolsUtils.assertionFailureCount,
+ EXPECTED_DTU_ASSERT_FAILURE_COUNT,
+ "Should have had the expected number of DevToolsUtils.assert() failures."
+ );
+});
+
+function dumpn(msg) {
+ dump(`MEMORY-TEST: ${msg}\n`);
+}
+
+function initDebugger() {
+ const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
+ addDebuggerToGlobal(global);
+ return new global.Debugger();
+}
+
+function StubbedMemoryFront() {
+ this.state = "detached";
+ this.dbg = initDebugger();
+}
+
+StubbedMemoryFront.prototype.attach = async function () {
+ this.state = "attached";
+};
+
+StubbedMemoryFront.prototype.detach = async function () {
+ this.state = "detached";
+};
+
+StubbedMemoryFront.prototype.saveHeapSnapshot = expectState(
+ "attached",
+ async function () {
+ return ChromeUtils.saveHeapSnapshot({ runtime: true });
+ },
+ "saveHeapSnapshot"
+);
+
+StubbedMemoryFront.prototype.startRecordingAllocations = expectState(
+ "attached",
+ async function () {}
+);
+
+StubbedMemoryFront.prototype.stopRecordingAllocations = expectState(
+ "attached",
+ async function () {}
+);
+
+function waitUntilSnapshotState(store, expected) {
+ const predicate = () => {
+ const snapshots = store.getState().snapshots;
+ info(snapshots.map(x => x.state));
+ return (
+ snapshots.length === expected.length &&
+ expected.every(
+ (state, i) => state === "*" || snapshots[i].state === state
+ )
+ );
+ };
+ info(`Waiting for snapshots to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+
+function findReportLeafIndex(node, name = null) {
+ if (node.reportLeafIndex && (!name || node.name === name)) {
+ return node.reportLeafIndex;
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const found = findReportLeafIndex(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+}
+
+function waitUntilCensusState(store, getCensus, expected) {
+ const predicate = () => {
+ const snapshots = store.getState().snapshots;
+
+ info(
+ "Current census state:" +
+ snapshots.map(x => (getCensus(x) ? getCensus(x).state : null))
+ );
+
+ return (
+ snapshots.length === expected.length &&
+ expected.every((state, i) => {
+ const census = getCensus(snapshots[i]);
+ return (
+ state === "*" ||
+ (!census && !state) ||
+ (census && census.state === state)
+ );
+ })
+ );
+ };
+ info(`Waiting for snapshots' censuses to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+
+async function createTempFile() {
+ const file = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, "tmp.fxsnapshot")
+ );
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ const destPath = file.path;
+ const stat = await IOUtils.stat(destPath);
+ Assert.strictEqual(stat.size, 0, "new file is 0 bytes at start");
+ return destPath;
+}
+
+// This is a copy of the same method from shared-head.js as
+// xpcshell test aren't using shared-head.js
+/**
+ * Wait for a specific action type to be dispatched.
+ *
+ * If the action is async and defines a `status` property, this helper will wait
+ * for the status to reach either "error" or "done".
+ *
+ * @param {Object} store
+ * Redux store where the action should be dispatched.
+ * @param {String} actionType
+ * The actionType to wait for.
+ * @param {Number} repeat
+ * Optional, number of time the action is expected to be dispatched.
+ * Defaults to 1
+ * @return {Promise}
+ */
+function waitForDispatch(store, actionType, repeat = 1) {
+ let count = 0;
+ return new Promise(resolve => {
+ store.dispatch({
+ type: "@@service/waitUntil",
+ predicate: action => {
+ const isDone =
+ !action.status ||
+ action.status === "done" ||
+ action.status === "error";
+
+ if (action.type === actionType && isDone && ++count == repeat) {
+ return true;
+ }
+
+ return false;
+ },
+ run: (dispatch, getState, action) => {
+ resolve(action);
+ },
+ });
+ });
+}
diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_01.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_01.js
new file mode 100644
index 0000000000..9511ba413f
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_01.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test clearSnapshots deletes snapshots with READ censuses
+
+const {
+ takeSnapshotAndCensus,
+ clearSnapshots,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const { actions } = require("resource://devtools/client/memory/constants.js");
+const {
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(true, "snapshot created");
+
+ ok(true, "dispatch clearSnapshots action");
+ const deleteEvents = Promise.all([
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_START),
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_END),
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ await deleteEvents;
+ ok(true, "received delete snapshots events");
+
+ equal(getState().snapshots.length, 0, "no snapshot remaining");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_02.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_02.js
new file mode 100644
index 0000000000..09f1fa6b54
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_02.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test clearSnapshots preserves snapshots with state != READ or ERROR
+
+const {
+ takeSnapshotAndCensus,
+ clearSnapshots,
+ takeSnapshot,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ snapshotState: states,
+ treeMapState,
+ actions,
+} = require("resource://devtools/client/memory/constants.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create a snapshot with a census in SAVED state");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ ok(true, "create a snapshot in SAVED state");
+ dispatch(takeSnapshot(front));
+ await waitUntilSnapshotState(store, [states.SAVED, states.SAVED]);
+ await waitUntilCensusState(store, snapshot => snapshot.treeMap, [
+ treeMapState.SAVED,
+ null,
+ ]);
+ ok(true, "snapshots created with expected states");
+
+ ok(true, "dispatch clearSnapshots action");
+ const deleteEvents = Promise.all([
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_START),
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_END),
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ await deleteEvents;
+ ok(true, "received delete snapshots events");
+
+ equal(getState().snapshots.length, 1, "one snapshot remaining");
+ const remainingSnapshot = getState().snapshots[0];
+ equal(
+ remainingSnapshot.treeMap,
+ undefined,
+ "remaining snapshot doesn't have a treeMap property"
+ );
+ equal(
+ remainingSnapshot.census,
+ undefined,
+ "remaining snapshot doesn't have a census property"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_03.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_03.js
new file mode 100644
index 0000000000..b920c5264b
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_03.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test clearSnapshots deletes snapshots with state ERROR
+
+const {
+ takeSnapshotAndCensus,
+ clearSnapshots,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ snapshotState: states,
+ treeMapState,
+ actions,
+} = require("resource://devtools/client/memory/constants.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create a snapshot with a treeMap");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilSnapshotState(store, [states.SAVED]);
+ ok(true, "snapshot created with a SAVED state");
+ await waitUntilCensusState(store, snapshot => snapshot.treeMap, [
+ treeMapState.SAVED,
+ ]);
+ ok(true, "treeMap created with a SAVED state");
+
+ ok(true, "set snapshot state to error");
+ const id = getState().snapshots[0].id;
+ dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") });
+ await waitUntilSnapshotState(store, [states.ERROR]);
+ ok(true, "snapshot set to error state");
+
+ ok(true, "dispatch clearSnapshots action");
+ const deleteEvents = Promise.all([
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_START),
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_END),
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ await deleteEvents;
+ ok(true, "received delete snapshots events");
+ equal(getState().snapshots.length, 0, "error snapshot deleted");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_04.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_04.js
new file mode 100644
index 0000000000..723029c90b
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_04.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test clearSnapshots deletes several snapshots
+
+const {
+ takeSnapshotAndCensus,
+ clearSnapshots,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ snapshotState: states,
+ actions,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create 3 snapshots with a saved census");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, snapshot => snapshot.treeMap, [
+ treeMapState.SAVED,
+ treeMapState.SAVED,
+ treeMapState.SAVED,
+ ]);
+ ok(true, "snapshots created with a saved census");
+
+ ok(true, "set first snapshot state to error");
+ const id = getState().snapshots[0].id;
+ dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") });
+ await waitUntilSnapshotState(store, [states.ERROR, states.READ, states.READ]);
+ ok(true, "first snapshot set to error state");
+
+ ok(true, "dispatch clearSnapshots action");
+ const deleteEvents = Promise.all([
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_START),
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_END),
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ await deleteEvents;
+ ok(true, "received delete snapshots events");
+
+ equal(getState().snapshots.length, 0, "no snapshot remaining");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_05.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_05.js
new file mode 100644
index 0000000000..485e0f0513
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_05.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test clearSnapshots deletes several snapshots
+
+const {
+ takeSnapshotAndCensus,
+ clearSnapshots,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ actions,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create 2 snapshots with a saved census");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ ok(true, "snapshots created with a saved census");
+ await waitUntilCensusState(store, snapshot => snapshot.treeMap, [
+ treeMapState.SAVED,
+ treeMapState.SAVED,
+ ]);
+
+ const errorHeapWorker = {
+ deleteHeapSnapshot() {
+ return Promise.reject("_");
+ },
+ };
+
+ ok(true, "dispatch clearSnapshots action");
+ const deleteEvents = Promise.all([
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_START),
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_END),
+ waitForDispatch(store, actions.SNAPSHOT_ERROR),
+ waitForDispatch(store, actions.SNAPSHOT_ERROR),
+ ]);
+ dispatch(clearSnapshots(errorHeapWorker));
+ await deleteEvents;
+ ok(true, "received delete snapshots and snapshot error events");
+ equal(getState().snapshots.length, 0, "no snapshot remaining");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_06.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_06.js
new file mode 100644
index 0000000000..0fe84daaea
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_06.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that clearSnapshots disables diffing when deleting snapshots
+
+const {
+ takeSnapshotAndCensus,
+ clearSnapshots,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ actions,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffingAndRefresh,
+} = require("resource://devtools/client/memory/actions/diffing.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ ok(true, "create 2 snapshots with a saved census");
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, snapshot => snapshot.treeMap, [
+ treeMapState.SAVED,
+ treeMapState.SAVED,
+ ]);
+ ok(true, "snapshots created with a saved census");
+
+ dispatch(toggleDiffing());
+ dispatch(
+ selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[0])
+ );
+ dispatch(
+ selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[1])
+ );
+
+ ok(getState().diffing, "We should be in diffing view");
+
+ await waitForDispatch(store, actions.TAKE_CENSUS_DIFF_END);
+ ok(true, "Received TAKE_CENSUS_DIFF_END action");
+
+ ok(true, "Dispatch clearSnapshots action");
+ const deleteEvents = Promise.all([
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_START),
+ waitForDispatch(store, actions.DELETE_SNAPSHOTS_END),
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ await deleteEvents;
+ ok(true, "received delete snapshots events");
+
+ Assert.strictEqual(
+ getState().snapshots.length,
+ 0,
+ "Snapshots array should be empty"
+ );
+ ok(!getState().diffing, "We should no longer be diffing");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-export-snapshot.js b/devtools/client/memory/test/xpcshell/test_action-export-snapshot.js
new file mode 100644
index 0000000000..c481ab1315
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-export-snapshot.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test exporting a snapshot to a user specified location on disk.
+
+const {
+ exportSnapshot,
+} = require("resource://devtools/client/memory/actions/io.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ actions,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ const destPath = await createTempFile();
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, snapshot => snapshot.treeMap, [
+ treeMapState.SAVED,
+ ]);
+
+ const exportEvents = Promise.all([
+ waitForDispatch(store, actions.EXPORT_SNAPSHOT_START),
+ waitForDispatch(store, actions.EXPORT_SNAPSHOT_END),
+ ]);
+ dispatch(exportSnapshot(getState().snapshots[0], destPath));
+ await exportEvents;
+
+ const stat = await IOUtils.stat(destPath);
+ info(stat.size);
+ Assert.greater(stat.size, 0, "destination file is more than 0 bytes");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-filter-01.js b/devtools/client/memory/test/xpcshell/test_action-filter-01.js
new file mode 100644
index 0000000000..b16e66a62c
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-filter-01.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test setting the filter string.
+
+const {
+ setFilterString,
+} = require("resource://devtools/client/memory/actions/filter.js");
+
+add_task(async function () {
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().filter, null, "no filter by default");
+
+ dispatch(setFilterString("my filter"));
+ equal(getState().filter, "my filter", "now we have the expected filter");
+
+ dispatch(setFilterString(""));
+ equal(getState().filter, null, "no filter again");
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-filter-02.js b/devtools/client/memory/test/xpcshell/test_action-filter-02.js
new file mode 100644
index 0000000000..828b976aa3
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-filter-02.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that changing filter state properly refreshes the selected census.
+
+const {
+ viewState,
+ censusState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setFilterStringAndRefresh,
+} = require("resource://devtools/client/memory/actions/filter.js");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ equal(getState().filter, null, "no filter by default");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+ ok(true, "saved 3 snapshots and took a census of each of them");
+
+ dispatch(setFilterStringAndRefresh("str", heapWorker));
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVING,
+ ]);
+ ok(
+ true,
+ "setting filter string should recompute the selected snapshot's census"
+ );
+
+ equal(getState().filter, "str", "now inverted");
+
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+
+ equal(getState().snapshots[0].census.filter, null);
+ equal(getState().snapshots[1].census.filter, null);
+ equal(getState().snapshots[2].census.filter, "str");
+
+ dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVING,
+ censusState.SAVED,
+ ]);
+ ok(true, "selecting non-inverted census should trigger a recompute");
+
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+
+ equal(getState().snapshots[0].census.filter, null);
+ equal(getState().snapshots[1].census.filter, "str");
+ equal(getState().snapshots[2].census.filter, "str");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-filter-03.js b/devtools/client/memory/test/xpcshell/test_action-filter-03.js
new file mode 100644
index 0000000000..2fcc3e9156
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-filter-03.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that changing filter state in the middle of taking a snapshot results in
+// the properly fitered census.
+
+const {
+ snapshotState: states,
+ censusState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setFilterString,
+ setFilterStringAndRefresh,
+} = require("resource://devtools/client/memory/actions/filter.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilSnapshotState(store, [states.SAVING]);
+
+ dispatch(setFilterString("str"));
+
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ ]);
+ equal(getState().filter, "str", "should want filtered trees");
+ equal(
+ getState().snapshots[0].census.filter,
+ "str",
+ "snapshot-we-were-in-the-middle-of-saving's census should be filtered"
+ );
+
+ dispatch(setFilterStringAndRefresh("", heapWorker));
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVING,
+ ]);
+ ok(true, "changing filter string retriggers census");
+ ok(!getState().filter, "no longer filtering");
+
+ dispatch(setFilterString("obj"));
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ ]);
+ equal(getState().filter, "obj", "filtering for obj now");
+ equal(
+ getState().snapshots[0].census.filter,
+ "obj",
+ "census-we-were-in-the-middle-of-recomputing should be filtered again"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-import-snapshot-and-census.js b/devtools/client/memory/test/xpcshell/test_action-import-snapshot-and-census.js
new file mode 100644
index 0000000000..3e97f7c45f
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-import-snapshot-and-census.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the task creator `importSnapshotAndCensus()` for the whole flow of
+ * importing a snapshot, and its sub-actions.
+ */
+
+const {
+ actions,
+ snapshotState: states,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ exportSnapshot,
+ importSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/io.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { subscribe, dispatch, getState } = store;
+
+ const destPath = await createTempFile();
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+
+ const exportEvents = Promise.all([
+ waitForDispatch(store, actions.EXPORT_SNAPSHOT_START),
+ waitForDispatch(store, actions.EXPORT_SNAPSHOT_END),
+ ]);
+ dispatch(exportSnapshot(getState().snapshots[0], destPath));
+ await exportEvents;
+
+ // Now import our freshly exported snapshot
+ let snapshotI = 0;
+ let censusI = 0;
+ const snapshotStates = ["IMPORTING", "READING", "READ"];
+ const censusStates = ["SAVING", "SAVED"];
+ const expectStates = () => {
+ const snapshot = getState().snapshots[1];
+ if (!snapshot) {
+ return;
+ }
+ if (snapshotI < snapshotStates.length) {
+ const isCorrectState =
+ snapshot.state === states[snapshotStates[snapshotI]];
+ if (isCorrectState) {
+ ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
+ snapshotI++;
+ }
+ }
+ if (snapshot.treeMap && censusI < censusStates.length) {
+ if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
+ ok(true, `Found expected census state ${censusStates[censusI]}`);
+ censusI++;
+ }
+ }
+ };
+
+ const unsubscribe = subscribe(expectStates);
+ dispatch(importSnapshotAndCensus(heapWorker, destPath));
+
+ await waitUntilState(store, () => {
+ return (
+ snapshotI === snapshotStates.length && censusI === censusStates.length
+ );
+ });
+ unsubscribe();
+ equal(
+ snapshotI,
+ snapshotStates.length,
+ "importSnapshotAndCensus() produces the correct sequence of states in a snapshot"
+ );
+ equal(
+ getState().snapshots[1].state,
+ states.READ,
+ "imported snapshot is in READ state"
+ );
+ equal(
+ censusI,
+ censusStates.length,
+ "importSnapshotAndCensus() produces the correct sequence of states in a census"
+ );
+ equal(
+ getState().snapshots[1].treeMap.state,
+ treeMapState.SAVED,
+ "imported snapshot is in READ state"
+ );
+ ok(getState().snapshots[1].selected, "imported snapshot is selected");
+
+ // Check snapshot data
+ const snapshot1 = getState().snapshots[0];
+ const snapshot2 = getState().snapshots[1];
+
+ equal(
+ snapshot1.treeMap.display,
+ snapshot2.treeMap.display,
+ "imported snapshot has correct display"
+ );
+
+ // Clone the census data so we can destructively remove the ID/parents to compare
+ // equal census data
+ const census1 = stripUnique(
+ JSON.parse(JSON.stringify(snapshot1.treeMap.report))
+ );
+ const census2 = stripUnique(
+ JSON.parse(JSON.stringify(snapshot2.treeMap.report))
+ );
+
+ equal(
+ JSON.stringify(census1),
+ JSON.stringify(census2),
+ "Imported snapshot has correct census"
+ );
+
+ function stripUnique(obj) {
+ const children = obj.children || [];
+ for (const child of children) {
+ delete child.id;
+ delete child.parent;
+ stripUnique(child);
+ }
+ delete obj.id;
+ delete obj.parent;
+ return obj;
+ }
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-import-snapshot-dominator-tree.js b/devtools/client/memory/test/xpcshell/test_action-import-snapshot-dominator-tree.js
new file mode 100644
index 0000000000..3c11268e94
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-import-snapshot-dominator-tree.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests `importSnapshotAndCensus()` when importing snapshots from the dominator
+ * tree view. The snapshot is expected to be loaded and its dominator tree
+ * should be computed.
+ */
+
+const {
+ snapshotState,
+ dominatorTreeState,
+ viewState,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ importSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/io.js");
+const {
+ changeViewAndRefresh,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { subscribe, dispatch, getState } = store;
+
+ dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
+ equal(
+ getState().view.state,
+ viewState.DOMINATOR_TREE,
+ "We should now be in the DOMINATOR_TREE view"
+ );
+
+ let i = 0;
+ const expected = [
+ "IMPORTING",
+ "READING",
+ "READ",
+ "treeMap:SAVING",
+ "treeMap:SAVED",
+ "dominatorTree:COMPUTING",
+ "dominatorTree:FETCHING",
+ "dominatorTree:LOADED",
+ ];
+ const expectStates = () => {
+ const snapshot = getState().snapshots[0];
+ if (snapshot && hasExpectedState(snapshot, expected[i])) {
+ ok(true, `Found expected state ${expected[i]}`);
+ i++;
+ }
+ };
+
+ const unsubscribe = subscribe(expectStates);
+ const snapshotPath = await front.saveHeapSnapshot();
+ dispatch(importSnapshotAndCensus(heapWorker, snapshotPath));
+
+ await waitUntilState(store, () => i === expected.length);
+ unsubscribe();
+ equal(
+ i,
+ expected.length,
+ "importSnapshotAndCensus() produces the correct " +
+ "sequence of states in a snapshot"
+ );
+ equal(
+ getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.LOADED,
+ "imported snapshot's dominator tree is in LOADED state"
+ );
+ ok(getState().snapshots[0].selected, "imported snapshot is selected");
+});
+
+/**
+ * Check that the provided snapshot is in the expected state. The expected state
+ * is a snapshotState by default. If the expected state is prefixed by
+ * dominatorTree, a dominatorTree is expected on the provided snapshot, in the
+ * corresponding state from dominatorTreeState.
+ */
+function hasExpectedState(snapshot, expectedState) {
+ const isDominatorState = expectedState.indexOf("dominatorTree:") === 0;
+ if (isDominatorState) {
+ const state =
+ dominatorTreeState[expectedState.replace("dominatorTree:", "")];
+ return snapshot.dominatorTree && snapshot.dominatorTree.state === state;
+ }
+
+ const isTreeMapState = expectedState.indexOf("treeMap:") === 0;
+ if (isTreeMapState) {
+ const state = treeMapState[expectedState.replace("treeMap:", "")];
+ return snapshot.treeMap && snapshot.treeMap.state === state;
+ }
+
+ const state = snapshotState[expectedState];
+ return snapshot.state === state;
+}
diff --git a/devtools/client/memory/test/xpcshell/test_action-select-snapshot.js b/devtools/client/memory/test/xpcshell/test_action-select-snapshot.js
new file mode 100644
index 0000000000..f4abac423b
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-select-snapshot.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the reducer responding to the action `selectSnapshot(snapshot)`
+ */
+
+const actions = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ snapshotState: states,
+} = require("resource://devtools/client/memory/constants.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ await front.attach();
+ const store = Store();
+
+ for (let i = 0; i < 5; i++) {
+ store.dispatch(actions.takeSnapshot(front));
+ }
+
+ await waitUntilState(
+ store,
+ ({ snapshots }) => snapshots.length === 5 && snapshots.every(isDone)
+ );
+
+ for (let i = 0; i < 5; i++) {
+ info(`Selecting snapshot[${i}]`);
+ store.dispatch(actions.selectSnapshot(store.getState().snapshots[i].id));
+ await waitUntilState(store, ({ snapshots }) => snapshots[i].selected);
+
+ const { snapshots } = store.getState();
+ ok(snapshots[i].selected, `snapshot[${i}] selected`);
+ equal(
+ snapshots.filter(s => !s.selected).length,
+ 4,
+ "All other snapshots are unselected"
+ );
+ }
+});
+
+function isDone(s) {
+ return s.state === states.SAVED;
+}
diff --git a/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-01.js b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-01.js
new file mode 100644
index 0000000000..6f1332c339
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-01.js
@@ -0,0 +1,186 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for display
+ * changing. We test this rather than `setCensusDisplayAndRefresh` directly, as
+ * we use the refresh action in the app itself composed from
+ * `setCensusDisplayAndRefresh`.
+ */
+
+const {
+ censusDisplays,
+ censusState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setCensusDisplayAndRefresh,
+} = require("resource://devtools/client/memory/actions/census-display.js");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+// We test setting an invalid display, which triggers an assertion failure.
+EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1;
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Test default display with no snapshots
+ equal(
+ getState().censusDisplay.breakdown.by,
+ "coarseType",
+ "default coarseType display selected at start."
+ );
+ dispatch(
+ setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)
+ );
+ equal(
+ getState().censusDisplay.breakdown.by,
+ "allocationStack",
+ "display changed with no snapshots"
+ );
+
+ // Test invalid displays
+ Assert.strictEqual(
+ getState().errors.length,
+ 0,
+ "No error actions in the queue."
+ );
+ dispatch(setCensusDisplayAndRefresh(heapWorker, {}));
+ await waitUntilState(store, () => getState().errors.length === 1);
+ ok(true, "Emits an error action when passing in an invalid display object");
+
+ equal(
+ getState().censusDisplay.breakdown.by,
+ "allocationStack",
+ "current display unchanged when passing invalid display"
+ );
+
+ // Test new snapshots
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ ]);
+
+ equal(
+ getState().snapshots[0].census.display,
+ censusDisplays.allocationStack,
+ "New snapshot's census uses correct display"
+ );
+
+ // Updates when changing display during `SAVING`
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVING,
+ ]);
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+ equal(
+ getState().snapshots[1].census.display,
+ censusDisplays.coarseType,
+ "Changing display while saving a snapshot results " +
+ "in a census using the new display"
+ );
+
+ // Updates when changing display during `SAVING_CENSUS`
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVING,
+ ]);
+ dispatch(
+ setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)
+ );
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+ equal(
+ getState().snapshots[2].census.display,
+ censusDisplays.allocationStack,
+ "Display can be changed while saving census, stores updated display in snapshot"
+ );
+
+ // Updates census on currently selected snapshot when changing display
+ ok(getState().snapshots[2].selected, "Third snapshot currently selected");
+ dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
+ await waitUntilState(
+ store,
+ state => state.snapshots[2].census.state === censusState.SAVING
+ );
+ await waitUntilState(
+ store,
+ state => state.snapshots[2].census.state === censusState.SAVED
+ );
+ equal(
+ getState().snapshots[2].census.display,
+ censusDisplays.coarseType,
+ "Snapshot census updated when changing displays " +
+ "after already generating one census"
+ );
+
+ dispatch(
+ setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)
+ );
+ await waitUntilState(
+ store,
+ state => state.snapshots[2].census.state === censusState.SAVED
+ );
+ equal(
+ getState().snapshots[2].census.display,
+ censusDisplays.allocationStack,
+ "Snapshot census updated when changing displays " +
+ "after already generating one census"
+ );
+
+ // Does not update unselected censuses.
+ ok(!getState().snapshots[1].selected, "Second snapshot selected currently");
+ equal(
+ getState().snapshots[1].census.display,
+ censusDisplays.coarseType,
+ "Second snapshot using `coarseType` display still and " +
+ "not yet updated to correct display"
+ );
+
+ // Updates to current display when switching to stale snapshot.
+ dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVING,
+ censusState.SAVED,
+ ]);
+ await waitUntilCensusState(store, snapshot => snapshot.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+
+ ok(getState().snapshots[1].selected, "Second snapshot selected currently");
+ equal(
+ getState().snapshots[1].census.display,
+ censusDisplays.allocationStack,
+ "Second snapshot using `allocationStack` display and updated to correct display"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-02.js b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-02.js
new file mode 100644
index 0000000000..2b2b91bc97
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-02.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for custom
+ * displays.
+ */
+
+const {
+ censusState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setCensusDisplayAndRefresh,
+} = require("resource://devtools/client/memory/actions/census-display.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const CUSTOM = {
+ displayName: "Custom",
+ tooltip: "Custom tooltip",
+ inverted: false,
+ breakdown: {
+ by: "internalType",
+ then: { by: "count", bytes: true, count: false },
+ },
+};
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(setCensusDisplayAndRefresh(heapWorker, CUSTOM));
+ equal(
+ getState().censusDisplay,
+ CUSTOM,
+ "CUSTOM display stored in display state."
+ );
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ equal(
+ getState().snapshots[0].census.display,
+ CUSTOM,
+ "New snapshot stored CUSTOM display when done taking census"
+ );
+ ok(
+ getState().snapshots[0].census.report.children.length,
+ "Census has some children"
+ );
+ // Ensure we don't have `count` in any results
+ ok(
+ getState().snapshots[0].census.report.children.every(c => !c.count),
+ "Census used CUSTOM display without counts"
+ );
+ // Ensure we do have `bytes` in the results
+ ok(
+ getState().snapshots[0].census.report.children.every(
+ c => typeof c.bytes === "number"
+ ),
+ "Census used CUSTOM display with bytes"
+ );
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-set-display.js b/devtools/client/memory/test/xpcshell/test_action-set-display.js
new file mode 100644
index 0000000000..dfc14f609a
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-set-display.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the action creator `setCensusDisplay()` for display changing. Does not
+ * test refreshing the census information, check `setCensusDisplayAndRefresh`
+ * action for that.
+ */
+
+const {
+ censusDisplays,
+ censusState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setCensusDisplay,
+} = require("resource://devtools/client/memory/actions/census-display.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+// We test setting an invalid display, which triggers an assertion failure.
+EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1;
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Test default display with no snapshots
+ equal(
+ getState().censusDisplay.breakdown.by,
+ "coarseType",
+ "default coarseType display selected at start."
+ );
+
+ dispatch(setCensusDisplay(censusDisplays.allocationStack));
+ equal(
+ getState().censusDisplay.breakdown.by,
+ "allocationStack",
+ "display changed with no snapshots"
+ );
+
+ // Test invalid displays
+ try {
+ dispatch(setCensusDisplay({}));
+ ok(false, "Throws when passing in an invalid display object");
+ } catch (e) {
+ ok(true, "Throws when passing in an invalid display object");
+ }
+ equal(
+ getState().censusDisplay.breakdown.by,
+ "allocationStack",
+ "current display unchanged when passing invalid display"
+ );
+
+ // Test new snapshots
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+ equal(
+ getState().snapshots[0].census.display,
+ censusDisplays.allocationStack,
+ "New snapshots use the current, non-default display"
+ );
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-take-census.js b/devtools/client/memory/test/xpcshell/test_action-take-census.js
new file mode 100644
index 0000000000..005208024b
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-take-census.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the async reducer responding to the action `takeCensus(heapWorker, snapshot)`
+ */
+
+var {
+ snapshotState: states,
+ censusDisplays,
+ censusState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+var actions = require("resource://devtools/client/memory/actions/snapshot.js");
+var {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+// This tests taking a census on a snapshot that is still being read, which
+// triggers an assertion failure.
+EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1;
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+
+ store.dispatch(changeView(viewState.CENSUS));
+
+ store.dispatch(actions.takeSnapshot(front));
+ await waitUntilState(store, () => {
+ const snapshots = store.getState().snapshots;
+ return snapshots.length === 1 && snapshots[0].state === states.SAVED;
+ });
+
+ let snapshot = store.getState().snapshots[0];
+ equal(snapshot.census, null, "No census data exists yet on the snapshot.");
+
+ // Test error case of wrong state.
+ store.dispatch(actions.takeCensus(heapWorker, snapshot.id));
+ await waitUntilState(store, () => store.getState().errors.length === 1);
+
+ dumpn("Found error: " + store.getState().errors[0]);
+ ok(
+ /Assertion failure/.test(store.getState().errors[0]),
+ "Error thrown when taking a census of a snapshot that has not been read."
+ );
+
+ store.dispatch(actions.readSnapshot(heapWorker, snapshot.id));
+ await waitUntilState(
+ store,
+ () => store.getState().snapshots[0].state === states.READ
+ );
+
+ store.dispatch(actions.takeCensus(heapWorker, snapshot.id));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ snapshot = store.getState().snapshots[0];
+ ok(snapshot.census, "Snapshot has census after saved census");
+ ok(snapshot.census.report.children.length, "Census is in tree node form");
+ equal(
+ snapshot.census.display,
+ censusDisplays.coarseType,
+ "Snapshot stored correct display used for the census"
+ );
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-take-snapshot-and-census.js b/devtools/client/memory/test/xpcshell/test_action-take-snapshot-and-census.js
new file mode 100644
index 0000000000..ce7d166aca
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-take-snapshot-and-census.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the task creator `takeSnapshotAndCensus()` for the whole flow of
+ * taking a snapshot, and its sub-actions.
+ */
+
+const {
+ snapshotState: states,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const actions = require("resource://devtools/client/memory/actions/snapshot.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+
+ let snapshotI = 0;
+ let censusI = 0;
+ const snapshotStates = ["SAVING", "SAVED", "READING", "READ"];
+ const censusStates = ["SAVING", "SAVED"];
+ const expectStates = () => {
+ const snapshot = store.getState().snapshots[0];
+ if (!snapshot) {
+ return;
+ }
+ if (snapshotI < snapshotStates.length) {
+ const isCorrectState =
+ snapshot.state === states[snapshotStates[snapshotI]];
+ if (isCorrectState) {
+ ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
+ snapshotI++;
+ }
+ }
+ if (snapshot.treeMap && censusI < censusStates.length) {
+ if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
+ ok(true, `Found expected census state ${censusStates[censusI]}`);
+ censusI++;
+ }
+ }
+ };
+
+ const unsubscribe = store.subscribe(expectStates);
+ store.dispatch(actions.takeSnapshotAndCensus(front, heapWorker));
+
+ await waitUntilState(store, () => {
+ return (
+ snapshotI === snapshotStates.length && censusI === censusStates.length
+ );
+ });
+ unsubscribe();
+
+ ok(
+ true,
+ "takeSnapshotAndCensus() produces the correct sequence of states in a snapshot"
+ );
+ const snapshot = store.getState().snapshots[0];
+ ok(snapshot.treeMap, "snapshot has tree map census data");
+ ok(snapshot.selected, "snapshot is selected");
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-take-snapshot.js b/devtools/client/memory/test/xpcshell/test_action-take-snapshot.js
new file mode 100644
index 0000000000..e8bd46898c
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-take-snapshot.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the async reducer responding to the action `takeSnapshot(front)`
+ */
+
+const actions = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ snapshotState: states,
+} = require("resource://devtools/client/memory/constants.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ await front.attach();
+ const store = Store();
+
+ const unsubscribe = store.subscribe(checkState);
+
+ let foundPendingState = false;
+ let foundDoneState = false;
+
+ function checkState() {
+ const { snapshots } = store.getState();
+ const lastSnapshot = snapshots[snapshots.length - 1];
+
+ if (lastSnapshot.state === states.SAVING) {
+ foundPendingState = true;
+ ok(
+ foundPendingState,
+ "Got state change for pending heap snapshot request"
+ );
+ ok(!lastSnapshot.path, "Snapshot does not yet have a path");
+ ok(!lastSnapshot.census, "Has no census data when loading");
+ } else if (lastSnapshot.state === states.SAVED) {
+ foundDoneState = true;
+ ok(
+ foundDoneState,
+ "Got state change for completed heap snapshot request"
+ );
+ ok(foundPendingState, "SAVED state occurs after SAVING state");
+ ok(lastSnapshot.path, "Snapshot fetched with a path");
+ ok(
+ snapshots.every(s => s.selected === (s.id === lastSnapshot.id)),
+ "Only recent snapshot is selected"
+ );
+ }
+ }
+
+ for (let i = 0; i < 4; i++) {
+ store.dispatch(actions.takeSnapshot(front));
+ await waitUntilState(store, () => foundPendingState && foundDoneState);
+
+ // reset state trackers
+ foundDoneState = foundPendingState = false;
+ }
+
+ unsubscribe();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-01.js b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-01.js
new file mode 100644
index 0000000000..6744390ecd
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-01.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that changing displays with different inverted state properly
+// refreshes the selected census.
+
+const {
+ censusDisplays,
+ censusState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setCensusDisplayAndRefresh,
+} = require("resource://devtools/client/memory/actions/census-display.js");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Select a non-inverted display.
+ dispatch(
+ setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)
+ );
+ equal(getState().censusDisplay.inverted, false, "not inverted by default");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ await waitUntilCensusState(store, s => s.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+ ok(true, "saved 3 snapshots and took a census of each of them");
+
+ // Select an inverted display.
+ dispatch(
+ setCensusDisplayAndRefresh(
+ heapWorker,
+ censusDisplays.invertedAllocationStack
+ )
+ );
+
+ await waitUntilCensusState(store, s => s.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVING,
+ ]);
+ ok(true, "toggling inverted should recompute the selected snapshot's census");
+
+ equal(getState().censusDisplay.inverted, true, "now inverted");
+
+ await waitUntilCensusState(store, s => s.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+
+ equal(getState().snapshots[0].census.display.inverted, false);
+ equal(getState().snapshots[1].census.display.inverted, false);
+ equal(getState().snapshots[2].census.display.inverted, true);
+
+ dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
+ await waitUntilCensusState(store, s => s.census, [
+ censusState.SAVED,
+ censusState.SAVING,
+ censusState.SAVED,
+ ]);
+ ok(true, "selecting non-inverted census should trigger a recompute");
+
+ await waitUntilCensusState(store, s => s.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+
+ equal(getState().snapshots[0].census.display.inverted, false);
+ equal(getState().snapshots[1].census.display.inverted, true);
+ equal(getState().snapshots[2].census.display.inverted, true);
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-02.js b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-02.js
new file mode 100644
index 0000000000..7047f6410e
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-02.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that changing inverted state in the middle of taking a snapshot results
+// in an inverted census.
+
+const {
+ censusDisplays,
+ snapshotState: states,
+ censusState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ setCensusDisplay,
+ setCensusDisplayAndRefresh,
+} = require("resource://devtools/client/memory/actions/census-display.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ dispatch(setCensusDisplay(censusDisplays.allocationStack));
+ equal(
+ getState().censusDisplay.inverted,
+ false,
+ "Should not have an inverted census display"
+ );
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilSnapshotState(store, [states.SAVING]);
+
+ dispatch(
+ setCensusDisplayAndRefresh(
+ heapWorker,
+ censusDisplays.invertedAllocationStack
+ )
+ );
+
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ ok(getState().censusDisplay.inverted, "should want inverted trees");
+ ok(
+ getState().snapshots[0].census.display.inverted,
+ "snapshot-we-were-in-the-middle-of-saving's census should be inverted"
+ );
+
+ dispatch(
+ setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)
+ );
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+ ok(true, "toggling inverted retriggers census");
+ ok(!getState().censusDisplay.inverted, "no longer inverted");
+
+ dispatch(
+ setCensusDisplayAndRefresh(
+ heapWorker,
+ censusDisplays.invertedAllocationStack
+ )
+ );
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+ ok(getState().censusDisplay.inverted, "inverted again");
+ ok(
+ getState().snapshots[0].census.display.inverted,
+ "census-we-were-in-the-middle-of-recomputing should be inverted again"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-toggle-inverted.js b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted.js
new file mode 100644
index 0000000000..8d946303fa
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the top level inversion state of the tree.
+
+const {
+ censusDisplays,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setCensusDisplay,
+} = require("resource://devtools/client/memory/actions/census-display.js");
+
+add_task(async function () {
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(setCensusDisplay(censusDisplays.allocationStack));
+ equal(getState().censusDisplay.inverted, false, "not inverted initially");
+
+ dispatch(setCensusDisplay(censusDisplays.invertedAllocationStack));
+ equal(getState().censusDisplay.inverted, true, "now inverted after toggling");
+
+ dispatch(setCensusDisplay(censusDisplays.allocationStack));
+ equal(
+ getState().censusDisplay.inverted,
+ false,
+ "not inverted again after toggling again"
+ );
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action-toggle-recording-allocations.js b/devtools/client/memory/test/xpcshell/test_action-toggle-recording-allocations.js
new file mode 100644
index 0000000000..9ff428b093
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-toggle-recording-allocations.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test toggling the recording of allocation stacks.
+ */
+
+const {
+ toggleRecordingAllocationStacks,
+} = require("resource://devtools/client/memory/actions/allocations.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ await front.attach();
+ // Implement the minimal mock, doing nothing to make toggleRecordingAllocationStacks pass
+ const commands = {
+ targetCommand: {
+ hasTargetWatcherSupport() {
+ return true;
+ },
+ },
+ targetConfigurationCommand: {
+ updateConfiguration() {},
+ },
+ };
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().allocations.recording, false, "not recording by default");
+ equal(
+ getState().allocations.togglingInProgress,
+ false,
+ "not in the process of toggling by default"
+ );
+
+ dispatch(toggleRecordingAllocationStacks(commands));
+ await waitUntilState(store, () => getState().allocations.togglingInProgress);
+ ok(true, "`togglingInProgress` set to true when toggling on");
+ await waitUntilState(store, () => !getState().allocations.togglingInProgress);
+
+ equal(getState().allocations.recording, true, "now we are recording");
+
+ dispatch(toggleRecordingAllocationStacks(commands));
+ await waitUntilState(store, () => getState().allocations.togglingInProgress);
+ ok(true, "`togglingInProgress` set to true when toggling off");
+ await waitUntilState(store, () => !getState().allocations.togglingInProgress);
+
+ equal(getState().allocations.recording, false, "now we are not recording");
+
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_01.js b/devtools/client/memory/test/xpcshell/test_action_diffing_01.js
new file mode 100644
index 0000000000..ede92d0ed7
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_01.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling of diffing.
+
+const {
+ toggleDiffing,
+} = require("resource://devtools/client/memory/actions/diffing.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().diffing, null, "not diffing by default");
+
+ dispatch(toggleDiffing());
+ ok(getState().diffing, "now diffing after toggling");
+
+ dispatch(toggleDiffing());
+ equal(getState().diffing, null, "not diffing again after toggling again");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_02.js b/devtools/client/memory/test/xpcshell/test_action_diffing_02.js
new file mode 100644
index 0000000000..b0c4f21fc1
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_02.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that toggling diffing unselects all snapshots.
+
+const {
+ censusState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ toggleDiffing,
+} = require("resource://devtools/client/memory/actions/diffing.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ equal(getState().diffing, null, "not diffing by default");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+
+ ok(
+ getState().snapshots.some(s => s.selected),
+ "One of the new snapshots is selected"
+ );
+
+ dispatch(toggleDiffing());
+ ok(getState().diffing, "now diffing after toggling");
+
+ for (const s of getState().snapshots) {
+ ok(
+ !s.selected,
+ "No snapshot should be selected after entering diffing mode"
+ );
+ }
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_03.js b/devtools/client/memory/test/xpcshell/test_action_diffing_03.js
new file mode 100644
index 0000000000..8db0e8568b
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_03.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test selecting snapshots for diffing.
+
+const {
+ diffingState,
+ snapshotState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffing,
+} = require("resource://devtools/client/memory/actions/diffing.js");
+const {
+ takeSnapshot,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+// We test that you (1) cannot select a snapshot that is not in a diffable
+// state, and (2) cannot select more than 2 snapshots for diffing. Both attempts
+// trigger assertion failures.
+EXPECTED_DTU_ASSERT_FAILURE_COUNT = 2;
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+ equal(getState().diffing, null, "not diffing by default");
+
+ dispatch(takeSnapshot(front, heapWorker));
+ dispatch(takeSnapshot(front, heapWorker));
+ dispatch(takeSnapshot(front, heapWorker));
+
+ await waitUntilSnapshotState(store, [
+ snapshotState.SAVED,
+ snapshotState.SAVED,
+ snapshotState.SAVED,
+ ]);
+ dispatch(takeSnapshot(front));
+
+ // Start diffing.
+ dispatch(toggleDiffing());
+ ok(getState().diffing, "now diffing after toggling");
+ equal(getState().diffing.firstSnapshotId, null, "no first snapshot selected");
+ equal(
+ getState().diffing.secondSnapshotId,
+ null,
+ "no second snapshot selected"
+ );
+ equal(
+ getState().diffing.state,
+ diffingState.SELECTING,
+ "should be in diffing state SELECTING"
+ );
+
+ // Can't select a snapshot that is not in a diffable state.
+ equal(
+ getState().snapshots[3].state,
+ snapshotState.SAVING,
+ "the last snapshot is still in the process of being saved"
+ );
+ dumpn("Expecting exception:");
+ let threw = false;
+ try {
+ dispatch(selectSnapshotForDiffing(getState().snapshots[3]));
+ } catch (error) {
+ threw = true;
+ }
+ ok(
+ threw,
+ "Should not be able to select snapshots that aren't ready for diffing"
+ );
+
+ // Select first snapshot for diffing.
+ dispatch(selectSnapshotForDiffing(getState().snapshots[0]));
+ ok(getState().diffing, "now diffing after toggling");
+ equal(
+ getState().diffing.firstSnapshotId,
+ getState().snapshots[0].id,
+ "first snapshot selected"
+ );
+ equal(
+ getState().diffing.secondSnapshotId,
+ null,
+ "no second snapshot selected"
+ );
+ equal(
+ getState().diffing.state,
+ diffingState.SELECTING,
+ "should still be in diffing state SELECTING"
+ );
+
+ // Can't diff first snapshot with itself; this is a noop.
+ dispatch(selectSnapshotForDiffing(getState().snapshots[0]));
+ ok(getState().diffing, "still diffing");
+ equal(
+ getState().diffing.firstSnapshotId,
+ getState().snapshots[0].id,
+ "first snapshot still selected"
+ );
+ equal(
+ getState().diffing.secondSnapshotId,
+ null,
+ "still no second snapshot selected"
+ );
+ equal(
+ getState().diffing.state,
+ diffingState.SELECTING,
+ "should still be in diffing state SELECTING"
+ );
+
+ // Select second snapshot for diffing.
+ dispatch(selectSnapshotForDiffing(getState().snapshots[1]));
+ ok(getState().diffing, "still diffing");
+ equal(
+ getState().diffing.firstSnapshotId,
+ getState().snapshots[0].id,
+ "first snapshot still selected"
+ );
+ equal(
+ getState().diffing.secondSnapshotId,
+ getState().snapshots[1].id,
+ "second snapshot selected"
+ );
+
+ // Can't select more than two snapshots for diffing.
+ dumpn("Expecting exception:");
+ threw = false;
+ try {
+ dispatch(selectSnapshotForDiffing(getState().snapshots[2]));
+ } catch (error) {
+ threw = true;
+ }
+ ok(threw, "Can't select more than two snapshots for diffing");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_04.js b/devtools/client/memory/test/xpcshell/test_action_diffing_04.js
new file mode 100644
index 0000000000..92a57276bb
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_04.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we compute census diffs.
+
+const {
+ diffingState,
+ snapshotState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffingAndRefresh,
+} = require("resource://devtools/client/memory/actions/diffing.js");
+const {
+ takeSnapshot,
+ readSnapshot,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+ dispatch(changeView(viewState.CENSUS));
+
+ equal(getState().diffing, null, "not diffing by default");
+
+ const s1 = await dispatch(takeSnapshot(front, heapWorker));
+ const s2 = await dispatch(takeSnapshot(front, heapWorker));
+ const s3 = await dispatch(takeSnapshot(front, heapWorker));
+ dispatch(readSnapshot(heapWorker, s1));
+ dispatch(readSnapshot(heapWorker, s2));
+ dispatch(readSnapshot(heapWorker, s3));
+ await waitUntilSnapshotState(store, [
+ snapshotState.READ,
+ snapshotState.READ,
+ snapshotState.READ,
+ ]);
+
+ dispatch(toggleDiffing());
+ dispatch(
+ selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[0])
+ );
+ dispatch(
+ selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[1])
+ );
+
+ ok(getState().diffing, "We should be diffing.");
+ equal(
+ getState().diffing.firstSnapshotId,
+ getState().snapshots[0].id,
+ "First snapshot selected."
+ );
+ equal(
+ getState().diffing.secondSnapshotId,
+ getState().snapshots[1].id,
+ "Second snapshot selected."
+ );
+
+ await waitUntilState(
+ store,
+ state => state.diffing.state === diffingState.TAKING_DIFF
+ );
+ ok(
+ true,
+ "Selecting two snapshots for diffing should trigger computing a diff."
+ );
+
+ await waitUntilState(
+ store,
+ state => state.diffing.state === diffingState.TOOK_DIFF
+ );
+ ok(true, "And then the diff should complete.");
+ ok(getState().diffing.census, "And we should have a census.");
+ ok(getState().diffing.census.report, "And that census should have a report.");
+ equal(
+ getState().diffing.census.display,
+ getState().censusDisplay,
+ "And that census should have the correct display"
+ );
+ equal(
+ getState().diffing.census.filter,
+ getState().filter,
+ "And that census should have the correct filter"
+ );
+ equal(
+ getState().diffing.census.display.inverted,
+ getState().censusDisplay.inverted,
+ "And that census should have the correct inversion"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_05.js b/devtools/client/memory/test/xpcshell/test_action_diffing_05.js
new file mode 100644
index 0000000000..885ae5c1d4
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_05.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we recompute census diffs at the appropriate times.
+
+const {
+ diffingState,
+ snapshotState,
+ censusDisplays,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setCensusDisplayAndRefresh,
+} = require("resource://devtools/client/memory/actions/census-display.js");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffingAndRefresh,
+} = require("resource://devtools/client/memory/actions/diffing.js");
+const {
+ setFilterStringAndRefresh,
+} = require("resource://devtools/client/memory/actions/filter.js");
+const {
+ takeSnapshot,
+ readSnapshot,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+ dispatch(changeView(viewState.CENSUS));
+
+ await dispatch(
+ setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)
+ );
+ equal(getState().censusDisplay.inverted, false, "not inverted at start");
+
+ equal(getState().diffing, null, "not diffing by default");
+
+ const s1 = await dispatch(takeSnapshot(front, heapWorker));
+ const s2 = await dispatch(takeSnapshot(front, heapWorker));
+ const s3 = await dispatch(takeSnapshot(front, heapWorker));
+ dispatch(readSnapshot(heapWorker, s1));
+ dispatch(readSnapshot(heapWorker, s2));
+ dispatch(readSnapshot(heapWorker, s3));
+ await waitUntilSnapshotState(store, [
+ snapshotState.READ,
+ snapshotState.READ,
+ snapshotState.READ,
+ ]);
+
+ await dispatch(toggleDiffing());
+ dispatch(
+ selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[0])
+ );
+ dispatch(
+ selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[1])
+ );
+ await waitUntilState(
+ store,
+ state => state.diffing.state === diffingState.TOOK_DIFF
+ );
+
+ const shouldTriggerRecompute = [
+ {
+ name: "toggling inversion",
+ func: () =>
+ dispatch(
+ setCensusDisplayAndRefresh(
+ heapWorker,
+ censusDisplays.invertedAllocationStack
+ )
+ ),
+ },
+ {
+ name: "filtering",
+ func: () => dispatch(setFilterStringAndRefresh("scr", heapWorker)),
+ },
+ {
+ name: "changing displays",
+ func: () =>
+ dispatch(
+ setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType)
+ ),
+ },
+ ];
+
+ for (const { name, func } of shouldTriggerRecompute) {
+ dumpn(`Testing that "${name}" triggers a diff recompute`);
+ func();
+
+ await waitUntilState(
+ store,
+ state => state.diffing.state === diffingState.TAKING_DIFF
+ );
+ ok(true, "triggered diff recompute.");
+
+ await waitUntilState(
+ store,
+ state => state.diffing.state === diffingState.TOOK_DIFF
+ );
+ ok(true, "And then the diff should complete.");
+ ok(getState().diffing.census, "And we should have a census.");
+ ok(
+ getState().diffing.census.report,
+ "And that census should have a report."
+ );
+ equal(
+ getState().diffing.census.display,
+ getState().censusDisplay,
+ "And that census should have the correct display"
+ );
+ equal(
+ getState().diffing.census.filter,
+ getState().filter,
+ "And that census should have the correct filter"
+ );
+ equal(
+ getState().diffing.census.display.inverted,
+ getState().censusDisplay.inverted,
+ "And that census should have the correct inversion"
+ );
+ }
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_01.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_01.js
new file mode 100644
index 0000000000..9b8bbb2533
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_01.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can compute and fetch the dominator tree for a snapshot.
+
+const {
+ dominatorTreeState,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+ computeAndFetchDominatorTree,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(
+ !getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view."
+ );
+
+ // Change to the dominator tree view.
+ dispatch(
+ computeAndFetchDominatorTree(heapWorker, getState().snapshots[0].id)
+ );
+ ok(
+ getState().snapshots[0].dominatorTree,
+ "Should now have a dominator tree model for the selected snapshot"
+ );
+
+ // Wait for the dominator tree to start being computed.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.COMPUTING
+ );
+ ok(true, "The dominator tree started computing");
+ ok(
+ !getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root"
+ );
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING
+ );
+ ok(true, "The dominator tree started fetching");
+ ok(
+ !getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root"
+ );
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "The dominator tree was fetched");
+ ok(
+ getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_02.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_02.js
new file mode 100644
index 0000000000..eb38591286
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_02.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that selecting the dominator tree view automatically kicks off fetching
+// and computing dominator trees.
+
+const {
+ dominatorTreeState,
+ viewState,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeViewAndRefresh,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(
+ !getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view."
+ );
+
+ dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
+ ok(
+ getState().snapshots[0].dominatorTree,
+ "Should now have a dominator tree model for the selected snapshot"
+ );
+
+ // Wait for the dominator tree to start being computed.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.COMPUTING
+ );
+ ok(true, "The dominator tree started computing");
+ ok(
+ !getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root"
+ );
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING
+ );
+ ok(true, "The dominator tree started fetching");
+ ok(
+ !getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root"
+ );
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "The dominator tree was fetched");
+ ok(
+ getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_03.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_03.js
new file mode 100644
index 0000000000..e46c1969c6
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_03.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that selecting the dominator tree view and then taking a snapshot
+// properly kicks off fetching and computing dominator trees.
+
+const {
+ dominatorTreeState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ equal(
+ getState().view.state,
+ viewState.DOMINATOR_TREE,
+ "We should now be in the DOMINATOR_TREE view"
+ );
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to start being computed.
+ await waitUntilState(
+ store,
+ state => state.snapshots[0] && state.snapshots[0].dominatorTree
+ );
+ equal(
+ getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.COMPUTING,
+ "The dominator tree started computing"
+ );
+ ok(
+ !getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root"
+ );
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING
+ );
+ ok(true, "The dominator tree started fetching");
+ ok(
+ !getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root"
+ );
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "The dominator tree was fetched");
+ ok(
+ getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_04.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_04.js
new file mode 100644
index 0000000000..d9814590a9
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_04.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that selecting the dominator tree view while in the middle of taking a
+// snapshot properly kicks off fetching and computing dominator trees.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+
+ for (const intermediateSnapshotState of [
+ states.SAVING,
+ states.READING,
+ states.READ,
+ ]) {
+ dumpn(
+ "Testing switching to the DOMINATOR_TREE view in the middle of the " +
+ `${intermediateSnapshotState} snapshot state`
+ );
+
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilSnapshotState(store, [intermediateSnapshotState]);
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ equal(
+ getState().view.state,
+ viewState.DOMINATOR_TREE,
+ "We should now be in the DOMINATOR_TREE view"
+ );
+
+ // Wait for the dominator tree to start being computed.
+ await waitUntilState(
+ store,
+ state => state.snapshots[0] && state.snapshots[0].dominatorTree
+ );
+ equal(
+ getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.COMPUTING,
+ "The dominator tree started computing"
+ );
+ ok(
+ !getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root"
+ );
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING
+ );
+ ok(true, "The dominator tree started fetching");
+ ok(
+ !getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root"
+ );
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "The dominator tree was fetched");
+ ok(
+ getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root"
+ );
+ }
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_05.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_05.js
new file mode 100644
index 0000000000..7f6ed31bb3
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_05.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that changing the currently selected snapshot to a snapshot that does
+// not have a dominator tree will automatically compute and fetch one for it.
+
+const {
+ dominatorTreeState,
+ viewState,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.treeMap, [
+ treeMapState.SAVED,
+ treeMapState.SAVED,
+ ]);
+
+ ok(getState().snapshots[1].selected, "The second snapshot is selected");
+
+ // Change to the dominator tree view.
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[1].dominatorTree &&
+ state.snapshots[1].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "The second snapshot's dominator tree was fetched");
+
+ // Select the first snapshot.
+ dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[0].id));
+
+ // And now the first snapshot should have its dominator tree fetched and
+ // computed because of the new selection.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "The first snapshot's dominator tree was fetched");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_06.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_06.js
new file mode 100644
index 0000000000..7caffb972e
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_06.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can incrementally fetch a subtree of a dominator tree.
+
+const {
+ dominatorTreeState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+ fetchImmediatelyDominated,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const DominatorTreeLazyChildren = require("resource://devtools/client/memory/dominator-tree-lazy-children.js");
+
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(
+ getState().snapshots[0].dominatorTree.root,
+ "The dominator tree was fetched"
+ );
+
+ // Find a node that has children, but none of them are loaded.
+
+ function findNode(node) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const found = findNode(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const oldRoot = getState().snapshots[0].dominatorTree.root;
+ const oldNode = findNode(oldRoot);
+ ok(
+ oldNode,
+ "Should have found a node with children that are not loaded since we " +
+ "only send partial dominator trees across initially and load the rest " +
+ "on demand"
+ );
+ Assert.notStrictEqual(
+ oldNode,
+ oldRoot,
+ "But the node should not be the root"
+ );
+
+ const lazyChildren = new DominatorTreeLazyChildren(oldNode.nodeId, 0);
+ dispatch(
+ fetchImmediatelyDominated(
+ heapWorker,
+ getState().snapshots[0].id,
+ lazyChildren
+ )
+ );
+
+ equal(
+ getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Fetching immediately dominated children should put us in the " +
+ "INCREMENTAL_FETCHING state"
+ );
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(
+ true,
+ "The dominator tree should go back to LOADED after the incremental " +
+ "fetching is done."
+ );
+
+ const newRoot = getState().snapshots[0].dominatorTree.root;
+ Assert.notStrictEqual(
+ oldRoot,
+ newRoot,
+ "When we insert new nodes, we get a new tree"
+ );
+ equal(
+ oldRoot.children.length,
+ newRoot.children.length,
+ "The new tree's root should have the same number of children as the " +
+ "old root's"
+ );
+
+ let differentChildrenCount = 0;
+ for (let i = 0; i < oldRoot.children.length; i++) {
+ if (oldRoot.children[i] !== newRoot.children[i]) {
+ differentChildrenCount++;
+ }
+ }
+ equal(
+ differentChildrenCount,
+ 1,
+ "All subtrees except the subtree we inserted incrementally fetched " +
+ "children into should be the same because we use persistent updates"
+ );
+
+ // Find the new node which has the children inserted.
+
+ function findNewNode(node) {
+ if (node.nodeId === oldNode.nodeId) {
+ return node;
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const found = findNewNode(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const newNode = findNewNode(newRoot);
+ ok(newNode, "Should find the node in the new tree again");
+ Assert.notStrictEqual(
+ newNode,
+ oldNode,
+ "We did not mutate the old node in place, instead created a new node"
+ );
+ ok(newNode.children, "And the new node should have the children attached");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_07.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_07.js
new file mode 100644
index 0000000000..21fda9d951
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_07.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can incrementally fetch two subtrees in the same dominator tree
+// concurrently. This exercises the activeFetchRequestCount machinery.
+
+const {
+ dominatorTreeState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+ fetchImmediatelyDominated,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const DominatorTreeLazyChildren = require("resource://devtools/client/memory/dominator-tree-lazy-children.js");
+
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(
+ getState().snapshots[0].dominatorTree.root,
+ "The dominator tree was fetched"
+ );
+
+ // Find a node that has more children.
+
+ function findNode(node) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const found = findNode(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const oldRoot = getState().snapshots[0].dominatorTree.root;
+ const oldNode = findNode(oldRoot);
+ ok(oldNode, "Should have found a node with more children.");
+
+ // Find another node that has more children.
+ function findNodeRev(node) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (const child of node.children.slice().reverse()) {
+ const found = findNodeRev(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const oldNode2 = findNodeRev(oldRoot);
+ ok(oldNode2, "Should have found another node with more children.");
+ Assert.notStrictEqual(
+ oldNode,
+ oldNode2,
+ "The second node should not be the same as the first one"
+ );
+
+ // Fetch both subtrees concurrently.
+ dispatch(
+ fetchImmediatelyDominated(
+ heapWorker,
+ getState().snapshots[0].id,
+ new DominatorTreeLazyChildren(oldNode.nodeId, 0)
+ )
+ );
+ dispatch(
+ fetchImmediatelyDominated(
+ heapWorker,
+ getState().snapshots[0].id,
+ new DominatorTreeLazyChildren(oldNode2.nodeId, 0)
+ )
+ );
+
+ equal(
+ getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Fetching immediately dominated children should put us in the " +
+ "INCREMENTAL_FETCHING state"
+ );
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(
+ true,
+ "The dominator tree should go back to LOADED after the incremental " +
+ "fetching is done."
+ );
+
+ const newRoot = getState().snapshots[0].dominatorTree.root;
+ Assert.notStrictEqual(
+ oldRoot,
+ newRoot,
+ "When we insert new nodes, we get a new tree"
+ );
+
+ // Find the new node which has the children inserted.
+
+ function findNodeWithId(id, node) {
+ if (node.nodeId === id) {
+ return node;
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const found = findNodeWithId(id, child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const newNode = findNodeWithId(oldNode.nodeId, newRoot);
+ ok(newNode, "Should find the node in the new tree again");
+ Assert.notStrictEqual(
+ newNode,
+ oldNode,
+ "We did not mutate the old node in place, instead created a new node"
+ );
+ ok(
+ newNode.children.length,
+ "And the new node should have the new children attached"
+ );
+
+ const newNode2 = findNodeWithId(oldNode2.nodeId, newRoot);
+ ok(newNode2, "Should find the second node in the new tree again");
+ Assert.notStrictEqual(
+ newNode2,
+ oldNode2,
+ "We did not mutate the second old node in place, instead created a new node"
+ );
+ ok(
+ newNode2.children,
+ "And the new node should have the new children attached"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_08.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_08.js
new file mode 100644
index 0000000000..e61c8b66b8
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_08.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can change the display with which we describe a dominator tree
+// and that the dominator tree is re-fetched.
+
+const {
+ dominatorTreeState,
+ viewState,
+ labelDisplays,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setLabelDisplayAndRefresh,
+} = require("resource://devtools/client/memory/actions/label-display.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(
+ !getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view."
+ );
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+
+ ok(
+ getState().labelDisplay,
+ "We have a default display for describing nodes in a dominator tree"
+ );
+ equal(
+ getState().labelDisplay,
+ labelDisplays.coarseType,
+ "and the default is coarse type"
+ );
+ equal(
+ getState().labelDisplay,
+ getState().snapshots[0].dominatorTree.display,
+ "and the newly computed dominator tree has that display"
+ );
+
+ // Switch to the allocationStack display.
+ dispatch(
+ setLabelDisplayAndRefresh(heapWorker, labelDisplays.allocationStack)
+ );
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING
+ );
+ ok(
+ true,
+ "switching display types caused the dominator tree to be fetched " +
+ "again."
+ );
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ equal(
+ getState().snapshots[0].dominatorTree.display,
+ labelDisplays.allocationStack,
+ "The new dominator tree's display is allocationStack"
+ );
+ equal(
+ getState().labelDisplay,
+ labelDisplays.allocationStack,
+ "as is our requested dominator tree display"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_09.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_09.js
new file mode 100644
index 0000000000..b87964970e
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_09.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can change the display with which we describe a dominator tree
+// while the dominator tree is in the middle of being fetched.
+
+const {
+ dominatorTreeState,
+ viewState,
+ labelDisplays,
+ treeMapState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ setLabelDisplayAndRefresh,
+} = require("resource://devtools/client/memory/actions/label-display.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
+ ok(
+ !getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view."
+ );
+
+ // Wait for the dominator tree to start fetching.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING
+ );
+
+ ok(
+ getState().labelDisplay,
+ "We have a default display for describing nodes in a dominator tree"
+ );
+ equal(
+ getState().labelDisplay,
+ labelDisplays.coarseType,
+ "and the default is coarse type"
+ );
+ equal(
+ getState().labelDisplay,
+ getState().snapshots[0].dominatorTree.display,
+ "and the newly computed dominator tree has that display"
+ );
+
+ // Switch to the allocationStack display while we are still fetching the
+ // dominator tree.
+ dispatch(
+ setLabelDisplayAndRefresh(heapWorker, labelDisplays.allocationStack)
+ );
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+
+ equal(
+ getState().snapshots[0].dominatorTree.display,
+ labelDisplays.allocationStack,
+ "The new dominator tree's display is allocationStack"
+ );
+ equal(
+ getState().labelDisplay,
+ labelDisplays.allocationStack,
+ "as is our requested dominator tree display"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_10.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_10.js
new file mode 100644
index 0000000000..f6dc791461
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_10.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we maintain focus of the selected dominator tree node across
+// changing breakdowns for labeling them.
+
+const {
+ dominatorTreeState,
+ labelDisplays,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ takeSnapshotAndCensus,
+ focusDominatorTreeNode,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+const {
+ setLabelDisplayAndRefresh,
+} = require("resource://devtools/client/memory/actions/label-display.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to finish being fetched.
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "The dominator tree was fetched");
+
+ const root = getState().snapshots[0].dominatorTree.root;
+ ok(root, "When the dominator tree is loaded, we should have its root");
+
+ dispatch(focusDominatorTreeNode(getState().snapshots[0].id, root));
+ equal(
+ root,
+ getState().snapshots[0].dominatorTree.focused,
+ "The root should be focused."
+ );
+
+ equal(
+ getState().labelDisplay,
+ labelDisplays.coarseType,
+ "Using labelDisplays.coarseType by default"
+ );
+ dispatch(
+ setLabelDisplayAndRefresh(heapWorker, labelDisplays.allocationStack)
+ );
+ equal(
+ getState().labelDisplay,
+ labelDisplays.allocationStack,
+ "Using labelDisplays.allocationStack now"
+ );
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING
+ );
+ ok(true, "We started re-fetching the dominator tree");
+
+ await waitUntilState(
+ store,
+ state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED
+ );
+ ok(true, "The dominator tree was loaded again");
+
+ ok(
+ getState().snapshots[0].dominatorTree.focused,
+ "Still have a focused node"
+ );
+ equal(
+ getState().snapshots[0].dominatorTree.focused.nodeId,
+ root.nodeId,
+ "Focused node is the same as before"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_individuals_01.js b/devtools/client/memory/test/xpcshell/test_individuals_01.js
new file mode 100644
index 0000000000..9278537136
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_01.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Basic test for switching to the individuals view.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().individuals, null, "no individuals state by default");
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().snapshots[0].census.display.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(
+ fetchIndividuals(heapWorker, snapshotId, breakdown, reportLeafIndex)
+ );
+
+ // Wait for each expected state.
+ for (const state of EXPECTED_INDIVIDUAL_STATES) {
+ await waitUntilState(store, s => {
+ return (
+ s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state
+ );
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(
+ !!getState().individuals.nodes.length,
+ "Should have a positive number of nodes"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_individuals_02.js b/devtools/client/memory/test/xpcshell/test_individuals_02.js
new file mode 100644
index 0000000000..76d34187a0
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_02.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test switching to the individuals view when we are in the middle of computing
+// a dominator tree.
+
+const {
+ censusState,
+ dominatorTreeState,
+ viewState,
+ individualsState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+ computeDominatorTree,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().individuals, null, "no individuals state by default");
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().snapshots[0].census.display.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ // Start computing a dominator tree.
+
+ dispatch(computeDominatorTree(heapWorker, snapshotId));
+ equal(
+ getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.COMPUTING,
+ "Should be computing dominator tree"
+ );
+
+ // Fetch individuals in the middle of computing the dominator tree.
+
+ dispatch(
+ fetchIndividuals(heapWorker, snapshotId, breakdown, reportLeafIndex)
+ );
+
+ // Wait for each expected state.
+ for (const state of EXPECTED_INDIVIDUAL_STATES) {
+ await waitUntilState(store, s => {
+ return (
+ s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state
+ );
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(
+ !!getState().individuals.nodes.length,
+ "Should have a positive number of nodes"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_individuals_03.js b/devtools/client/memory/test/xpcshell/test_individuals_03.js
new file mode 100644
index 0000000000..db31bcf347
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_03.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test switching to the individuals view when we are in the diffing view.
+
+const {
+ censusState,
+ diffingState,
+ viewState,
+ individualsState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+ popViewAndRefresh,
+} = require("resource://devtools/client/memory/actions/view.js");
+const {
+ selectSnapshotForDiffingAndRefresh,
+} = require("resource://devtools/client/memory/actions/diffing.js");
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Take two snapshots and diff them from each other.
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [
+ censusState.SAVED,
+ censusState.SAVED,
+ ]);
+
+ dispatch(changeView(viewState.DIFFING));
+ dispatch(
+ selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[0])
+ );
+ dispatch(
+ selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[1])
+ );
+
+ await waitUntilState(store, state => {
+ return state.diffing && state.diffing.state === diffingState.TOOK_DIFF;
+ });
+ ok(getState().diffing.census);
+
+ // Fetch individuals.
+
+ const root = getState().diffing.census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().diffing.secondSnapshotId;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().censusDisplay.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(
+ fetchIndividuals(heapWorker, snapshotId, breakdown, reportLeafIndex)
+ );
+
+ for (const state of EXPECTED_INDIVIDUAL_STATES) {
+ await waitUntilState(store, s => {
+ return (
+ s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state
+ );
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(
+ !!getState().individuals.nodes.length,
+ "Should have a positive number of nodes"
+ );
+
+ // Pop the view back to the diffing.
+
+ dispatch(popViewAndRefresh(heapWorker));
+
+ await waitUntilState(store, state => {
+ return state.diffing && state.diffing.state === diffingState.TOOK_DIFF;
+ });
+
+ ok(
+ getState().diffing.census.report,
+ "We have our census diff again after popping back to the last view"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_individuals_04.js b/devtools/client/memory/test/xpcshell/test_individuals_04.js
new file mode 100644
index 0000000000..f2cd71d4b9
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_04.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test showing individual Array objects.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+const {
+ setFilterString,
+} = require("resource://devtools/client/memory/actions/filter.js");
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(setFilterString("Array"));
+
+ // Take a snapshot and wait for the census to finish.
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ // Fetch individuals.
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root, "Array");
+ ok(reportLeafIndex, "Should get a reportLeafIndex for Array");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().censusDisplay.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(
+ fetchIndividuals(heapWorker, snapshotId, breakdown, reportLeafIndex)
+ );
+
+ for (const state of EXPECTED_INDIVIDUAL_STATES) {
+ await waitUntilState(store, s => {
+ return (
+ s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state
+ );
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(
+ !!getState().individuals.nodes.length,
+ "Should have a positive number of nodes"
+ );
+
+ // Assert that all the individuals are `Array`s.
+
+ for (const node of getState().individuals.nodes) {
+ dumpn("Checking node: " + node.label.join(" > "));
+ ok(
+ node.label.find(part => part === "Array"),
+ "The node should be an Array node"
+ );
+ }
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_individuals_05.js b/devtools/client/memory/test/xpcshell/test_individuals_05.js
new file mode 100644
index 0000000000..506751916a
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_05.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test showing individual objects that do not have allocation stacks.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+ censusDisplays,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+const {
+ setCensusDisplay,
+} = require("resource://devtools/client/memory/actions/census-display.js");
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(setCensusDisplay(censusDisplays.invertedAllocationStack));
+
+ // Take a snapshot and wait for the census to finish.
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ // Fetch individuals.
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root, "noStack");
+ ok(reportLeafIndex, "Should get a reportLeafIndex for noStack");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().censusDisplay.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(
+ fetchIndividuals(heapWorker, snapshotId, breakdown, reportLeafIndex)
+ );
+
+ for (const state of EXPECTED_INDIVIDUAL_STATES) {
+ await waitUntilState(store, s => {
+ return (
+ s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state
+ );
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(
+ !!getState().individuals.nodes.length,
+ "Should have a positive number of nodes"
+ );
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_individuals_06.js b/devtools/client/memory/test/xpcshell/test_individuals_06.js
new file mode 100644
index 0000000000..54aa759464
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_06.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that clearing the current individuals' snapshot leaves the individuals
+// view.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+ clearSnapshots,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const EXPECTED_INDIVIDUAL_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ // Take a snapshot and wait for the census to finish.
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ // Fetch individuals.
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().censusDisplay.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ dispatch(
+ fetchIndividuals(heapWorker, snapshotId, breakdown, reportLeafIndex)
+ );
+
+ for (const state of EXPECTED_INDIVIDUAL_STATES) {
+ await waitUntilState(store, s => {
+ return (
+ s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state
+ );
+ });
+ ok(true, `Reached state = ${state}`);
+ }
+
+ ok(getState().individuals, "Should have individuals state");
+ ok(getState().individuals.nodes, "Should have individuals nodes");
+ ok(
+ !!getState().individuals.nodes.length,
+ "Should have a positive number of nodes"
+ );
+
+ dispatch(clearSnapshots(heapWorker));
+
+ equal(getState().view.state, viewState.CENSUS, "Went back to census view");
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_pop_view_01.js b/devtools/client/memory/test/xpcshell/test_pop_view_01.js
new file mode 100644
index 0000000000..da44d1d125
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_pop_view_01.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test popping views from each intermediate individuals model state.
+
+const {
+ censusState,
+ viewState,
+ individualsState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ changeView,
+ popViewAndRefresh,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+const TEST_STATES = [
+ individualsState.COMPUTING_DOMINATOR_TREE,
+ individualsState.FETCHING,
+ individualsState.FETCHED,
+];
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().individuals, null, "no individuals state by default");
+
+ dispatch(changeView(viewState.CENSUS));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ const root = getState().snapshots[0].census.report;
+ ok(root, "Should have a census");
+
+ const reportLeafIndex = findReportLeafIndex(root);
+ ok(reportLeafIndex, "Should get a reportLeafIndex");
+
+ const snapshotId = getState().snapshots[0].id;
+ ok(snapshotId, "Should have a snapshot id");
+
+ const breakdown = getState().snapshots[0].census.display.breakdown;
+ ok(breakdown, "Should have a breakdown");
+
+ for (const state of TEST_STATES) {
+ dumpn(`Testing popping back to the old view from state = ${state}`);
+
+ dispatch(
+ fetchIndividuals(heapWorker, snapshotId, breakdown, reportLeafIndex)
+ );
+
+ // Wait for the expected test state.
+ await waitUntilState(store, s => {
+ return (
+ s.view.state === viewState.INDIVIDUALS &&
+ s.individuals &&
+ s.individuals.state === state
+ );
+ });
+ ok(true, `Reached state = ${state}`);
+
+ // Pop back to the CENSUS state.
+ dispatch(popViewAndRefresh(heapWorker));
+ await waitUntilState(store, s => {
+ return s.view.state === viewState.CENSUS;
+ });
+ ok(!getState().individuals, "Should no longer have individuals");
+ }
+
+ heapWorker.destroy();
+ await front.detach();
+});
diff --git a/devtools/client/memory/test/xpcshell/test_tree-map-01.js b/devtools/client/memory/test/xpcshell/test_tree-map-01.js
new file mode 100644
index 0000000000..1e91a43ec9
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_tree-map-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ drawBox,
+} = require("resource://devtools/client/memory/components/tree-map/draw.js");
+
+add_task(async function () {
+ let fillRectValues, strokeRectValues;
+ const ctx = {
+ fillRect: (...args) => {
+ fillRectValues = args;
+ },
+ strokeRect: (...args) => {
+ strokeRectValues = args;
+ },
+ };
+ const node = {
+ x: 20,
+ y: 30,
+ dx: 50,
+ dy: 70,
+ type: "other",
+ depth: 2,
+ };
+ const padding = [10, 10];
+ const borderWidth = () => 1;
+ const dragZoom = {
+ offsetX: 0,
+ offsetY: 0,
+ zoom: 0,
+ };
+ drawBox(ctx, node, borderWidth, dragZoom, padding);
+ ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
+ equal(ctx.fillStyle, "hsl(204,60%,70%)", "The fillStyle is set");
+ equal(ctx.strokeStyle, "hsl(204,60%,35%)", "The strokeStyle is set");
+ equal(ctx.lineWidth, 1, "The lineWidth is set");
+ deepEqual(fillRectValues, [10.5, 20.5, 49, 69], "Draws a filled rectangle");
+ deepEqual(
+ strokeRectValues,
+ [10.5, 20.5, 49, 69],
+ "Draws a stroked rectangle"
+ );
+
+ dragZoom.zoom = 0.5;
+
+ drawBox(ctx, node, borderWidth, dragZoom, padding);
+ ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
+ deepEqual(
+ fillRectValues,
+ [15.5, 30.5, 74, 104],
+ "Draws a zoomed filled rectangle"
+ );
+ deepEqual(
+ strokeRectValues,
+ [15.5, 30.5, 74, 104],
+ "Draws a zoomed stroked rectangle"
+ );
+
+ dragZoom.offsetX = 110;
+ dragZoom.offsetY = 130;
+
+ drawBox(ctx, node, borderWidth, dragZoom, padding);
+ deepEqual(
+ fillRectValues,
+ [-94.5, -99.5, 74, 104],
+ "Draws a zoomed and offset filled rectangle"
+ );
+ deepEqual(
+ strokeRectValues,
+ [-94.5, -99.5, 74, 104],
+ "Draws a zoomed and offset stroked rectangle"
+ );
+});
diff --git a/devtools/client/memory/test/xpcshell/test_tree-map-02.js b/devtools/client/memory/test/xpcshell/test_tree-map-02.js
new file mode 100644
index 0000000000..528c59dcfa
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_tree-map-02.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ drawText,
+} = require("resource://devtools/client/memory/components/tree-map/draw.js");
+
+add_task(async function () {
+ // Mock out the Canvas2dContext
+ const ctx = {
+ fillText: (...args) => fillTextValues.push(args),
+ measureText: text => {
+ const width = text ? text.length * 10 : 0;
+ return { width };
+ },
+ };
+ const node = {
+ x: 20,
+ y: 30,
+ dx: 500,
+ dy: 70,
+ name: "Example Node",
+ totalBytes: 1200,
+ totalCount: 100,
+ };
+ const ratio = 0;
+ const borderWidth = () => 1;
+ const dragZoom = {
+ offsetX: 0,
+ offsetY: 0,
+ zoom: 0,
+ };
+ let fillTextValues = [];
+ const padding = [10, 10];
+
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+ deepEqual(
+ fillTextValues[0],
+ ["Example Node", 11.5, 21.5],
+ "Fills in the full node name"
+ );
+ deepEqual(
+ fillTextValues[1],
+ ["1KiB 100 count", 141.5, 21.5],
+ "Includes the full byte and count information"
+ );
+
+ fillTextValues = [];
+ node.dx = 250;
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+
+ deepEqual(
+ fillTextValues[0],
+ ["Example Node", 11.5, 21.5],
+ "Fills in the full node name"
+ );
+ deepEqual(
+ fillTextValues[1],
+ undefined,
+ "Drops off the byte and count information if not enough room"
+ );
+
+ fillTextValues = [];
+ node.dx = 100;
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+
+ deepEqual(
+ fillTextValues[0],
+ ["Exampl...", 11.5, 21.5],
+ "Cuts the name with ellipsis"
+ );
+ deepEqual(
+ fillTextValues[1],
+ undefined,
+ "Drops off the byte and count information if not enough room"
+ );
+
+ fillTextValues = [];
+ node.dx = 40;
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+
+ deepEqual(
+ fillTextValues[0],
+ ["...", 11.5, 21.5],
+ "Shows only ellipsis when smaller"
+ );
+ deepEqual(
+ fillTextValues[1],
+ undefined,
+ "Drops off the byte and count information if not enough room"
+ );
+
+ fillTextValues = [];
+ node.dx = 20;
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+
+ deepEqual(fillTextValues[0], undefined, "Draw nothing when not enough room");
+ deepEqual(
+ fillTextValues[1],
+ undefined,
+ "Drops off the byte and count information if not enough room"
+ );
+});
diff --git a/devtools/client/memory/test/xpcshell/test_utils-get-snapshot-totals.js b/devtools/client/memory/test/xpcshell/test_utils-get-snapshot-totals.js
new file mode 100644
index 0000000000..cbb72586a4
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_utils-get-snapshot-totals.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that we use the correct snapshot aggregate value
+ * in `utils.getSnapshotTotals(snapshot)`
+ */
+
+const {
+ censusDisplays,
+ viewState,
+ censusState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ getSnapshotTotals,
+} = require("resource://devtools/client/memory/utils.js");
+const {
+ takeSnapshotAndCensus,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+const {
+ setCensusDisplayAndRefresh,
+} = require("resource://devtools/client/memory/actions/census-display.js");
+const {
+ changeView,
+} = require("resource://devtools/client/memory/actions/view.js");
+
+add_task(async function () {
+ const front = new StubbedMemoryFront();
+ const heapWorker = new HeapAnalysesClient();
+ await front.attach();
+ const store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.CENSUS));
+
+ await dispatch(
+ setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)
+ );
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
+ ok(
+ !getState().snapshots[0].census.display.inverted,
+ "Snapshot is not inverted"
+ );
+
+ const census = getState().snapshots[0].census;
+ let result = aggregate(census.report);
+ const totalBytes = result.bytes;
+ const totalCount = result.count;
+
+ Assert.greater(totalBytes, 0, "counted up bytes in the census");
+ Assert.greater(totalCount, 0, "counted up count in the census");
+
+ result = getSnapshotTotals(getState().snapshots[0].census);
+ equal(
+ totalBytes,
+ result.bytes,
+ "getSnapshotTotals reuslted in correct bytes"
+ );
+ equal(
+ totalCount,
+ result.count,
+ "getSnapshotTotals reuslted in correct count"
+ );
+
+ dispatch(
+ setCensusDisplayAndRefresh(
+ heapWorker,
+ censusDisplays.invertedAllocationStack
+ )
+ );
+
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+ await waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+ ok(getState().snapshots[0].census.display.inverted, "Snapshot is inverted");
+
+ result = getSnapshotTotals(getState().snapshots[0].census);
+ equal(
+ totalBytes,
+ result.bytes,
+ "getSnapshotTotals reuslted in correct bytes when inverted"
+ );
+ equal(
+ totalCount,
+ result.count,
+ "getSnapshotTotals reuslted in correct count when inverted"
+ );
+});
+
+function aggregate(report) {
+ let totalBytes = report.bytes;
+ let totalCount = report.count;
+ for (const child of report.children || []) {
+ const { bytes, count } = aggregate(child);
+ totalBytes += bytes;
+ totalCount += count;
+ }
+ return { bytes: totalBytes, count: totalCount };
+}
diff --git a/devtools/client/memory/test/xpcshell/test_utils.js b/devtools/client/memory/test/xpcshell/test_utils.js
new file mode 100644
index 0000000000..47b4dfcc73
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_utils.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the task creator `takeSnapshotAndCensus()` for the whole flow of
+ * taking a snapshot, and its sub-actions. Tests the formatNumber and
+ * formatPercent methods.
+ */
+
+const utils = require("resource://devtools/client/memory/utils.js");
+const {
+ snapshotState: states,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+add_task(async function () {
+ const s1 = utils.createSnapshot({ view: { state: viewState.CENSUS } });
+ const s2 = utils.createSnapshot({ view: { state: viewState.CENSUS } });
+ equal(
+ s1.state,
+ states.SAVING,
+ "utils.createSnapshot() creates snapshot in saving state"
+ );
+ Assert.notStrictEqual(
+ s1.id,
+ s2.id,
+ "utils.createSnapshot() creates snapshot with unique ids"
+ );
+
+ const custom = { by: "internalType", then: { by: "count", bytes: true } };
+ Preferences.set(
+ "devtools.memory.custom-census-displays",
+ JSON.stringify({ "My Display": custom })
+ );
+
+ equal(
+ utils.getCustomCensusDisplays()["My Display"].by,
+ custom.by,
+ "utils.getCustomCensusDisplays() returns custom displays"
+ );
+
+ ok(true, "test formatNumber util functions");
+ equal(utils.formatNumber(12), "12", "formatNumber returns 12 for 12");
+
+ equal(utils.formatNumber(0), "0", "formatNumber returns 0 for 0");
+ equal(utils.formatNumber(-0), "0", "formatNumber returns 0 for -0");
+ equal(utils.formatNumber(+0), "0", "formatNumber returns 0 for +0");
+
+ equal(
+ utils.formatNumber(1234567),
+ "1 234 567",
+ "formatNumber adds a space every 3rd digit"
+ );
+ equal(
+ utils.formatNumber(12345678),
+ "12 345 678",
+ "formatNumber adds a space every 3rd digit"
+ );
+ equal(
+ utils.formatNumber(123456789),
+ "123 456 789",
+ "formatNumber adds a space every 3rd digit"
+ );
+
+ equal(
+ utils.formatNumber(12, true),
+ "+12",
+ "formatNumber can display number sign"
+ );
+ equal(
+ utils.formatNumber(-12, true),
+ "-12",
+ "formatNumber can display number sign (negative)"
+ );
+
+ ok(true, "test formatPercent util functions");
+ equal(utils.formatPercent(12), "12%", "formatPercent returns 12% for 12");
+ equal(
+ utils.formatPercent(12345),
+ "12 345%",
+ "formatPercent returns 12 345% for 12345"
+ );
+
+ equal(utils.formatAbbreviatedBytes(12), "12B", "Formats bytes");
+ equal(utils.formatAbbreviatedBytes(12345), "12KiB", "Formats kilobytes");
+ equal(utils.formatAbbreviatedBytes(12345678), "11MiB", "Formats megabytes");
+ equal(
+ utils.formatAbbreviatedBytes(12345678912),
+ "11GiB",
+ "Formats gigabytes"
+ );
+
+ equal(
+ utils.hslToStyle(0.5, 0.6, 0.7),
+ "hsl(180,60%,70%)",
+ "hslToStyle converts an array to a style string"
+ );
+ equal(
+ utils.hslToStyle(0, 0, 0),
+ "hsl(0,0%,0%)",
+ "hslToStyle converts an array to a style string"
+ );
+ equal(
+ utils.hslToStyle(1, 1, 1),
+ "hsl(360,100%,100%)",
+ "hslToStyle converts an array to a style string"
+ );
+
+ equal(utils.lerp(5, 7, 0), 5, "lerp return first number for 0");
+ equal(utils.lerp(5, 7, 1), 7, "lerp return second number for 1");
+ equal(utils.lerp(5, 7, 0.5), 6, "lerp interpolates the numbers for 0.5");
+});
diff --git a/devtools/client/memory/test/xpcshell/xpcshell.toml b/devtools/client/memory/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..3f248d9bb4
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/xpcshell.toml
@@ -0,0 +1,106 @@
+[DEFAULT]
+tags = "devtools devtools-memory"
+head = "../../../shared/test/shared-head.js head.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+
+["test_action-clear-snapshots_01.js"]
+
+["test_action-clear-snapshots_02.js"]
+
+["test_action-clear-snapshots_03.js"]
+
+["test_action-clear-snapshots_04.js"]
+
+["test_action-clear-snapshots_05.js"]
+
+["test_action-clear-snapshots_06.js"]
+
+["test_action-export-snapshot.js"]
+
+["test_action-filter-01.js"]
+
+["test_action-filter-02.js"]
+
+["test_action-filter-03.js"]
+
+["test_action-import-snapshot-and-census.js"]
+
+["test_action-import-snapshot-dominator-tree.js"]
+
+["test_action-select-snapshot.js"]
+
+["test_action-set-display-and-refresh-01.js"]
+
+["test_action-set-display-and-refresh-02.js"]
+
+["test_action-set-display.js"]
+
+["test_action-take-census.js"]
+skip-if = ["tsan"] # Unreasonably slow, bug 1612707
+
+["test_action-take-snapshot-and-census.js"]
+
+["test_action-take-snapshot.js"]
+
+["test_action-toggle-inverted-and-refresh-01.js"]
+
+["test_action-toggle-inverted-and-refresh-02.js"]
+
+["test_action-toggle-inverted.js"]
+
+["test_action-toggle-recording-allocations.js"]
+
+["test_action_diffing_01.js"]
+
+["test_action_diffing_02.js"]
+
+["test_action_diffing_03.js"]
+
+["test_action_diffing_04.js"]
+
+["test_action_diffing_05.js"]
+
+["test_dominator_trees_01.js"]
+
+["test_dominator_trees_02.js"]
+
+["test_dominator_trees_03.js"]
+
+["test_dominator_trees_04.js"]
+skip-if = ["tsan"] # Unreasonably slow, bug 1612707
+
+["test_dominator_trees_05.js"]
+
+["test_dominator_trees_06.js"]
+
+["test_dominator_trees_07.js"]
+
+["test_dominator_trees_08.js"]
+
+["test_dominator_trees_09.js"]
+
+["test_dominator_trees_10.js"]
+
+["test_individuals_01.js"]
+
+["test_individuals_02.js"]
+
+["test_individuals_03.js"]
+
+["test_individuals_04.js"]
+
+["test_individuals_05.js"]
+skip-if = ["tsan"] # Times out, bug 1612707
+
+["test_individuals_06.js"]
+
+["test_pop_view_01.js"]
+
+["test_tree-map-01.js"]
+
+["test_tree-map-02.js"]
+
+["test_utils-get-snapshot-totals.js"]
+
+["test_utils.js"]