diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /devtools/client/memory/actions | |
parent | Initial commit. (diff) | |
download | firefox-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.js | 32 | ||||
-rw-r--r-- | devtools/client/memory/actions/census-display.js | 39 | ||||
-rw-r--r-- | devtools/client/memory/actions/diffing.js | 221 | ||||
-rw-r--r-- | devtools/client/memory/actions/filter.js | 33 | ||||
-rw-r--r-- | devtools/client/memory/actions/front.js | 17 | ||||
-rw-r--r-- | devtools/client/memory/actions/io.js | 103 | ||||
-rw-r--r-- | devtools/client/memory/actions/label-display.js | 43 | ||||
-rw-r--r-- | devtools/client/memory/actions/moz.build | 20 | ||||
-rw-r--r-- | devtools/client/memory/actions/refresh.js | 49 | ||||
-rw-r--r-- | devtools/client/memory/actions/sizes.js | 13 | ||||
-rw-r--r-- | devtools/client/memory/actions/snapshot.js | 939 | ||||
-rw-r--r-- | devtools/client/memory/actions/task-cache.js | 105 | ||||
-rw-r--r-- | devtools/client/memory/actions/tree-map-display.js | 42 | ||||
-rw-r--r-- | devtools/client/memory/actions/view.js | 69 |
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)); + }; +}; |