summaryrefslogtreecommitdiffstats
path: root/devtools/client/memory/actions
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /devtools/client/memory/actions
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/memory/actions')
-rw-r--r--devtools/client/memory/actions/allocations.js32
-rw-r--r--devtools/client/memory/actions/census-display.js39
-rw-r--r--devtools/client/memory/actions/diffing.js221
-rw-r--r--devtools/client/memory/actions/filter.js33
-rw-r--r--devtools/client/memory/actions/front.js17
-rw-r--r--devtools/client/memory/actions/io.js103
-rw-r--r--devtools/client/memory/actions/label-display.js43
-rw-r--r--devtools/client/memory/actions/moz.build20
-rw-r--r--devtools/client/memory/actions/refresh.js49
-rw-r--r--devtools/client/memory/actions/sizes.js13
-rw-r--r--devtools/client/memory/actions/snapshot.js939
-rw-r--r--devtools/client/memory/actions/task-cache.js105
-rw-r--r--devtools/client/memory/actions/tree-map-display.js42
-rw-r--r--devtools/client/memory/actions/view.js69
14 files changed, 1725 insertions, 0 deletions
diff --git a/devtools/client/memory/actions/allocations.js b/devtools/client/memory/actions/allocations.js
new file mode 100644
index 0000000000..47bc68e1c2
--- /dev/null
+++ b/devtools/client/memory/actions/allocations.js
@@ -0,0 +1,32 @@
+/* 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("resource://devtools/client/memory/constants.js");
+
+exports.toggleRecordingAllocationStacks = function (commands) {
+ return async function ({ dispatch, getState }) {
+ dispatch({ type: actions.TOGGLE_RECORD_ALLOCATION_STACKS_START });
+
+ if (commands.targetCommand.hasTargetWatcherSupport()) {
+ await commands.targetConfigurationCommand.updateConfiguration({
+ recordAllocations: getState().recordingAllocationStacks
+ ? null
+ : ALLOCATION_RECORDING_OPTIONS,
+ });
+ } else {
+ const front = await commands.targetCommand.targetFront.getFront("memory");
+ 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..d266fe04a5
--- /dev/null
+++ b/devtools/client/memory/actions/census-display.js
@@ -0,0 +1,39 @@
+/* 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("resource://devtools/shared/DevToolsUtils.js");
+const { actions } = require("resource://devtools/client/memory/constants.js");
+const {
+ refresh,
+} = require("resource://devtools/client/memory/actions/refresh.js");
+
+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..1442cbc0a7
--- /dev/null
+++ b/devtools/client/memory/actions/diffing.js
@@ -0,0 +1,221 @@
+/* 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("resource://devtools/shared/DevToolsUtils.js");
+const {
+ actions,
+ diffingState,
+ viewState,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ getSnapshot,
+ censusIsUpToDate,
+ snapshotIsDiffable,
+ findSelectedSnapshot,
+} = require("resource://devtools/client/memory/utils.js");
+
+/**
+ * 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..1bc9fd35db
--- /dev/null
+++ b/devtools/client/memory/actions/filter.js
@@ -0,0 +1,33 @@
+/* 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("resource://devtools/client/memory/constants.js");
+const {
+ refresh,
+} = require("resource://devtools/client/memory/actions/refresh.js");
+const { debounce } = require("resource://devtools/shared/debounce.js");
+
+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;
+const debouncedRefreshDispatcher = debounce(
+ (dispatch, heapWorker) => dispatch(refresh(heapWorker)),
+ FILTER_INPUT_DEBOUNCE_MS
+);
+
+exports.setFilterStringAndRefresh = function (filterString, heapWorker) {
+ return ({ dispatch, getState }) => {
+ dispatch(setFilterString(filterString));
+ debouncedRefreshDispatcher(dispatch, heapWorker);
+ };
+};
diff --git a/devtools/client/memory/actions/front.js b/devtools/client/memory/actions/front.js
new file mode 100644
index 0000000000..3ae10ea9cc
--- /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("resource://devtools/client/memory/constants.js");
+
+/**
+ * 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..c811478df5
--- /dev/null
+++ b/devtools/client/memory/actions/io.js
@@ -0,0 +1,103 @@
+/* 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("resource://devtools/shared/DevToolsUtils.js");
+const {
+ snapshotState: states,
+ actions,
+} = require("resource://devtools/client/memory/constants.js");
+const {
+ L10N,
+ openFilePicker,
+ createSnapshot,
+} = require("resource://devtools/client/memory/utils.js");
+const {
+ selectSnapshot,
+ computeSnapshotData,
+ readSnapshot,
+} = require("resource://devtools/client/memory/actions/snapshot.js");
+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: PathUtils.filename(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 IOUtils.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..c8a6db35ec
--- /dev/null
+++ b/devtools/client/memory/actions/label-display.js
@@ -0,0 +1,43 @@
+/* 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("resource://devtools/shared/DevToolsUtils.js");
+const { actions } = require("resource://devtools/client/memory/constants.js");
+const {
+ refresh,
+} = require("resource://devtools/client/memory/actions/refresh.js");
+
+/**
+ * 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..33401fa299
--- /dev/null
+++ b/devtools/client/memory/actions/refresh.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("resource://devtools/shared/DevToolsUtils.js");
+const { viewState } = require("resource://devtools/client/memory/constants.js");
+const {
+ refreshDiffing,
+} = require("resource://devtools/client/memory/actions/diffing.js");
+const snapshot = require("resource://devtools/client/memory/actions/snapshot.js");
+
+/**
+ * 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..c5a98e23e9
--- /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("resource://devtools/client/memory/constants.js");
+
+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..6593eec7a3
--- /dev/null
+++ b/devtools/client/memory/actions/snapshot.js
@@ -0,0 +1,939 @@
+/* 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 } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+const {
+ assert,
+ reportException,
+ isSet,
+} = require("resource://devtools/shared/DevToolsUtils.js");
+const {
+ censusIsUpToDate,
+ getSnapshot,
+ createSnapshot,
+ dominatorTreeIsComputed,
+} = require("resource://devtools/client/memory/utils.js");
+const {
+ actions,
+ snapshotState: states,
+ viewState,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ individualsState,
+} = require("resource://devtools/client/memory/constants.js");
+const view = require("resource://devtools/client/memory/actions/view.js");
+const refresh = require("resource://devtools/client/memory/actions/refresh.js");
+const diffing = require("resource://devtools/client/memory/actions/diffing.js");
+const TaskCache = require("resource://devtools/client/memory/actions/task-cache.js");
+
+/**
+ * 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;
+ },
+
+ async task(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}`;
+ },
+
+ async task(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;
+ },
+
+ async task(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;
+ },
+
+ async task(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()}`;
+ },
+
+ async task(
+ 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;
+ },
+
+ async task(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..c472cb69da
--- /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("resource://devtools/shared/DevToolsUtils.js");
+
+/**
+ * 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..e1c4a21132
--- /dev/null
+++ b/devtools/client/memory/actions/tree-map-display.js
@@ -0,0 +1,42 @@
+/* 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("resource://devtools/shared/DevToolsUtils.js");
+const { actions } = require("resource://devtools/client/memory/constants.js");
+const {
+ refresh,
+} = require("resource://devtools/client/memory/actions/refresh.js");
+/**
+ * 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..af1bc7b21a
--- /dev/null
+++ b/devtools/client/memory/actions/view.js
@@ -0,0 +1,69 @@
+/* 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("resource://devtools/shared/DevToolsUtils.js");
+const { actions } = require("resource://devtools/client/memory/constants.js");
+const {
+ findSelectedSnapshot,
+} = require("resource://devtools/client/memory/utils.js");
+const refresh = require("resource://devtools/client/memory/actions/refresh.js");
+
+/**
+ * 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));
+ };
+};