summaryrefslogtreecommitdiffstats
path: root/devtools/client/memory
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/memory
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/memory')
-rw-r--r--devtools/client/memory/.eslintrc.js15
-rw-r--r--devtools/client/memory/actions/allocations.js23
-rw-r--r--devtools/client/memory/actions/census-display.js37
-rw-r--r--devtools/client/memory/actions/diffing.js218
-rw-r--r--devtools/client/memory/actions/filter.js37
-rw-r--r--devtools/client/memory/actions/front.js17
-rw-r--r--devtools/client/memory/actions/io.js104
-rw-r--r--devtools/client/memory/actions/label-display.js41
-rw-r--r--devtools/client/memory/actions/moz.build20
-rw-r--r--devtools/client/memory/actions/refresh.js47
-rw-r--r--devtools/client/memory/actions/sizes.js13
-rw-r--r--devtools/client/memory/actions/snapshot.js940
-rw-r--r--devtools/client/memory/actions/task-cache.js105
-rw-r--r--devtools/client/memory/actions/tree-map-display.js40
-rw-r--r--devtools/client/memory/actions/view.js67
-rw-r--r--devtools/client/memory/app.js430
-rw-r--r--devtools/client/memory/components/Census.js89
-rw-r--r--devtools/client/memory/components/CensusHeader.js77
-rw-r--r--devtools/client/memory/components/CensusTreeItem.js181
-rw-r--r--devtools/client/memory/components/DominatorTree.js249
-rw-r--r--devtools/client/memory/components/DominatorTreeHeader.js49
-rw-r--r--devtools/client/memory/components/DominatorTreeItem.js167
-rw-r--r--devtools/client/memory/components/Heap.js541
-rw-r--r--devtools/client/memory/components/Individuals.js72
-rw-r--r--devtools/client/memory/components/IndividualsHeader.js49
-rw-r--r--devtools/client/memory/components/List.js44
-rw-r--r--devtools/client/memory/components/ShortestPaths.js192
-rw-r--r--devtools/client/memory/components/SnapshotListItem.js138
-rw-r--r--devtools/client/memory/components/Toolbar.js307
-rw-r--r--devtools/client/memory/components/TreeMap.js75
-rw-r--r--devtools/client/memory/components/moz.build25
-rw-r--r--devtools/client/memory/components/tree-map/canvas-utils.js132
-rw-r--r--devtools/client/memory/components/tree-map/color-coarse-type.js70
-rw-r--r--devtools/client/memory/components/tree-map/drag-zoom.js337
-rw-r--r--devtools/client/memory/components/tree-map/draw.js317
-rw-r--r--devtools/client/memory/components/tree-map/moz.build12
-rw-r--r--devtools/client/memory/components/tree-map/start.js40
-rw-r--r--devtools/client/memory/constants.js360
-rw-r--r--devtools/client/memory/dominator-tree-lazy-children.js60
-rw-r--r--devtools/client/memory/index.xhtml34
-rw-r--r--devtools/client/memory/initializer.js80
-rw-r--r--devtools/client/memory/models.js543
-rw-r--r--devtools/client/memory/moz.build29
-rw-r--r--devtools/client/memory/panel.js77
-rw-r--r--devtools/client/memory/reducers.js17
-rw-r--r--devtools/client/memory/reducers/allocations.js54
-rw-r--r--devtools/client/memory/reducers/census-display.js21
-rw-r--r--devtools/client/memory/reducers/diffing.js170
-rw-r--r--devtools/client/memory/reducers/errors.js21
-rw-r--r--devtools/client/memory/reducers/filter.js14
-rw-r--r--devtools/client/memory/reducers/front.js11
-rw-r--r--devtools/client/memory/reducers/individuals.js81
-rw-r--r--devtools/client/memory/reducers/label-display.js19
-rw-r--r--devtools/client/memory/reducers/moz.build19
-rw-r--r--devtools/client/memory/reducers/sizes.js18
-rw-r--r--devtools/client/memory/reducers/snapshots.js511
-rw-r--r--devtools/client/memory/reducers/tree-map-display.js22
-rw-r--r--devtools/client/memory/reducers/view.js49
-rw-r--r--devtools/client/memory/store.js15
-rw-r--r--devtools/client/memory/test/browser/.eslintrc.js9
-rw-r--r--devtools/client/memory/test/browser/browser.ini36
-rw-r--r--devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js50
-rw-r--r--devtools/client/memory/test/browser/browser_memory_clear_snapshots.js76
-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.js47
-rw-r--r--devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js176
-rw-r--r--devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js79
-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.js87
-rw-r--r--devtools/client/memory/test/browser/browser_memory_individuals_01.js72
-rw-r--r--devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js110
-rw-r--r--devtools/client/memory/test/browser/browser_memory_keyboard.js109
-rw-r--r--devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js52
-rw-r--r--devtools/client/memory/test/browser/browser_memory_no_auto_expand.js47
-rw-r--r--devtools/client/memory/test/browser/browser_memory_percents_01.js60
-rw-r--r--devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js136
-rw-r--r--devtools/client/memory/test/browser/browser_memory_simple_01.js58
-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.js279
-rw-r--r--devtools/client/memory/test/chrome/.eslintrc.js9
-rw-r--r--devtools/client/memory/test/chrome/chrome.ini20
-rw-r--r--devtools/client/memory/test/chrome/head.js349
-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.js155
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-clear-snapshots_01.js39
-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.js50
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-clear-snapshots_06.js61
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-export-snapshot.js40
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-filter-01.js21
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-filter-02.js83
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-filter-03.js67
-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.js98
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-select-snapshot.js44
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-01.js180
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-02.js69
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-set-display.js72
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-take-census.js68
-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.js59
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-01.js96
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-02.js80
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-toggle-inverted.js29
-rw-r--r--devtools/client/memory/test/xpcshell/test_action-toggle-recording-allocations.js44
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_01.js27
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_02.js52
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_03.js143
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_04.js99
-rw-r--r--devtools/client/memory/test/xpcshell/test_action_diffing_05.js132
-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.js78
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_03.js75
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_04.js89
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_05.js64
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_06.js150
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_07.js172
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_08.js98
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_09.js90
-rw-r--r--devtools/client/memory/test/xpcshell/test_dominator_trees_10.js93
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_01.js75
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_02.js89
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_03.js114
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_04.js89
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_05.js82
-rw-r--r--devtools/client/memory/test/xpcshell/test_individuals_06.js83
-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.js74
-rw-r--r--devtools/client/memory/test/xpcshell/test_tree-map-02.js103
-rw-r--r--devtools/client/memory/test/xpcshell/test_utils-get-snapshot-totals.js99
-rw-r--r--devtools/client/memory/test/xpcshell/test_utils.js114
-rw-r--r--devtools/client/memory/test/xpcshell/xpcshell.ini58
-rw-r--r--devtools/client/memory/utils.js548
156 files changed, 15932 insertions, 0 deletions
diff --git a/devtools/client/memory/.eslintrc.js b/devtools/client/memory/.eslintrc.js
new file mode 100644
index 0000000000..5500a7eb10
--- /dev/null
+++ b/devtools/client/memory/.eslintrc.js
@@ -0,0 +1,15 @@
+/* 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/. */
+
+"use strict";
+
+module.exports = {
+ env: {
+ browser: true,
+ },
+ globals: {
+ d3: true,
+ dagreD3: true,
+ },
+};
diff --git a/devtools/client/memory/actions/allocations.js b/devtools/client/memory/actions/allocations.js
new file mode 100644
index 0000000000..1abf51d22d
--- /dev/null
+++ b/devtools/client/memory/actions/allocations.js
@@ -0,0 +1,23 @@
+/* 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/. */
+"use strict";
+
+const {
+ actions,
+ ALLOCATION_RECORDING_OPTIONS,
+} = require("devtools/client/memory/constants");
+
+exports.toggleRecordingAllocationStacks = function(front) {
+ return async function({ dispatch, getState }) {
+ dispatch({ type: actions.TOGGLE_RECORD_ALLOCATION_STACKS_START });
+
+ if (getState().recordingAllocationStacks) {
+ await front.stopRecordingAllocations();
+ } else {
+ await front.startRecordingAllocations(ALLOCATION_RECORDING_OPTIONS);
+ }
+
+ dispatch({ type: actions.TOGGLE_RECORD_ALLOCATION_STACKS_END });
+ };
+};
diff --git a/devtools/client/memory/actions/census-display.js b/devtools/client/memory/actions/census-display.js
new file mode 100644
index 0000000000..11e1705418
--- /dev/null
+++ b/devtools/client/memory/actions/census-display.js
@@ -0,0 +1,37 @@
+/* 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/. */
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("devtools/client/memory/constants");
+const { refresh } = require("devtools/client/memory/actions/refresh");
+
+exports.setCensusDisplayAndRefresh = function(heapWorker, display) {
+ return async function({ dispatch, getState }) {
+ dispatch(setCensusDisplay(display));
+ await dispatch(refresh(heapWorker));
+ };
+};
+
+/**
+ * Clears out all cached census data in the snapshots and sets new display data
+ * for censuses.
+ *
+ * @param {censusDisplayModel} display
+ */
+const setCensusDisplay = (exports.setCensusDisplay = function(display) {
+ assert(
+ typeof display === "object" &&
+ display &&
+ display.breakdown &&
+ display.breakdown.by,
+ "Breakdowns must be an object with a `by` property, attempted to set: " +
+ JSON.stringify(display)
+ );
+
+ return {
+ type: actions.SET_CENSUS_DISPLAY,
+ display,
+ };
+});
diff --git a/devtools/client/memory/actions/diffing.js b/devtools/client/memory/actions/diffing.js
new file mode 100644
index 0000000000..3268c00a0e
--- /dev/null
+++ b/devtools/client/memory/actions/diffing.js
@@ -0,0 +1,218 @@
+/* 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/. */
+
+"use strict";
+
+const { assert, reportException } = require("devtools/shared/DevToolsUtils");
+const {
+ actions,
+ diffingState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ getSnapshot,
+ censusIsUpToDate,
+ snapshotIsDiffable,
+ findSelectedSnapshot,
+} = require("devtools/client/memory/utils");
+
+/**
+ * Toggle diffing mode on or off.
+ */
+exports.toggleDiffing = function() {
+ return function({ dispatch, getState }) {
+ dispatch({
+ type: actions.CHANGE_VIEW,
+ newViewState: getState().diffing ? viewState.CENSUS : viewState.DIFFING,
+ oldDiffing: getState().diffing,
+ oldSelected: findSelectedSnapshot(getState()),
+ });
+ };
+};
+
+/**
+ * Select the given snapshot for diffing.
+ *
+ * @param {snapshotModel} snapshot
+ */
+const selectSnapshotForDiffing = (exports.selectSnapshotForDiffing = function(
+ snapshot
+) {
+ assert(
+ snapshotIsDiffable(snapshot),
+ "To select a snapshot for diffing, it must be diffable"
+ );
+ return { type: actions.SELECT_SNAPSHOT_FOR_DIFFING, snapshot };
+});
+
+/**
+ * Compute the difference between the first and second snapshots.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotModel} first
+ * @param {snapshotModel} second
+ */
+const takeCensusDiff = (exports.takeCensusDiff = function(
+ heapWorker,
+ first,
+ second
+) {
+ return async function({ dispatch, getState }) {
+ assert(
+ snapshotIsDiffable(first),
+ `First snapshot must be in a diffable state, found ${first.state}`
+ );
+ assert(
+ snapshotIsDiffable(second),
+ `Second snapshot must be in a diffable state, found ${second.state}`
+ );
+
+ let report, parentMap;
+ let display = getState().censusDisplay;
+ let filter = getState().filter;
+
+ if (censusIsUpToDate(filter, display, getState().diffing.census)) {
+ return;
+ }
+
+ do {
+ if (
+ !getState().diffing ||
+ getState().diffing.firstSnapshotId !== first.id ||
+ getState().diffing.secondSnapshotId !== second.id
+ ) {
+ // If we stopped diffing or stopped and then started diffing a different
+ // pair of snapshots, then just give up with diffing this pair. In the
+ // latter case, a newly spawned task will handle the diffing for the new
+ // pair.
+ return;
+ }
+
+ display = getState().censusDisplay;
+ filter = getState().filter;
+
+ dispatch({
+ type: actions.TAKE_CENSUS_DIFF_START,
+ first,
+ second,
+ filter,
+ display,
+ });
+
+ const opts = display.inverted
+ ? { asInvertedTreeNode: true }
+ : { asTreeNode: true };
+ opts.filter = filter || null;
+
+ try {
+ ({ delta: report, parentMap } = await heapWorker.takeCensusDiff(
+ first.path,
+ second.path,
+ { breakdown: display.breakdown },
+ opts
+ ));
+ } catch (error) {
+ reportException("actions/diffing/takeCensusDiff", error);
+ dispatch({ type: actions.DIFFING_ERROR, error });
+ return;
+ }
+ } while (
+ filter !== getState().filter ||
+ display !== getState().censusDisplay
+ );
+
+ dispatch({
+ type: actions.TAKE_CENSUS_DIFF_END,
+ first,
+ second,
+ report,
+ parentMap,
+ filter,
+ display,
+ });
+ };
+});
+
+/**
+ * Ensure that the current diffing data is up to date with the currently
+ * selected display, filter, etc. If the state is not up-to-date, then a
+ * recompute is triggered.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshDiffing = (exports.refreshDiffing = function(heapWorker) {
+ return function({ dispatch, getState }) {
+ if (getState().diffing.secondSnapshotId === null) {
+ return;
+ }
+
+ assert(getState().diffing.firstSnapshotId, "Should have first snapshot id");
+
+ if (getState().diffing.state === diffingState.TAKING_DIFF) {
+ // There is an existing task that will ensure that the diffing data is
+ // up-to-date.
+ return;
+ }
+
+ const { firstSnapshotId, secondSnapshotId } = getState().diffing;
+
+ const first = getSnapshot(getState(), firstSnapshotId);
+ const second = getSnapshot(getState(), secondSnapshotId);
+ dispatch(takeCensusDiff(heapWorker, first, second));
+ };
+});
+
+/**
+ * Select the given snapshot for diffing and refresh the diffing data if
+ * necessary (for example, if two snapshots are now selected for diffing).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotModel} snapshot
+ */
+exports.selectSnapshotForDiffingAndRefresh = function(heapWorker, snapshot) {
+ return async function({ dispatch, getState }) {
+ assert(
+ getState().diffing,
+ "If we are selecting for diffing, we must be in diffing mode"
+ );
+ dispatch(selectSnapshotForDiffing(snapshot));
+ await dispatch(refreshDiffing(heapWorker));
+ };
+};
+
+/**
+ * Expand the given node in the diffing's census's delta-report.
+ *
+ * @param {CensusTreeNode} node
+ */
+exports.expandDiffingCensusNode = function(node) {
+ return {
+ type: actions.EXPAND_DIFFING_CENSUS_NODE,
+ node,
+ };
+};
+
+/**
+ * Collapse the given node in the diffing's census's delta-report.
+ *
+ * @param {CensusTreeNode} node
+ */
+exports.collapseDiffingCensusNode = function(node) {
+ return {
+ type: actions.COLLAPSE_DIFFING_CENSUS_NODE,
+ node,
+ };
+};
+
+/**
+ * Focus the given node in the snapshot's census's report.
+ *
+ * @param {DominatorTreeNode} node
+ */
+exports.focusDiffingCensusNode = function(node) {
+ return {
+ type: actions.FOCUS_DIFFING_CENSUS_NODE,
+ node,
+ };
+};
diff --git a/devtools/client/memory/actions/filter.js b/devtools/client/memory/actions/filter.js
new file mode 100644
index 0000000000..9ed7bcf5ac
--- /dev/null
+++ b/devtools/client/memory/actions/filter.js
@@ -0,0 +1,37 @@
+/* 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/. */
+"use strict";
+
+const { actions } = require("devtools/client/memory/constants");
+const { refresh } = require("devtools/client/memory/actions/refresh");
+
+const setFilterString = (exports.setFilterString = function(filterString) {
+ return {
+ type: actions.SET_FILTER_STRING,
+ filter: filterString,
+ };
+});
+
+// The number of milliseconds we should wait before kicking off a new census
+// when the filter string is updated. This helps us avoid doing any work while
+// the user is still typing.
+const FILTER_INPUT_DEBOUNCE_MS = 250;
+
+// The timer id for the debounced census refresh.
+let timerId = null;
+
+exports.setFilterStringAndRefresh = function(filterString, heapWorker) {
+ return function*({ dispatch, getState }) {
+ dispatch(setFilterString(filterString));
+
+ if (timerId !== null) {
+ clearTimeout(timerId);
+ }
+
+ timerId = setTimeout(
+ () => dispatch(refresh(heapWorker)),
+ FILTER_INPUT_DEBOUNCE_MS
+ );
+ };
+};
diff --git a/devtools/client/memory/actions/front.js b/devtools/client/memory/actions/front.js
new file mode 100644
index 0000000000..35cd25d2b1
--- /dev/null
+++ b/devtools/client/memory/actions/front.js
@@ -0,0 +1,17 @@
+/* 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/. */
+
+"use strict";
+
+const { actions } = require("devtools/client/memory/constants");
+
+/**
+ * Update the memory front.
+ */
+exports.updateMemoryFront = front => {
+ return {
+ type: actions.UPDATE_MEMORY_FRONT,
+ front,
+ };
+};
diff --git a/devtools/client/memory/actions/io.js b/devtools/client/memory/actions/io.js
new file mode 100644
index 0000000000..987514b6e3
--- /dev/null
+++ b/devtools/client/memory/actions/io.js
@@ -0,0 +1,104 @@
+/* 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/. */
+"use strict";
+
+const {
+ immutableUpdate,
+ reportException,
+ assert,
+} = require("devtools/shared/DevToolsUtils");
+const {
+ snapshotState: states,
+ actions,
+} = require("devtools/client/memory/constants");
+const {
+ L10N,
+ openFilePicker,
+ createSnapshot,
+} = require("devtools/client/memory/utils");
+const { OS } = require("resource://gre/modules/osfile.jsm");
+const {
+ selectSnapshot,
+ computeSnapshotData,
+ readSnapshot,
+} = require("devtools/client/memory/actions/snapshot");
+const VALID_EXPORT_STATES = [states.SAVED, states.READ];
+
+exports.pickFileAndExportSnapshot = function(snapshot) {
+ return async function({ dispatch, getState }) {
+ const outputFile = await openFilePicker({
+ title: L10N.getFormatStr("snapshot.io.save.window"),
+ defaultName: OS.Path.basename(snapshot.path),
+ filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]],
+ mode: "save",
+ });
+
+ if (!outputFile) {
+ return;
+ }
+
+ await dispatch(exportSnapshot(snapshot, outputFile.path));
+ };
+};
+
+const exportSnapshot = (exports.exportSnapshot = function(snapshot, dest) {
+ return async function({ dispatch, getState }) {
+ dispatch({ type: actions.EXPORT_SNAPSHOT_START, snapshot });
+
+ assert(
+ VALID_EXPORT_STATES.includes(snapshot.state),
+ `Snapshot is in invalid state for exporting: ${snapshot.state}`
+ );
+
+ try {
+ await OS.File.copy(snapshot.path, dest);
+ } catch (error) {
+ reportException("exportSnapshot", error);
+ dispatch({ type: actions.EXPORT_SNAPSHOT_ERROR, snapshot, error });
+ }
+
+ dispatch({ type: actions.EXPORT_SNAPSHOT_END, snapshot });
+ };
+});
+
+exports.pickFileAndImportSnapshotAndCensus = function(heapWorker) {
+ return async function({ dispatch, getState }) {
+ const input = await openFilePicker({
+ title: L10N.getFormatStr("snapshot.io.import.window"),
+ filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]],
+ mode: "open",
+ });
+
+ if (!input) {
+ return;
+ }
+
+ await dispatch(importSnapshotAndCensus(heapWorker, input.path));
+ };
+};
+
+const importSnapshotAndCensus = function(heapWorker, path) {
+ return async function({ dispatch, getState }) {
+ const snapshot = immutableUpdate(createSnapshot(getState()), {
+ path,
+ state: states.IMPORTING,
+ imported: true,
+ });
+ const id = snapshot.id;
+
+ dispatch({ type: actions.IMPORT_SNAPSHOT_START, snapshot });
+ dispatch(selectSnapshot(snapshot.id));
+
+ try {
+ await dispatch(readSnapshot(heapWorker, id));
+ await dispatch(computeSnapshotData(heapWorker, id));
+ } catch (error) {
+ reportException("importSnapshot", error);
+ dispatch({ type: actions.IMPORT_SNAPSHOT_ERROR, error, id });
+ }
+
+ dispatch({ type: actions.IMPORT_SNAPSHOT_END, id });
+ };
+};
+exports.importSnapshotAndCensus = importSnapshotAndCensus;
diff --git a/devtools/client/memory/actions/label-display.js b/devtools/client/memory/actions/label-display.js
new file mode 100644
index 0000000000..b100cbc1b5
--- /dev/null
+++ b/devtools/client/memory/actions/label-display.js
@@ -0,0 +1,41 @@
+/* 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/. */
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("devtools/client/memory/constants");
+const { refresh } = require("devtools/client/memory/actions/refresh");
+
+/**
+ * Change the display we use for labeling individual nodes and refresh the
+ * current data.
+ */
+exports.setLabelDisplayAndRefresh = function(heapWorker, display) {
+ return async function({ dispatch, getState }) {
+ // Clears out all stored census data and sets the display.
+ dispatch(setLabelDisplay(display));
+ await dispatch(refresh(heapWorker));
+ };
+};
+
+/**
+ * Change the display we use for labeling individual nodes.
+ *
+ * @param {labelDisplayModel} display
+ */
+const setLabelDisplay = (exports.setLabelDisplay = function(display) {
+ assert(
+ typeof display === "object" &&
+ display &&
+ display.breakdown &&
+ display.breakdown.by,
+ "Breakdowns must be an object with a `by` property, attempted to set: " +
+ JSON.stringify(display)
+ );
+
+ return {
+ type: actions.SET_LABEL_DISPLAY,
+ display,
+ };
+});
diff --git a/devtools/client/memory/actions/moz.build b/devtools/client/memory/actions/moz.build
new file mode 100644
index 0000000000..939712ca32
--- /dev/null
+++ b/devtools/client/memory/actions/moz.build
@@ -0,0 +1,20 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "allocations.js",
+ "census-display.js",
+ "diffing.js",
+ "filter.js",
+ "front.js",
+ "io.js",
+ "label-display.js",
+ "refresh.js",
+ "sizes.js",
+ "snapshot.js",
+ "task-cache.js",
+ "tree-map-display.js",
+ "view.js",
+)
diff --git a/devtools/client/memory/actions/refresh.js b/devtools/client/memory/actions/refresh.js
new file mode 100644
index 0000000000..7377a1108d
--- /dev/null
+++ b/devtools/client/memory/actions/refresh.js
@@ -0,0 +1,47 @@
+/* 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/. */
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { viewState } = require("devtools/client/memory/constants");
+const { refreshDiffing } = require("devtools/client/memory/actions/diffing");
+const snapshot = require("devtools/client/memory/actions/snapshot");
+
+/**
+ * Refresh the main thread's data from the heap analyses worker, if needed.
+ *
+ * @param {HeapAnalysesWorker} heapWorker
+ */
+exports.refresh = function(heapWorker) {
+ return async function({ dispatch, getState }) {
+ switch (getState().view.state) {
+ case viewState.DIFFING:
+ assert(
+ getState().diffing,
+ "Should have diffing state if in diffing view"
+ );
+ await dispatch(refreshDiffing(heapWorker));
+ return;
+
+ case viewState.CENSUS:
+ await dispatch(snapshot.refreshSelectedCensus(heapWorker));
+ return;
+
+ case viewState.DOMINATOR_TREE:
+ await dispatch(snapshot.refreshSelectedDominatorTree(heapWorker));
+ return;
+
+ case viewState.TREE_MAP:
+ await dispatch(snapshot.refreshSelectedTreeMap(heapWorker));
+ return;
+
+ case viewState.INDIVIDUALS:
+ await dispatch(snapshot.refreshIndividuals(heapWorker));
+ return;
+
+ default:
+ assert(false, `Unexpected view state: ${getState().view.state}`);
+ }
+ };
+};
diff --git a/devtools/client/memory/actions/sizes.js b/devtools/client/memory/actions/sizes.js
new file mode 100644
index 0000000000..a0f6116c86
--- /dev/null
+++ b/devtools/client/memory/actions/sizes.js
@@ -0,0 +1,13 @@
+/* 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/. */
+"use strict";
+
+const { actions } = require("devtools/client/memory/constants");
+
+exports.resizeShortestPaths = function(newSize) {
+ return {
+ type: actions.RESIZE_SHORTEST_PATHS,
+ size: newSize,
+ };
+};
diff --git a/devtools/client/memory/actions/snapshot.js b/devtools/client/memory/actions/snapshot.js
new file mode 100644
index 0000000000..069a3633d5
--- /dev/null
+++ b/devtools/client/memory/actions/snapshot.js
@@ -0,0 +1,940 @@
+/* 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/. */
+"use strict";
+
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+const {
+ assert,
+ reportException,
+ isSet,
+} = require("devtools/shared/DevToolsUtils");
+const {
+ censusIsUpToDate,
+ getSnapshot,
+ createSnapshot,
+ dominatorTreeIsComputed,
+} = require("devtools/client/memory/utils");
+const {
+ actions,
+ snapshotState: states,
+ viewState,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+const view = require("devtools/client/memory/actions/view");
+const refresh = require("devtools/client/memory/actions/refresh");
+const diffing = require("devtools/client/memory/actions/diffing");
+const TaskCache = require("devtools/client/memory/actions/task-cache");
+
+/**
+ * A series of actions are fired from this task to save, read and generate the
+ * initial census from a snapshot.
+ *
+ * @param {MemoryFront}
+ * @param {HeapAnalysesClient}
+ * @param {Object}
+ */
+exports.takeSnapshotAndCensus = function(front, heapWorker) {
+ return async function({ dispatch, getState }) {
+ const id = await dispatch(takeSnapshot(front));
+ if (id === null) {
+ return;
+ }
+
+ await dispatch(readSnapshot(heapWorker, id));
+ if (getSnapshot(getState(), id).state !== states.READ) {
+ return;
+ }
+
+ await dispatch(computeSnapshotData(heapWorker, id));
+ };
+};
+
+/**
+ * Create the census for the snapshot with the provided snapshot id. If the
+ * current view is the DOMINATOR_TREE view, create the dominator tree for this
+ * snapshot as well.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotId} id
+ */
+const computeSnapshotData = (exports.computeSnapshotData = function(
+ heapWorker,
+ id
+) {
+ return async function({ dispatch, getState }) {
+ if (getSnapshot(getState(), id).state !== states.READ) {
+ return;
+ }
+
+ // Decide which type of census to take.
+ const censusTaker = getCurrentCensusTaker(getState().view.state);
+ await dispatch(censusTaker(heapWorker, id));
+
+ if (
+ getState().view.state === viewState.DOMINATOR_TREE &&
+ !getSnapshot(getState(), id).dominatorTree
+ ) {
+ await dispatch(computeAndFetchDominatorTree(heapWorker, id));
+ }
+ };
+});
+
+/**
+ * Selects a snapshot and if the snapshot's census is using a different
+ * display, take a new census.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotId} id
+ */
+exports.selectSnapshotAndRefresh = function(heapWorker, id) {
+ return async function({ dispatch, getState }) {
+ if (getState().diffing || getState().individuals) {
+ dispatch(view.changeView(viewState.CENSUS));
+ }
+
+ dispatch(selectSnapshot(id));
+ await dispatch(refresh.refresh(heapWorker));
+ };
+};
+
+/**
+ * Take a snapshot and return its id on success, or null on failure.
+ *
+ * @param {MemoryFront} front
+ * @returns {Number|null}
+ */
+const takeSnapshot = (exports.takeSnapshot = function(front) {
+ return async function({ dispatch, getState }) {
+ if (getState().diffing || getState().individuals) {
+ dispatch(view.changeView(viewState.CENSUS));
+ }
+
+ const snapshot = createSnapshot(getState());
+ const id = snapshot.id;
+ dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot });
+ dispatch(selectSnapshot(id));
+
+ let path;
+ try {
+ path = await front.saveHeapSnapshot();
+ } catch (error) {
+ reportException("takeSnapshot", error);
+ dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
+ return null;
+ }
+
+ dispatch({ type: actions.TAKE_SNAPSHOT_END, id, path });
+ return snapshot.id;
+ };
+});
+
+/**
+ * Reads a snapshot into memory; necessary to do before taking
+ * a census on the snapshot. May only be called once per snapshot.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotId} id
+ */
+const readSnapshot = (exports.readSnapshot = TaskCache.declareCacheableTask({
+ getCacheKey(_, id) {
+ return id;
+ },
+
+ task: async function(heapWorker, id, removeFromCache, dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(
+ [states.SAVED, states.IMPORTING].includes(snapshot.state),
+ `Should only read a snapshot once. Found snapshot in state ${snapshot.state}`
+ );
+
+ let creationTime;
+
+ dispatch({ type: actions.READ_SNAPSHOT_START, id });
+ try {
+ await heapWorker.readHeapSnapshot(snapshot.path);
+ creationTime = await heapWorker.getCreationTime(snapshot.path);
+ } catch (error) {
+ removeFromCache();
+ reportException("readSnapshot", error);
+ dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
+ return;
+ }
+
+ removeFromCache();
+ dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime });
+ },
+}));
+
+let takeCensusTaskCounter = 0;
+
+/**
+ * Census and tree maps both require snapshots. This function shares the logic
+ * of creating snapshots, but is configurable with specific actions for the
+ * individual census types.
+ *
+ * @param {getDisplay} Get the display object from the state.
+ * @param {getCensus} Get the census from the snapshot.
+ * @param {beginAction} Action to send at the beginning of a heap snapshot.
+ * @param {endAction} Action to send at the end of a heap snapshot.
+ * @param {errorAction} Action to send if a snapshot has an error.
+ */
+function makeTakeCensusTask({
+ getDisplay,
+ getFilter,
+ getCensus,
+ beginAction,
+ endAction,
+ errorAction,
+ canTakeCensus,
+}) {
+ /**
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotId} id
+ *
+ * @see {Snapshot} model defined in devtools/client/memory/models.js
+ * @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
+ * @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
+ */
+ const thisTakeCensusTaskId = ++takeCensusTaskCounter;
+ return TaskCache.declareCacheableTask({
+ getCacheKey(_, id) {
+ return `take-census-task-${thisTakeCensusTaskId}-${id}`;
+ },
+
+ task: async function(heapWorker, id, removeFromCache, dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ if (!snapshot) {
+ removeFromCache();
+ return;
+ }
+
+ // Assert that snapshot is in a valid state
+ assert(
+ canTakeCensus(snapshot),
+ "Attempting to take a census when the snapshot is not in a ready state. " +
+ `snapshot.state = ${snapshot.state}, ` +
+ `census.state = ${(getCensus(snapshot) || { state: null }).state}`
+ );
+
+ let report, parentMap;
+ let display = getDisplay(getState());
+ let filter = getFilter(getState());
+
+ // If display, filter and inversion haven't changed, don't do anything.
+ if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
+ removeFromCache();
+ return;
+ }
+
+ // Keep taking a census if the display changes while our request is in
+ // flight. Recheck that the display used for the census is the same as the
+ // state's display.
+ do {
+ display = getDisplay(getState());
+ filter = getState().filter;
+
+ dispatch({
+ type: beginAction,
+ id,
+ filter,
+ display,
+ });
+
+ const opts = display.inverted
+ ? { asInvertedTreeNode: true }
+ : { asTreeNode: true };
+
+ opts.filter = filter || null;
+
+ try {
+ ({ report, parentMap } = await heapWorker.takeCensus(
+ snapshot.path,
+ { breakdown: display.breakdown },
+ opts
+ ));
+ } catch (error) {
+ removeFromCache();
+ reportException("takeCensus", error);
+ dispatch({ type: errorAction, id, error });
+ return;
+ }
+ } while (
+ filter !== getState().filter ||
+ display !== getDisplay(getState())
+ );
+
+ removeFromCache();
+ dispatch({
+ type: endAction,
+ id,
+ display,
+ filter,
+ report,
+ parentMap,
+ });
+ },
+ });
+}
+
+/**
+ * Take a census.
+ */
+const takeCensus = (exports.takeCensus = makeTakeCensusTask({
+ getDisplay: state => state.censusDisplay,
+ getFilter: state => state.filter,
+ getCensus: snapshot => snapshot.census,
+ beginAction: actions.TAKE_CENSUS_START,
+ endAction: actions.TAKE_CENSUS_END,
+ errorAction: actions.TAKE_CENSUS_ERROR,
+ canTakeCensus: snapshot =>
+ snapshot.state === states.READ &&
+ (!snapshot.census || snapshot.census.state === censusState.SAVED),
+}));
+
+/**
+ * Take a census for the treemap.
+ */
+const takeTreeMap = (exports.takeTreeMap = makeTakeCensusTask({
+ getDisplay: state => state.treeMapDisplay,
+ getFilter: () => null,
+ getCensus: snapshot => snapshot.treeMap,
+ beginAction: actions.TAKE_TREE_MAP_START,
+ endAction: actions.TAKE_TREE_MAP_END,
+ errorAction: actions.TAKE_TREE_MAP_ERROR,
+ canTakeCensus: snapshot =>
+ snapshot.state === states.READ &&
+ (!snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED),
+}));
+
+/**
+ * Define what should be the default mode for taking a census based on the
+ * default view of the tool.
+ */
+const defaultCensusTaker = takeTreeMap;
+
+/**
+ * Pick the default census taker when taking a snapshot. This should be
+ * determined by the current view. If the view doesn't include a census, then
+ * use the default one defined above. Some census information is always needed
+ * to display some basic information about a snapshot.
+ *
+ * @param {string} value from viewState
+ */
+const getCurrentCensusTaker = (exports.getCurrentCensusTaker = function(
+ currentView
+) {
+ switch (currentView) {
+ case viewState.TREE_MAP:
+ return takeTreeMap;
+ case viewState.CENSUS:
+ return takeCensus;
+ default:
+ return defaultCensusTaker;
+ }
+});
+
+/**
+ * Focus the given node in the individuals view.
+ *
+ * @param {DominatorTreeNode} node.
+ */
+exports.focusIndividual = function(node) {
+ return {
+ type: actions.FOCUS_INDIVIDUAL,
+ node,
+ };
+};
+
+/**
+ * Fetch the individual `DominatorTreeNodes` for the census group specified by
+ * `censusBreakdown` and `reportLeafIndex`.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ * @param {Object} censusBreakdown
+ * @param {Set<Number> | Number} reportLeafIndex
+ */
+const fetchIndividuals = (exports.fetchIndividuals = function(
+ heapWorker,
+ id,
+ censusBreakdown,
+ reportLeafIndex
+) {
+ return async function({ dispatch, getState }) {
+ if (getState().view.state !== viewState.INDIVIDUALS) {
+ dispatch(view.changeView(viewState.INDIVIDUALS));
+ }
+
+ const snapshot = getSnapshot(getState(), id);
+ assert(
+ snapshot && snapshot.state === states.READ,
+ "The snapshot should already be read into memory"
+ );
+
+ if (!dominatorTreeIsComputed(snapshot)) {
+ await dispatch(computeAndFetchDominatorTree(heapWorker, id));
+ }
+
+ const snapshot_ = getSnapshot(getState(), id);
+ assert(
+ snapshot_.dominatorTree?.root,
+ "Should have a dominator tree with a root."
+ );
+
+ const dominatorTreeId = snapshot_.dominatorTree.dominatorTreeId;
+
+ const indices = isSet(reportLeafIndex)
+ ? reportLeafIndex
+ : new Set([reportLeafIndex]);
+
+ let labelDisplay;
+ let nodes;
+ do {
+ labelDisplay = getState().labelDisplay;
+ assert(
+ labelDisplay?.breakdown?.by,
+ `Should have a breakdown to label nodes with, got: ${JSON.stringify(
+ labelDisplay
+ )}`
+ );
+
+ if (getState().view.state !== viewState.INDIVIDUALS) {
+ // We switched views while in the process of fetching individuals -- any
+ // further work is useless.
+ return;
+ }
+
+ dispatch({ type: actions.FETCH_INDIVIDUALS_START });
+
+ try {
+ ({ nodes } = await heapWorker.getCensusIndividuals({
+ dominatorTreeId,
+ indices,
+ censusBreakdown,
+ labelBreakdown: labelDisplay.breakdown,
+ maxRetainingPaths: Preferences.get(
+ "devtools.memory.max-retaining-paths"
+ ),
+ maxIndividuals: Preferences.get("devtools.memory.max-individuals"),
+ }));
+ } catch (error) {
+ reportException("actions/snapshot/fetchIndividuals", error);
+ dispatch({ type: actions.INDIVIDUALS_ERROR, error });
+ return;
+ }
+ } while (labelDisplay !== getState().labelDisplay);
+
+ dispatch({
+ type: actions.FETCH_INDIVIDUALS_END,
+ id,
+ censusBreakdown,
+ indices,
+ labelDisplay,
+ nodes,
+ dominatorTree: snapshot_.dominatorTree,
+ });
+ };
+});
+
+/**
+ * Refresh the current individuals view.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+exports.refreshIndividuals = function(heapWorker) {
+ return async function({ dispatch, getState }) {
+ assert(
+ getState().view.state === viewState.INDIVIDUALS,
+ "Should be in INDIVIDUALS view."
+ );
+
+ const { individuals } = getState();
+
+ switch (individuals.state) {
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ case individualsState.FETCHING:
+ // Nothing to do here.
+ return;
+
+ case individualsState.FETCHED:
+ if (getState().individuals.labelDisplay === getState().labelDisplay) {
+ return;
+ }
+ break;
+
+ case individualsState.ERROR:
+ // Doesn't hurt to retry: maybe we won't get an error this time around?
+ break;
+
+ default:
+ assert(false, `Unexpected individuals state: ${individuals.state}`);
+ return;
+ }
+
+ await dispatch(
+ fetchIndividuals(
+ heapWorker,
+ individuals.id,
+ individuals.censusBreakdown,
+ individuals.indices
+ )
+ );
+ };
+};
+
+/**
+ * Refresh the selected snapshot's census data, if need be (for example,
+ * display configuration changed).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+exports.refreshSelectedCensus = function(heapWorker) {
+ return async function({ dispatch, getState }) {
+ const snapshot = getState().snapshots.find(s => s.selected);
+ if (!snapshot || snapshot.state !== states.READ) {
+ return;
+ }
+
+ // Intermediate snapshot states will get handled by the task action that is
+ // orchestrating them. For example, if the snapshot census's state is
+ // SAVING, then the takeCensus action will keep taking a census until
+ // the inverted property matches the inverted state. If the snapshot is
+ // still in the process of being saved or read, the takeSnapshotAndCensus
+ // task action will follow through and ensure that a census is taken.
+ if (
+ (snapshot.census && snapshot.census.state === censusState.SAVED) ||
+ !snapshot.census
+ ) {
+ await dispatch(takeCensus(heapWorker, snapshot.id));
+ }
+ };
+};
+
+/**
+ * Refresh the selected snapshot's tree map data, if need be (for example,
+ * display configuration changed).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+exports.refreshSelectedTreeMap = function(heapWorker) {
+ return async function({ dispatch, getState }) {
+ const snapshot = getState().snapshots.find(s => s.selected);
+ if (!snapshot || snapshot.state !== states.READ) {
+ return;
+ }
+
+ // Intermediate snapshot states will get handled by the task action that is
+ // orchestrating them. For example, if the snapshot census's state is
+ // SAVING, then the takeCensus action will keep taking a census until
+ // the inverted property matches the inverted state. If the snapshot is
+ // still in the process of being saved or read, the takeSnapshotAndCensus
+ // task action will follow through and ensure that a census is taken.
+ if (
+ (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
+ !snapshot.treeMap
+ ) {
+ await dispatch(takeTreeMap(heapWorker, snapshot.id));
+ }
+ };
+};
+
+/**
+ * Request that the `HeapAnalysesWorker` compute the dominator tree for the
+ * snapshot with the given `id`.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ *
+ * @returns {Promise<DominatorTreeId>}
+ */
+const computeDominatorTree = (exports.computeDominatorTree = TaskCache.declareCacheableTask(
+ {
+ getCacheKey(_, id) {
+ return id;
+ },
+
+ task: async function(heapWorker, id, removeFromCache, dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(
+ !snapshot.dominatorTree?.dominatorTreeId,
+ "Should not re-compute dominator trees"
+ );
+
+ dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_START, id });
+
+ let dominatorTreeId;
+ try {
+ dominatorTreeId = await heapWorker.computeDominatorTree(snapshot.path);
+ } catch (error) {
+ removeFromCache();
+ reportException("actions/snapshot/computeDominatorTree", error);
+ dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
+ return null;
+ }
+
+ removeFromCache();
+ dispatch({
+ type: actions.COMPUTE_DOMINATOR_TREE_END,
+ id,
+ dominatorTreeId,
+ });
+ return dominatorTreeId;
+ },
+ }
+));
+
+/**
+ * Get the partial subtree, starting from the root, of the
+ * snapshot-with-the-given-id's dominator tree.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ *
+ * @returns {Promise<DominatorTreeNode>}
+ */
+const fetchDominatorTree = (exports.fetchDominatorTree = TaskCache.declareCacheableTask(
+ {
+ getCacheKey(_, id) {
+ return id;
+ },
+
+ task: async function(heapWorker, id, removeFromCache, dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(
+ dominatorTreeIsComputed(snapshot),
+ "Should have dominator tree model and it should be computed"
+ );
+
+ let display;
+ let root;
+ do {
+ display = getState().labelDisplay;
+ assert(
+ display?.breakdown,
+ `Should have a breakdown to describe nodes with, got: ${JSON.stringify(
+ display
+ )}`
+ );
+
+ dispatch({ type: actions.FETCH_DOMINATOR_TREE_START, id, display });
+
+ try {
+ root = await heapWorker.getDominatorTree({
+ dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
+ breakdown: display.breakdown,
+ maxRetainingPaths: Preferences.get(
+ "devtools.memory.max-retaining-paths"
+ ),
+ });
+ } catch (error) {
+ removeFromCache();
+ reportException("actions/snapshot/fetchDominatorTree", error);
+ dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
+ return null;
+ }
+ } while (display !== getState().labelDisplay);
+
+ removeFromCache();
+ dispatch({ type: actions.FETCH_DOMINATOR_TREE_END, id, root });
+ return root;
+ },
+ }
+));
+
+/**
+ * Fetch the immediately dominated children represented by the placeholder
+ * `lazyChildren` from snapshot-with-the-given-id's dominator tree.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ * @param {DominatorTreeLazyChildren} lazyChildren
+ */
+exports.fetchImmediatelyDominated = TaskCache.declareCacheableTask({
+ getCacheKey(_, id, lazyChildren) {
+ return `${id}-${lazyChildren.key()}`;
+ },
+
+ task: async function(
+ heapWorker,
+ id,
+ lazyChildren,
+ removeFromCache,
+ dispatch,
+ getState
+ ) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(snapshot.dominatorTree, "Should have dominator tree model");
+ assert(
+ snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
+ snapshot.dominatorTree.state ===
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Cannot fetch immediately dominated nodes in a dominator tree unless " +
+ " the dominator tree has already been computed"
+ );
+
+ let display;
+ let response;
+ do {
+ display = getState().labelDisplay;
+ assert(display, "Should have a display to describe nodes with.");
+
+ dispatch({ type: actions.FETCH_IMMEDIATELY_DOMINATED_START, id });
+
+ try {
+ response = await heapWorker.getImmediatelyDominated({
+ dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
+ breakdown: display.breakdown,
+ nodeId: lazyChildren.parentNodeId(),
+ startIndex: lazyChildren.siblingIndex(),
+ maxRetainingPaths: Preferences.get(
+ "devtools.memory.max-retaining-paths"
+ ),
+ });
+ } catch (error) {
+ removeFromCache();
+ reportException("actions/snapshot/fetchImmediatelyDominated", error);
+ dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
+ return;
+ }
+ } while (display !== getState().labelDisplay);
+
+ removeFromCache();
+ dispatch({
+ type: actions.FETCH_IMMEDIATELY_DOMINATED_END,
+ id,
+ path: response.path,
+ nodes: response.nodes,
+ moreChildrenAvailable: response.moreChildrenAvailable,
+ });
+ },
+});
+
+/**
+ * Compute and then fetch the dominator tree of the snapshot with the given
+ * `id`.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ *
+ * @returns {Promise<DominatorTreeNode>}
+ */
+const computeAndFetchDominatorTree = (exports.computeAndFetchDominatorTree = TaskCache.declareCacheableTask(
+ {
+ getCacheKey(_, id) {
+ return id;
+ },
+
+ task: async function(heapWorker, id, removeFromCache, dispatch, getState) {
+ const dominatorTreeId = await dispatch(
+ computeDominatorTree(heapWorker, id)
+ );
+ if (dominatorTreeId === null) {
+ removeFromCache();
+ return null;
+ }
+
+ const root = await dispatch(fetchDominatorTree(heapWorker, id));
+ removeFromCache();
+
+ if (!root) {
+ return null;
+ }
+
+ return root;
+ },
+ }
+));
+
+/**
+ * Update the currently selected snapshot's dominator tree.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+exports.refreshSelectedDominatorTree = function(heapWorker) {
+ return async function({ dispatch, getState }) {
+ const snapshot = getState().snapshots.find(s => s.selected);
+ if (!snapshot) {
+ return;
+ }
+
+ if (
+ snapshot.dominatorTree &&
+ !(
+ snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
+ snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
+ snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING
+ )
+ ) {
+ return;
+ }
+
+ // We need to check for the snapshot state because if there was an error,
+ // we can't continue and if we are still saving or reading the snapshot,
+ // then takeSnapshotAndCensus will finish the job for us
+ if (snapshot.state === states.READ) {
+ if (snapshot.dominatorTree) {
+ await dispatch(fetchDominatorTree(heapWorker, snapshot.id));
+ } else {
+ await dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
+ }
+ }
+ };
+};
+
+/**
+ * Select the snapshot with the given id.
+ *
+ * @param {snapshotId} id
+ * @see {Snapshot} model defined in devtools/client/memory/models.js
+ */
+const selectSnapshot = (exports.selectSnapshot = function(id) {
+ return {
+ type: actions.SELECT_SNAPSHOT,
+ id,
+ };
+});
+
+/**
+ * Delete all snapshots that are in the READ or ERROR state
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+exports.clearSnapshots = function(heapWorker) {
+ return async function({ dispatch, getState }) {
+ const snapshots = getState().snapshots.filter(s => {
+ const snapshotReady = s.state === states.READ || s.state === states.ERROR;
+ const censusReady =
+ (s.treeMap && s.treeMap.state === treeMapState.SAVED) ||
+ (s.census && s.census.state === censusState.SAVED);
+
+ return snapshotReady && censusReady;
+ });
+
+ const ids = snapshots.map(s => s.id);
+
+ dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids });
+
+ if (getState().diffing) {
+ dispatch(diffing.toggleDiffing());
+ }
+ if (getState().individuals) {
+ dispatch(view.popView());
+ }
+
+ await Promise.all(
+ snapshots.map(snapshot => {
+ return heapWorker.deleteHeapSnapshot(snapshot.path).catch(error => {
+ reportException("clearSnapshots", error);
+ dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
+ });
+ })
+ );
+
+ dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids });
+ };
+};
+
+/**
+ * Delete a snapshot
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotModel} snapshot
+ */
+exports.deleteSnapshot = function(heapWorker, snapshot) {
+ return async function({ dispatch, getState }) {
+ dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids: [snapshot.id] });
+
+ try {
+ await heapWorker.deleteHeapSnapshot(snapshot.path);
+ } catch (error) {
+ reportException("deleteSnapshot", error);
+ dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
+ }
+
+ dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids: [snapshot.id] });
+ };
+};
+
+/**
+ * Expand the given node in the snapshot's census report.
+ *
+ * @param {CensusTreeNode} node
+ */
+exports.expandCensusNode = function(id, node) {
+ return {
+ type: actions.EXPAND_CENSUS_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Collapse the given node in the snapshot's census report.
+ *
+ * @param {CensusTreeNode} node
+ */
+exports.collapseCensusNode = function(id, node) {
+ return {
+ type: actions.COLLAPSE_CENSUS_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Focus the given node in the snapshot's census's report.
+ *
+ * @param {SnapshotId} id
+ * @param {DominatorTreeNode} node
+ */
+exports.focusCensusNode = function(id, node) {
+ return {
+ type: actions.FOCUS_CENSUS_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Expand the given node in the snapshot's dominator tree.
+ *
+ * @param {DominatorTreeTreeNode} node
+ */
+exports.expandDominatorTreeNode = function(id, node) {
+ return {
+ type: actions.EXPAND_DOMINATOR_TREE_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Collapse the given node in the snapshot's dominator tree.
+ *
+ * @param {DominatorTreeTreeNode} node
+ */
+exports.collapseDominatorTreeNode = function(id, node) {
+ return {
+ type: actions.COLLAPSE_DOMINATOR_TREE_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Focus the given node in the snapshot's dominator tree.
+ *
+ * @param {SnapshotId} id
+ * @param {DominatorTreeNode} node
+ */
+exports.focusDominatorTreeNode = function(id, node) {
+ return {
+ type: actions.FOCUS_DOMINATOR_TREE_NODE,
+ id,
+ node,
+ };
+};
diff --git a/devtools/client/memory/actions/task-cache.js b/devtools/client/memory/actions/task-cache.js
new file mode 100644
index 0000000000..f79ed1d5a4
--- /dev/null
+++ b/devtools/client/memory/actions/task-cache.js
@@ -0,0 +1,105 @@
+/* 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/. */
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+
+/**
+ * The `TaskCache` allows for re-using active tasks when spawning a second task
+ * would simply duplicate work and is unnecessary. It maps from a task's unique
+ * key to the promise of its result.
+ */
+const TaskCache = (module.exports = class TaskCache {
+ constructor() {
+ this._cache = new Map();
+ }
+
+ /**
+ * Get the promise keyed by the given unique `key`, if one exists.
+ *
+ * @param {Any} key
+ * @returns {Promise<Any> | undefined}
+ */
+ get(key) {
+ return this._cache.get(key);
+ }
+
+ /**
+ * Put the task result promise in the cache and associate it with the given
+ * `key` which must not already have an entry in the cache.
+ *
+ * @param {Any} key
+ * @param {Promise<Any>} promise
+ */
+ put(key, promise) {
+ assert(!this._cache.has(key), "We should not override extant entries");
+
+ this._cache.set(key, promise);
+ }
+
+ /**
+ * Remove the cache entry with the given key.
+ *
+ * @param {Any} key
+ */
+ remove(key) {
+ assert(
+ this._cache.has(key),
+ `Should have an extant entry for key = ${key}`
+ );
+
+ this._cache.delete(key);
+ }
+});
+
+/**
+ * Create a new action-orchestrating task that is automatically cached. The
+ * tasks themselves are responsible from removing themselves from the cache.
+ *
+ * @param {Function(...args) -> Any} getCacheKey
+ * @param {Generator(...args) -> Any} task
+ *
+ * @returns Cacheable, Action-Creating Task
+ */
+TaskCache.declareCacheableTask = function({ getCacheKey, task }) {
+ const cache = new TaskCache();
+
+ return function(...args) {
+ return async function({ dispatch, getState }) {
+ const key = getCacheKey(...args);
+
+ const extantResult = cache.get(key);
+ if (extantResult) {
+ return extantResult;
+ }
+
+ // Ensure that we have our new entry in the cache *before* dispatching the
+ // task!
+ let resolve;
+ cache.put(
+ key,
+ new Promise(r => {
+ resolve = r;
+ })
+ );
+
+ resolve(
+ dispatch(async function() {
+ try {
+ args.push(() => cache.remove(key), dispatch, getState);
+ return await task(...args);
+ } catch (error) {
+ // Don't perma-cache errors.
+ if (cache.get(key)) {
+ cache.remove(key);
+ }
+ throw error;
+ }
+ })
+ );
+
+ return cache.get(key);
+ };
+ };
+};
diff --git a/devtools/client/memory/actions/tree-map-display.js b/devtools/client/memory/actions/tree-map-display.js
new file mode 100644
index 0000000000..9ad1528c83
--- /dev/null
+++ b/devtools/client/memory/actions/tree-map-display.js
@@ -0,0 +1,40 @@
+/* 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/. */
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("devtools/client/memory/constants");
+const { refresh } = require("devtools/client/memory/actions/refresh");
+/**
+ * Sets the tree map display as the current display and refreshes the tree map
+ * census.
+ */
+exports.setTreeMapAndRefresh = function(heapWorker, display) {
+ return async function({ dispatch, getState }) {
+ dispatch(setTreeMap(display));
+ await dispatch(refresh(heapWorker));
+ };
+};
+
+/**
+ * Clears out all cached census data in the snapshots and sets new display data
+ * for tree maps.
+ *
+ * @param {treeMapModel} display
+ */
+const setTreeMap = (exports.setTreeMap = function(display) {
+ assert(
+ typeof display === "object" &&
+ display &&
+ display.breakdown &&
+ display.breakdown.by,
+ "Breakdowns must be an object with a `by` property, attempted to set: " +
+ JSON.stringify(display)
+ );
+
+ return {
+ type: actions.SET_TREE_MAP_DISPLAY,
+ display,
+ };
+});
diff --git a/devtools/client/memory/actions/view.js b/devtools/client/memory/actions/view.js
new file mode 100644
index 0000000000..1e7fb12ee9
--- /dev/null
+++ b/devtools/client/memory/actions/view.js
@@ -0,0 +1,67 @@
+/* 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/. */
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("devtools/client/memory/constants");
+const { findSelectedSnapshot } = require("devtools/client/memory/utils");
+const refresh = require("devtools/client/memory/actions/refresh");
+
+/**
+ * Change the currently selected view.
+ *
+ * @param {viewState} view
+ */
+const changeView = (exports.changeView = function(view) {
+ return function({ dispatch, getState }) {
+ dispatch({
+ type: actions.CHANGE_VIEW,
+ newViewState: view,
+ oldDiffing: getState().diffing,
+ oldSelected: findSelectedSnapshot(getState()),
+ });
+ };
+});
+
+/**
+ * Given that we are in the INDIVIDUALS view state, go back to the state we were
+ * in before.
+ */
+const popView = (exports.popView = function() {
+ return function({ dispatch, getState }) {
+ const { previous } = getState().view;
+ assert(previous);
+ dispatch({
+ type: actions.POP_VIEW,
+ previousView: previous,
+ });
+ };
+});
+
+/**
+ * Change the currently selected view and ensure all our data is up to date from
+ * the heap worker.
+ *
+ * @param {viewState} view
+ * @param {HeapAnalysesClient} heapWorker
+ */
+exports.changeViewAndRefresh = function(view, heapWorker) {
+ return async function({ dispatch, getState }) {
+ dispatch(changeView(view));
+ await dispatch(refresh.refresh(heapWorker));
+ };
+};
+
+/**
+ * Given that we are in the INDIVIDUALS view state, go back to the state we were
+ * previously in and refresh our data.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+exports.popViewAndRefresh = function(heapWorker) {
+ return async function({ dispatch, getState }) {
+ dispatch(popView());
+ await dispatch(refresh.refresh(heapWorker));
+ };
+};
diff --git a/devtools/client/memory/app.js b/devtools/client/memory/app.js
new file mode 100644
index 0000000000..4b76d94ced
--- /dev/null
+++ b/devtools/client/memory/app.js
@@ -0,0 +1,430 @@
+/* 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/. */
+
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { appinfo } = require("Services");
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const {
+ censusDisplays,
+ labelDisplays,
+ treeMapDisplays,
+ diffingState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ toggleRecordingAllocationStacks,
+} = require("devtools/client/memory/actions/allocations");
+const {
+ setCensusDisplayAndRefresh,
+} = require("devtools/client/memory/actions/census-display");
+const {
+ setLabelDisplayAndRefresh,
+} = require("devtools/client/memory/actions/label-display");
+const {
+ setTreeMapDisplayAndRefresh,
+} = require("devtools/client/memory/actions/tree-map-display");
+
+const {
+ getCustomCensusDisplays,
+ getCustomLabelDisplays,
+ getCustomTreeMapDisplays,
+} = require("devtools/client/memory/utils");
+const {
+ selectSnapshotForDiffingAndRefresh,
+ toggleDiffing,
+ expandDiffingCensusNode,
+ collapseDiffingCensusNode,
+ focusDiffingCensusNode,
+} = require("devtools/client/memory/actions/diffing");
+const {
+ setFilterStringAndRefresh,
+} = require("devtools/client/memory/actions/filter");
+const {
+ pickFileAndExportSnapshot,
+ pickFileAndImportSnapshotAndCensus,
+} = require("devtools/client/memory/actions/io");
+const {
+ selectSnapshotAndRefresh,
+ takeSnapshotAndCensus,
+ clearSnapshots,
+ deleteSnapshot,
+ fetchImmediatelyDominated,
+ expandCensusNode,
+ collapseCensusNode,
+ focusCensusNode,
+ expandDominatorTreeNode,
+ collapseDominatorTreeNode,
+ focusDominatorTreeNode,
+ fetchIndividuals,
+ focusIndividual,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeViewAndRefresh,
+ popViewAndRefresh,
+} = require("devtools/client/memory/actions/view");
+const { resizeShortestPaths } = require("devtools/client/memory/actions/sizes");
+const Toolbar = createFactory(
+ require("devtools/client/memory/components/Toolbar")
+);
+const List = createFactory(require("devtools/client/memory/components/List"));
+const SnapshotListItem = createFactory(
+ require("devtools/client/memory/components/SnapshotListItem")
+);
+const Heap = createFactory(require("devtools/client/memory/components/Heap"));
+const { app: appModel } = require("devtools/client/memory/models");
+
+class MemoryApp extends Component {
+ static get propTypes() {
+ return {
+ allocations: appModel.allocations,
+ censusDisplay: appModel.censusDisplay,
+ diffing: appModel.diffing,
+ dispatch: PropTypes.func,
+ filter: appModel.filter,
+ front: appModel.front,
+ heapWorker: appModel.heapWorker,
+ individuals: appModel.individuals,
+ labelDisplay: appModel.labelDisplay,
+ sizes: PropTypes.object,
+ snapshots: appModel.snapshots,
+ toolbox: PropTypes.object,
+ view: appModel.view,
+ };
+ }
+
+ static get childContextTypes() {
+ return {
+ front: PropTypes.any,
+ heapWorker: PropTypes.any,
+ toolbox: PropTypes.any,
+ };
+ }
+
+ static get defaultProps() {
+ return {};
+ }
+
+ constructor(props) {
+ super(props);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this._getCensusDisplays = this._getCensusDisplays.bind(this);
+ this._getLabelDisplays = this._getLabelDisplays.bind(this);
+ this._getTreeMapDisplays = this._getTreeMapDisplays.bind(this);
+ }
+
+ getChildContext() {
+ return {
+ front: this.props.front,
+ heapWorker: this.props.heapWorker,
+ toolbox: this.props.toolbox,
+ };
+ }
+
+ componentDidMount() {
+ // Attach the keydown listener directly to the window. When an element that
+ // has the focus (such as a tree node) is removed from the DOM, the focus
+ // falls back to the body.
+ window.addEventListener("keydown", this.onKeyDown);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("keydown", this.onKeyDown);
+ }
+
+ onKeyDown(e) {
+ const { snapshots, dispatch, heapWorker } = this.props;
+ const selectedSnapshot = snapshots.find(s => s.selected);
+ const selectedIndex = snapshots.indexOf(selectedSnapshot);
+
+ const isOSX = appinfo.OS == "Darwin";
+ const isAccelKey = (isOSX && e.metaKey) || (!isOSX && e.ctrlKey);
+
+ // On ACCEL+UP, select previous snapshot.
+ if (isAccelKey && e.key === "ArrowUp") {
+ const previousIndex = Math.max(0, selectedIndex - 1);
+ const previousSnapshotId = snapshots[previousIndex].id;
+ dispatch(selectSnapshotAndRefresh(heapWorker, previousSnapshotId));
+ }
+
+ // On ACCEL+DOWN, select next snapshot.
+ if (isAccelKey && e.key === "ArrowDown") {
+ const nextIndex = Math.min(snapshots.length - 1, selectedIndex + 1);
+ const nextSnapshotId = snapshots[nextIndex].id;
+ dispatch(selectSnapshotAndRefresh(heapWorker, nextSnapshotId));
+ }
+ }
+
+ _getCensusDisplays() {
+ const customDisplays = getCustomCensusDisplays();
+ const custom = Object.keys(customDisplays).reduce((arr, key) => {
+ arr.push(customDisplays[key]);
+ return arr;
+ }, []);
+
+ return [
+ censusDisplays.coarseType,
+ censusDisplays.allocationStack,
+ censusDisplays.invertedAllocationStack,
+ ].concat(custom);
+ }
+
+ _getLabelDisplays() {
+ const customDisplays = getCustomLabelDisplays();
+ const custom = Object.keys(customDisplays).reduce((arr, key) => {
+ arr.push(customDisplays[key]);
+ return arr;
+ }, []);
+
+ return [labelDisplays.coarseType, labelDisplays.allocationStack].concat(
+ custom
+ );
+ }
+
+ _getTreeMapDisplays() {
+ const customDisplays = getCustomTreeMapDisplays();
+ const custom = Object.keys(customDisplays).reduce((arr, key) => {
+ arr.push(customDisplays[key]);
+ return arr;
+ }, []);
+
+ return [treeMapDisplays.coarseType].concat(custom);
+ }
+
+ render() {
+ const {
+ dispatch,
+ snapshots,
+ front,
+ heapWorker,
+ allocations,
+ toolbox,
+ filter,
+ diffing,
+ view,
+ sizes,
+ censusDisplay,
+ labelDisplay,
+ individuals,
+ } = this.props;
+
+ const selectedSnapshot = snapshots.find(s => s.selected);
+
+ const onClickSnapshotListItem =
+ diffing && diffing.state === diffingState.SELECTING
+ ? snapshot =>
+ dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, snapshot))
+ : snapshot =>
+ dispatch(selectSnapshotAndRefresh(heapWorker, snapshot.id));
+
+ return dom.div(
+ {
+ id: "memory-tool",
+ },
+
+ Toolbar({
+ snapshots,
+ censusDisplays: this._getCensusDisplays(),
+ censusDisplay,
+ onCensusDisplayChange: newDisplay =>
+ dispatch(setCensusDisplayAndRefresh(heapWorker, newDisplay)),
+ onImportClick: () =>
+ dispatch(pickFileAndImportSnapshotAndCensus(heapWorker)),
+ onClearSnapshotsClick: () => dispatch(clearSnapshots(heapWorker)),
+ onTakeSnapshotClick: () =>
+ dispatch(takeSnapshotAndCensus(front, heapWorker)),
+ onToggleRecordAllocationStacks: () =>
+ dispatch(toggleRecordingAllocationStacks(front)),
+ allocations,
+ filterString: filter,
+ setFilterString: filterString =>
+ dispatch(setFilterStringAndRefresh(filterString, heapWorker)),
+ diffing,
+ onToggleDiffing: () => dispatch(toggleDiffing()),
+ view,
+ labelDisplays: this._getLabelDisplays(),
+ labelDisplay,
+ onLabelDisplayChange: newDisplay =>
+ dispatch(setLabelDisplayAndRefresh(heapWorker, newDisplay)),
+ treeMapDisplays: this._getTreeMapDisplays(),
+ onTreeMapDisplayChange: newDisplay =>
+ dispatch(setTreeMapDisplayAndRefresh(heapWorker, newDisplay)),
+ onViewChange: v => dispatch(changeViewAndRefresh(v, heapWorker)),
+ }),
+
+ dom.div(
+ {
+ id: "memory-tool-container",
+ },
+
+ List({
+ itemComponent: SnapshotListItem,
+ items: snapshots,
+ onSave: snapshot => dispatch(pickFileAndExportSnapshot(snapshot)),
+ onDelete: snapshot => dispatch(deleteSnapshot(heapWorker, snapshot)),
+ onClick: onClickSnapshotListItem,
+ diffing,
+ }),
+
+ Heap({
+ snapshot: selectedSnapshot,
+ diffing,
+ onViewSourceInDebugger: ({ url, line, column }) => {
+ toolbox.viewSourceInDebugger(url, line, column);
+ },
+ onSnapshotClick: () =>
+ dispatch(takeSnapshotAndCensus(front, heapWorker)),
+ onLoadMoreSiblings: lazyChildren =>
+ dispatch(
+ fetchImmediatelyDominated(
+ heapWorker,
+ selectedSnapshot.id,
+ lazyChildren
+ )
+ ),
+ onPopView: () => dispatch(popViewAndRefresh(heapWorker)),
+ individuals,
+ onViewIndividuals: node => {
+ const snapshotId = diffing
+ ? diffing.secondSnapshotId
+ : selectedSnapshot.id;
+ dispatch(
+ fetchIndividuals(
+ heapWorker,
+ snapshotId,
+ censusDisplay.breakdown,
+ node.reportLeafIndex
+ )
+ );
+ },
+ onFocusIndividual: node => {
+ assert(
+ view.state === viewState.INDIVIDUALS,
+ "Should be in the individuals view"
+ );
+ dispatch(focusIndividual(node));
+ },
+ onCensusExpand: (census, node) => {
+ if (diffing) {
+ assert(
+ diffing.census === census,
+ "Should only expand active census"
+ );
+ dispatch(expandDiffingCensusNode(node));
+ } else {
+ assert(
+ selectedSnapshot && selectedSnapshot.census === census,
+ "If not diffing, " +
+ "should be expanding on selected snapshot's census"
+ );
+ dispatch(expandCensusNode(selectedSnapshot.id, node));
+ }
+ },
+ onCensusCollapse: (census, node) => {
+ if (diffing) {
+ assert(
+ diffing.census === census,
+ "Should only collapse active census"
+ );
+ dispatch(collapseDiffingCensusNode(node));
+ } else {
+ assert(
+ selectedSnapshot && selectedSnapshot.census === census,
+ "If not diffing, " +
+ "should be collapsing on selected snapshot's census"
+ );
+ dispatch(collapseCensusNode(selectedSnapshot.id, node));
+ }
+ },
+ onCensusFocus: (census, node) => {
+ if (diffing) {
+ assert(
+ diffing.census === census,
+ "Should only focus nodes in active census"
+ );
+ dispatch(focusDiffingCensusNode(node));
+ } else {
+ assert(
+ selectedSnapshot && selectedSnapshot.census === census,
+ "If not diffing, " +
+ "should be focusing on nodes in selected snapshot's census"
+ );
+ dispatch(focusCensusNode(selectedSnapshot.id, node));
+ }
+ },
+ onDominatorTreeExpand: node => {
+ assert(
+ view.state === viewState.DOMINATOR_TREE,
+ "If expanding dominator tree nodes, " +
+ "should be in dominator tree view"
+ );
+ assert(
+ selectedSnapshot,
+ "...and we should have a selected snapshot"
+ );
+ assert(
+ selectedSnapshot.dominatorTree,
+ "...and that snapshot should have a dominator tree"
+ );
+ dispatch(expandDominatorTreeNode(selectedSnapshot.id, node));
+ },
+ onDominatorTreeCollapse: node => {
+ assert(
+ view.state === viewState.DOMINATOR_TREE,
+ "If collapsing dominator tree nodes, " +
+ "should be in dominator tree view"
+ );
+ assert(
+ selectedSnapshot,
+ "...and we should have a selected snapshot"
+ );
+ assert(
+ selectedSnapshot.dominatorTree,
+ "...and that snapshot should have a dominator tree"
+ );
+ dispatch(collapseDominatorTreeNode(selectedSnapshot.id, node));
+ },
+ onDominatorTreeFocus: node => {
+ assert(
+ view.state === viewState.DOMINATOR_TREE,
+ "If focusing dominator tree nodes, " +
+ "should be in dominator tree view"
+ );
+ assert(
+ selectedSnapshot,
+ "...and we should have a selected snapshot"
+ );
+ assert(
+ selectedSnapshot.dominatorTree,
+ "...and that snapshot should have a dominator tree"
+ );
+ dispatch(focusDominatorTreeNode(selectedSnapshot.id, node));
+ },
+ onShortestPathsResize: newSize => {
+ dispatch(resizeShortestPaths(newSize));
+ },
+ sizes,
+ view,
+ })
+ )
+ );
+ }
+}
+
+/**
+ * Passed into react-redux's `connect` method that is called on store change
+ * and passed to components.
+ */
+function mapStateToProps(state) {
+ return state;
+}
+
+module.exports = connect(mapStateToProps)(MemoryApp);
diff --git a/devtools/client/memory/components/Census.js b/devtools/client/memory/components/Census.js
new file mode 100644
index 0000000000..8f3118c085
--- /dev/null
+++ b/devtools/client/memory/components/Census.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Tree = createFactory(
+ require("devtools/client/shared/components/VirtualizedTree")
+);
+const CensusTreeItem = createFactory(
+ require("devtools/client/memory/components/CensusTreeItem")
+);
+const { TREE_ROW_HEIGHT } = require("devtools/client/memory/constants");
+const { censusModel, diffingModel } = require("devtools/client/memory/models");
+
+class Census extends Component {
+ static get propTypes() {
+ return {
+ census: censusModel,
+ onExpand: PropTypes.func.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onViewIndividuals: PropTypes.func.isRequired,
+ diffing: diffingModel,
+ };
+ }
+
+ render() {
+ const {
+ census,
+ onExpand,
+ onCollapse,
+ onFocus,
+ diffing,
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ } = this.props;
+
+ const report = census.report;
+ const parentMap = census.parentMap;
+ const { totalBytes, totalCount } = report;
+
+ const getPercentBytes =
+ totalBytes === 0 ? _ => 0 : bytes => (bytes / totalBytes) * 100;
+
+ const getPercentCount =
+ totalCount === 0 ? _ => 0 : count => (count / totalCount) * 100;
+
+ return Tree({
+ autoExpandDepth: 0,
+ preventNavigationOnArrowRight: false,
+ focused: census.focused,
+ getParent: node => {
+ const parent = parentMap[node.id];
+ return parent === report ? null : parent;
+ },
+ getChildren: node => node.children || [],
+ isExpanded: node => census.expanded.has(node.id),
+ onExpand,
+ onCollapse,
+ onFocus,
+ renderItem: (item, depth, focused, arrow, expanded) =>
+ new CensusTreeItem({
+ onViewSourceInDebugger,
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ getPercentBytes,
+ getPercentCount,
+ diffing,
+ inverted: census.display.inverted,
+ onViewIndividuals,
+ }),
+ getRoots: () => report.children || [],
+ getKey: node => node.id,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+}
+
+module.exports = Census;
diff --git a/devtools/client/memory/components/CensusHeader.js b/devtools/client/memory/components/CensusHeader.js
new file mode 100644
index 0000000000..a0b05f6b49
--- /dev/null
+++ b/devtools/client/memory/components/CensusHeader.js
@@ -0,0 +1,77 @@
+/* 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/. */
+
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { L10N } = require("devtools/client/memory/utils");
+const models = require("devtools/client/memory/models");
+
+class CensusHeader extends Component {
+ static get propTypes() {
+ return {
+ diffing: models.diffingModel,
+ };
+ }
+
+ render() {
+ let individualsCell;
+ if (!this.props.diffing) {
+ individualsCell = dom.span({
+ className: "heap-tree-item-field heap-tree-item-individuals",
+ });
+ }
+
+ return dom.div(
+ {
+ className: "header",
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.bytes.tooltip"),
+ },
+ L10N.getStr("heapview.field.bytes")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-count",
+ title: L10N.getStr("heapview.field.count.tooltip"),
+ },
+ L10N.getStr("heapview.field.count")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-total-bytes",
+ title: L10N.getStr("heapview.field.totalbytes.tooltip"),
+ },
+ L10N.getStr("heapview.field.totalbytes")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-total-count",
+ title: L10N.getStr("heapview.field.totalcount.tooltip"),
+ },
+ L10N.getStr("heapview.field.totalcount")
+ ),
+
+ individualsCell,
+
+ dom.span(
+ {
+ className: "heap-tree-item-name",
+ title: L10N.getStr("heapview.field.name.tooltip"),
+ },
+ L10N.getStr("heapview.field.name")
+ )
+ );
+ }
+}
+
+module.exports = CensusHeader;
diff --git a/devtools/client/memory/components/CensusTreeItem.js b/devtools/client/memory/components/CensusTreeItem.js
new file mode 100644
index 0000000000..a5285778a4
--- /dev/null
+++ b/devtools/client/memory/components/CensusTreeItem.js
@@ -0,0 +1,181 @@
+/* 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/. */
+"use strict";
+
+const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const {
+ L10N,
+ formatNumber,
+ formatPercent,
+} = require("devtools/client/memory/utils");
+const Frame = createFactory(require("devtools/client/shared/components/Frame"));
+const { TREE_ROW_HEIGHT } = require("devtools/client/memory/constants");
+const models = require("devtools/client/memory/models");
+
+class CensusTreeItem extends Component {
+ static get propTypes() {
+ return {
+ arrow: PropTypes.any,
+ depth: PropTypes.number.isRequired,
+ diffing: models.app.diffing,
+ expanded: PropTypes.bool.isRequired,
+ focused: PropTypes.bool.isRequired,
+ getPercentBytes: PropTypes.func.isRequired,
+ getPercentCount: PropTypes.func.isRequired,
+ inverted: PropTypes.bool,
+ item: PropTypes.object.isRequired,
+ onViewIndividuals: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.toLabel = this.toLabel.bind(this);
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.item != nextProps.item ||
+ this.props.depth != nextProps.depth ||
+ this.props.expanded != nextProps.expanded ||
+ this.props.focused != nextProps.focused ||
+ this.props.diffing != nextProps.diffing
+ );
+ }
+
+ toLabel(name, onViewSourceInDebugger) {
+ if (isSavedFrame(name)) {
+ return Frame({
+ frame: name,
+ onClick: onViewSourceInDebugger,
+ showFunctionName: true,
+ showHost: true,
+ });
+ }
+
+ if (name === null) {
+ return L10N.getStr("tree-item.root");
+ }
+
+ if (name === "noStack") {
+ return L10N.getStr("tree-item.nostack");
+ }
+
+ if (name === "noFilename") {
+ return L10N.getStr("tree-item.nofilename");
+ }
+
+ return String(name);
+ }
+
+ render() {
+ const {
+ item,
+ depth,
+ arrow,
+ focused,
+ getPercentBytes,
+ getPercentCount,
+ diffing,
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ inverted,
+ } = this.props;
+
+ const bytes = formatNumber(item.bytes, !!diffing);
+ const percentBytes = formatPercent(getPercentBytes(item.bytes), !!diffing);
+
+ const count = formatNumber(item.count, !!diffing);
+ const percentCount = formatPercent(getPercentCount(item.count), !!diffing);
+
+ const totalBytes = formatNumber(item.totalBytes, !!diffing);
+ const percentTotalBytes = formatPercent(
+ getPercentBytes(item.totalBytes),
+ !!diffing
+ );
+
+ const totalCount = formatNumber(item.totalCount, !!diffing);
+ const percentTotalCount = formatPercent(
+ getPercentCount(item.totalCount),
+ !!diffing
+ );
+
+ let pointer;
+ if (inverted && depth > 0) {
+ pointer = dom.span({ className: "children-pointer" }, "↖");
+ } else if (!inverted && item.children?.length) {
+ pointer = dom.span({ className: "children-pointer" }, "↘");
+ }
+
+ let individualsCell;
+ if (!diffing) {
+ let individualsButton;
+ if (item.reportLeafIndex !== undefined) {
+ individualsButton = dom.button(
+ {
+ key: `individuals-button-${item.id}`,
+ title: L10N.getStr("tree-item.view-individuals.tooltip"),
+ className: "devtools-button individuals-button",
+ onClick: e => {
+ // Don't let the event bubble up to cause this item to focus after
+ // we have switched views, which would lead to assertion failures.
+ e.preventDefault();
+ e.stopPropagation();
+
+ onViewIndividuals(item);
+ },
+ },
+ "⁂"
+ );
+ }
+ individualsCell = dom.span(
+ { className: "heap-tree-item-field heap-tree-item-individuals" },
+ individualsButton
+ );
+ }
+
+ return dom.div(
+ { className: `heap-tree-item ${focused ? "focused" : ""}` },
+ dom.span(
+ { className: "heap-tree-item-field heap-tree-item-bytes" },
+ dom.span({ className: "heap-tree-number" }, bytes),
+ dom.span({ className: "heap-tree-percent" }, percentBytes)
+ ),
+ dom.span(
+ { className: "heap-tree-item-field heap-tree-item-count" },
+ dom.span({ className: "heap-tree-number" }, count),
+ dom.span({ className: "heap-tree-percent" }, percentCount)
+ ),
+ dom.span(
+ { className: "heap-tree-item-field heap-tree-item-total-bytes" },
+ dom.span({ className: "heap-tree-number" }, totalBytes),
+ dom.span({ className: "heap-tree-percent" }, percentTotalBytes)
+ ),
+ dom.span(
+ { className: "heap-tree-item-field heap-tree-item-total-count" },
+ dom.span({ className: "heap-tree-number" }, totalCount),
+ dom.span({ className: "heap-tree-percent" }, percentTotalCount)
+ ),
+ individualsCell,
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT },
+ },
+ arrow,
+ pointer,
+ this.toLabel(item.name, onViewSourceInDebugger)
+ )
+ );
+ }
+}
+
+module.exports = CensusTreeItem;
diff --git a/devtools/client/memory/components/DominatorTree.js b/devtools/client/memory/components/DominatorTree.js
new file mode 100644
index 0000000000..2a2c50aa89
--- /dev/null
+++ b/devtools/client/memory/components/DominatorTree.js
@@ -0,0 +1,249 @@
+/* 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/. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { createParentMap } = require("devtools/shared/heapsnapshot/CensusUtils");
+const Tree = createFactory(
+ require("devtools/client/shared/components/VirtualizedTree")
+);
+const DominatorTreeItem = createFactory(
+ require("devtools/client/memory/components/DominatorTreeItem")
+);
+const { L10N } = require("devtools/client/memory/utils");
+const {
+ TREE_ROW_HEIGHT,
+ dominatorTreeState,
+} = require("devtools/client/memory/constants");
+const { dominatorTreeModel } = require("devtools/client/memory/models");
+const DominatorTreeLazyChildren = require("devtools/client/memory/dominator-tree-lazy-children");
+
+const DOMINATOR_TREE_AUTO_EXPAND_DEPTH = 3;
+
+/**
+ * A throbber that represents a subtree in the dominator tree that is actively
+ * being incrementally loaded and fetched from the `HeapAnalysesWorker`.
+ */
+class DominatorTreeSubtreeFetchingClass extends Component {
+ static get propTypes() {
+ return {
+ depth: PropTypes.number.isRequired,
+ focused: PropTypes.bool.isRequired,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.depth !== nextProps.depth ||
+ this.props.focused !== nextProps.focused
+ );
+ }
+
+ render() {
+ const { depth, focused } = this.props;
+
+ return dom.div(
+ {
+ className: `heap-tree-item subtree-fetching ${
+ focused ? "focused" : ""
+ }`,
+ },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({
+ className: "heap-tree-item-field heap-tree-item-name devtools-throbber",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT },
+ })
+ );
+ }
+}
+
+/**
+ * A link to fetch and load more siblings in the dominator tree, when there are
+ * already many loaded above.
+ */
+class DominatorTreeSiblingLinkClass extends Component {
+ static get propTypes() {
+ return {
+ depth: PropTypes.number.isRequired,
+ focused: PropTypes.bool.isRequired,
+ item: PropTypes.instanceOf(DominatorTreeLazyChildren).isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.depth !== nextProps.depth ||
+ this.props.focused !== nextProps.focused
+ );
+ }
+
+ render() {
+ const { depth, focused, item, onLoadMoreSiblings } = this.props;
+
+ return dom.div(
+ {
+ className: `heap-tree-item more-children ${focused ? "focused" : ""}`,
+ },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT },
+ },
+ dom.a(
+ {
+ className: "load-more-link",
+ onClick: () => onLoadMoreSiblings(item),
+ },
+ L10N.getStr("tree-item.load-more")
+ )
+ )
+ );
+ }
+}
+
+class DominatorTree extends Component {
+ static get propTypes() {
+ return {
+ dominatorTree: dominatorTreeModel.isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onExpand: PropTypes.func.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ // Safe to use referential equality here because all of our mutations on
+ // dominator tree models use immutableUpdate in a persistent manner. The
+ // exception to the rule are mutations of the expanded set, however we take
+ // care that the dominatorTree model itself is still re-allocated when
+ // mutations to the expanded set occur. Because of the re-allocations, we
+ // can continue using referential equality here.
+ return this.props.dominatorTree !== nextProps.dominatorTree;
+ }
+
+ render() {
+ const {
+ dominatorTree,
+ onViewSourceInDebugger,
+ onLoadMoreSiblings,
+ } = this.props;
+
+ const parentMap = createParentMap(dominatorTree.root, node => node.nodeId);
+
+ return Tree({
+ key: "dominator-tree-tree",
+ autoExpandDepth: DOMINATOR_TREE_AUTO_EXPAND_DEPTH,
+ preventNavigationOnArrowRight: false,
+ focused: dominatorTree.focused,
+ getParent: node =>
+ node instanceof DominatorTreeLazyChildren
+ ? parentMap[node.parentNodeId()]
+ : parentMap[node.nodeId],
+ getChildren: node => {
+ const children = node.children ? node.children.slice() : [];
+ if (node.moreChildrenAvailable) {
+ children.push(
+ new DominatorTreeLazyChildren(node.nodeId, children.length)
+ );
+ }
+ return children;
+ },
+ isExpanded: node => {
+ return node instanceof DominatorTreeLazyChildren
+ ? false
+ : dominatorTree.expanded.has(node.nodeId);
+ },
+ onExpand: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ if (
+ item.moreChildrenAvailable &&
+ (!item.children || !item.children.length)
+ ) {
+ const startIndex = item.children ? item.children.length : 0;
+ onLoadMoreSiblings(
+ new DominatorTreeLazyChildren(item.nodeId, startIndex)
+ );
+ }
+
+ this.props.onExpand(item);
+ },
+ onCollapse: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ this.props.onCollapse(item);
+ },
+ onFocus: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ this.props.onFocus(item);
+ },
+ renderItem: (item, depth, focused, arrow, expanded) => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ if (item.isFirstChild()) {
+ assert(
+ dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
+ "If we are displaying a throbber for loading a subtree, " +
+ "then we should be INCREMENTAL_FETCHING those children right now"
+ );
+ return DominatorTreeSubtreeFetching({
+ key: item.key(),
+ depth,
+ focused,
+ });
+ }
+
+ return DominatorTreeSiblingLink({
+ key: item.key(),
+ item,
+ depth,
+ focused,
+ onLoadMoreSiblings,
+ });
+ }
+
+ return DominatorTreeItem({
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ getPercentSize: size =>
+ (size / dominatorTree.root.retainedSize) * 100,
+ onViewSourceInDebugger,
+ });
+ },
+ getRoots: () => [dominatorTree.root],
+ getKey: node =>
+ node instanceof DominatorTreeLazyChildren ? node.key() : node.nodeId,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+}
+
+const DominatorTreeSubtreeFetching = createFactory(
+ DominatorTreeSubtreeFetchingClass
+);
+const DominatorTreeSiblingLink = createFactory(DominatorTreeSiblingLinkClass);
+
+module.exports = DominatorTree;
diff --git a/devtools/client/memory/components/DominatorTreeHeader.js b/devtools/client/memory/components/DominatorTreeHeader.js
new file mode 100644
index 0000000000..432be4d8ec
--- /dev/null
+++ b/devtools/client/memory/components/DominatorTreeHeader.js
@@ -0,0 +1,49 @@
+/* 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/. */
+
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { L10N } = require("devtools/client/memory/utils");
+
+class DominatorTreeHeader extends Component {
+ static get propTypes() {
+ return {};
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "header",
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.retainedSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.retainedSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.shallowSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.shallowSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-name",
+ title: L10N.getStr("dominatortree.field.label.tooltip"),
+ },
+ L10N.getStr("dominatortree.field.label")
+ )
+ );
+ }
+}
+
+module.exports = DominatorTreeHeader;
diff --git a/devtools/client/memory/components/DominatorTreeItem.js b/devtools/client/memory/components/DominatorTreeItem.js
new file mode 100644
index 0000000000..f8d1660d1b
--- /dev/null
+++ b/devtools/client/memory/components/DominatorTreeItem.js
@@ -0,0 +1,167 @@
+/* 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/. */
+
+"use strict";
+
+const { assert, isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const {
+ L10N,
+ formatNumber,
+ formatPercent,
+} = require("devtools/client/memory/utils");
+const Frame = createFactory(require("devtools/client/shared/components/Frame"));
+const { TREE_ROW_HEIGHT } = require("devtools/client/memory/constants");
+
+class SeparatorClass extends Component {
+ render() {
+ return dom.span({ className: "separator" }, "›");
+ }
+}
+
+const Separator = createFactory(SeparatorClass);
+
+class DominatorTreeItem extends Component {
+ static get propTypes() {
+ return {
+ item: PropTypes.object.isRequired,
+ depth: PropTypes.number.isRequired,
+ arrow: PropTypes.object,
+ expanded: PropTypes.bool.isRequired,
+ focused: PropTypes.bool.isRequired,
+ getPercentSize: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.item != nextProps.item ||
+ this.props.depth != nextProps.depth ||
+ this.props.expanded != nextProps.expanded ||
+ this.props.focused != nextProps.focused
+ );
+ }
+
+ render() {
+ const {
+ item,
+ depth,
+ arrow,
+ focused,
+ getPercentSize,
+ onViewSourceInDebugger,
+ } = this.props;
+
+ const retainedSize = formatNumber(item.retainedSize);
+ const percentRetainedSize = formatPercent(
+ getPercentSize(item.retainedSize)
+ );
+
+ const shallowSize = formatNumber(item.shallowSize);
+ const percentShallowSize = formatPercent(getPercentSize(item.shallowSize));
+
+ // Build up our label UI as an array of each label piece, which is either a
+ // string or a frame, and separators in between them.
+
+ assert(item.label.length > 0, "Our label should not be empty");
+ const label = Array(item.label.length * 2 - 1);
+ label.fill(undefined);
+
+ for (let i = 0, length = item.label.length; i < length; i++) {
+ const piece = item.label[i];
+ const key = `${item.nodeId}-label-${i}`;
+
+ // `i` is the index of the label piece we are rendering, `label[i*2]` is
+ // where the rendered label piece belngs, and `label[i*2+1]` (if it isn't
+ // out of bounds) is where the separator belongs.
+
+ if (isSavedFrame(piece)) {
+ label[i * 2] = Frame({
+ key,
+ onClick: onViewSourceInDebugger,
+ frame: piece,
+ showFunctionName: true,
+ });
+ } else if (piece === "noStack") {
+ label[i * 2] = dom.span(
+ { key, className: "not-available" },
+ L10N.getStr("tree-item.nostack")
+ );
+ } else if (piece === "noFilename") {
+ label[i * 2] = dom.span(
+ { key, className: "not-available" },
+ L10N.getStr("tree-item.nofilename")
+ );
+ } else if (piece === "JS::ubi::RootList") {
+ // Don't use the usual labeling machinery for root lists: replace it
+ // with the "GC Roots" string.
+ label.splice(0, label.length);
+ label.push(L10N.getStr("tree-item.rootlist"));
+ break;
+ } else {
+ label[i * 2] = piece;
+ }
+
+ // If this is not the last piece of the label, add a separator.
+ if (i < length - 1) {
+ label[i * 2 + 1] = Separator({ key: `${item.nodeId}-separator-${i}` });
+ }
+ }
+
+ return dom.div(
+ {
+ className: `heap-tree-item ${focused ? "focused" : ""} node-${
+ item.nodeId
+ }`,
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-bytes",
+ },
+ dom.span(
+ {
+ className: "heap-tree-number",
+ },
+ retainedSize
+ ),
+ dom.span({ className: "heap-tree-percent" }, percentRetainedSize)
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-bytes",
+ },
+ dom.span(
+ {
+ className: "heap-tree-number",
+ },
+ shallowSize
+ ),
+ dom.span({ className: "heap-tree-percent" }, percentShallowSize)
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT },
+ },
+ arrow,
+ label,
+ dom.span(
+ { className: "heap-tree-item-address" },
+ `@ 0x${item.nodeId.toString(16)}`
+ )
+ )
+ );
+ }
+}
+
+module.exports = DominatorTreeItem;
diff --git a/devtools/client/memory/components/Heap.js b/devtools/client/memory/components/Heap.js
new file mode 100644
index 0000000000..3eb4557a4f
--- /dev/null
+++ b/devtools/client/memory/components/Heap.js
@@ -0,0 +1,541 @@
+/* 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/. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils");
+const Census = createFactory(
+ require("devtools/client/memory/components/Census")
+);
+const CensusHeader = createFactory(
+ require("devtools/client/memory/components/CensusHeader")
+);
+const DominatorTree = createFactory(
+ require("devtools/client/memory/components/DominatorTree")
+);
+const DominatorTreeHeader = createFactory(
+ require("devtools/client/memory/components/DominatorTreeHeader")
+);
+const TreeMap = createFactory(
+ require("devtools/client/memory/components/TreeMap")
+);
+const HSplitBox = createFactory(
+ require("devtools/client/shared/components/HSplitBox")
+);
+const Individuals = createFactory(
+ require("devtools/client/memory/components/Individuals")
+);
+const IndividualsHeader = createFactory(
+ require("devtools/client/memory/components/IndividualsHeader")
+);
+const ShortestPaths = createFactory(
+ require("devtools/client/memory/components/ShortestPaths")
+);
+const { getStatusTextFull, L10N } = require("devtools/client/memory/utils");
+const {
+ snapshotState: states,
+ diffingState,
+ viewState,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+const models = require("devtools/client/memory/models");
+const { snapshot: snapshotModel, diffingModel } = models;
+
+/**
+ * Get the app state's current state atom.
+ *
+ * @see the relevant state string constants in `../constants.js`.
+ *
+ * @param {models.view} view
+ * @param {snapshotModel} snapshot
+ * @param {diffingModel} diffing
+ * @param {individualsModel} individuals
+ *
+ * @return {snapshotState|diffingState|dominatorTreeState}
+ */
+function getState(view, snapshot, diffing, individuals) {
+ switch (view.state) {
+ case viewState.CENSUS:
+ return snapshot.census ? snapshot.census.state : snapshot.state;
+
+ case viewState.DIFFING:
+ return diffing.state;
+
+ case viewState.TREE_MAP:
+ return snapshot.treeMap ? snapshot.treeMap.state : snapshot.state;
+
+ case viewState.DOMINATOR_TREE:
+ return snapshot.dominatorTree
+ ? snapshot.dominatorTree.state
+ : snapshot.state;
+
+ case viewState.INDIVIDUALS:
+ return individuals.state;
+ }
+
+ assert(false, `Unexpected view state: ${view.state}`);
+ return null;
+}
+
+/**
+ * Return true if we should display a status message when we are in the given
+ * state. Return false otherwise.
+ *
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {models.view} view
+ * @param {snapshotModel} snapshot
+ *
+ * @returns {Boolean}
+ */
+function shouldDisplayStatus(state, view, snapshot) {
+ switch (state) {
+ case states.IMPORTING:
+ case states.SAVING:
+ case states.SAVED:
+ case states.READING:
+ case censusState.SAVING:
+ case treeMapState.SAVING:
+ case diffingState.SELECTING:
+ case diffingState.TAKING_DIFF:
+ case dominatorTreeState.COMPUTING:
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ case individualsState.FETCHING:
+ return true;
+ }
+ return view.state === viewState.DOMINATOR_TREE && !snapshot.dominatorTree;
+}
+
+/**
+ * Get the status text to display for the given state.
+ *
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {diffingModel} diffing
+ *
+ * @returns {String}
+ */
+function getStateStatusText(state, diffing) {
+ if (state === diffingState.SELECTING) {
+ return L10N.getStr(
+ diffing.firstSnapshotId === null
+ ? "diffing.prompt.selectBaseline"
+ : "diffing.prompt.selectComparison"
+ );
+ }
+
+ return getStatusTextFull(state);
+}
+
+/**
+ * Given that we should display a status message, return true if we should also
+ * display a throbber along with the status message. Return false otherwise.
+ *
+ * @param {diffingModel} diffing
+ *
+ * @returns {Boolean}
+ */
+function shouldDisplayThrobber(diffing) {
+ return !diffing || diffing.state !== diffingState.SELECTING;
+}
+
+/**
+ * Get the current state's error, or return null if there is none.
+ *
+ * @param {snapshotModel} snapshot
+ * @param {diffingModel} diffing
+ * @param {individualsModel} individuals
+ *
+ * @returns {Error|null}
+ */
+function getError(snapshot, diffing, individuals) {
+ if (diffing) {
+ if (diffing.state === diffingState.ERROR) {
+ return diffing.error;
+ }
+ if (diffing.census === censusState.ERROR) {
+ return diffing.census.error;
+ }
+ }
+
+ if (snapshot) {
+ if (snapshot.state === states.ERROR) {
+ return snapshot.error;
+ }
+
+ if (snapshot.census === censusState.ERROR) {
+ return snapshot.census.error;
+ }
+
+ if (snapshot.treeMap === treeMapState.ERROR) {
+ return snapshot.treeMap.error;
+ }
+
+ if (
+ snapshot.dominatorTree &&
+ snapshot.dominatorTree.state === dominatorTreeState.ERROR
+ ) {
+ return snapshot.dominatorTree.error;
+ }
+ }
+
+ if (individuals && individuals.state === individualsState.ERROR) {
+ return individuals.error;
+ }
+
+ return null;
+}
+
+/**
+ * Main view for the memory tool.
+ *
+ * The Heap component contains several panels for different states; an initial
+ * state of only a button to take a snapshot, loading states, the census view
+ * tree, the dominator tree, etc.
+ */
+class Heap extends Component {
+ static get propTypes() {
+ return {
+ onSnapshotClick: PropTypes.func.isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ onCensusExpand: PropTypes.func.isRequired,
+ onCensusCollapse: PropTypes.func.isRequired,
+ onDominatorTreeExpand: PropTypes.func.isRequired,
+ onDominatorTreeCollapse: PropTypes.func.isRequired,
+ onCensusFocus: PropTypes.func.isRequired,
+ onDominatorTreeFocus: PropTypes.func.isRequired,
+ onShortestPathsResize: PropTypes.func.isRequired,
+ snapshot: snapshotModel,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onPopView: PropTypes.func.isRequired,
+ individuals: models.individuals,
+ onViewIndividuals: PropTypes.func.isRequired,
+ onFocusIndividual: PropTypes.func.isRequired,
+ diffing: diffingModel,
+ view: models.view.isRequired,
+ sizes: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this._renderHeapView = this._renderHeapView.bind(this);
+ this._renderInitial = this._renderInitial.bind(this);
+ this._renderStatus = this._renderStatus.bind(this);
+ this._renderError = this._renderError.bind(this);
+ this._renderCensus = this._renderCensus.bind(this);
+ this._renderTreeMap = this._renderTreeMap.bind(this);
+ this._renderIndividuals = this._renderIndividuals.bind(this);
+ this._renderDominatorTree = this._renderDominatorTree.bind(this);
+ }
+
+ /**
+ * Render the heap view's container panel with the given contents inside of
+ * it.
+ *
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {...Any} contents
+ */
+ _renderHeapView(state, ...contents) {
+ return dom.div(
+ {
+ id: "heap-view",
+ "data-state": state,
+ },
+ dom.div(
+ {
+ className: "heap-view-panel",
+ "data-state": state,
+ },
+ ...contents
+ )
+ );
+ }
+
+ _renderInitial(onSnapshotClick) {
+ return this._renderHeapView(
+ "initial",
+ dom.button(
+ {
+ className: "devtools-button take-snapshot",
+ onClick: onSnapshotClick,
+ "data-standalone": true,
+ },
+ L10N.getStr("take-snapshot")
+ )
+ );
+ }
+
+ _renderStatus(state, statusText, diffing) {
+ let throbber = "";
+ if (shouldDisplayThrobber(diffing)) {
+ throbber = "devtools-throbber";
+ }
+
+ return this._renderHeapView(
+ state,
+ dom.span(
+ {
+ className: `snapshot-status ${throbber}`,
+ },
+ statusText
+ )
+ );
+ }
+
+ _renderError(state, statusText, error) {
+ return this._renderHeapView(
+ state,
+ dom.span({ className: "snapshot-status error" }, statusText),
+ dom.pre({}, safeErrorString(error))
+ );
+ }
+
+ _renderCensus(
+ state,
+ census,
+ diffing,
+ onViewSourceInDebugger,
+ onViewIndividuals
+ ) {
+ assert(
+ census.report,
+ "Should not render census that does not have a report"
+ );
+
+ if (!census.report.children) {
+ const censusFilterMsg = census.filter
+ ? L10N.getStr("heapview.none-match")
+ : L10N.getStr("heapview.empty");
+ const msg = diffing
+ ? L10N.getStr("heapview.no-difference")
+ : censusFilterMsg;
+ return this._renderHeapView(state, dom.div({ className: "empty" }, msg));
+ }
+
+ const contents = [];
+
+ if (
+ census.display.breakdown.by === "allocationStack" &&
+ census.report.children &&
+ census.report.children.length === 1 &&
+ census.report.children[0].name === "noStack"
+ ) {
+ contents.push(
+ dom.div(
+ { className: "error no-allocation-stacks" },
+ L10N.getStr("heapview.noAllocationStacks")
+ )
+ );
+ }
+
+ contents.push(CensusHeader({ diffing }));
+ contents.push(
+ Census({
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ diffing,
+ census,
+ onExpand: node => this.props.onCensusExpand(census, node),
+ onCollapse: node => this.props.onCensusCollapse(census, node),
+ onFocus: node => this.props.onCensusFocus(census, node),
+ })
+ );
+
+ return this._renderHeapView(state, ...contents);
+ }
+
+ _renderTreeMap(state, treeMap) {
+ return this._renderHeapView(state, TreeMap({ treeMap }));
+ }
+
+ _renderIndividuals(
+ state,
+ individuals,
+ dominatorTree,
+ onViewSourceInDebugger
+ ) {
+ assert(
+ individuals.state === individualsState.FETCHED,
+ "Should have fetched individuals"
+ );
+ assert(dominatorTree?.root, "Should have a dominator tree and its root");
+
+ const tree = dom.div(
+ {
+ className: "vbox",
+ style: {
+ overflowY: "auto",
+ },
+ },
+ IndividualsHeader(),
+ Individuals({
+ individuals,
+ dominatorTree,
+ onViewSourceInDebugger,
+ onFocus: this.props.onFocusIndividual,
+ })
+ );
+
+ const shortestPaths = ShortestPaths({
+ graph: individuals.focused ? individuals.focused.shortestPaths : null,
+ });
+
+ return this._renderHeapView(
+ state,
+ dom.div(
+ { className: "hbox devtools-toolbar" },
+ dom.label(
+ { id: "pop-view-button-label" },
+ dom.button(
+ {
+ id: "pop-view-button",
+ className: "devtools-button",
+ onClick: this.props.onPopView,
+ },
+ L10N.getStr("toolbar.pop-view")
+ ),
+ L10N.getStr("toolbar.pop-view.label")
+ ),
+ dom.span(
+ { className: "toolbar-text" },
+ L10N.getStr("toolbar.viewing-individuals")
+ )
+ ),
+ HSplitBox({
+ start: tree,
+ end: shortestPaths,
+ startWidth: this.props.sizes.shortestPathsSize,
+ onResize: this.props.onShortestPathsResize,
+ })
+ );
+ }
+
+ _renderDominatorTree(
+ state,
+ onViewSourceInDebugger,
+ dominatorTree,
+ onLoadMoreSiblings
+ ) {
+ const tree = dom.div(
+ {
+ className: "vbox",
+ style: {
+ overflowY: "auto",
+ },
+ },
+ DominatorTreeHeader(),
+ DominatorTree({
+ onViewSourceInDebugger,
+ dominatorTree,
+ onLoadMoreSiblings,
+ onExpand: this.props.onDominatorTreeExpand,
+ onCollapse: this.props.onDominatorTreeCollapse,
+ onFocus: this.props.onDominatorTreeFocus,
+ })
+ );
+
+ const shortestPaths = ShortestPaths({
+ graph: dominatorTree.focused ? dominatorTree.focused.shortestPaths : null,
+ });
+
+ return this._renderHeapView(
+ state,
+ HSplitBox({
+ start: tree,
+ end: shortestPaths,
+ startWidth: this.props.sizes.shortestPathsSize,
+ onResize: this.props.onShortestPathsResize,
+ })
+ );
+ }
+
+ render() {
+ const {
+ snapshot,
+ diffing,
+ onSnapshotClick,
+ onLoadMoreSiblings,
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ individuals,
+ view,
+ } = this.props;
+
+ if (!diffing && !snapshot && !individuals) {
+ return this._renderInitial(onSnapshotClick);
+ }
+
+ const state = getState(view, snapshot, diffing, individuals);
+ const statusText = getStateStatusText(state, diffing);
+
+ if (shouldDisplayStatus(state, view, snapshot)) {
+ return this._renderStatus(state, statusText, diffing);
+ }
+
+ const error = getError(snapshot, diffing, individuals);
+ if (error) {
+ return this._renderError(state, statusText, error);
+ }
+
+ if (view.state === viewState.CENSUS || view.state === viewState.DIFFING) {
+ const census =
+ view.state === viewState.CENSUS ? snapshot.census : diffing.census;
+ if (!census) {
+ return this._renderStatus(state, statusText, diffing);
+ }
+ return this._renderCensus(
+ state,
+ census,
+ diffing,
+ onViewSourceInDebugger,
+ onViewIndividuals
+ );
+ }
+
+ if (view.state === viewState.TREE_MAP) {
+ return this._renderTreeMap(state, snapshot.treeMap);
+ }
+
+ if (view.state === viewState.INDIVIDUALS) {
+ assert(
+ individuals.state === individualsState.FETCHED,
+ "Should have fetched the individuals -- " +
+ "other states are rendered as statuses"
+ );
+ return this._renderIndividuals(
+ state,
+ individuals,
+ individuals.dominatorTree,
+ onViewSourceInDebugger
+ );
+ }
+
+ assert(
+ view.state === viewState.DOMINATOR_TREE,
+ "If we aren't in progress, looking at a census, or diffing, then we " +
+ "must be looking at a dominator tree"
+ );
+ assert(!diffing, "Should not have diffing");
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+
+ return this._renderDominatorTree(
+ state,
+ onViewSourceInDebugger,
+ snapshot.dominatorTree,
+ onLoadMoreSiblings
+ );
+ }
+}
+
+module.exports = Heap;
diff --git a/devtools/client/memory/components/Individuals.js b/devtools/client/memory/components/Individuals.js
new file mode 100644
index 0000000000..dcf3946322
--- /dev/null
+++ b/devtools/client/memory/components/Individuals.js
@@ -0,0 +1,72 @@
+/* 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/. */
+
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Tree = createFactory(
+ require("devtools/client/shared/components/VirtualizedTree")
+);
+const DominatorTreeItem = createFactory(
+ require("devtools/client/memory/components/DominatorTreeItem")
+);
+const { TREE_ROW_HEIGHT } = require("devtools/client/memory/constants");
+const models = require("devtools/client/memory/models");
+
+/**
+ * The list of individuals in a census group.
+ */
+class Individuals extends Component {
+ static get propTypes() {
+ return {
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ individuals: models.individuals,
+ dominatorTree: models.dominatorTreeModel,
+ };
+ }
+
+ render() {
+ const {
+ individuals,
+ dominatorTree,
+ onViewSourceInDebugger,
+ onFocus,
+ } = this.props;
+
+ return Tree({
+ key: "individuals-tree",
+ autoExpandDepth: 0,
+ preventNavigationOnArrowRight: false,
+ focused: individuals.focused,
+ getParent: node => null,
+ getChildren: node => [],
+ isExpanded: node => false,
+ onExpand: () => {},
+ onCollapse: () => {},
+ onFocus,
+ renderItem: (item, depth, focused, _, expanded) => {
+ return DominatorTreeItem({
+ item,
+ depth,
+ focused,
+ arrow: undefined,
+ expanded,
+ getPercentSize: size =>
+ (size / dominatorTree.root.retainedSize) * 100,
+ onViewSourceInDebugger,
+ });
+ },
+ getRoots: () => individuals.nodes,
+ getKey: node => node.nodeId,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+}
+
+module.exports = Individuals;
diff --git a/devtools/client/memory/components/IndividualsHeader.js b/devtools/client/memory/components/IndividualsHeader.js
new file mode 100644
index 0000000000..e63cff7a17
--- /dev/null
+++ b/devtools/client/memory/components/IndividualsHeader.js
@@ -0,0 +1,49 @@
+/* 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/. */
+
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { L10N } = require("devtools/client/memory/utils");
+
+class IndividualsHeader extends Component {
+ static get propTypes() {
+ return {};
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "header",
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.retainedSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.retainedSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.shallowSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.shallowSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-name",
+ title: L10N.getStr("individuals.field.node.tooltip"),
+ },
+ L10N.getStr("individuals.field.node")
+ )
+ );
+ }
+}
+
+module.exports = IndividualsHeader;
diff --git a/devtools/client/memory/components/List.js b/devtools/client/memory/components/List.js
new file mode 100644
index 0000000000..45c727eb07
--- /dev/null
+++ b/devtools/client/memory/components/List.js
@@ -0,0 +1,44 @@
+/* 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/. */
+
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+/**
+ * Generic list component that takes another react component to represent
+ * the children nodes as `itemComponent`, and a list of items to render
+ * as that component with a click handler.
+ */
+class List extends Component {
+ static get propTypes() {
+ return {
+ itemComponent: PropTypes.any.isRequired,
+ onClick: PropTypes.func,
+ items: PropTypes.array.isRequired,
+ };
+ }
+
+ render() {
+ const { items, onClick, itemComponent: Item } = this.props;
+
+ return dom.ul(
+ { className: "list" },
+ ...items.map((item, index) => {
+ return Item(
+ Object.assign({}, this.props, {
+ key: index,
+ item,
+ index,
+ onClick: () => onClick(item),
+ })
+ );
+ })
+ );
+ }
+}
+
+module.exports = List;
diff --git a/devtools/client/memory/components/ShortestPaths.js b/devtools/client/memory/components/ShortestPaths.js
new file mode 100644
index 0000000000..9779e23501
--- /dev/null
+++ b/devtools/client/memory/components/ShortestPaths.js
@@ -0,0 +1,192 @@
+/* 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/. */
+
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const { L10N } = require("devtools/client/memory/utils");
+
+const GRAPH_DEFAULTS = {
+ translate: [20, 20],
+ scale: 1,
+};
+
+const NO_STACK = "noStack";
+const NO_FILENAME = "noFilename";
+const ROOT_LIST = "JS::ubi::RootList";
+
+function stringifyLabel(label, id) {
+ const sanitized = [];
+
+ for (let i = 0, length = label.length; i < length; i++) {
+ const piece = label[i];
+
+ if (isSavedFrame(piece)) {
+ const { short } = getSourceNames(piece.source);
+ sanitized[i] =
+ `${piece.functionDisplayName} @ ` +
+ `${short}:${piece.line}:${piece.column}`;
+ } else if (piece === NO_STACK) {
+ sanitized[i] = L10N.getStr("tree-item.nostack");
+ } else if (piece === NO_FILENAME) {
+ sanitized[i] = L10N.getStr("tree-item.nofilename");
+ } else if (piece === ROOT_LIST) {
+ // Don't use the usual labeling machinery for root lists: replace it
+ // with the "GC Roots" string.
+ sanitized.splice(0, label.length);
+ sanitized.push(L10N.getStr("tree-item.rootlist"));
+ break;
+ } else {
+ sanitized[i] = "" + piece;
+ }
+ }
+
+ return `${sanitized.join(" › ")} @ 0x${id.toString(16)}`;
+}
+
+class ShortestPaths extends Component {
+ static get propTypes() {
+ return {
+ graph: PropTypes.shape({
+ nodes: PropTypes.arrayOf(PropTypes.object),
+ edges: PropTypes.arrayOf(PropTypes.object),
+ }),
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = { zoom: null };
+ this._renderGraph = this._renderGraph.bind(this);
+ }
+
+ componentDidMount() {
+ if (this.props.graph) {
+ this._renderGraph(this.refs.container, this.props.graph);
+ }
+ }
+
+ shouldComponentUpdate(nextProps) {
+ return this.props.graph != nextProps.graph;
+ }
+
+ componentDidUpdate() {
+ if (this.props.graph) {
+ this._renderGraph(this.refs.container, this.props.graph);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.state.zoom) {
+ this.state.zoom.on("zoom", null);
+ }
+ }
+
+ _renderGraph(container, { nodes, edges }) {
+ if (!container.firstChild) {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("id", "graph-svg");
+ svg.setAttribute("xlink", "http://www.w3.org/1999/xlink");
+ svg.style.width = "100%";
+ svg.style.height = "100%";
+
+ const target = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ target.setAttribute("id", "graph-target");
+ target.style.width = "100%";
+ target.style.height = "100%";
+
+ svg.appendChild(target);
+ container.appendChild(svg);
+ }
+
+ const graph = new dagreD3.Digraph();
+
+ for (let i = 0; i < nodes.length; i++) {
+ graph.addNode(nodes[i].id, {
+ id: nodes[i].id,
+ label: stringifyLabel(nodes[i].label, nodes[i].id),
+ });
+ }
+
+ for (let i = 0; i < edges.length; i++) {
+ graph.addEdge(null, edges[i].from, edges[i].to, {
+ label: edges[i].name,
+ });
+ }
+
+ const renderer = new dagreD3.Renderer();
+ renderer.drawNodes();
+ renderer.drawEdgePaths();
+
+ const svg = d3.select("#graph-svg");
+ const target = d3.select("#graph-target");
+
+ let zoom = this.state.zoom;
+ if (!zoom) {
+ zoom = d3.behavior.zoom().on("zoom", function() {
+ target.attr(
+ "transform",
+ `translate(${d3.event.translate}) scale(${d3.event.scale})`
+ );
+ });
+ svg.call(zoom);
+ this.setState({ zoom });
+ }
+
+ const { translate, scale } = GRAPH_DEFAULTS;
+ zoom.scale(scale);
+ zoom.translate(translate);
+ target.attr("transform", `translate(${translate}) scale(${scale})`);
+
+ const layout = dagreD3.layout();
+ renderer.layout(layout).run(graph, target);
+ }
+
+ render() {
+ let contents;
+ if (this.props.graph) {
+ // Let the componentDidMount or componentDidUpdate method draw the graph
+ // with DagreD3. We just provide the container for the graph here.
+ contents = dom.div({
+ ref: "container",
+ style: {
+ flex: 1,
+ height: "100%",
+ width: "100%",
+ },
+ });
+ } else {
+ contents = dom.div(
+ {
+ id: "shortest-paths-select-node-msg",
+ },
+ L10N.getStr("shortest-paths.select-node")
+ );
+ }
+
+ return dom.div(
+ {
+ id: "shortest-paths",
+ className: "vbox",
+ },
+ dom.label(
+ {
+ id: "shortest-paths-header",
+ className: "header",
+ },
+ L10N.getStr("shortest-paths.header")
+ ),
+ contents
+ );
+ }
+}
+
+module.exports = ShortestPaths;
diff --git a/devtools/client/memory/components/SnapshotListItem.js b/devtools/client/memory/components/SnapshotListItem.js
new file mode 100644
index 0000000000..23768da246
--- /dev/null
+++ b/devtools/client/memory/components/SnapshotListItem.js
@@ -0,0 +1,138 @@
+/* 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/. */
+
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const {
+ L10N,
+ getSnapshotTitle,
+ getSnapshotTotals,
+ getStatusText,
+ snapshotIsDiffable,
+ getSavedCensus,
+} = require("devtools/client/memory/utils");
+const { diffingState } = require("devtools/client/memory/constants");
+const {
+ snapshot: snapshotModel,
+ app: appModel,
+} = require("devtools/client/memory/models");
+
+class SnapshotListItem extends Component {
+ static get propTypes() {
+ return {
+ onClick: PropTypes.func.isRequired,
+ onSave: PropTypes.func.isRequired,
+ onDelete: PropTypes.func.isRequired,
+ item: snapshotModel.isRequired,
+ index: PropTypes.number.isRequired,
+ diffing: appModel.diffing,
+ };
+ }
+
+ render() {
+ const { item: snapshot, onClick, onSave, onDelete, diffing } = this.props;
+ let className = `snapshot-list-item ${
+ snapshot.selected ? " selected" : ""
+ }`;
+ let statusText = getStatusText(snapshot.state);
+ let wantThrobber = !!statusText;
+ const title = getSnapshotTitle(snapshot);
+
+ const selectedForDiffing =
+ diffing &&
+ (diffing.firstSnapshotId === snapshot.id ||
+ diffing.secondSnapshotId === snapshot.id);
+
+ let checkbox;
+ if (diffing && snapshotIsDiffable(snapshot)) {
+ if (diffing.state === diffingState.SELECTING) {
+ wantThrobber = false;
+ }
+
+ const checkboxAttrs = {
+ type: "checkbox",
+ checked: false,
+ };
+
+ if (selectedForDiffing) {
+ checkboxAttrs.checked = true;
+ checkboxAttrs.disabled = true;
+ className += " selected";
+ statusText = L10N.getStr(
+ diffing.firstSnapshotId === snapshot.id
+ ? "diffing.baseline"
+ : "diffing.comparison"
+ );
+ }
+
+ if (selectedForDiffing || diffing.state == diffingState.SELECTING) {
+ checkbox = dom.input(checkboxAttrs);
+ }
+ }
+
+ let details;
+ if (!selectedForDiffing) {
+ // See if a tree map or census is in the read state.
+ const census = getSavedCensus(snapshot);
+
+ // If there is census data, fill in the total bytes.
+ if (census) {
+ const { bytes } = getSnapshotTotals(census);
+ const formatBytes = L10N.getFormatStr(
+ "aggregate.mb",
+ L10N.numberWithDecimals(bytes / 1000000, 2)
+ );
+
+ details = dom.span(
+ { className: "snapshot-totals" },
+ dom.span({ className: "total-bytes" }, formatBytes)
+ );
+ }
+ }
+ if (!details) {
+ details = dom.span({ className: "snapshot-state" }, statusText);
+ }
+
+ const saveLink = !snapshot.path
+ ? void 0
+ : dom.a(
+ {
+ onClick: () => onSave(snapshot),
+ className: "save",
+ },
+ L10N.getStr("snapshot.io.save")
+ );
+
+ const deleteButton = !snapshot.path
+ ? void 0
+ : dom.button({
+ onClick: event => {
+ event.stopPropagation();
+ onDelete(snapshot);
+ },
+ className: "delete",
+ title: L10N.getStr("snapshot.io.delete"),
+ });
+
+ return dom.li(
+ { className, onClick },
+ dom.span(
+ {
+ className: `snapshot-title ${
+ wantThrobber ? " devtools-throbber" : ""
+ }`,
+ },
+ checkbox,
+ title,
+ deleteButton
+ ),
+ dom.span({ className: "snapshot-info" }, details, saveLink)
+ );
+ }
+}
+
+module.exports = SnapshotListItem;
diff --git a/devtools/client/memory/components/Toolbar.js b/devtools/client/memory/components/Toolbar.js
new file mode 100644
index 0000000000..6f8883e944
--- /dev/null
+++ b/devtools/client/memory/components/Toolbar.js
@@ -0,0 +1,307 @@
+/* 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/. */
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { L10N } = require("devtools/client/memory/utils");
+const models = require("devtools/client/memory/models");
+const { viewState } = require("devtools/client/memory/constants");
+
+class Toolbar extends Component {
+ static get propTypes() {
+ return {
+ censusDisplays: PropTypes.arrayOf(models.censusDisplay).isRequired,
+ censusDisplay: models.censusDisplay.isRequired,
+ onTakeSnapshotClick: PropTypes.func.isRequired,
+ onImportClick: PropTypes.func.isRequired,
+ onClearSnapshotsClick: PropTypes.func.isRequired,
+ onCensusDisplayChange: PropTypes.func.isRequired,
+ onToggleRecordAllocationStacks: PropTypes.func.isRequired,
+ allocations: models.allocations,
+ filterString: PropTypes.string,
+ setFilterString: PropTypes.func.isRequired,
+ diffing: models.diffingModel,
+ onToggleDiffing: PropTypes.func.isRequired,
+ view: models.view.isRequired,
+ onViewChange: PropTypes.func.isRequired,
+ labelDisplays: PropTypes.arrayOf(models.labelDisplay).isRequired,
+ labelDisplay: models.labelDisplay.isRequired,
+ onLabelDisplayChange: PropTypes.func.isRequired,
+ treeMapDisplays: PropTypes.arrayOf(models.treeMapDisplay).isRequired,
+ onTreeMapDisplayChange: PropTypes.func.isRequired,
+ snapshots: PropTypes.arrayOf(models.snapshot).isRequired,
+ };
+ }
+
+ render() {
+ const {
+ onTakeSnapshotClick,
+ onImportClick,
+ onClearSnapshotsClick,
+ onCensusDisplayChange,
+ censusDisplays,
+ censusDisplay,
+ labelDisplays,
+ labelDisplay,
+ onLabelDisplayChange,
+ treeMapDisplays,
+ onTreeMapDisplayChange,
+ onToggleRecordAllocationStacks,
+ allocations,
+ filterString,
+ setFilterString,
+ snapshots,
+ diffing,
+ onToggleDiffing,
+ view,
+ onViewChange,
+ } = this.props;
+
+ let viewToolbarOptions;
+ if (view.state == viewState.CENSUS || view.state === viewState.DIFFING) {
+ viewToolbarOptions = dom.div(
+ {
+ className: "toolbar-group",
+ },
+
+ dom.label(
+ {
+ className: "display-by",
+ title: L10N.getStr("toolbar.displayBy.tooltip"),
+ },
+ L10N.getStr("toolbar.displayBy"),
+ dom.select(
+ {
+ id: "select-display",
+ className: "devtools-toolbar-select select-display",
+ onChange: e => {
+ const newDisplay = censusDisplays.find(
+ b => b.displayName === e.target.value
+ );
+ onCensusDisplayChange(newDisplay);
+ },
+ value: censusDisplay.displayName,
+ },
+ censusDisplays.map(({ tooltip, displayName }) =>
+ dom.option(
+ {
+ key: `display-${displayName}`,
+ value: displayName,
+ title: tooltip,
+ },
+ displayName
+ )
+ )
+ )
+ ),
+
+ dom.span({ className: "devtools-separator" }),
+
+ dom.input({
+ id: "filter",
+ type: "search",
+ className: "devtools-filterinput",
+ placeholder: L10N.getStr("filter.placeholder"),
+ title: L10N.getStr("filter.tooltip"),
+ onChange: event => setFilterString(event.target.value),
+ value: filterString || undefined,
+ })
+ );
+ } else if (view.state == viewState.TREE_MAP) {
+ assert(
+ treeMapDisplays.length >= 1,
+ "Should always have at least one tree map display"
+ );
+
+ // Only show the dropdown if there are multiple display options
+ viewToolbarOptions =
+ treeMapDisplays.length > 1
+ ? dom.div(
+ {
+ className: "toolbar-group",
+ },
+
+ dom.label(
+ {
+ className: "display-by",
+ title: L10N.getStr("toolbar.displayBy.tooltip"),
+ },
+ L10N.getStr("toolbar.displayBy"),
+ dom.select(
+ {
+ id: "select-tree-map-display",
+ onChange: e => {
+ const newDisplay = treeMapDisplays.find(
+ b => b.displayName === e.target.value
+ );
+ onTreeMapDisplayChange(newDisplay);
+ },
+ },
+ treeMapDisplays.map(({ tooltip, displayName }) =>
+ dom.option(
+ {
+ key: `tree-map-display-${displayName}`,
+ value: displayName,
+ title: tooltip,
+ },
+ displayName
+ )
+ )
+ )
+ )
+ )
+ : null;
+ } else {
+ assert(
+ view.state === viewState.DOMINATOR_TREE ||
+ view.state === viewState.INDIVIDUALS
+ );
+
+ viewToolbarOptions = dom.div(
+ {
+ className: "toolbar-group",
+ },
+
+ dom.label(
+ {
+ className: "label-by",
+ title: L10N.getStr("toolbar.labelBy.tooltip"),
+ },
+ L10N.getStr("toolbar.labelBy"),
+ dom.select(
+ {
+ id: "select-label-display",
+ className: "devtools-toolbar-select select-label-display",
+ onChange: e => {
+ const newDisplay = labelDisplays.find(
+ b => b.displayName === e.target.value
+ );
+ onLabelDisplayChange(newDisplay);
+ },
+ value: labelDisplay.displayName,
+ },
+ labelDisplays.map(({ tooltip, displayName }) =>
+ dom.option(
+ {
+ key: `label-display-${displayName}`,
+ value: displayName,
+ title: tooltip,
+ },
+ displayName
+ )
+ )
+ )
+ )
+ );
+ }
+
+ let viewSelect;
+ if (
+ view.state !== viewState.DIFFING &&
+ view.state !== viewState.INDIVIDUALS
+ ) {
+ viewSelect = dom.label(
+ {
+ title: L10N.getStr("toolbar.view.tooltip"),
+ },
+ L10N.getStr("toolbar.view"),
+ dom.select(
+ {
+ id: "select-view",
+ className: "devtools-toolbar-select select-view",
+ onChange: e => onViewChange(e.target.value),
+ value: view.state,
+ },
+ dom.option(
+ {
+ value: viewState.TREE_MAP,
+ title: L10N.getStr("toolbar.view.treemap.tooltip"),
+ },
+ L10N.getStr("toolbar.view.treemap")
+ ),
+ dom.option(
+ {
+ value: viewState.CENSUS,
+ title: L10N.getStr("toolbar.view.census.tooltip"),
+ },
+ L10N.getStr("toolbar.view.census")
+ ),
+ dom.option(
+ {
+ value: viewState.DOMINATOR_TREE,
+ title: L10N.getStr("toolbar.view.dominators.tooltip"),
+ },
+ L10N.getStr("toolbar.view.dominators")
+ )
+ )
+ );
+ }
+
+ return dom.div(
+ {
+ className: "devtools-toolbar",
+ },
+
+ dom.div(
+ {
+ className: "toolbar-group",
+ },
+
+ dom.button({
+ id: "clear-snapshots",
+ className: "clear-snapshots devtools-button",
+ disabled: !snapshots.length,
+ onClick: onClearSnapshotsClick,
+ title: L10N.getStr("clear-snapshots.tooltip"),
+ }),
+
+ dom.button({
+ id: "take-snapshot",
+ className: "take-snapshot devtools-button",
+ onClick: onTakeSnapshotClick,
+ title: L10N.getStr("take-snapshot"),
+ }),
+
+ dom.button({
+ id: "diff-snapshots",
+ className:
+ "devtools-button devtools-monospace" + (diffing ? " checked" : ""),
+ disabled: snapshots.length < 2,
+ onClick: onToggleDiffing,
+ title: L10N.getStr("diff-snapshots.tooltip"),
+ }),
+
+ dom.button({
+ id: "import-snapshot",
+ className: "import-snapshot devtools-button",
+ onClick: onImportClick,
+ title: L10N.getStr("import-snapshot"),
+ })
+ ),
+
+ dom.label(
+ {
+ id: "record-allocation-stacks-label",
+ title: L10N.getStr("checkbox.recordAllocationStacks.tooltip"),
+ },
+ dom.input({
+ id: "record-allocation-stacks-checkbox",
+ type: "checkbox",
+ checked: allocations.recording,
+ disabled: allocations.togglingInProgress,
+ onChange: onToggleRecordAllocationStacks,
+ }),
+ L10N.getStr("checkbox.recordAllocationStacks")
+ ),
+
+ viewSelect,
+ viewToolbarOptions
+ );
+ }
+}
+
+module.exports = Toolbar;
diff --git a/devtools/client/memory/components/TreeMap.js b/devtools/client/memory/components/TreeMap.js
new file mode 100644
index 0000000000..edb188294d
--- /dev/null
+++ b/devtools/client/memory/components/TreeMap.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { treeMapModel } = require("devtools/client/memory/models");
+const startVisualization = require("devtools/client/memory/components/tree-map/start");
+
+class TreeMap extends Component {
+ static get propTypes() {
+ return {
+ treeMap: treeMapModel,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {};
+ this._stopVisualization = this._stopVisualization.bind(this);
+ this._startVisualization = this._startVisualization.bind(this);
+ }
+
+ componentDidMount() {
+ const { treeMap } = this.props;
+ if (treeMap?.report) {
+ this._startVisualization();
+ }
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const oldTreeMap = this.props.treeMap;
+ const newTreeMap = nextProps.treeMap;
+ return oldTreeMap !== newTreeMap;
+ }
+
+ componentDidUpdate(prevProps) {
+ this._stopVisualization();
+
+ if (this.props.treeMap && this.props.treeMap.report) {
+ this._startVisualization();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.state.stopVisualization) {
+ this.state.stopVisualization();
+ }
+ }
+
+ _stopVisualization() {
+ if (this.state.stopVisualization) {
+ this.state.stopVisualization();
+ this.setState({ stopVisualization: null });
+ }
+ }
+
+ _startVisualization() {
+ const { container } = this.refs;
+ const { report } = this.props.treeMap;
+ const stopVisualization = startVisualization(container, report);
+ this.setState({ stopVisualization });
+ }
+
+ render() {
+ return dom.div({
+ ref: "container",
+ className: "tree-map-container",
+ });
+ }
+}
+
+module.exports = TreeMap;
diff --git a/devtools/client/memory/components/moz.build b/devtools/client/memory/components/moz.build
new file mode 100644
index 0000000000..82739bf97f
--- /dev/null
+++ b/devtools/client/memory/components/moz.build
@@ -0,0 +1,25 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "tree-map",
+]
+
+DevToolsModules(
+ "Census.js",
+ "CensusHeader.js",
+ "CensusTreeItem.js",
+ "DominatorTree.js",
+ "DominatorTreeHeader.js",
+ "DominatorTreeItem.js",
+ "Heap.js",
+ "Individuals.js",
+ "IndividualsHeader.js",
+ "List.js",
+ "ShortestPaths.js",
+ "SnapshotListItem.js",
+ "Toolbar.js",
+ "TreeMap.js",
+)
diff --git a/devtools/client/memory/components/tree-map/canvas-utils.js b/devtools/client/memory/components/tree-map/canvas-utils.js
new file mode 100644
index 0000000000..bbafb4a5fc
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/canvas-utils.js
@@ -0,0 +1,132 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+/**
+ * Create 2 canvases and contexts for drawing onto, 1 main canvas, and 1 zoom
+ * canvas. The main canvas dimensions match the parent div, but the CSS can be
+ * transformed to be zoomed and dragged around (potentially creating a blurry
+ * canvas once zoomed in). The zoom canvas is a zoomed in section that matches
+ * the parent div's dimensions and is kept in place through CSS. A zoomed in
+ * view of the visualization is drawn onto this canvas, providing a crisp zoomed
+ * in view of the tree map.
+ */
+const { debounce } = require("devtools/shared/debounce");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const FULLSCREEN_STYLE = {
+ width: "100%",
+ height: "100%",
+ position: "absolute",
+};
+
+/**
+ * Create the canvases, resize handlers, and return references to them all
+ *
+ * @param {HTMLDivElement} parentEl
+ * @param {Number} debounceRate
+ * @return {Object}
+ */
+function Canvases(parentEl, debounceRate) {
+ EventEmitter.decorate(this);
+ this.container = createContainingDiv(parentEl);
+
+ // This canvas contains all of the treemap
+ this.main = createCanvas(this.container, "main");
+ // This canvas contains only the zoomed in portion, overlaying the main canvas
+ this.zoom = createCanvas(this.container, "zoom");
+
+ this.removeHandlers = handleResizes(this, debounceRate);
+}
+
+Canvases.prototype = {
+ /**
+ * Remove the handlers and elements
+ *
+ * @return {type} description
+ */
+ destroy: function() {
+ this.removeHandlers();
+ this.container.removeChild(this.main.canvas);
+ this.container.removeChild(this.zoom.canvas);
+ },
+};
+
+module.exports = Canvases;
+
+/**
+ * Create the containing div
+ *
+ * @param {HTMLDivElement} parentEl
+ * @return {HTMLDivElement}
+ */
+function createContainingDiv(parentEl) {
+ const div = parentEl.ownerDocument.createElementNS(HTML_NS, "div");
+ Object.assign(div.style, FULLSCREEN_STYLE);
+ parentEl.appendChild(div);
+ return div;
+}
+
+/**
+ * Create a canvas and context
+ *
+ * @param {HTMLDivElement} container
+ * @param {String} className
+ * @return {Object} { canvas, ctx }
+ */
+function createCanvas(container, className) {
+ const window = container.ownerDocument.defaultView;
+ const canvas = container.ownerDocument.createElementNS(HTML_NS, "canvas");
+ container.appendChild(canvas);
+ canvas.width = container.offsetWidth * window.devicePixelRatio;
+ canvas.height = container.offsetHeight * window.devicePixelRatio;
+ canvas.className = className;
+
+ Object.assign(canvas.style, FULLSCREEN_STYLE, {
+ pointerEvents: "none",
+ });
+
+ const ctx = canvas.getContext("2d");
+
+ return { canvas, ctx };
+}
+
+/**
+ * Resize the canvases' resolutions, and fires out the onResize callback
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} canvases
+ * @param {Number} debounceRate
+ */
+function handleResizes(canvases, debounceRate) {
+ const { container, main, zoom } = canvases;
+ const window = container.ownerDocument.defaultView;
+
+ function resize() {
+ const width = container.offsetWidth * window.devicePixelRatio;
+ const height = container.offsetHeight * window.devicePixelRatio;
+
+ main.canvas.width = width;
+ main.canvas.height = height;
+ zoom.canvas.width = width;
+ zoom.canvas.height = height;
+
+ canvases.emit("resize");
+ }
+
+ // Tests may not need debouncing
+ const debouncedResize =
+ debounceRate > 0 ? debounce(resize, debounceRate) : resize;
+
+ window.addEventListener("resize", debouncedResize);
+ resize();
+
+ return function removeResizeHandlers() {
+ window.removeEventListener("resize", debouncedResize);
+ };
+}
diff --git a/devtools/client/memory/components/tree-map/color-coarse-type.js b/devtools/client/memory/components/tree-map/color-coarse-type.js
new file mode 100644
index 0000000000..b511f9f50e
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/color-coarse-type.js
@@ -0,0 +1,70 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Color the boxes in the treemap
+ */
+
+const TYPES = ["objects", "other", "strings", "scripts", "domNode"];
+
+// The factors determine how much the hue shifts
+const TYPE_FACTOR = TYPES.length * 3;
+const DEPTH_FACTOR = -10;
+const H = 0.5;
+const S = 0.6;
+const L = 0.9;
+
+/**
+ * Recursively find the index of the coarse type of a node
+ *
+ * @param {Object} node
+ * d3 treemap
+ * @return {Integer}
+ * index
+ */
+function findCoarseTypeIndex(node) {
+ const index = TYPES.indexOf(node.name);
+
+ if (node.parent) {
+ return index === -1 ? findCoarseTypeIndex(node.parent) : index;
+ }
+
+ return TYPES.indexOf("other");
+}
+
+/**
+ * Decide a color value for depth to be used in the HSL computation
+ *
+ * @param {Object} node
+ * @return {Number}
+ */
+function depthColorFactor(node) {
+ return Math.min(1, node.depth / DEPTH_FACTOR);
+}
+
+/**
+ * Decide a color value for type to be used in the HSL computation
+ *
+ * @param {Object} node
+ * @return {Number}
+ */
+function typeColorFactor(node) {
+ return findCoarseTypeIndex(node) / TYPE_FACTOR;
+}
+
+/**
+ * Color a node
+ *
+ * @param {Object} node
+ * @return {Array} HSL values ranged 0-1
+ */
+module.exports = function colorCoarseType(node) {
+ const h = Math.min(1, H + typeColorFactor(node));
+ const s = Math.min(1, S);
+ const l = Math.min(1, L + depthColorFactor(node));
+
+ return [h, s, l];
+};
diff --git a/devtools/client/memory/components/tree-map/drag-zoom.js b/devtools/client/memory/components/tree-map/drag-zoom.js
new file mode 100644
index 0000000000..066c6f055f
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/drag-zoom.js
@@ -0,0 +1,337 @@
+/* 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/. */
+
+"use strict";
+
+const { debounce } = require("devtools/shared/debounce");
+const { lerp } = require("devtools/client/memory/utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const LERP_SPEED = 0.5;
+const ZOOM_SPEED = 0.01;
+const TRANSLATE_EPSILON = 1;
+const ZOOM_EPSILON = 0.001;
+const LINE_SCROLL_MODE = 1;
+const SCROLL_LINE_SIZE = 15;
+
+/**
+ * DragZoom is a constructor that contains the state of the current dragging and
+ * zooming behavior. It sets the scrolling and zooming behaviors.
+ *
+ * @param {HTMLElement} container description
+ * The container for the canvases
+ */
+function DragZoom(container, debounceRate, requestAnimationFrame) {
+ EventEmitter.decorate(this);
+
+ this.isDragging = false;
+
+ // The current mouse position
+ this.mouseX = container.offsetWidth / 2;
+ this.mouseY = container.offsetHeight / 2;
+
+ // The total size of the visualization after being zoomed, in pixels
+ this.zoomedWidth = container.offsetWidth;
+ this.zoomedHeight = container.offsetHeight;
+
+ // How much the visualization has been zoomed in
+ this.zoom = 0;
+
+ // The offset of visualization from the container. This is applied after
+ // the zoom, and the visualization by default is centered
+ this.translateX = 0;
+ this.translateY = 0;
+
+ // The size of the offset between the top/left of the container, and the
+ // top/left of the containing element. This value takes into account
+ // the device pixel ratio for canvas draws.
+ this.offsetX = 0;
+ this.offsetY = 0;
+
+ // The smoothed values that are animated and eventually match the target
+ // values. The values are updated by the update loop
+ this.smoothZoom = 0;
+ this.smoothTranslateX = 0;
+ this.smoothTranslateY = 0;
+
+ // Add the constant values for testing purposes
+ this.ZOOM_SPEED = ZOOM_SPEED;
+ this.ZOOM_EPSILON = ZOOM_EPSILON;
+
+ const update = createUpdateLoop(container, this, requestAnimationFrame);
+
+ this.destroy = setHandlers(this, container, update, debounceRate);
+}
+
+module.exports = DragZoom;
+
+/**
+ * Returns an update loop. This loop smoothly updates the visualization when
+ * actions are performed. Once the animations have reached their target values
+ * the animation loop is stopped.
+ *
+ * Any value in the `dragZoom` object that starts with "smooth" is the
+ * smoothed version of a value that is interpolating toward the target value.
+ * For instance `dragZoom.smoothZoom` approaches `dragZoom.zoom` on each
+ * iteration of the update loop until it's sufficiently close as defined by
+ * the epsilon values.
+ *
+ * Only these smoothed values and the container CSS are updated by the loop.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ * The values that represent the current dragZoom state
+ * @param {Function} requestAnimationFrame
+ */
+function createUpdateLoop(container, dragZoom, requestAnimationFrame) {
+ let isLooping = false;
+
+ function update() {
+ const isScrollChanging =
+ Math.abs(dragZoom.smoothZoom - dragZoom.zoom) > ZOOM_EPSILON;
+ const isTranslateChanging =
+ Math.abs(dragZoom.smoothTranslateX - dragZoom.translateX) >
+ TRANSLATE_EPSILON ||
+ Math.abs(dragZoom.smoothTranslateY - dragZoom.translateY) >
+ TRANSLATE_EPSILON;
+
+ isLooping = isScrollChanging || isTranslateChanging;
+
+ if (isScrollChanging) {
+ dragZoom.smoothZoom = lerp(
+ dragZoom.smoothZoom,
+ dragZoom.zoom,
+ LERP_SPEED
+ );
+ } else {
+ dragZoom.smoothZoom = dragZoom.zoom;
+ }
+
+ if (isTranslateChanging) {
+ dragZoom.smoothTranslateX = lerp(
+ dragZoom.smoothTranslateX,
+ dragZoom.translateX,
+ LERP_SPEED
+ );
+ dragZoom.smoothTranslateY = lerp(
+ dragZoom.smoothTranslateY,
+ dragZoom.translateY,
+ LERP_SPEED
+ );
+ } else {
+ dragZoom.smoothTranslateX = dragZoom.translateX;
+ dragZoom.smoothTranslateY = dragZoom.translateY;
+ }
+
+ const zoom = 1 + dragZoom.smoothZoom;
+ const x = dragZoom.smoothTranslateX;
+ const y = dragZoom.smoothTranslateY;
+ container.style.transform = `translate(${x}px, ${y}px) scale(${zoom})`;
+
+ if (isLooping) {
+ requestAnimationFrame(update);
+ }
+ }
+
+ // Go ahead and start the update loop
+ update();
+
+ return function restartLoopingIfStopped() {
+ if (!isLooping) {
+ update();
+ }
+ };
+}
+
+/**
+ * Set the various event listeners and return a function to remove them
+ *
+ * @param {Object} dragZoom
+ * @param {HTMLElement} container
+ * @param {Function} update
+ * @return {Function} The function to remove the handlers
+ */
+function setHandlers(dragZoom, container, update, debounceRate) {
+ const emitChanged = debounce(() => dragZoom.emit("change"), debounceRate);
+
+ const removeDragHandlers = setDragHandlers(
+ container,
+ dragZoom,
+ emitChanged,
+ update
+ );
+ const removeScrollHandlers = setScrollHandlers(
+ container,
+ dragZoom,
+ emitChanged,
+ update
+ );
+
+ return function removeHandlers() {
+ removeDragHandlers();
+ removeScrollHandlers();
+ };
+}
+
+/**
+ * Sets handlers for when the user drags on the canvas. It will update dragZoom
+ * object with new translate and offset values.
+ *
+ * @param {HTMLElement} container
+ * @param {Object} dragZoom
+ * @param {Function} changed
+ * @param {Function} update
+ */
+function setDragHandlers(container, dragZoom, emitChanged, update) {
+ const parentEl = container.parentElement;
+
+ function startDrag() {
+ dragZoom.isDragging = true;
+ container.style.cursor = "grabbing";
+ }
+
+ function stopDrag() {
+ dragZoom.isDragging = false;
+ container.style.cursor = "grab";
+ }
+
+ function drag(event) {
+ const prevMouseX = dragZoom.mouseX;
+ const prevMouseY = dragZoom.mouseY;
+
+ dragZoom.mouseX = event.clientX - parentEl.offsetLeft;
+ dragZoom.mouseY = event.clientY - parentEl.offsetTop;
+
+ if (!dragZoom.isDragging) {
+ return;
+ }
+
+ dragZoom.translateX += dragZoom.mouseX - prevMouseX;
+ dragZoom.translateY += dragZoom.mouseY - prevMouseY;
+
+ keepInView(container, dragZoom);
+
+ emitChanged();
+ update();
+ }
+
+ parentEl.addEventListener("mousedown", startDrag);
+ parentEl.addEventListener("mouseup", stopDrag);
+ parentEl.addEventListener("mouseout", stopDrag);
+ parentEl.addEventListener("mousemove", drag);
+
+ return function removeListeners() {
+ parentEl.removeEventListener("mousedown", startDrag);
+ parentEl.removeEventListener("mouseup", stopDrag);
+ parentEl.removeEventListener("mouseout", stopDrag);
+ parentEl.removeEventListener("mousemove", drag);
+ };
+}
+
+/**
+ * Sets the handlers for when the user scrolls. It updates the dragZoom object
+ * and keeps the canvases all within the view. After changing values update
+ * loop is called, and the changed event is emitted.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ * @param {Function} changed
+ * @param {Function} update
+ */
+function setScrollHandlers(container, dragZoom, emitChanged, update) {
+ const window = container.ownerDocument.defaultView;
+
+ function handleWheel(event) {
+ event.preventDefault();
+
+ if (dragZoom.isDragging) {
+ return;
+ }
+
+ // Update the zoom level
+ const scrollDelta = getScrollDelta(event, window);
+ const prevZoom = dragZoom.zoom;
+ dragZoom.zoom = Math.max(0, dragZoom.zoom - scrollDelta * ZOOM_SPEED);
+
+ // Calculate the updated width and height
+ const prevZoomedWidth = container.offsetWidth * (1 + prevZoom);
+ const prevZoomedHeight = container.offsetHeight * (1 + prevZoom);
+ dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
+ dragZoom.zoomedHeight = container.offsetHeight * (1 + dragZoom.zoom);
+ const deltaWidth = dragZoom.zoomedWidth - prevZoomedWidth;
+ const deltaHeight = dragZoom.zoomedHeight - prevZoomedHeight;
+
+ const mouseOffsetX = dragZoom.mouseX - container.offsetWidth / 2;
+ const mouseOffsetY = dragZoom.mouseY - container.offsetHeight / 2;
+
+ // The ratio of where the center of the mouse is in regards to the total
+ // zoomed width/height
+ const ratioZoomX =
+ (prevZoomedWidth / 2 + mouseOffsetX - dragZoom.translateX) /
+ prevZoomedWidth;
+ const ratioZoomY =
+ (prevZoomedHeight / 2 + mouseOffsetY - dragZoom.translateY) /
+ prevZoomedHeight;
+
+ // Distribute the change in width and height based on the above ratio
+ dragZoom.translateX -= lerp(-deltaWidth / 2, deltaWidth / 2, ratioZoomX);
+ dragZoom.translateY -= lerp(-deltaHeight / 2, deltaHeight / 2, ratioZoomY);
+
+ // Keep the canvas in range of the container
+ keepInView(container, dragZoom);
+ emitChanged();
+ update();
+ }
+
+ container.addEventListener("wheel", handleWheel);
+
+ return function removeListener() {
+ container.removeEventListener("wheel", handleWheel);
+ };
+}
+
+/**
+ * Account for the various mouse wheel event types, per pixel or per line
+ *
+ * @param {WheelEvent} event
+ * @param {Window} window
+ * @return {Number} The scroll size in pixels
+ */
+function getScrollDelta(event, window) {
+ if (event.deltaMode === LINE_SCROLL_MODE) {
+ // Update by a fixed arbitrary value to normalize scroll types
+ return event.deltaY * SCROLL_LINE_SIZE;
+ }
+ return event.deltaY;
+}
+
+/**
+ * Keep the dragging and zooming within the view by updating the values in the
+ * `dragZoom` object.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ */
+function keepInView(container, dragZoom) {
+ const { devicePixelRatio } = container.ownerDocument.defaultView;
+ const overdrawX = (dragZoom.zoomedWidth - container.offsetWidth) / 2;
+ const overdrawY = (dragZoom.zoomedHeight - container.offsetHeight) / 2;
+
+ dragZoom.translateX = Math.max(
+ -overdrawX,
+ Math.min(overdrawX, dragZoom.translateX)
+ );
+ dragZoom.translateY = Math.max(
+ -overdrawY,
+ Math.min(overdrawY, dragZoom.translateY)
+ );
+
+ dragZoom.offsetX =
+ devicePixelRatio *
+ ((dragZoom.zoomedWidth - container.offsetWidth) / 2 - dragZoom.translateX);
+ dragZoom.offsetY =
+ devicePixelRatio *
+ ((dragZoom.zoomedHeight - container.offsetHeight) / 2 -
+ dragZoom.translateY);
+}
diff --git a/devtools/client/memory/components/tree-map/draw.js b/devtools/client/memory/components/tree-map/draw.js
new file mode 100644
index 0000000000..f365b40834
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/draw.js
@@ -0,0 +1,317 @@
+/* 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/. */
+"use strict";
+/**
+ * Draw the treemap into the provided canvases using the 2d context. The treemap
+ * layout is computed with d3. There are 2 canvases provided, each matching
+ * the resolution of the window. The main canvas is a fully drawn version of
+ * the treemap that is positioned and zoomed using css. It gets blurry the more
+ * you zoom in as it doesn't get redrawn when zooming. The zoom canvas is
+ * repositioned absolutely after every change in the dragZoom object, and then
+ * redrawn to provide a full-resolution (non-blurry) view of zoomed in segment
+ * of the treemap.
+ */
+
+const colorCoarseType = require("devtools/client/memory/components/tree-map/color-coarse-type");
+const {
+ hslToStyle,
+ formatAbbreviatedBytes,
+ L10N,
+} = require("devtools/client/memory/utils");
+
+// A constant fully zoomed out dragZoom object for the main canvas
+const NO_SCROLL = {
+ translateX: 0,
+ translateY: 0,
+ zoom: 0,
+ offsetX: 0,
+ offsetY: 0,
+};
+
+// Drawing constants
+const ELLIPSIS = "...";
+const TEXT_MARGIN = 2;
+const TEXT_COLOR = "#000000";
+const TEXT_LIGHT_COLOR = "rgba(0,0,0,0.5)";
+const LINE_WIDTH = 1;
+const FONT_SIZE = 10;
+const FONT_LINE_HEIGHT = 2;
+const PADDING = [5 + FONT_SIZE, 5, 5, 5];
+const COUNT_LABEL = L10N.getStr("tree-map.node-count");
+
+/**
+ * Setup and start drawing the treemap visualization
+ *
+ * @param {Object} report
+ * @param {Object} canvases
+ * A CanvasUtils object that contains references to the main and zoom
+ * canvases and contexts
+ * @param {Object} dragZoom
+ * A DragZoom object representing the current state of the dragging
+ * and zooming behavior
+ */
+exports.setupDraw = function(report, canvases, dragZoom) {
+ const getTreemap = configureD3Treemap.bind(null, canvases.main.canvas);
+
+ let treemap, nodes;
+
+ function drawFullTreemap() {
+ treemap = getTreemap();
+ nodes = treemap(report);
+ drawTreemap(canvases.main, nodes, NO_SCROLL);
+ drawTreemap(canvases.zoom, nodes, dragZoom);
+ }
+
+ function drawZoomedTreemap() {
+ drawTreemap(canvases.zoom, nodes, dragZoom);
+ positionZoomedCanvas(canvases.zoom.canvas, dragZoom);
+ }
+
+ drawFullTreemap();
+ canvases.on("resize", drawFullTreemap);
+ dragZoom.on("change", drawZoomedTreemap);
+};
+
+/**
+ * Returns a configured d3 treemap function
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @return {Function}
+ */
+const configureD3Treemap = (exports.configureD3Treemap = function(canvas) {
+ const window = canvas.ownerDocument.defaultView;
+ const ratio = window.devicePixelRatio;
+ const treemap = window.d3.layout
+ .treemap()
+ .size([
+ // The d3 layout includes the padding around everything, add some
+ // extra padding to the size to compensate for thi
+ canvas.width + (PADDING[1] + PADDING[3]) * ratio,
+ canvas.height + (PADDING[0] + PADDING[2]) * ratio,
+ ])
+ .sticky(true)
+ .padding([
+ PADDING[0] * ratio,
+ PADDING[1] * ratio,
+ PADDING[2] * ratio,
+ PADDING[3] * ratio,
+ ])
+ .value(d => d.bytes);
+
+ /**
+ * Create treemap nodes from a census report that are sorted by depth
+ *
+ * @param {Object} report
+ * @return {Array} An array of d3 treemap nodes
+ * // https://github.com/mbostock/d3/wiki/Treemap-Layout
+ * parent - the parent node, or null for the root.
+ * children - the array of child nodes, or null for leaf nodes.
+ * value - the node value, as returned by the value accessor.
+ * depth - the depth of the node, starting at 0 for the root.
+ * area - the computed pixel area of this node.
+ * x - the minimum x-coordinate of the node position.
+ * y - the minimum y-coordinate of the node position.
+ * z - the orientation of this cell’s subdivision, if any.
+ * dx - the x-extent of the node position.
+ * dy - the y-extent of the node position.
+ */
+ return function depthSortedNodes(report) {
+ const nodes = treemap(report);
+ nodes.sort((a, b) => a.depth - b.depth);
+ return nodes;
+ };
+});
+
+/**
+ * Draw the text, cut it in half every time it doesn't fit until it fits or
+ * it's smaller than the "..." text.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} x
+ * the position of the text
+ * @param {Number} y
+ * the position of the text
+ * @param {Number} innerWidth
+ * the inner width of the containing treemap cell
+ * @param {Text} name
+ */
+const drawTruncatedName = (exports.drawTruncatedName = function(
+ ctx,
+ x,
+ y,
+ innerWidth,
+ name
+) {
+ const truncated = name.substr(0, Math.floor(name.length / 2));
+ const formatted = truncated + ELLIPSIS;
+
+ if (ctx.measureText(formatted).width > innerWidth) {
+ drawTruncatedName(ctx, x, y, innerWidth, truncated);
+ } else {
+ ctx.fillText(formatted, x, y);
+ }
+});
+
+/**
+ * Fit and draw the text in a node with the following strategies to shrink
+ * down the text size:
+ *
+ * Function 608KB 9083 count
+ * Function
+ * Func...
+ * Fu...
+ * ...
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Object} node
+ * @param {Number} borderWidth
+ * @param {Object} dragZoom
+ * @param {Array} padding
+ */
+const drawText = (exports.drawText = function(
+ ctx,
+ node,
+ borderWidth,
+ ratio,
+ dragZoom,
+ padding
+) {
+ let { dx, dy, name, totalBytes, totalCount } = node;
+ const scale = dragZoom.zoom + 1;
+ dx *= scale;
+ dy *= scale;
+
+ // Start checking to see how much text we can fit in, optimizing for the
+ // common case of lots of small leaf nodes
+ if (FONT_SIZE * FONT_LINE_HEIGHT < dy) {
+ const margin = borderWidth(node) * 1.5 + ratio * TEXT_MARGIN;
+ const x = margin + (node.x - padding[0]) * scale - dragZoom.offsetX;
+ const y = margin + (node.y - padding[1]) * scale - dragZoom.offsetY;
+ const innerWidth = dx - margin * 2;
+ const nameSize = ctx.measureText(name).width;
+
+ if (ctx.measureText(ELLIPSIS).width > innerWidth) {
+ return;
+ }
+
+ ctx.fillStyle = TEXT_COLOR;
+
+ if (nameSize > innerWidth) {
+ // The name is too long - halve the name as an expediant way to shorten it
+ drawTruncatedName(ctx, x, y, innerWidth, name);
+ } else {
+ const bytesFormatted = formatAbbreviatedBytes(totalBytes);
+ const countFormatted = `${totalCount} ${COUNT_LABEL}`;
+ const byteSize = ctx.measureText(bytesFormatted).width;
+ const countSize = ctx.measureText(countFormatted).width;
+ const spaceSize = ctx.measureText(" ").width;
+
+ if (nameSize + byteSize + countSize + spaceSize * 3 > innerWidth) {
+ // The full name will fit
+ ctx.fillText(`${name}`, x, y);
+ } else {
+ // The full name plus the byte information will fit
+ ctx.fillText(name, x, y);
+ ctx.fillStyle = TEXT_LIGHT_COLOR;
+ ctx.fillText(
+ `${bytesFormatted} ${countFormatted}`,
+ x + nameSize + spaceSize,
+ y
+ );
+ }
+ }
+ }
+});
+
+/**
+ * Draw a box given a node
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Object} node
+ * @param {Number} borderWidth
+ * @param {Number} ratio
+ * @param {Object} dragZoom
+ * @param {Array} padding
+ */
+const drawBox = (exports.drawBox = function(
+ ctx,
+ node,
+ borderWidth,
+ dragZoom,
+ padding
+) {
+ const border = borderWidth(node);
+ const fillHSL = colorCoarseType(node);
+ const strokeHSL = [fillHSL[0], fillHSL[1], fillHSL[2] * 0.5];
+ const scale = 1 + dragZoom.zoom;
+
+ // Offset the draw so that box strokes don't overlap
+ const x = scale * (node.x - padding[0]) - dragZoom.offsetX + border / 2;
+ const y = scale * (node.y - padding[1]) - dragZoom.offsetY + border / 2;
+ const dx = scale * node.dx - border;
+ const dy = scale * node.dy - border;
+
+ ctx.fillStyle = hslToStyle(...fillHSL);
+ ctx.fillRect(x, y, dx, dy);
+
+ ctx.strokeStyle = hslToStyle(...strokeHSL);
+ ctx.lineWidth = border;
+ ctx.strokeRect(x, y, dx, dy);
+});
+
+/**
+ * Draw the overall treemap
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Array} nodes
+ * @param {Objbect} dragZoom
+ */
+const drawTreemap = (exports.drawTreemap = function(
+ { canvas, ctx },
+ nodes,
+ dragZoom
+) {
+ const window = canvas.ownerDocument.defaultView;
+ const ratio = window.devicePixelRatio;
+ const canvasArea = canvas.width * canvas.height;
+ // Subtract the outer padding from the tree map layout.
+ const padding = [PADDING[3] * ratio, PADDING[0] * ratio];
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.font = `${FONT_SIZE * ratio}px sans-serif`;
+ ctx.textBaseline = "top";
+
+ function borderWidth(node) {
+ const areaRatio = Math.sqrt(node.area / canvasArea);
+ return ratio * Math.max(1, LINE_WIDTH * areaRatio);
+ }
+
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ if (node.parent === undefined) {
+ continue;
+ }
+
+ drawBox(ctx, node, borderWidth, dragZoom, padding);
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+ }
+});
+
+/**
+ * Set the position of the zoomed in canvas. It always take up 100% of the view
+ * window, but is transformed relative to the zoomed in containing element,
+ * essentially reversing the transform of the containing element.
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @param {Object} dragZoom
+ */
+const positionZoomedCanvas = function(canvas, dragZoom) {
+ const scale = 1 / (1 + dragZoom.zoom);
+ const x = -dragZoom.translateX;
+ const y = -dragZoom.translateY;
+ canvas.style.transform = `scale(${scale}) translate(${x}px, ${y}px)`;
+};
+
+exports.positionZoomedCanvas = positionZoomedCanvas;
diff --git a/devtools/client/memory/components/tree-map/moz.build b/devtools/client/memory/components/tree-map/moz.build
new file mode 100644
index 0000000000..a9e5900339
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "canvas-utils.js",
+ "color-coarse-type.js",
+ "drag-zoom.js",
+ "draw.js",
+ "start.js",
+)
diff --git a/devtools/client/memory/components/tree-map/start.js b/devtools/client/memory/components/tree-map/start.js
new file mode 100644
index 0000000000..9c3257e529
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/start.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+"use strict";
+
+const {
+ setupDraw,
+} = require("devtools/client/memory/components/tree-map/draw");
+const DragZoom = require("devtools/client/memory/components/tree-map/drag-zoom");
+const CanvasUtils = require("devtools/client/memory/components/tree-map/canvas-utils");
+
+/**
+ * Start the tree map visualization
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} report
+ * the report from a census
+ * @param {Number} debounceRate
+ */
+module.exports = function startVisualization(
+ parentEl,
+ report,
+ debounceRate = 60
+) {
+ const window = parentEl.ownerDocument.defaultView;
+ const canvases = new CanvasUtils(parentEl, debounceRate);
+ const dragZoom = new DragZoom(
+ canvases.container,
+ debounceRate,
+ window.requestAnimationFrame
+ );
+
+ setupDraw(report, canvases, dragZoom);
+
+ return function stopVisualization() {
+ canvases.destroy();
+ dragZoom.destroy();
+ };
+};
diff --git a/devtools/client/memory/constants.js b/devtools/client/memory/constants.js
new file mode 100644
index 0000000000..01393d6450
--- /dev/null
+++ b/devtools/client/memory/constants.js
@@ -0,0 +1,360 @@
+/* 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/. */
+
+"use strict";
+
+// Options passed to MemoryFront's startRecordingAllocations never change.
+exports.ALLOCATION_RECORDING_OPTIONS = {
+ probability: 1,
+ maxLogLength: 1,
+};
+
+// If TREE_ROW_HEIGHT changes, be sure to change `var(--heap-tree-row-height)`
+// in `devtools/client/themes/memory.css`
+exports.TREE_ROW_HEIGHT = 18;
+
+/** * Actions ******************************************************************/
+
+const actions = (exports.actions = {});
+
+// Fired by UI to request a snapshot from the actor.
+actions.TAKE_SNAPSHOT_START = "take-snapshot-start";
+actions.TAKE_SNAPSHOT_END = "take-snapshot-end";
+
+// When a heap snapshot is read into memory -- only fired
+// once per snapshot.
+actions.READ_SNAPSHOT_START = "read-snapshot-start";
+actions.READ_SNAPSHOT_END = "read-snapshot-end";
+
+// When a census is being performed on a heap snapshot
+actions.TAKE_CENSUS_START = "take-census-start";
+actions.TAKE_CENSUS_END = "take-census-end";
+actions.TAKE_CENSUS_ERROR = "take-census-error";
+
+// When a tree map is being calculated on a heap snapshot
+actions.TAKE_TREE_MAP_START = "take-tree-map-start";
+actions.TAKE_TREE_MAP_END = "take-tree-map-end";
+actions.TAKE_TREE_MAP_ERROR = "take-tree-map-error";
+
+// When requesting that the server start/stop recording allocation stacks.
+actions.TOGGLE_RECORD_ALLOCATION_STACKS_START =
+ "toggle-record-allocation-stacks-start";
+actions.TOGGLE_RECORD_ALLOCATION_STACKS_END =
+ "toggle-record-allocation-stacks-end";
+
+// When a heap snapshot is being saved to a user-specified
+// location on disk.
+actions.EXPORT_SNAPSHOT_START = "export-snapshot-start";
+actions.EXPORT_SNAPSHOT_END = "export-snapshot-end";
+actions.EXPORT_SNAPSHOT_ERROR = "export-snapshot-error";
+
+// When a heap snapshot is being read from a user selected file,
+// and represents the entire state until the census is available.
+actions.IMPORT_SNAPSHOT_START = "import-snapshot-start";
+actions.IMPORT_SNAPSHOT_END = "import-snapshot-end";
+actions.IMPORT_SNAPSHOT_ERROR = "import-snapshot-error";
+
+// Fired by UI to select a snapshot to view.
+actions.SELECT_SNAPSHOT = "select-snapshot";
+
+// Fired to delete a provided list of snapshots
+actions.DELETE_SNAPSHOTS_START = "delete-snapshots-start";
+actions.DELETE_SNAPSHOTS_END = "delete-snapshots-end";
+
+// Fired to toggle tree inversion on or off.
+actions.TOGGLE_INVERTED = "toggle-inverted";
+
+// Fired when a snapshot is selected for diffing.
+actions.SELECT_SNAPSHOT_FOR_DIFFING = "select-snapshot-for-diffing";
+
+// Fired when taking a census diff.
+actions.TAKE_CENSUS_DIFF_START = "take-census-diff-start";
+actions.TAKE_CENSUS_DIFF_END = "take-census-diff-end";
+actions.DIFFING_ERROR = "diffing-error";
+
+// Fired to set a new census display.
+actions.SET_CENSUS_DISPLAY = "set-census-display";
+
+// Fired to change the display that controls the dominator tree labels.
+actions.SET_LABEL_DISPLAY = "set-label-display";
+
+// Fired to set a tree map display
+actions.SET_TREE_MAP_DISPLAY = "set-tree-map-display";
+
+// Fired when changing between census or dominators view.
+actions.CHANGE_VIEW = "change-view";
+actions.POP_VIEW = "pop-view";
+
+// Fired when there is an error processing a snapshot or taking a census.
+actions.SNAPSHOT_ERROR = "snapshot-error";
+
+// Fired when there is a new filter string set.
+actions.SET_FILTER_STRING = "set-filter-string";
+
+// Fired to expand or collapse nodes in census reports.
+actions.EXPAND_CENSUS_NODE = "expand-census-node";
+actions.EXPAND_DIFFING_CENSUS_NODE = "expand-diffing-census-node";
+actions.COLLAPSE_CENSUS_NODE = "collapse-census-node";
+actions.COLLAPSE_DIFFING_CENSUS_NODE = "collapse-diffing-census-node";
+
+// Fired when nodes in various trees are focused.
+actions.FOCUS_CENSUS_NODE = "focus-census-node";
+actions.FOCUS_DIFFING_CENSUS_NODE = "focus-diffing-census-node";
+actions.FOCUS_DOMINATOR_TREE_NODE = "focus-dominator-tree-node";
+
+actions.FOCUS_INDIVIDUAL = "focus-individual";
+actions.FETCH_INDIVIDUALS_START = "fetch-individuals-start";
+actions.FETCH_INDIVIDUALS_END = "fetch-individuals-end";
+actions.INDIVIDUALS_ERROR = "individuals-error";
+
+actions.COMPUTE_DOMINATOR_TREE_START = "compute-dominator-tree-start";
+actions.COMPUTE_DOMINATOR_TREE_END = "compute-dominator-tree-end";
+actions.FETCH_DOMINATOR_TREE_START = "fetch-dominator-tree-start";
+actions.FETCH_DOMINATOR_TREE_END = "fetch-dominator-tree-end";
+actions.DOMINATOR_TREE_ERROR = "dominator-tree-error";
+actions.FETCH_IMMEDIATELY_DOMINATED_START = "fetch-immediately-dominated-start";
+actions.FETCH_IMMEDIATELY_DOMINATED_END = "fetch-immediately-dominated-end";
+actions.EXPAND_DOMINATOR_TREE_NODE = "expand-dominator-tree-node";
+actions.COLLAPSE_DOMINATOR_TREE_NODE = "collapse-dominator-tree-node";
+
+actions.RESIZE_SHORTEST_PATHS = "resize-shortest-paths";
+
+// Fired when the memory front is changed.
+actions.UPDATE_MEMORY_FRONT = "update-memory-front";
+
+/** * Census Displays ***************************************************************/
+
+const COUNT = Object.freeze({ by: "count", count: true, bytes: true });
+const INTERNAL_TYPE = Object.freeze({ by: "internalType", then: COUNT });
+const DESCRIPTIVE_TYPE = Object.freeze({ by: "descriptiveType", then: COUNT });
+const ALLOCATION_STACK = Object.freeze({
+ by: "allocationStack",
+ then: COUNT,
+ noStack: COUNT,
+});
+const OBJECT_CLASS = Object.freeze({
+ by: "objectClass",
+ then: COUNT,
+ other: COUNT,
+});
+const COARSE_TYPE = Object.freeze({
+ by: "coarseType",
+ objects: OBJECT_CLASS,
+ strings: COUNT,
+ scripts: {
+ by: "filename",
+ then: INTERNAL_TYPE,
+ noFilename: INTERNAL_TYPE,
+ },
+ other: INTERNAL_TYPE,
+ domNode: DESCRIPTIVE_TYPE,
+});
+
+exports.censusDisplays = Object.freeze({
+ coarseType: Object.freeze({
+ displayName: "Type",
+ get tooltip() {
+ // Importing down here is necessary because of the circular dependency
+ // this introduces with `./utils.js`.
+ const { L10N } = require("devtools/client/memory/utils");
+ return L10N.getStr("censusDisplays.coarseType.tooltip");
+ },
+ inverted: true,
+ breakdown: COARSE_TYPE,
+ }),
+
+ allocationStack: Object.freeze({
+ displayName: "Call Stack",
+ get tooltip() {
+ const { L10N } = require("devtools/client/memory/utils");
+ return L10N.getStr("censusDisplays.allocationStack.tooltip");
+ },
+ inverted: false,
+ breakdown: ALLOCATION_STACK,
+ }),
+
+ invertedAllocationStack: Object.freeze({
+ displayName: "Inverted Call Stack",
+ get tooltip() {
+ const { L10N } = require("devtools/client/memory/utils");
+ return L10N.getStr("censusDisplays.invertedAllocationStack.tooltip");
+ },
+ inverted: true,
+ breakdown: ALLOCATION_STACK,
+ }),
+});
+
+const DOMINATOR_TREE_LABEL_COARSE_TYPE = Object.freeze({
+ by: "coarseType",
+ objects: OBJECT_CLASS,
+ scripts: Object.freeze({
+ by: "internalType",
+ then: Object.freeze({
+ by: "filename",
+ then: COUNT,
+ noFilename: COUNT,
+ }),
+ }),
+ strings: INTERNAL_TYPE,
+ other: INTERNAL_TYPE,
+ domNode: DESCRIPTIVE_TYPE,
+});
+
+exports.labelDisplays = Object.freeze({
+ coarseType: Object.freeze({
+ displayName: "Type",
+ get tooltip() {
+ const { L10N } = require("devtools/client/memory/utils");
+ return L10N.getStr("dominatorTreeDisplays.coarseType.tooltip");
+ },
+ breakdown: DOMINATOR_TREE_LABEL_COARSE_TYPE,
+ }),
+
+ allocationStack: Object.freeze({
+ displayName: "Call Stack",
+ get tooltip() {
+ const { L10N } = require("devtools/client/memory/utils");
+ return L10N.getStr("dominatorTreeDisplays.allocationStack.tooltip");
+ },
+ breakdown: Object.freeze({
+ by: "allocationStack",
+ then: DOMINATOR_TREE_LABEL_COARSE_TYPE,
+ noStack: DOMINATOR_TREE_LABEL_COARSE_TYPE,
+ }),
+ }),
+});
+
+exports.treeMapDisplays = Object.freeze({
+ coarseType: Object.freeze({
+ displayName: "Type",
+ get tooltip() {
+ const { L10N } = require("devtools/client/memory/utils");
+ return L10N.getStr("treeMapDisplays.coarseType.tooltip");
+ },
+ breakdown: COARSE_TYPE,
+ inverted: false,
+ }),
+});
+
+/** * View States **************************************************************/
+
+/**
+ * The various main views that the tool can be in.
+ */
+const viewState = (exports.viewState = Object.create(null));
+viewState.CENSUS = "view-state-census";
+viewState.DIFFING = "view-state-diffing";
+viewState.DOMINATOR_TREE = "view-state-dominator-tree";
+viewState.TREE_MAP = "view-state-tree-map";
+viewState.INDIVIDUALS = "view-state-individuals";
+
+/** * Snapshot States **********************************************************/
+
+const snapshotState = (exports.snapshotState = Object.create(null));
+
+/**
+ * Various states a snapshot can be in.
+ * An FSM describing snapshot states:
+ *
+ * SAVING -> SAVED -> READING -> READ
+ * ↗
+ * IMPORTING
+ *
+ * Any of these states may go to the ERROR state, from which they can never
+ * leave (mwah ha ha ha!)
+ */
+snapshotState.ERROR = "snapshot-state-error";
+snapshotState.IMPORTING = "snapshot-state-importing";
+snapshotState.SAVING = "snapshot-state-saving";
+snapshotState.SAVED = "snapshot-state-saved";
+snapshotState.READING = "snapshot-state-reading";
+snapshotState.READ = "snapshot-state-read";
+
+/*
+ * Various states the census model can be in.
+ *
+ * SAVING <-> SAVED
+ * |
+ * V
+ * ERROR
+ */
+
+const censusState = (exports.censusState = Object.create(null));
+
+censusState.SAVING = "census-state-saving";
+censusState.SAVED = "census-state-saved";
+censusState.ERROR = "census-state-error";
+
+/*
+ * Various states the tree map model can be in.
+ *
+ * SAVING <-> SAVED
+ * |
+ * V
+ * ERROR
+ */
+
+const treeMapState = (exports.treeMapState = Object.create(null));
+
+treeMapState.SAVING = "tree-map-state-saving";
+treeMapState.SAVED = "tree-map-state-saved";
+treeMapState.ERROR = "tree-map-state-error";
+
+/** * Diffing States ***********************************************************/
+
+/*
+ * Various states the diffing model can be in.
+ *
+ * SELECTING --> TAKING_DIFF <---> TOOK_DIFF
+ * |
+ * V
+ * ERROR
+ */
+const diffingState = (exports.diffingState = Object.create(null));
+
+// Selecting the two snapshots to diff.
+diffingState.SELECTING = "diffing-state-selecting";
+
+// Currently computing the diff between the two selected snapshots.
+diffingState.TAKING_DIFF = "diffing-state-taking-diff";
+
+// Have the diff between the two selected snapshots.
+diffingState.TOOK_DIFF = "diffing-state-took-diff";
+
+// An error occurred while computing the diff.
+diffingState.ERROR = "diffing-state-error";
+
+/** * Dominator Tree States ****************************************************/
+
+/*
+ * Various states the dominator tree model can be in.
+ *
+ * COMPUTING -> COMPUTED -> FETCHING -> LOADED <--> INCREMENTAL_FETCHING
+ *
+ * Any state may lead to the ERROR state, from which it can never leave.
+ */
+const dominatorTreeState = (exports.dominatorTreeState = Object.create(null));
+dominatorTreeState.COMPUTING = "dominator-tree-state-computing";
+dominatorTreeState.COMPUTED = "dominator-tree-state-computed";
+dominatorTreeState.FETCHING = "dominator-tree-state-fetching";
+dominatorTreeState.LOADED = "dominator-tree-state-loaded";
+dominatorTreeState.INCREMENTAL_FETCHING =
+ "dominator-tree-state-incremental-fetching";
+dominatorTreeState.ERROR = "dominator-tree-state-error";
+
+/** * States for Individuals Model *********************************************/
+
+/*
+ * Various states the individuals model can be in.
+ *
+ * COMPUTING_DOMINATOR_TREE -> FETCHING -> FETCHED
+ *
+ * Any state may lead to the ERROR state, from which it can never leave.
+ */
+const individualsState = (exports.individualsState = Object.create(null));
+individualsState.COMPUTING_DOMINATOR_TREE =
+ "individuals-state-computing-dominator-tree";
+individualsState.FETCHING = "individuals-state-fetching";
+individualsState.FETCHED = "individuals-state-fetched";
+individualsState.ERROR = "individuals-state-error";
diff --git a/devtools/client/memory/dominator-tree-lazy-children.js b/devtools/client/memory/dominator-tree-lazy-children.js
new file mode 100644
index 0000000000..24978ed099
--- /dev/null
+++ b/devtools/client/memory/dominator-tree-lazy-children.js
@@ -0,0 +1,60 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * The `DominatorTreeLazyChildren` is a placeholder that represents a future
+ * subtree in an existing `DominatorTreeNode` tree that is currently being
+ * incrementally fetched from the `HeapAnalysesWorker`.
+ *
+ * @param {NodeId} parentNodeId
+ * @param {Number} siblingIndex
+ */
+function DominatorTreeLazyChildren(parentNodeId, siblingIndex) {
+ this._parentNodeId = parentNodeId;
+ this._siblingIndex = siblingIndex;
+}
+
+/**
+ * Generate a unique key for this `DominatorTreeLazyChildren` instance. This can
+ * be used as the key in a hash table or as the `key` property for a React
+ * component, for example.
+ *
+ * @returns {String}
+ */
+DominatorTreeLazyChildren.prototype.key = function() {
+ return `dominator-tree-lazy-children-${this._parentNodeId}-${this._siblingIndex}`;
+};
+
+/**
+ * Return true if this is a placeholder for the first child of its
+ * parent. Return false if it is a placeholder for loading more of its parent's
+ * children.
+ *
+ * @returns {Boolean}
+ */
+DominatorTreeLazyChildren.prototype.isFirstChild = function() {
+ return this._siblingIndex === 0;
+};
+
+/**
+ * Get this subtree's parent node's identifier.
+ *
+ * @returns {NodeId}
+ */
+DominatorTreeLazyChildren.prototype.parentNodeId = function() {
+ return this._parentNodeId;
+};
+
+/**
+ * Get this subtree's index in its parent's children array.
+ *
+ * @returns {Number}
+ */
+DominatorTreeLazyChildren.prototype.siblingIndex = function() {
+ return this._siblingIndex;
+};
+
+module.exports = DominatorTreeLazyChildren;
diff --git a/devtools/client/memory/index.xhtml b/devtools/client/memory/index.xhtml
new file mode 100644
index 0000000000..33a6634b69
--- /dev/null
+++ b/devtools/client/memory/index.xhtml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+]>
+
+<!-- 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/. -->
+<html xmlns="http://www.w3.org/1999/xhtml" dir="">
+ <head>
+ <link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/memory.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/components-frame.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
+ </head>
+ <body class="theme-body">
+ <div id="app"></div>
+
+ <script src="chrome://devtools/content/shared/theme-switching.js"
+ defer="true">
+ </script>
+
+ <script src="chrome://global/content/third_party/d3/d3.js"
+ defer="true">
+ </script>
+
+ <script src="chrome://devtools/content/shared/vendor/dagre-d3.js"
+ defer="true">
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/memory/initializer.js b/devtools/client/memory/initializer.js
new file mode 100644
index 0000000000..65640bfd2e
--- /dev/null
+++ b/devtools/client/memory/initializer.js
@@ -0,0 +1,80 @@
+/* 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/. */
+
+/* exported initialize, destroy, Promise */
+
+"use strict";
+
+const {
+ createFactory,
+ createElement,
+} = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const App = createFactory(require("devtools/client/memory/app"));
+const Store = require("devtools/client/memory/store");
+const { assert } = require("devtools/shared/DevToolsUtils");
+
+const { updateMemoryFront } = require("devtools/client/memory/actions/front");
+
+// Shared variables used by several methods of this module.
+let root, store, unsubscribe;
+
+const initialize = async function() {
+ // Exposed by panel.js
+ const { gToolbox, gHeapAnalysesClient } = window;
+
+ root = document.querySelector("#app");
+ store = Store();
+ const app = createElement(App, {
+ toolbox: gToolbox,
+ heapWorker: gHeapAnalysesClient,
+ });
+ const provider = createElement(Provider, { store }, app);
+ ReactDOM.render(provider, root);
+ unsubscribe = store.subscribe(onStateChange);
+
+ // Exposed for tests.
+ window.gStore = store;
+};
+
+const updateFront = front => {
+ store.dispatch(updateMemoryFront(front));
+};
+
+const destroy = function() {
+ const ok = ReactDOM.unmountComponentAtNode(root);
+ assert(
+ ok,
+ "Should successfully unmount the memory tool's top level React component"
+ );
+
+ unsubscribe();
+};
+
+// Current state
+let isHighlighted;
+
+/**
+ * Fired on any state change, currently only handles toggling
+ * the highlighting of the tool when recording allocations.
+ */
+function onStateChange() {
+ const { gToolbox } = window;
+
+ const isRecording = store.getState().allocations.recording;
+ if (isRecording === isHighlighted) {
+ return;
+ }
+
+ if (isRecording) {
+ gToolbox.highlightTool("memory");
+ } else {
+ gToolbox.unhighlightTool("memory");
+ }
+
+ isHighlighted = isRecording;
+}
+
+module.exports = { initialize, updateFront, destroy };
diff --git a/devtools/client/memory/models.js b/devtools/client/memory/models.js
new file mode 100644
index 0000000000..ab358519ac
--- /dev/null
+++ b/devtools/client/memory/models.js
@@ -0,0 +1,543 @@
+/* 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/. */
+
+/* global treeMapState, censusState */
+/* eslint no-shadow: ["error", { "allow": ["app"] }] */
+
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { MemoryFront } = require("devtools/client/fronts/memory");
+const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const {
+ snapshotState: states,
+ diffingState,
+ dominatorTreeState,
+ viewState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+
+/**
+ * ONLY USE THIS FOR MODEL VALIDATORS IN CONJUCTION WITH assert()!
+ *
+ * React checks that the returned values from validator functions are instances
+ * of Error, but because React is loaded in its own global, that check is always
+ * false and always results in a warning.
+ *
+ * To work around this and still get model validation, just call assert() inside
+ * a function passed to catchAndIgnore. The assert() function will still report
+ * assertion failures, but this funciton will swallow the errors so that React
+ * doesn't go crazy and drown out the real error in irrelevant and incorrect
+ * warnings.
+ *
+ * Example usage:
+ *
+ * const MyModel = PropTypes.shape({
+ * someProperty: catchAndIgnore(function (model) {
+ * assert(someInvariant(model.someProperty), "Should blah blah");
+ * })
+ * });
+ */
+function catchAndIgnore(fn) {
+ return function(...args) {
+ try {
+ fn(...args);
+ } catch (err) {
+ // continue regardless of error
+ }
+
+ return null;
+ };
+}
+
+/**
+ * The data describing the census report's shape, and its associated metadata.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const censusDisplayModel = (exports.censusDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ inverted: PropTypes.bool.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ }),
+}));
+
+/**
+ * How we want to label nodes in the dominator tree, and associated
+ * metadata. The notable difference from `censusDisplayModel` is the lack of
+ * an `inverted` property.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const labelDisplayModel = (exports.labelDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ }),
+}));
+
+/**
+ * The data describing the tree map's shape, and its associated metadata.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const treeMapDisplayModel = (exports.treeMapDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ inverted: PropTypes.bool.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ }),
+}));
+
+/**
+ * Tree map model.
+ */
+const treeMapModel = (exports.treeMapModel = PropTypes.shape({
+ // The current census report data.
+ report: PropTypes.object,
+ // The display data used to generate the current census.
+ display: treeMapDisplayModel,
+ // The current treeMapState this is in
+ state: catchAndIgnore(function(treeMap) {
+ switch (treeMap.state) {
+ case treeMapState.SAVING:
+ assert(!treeMap.report, "Should not have a report");
+ assert(!treeMap.error, "Should not have an error");
+ break;
+
+ case treeMapState.SAVED:
+ assert(treeMap.report, "Should have a report");
+ assert(!treeMap.error, "Should not have an error");
+ break;
+
+ case treeMapState.ERROR:
+ assert(treeMap.error, "Should have an error");
+ break;
+
+ default:
+ assert(false, `Unexpected treeMap state: ${treeMap.state}`);
+ }
+ }),
+}));
+
+const censusModel = (exports.censusModel = PropTypes.shape({
+ // The current census report data.
+ report: PropTypes.object,
+ // The parent map for the report.
+ parentMap: PropTypes.object,
+ // The display data used to generate the current census.
+ display: censusDisplayModel,
+ // If present, the currently cached report's filter string used for pruning
+ // the tree items.
+ filter: PropTypes.string,
+ // The Immutable.Set<CensusTreeNode.id> of expanded node ids in the report
+ // tree.
+ expanded: catchAndIgnore(function(census) {
+ if (census.report) {
+ assert(
+ census.expanded,
+ "If we have a report, we should also have the set of expanded nodes"
+ );
+ }
+ }),
+ // If a node is currently focused in the report tree, then this is it.
+ focused: PropTypes.object,
+ // The censusModelState that this census is currently in.
+ state: catchAndIgnore(function(census) {
+ switch (census.state) {
+ case censusState.SAVING:
+ assert(!census.report, "Should not have a report");
+ assert(!census.parentMap, "Should not have a parent map");
+ assert(census.expanded, "Should not have an expanded set");
+ assert(!census.error, "Should not have an error");
+ break;
+
+ case censusState.SAVED:
+ assert(census.report, "Should have a report");
+ assert(census.parentMap, "Should have a parent map");
+ assert(census.expanded, "Should have an expanded set");
+ assert(!census.error, "Should not have an error");
+ break;
+
+ case censusState.ERROR:
+ assert(!census.report, "Should not have a report");
+ assert(census.error, "Should have an error");
+ break;
+
+ default:
+ assert(false, `Unexpected census state: ${census.state}`);
+ }
+ }),
+}));
+
+/**
+ * Dominator tree model.
+ */
+const dominatorTreeModel = (exports.dominatorTreeModel = PropTypes.shape({
+ // The id of this dominator tree.
+ dominatorTreeId: PropTypes.number,
+
+ // The root DominatorTreeNode of this dominator tree.
+ root: PropTypes.object,
+
+ // The Set<NodeId> of expanded nodes in this dominator tree.
+ expanded: PropTypes.object,
+
+ // If a node is currently focused in the dominator tree, then this is it.
+ focused: PropTypes.object,
+
+ // If an error was thrown while getting this dominator tree, the `Error`
+ // instance (or an error string message) is attached here.
+ error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+
+ // The display used to generate descriptive labels of nodes in this dominator
+ // tree.
+ display: labelDisplayModel,
+
+ // The number of active requests to incrementally fetch subtrees. This should
+ // only be non-zero when the state is INCREMENTAL_FETCHING.
+ activeFetchRequestCount: PropTypes.number,
+
+ // The dominatorTreeState that this domintor tree is currently in.
+ state: catchAndIgnore(function(dominatorTree) {
+ switch (dominatorTree.state) {
+ case dominatorTreeState.COMPUTING:
+ assert(
+ dominatorTree.dominatorTreeId == null,
+ "Should not have a dominator tree id yet"
+ );
+ assert(!dominatorTree.root, "Should not have the root of the tree yet");
+ assert(!dominatorTree.error, "Should not have an error");
+ break;
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ assert(
+ dominatorTree.dominatorTreeId != null,
+ "Should have a dominator tree id"
+ );
+ assert(!dominatorTree.root, "Should not have the root of the tree yet");
+ assert(!dominatorTree.error, "Should not have an error");
+ break;
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ assert(
+ typeof dominatorTree.activeFetchRequestCount === "number",
+ "The active fetch request count is a number when we are in the " +
+ "INCREMENTAL_FETCHING state"
+ );
+ assert(
+ dominatorTree.activeFetchRequestCount > 0,
+ "We are keeping track of how many active requests are in flight."
+ );
+ // Fall through...
+ case dominatorTreeState.LOADED:
+ assert(
+ dominatorTree.dominatorTreeId != null,
+ "Should have a dominator tree id"
+ );
+ assert(dominatorTree.root, "Should have the root of the tree");
+ assert(dominatorTree.expanded, "Should have an expanded set");
+ assert(!dominatorTree.error, "Should not have an error");
+ break;
+
+ case dominatorTreeState.ERROR:
+ assert(dominatorTree.error, "Should have an error");
+ break;
+
+ default:
+ assert(
+ false,
+ `Unexpected dominator tree state: ${dominatorTree.state}`
+ );
+ }
+ }),
+}));
+
+/**
+ * Snapshot model.
+ */
+const stateKeys = Object.keys(states).map(state => states[state]);
+const snapshotId = PropTypes.number;
+const snapshotModel = (exports.snapshot = PropTypes.shape({
+ // Unique ID for a snapshot
+ id: snapshotId.isRequired,
+ // Whether or not this snapshot is currently selected.
+ selected: PropTypes.bool.isRequired,
+ // Filesystem path to where the snapshot is stored; used to identify the
+ // snapshot for HeapAnalysesClient.
+ path: PropTypes.string,
+ // Current census data for this snapshot.
+ census: censusModel,
+ // Current dominator tree data for this snapshot.
+ dominatorTree: dominatorTreeModel,
+ // Current tree map data for this snapshot.
+ treeMap: treeMapModel,
+ // If an error was thrown while processing this snapshot, the `Error` instance
+ // is attached here.
+ error: PropTypes.object,
+ // Boolean indicating whether or not this snapshot was imported.
+ imported: PropTypes.bool.isRequired,
+ // The creation time of the snapshot; required after the snapshot has been
+ // read.
+ creationTime: PropTypes.number,
+ // The current state the snapshot is in.
+ // @see ./constants.js
+ state: catchAndIgnore(function(snapshot, propName) {
+ const current = snapshot.state;
+ const shouldHavePath = [states.IMPORTING, states.SAVED, states.READ];
+ const shouldHaveCreationTime = [states.READ];
+
+ if (!stateKeys.includes(current)) {
+ throw new Error(`Snapshot state must be one of ${stateKeys}.`);
+ }
+ if (shouldHavePath.includes(current) && !snapshot.path) {
+ throw new Error(
+ `Snapshots in state ${current} must have a snapshot path.`
+ );
+ }
+ if (shouldHaveCreationTime.includes(current) && !snapshot.creationTime) {
+ throw new Error(
+ `Snapshots in state ${current} must have a creation time.`
+ );
+ }
+ }),
+}));
+
+const allocationsModel = (exports.allocations = PropTypes.shape({
+ // True iff we are recording allocation stacks right now.
+ recording: PropTypes.bool.isRequired,
+ // True iff we are in the process of toggling the recording of allocation
+ // stacks on or off right now.
+ togglingInProgress: PropTypes.bool.isRequired,
+}));
+
+const diffingModel = (exports.diffingModel = PropTypes.shape({
+ // The id of the first snapshot to diff.
+ firstSnapshotId: snapshotId,
+
+ // The id of the second snapshot to diff.
+ secondSnapshotId: catchAndIgnore(function(diffing, propName) {
+ if (diffing.secondSnapshotId && !diffing.firstSnapshotId) {
+ throw new Error(
+ "Cannot have second snapshot without already having " + "first snapshot"
+ );
+ }
+ return snapshotId(diffing, propName);
+ }),
+
+ // The current census data for the diffing.
+ census: censusModel,
+
+ // If an error was thrown while diffing, the `Error` instance is attached
+ // here.
+ error: PropTypes.object,
+
+ // The current state the diffing is in.
+ // @see ./constants.js
+ state: catchAndIgnore(function(diffing) {
+ switch (diffing.state) {
+ case diffingState.TOOK_DIFF:
+ assert(diffing.census, "If we took a diff, we should have a census");
+ // Fall through...
+ case diffingState.TAKING_DIFF:
+ assert(diffing.firstSnapshotId, "Should have first snapshot");
+ assert(diffing.secondSnapshotId, "Should have second snapshot");
+ break;
+
+ case diffingState.SELECTING:
+ break;
+
+ case diffingState.ERROR:
+ assert(diffing.error, "Should have error");
+ break;
+
+ default:
+ assert(false, `Bad diffing state: ${diffing.state}`);
+ }
+ }),
+}));
+
+const previousViewModel = (exports.previousView = PropTypes.shape({
+ state: catchAndIgnore(function(previous) {
+ switch (previous.state) {
+ case viewState.DIFFING:
+ assert(previous.diffing, "Should have previous diffing state.");
+ assert(
+ !previous.selected,
+ "Should not have a previously selected snapshot."
+ );
+ break;
+
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.TREE_MAP:
+ assert(
+ previous.selected,
+ "Should have a previously selected snapshot."
+ );
+ break;
+
+ case viewState.INDIVIDUALS:
+ default:
+ assert(false, `Unexpected previous view state: ${previous.state}.`);
+ }
+ }),
+
+ // The previous diffing state, if any.
+ diffing: diffingModel,
+
+ // The previously selected snapshot, if any.
+ selected: snapshotId,
+}));
+
+exports.view = PropTypes.shape({
+ // The current view state.
+ state: catchAndIgnore(function(view) {
+ switch (view.state) {
+ case viewState.DIFFING:
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.INDIVIDUALS:
+ case viewState.TREE_MAP:
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${view.state}`);
+ }
+ }),
+
+ // The previous view state.
+ previous: previousViewModel,
+});
+
+const individualsModel = (exports.individuals = PropTypes.shape({
+ error: PropTypes.object,
+
+ nodes: PropTypes.arrayOf(PropTypes.object),
+
+ dominatorTree: dominatorTreeModel,
+
+ id: snapshotId,
+
+ censusBreakdown: PropTypes.object,
+
+ indices: PropTypes.object,
+
+ labelDisplay: labelDisplayModel,
+
+ focused: PropTypes.object,
+
+ state: catchAndIgnore(function(individuals) {
+ switch (individuals.state) {
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ case individualsState.FETCHING:
+ assert(!individuals.nodes, "Should not have individual nodes");
+ assert(!individuals.dominatorTree, "Should not have dominator tree");
+ assert(!individuals.id, "Should not have an id");
+ assert(
+ !individuals.censusBreakdown,
+ "Should not have a censusBreakdown"
+ );
+ assert(!individuals.indices, "Should not have indices");
+ assert(!individuals.labelDisplay, "Should not have a labelDisplay");
+ break;
+
+ case individualsState.FETCHED:
+ assert(individuals.nodes, "Should have individual nodes");
+ assert(individuals.dominatorTree, "Should have dominator tree");
+ assert(individuals.id, "Should have an id");
+ assert(individuals.censusBreakdown, "Should have a censusBreakdown");
+ assert(individuals.indices, "Should have indices");
+ assert(individuals.labelDisplay, "Should have a labelDisplay");
+ break;
+
+ case individualsState.ERROR:
+ assert(individuals.error, "Should have an error object");
+ break;
+
+ default:
+ assert(false, `Unexpected individuals state: ${individuals.state}`);
+ break;
+ }
+ }),
+}));
+
+exports.app = {
+ // {MemoryFront} Used to communicate with platform
+ front: PropTypes.instanceOf(MemoryFront),
+
+ // Allocations recording related data.
+ allocations: allocationsModel.isRequired,
+
+ // {HeapAnalysesClient} Used to interface with snapshots
+ heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
+
+ // The display data describing how we want the census data to be.
+ censusDisplay: censusDisplayModel.isRequired,
+
+ // The display data describing how we want the dominator tree labels to be
+ // computed.
+ labelDisplay: labelDisplayModel.isRequired,
+
+ // The display data describing how we want the dominator tree labels to be
+ // computed.
+ treeMapDisplay: treeMapDisplayModel.isRequired,
+
+ // List of reference to all snapshots taken
+ snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
+
+ // If present, a filter string for pruning the tree items.
+ filter: PropTypes.string,
+
+ // If present, the current diffing state.
+ diffing: diffingModel,
+
+ // If present, the current individuals state.
+ individuals: individualsModel,
+
+ // The current type of view.
+ view: function(app) {
+ catchAndIgnore(function(app) {
+ switch (app.view.state) {
+ case viewState.DIFFING:
+ assert(app.diffing, "Should be diffing");
+ break;
+
+ case viewState.INDIVIDUALS:
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.TREE_MAP:
+ assert(!app.diffing, "Should not be diffing");
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${app.view.state}`);
+ }
+ })(app);
+
+ catchAndIgnore(function(app) {
+ switch (app.view.state) {
+ case viewState.INDIVIDUALS:
+ assert(app.individuals, "Should have individuals state");
+ break;
+
+ case viewState.DIFFING:
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.TREE_MAP:
+ assert(!app.individuals, "Should not have individuals state");
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${app.view.state}`);
+ }
+ })(app);
+ },
+};
diff --git a/devtools/client/memory/moz.build b/devtools/client/memory/moz.build
new file mode 100644
index 0000000000..3e3282bd2e
--- /dev/null
+++ b/devtools/client/memory/moz.build
@@ -0,0 +1,29 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Memory")
+
+DIRS += [
+ "actions",
+ "components",
+ "reducers",
+]
+
+DevToolsModules(
+ "app.js",
+ "constants.js",
+ "dominator-tree-lazy-children.js",
+ "initializer.js",
+ "models.js",
+ "panel.js",
+ "reducers.js",
+ "store.js",
+ "utils.js",
+)
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome/chrome.ini"]
diff --git a/devtools/client/memory/panel.js b/devtools/client/memory/panel.js
new file mode 100644
index 0000000000..71728b3005
--- /dev/null
+++ b/devtools/client/memory/panel.js
@@ -0,0 +1,77 @@
+/* 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/. */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Cu } = require("chrome");
+const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
+
+function MemoryPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+
+ const { BrowserLoader } = Cu.import(
+ "resource://devtools/client/shared/browser-loader.js"
+ );
+ const browserRequire = BrowserLoader({
+ baseURI: "resource://devtools/client/memory/",
+ window: this.panelWin,
+ }).require;
+ this.initializer = browserRequire("devtools/client/memory/initializer");
+
+ this._onTargetAvailable = this._onTargetAvailable.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+MemoryPanel.prototype = {
+ async open() {
+ this.panelWin.gToolbox = this._toolbox;
+ this.panelWin.gHeapAnalysesClient = new HeapAnalysesClient();
+
+ await this.initializer.initialize();
+
+ await this._toolbox.targetList.watchTargets(
+ [this._toolbox.targetList.TYPES.FRAME],
+ this._onTargetAvailable
+ );
+
+ this.isReady = true;
+ this.emit("ready");
+
+ return this;
+ },
+
+ async _onTargetAvailable({ targetFront }) {
+ if (targetFront.isTopLevel) {
+ const front = await targetFront.getFront("memory");
+ await front.attach();
+ this.initializer.updateFront(front);
+ }
+ },
+
+ // DevToolPanel API
+
+ destroy() {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ this._toolbox.targetList.unwatchTargets(
+ [this._toolbox.targetList.TYPES.FRAME],
+ this._onTargetAvailable
+ );
+
+ this.initializer.destroy();
+
+ this.panelWin.gHeapAnalysesClient.destroy();
+ this.panelWin = null;
+ this.emit("destroyed");
+ },
+};
+
+exports.MemoryPanel = MemoryPanel;
diff --git a/devtools/client/memory/reducers.js b/devtools/client/memory/reducers.js
new file mode 100644
index 0000000000..ae7b670da8
--- /dev/null
+++ b/devtools/client/memory/reducers.js
@@ -0,0 +1,17 @@
+/* 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/. */
+"use strict";
+
+exports.allocations = require("devtools/client/memory/reducers/allocations");
+exports.censusDisplay = require("devtools/client/memory/reducers/census-display");
+exports.diffing = require("devtools/client/memory/reducers/diffing");
+exports.front = require("devtools/client/memory/reducers/front");
+exports.individuals = require("devtools/client/memory/reducers/individuals");
+exports.labelDisplay = require("devtools/client/memory/reducers/label-display");
+exports.treeMapDisplay = require("devtools/client/memory/reducers/tree-map-display");
+exports.errors = require("devtools/client/memory/reducers/errors");
+exports.filter = require("devtools/client/memory/reducers/filter");
+exports.sizes = require("devtools/client/memory/reducers/sizes");
+exports.snapshots = require("devtools/client/memory/reducers/snapshots");
+exports.view = require("devtools/client/memory/reducers/view");
diff --git a/devtools/client/memory/reducers/allocations.js b/devtools/client/memory/reducers/allocations.js
new file mode 100644
index 0000000000..8a6fba0e1c
--- /dev/null
+++ b/devtools/client/memory/reducers/allocations.js
@@ -0,0 +1,54 @@
+/* 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/. */
+
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("devtools/client/memory/constants");
+
+const handlers = Object.create(null);
+
+handlers[actions.TOGGLE_RECORD_ALLOCATION_STACKS_START] = function(
+ state,
+ action
+) {
+ assert(
+ !state.togglingInProgress,
+ "Changing recording state must not be reentrant."
+ );
+
+ return {
+ recording: !state.recording,
+ togglingInProgress: true,
+ };
+};
+
+handlers[actions.TOGGLE_RECORD_ALLOCATION_STACKS_END] = function(
+ state,
+ action
+) {
+ assert(
+ state.togglingInProgress,
+ "Should not complete changing recording state if we weren't changing " +
+ "recording state already."
+ );
+
+ return {
+ recording: state.recording,
+ togglingInProgress: false,
+ };
+};
+
+const DEFAULT_ALLOCATIONS_STATE = {
+ recording: false,
+ togglingInProgress: false,
+};
+
+module.exports = function(state = DEFAULT_ALLOCATIONS_STATE, action) {
+ const handle = handlers[action.type];
+ if (handle) {
+ return handle(state, action);
+ }
+ return state;
+};
diff --git a/devtools/client/memory/reducers/census-display.js b/devtools/client/memory/reducers/census-display.js
new file mode 100644
index 0000000000..e1809c9174
--- /dev/null
+++ b/devtools/client/memory/reducers/census-display.js
@@ -0,0 +1,21 @@
+/* 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/. */
+"use strict";
+
+const { actions, censusDisplays } = require("devtools/client/memory/constants");
+const DEFAULT_CENSUS_DISPLAY = censusDisplays.coarseType;
+
+const handlers = Object.create(null);
+
+handlers[actions.SET_CENSUS_DISPLAY] = function(_, { display }) {
+ return display;
+};
+
+module.exports = function(state = DEFAULT_CENSUS_DISPLAY, action) {
+ const handle = handlers[action.type];
+ if (handle) {
+ return handle(state, action);
+ }
+ return state;
+};
diff --git a/devtools/client/memory/reducers/diffing.js b/devtools/client/memory/reducers/diffing.js
new file mode 100644
index 0000000000..f4d0379b38
--- /dev/null
+++ b/devtools/client/memory/reducers/diffing.js
@@ -0,0 +1,170 @@
+/* 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/. */
+"use strict";
+
+const Immutable = require("devtools/client/shared/vendor/immutable");
+const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
+const {
+ actions,
+ diffingState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const { snapshotIsDiffable } = require("devtools/client/memory/utils");
+
+const handlers = Object.create(null);
+
+handlers[actions.POP_VIEW] = function(diffing, { previousView }) {
+ if (previousView.state === viewState.DIFFING) {
+ assert(previousView.diffing, "Should have previousView.diffing");
+ return previousView.diffing;
+ }
+
+ return null;
+};
+
+handlers[actions.CHANGE_VIEW] = function(diffing, { newViewState }) {
+ if (newViewState === viewState.DIFFING) {
+ assert(!diffing, "Should not switch to diffing view when already diffing");
+ return Object.freeze({
+ firstSnapshotId: null,
+ secondSnapshotId: null,
+ census: null,
+ state: diffingState.SELECTING,
+ });
+ }
+
+ return null;
+};
+
+handlers[actions.SELECT_SNAPSHOT_FOR_DIFFING] = function(
+ diffing,
+ { snapshot }
+) {
+ assert(
+ diffing,
+ "Should never select a snapshot for diffing when we aren't diffing " +
+ "anything"
+ );
+ assert(
+ diffing.state === diffingState.SELECTING,
+ "Can't select when not in SELECTING state"
+ );
+ assert(snapshotIsDiffable(snapshot), "snapshot must be in a diffable state");
+
+ if (!diffing.firstSnapshotId) {
+ return immutableUpdate(diffing, {
+ firstSnapshotId: snapshot.id,
+ });
+ }
+
+ assert(
+ !diffing.secondSnapshotId,
+ "If we aren't selecting the first, then we must be selecting the " +
+ "second"
+ );
+
+ if (snapshot.id === diffing.firstSnapshotId) {
+ // Ignore requests to select the same snapshot.
+ return diffing;
+ }
+
+ return immutableUpdate(diffing, {
+ secondSnapshotId: snapshot.id,
+ });
+};
+
+handlers[actions.TAKE_CENSUS_DIFF_START] = function(diffing, action) {
+ assert(diffing, "Should be diffing when starting a census diff");
+ assert(
+ action.first.id === diffing.firstSnapshotId,
+ "First snapshot's id should match"
+ );
+ assert(
+ action.second.id === diffing.secondSnapshotId,
+ "Second snapshot's id should match"
+ );
+
+ return immutableUpdate(diffing, {
+ state: diffingState.TAKING_DIFF,
+ census: {
+ report: null,
+ inverted: action.inverted,
+ filter: action.filter,
+ display: action.display,
+ },
+ });
+};
+
+handlers[actions.TAKE_CENSUS_DIFF_END] = function(diffing, action) {
+ assert(diffing, "Should be diffing when ending a census diff");
+ assert(
+ action.first.id === diffing.firstSnapshotId,
+ "First snapshot's id should match"
+ );
+ assert(
+ action.second.id === diffing.secondSnapshotId,
+ "Second snapshot's id should match"
+ );
+
+ return immutableUpdate(diffing, {
+ state: diffingState.TOOK_DIFF,
+ census: {
+ report: action.report,
+ parentMap: action.parentMap,
+ expanded: Immutable.Set(),
+ inverted: action.inverted,
+ filter: action.filter,
+ display: action.display,
+ },
+ });
+};
+
+handlers[actions.DIFFING_ERROR] = function(diffing, action) {
+ return {
+ state: diffingState.ERROR,
+ error: action.error,
+ };
+};
+
+handlers[actions.EXPAND_DIFFING_CENSUS_NODE] = function(diffing, { node }) {
+ assert(diffing, "Should be diffing if expanding diffing's census nodes");
+ assert(
+ diffing.state === diffingState.TOOK_DIFF,
+ "Should have taken the census diff if expanding nodes"
+ );
+ assert(diffing.census, "Should have a census");
+ assert(diffing.census.report, "Should have a census report");
+ assert(diffing.census.expanded, "Should have a census's expanded set");
+
+ const expanded = diffing.census.expanded.add(node.id);
+ const census = immutableUpdate(diffing.census, { expanded });
+ return immutableUpdate(diffing, { census });
+};
+
+handlers[actions.COLLAPSE_DIFFING_CENSUS_NODE] = function(diffing, { node }) {
+ assert(diffing, "Should be diffing if expanding diffing's census nodes");
+ assert(
+ diffing.state === diffingState.TOOK_DIFF,
+ "Should have taken the census diff if expanding nodes"
+ );
+ assert(diffing.census, "Should have a census");
+ assert(diffing.census.report, "Should have a census report");
+ assert(diffing.census.expanded, "Should have a census's expanded set");
+
+ const expanded = diffing.census.expanded.delete(node.id);
+ const census = immutableUpdate(diffing.census, { expanded });
+ return immutableUpdate(diffing, { census });
+};
+
+handlers[actions.FOCUS_DIFFING_CENSUS_NODE] = function(diffing, { node }) {
+ assert(diffing, "Should be diffing.");
+ assert(diffing.census, "Should have a census");
+ const census = immutableUpdate(diffing.census, { focused: node });
+ return immutableUpdate(diffing, { census });
+};
+
+module.exports = function(diffing = null, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(diffing, action) : diffing;
+};
diff --git a/devtools/client/memory/reducers/errors.js b/devtools/client/memory/reducers/errors.js
new file mode 100644
index 0000000000..8a7158e17d
--- /dev/null
+++ b/devtools/client/memory/reducers/errors.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+"use strict";
+
+const {
+ ERROR_TYPE: TASK_ERROR_TYPE,
+} = require("devtools/client/shared/redux/middleware/task");
+
+/**
+ * Handle errors dispatched from task middleware and
+ * store them so we can check in tests or dump them out.
+ */
+module.exports = function(state = [], action) {
+ switch (action.type) {
+ case TASK_ERROR_TYPE:
+ return [...state, action.error];
+ }
+ return state;
+};
diff --git a/devtools/client/memory/reducers/filter.js b/devtools/client/memory/reducers/filter.js
new file mode 100644
index 0000000000..b1fbcc95d3
--- /dev/null
+++ b/devtools/client/memory/reducers/filter.js
@@ -0,0 +1,14 @@
+/* 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/. */
+"use strict";
+
+const { actions } = require("devtools/client/memory/constants");
+
+module.exports = function(filterString = null, action) {
+ if (action.type === actions.SET_FILTER_STRING) {
+ return action.filter || null;
+ }
+
+ return filterString;
+};
diff --git a/devtools/client/memory/reducers/front.js b/devtools/client/memory/reducers/front.js
new file mode 100644
index 0000000000..1dd8f37d81
--- /dev/null
+++ b/devtools/client/memory/reducers/front.js
@@ -0,0 +1,11 @@
+/* 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/. */
+
+"use strict";
+
+const { actions } = require("devtools/client/memory/constants");
+
+module.exports = (front = null, action) => {
+ return action.type === actions.UPDATE_MEMORY_FRONT ? action.front : front;
+};
diff --git a/devtools/client/memory/reducers/individuals.js b/devtools/client/memory/reducers/individuals.js
new file mode 100644
index 0000000000..30d056c29f
--- /dev/null
+++ b/devtools/client/memory/reducers/individuals.js
@@ -0,0 +1,81 @@
+/* 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/. */
+"use strict";
+
+const { assert, immutableUpdate } = require("devtools/shared/DevToolsUtils");
+const {
+ actions,
+ individualsState,
+ viewState,
+} = require("devtools/client/memory/constants");
+
+const handlers = Object.create(null);
+
+handlers[actions.POP_VIEW] = function(_state, _action) {
+ return null;
+};
+
+handlers[actions.CHANGE_VIEW] = function(individuals, { newViewState }) {
+ if (newViewState === viewState.INDIVIDUALS) {
+ assert(
+ !individuals,
+ "Should not switch to individuals view when already in individuals view"
+ );
+ return Object.freeze({
+ state: individualsState.COMPUTING_DOMINATOR_TREE,
+ });
+ }
+
+ return null;
+};
+
+handlers[actions.FOCUS_INDIVIDUAL] = function(individuals, { node }) {
+ assert(individuals, "Should have individuals");
+ return immutableUpdate(individuals, { focused: node });
+};
+
+handlers[actions.FETCH_INDIVIDUALS_START] = function(individuals, action) {
+ assert(individuals, "Should have individuals");
+ return Object.freeze({
+ state: individualsState.FETCHING,
+ focused: individuals.focused,
+ });
+};
+
+handlers[actions.FETCH_INDIVIDUALS_END] = function(individuals, action) {
+ assert(individuals, "Should have individuals");
+ assert(!individuals.nodes, "Should not have nodes");
+ assert(
+ individuals.state === individualsState.FETCHING,
+ "Should only end fetching individuals after starting."
+ );
+
+ const focused = individuals.focused
+ ? action.nodes.find(n => n.nodeId === individuals.focused.nodeId)
+ : null;
+
+ return Object.freeze({
+ state: individualsState.FETCHED,
+ nodes: action.nodes,
+ id: action.id,
+ censusBreakdown: action.censusBreakdown,
+ indices: action.indices,
+ labelDisplay: action.labelDisplay,
+ focused,
+ dominatorTree: action.dominatorTree,
+ });
+};
+
+handlers[actions.INDIVIDUALS_ERROR] = function(_, { error }) {
+ return Object.freeze({
+ error,
+ nodes: null,
+ state: individualsState.ERROR,
+ });
+};
+
+module.exports = function(individuals = null, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(individuals, action) : individuals;
+};
diff --git a/devtools/client/memory/reducers/label-display.js b/devtools/client/memory/reducers/label-display.js
new file mode 100644
index 0000000000..f46ce3ce88
--- /dev/null
+++ b/devtools/client/memory/reducers/label-display.js
@@ -0,0 +1,19 @@
+/* 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/. */
+
+"use strict";
+
+const { actions, labelDisplays } = require("devtools/client/memory/constants");
+const DEFAULT_LABEL_DISPLAY = labelDisplays.coarseType;
+
+const handlers = Object.create(null);
+
+handlers[actions.SET_LABEL_DISPLAY] = function(_, { display }) {
+ return display;
+};
+
+module.exports = function(state = DEFAULT_LABEL_DISPLAY, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(state, action) : state;
+};
diff --git a/devtools/client/memory/reducers/moz.build b/devtools/client/memory/reducers/moz.build
new file mode 100644
index 0000000000..53677d1d8e
--- /dev/null
+++ b/devtools/client/memory/reducers/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "allocations.js",
+ "census-display.js",
+ "diffing.js",
+ "errors.js",
+ "filter.js",
+ "front.js",
+ "individuals.js",
+ "label-display.js",
+ "sizes.js",
+ "snapshots.js",
+ "tree-map-display.js",
+ "view.js",
+)
diff --git a/devtools/client/memory/reducers/sizes.js b/devtools/client/memory/reducers/sizes.js
new file mode 100644
index 0000000000..6b51b61c3a
--- /dev/null
+++ b/devtools/client/memory/reducers/sizes.js
@@ -0,0 +1,18 @@
+/* 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/. */
+"use strict";
+
+const { actions } = require("devtools/client/memory/constants");
+const { immutableUpdate } = require("devtools/shared/DevToolsUtils");
+
+const handlers = Object.create(null);
+
+handlers[actions.RESIZE_SHORTEST_PATHS] = function(sizes, { size }) {
+ return immutableUpdate(sizes, { shortestPathsSize: size });
+};
+
+module.exports = function(sizes = { shortestPathsSize: 0.5 }, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(sizes, action) : sizes;
+};
diff --git a/devtools/client/memory/reducers/snapshots.js b/devtools/client/memory/reducers/snapshots.js
new file mode 100644
index 0000000000..5ee724de2f
--- /dev/null
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -0,0 +1,511 @@
+/* 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/. */
+"use strict";
+
+const Immutable = require("devtools/client/shared/vendor/immutable");
+const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
+const {
+ actions,
+ snapshotState: states,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode");
+
+const handlers = Object.create(null);
+
+handlers[actions.SNAPSHOT_ERROR] = function(snapshots, { id, error }) {
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { state: states.ERROR, error })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_SNAPSHOT_START] = function(snapshots, { snapshot }) {
+ return [...snapshots, snapshot];
+};
+
+handlers[actions.TAKE_SNAPSHOT_END] = function(snapshots, { id, path }) {
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { state: states.SAVED, path })
+ : snapshot;
+ });
+};
+
+handlers[actions.IMPORT_SNAPSHOT_START] = handlers[actions.TAKE_SNAPSHOT_START];
+
+handlers[actions.READ_SNAPSHOT_START] = function(snapshots, { id }) {
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { state: states.READING })
+ : snapshot;
+ });
+};
+
+handlers[actions.READ_SNAPSHOT_END] = function(
+ snapshots,
+ { id, creationTime }
+) {
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { state: states.READ, creationTime })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_CENSUS_START] = function(
+ snapshots,
+ { id, display, filter }
+) {
+ const census = {
+ report: null,
+ display,
+ filter,
+ state: censusState.SAVING,
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { census })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_CENSUS_END] = function(
+ snapshots,
+ { id, report, parentMap, display, filter }
+) {
+ const census = {
+ report,
+ parentMap,
+ expanded: Immutable.Set(),
+ display,
+ filter,
+ state: censusState.SAVED,
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { census })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_CENSUS_ERROR] = function(snapshots, { id, error }) {
+ assert(error, "actions with TAKE_CENSUS_ERROR should have an error");
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ const census = Object.freeze({
+ state: censusState.ERROR,
+ error,
+ });
+
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.TAKE_TREE_MAP_START] = function(snapshots, { id, display }) {
+ const treeMap = {
+ report: null,
+ display,
+ state: treeMapState.SAVING,
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { treeMap })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_TREE_MAP_END] = function(snapshots, action) {
+ const { id, report, display } = action;
+ const treeMap = {
+ report,
+ display,
+ state: treeMapState.SAVED,
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { treeMap })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_TREE_MAP_ERROR] = function(snapshots, { id, error }) {
+ assert(error, "actions with TAKE_TREE_MAP_ERROR should have an error");
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ const treeMap = Object.freeze({
+ state: treeMapState.ERROR,
+ error,
+ });
+
+ return immutableUpdate(snapshot, { treeMap });
+ });
+};
+
+handlers[actions.EXPAND_CENSUS_NODE] = function(snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.census, "Should have a census");
+ assert(snapshot.census.report, "Should have a census report");
+ assert(snapshot.census.expanded, "Should have a census's expanded set");
+
+ const expanded = snapshot.census.expanded.add(node.id);
+ const census = immutableUpdate(snapshot.census, { expanded });
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.COLLAPSE_CENSUS_NODE] = function(snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.census, "Should have a census");
+ assert(snapshot.census.report, "Should have a census report");
+ assert(snapshot.census.expanded, "Should have a census's expanded set");
+
+ const expanded = snapshot.census.expanded.delete(node.id);
+ const census = immutableUpdate(snapshot.census, { expanded });
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.FOCUS_CENSUS_NODE] = function(snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.census, "Should have a census");
+ const census = immutableUpdate(snapshot.census, { focused: node });
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.SELECT_SNAPSHOT] = function(snapshots, { id }) {
+ return snapshots.map(s => immutableUpdate(s, { selected: s.id === id }));
+};
+
+handlers[actions.DELETE_SNAPSHOTS_START] = function(snapshots, { ids }) {
+ return snapshots.filter(s => !ids.includes(s.id));
+};
+
+handlers[actions.DELETE_SNAPSHOTS_END] = function(snapshots) {
+ return snapshots;
+};
+
+handlers[actions.CHANGE_VIEW] = function(snapshots, { newViewState }) {
+ return newViewState === viewState.DIFFING
+ ? snapshots.map(s => immutableUpdate(s, { selected: false }))
+ : snapshots;
+};
+
+handlers[actions.POP_VIEW] = function(snapshots, { previousView }) {
+ return snapshots.map(s =>
+ immutableUpdate(s, {
+ selected: s.id === previousView.selected,
+ })
+ );
+};
+
+handlers[actions.COMPUTE_DOMINATOR_TREE_START] = function(snapshots, { id }) {
+ const dominatorTree = Object.freeze({
+ state: dominatorTreeState.COMPUTING,
+ dominatorTreeId: undefined,
+ root: undefined,
+ });
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(!snapshot.dominatorTree, "Should not have a dominator tree model");
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.COMPUTE_DOMINATOR_TREE_END] = function(
+ snapshots,
+ { id, dominatorTreeId }
+) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(
+ snapshot.dominatorTree.state == dominatorTreeState.COMPUTING,
+ "Should be in the COMPUTING state"
+ );
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.COMPUTED,
+ dominatorTreeId,
+ });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_DOMINATOR_TREE_START] = function(
+ snapshots,
+ { id, display }
+) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(
+ snapshot.dominatorTree.state !== dominatorTreeState.COMPUTING &&
+ snapshot.dominatorTree.state !== dominatorTreeState.ERROR,
+ "Should have already computed the dominator tree, found state = " +
+ snapshot.dominatorTree.state
+ );
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.FETCHING,
+ root: undefined,
+ display,
+ });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_DOMINATOR_TREE_END] = function(snapshots, { id, root }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(
+ snapshot.dominatorTree.state == dominatorTreeState.FETCHING,
+ "Should be in the FETCHING state"
+ );
+
+ let focused;
+ if (snapshot.dominatorTree.focused) {
+ focused = (function findFocused(node) {
+ if (node.nodeId === snapshot.dominatorTree.focused.nodeId) {
+ return node;
+ }
+
+ if (node.children) {
+ const length = node.children.length;
+ for (let i = 0; i < length; i++) {
+ const result = findFocused(node.children[i]);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ return undefined;
+ })(root);
+ }
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.LOADED,
+ root,
+ expanded: Immutable.Set(),
+ focused,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.EXPAND_DOMINATOR_TREE_NODE] = function(
+ snapshots,
+ { id, node }
+) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+ assert(
+ snapshot.dominatorTree.expanded,
+ "Should have the dominator tree's expanded set"
+ );
+
+ const expanded = snapshot.dominatorTree.expanded.add(node.nodeId);
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.COLLAPSE_DOMINATOR_TREE_NODE] = function(
+ snapshots,
+ { id, node }
+) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+ assert(
+ snapshot.dominatorTree.expanded,
+ "Should have the dominator tree's expanded set"
+ );
+
+ const expanded = snapshot.dominatorTree.expanded.delete(node.nodeId);
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FOCUS_DOMINATOR_TREE_NODE] = function(
+ snapshots,
+ { id, node }
+) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ focused: node,
+ });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_IMMEDIATELY_DOMINATED_START] = function(
+ snapshots,
+ { id }
+) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(
+ snapshot.dominatorTree.state == dominatorTreeState.INCREMENTAL_FETCHING ||
+ snapshot.dominatorTree.state == dominatorTreeState.LOADED,
+ "The dominator tree should be loaded if we are going to " +
+ "incrementally fetch children."
+ );
+
+ const activeFetchRequestCount = snapshot.dominatorTree
+ .activeFetchRequestCount
+ ? snapshot.dominatorTree.activeFetchRequestCount + 1
+ : 1;
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.INCREMENTAL_FETCHING,
+ activeFetchRequestCount,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_IMMEDIATELY_DOMINATED_END] = function(
+ snapshots,
+ { id, path, nodes, moreChildrenAvailable }
+) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(
+ snapshot.dominatorTree.root,
+ "Should have a dominator tree model root"
+ );
+ assert(
+ snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
+ "The dominator tree state should be INCREMENTAL_FETCHING"
+ );
+
+ const root = DominatorTreeNode.insert(
+ snapshot.dominatorTree.root,
+ path,
+ nodes,
+ moreChildrenAvailable
+ );
+
+ const focused = snapshot.dominatorTree.focused
+ ? DominatorTreeNode.getNodeByIdAlongPath(
+ snapshot.dominatorTree.focused.nodeId,
+ root,
+ path
+ )
+ : undefined;
+
+ const activeFetchRequestCount =
+ snapshot.dominatorTree.activeFetchRequestCount === 1
+ ? undefined
+ : snapshot.dominatorTree.activeFetchRequestCount - 1;
+
+ // If there are still outstanding requests, we need to stay in the
+ // INCREMENTAL_FETCHING state until they complete.
+ const state = activeFetchRequestCount
+ ? dominatorTreeState.INCREMENTAL_FETCHING
+ : dominatorTreeState.LOADED;
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state,
+ root,
+ focused,
+ activeFetchRequestCount,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.DOMINATOR_TREE_ERROR] = function(snapshots, { id, error }) {
+ assert(error, "actions with DOMINATOR_TREE_ERROR should have an error");
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ const dominatorTree = Object.freeze({
+ state: dominatorTreeState.ERROR,
+ error,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+module.exports = function(snapshots = [], action) {
+ const handler = handlers[action.type];
+ if (handler) {
+ return handler(snapshots, action);
+ }
+ return snapshots;
+};
diff --git a/devtools/client/memory/reducers/tree-map-display.js b/devtools/client/memory/reducers/tree-map-display.js
new file mode 100644
index 0000000000..233e26a099
--- /dev/null
+++ b/devtools/client/memory/reducers/tree-map-display.js
@@ -0,0 +1,22 @@
+/* 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/. */
+
+"use strict";
+
+const {
+ actions,
+ treeMapDisplays,
+} = require("devtools/client/memory/constants");
+const DEFAULT_TREE_MAP_DISPLAY = treeMapDisplays.coarseType;
+
+const handlers = Object.create(null);
+
+handlers[actions.SET_TREE_MAP_DISPLAY] = function(_, { display }) {
+ return display;
+};
+
+module.exports = function(state = DEFAULT_TREE_MAP_DISPLAY, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(state, action) : state;
+};
diff --git a/devtools/client/memory/reducers/view.js b/devtools/client/memory/reducers/view.js
new file mode 100644
index 0000000000..0fc395b0d3
--- /dev/null
+++ b/devtools/client/memory/reducers/view.js
@@ -0,0 +1,49 @@
+/* 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/. */
+"use strict";
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { actions, viewState } = require("devtools/client/memory/constants");
+
+const handlers = Object.create(null);
+
+handlers[actions.POP_VIEW] = function(view, _) {
+ assert(view.previous, "Had better have a previous view state when POP_VIEW");
+ return Object.freeze({
+ state: view.previous.state,
+ previous: null,
+ });
+};
+
+handlers[actions.CHANGE_VIEW] = function(view, action) {
+ const { newViewState, oldDiffing, oldSelected } = action;
+ assert(newViewState);
+
+ if (newViewState === viewState.INDIVIDUALS) {
+ assert(oldDiffing || oldSelected);
+ return Object.freeze({
+ state: newViewState,
+ previous: Object.freeze({
+ state: view.state,
+ selected: oldSelected,
+ diffing: oldDiffing,
+ }),
+ });
+ }
+
+ return Object.freeze({
+ state: newViewState,
+ previous: null,
+ });
+};
+
+const DEFAULT_VIEW = {
+ state: viewState.TREE_MAP,
+ previous: null,
+};
+
+module.exports = function(view = DEFAULT_VIEW, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(view, action) : view;
+};
diff --git a/devtools/client/memory/store.js b/devtools/client/memory/store.js
new file mode 100644
index 0000000000..6195140cd8
--- /dev/null
+++ b/devtools/client/memory/store.js
@@ -0,0 +1,15 @@
+/* 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/. */
+
+"use strict";
+
+const createStore = require("devtools/client/shared/redux/create-store");
+const reducers = require("devtools/client/memory/reducers");
+
+module.exports = () =>
+ createStore(reducers, {
+ enableTaskMiddleware: true,
+ // Uncomment this for logging in tests.
+ // shouldLog: true,
+ });
diff --git a/devtools/client/memory/test/browser/.eslintrc.js b/devtools/client/memory/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..e9ac813b19
--- /dev/null
+++ b/devtools/client/memory/test/browser/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../../.eslintrc.mochitests.js",
+ rules: {
+ "no-unused-vars": ["error", { vars: "local", args: "none" }],
+ },
+};
diff --git a/devtools/client/memory/test/browser/browser.ini b/devtools/client/memory/test/browser/browser.ini
new file mode 100644
index 0000000000..798ee8e052
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser.ini
@@ -0,0 +1,36 @@
+[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/shared-redux-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+
+[browser_memory_allocationStackDisplay_01.js]
+skip-if = debug # bug 1219554
+[browser_memory_displays_01.js]
+[browser_memory_clear_snapshots.js]
+[browser_memory_diff_01.js]
+[browser_memory_dominator_trees_01.js]
+skip-if = ccov # bug 1347244
+[browser_memory_dominator_trees_02.js]
+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]
+[browser_memory_keyboard.js]
+[browser_memory_keyboard-snapshot-list.js]
+[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..472b6b6cc3
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js
@@ -0,0 +1,50 @@
+/* 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("devtools/client/memory/actions/allocations");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const censusDisplayActions = require("devtools/client/memory/actions/census-display");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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(front));
+ 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_clear_snapshots.js b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
new file mode 100644
index 0000000000..878a843d73
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
@@ -0,0 +1,76 @@
+/* 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("devtools/client/memory/constants");
+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..3a21040f40
--- /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("devtools/client/memory/constants");
+
+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..d49cba8eca
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_displays_01.js
@@ -0,0 +1,47 @@
+/* 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("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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::Shape", "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..48ac43380b
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js
@@ -0,0 +1,176 @@
+/* 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("devtools/client/memory/constants");
+const {
+ expandDominatorTreeNode,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..1a79ee82c2
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js
@@ -0,0 +1,79 @@
+/* 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("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..a38aed51d8
--- /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("devtools/client/memory/constants");
+const {
+ changeViewAndRefresh,
+ changeView,
+} = require("devtools/client/memory/actions/view");
+
+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..1b057bf4da
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_fission_switch_target.js
@@ -0,0 +1,87 @@
+/* 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("devtools/client/memory/constants");
+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 { gToolbox: toolbox, gStore: store } = panel.panelWin;
+
+ info("Open a page running on the content process");
+ BrowserTestUtils.loadURI(tab.linkedBrowser, CONTENT_PROCESS_URI);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ 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, toolbox, tab);
+ 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, toolbox, tab);
+ 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());
+}
+
+async function navigateTo(uri, toolbox, tab) {
+ const onSwitched = toolbox.targetList.once("switched-target");
+ const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ BrowserTestUtils.loadURI(tab.linkedBrowser, uri);
+ await onLoaded;
+ await onSwitched;
+ ok(true, "switched-target event is fired");
+}
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..2c0694fbc2
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_individuals_01.js
@@ -0,0 +1,72 @@
+/* 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("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..de634b4d41
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js
@@ -0,0 +1,110 @@
+/* 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("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..a74d217ea0
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard.js
@@ -0,0 +1,109 @@
+/* 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("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..8744b89f36
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.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 {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const censusDisplayActions = require("devtools/client/memory/actions/census-display");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..638ad517e4
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
@@ -0,0 +1,47 @@
+/* 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("devtools/client/memory/actions/snapshot");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..2b0c7b1820
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_percents_01.js
@@ -0,0 +1,60 @@
+/* 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("devtools/client/memory/actions/snapshot");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL =
+ "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+
+function checkCells(cells) {
+ ok(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..eebb7d8b9b
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js
@@ -0,0 +1,136 @@
+/* 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("devtools/shared/heapsnapshot/DominatorTreeNode");
+
+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 }) {
+ const 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 refreshTab();
+
+ 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..70e9aff44c
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_simple_01.js
@@ -0,0 +1,58 @@
+/* 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("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..3013260b81
--- /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 OS.File.stat(snapshotFilePath)),
+ "Should have the heap snapshot file"
+ );
+
+ const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath);
+ ok(
+ snapshot instanceof HeapSnapshot,
+ "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..281daf1b6c
--- /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("devtools/client/memory/components/tree-map/canvas-utils");
+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..012ec16160
--- /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("devtools/client/memory/components/tree-map/canvas-utils");
+const DragZoom = require("devtools/client/memory/components/tree-map/drag-zoom");
+
+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..b15402d3e6
--- /dev/null
+++ b/devtools/client/memory/test/browser/head.js
@@ -0,0 +1,279 @@
+/* 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.
+/* import-globals-from ../../../shared/test/shared-head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+// Load the shared Redux helpers into this compartment.
+/* import-globals-from ../../../shared/test/shared-redux-head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-redux-head.js",
+ this
+);
+
+var {
+ censusDisplays,
+ censusState,
+ snapshotState: states,
+} = require("devtools/client/memory/constants");
+var { L10N } = require("devtools/client/memory/utils");
+
+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 target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target, "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 target = await TargetFactory.forTab(tab);
+ const toolbox = gDevTools.getToolbox(target);
+ 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("devtools/client/memory/actions/census-display").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/.eslintrc.js b/devtools/client/memory/test/chrome/.eslintrc.js
new file mode 100644
index 0000000000..e9ac813b19
--- /dev/null
+++ b/devtools/client/memory/test/chrome/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../../.eslintrc.mochitests.js",
+ rules: {
+ "no-unused-vars": ["error", { vars: "local", args: "none" }],
+ },
+};
diff --git a/devtools/client/memory/test/chrome/chrome.ini b/devtools/client/memory/test/chrome/chrome.ini
new file mode 100644
index 0000000000..7803bcda35
--- /dev/null
+++ b/devtools/client/memory/test/chrome/chrome.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_CensusTreeItem_01.html]
+[test_DominatorTree_01.html]
+[test_DominatorTree_02.html]
+[test_DominatorTree_03.html]
+[test_DominatorTreeItem_01.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..b757c6c6a2
--- /dev/null
+++ b/devtools/client/memory/test/chrome/head.js
@@ -0,0 +1,349 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/client/shared/browser-loader.js"
+);
+var { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/memory/",
+ window,
+});
+var { Assert } = require("resource://testing-common/Assert.jsm");
+var Services = require("Services");
+
+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("devtools/shared/DevToolsUtils");
+var { immutableUpdate } = DevToolsUtils;
+
+var constants = require("devtools/client/memory/constants");
+var {
+ censusDisplays,
+ diffingState,
+ labelDisplays,
+ dominatorTreeState,
+ snapshotState,
+ viewState,
+ censusState,
+} = constants;
+
+const { L10N } = require("devtools/client/memory/utils");
+
+var models = require("devtools/client/memory/models");
+
+var Immutable = require("devtools/client/shared/vendor/immutable");
+var React = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+var ReactDOM = require("devtools/client/shared/vendor/react-dom");
+var { createFactory } = React;
+var Heap = createFactory(require("devtools/client/memory/components/Heap"));
+var CensusTreeItem = createFactory(
+ require("devtools/client/memory/components/CensusTreeItem")
+);
+var DominatorTreeComponent = createFactory(
+ require("devtools/client/memory/components/DominatorTree")
+);
+var DominatorTreeItem = createFactory(
+ require("devtools/client/memory/components/DominatorTreeItem")
+);
+var ShortestPaths = createFactory(
+ require("devtools/client/memory/components/ShortestPaths")
+);
+var TreeMap = createFactory(
+ require("devtools/client/memory/components/TreeMap")
+);
+var SnapshotListItem = createFactory(
+ require("devtools/client/memory/components/SnapshotListItem")
+);
+var List = createFactory(require("devtools/client/memory/components/List"));
+var Toolbar = createFactory(
+ require("devtools/client/memory/components/Toolbar")
+);
+
+// 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..2d47fef7c3
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/head.js
@@ -0,0 +1,155 @@
+/* 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-redux-head.js */
+
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+
+var Services = require("Services");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+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 { OS } = require("resource://gre/modules/osfile.jsm");
+var { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
+var { TargetFactory } = require("devtools/client/framework/target");
+var promise = require("promise");
+var { expectState } = require("devtools/server/actors/common");
+var HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
+var HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
+var { addDebuggerToGlobal } = require("resource://gre/modules/jsdebugger.jsm");
+var Store = require("devtools/client/memory/store");
+var { L10N } = require("devtools/client/memory/utils");
+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.recordingAllocations = false;
+ 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() {
+ this.recordingAllocations = true;
+ }
+);
+
+StubbedMemoryFront.prototype.stopRecordingAllocations = expectState(
+ "attached",
+ async function() {
+ this.recordingAllocations = false;
+ }
+);
+
+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 = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ const destPath = file.path;
+ const stat = await OS.File.stat(destPath);
+ ok(stat.size === 0, "new file is 0 bytes at start");
+ return destPath;
+}
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..748083a899
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_01.js
@@ -0,0 +1,39 @@
+/* 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("devtools/client/memory/actions/snapshot");
+const { actions } = require("devtools/client/memory/constants");
+const { treeMapState } = require("devtools/client/memory/constants");
+
+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([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(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..9fdc334720
--- /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("devtools/client/memory/actions/snapshot");
+const {
+ snapshotState: states,
+ treeMapState,
+ actions,
+} = require("devtools/client/memory/constants");
+
+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([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(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..f7b5c4ccd6
--- /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("devtools/client/memory/actions/snapshot");
+const {
+ snapshotState: states,
+ treeMapState,
+ actions,
+} = require("devtools/client/memory/constants");
+
+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([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(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..6414e3329c
--- /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("devtools/client/memory/actions/snapshot");
+const {
+ snapshotState: states,
+ actions,
+ treeMapState,
+} = require("devtools/client/memory/constants");
+
+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([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(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..069ef83bd9
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_05.js
@@ -0,0 +1,50 @@
+/* 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("devtools/client/memory/actions/snapshot");
+const { actions, treeMapState } = require("devtools/client/memory/constants");
+
+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: function() {
+ return Promise.reject("_");
+ },
+ };
+
+ ok(true, "dispatch clearSnapshots action");
+ const deleteEvents = Promise.all([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_END),
+ waitUntilAction(store, actions.SNAPSHOT_ERROR),
+ waitUntilAction(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..ea1a8901fe
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_06.js
@@ -0,0 +1,61 @@
+/* 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("devtools/client/memory/actions/snapshot");
+const { actions, treeMapState } = require("devtools/client/memory/constants");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffingAndRefresh,
+} = require("devtools/client/memory/actions/diffing");
+
+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 waitUntilAction(store, actions.TAKE_CENSUS_DIFF_END);
+ ok(true, "Received TAKE_CENSUS_DIFF_END action");
+
+ ok(true, "Dispatch clearSnapshots action");
+ const deleteEvents = Promise.all([
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
+ waitUntilAction(store, actions.DELETE_SNAPSHOTS_END),
+ ]);
+ dispatch(clearSnapshots(heapWorker));
+ await deleteEvents;
+ ok(true, "received delete snapshots events");
+
+ ok(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..cd3367dd6c
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-export-snapshot.js
@@ -0,0 +1,40 @@
+/* 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("devtools/client/memory/actions/io");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { actions, treeMapState } = require("devtools/client/memory/constants");
+
+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([
+ waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
+ waitUntilAction(store, actions.EXPORT_SNAPSHOT_END),
+ ]);
+ dispatch(exportSnapshot(getState().snapshots[0], destPath));
+ await exportEvents;
+
+ const stat = await OS.File.stat(destPath);
+ info(stat.size);
+ ok(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..237b6071d1
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-filter-01.js
@@ -0,0 +1,21 @@
+/* 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("devtools/client/memory/actions/filter");
+
+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..0be90292ea
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-filter-02.js
@@ -0,0 +1,83 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setFilterStringAndRefresh,
+} = require("devtools/client/memory/actions/filter");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..da2deabde8
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-filter-03.js
@@ -0,0 +1,67 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setFilterString,
+ setFilterStringAndRefresh,
+} = require("devtools/client/memory/actions/filter");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..fede0351b9
--- /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("devtools/client/memory/constants");
+const {
+ exportSnapshot,
+ importSnapshotAndCensus,
+} = require("devtools/client/memory/actions/io");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+
+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([
+ waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
+ waitUntilAction(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..559ffd3961
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-import-snapshot-dominator-tree.js
@@ -0,0 +1,98 @@
+/* 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("devtools/client/memory/constants");
+const {
+ importSnapshotAndCensus,
+} = require("devtools/client/memory/actions/io");
+const { changeViewAndRefresh } = require("devtools/client/memory/actions/view");
+
+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..5047ac2ff1
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-select-snapshot.js
@@ -0,0 +1,44 @@
+/* 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("devtools/client/memory/actions/snapshot");
+const { snapshotState: states } = require("devtools/client/memory/constants");
+
+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..10a2cb16a5
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-01.js
@@ -0,0 +1,180 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setCensusDisplayAndRefresh,
+} = require("devtools/client/memory/actions/census-display");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+// 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
+ ok(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..948760e082
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-02.js
@@ -0,0 +1,69 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setCensusDisplayAndRefresh,
+} = require("devtools/client/memory/actions/census-display");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..5b4bc58d3c
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-set-display.js
@@ -0,0 +1,72 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setCensusDisplay,
+} = require("devtools/client/memory/actions/census-display");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+// 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..edc5011ca7
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-take-census.js
@@ -0,0 +1,68 @@
+/* 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("devtools/client/memory/constants");
+var actions = require("devtools/client/memory/actions/snapshot");
+var { changeView } = require("devtools/client/memory/actions/view");
+
+// 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..e58a34e657
--- /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("devtools/client/memory/constants");
+const actions = require("devtools/client/memory/actions/snapshot");
+
+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..5df2356624
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-take-snapshot.js
@@ -0,0 +1,59 @@
+/* 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("devtools/client/memory/actions/snapshot");
+const { snapshotState: states } = require("devtools/client/memory/constants");
+
+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..10c2fa3dbd
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-01.js
@@ -0,0 +1,96 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setCensusDisplayAndRefresh,
+} = require("devtools/client/memory/actions/census-display");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..a15795733b
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-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 changing inverted state in the middle of taking a snapshot results
+// in an inverted census.
+
+const {
+ censusDisplays,
+ snapshotState: states,
+ censusState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ setCensusDisplay,
+ setCensusDisplayAndRefresh,
+} = require("devtools/client/memory/actions/census-display");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..98286101a7
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted.js
@@ -0,0 +1,29 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setCensusDisplay,
+} = require("devtools/client/memory/actions/census-display");
+
+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..7d5c369480
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action-toggle-recording-allocations.js
@@ -0,0 +1,44 @@
+/* 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("devtools/client/memory/actions/allocations");
+
+add_task(async function() {
+ const front = new StubbedMemoryFront();
+ await front.attach();
+ 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(front));
+ 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");
+ ok(front.recordingAllocations, "front is recording too");
+
+ dispatch(toggleRecordingAllocationStacks(front));
+ 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");
+ ok(front.recordingAllocations, "front is not recording anymore");
+
+ 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..121983206c
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_01.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling of diffing.
+
+const { toggleDiffing } = require("devtools/client/memory/actions/diffing");
+
+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..3864979ef9
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_02.js
@@ -0,0 +1,52 @@
+/* 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("devtools/client/memory/constants");
+const { toggleDiffing } = require("devtools/client/memory/actions/diffing");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..95143fea08
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_03.js
@@ -0,0 +1,143 @@
+/* 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("devtools/client/memory/constants");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffing,
+} = require("devtools/client/memory/actions/diffing");
+const { takeSnapshot } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+// 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..7f15d8f014
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_04.js
@@ -0,0 +1,99 @@
+/* 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("devtools/client/memory/constants");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffingAndRefresh,
+} = require("devtools/client/memory/actions/diffing");
+const {
+ takeSnapshot,
+ readSnapshot,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..eeeb8ccbf0
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_action_diffing_05.js
@@ -0,0 +1,132 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setCensusDisplayAndRefresh,
+} = require("devtools/client/memory/actions/census-display");
+const {
+ toggleDiffing,
+ selectSnapshotForDiffingAndRefresh,
+} = require("devtools/client/memory/actions/diffing");
+const {
+ setFilterStringAndRefresh,
+} = require("devtools/client/memory/actions/filter");
+const {
+ takeSnapshot,
+ readSnapshot,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..21130ecf74
--- /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("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+ computeAndFetchDominatorTree,
+} = require("devtools/client/memory/actions/snapshot");
+
+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..22949133b3
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_02.js
@@ -0,0 +1,78 @@
+/* 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("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeViewAndRefresh } = require("devtools/client/memory/actions/view");
+
+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..e845437b98
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_03.js
@@ -0,0 +1,75 @@
+/* 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("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..0cb37b114f
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_04.js
@@ -0,0 +1,89 @@
+/* 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("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..862f7c258e
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_05.js
@@ -0,0 +1,64 @@
+/* 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("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("devtools/client/memory/actions/snapshot");
+
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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..1fa398b7f6
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_06.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can incrementally fetch a subtree of a dominator tree.
+
+const {
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+ fetchImmediatelyDominated,
+} = require("devtools/client/memory/actions/snapshot");
+const DominatorTreeLazyChildren = require("devtools/client/memory/dominator-tree-lazy-children");
+
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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"
+ );
+ ok(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;
+ ok(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");
+ ok(
+ 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..aad4f88f3c
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_07.js
@@ -0,0 +1,172 @@
+/* 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("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+ fetchImmediatelyDominated,
+} = require("devtools/client/memory/actions/snapshot");
+const DominatorTreeLazyChildren = require("devtools/client/memory/dominator-tree-lazy-children");
+
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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.");
+ ok(
+ 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;
+ ok(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");
+ ok(
+ 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");
+ ok(
+ 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..6dd7f35be0
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_08.js
@@ -0,0 +1,98 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setLabelDisplayAndRefresh,
+} = require("devtools/client/memory/actions/label-display");
+const { changeView } = require("devtools/client/memory/actions/view");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+
+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..6edee44f49
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_09.js
@@ -0,0 +1,90 @@
+/* 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("devtools/client/memory/constants");
+const {
+ setLabelDisplayAndRefresh,
+} = require("devtools/client/memory/actions/label-display");
+const { changeView } = require("devtools/client/memory/actions/view");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+
+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..036e40775b
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_10.js
@@ -0,0 +1,93 @@
+/* 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("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+ focusDominatorTreeNode,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+const {
+ setLabelDisplayAndRefresh,
+} = require("devtools/client/memory/actions/label-display");
+
+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..f317f15a73
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_01.js
@@ -0,0 +1,75 @@
+/* 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("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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 > 0,
+ "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..5cafb00ba7
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_02.js
@@ -0,0 +1,89 @@
+/* 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("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+ computeDominatorTree,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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 > 0,
+ "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..882f3641d4
--- /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("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+ popViewAndRefresh,
+} = require("devtools/client/memory/actions/view");
+const {
+ selectSnapshotForDiffingAndRefresh,
+} = require("devtools/client/memory/actions/diffing");
+
+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 > 0,
+ "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..197419abd8
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_04.js
@@ -0,0 +1,89 @@
+/* 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("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+const { setFilterString } = require("devtools/client/memory/actions/filter");
+
+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 > 0,
+ "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..bff4539f25
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_05.js
@@ -0,0 +1,82 @@
+/* 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("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+const {
+ setCensusDisplay,
+} = require("devtools/client/memory/actions/census-display");
+
+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 > 0,
+ "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..f20113693e
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_individuals_06.js
@@ -0,0 +1,83 @@
+/* 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("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+ clearSnapshots,
+} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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 > 0,
+ "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..d9508c6c2b
--- /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("devtools/client/memory/constants");
+const {
+ fetchIndividuals,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView,
+ popViewAndRefresh,
+} = require("devtools/client/memory/actions/view");
+
+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..12d21777fc
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_tree-map-01.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { drawBox } = require("devtools/client/memory/components/tree-map/draw");
+
+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..4f16e60907
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_tree-map-02.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { drawText } = require("devtools/client/memory/components/tree-map/draw");
+
+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..c943b08e67
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_utils-get-snapshot-totals.js
@@ -0,0 +1,99 @@
+/* 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("devtools/client/memory/constants");
+const { getSnapshotTotals } = require("devtools/client/memory/utils");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ setCensusDisplayAndRefresh,
+} = require("devtools/client/memory/actions/census-display");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+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;
+
+ ok(totalBytes > 0, "counted up bytes in the census");
+ ok(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..eb2d293978
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/test_utils.js
@@ -0,0 +1,114 @@
+/* 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("devtools/client/memory/utils");
+const {
+ snapshotState: states,
+ viewState,
+} = require("devtools/client/memory/constants");
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+
+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"
+ );
+ ok(
+ 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.ini b/devtools/client/memory/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..f88990263c
--- /dev/null
+++ b/devtools/client/memory/test/xpcshell/xpcshell.ini
@@ -0,0 +1,58 @@
+[DEFAULT]
+tags = devtools devtools-memory
+head = head.js ../../../shared/test/shared-redux-head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[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_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.js]
+[test_action-set-display-and-refresh-01.js]
+[test_action-set-display-and-refresh-02.js]
+[test_action-take-census.js]
+skip-if = tsan # Unreasonably slow, bug 1612707
+[test_action-take-snapshot.js]
+[test_action-take-snapshot-and-census.js]
+[test_action-toggle-inverted.js]
+[test_action-toggle-inverted-and-refresh-01.js]
+[test_action-toggle-inverted-and-refresh-02.js]
+[test_action-toggle-recording-allocations.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.js]
+[test_utils-get-snapshot-totals.js]
diff --git a/devtools/client/memory/utils.js b/devtools/client/memory/utils.js
new file mode 100644
index 0000000000..36e6785dc8
--- /dev/null
+++ b/devtools/client/memory/utils.js
@@ -0,0 +1,548 @@
+/* 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/. */
+
+"use strict";
+
+const { Cc, Ci } = require("chrome");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/memory.properties";
+const L10N = (exports.L10N = new LocalizationHelper(STRINGS_URI));
+
+const { OS } = require("resource://gre/modules/osfile.jsm");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+const CUSTOM_CENSUS_DISPLAY_PREF = "devtools.memory.custom-census-displays";
+const CUSTOM_LABEL_DISPLAY_PREF = "devtools.memory.custom-label-displays";
+const CUSTOM_TREE_MAP_DISPLAY_PREF = "devtools.memory.custom-tree-map-displays";
+const BYTES = 1024;
+const KILOBYTES = Math.pow(BYTES, 2);
+const MEGABYTES = Math.pow(BYTES, 3);
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const {
+ snapshotState: states,
+ diffingState,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ individualsState,
+} = require("devtools/client/memory/constants");
+
+/**
+ * Takes a snapshot object and returns the localized form of its timestamp to be
+ * used as a title.
+ *
+ * @param {Snapshot} snapshot
+ * @return {String}
+ */
+exports.getSnapshotTitle = function(snapshot) {
+ if (!snapshot.creationTime) {
+ return L10N.getStr("snapshot-title.loading");
+ }
+
+ if (snapshot.imported) {
+ // Strip out the extension if it's the expected ".fxsnapshot"
+ return OS.Path.basename(snapshot.path.replace(/\.fxsnapshot$/, ""));
+ }
+
+ const date = new Date(snapshot.creationTime / 1000);
+ return date.toLocaleTimeString(void 0, {
+ year: "2-digit",
+ month: "2-digit",
+ day: "2-digit",
+ hour12: false,
+ });
+};
+
+function getCustomDisplaysHelper(pref) {
+ let customDisplays = Object.create(null);
+ try {
+ customDisplays = JSON.parse(Preferences.get(pref)) || Object.create(null);
+ } catch (e) {
+ DevToolsUtils.reportException(
+ `String stored in "${pref}" pref cannot be parsed by \`JSON.parse()\`.`
+ );
+ }
+ return Object.freeze(customDisplays);
+}
+
+/**
+ * Returns custom displays defined in `devtools.memory.custom-census-displays`
+ * pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomCensusDisplays = function() {
+ return getCustomDisplaysHelper(CUSTOM_CENSUS_DISPLAY_PREF);
+};
+
+/**
+ * Returns custom displays defined in
+ * `devtools.memory.custom-label-displays` pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomLabelDisplays = function() {
+ return getCustomDisplaysHelper(CUSTOM_LABEL_DISPLAY_PREF);
+};
+
+/**
+ * Returns custom displays defined in
+ * `devtools.memory.custom-tree-map-displays` pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomTreeMapDisplays = function() {
+ return getCustomDisplaysHelper(CUSTOM_TREE_MAP_DISPLAY_PREF);
+};
+
+/**
+ * Returns a string representing a readable form of the snapshot's state. More
+ * concise than `getStatusTextFull`.
+ *
+ * @param {snapshotState | diffingState} state
+ * @return {String}
+ */
+// eslint-disable-next-line complexity
+exports.getStatusText = function(state) {
+ assert(state, "Must have a state");
+
+ switch (state) {
+ case diffingState.ERROR:
+ return L10N.getStr("diffing.state.error");
+
+ case states.ERROR:
+ return L10N.getStr("snapshot.state.error");
+
+ case states.SAVING:
+ return L10N.getStr("snapshot.state.saving");
+
+ case states.IMPORTING:
+ return L10N.getStr("snapshot.state.importing");
+
+ case states.SAVED:
+ case states.READING:
+ return L10N.getStr("snapshot.state.reading");
+
+ case censusState.SAVING:
+ return L10N.getStr("snapshot.state.saving-census");
+
+ case treeMapState.SAVING:
+ return L10N.getStr("snapshot.state.saving-tree-map");
+
+ case diffingState.TAKING_DIFF:
+ return L10N.getStr("diffing.state.taking-diff");
+
+ case diffingState.SELECTING:
+ return L10N.getStr("diffing.state.selecting");
+
+ case dominatorTreeState.COMPUTING:
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ return L10N.getStr("dominatorTree.state.computing");
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ return L10N.getStr("dominatorTree.state.fetching");
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ return L10N.getStr("dominatorTree.state.incrementalFetching");
+
+ case dominatorTreeState.ERROR:
+ return L10N.getStr("dominatorTree.state.error");
+
+ case individualsState.ERROR:
+ return L10N.getStr("individuals.state.error");
+
+ case individualsState.FETCHING:
+ return L10N.getStr("individuals.state.fetching");
+
+ // These states do not have any message to show as other content will be
+ // displayed.
+ case dominatorTreeState.LOADED:
+ case diffingState.TOOK_DIFF:
+ case states.READ:
+ case censusState.SAVED:
+ case treeMapState.SAVED:
+ case individualsState.FETCHED:
+ return "";
+
+ default:
+ assert(false, `Unexpected state: ${state}`);
+ return "";
+ }
+};
+
+/**
+ * Returns a string representing a readable form of the snapshot's state;
+ * more verbose than `getStatusText`.
+ *
+ * @param {snapshotState | diffingState} state
+ * @return {String}
+ */
+// eslint-disable-next-line complexity
+exports.getStatusTextFull = function(state) {
+ assert(!!state, "Must have a state");
+
+ switch (state) {
+ case diffingState.ERROR:
+ return L10N.getStr("diffing.state.error.full");
+
+ case states.ERROR:
+ return L10N.getStr("snapshot.state.error.full");
+
+ case states.SAVING:
+ return L10N.getStr("snapshot.state.saving.full");
+
+ case states.IMPORTING:
+ return L10N.getStr("snapshot.state.importing");
+
+ case states.SAVED:
+ case states.READING:
+ return L10N.getStr("snapshot.state.reading.full");
+
+ case censusState.SAVING:
+ return L10N.getStr("snapshot.state.saving-census.full");
+
+ case treeMapState.SAVING:
+ return L10N.getStr("snapshot.state.saving-tree-map.full");
+
+ case diffingState.TAKING_DIFF:
+ return L10N.getStr("diffing.state.taking-diff.full");
+
+ case diffingState.SELECTING:
+ return L10N.getStr("diffing.state.selecting.full");
+
+ case dominatorTreeState.COMPUTING:
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ return L10N.getStr("dominatorTree.state.computing.full");
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ return L10N.getStr("dominatorTree.state.fetching.full");
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ return L10N.getStr("dominatorTree.state.incrementalFetching.full");
+
+ case dominatorTreeState.ERROR:
+ return L10N.getStr("dominatorTree.state.error.full");
+
+ case individualsState.ERROR:
+ return L10N.getStr("individuals.state.error.full");
+
+ case individualsState.FETCHING:
+ return L10N.getStr("individuals.state.fetching.full");
+
+ // These states do not have any full message to show as other content will
+ // be displayed.
+ case dominatorTreeState.LOADED:
+ case diffingState.TOOK_DIFF:
+ case states.READ:
+ case censusState.SAVED:
+ case treeMapState.SAVED:
+ case individualsState.FETCHED:
+ return "";
+
+ default:
+ assert(false, `Unexpected state: ${state}`);
+ return "";
+ }
+};
+
+/**
+ * Return true if the snapshot is in a diffable state, false otherwise.
+ *
+ * @param {snapshotModel} snapshot
+ * @returns {Boolean}
+ */
+exports.snapshotIsDiffable = function snapshotIsDiffable(snapshot) {
+ return (
+ (snapshot.census && snapshot.census.state === censusState.SAVED) ||
+ (snapshot.census && snapshot.census.state === censusState.SAVING) ||
+ snapshot.state === states.SAVED ||
+ snapshot.state === states.READ
+ );
+};
+
+/**
+ * Takes an array of snapshots and a snapshot and returns
+ * the snapshot instance in `snapshots` that matches
+ * the snapshot passed in.
+ *
+ * @param {appModel} state
+ * @param {snapshotId} id
+ * @return {snapshotModel|null}
+ */
+exports.getSnapshot = function getSnapshot(state, id) {
+ const found = state.snapshots.find(s => s.id === id);
+ assert(found, `No matching snapshot found with id = ${id}`);
+ return found;
+};
+
+/**
+ * Get the ID of the selected snapshot, if one is selected, null otherwise.
+ *
+ * @returns {SnapshotId|null}
+ */
+exports.findSelectedSnapshot = function(state) {
+ const found = state.snapshots.find(s => s.selected);
+ return found ? found.id : null;
+};
+
+/**
+ * Creates a new snapshot object.
+ *
+ * @param {appModel} state
+ * @return {Snapshot}
+ */
+let ID_COUNTER = 0;
+exports.createSnapshot = function createSnapshot(state) {
+ let dominatorTree = null;
+ if (state.view.state === dominatorTreeState.DOMINATOR_TREE) {
+ dominatorTree = Object.freeze({
+ dominatorTreeId: null,
+ root: null,
+ error: null,
+ state: dominatorTreeState.COMPUTING,
+ });
+ }
+
+ return Object.freeze({
+ id: ++ID_COUNTER,
+ state: states.SAVING,
+ dominatorTree,
+ census: null,
+ treeMap: null,
+ path: null,
+ imported: false,
+ selected: false,
+ error: null,
+ });
+};
+
+/**
+ * Return true if the census is up to date with regards to the current filtering
+ * and requested display, false otherwise.
+ *
+ * @param {String} filter
+ * @param {censusDisplayModel} display
+ * @param {censusModel} census
+ *
+ * @returns {Boolean}
+ */
+exports.censusIsUpToDate = function(filter, display, census) {
+ return (
+ census &&
+ // Filter could be null == undefined so use loose equality.
+ filter == census.filter &&
+ display === census.display
+ );
+};
+
+/**
+ * Check to see if the snapshot is in a state that it can take a census.
+ *
+ * @param {SnapshotModel} A snapshot to check.
+ * @param {Boolean} Assert that the snapshot must be in a ready state.
+ * @returns {Boolean}
+ */
+exports.canTakeCensus = function(snapshot) {
+ return (
+ snapshot.state === states.READ &&
+ (!snapshot.census ||
+ snapshot.census.state === censusState.SAVED ||
+ !snapshot.treeMap ||
+ snapshot.treeMap.state === treeMapState.SAVED)
+ );
+};
+
+/**
+ * Returns true if the given snapshot's dominator tree has been computed, false
+ * otherwise.
+ *
+ * @param {SnapshotModel} snapshot
+ * @returns {Boolean}
+ */
+exports.dominatorTreeIsComputed = function(snapshot) {
+ return (
+ snapshot.dominatorTree &&
+ (snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
+ snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
+ snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING)
+ );
+};
+
+/**
+ * Find the first SAVED census, either from the tree map or the normal
+ * census.
+ *
+ * @param {SnapshotModel} snapshot
+ * @returns {Object|null} Either the census, or null if one hasn't completed
+ */
+exports.getSavedCensus = function(snapshot) {
+ if (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) {
+ return snapshot.treeMap;
+ }
+ if (snapshot.census && snapshot.census.state === censusState.SAVED) {
+ return snapshot.census;
+ }
+ return null;
+};
+
+/**
+ * Takes a snapshot and returns the total bytes and total count that this
+ * snapshot represents.
+ *
+ * @param {CensusModel} census
+ * @return {Object}
+ */
+exports.getSnapshotTotals = function(census) {
+ let bytes = 0;
+ let count = 0;
+
+ const report = census.report;
+ if (report) {
+ bytes = report.totalBytes;
+ count = report.totalCount;
+ }
+
+ return { bytes, count };
+};
+
+/**
+ * Takes some configurations and opens up a file picker and returns
+ * a promise to the chosen file if successful.
+ *
+ * @param {String} .title
+ * The title displayed in the file picker window.
+ * @param {Array<Array<String>>} .filters
+ * An array of filters to display in the file picker. Each filter in the array
+ * is a duple of two strings, one a name for the filter, and one the filter itself
+ * (like "*.json").
+ * @param {String} .defaultName
+ * The default name chosen by the file picker window.
+ * @param {String} .mode
+ * The mode that this filepicker should open in. Can be "open" or "save".
+ * @return {Promise<?nsIFile>}
+ * The file selected by the user, or null, if cancelled.
+ */
+exports.openFilePicker = function({ title, filters, defaultName, mode }) {
+ let fpMode;
+ if (mode === "save") {
+ fpMode = Ci.nsIFilePicker.modeSave;
+ } else if (mode === "open") {
+ fpMode = Ci.nsIFilePicker.modeOpen;
+ } else {
+ throw new Error("No valid mode specified for nsIFilePicker.");
+ }
+
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, title, fpMode);
+
+ for (const filter of filters || []) {
+ fp.appendFilter(filter[0], filter[1]);
+ }
+ fp.defaultString = defaultName;
+
+ return new Promise(resolve => {
+ fp.open({
+ done: result => {
+ if (result === Ci.nsIFilePicker.returnCancel) {
+ resolve(null);
+ return;
+ }
+ resolve(fp.file);
+ },
+ });
+ });
+};
+
+/**
+ * Format the provided number with a space every 3 digits, and optionally
+ * prefixed by its sign.
+ *
+ * @param {Number} number
+ * @param {Boolean} showSign (defaults to false)
+ */
+exports.formatNumber = function(number, showSign = false) {
+ const rounded = Math.round(number);
+ // eslint-disable-next-line no-compare-neg-zero
+ if (rounded === 0 || rounded === -0) {
+ return "0";
+ }
+
+ const abs = String(Math.abs(rounded));
+ // replace every digit followed by (sets of 3 digits) by (itself and a space)
+ const formatted = abs.replace(/(\d)(?=(\d{3})+$)/g, "$1 ");
+
+ if (showSign) {
+ const sign = rounded < 0 ? "-" : "+";
+ return sign + formatted;
+ }
+ return formatted;
+};
+
+/**
+ * Format the provided percentage following the same logic as formatNumber and
+ * an additional % suffix.
+ *
+ * @param {Number} percent
+ * @param {Boolean} showSign (defaults to false)
+ */
+exports.formatPercent = function(percent, showSign = false) {
+ return exports.L10N.getFormatStr(
+ "tree-item.percent2",
+ exports.formatNumber(percent, showSign)
+ );
+};
+
+/**
+ * Change an HSL color array with values ranged 0-1 to a properly formatted
+ * ctx.fillStyle string.
+ *
+ * @param {Number} h
+ * hue values ranged between [0 - 1]
+ * @param {Number} s
+ * hue values ranged between [0 - 1]
+ * @param {Number} l
+ * hue values ranged between [0 - 1]
+ * @return {type}
+ */
+exports.hslToStyle = function(h, s, l) {
+ h = parseInt(h * 360, 10);
+ s = parseInt(s * 100, 10);
+ l = parseInt(l * 100, 10);
+
+ return `hsl(${h},${s}%,${l}%)`;
+};
+
+/**
+ * Linearly interpolate between 2 numbers.
+ *
+ * @param {Number} a
+ * @param {Number} b
+ * @param {Number} t
+ * A value of 0 returns a, and 1 returns b
+ * @return {Number}
+ */
+exports.lerp = function(a, b, t) {
+ return a * (1 - t) + b * t;
+};
+
+/**
+ * Format a number of bytes as human readable, e.g. 13434 => '13KiB'.
+ *
+ * @param {Number} n
+ * Number of bytes
+ * @return {String}
+ */
+exports.formatAbbreviatedBytes = function(n) {
+ if (n < BYTES) {
+ return n + "B";
+ } else if (n < KILOBYTES) {
+ return Math.floor(n / BYTES) + "KiB";
+ } else if (n < MEGABYTES) {
+ return Math.floor(n / KILOBYTES) + "MiB";
+ }
+ return Math.floor(n / MEGABYTES) + "GiB";
+};