diff options
Diffstat (limited to 'devtools/client/memory/actions')
-rw-r--r-- | devtools/client/memory/actions/allocations.js | 20 | ||||
-rw-r--r-- | devtools/client/memory/actions/census-display.js | 34 | ||||
-rw-r--r-- | devtools/client/memory/actions/diffing.js | 201 | ||||
-rw-r--r-- | devtools/client/memory/actions/filter.js | 35 | ||||
-rw-r--r-- | devtools/client/memory/actions/io.js | 97 | ||||
-rw-r--r-- | devtools/client/memory/actions/label-display.js | 38 | ||||
-rw-r--r-- | devtools/client/memory/actions/moz.build | 19 | ||||
-rw-r--r-- | devtools/client/memory/actions/refresh.js | 44 | ||||
-rw-r--r-- | devtools/client/memory/actions/sizes.js | 13 | ||||
-rw-r--r-- | devtools/client/memory/actions/snapshot.js | 865 | ||||
-rw-r--r-- | devtools/client/memory/actions/task-cache.js | 99 | ||||
-rw-r--r-- | devtools/client/memory/actions/tree-map-display.js | 37 | ||||
-rw-r--r-- | devtools/client/memory/actions/view.js | 67 |
13 files changed, 1569 insertions, 0 deletions
diff --git a/devtools/client/memory/actions/allocations.js b/devtools/client/memory/actions/allocations.js new file mode 100644 index 000000000..521319925 --- /dev/null +++ b/devtools/client/memory/actions/allocations.js @@ -0,0 +1,20 @@ +/* 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("../constants"); + +exports.toggleRecordingAllocationStacks = function (front) { + return function* (dispatch, getState) { + dispatch({ type: actions.TOGGLE_RECORD_ALLOCATION_STACKS_START }); + + if (getState().recordingAllocationStacks) { + yield front.stopRecordingAllocations(); + } else { + yield 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 000000000..348a8951a --- /dev/null +++ b/devtools/client/memory/actions/census-display.js @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { assert } = require("devtools/shared/DevToolsUtils"); +const { actions } = require("../constants"); +const { refresh } = require("./refresh"); + +exports.setCensusDisplayAndRefresh = function (heapWorker, display) { + return function* (dispatch, getState) { + dispatch(setCensusDisplay(display)); + yield 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: ${uneval(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 000000000..70af307bb --- /dev/null +++ b/devtools/client/memory/actions/diffing.js @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { assert, reportException } = require("devtools/shared/DevToolsUtils"); +const { actions, diffingState, viewState } = require("../constants"); +const telemetry = require("../telemetry"); +const { + getSnapshot, + censusIsUpToDate, + snapshotIsDiffable, + findSelectedSnapshot, +} = require("../utils"); +// This is a circular dependency, so do not destructure the needed properties. +const snapshotActions = require("./snapshot"); + +/** + * Toggle diffing mode on or off. + */ +const toggleDiffing = 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 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, + }); + + let opts = display.inverted + ? { asInvertedTreeNode: true } + : { asTreeNode: true }; + opts.filter = filter || null; + + try { + ({ delta: report, parentMap } = yield 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, + }); + + telemetry.countDiff({ 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 + */ +const selectSnapshotForDiffingAndRefresh = exports.selectSnapshotForDiffingAndRefresh = function (heapWorker, snapshot) { + return function* (dispatch, getState) { + assert(getState().diffing, + "If we are selecting for diffing, we must be in diffing mode"); + dispatch(selectSnapshotForDiffing(snapshot)); + yield dispatch(refreshDiffing(heapWorker)); + }; +}; + +/** + * Expand the given node in the diffing's census's delta-report. + * + * @param {CensusTreeNode} node + */ +const expandDiffingCensusNode = 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 + */ +const collapseDiffingCensusNode = 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 + */ +const focusDiffingCensusNode = 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 000000000..c578b037a --- /dev/null +++ b/devtools/client/memory/actions/filter.js @@ -0,0 +1,35 @@ +/* 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("../constants"); +const { refresh } = require("./refresh"); + +const setFilterString = exports.setFilterString = function (filterString) { + return { + type: actions.SET_FILTER_STRING, + filter: filterString + }; +}; + +// The number of milliseconds we should wait before kicking off a new census +// when the filter string is updated. This helps us avoid doing any work while +// the user is still typing. +const FILTER_INPUT_DEBOUNCE_MS = 250; + +// The timer id for the debounced census refresh. +let timerId = null; + +exports.setFilterStringAndRefresh = function (filterString, heapWorker) { + return function* (dispatch, getState) { + dispatch(setFilterString(filterString)); + + if (timerId !== null) { + clearTimeout(timerId); + } + + timerId = setTimeout(() => dispatch(refresh(heapWorker)), + FILTER_INPUT_DEBOUNCE_MS); + }; +}; diff --git a/devtools/client/memory/actions/io.js b/devtools/client/memory/actions/io.js new file mode 100644 index 000000000..10b45aee5 --- /dev/null +++ b/devtools/client/memory/actions/io.js @@ -0,0 +1,97 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { immutableUpdate, reportException, assert } = require("devtools/shared/DevToolsUtils"); +const { snapshotState: states, actions, viewState } = require("../constants"); +const { L10N, openFilePicker, createSnapshot } = require("../utils"); +const telemetry = require("../telemetry"); +const { OS } = require("resource://gre/modules/osfile.jsm"); +const { + selectSnapshot, + computeSnapshotData, + readSnapshot, + takeCensus, + takeTreeMap +} = require("./snapshot"); +const VALID_EXPORT_STATES = [states.SAVED, states.READ]; + +exports.pickFileAndExportSnapshot = function (snapshot) { + return function* (dispatch, getState) { + let outputFile = yield openFilePicker({ + title: L10N.getFormatStr("snapshot.io.save.window"), + defaultName: OS.Path.basename(snapshot.path), + filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]], + mode: "save", + }); + + if (!outputFile) { + return; + } + + yield dispatch(exportSnapshot(snapshot, outputFile.path)); + }; +}; + +const exportSnapshot = exports.exportSnapshot = function (snapshot, dest) { + return function* (dispatch, getState) { + telemetry.countExportSnapshot(); + + dispatch({ type: actions.EXPORT_SNAPSHOT_START, snapshot }); + + assert(VALID_EXPORT_STATES.includes(snapshot.state), + `Snapshot is in invalid state for exporting: ${snapshot.state}`); + + try { + yield OS.File.copy(snapshot.path, dest); + } catch (error) { + reportException("exportSnapshot", error); + dispatch({ type: actions.EXPORT_SNAPSHOT_ERROR, snapshot, error }); + } + + dispatch({ type: actions.EXPORT_SNAPSHOT_END, snapshot }); + }; +}; + +const pickFileAndImportSnapshotAndCensus = exports.pickFileAndImportSnapshotAndCensus = function (heapWorker) { + return function* (dispatch, getState) { + let input = yield openFilePicker({ + title: L10N.getFormatStr("snapshot.io.import.window"), + filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]], + mode: "open", + }); + + if (!input) { + return; + } + + yield dispatch(importSnapshotAndCensus(heapWorker, input.path)); + }; +}; + +const importSnapshotAndCensus = exports.importSnapshotAndCensus = function (heapWorker, path) { + return function* (dispatch, getState) { + telemetry.countImportSnapshot(); + + 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 { + yield dispatch(readSnapshot(heapWorker, id)); + yield dispatch(computeSnapshotData(heapWorker, id)); + } catch (error) { + reportException("importSnapshot", error); + dispatch({ type: actions.IMPORT_SNAPSHOT_ERROR, error, id }); + } + + dispatch({ type: actions.IMPORT_SNAPSHOT_END, id }); + }; +}; diff --git a/devtools/client/memory/actions/label-display.js b/devtools/client/memory/actions/label-display.js new file mode 100644 index 000000000..6e68293ab --- /dev/null +++ b/devtools/client/memory/actions/label-display.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { assert } = require("devtools/shared/DevToolsUtils"); +const { actions } = require("../constants"); +const { refresh } = require("./refresh"); + +/** + * Change the display we use for labeling individual nodes and refresh the + * current data. + */ +exports.setLabelDisplayAndRefresh = function (heapWorker, display) { + return function* (dispatch, getState) { + // Clears out all stored census data and sets the display. + dispatch(setLabelDisplay(display)); + yield 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: ${uneval(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 000000000..8b26aff9f --- /dev/null +++ b/devtools/client/memory/actions/moz.build @@ -0,0 +1,19 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'allocations.js', + 'census-display.js', + 'diffing.js', + 'filter.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 000000000..801a4f867 --- /dev/null +++ b/devtools/client/memory/actions/refresh.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { assert } = require("devtools/shared/DevToolsUtils"); +const { viewState } = require("../constants"); +const { refreshDiffing } = require("./diffing"); +const snapshot = require("./snapshot"); + +/** + * Refresh the main thread's data from the heap analyses worker, if needed. + * + * @param {HeapAnalysesWorker} heapWorker + */ +exports.refresh = function (heapWorker) { + return function* (dispatch, getState) { + switch (getState().view.state) { + case viewState.DIFFING: + assert(getState().diffing, "Should have diffing state if in diffing view"); + yield dispatch(refreshDiffing(heapWorker)); + return; + + case viewState.CENSUS: + yield dispatch(snapshot.refreshSelectedCensus(heapWorker)); + return; + + case viewState.DOMINATOR_TREE: + yield dispatch(snapshot.refreshSelectedDominatorTree(heapWorker)); + return; + + case viewState.TREE_MAP: + yield dispatch(snapshot.refreshSelectedTreeMap(heapWorker)); + return; + + case viewState.INDIVIDUALS: + yield 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 000000000..7239803e7 --- /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("../constants"); + +exports.resizeShortestPaths = function (newSize) { + return { + type: actions.RESIZE_SHORTEST_PATHS, + size: newSize, + }; +}; diff --git a/devtools/client/memory/actions/snapshot.js b/devtools/client/memory/actions/snapshot.js new file mode 100644 index 000000000..adb24977e --- /dev/null +++ b/devtools/client/memory/actions/snapshot.js @@ -0,0 +1,865 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Preferences } = require("resource://gre/modules/Preferences.jsm"); +const { assert, reportException, isSet } = require("devtools/shared/DevToolsUtils"); +const { + censusIsUpToDate, + getSnapshot, + createSnapshot, + dominatorTreeIsComputed, +} = require("../utils"); +const { + actions, + snapshotState: states, + viewState, + censusState, + treeMapState, + dominatorTreeState, + individualsState, +} = require("../constants"); +const telemetry = require("../telemetry"); +const view = require("./view"); +const refresh = require("./refresh"); +const diffing = require("./diffing"); +const TaskCache = require("./task-cache"); + +/** + * A series of actions are fired from this task to save, read and generate the + * initial census from a snapshot. + * + * @param {MemoryFront} + * @param {HeapAnalysesClient} + * @param {Object} + */ +const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function (front, heapWorker) { + return function* (dispatch, getState) { + const id = yield dispatch(takeSnapshot(front)); + if (id === null) { + return; + } + + yield dispatch(readSnapshot(heapWorker, id)); + if (getSnapshot(getState(), id).state !== states.READ) { + return; + } + + yield 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 function* (dispatch, getState) { + if (getSnapshot(getState(), id).state !== states.READ) { + return; + } + + // Decide which type of census to take. + const censusTaker = getCurrentCensusTaker(getState().view.state); + yield dispatch(censusTaker(heapWorker, id)); + + if (getState().view.state === viewState.DOMINATOR_TREE && + !getSnapshot(getState(), id).dominatorTree) { + yield 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 + */ +const selectSnapshotAndRefresh = exports.selectSnapshotAndRefresh = function (heapWorker, id) { + return function* (dispatch, getState) { + if (getState().diffing || getState().individuals) { + dispatch(view.changeView(viewState.CENSUS)); + } + + dispatch(selectSnapshot(id)); + yield 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 function* (dispatch, getState) { + telemetry.countTakeSnapshot(); + + 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 = yield front.saveHeapSnapshot(); + } catch (error) { + reportException("takeSnapshot", error); + dispatch({ type: actions.SNAPSHOT_ERROR, id, error }); + return null; + } + + dispatch({ type: actions.TAKE_SNAPSHOT_END, id, path }); + return snapshot.id; + }; +}; + +/** + * Reads a snapshot into memory; necessary to do before taking + * a census on the snapshot. May only be called once per snapshot. + * + * @param {HeapAnalysesClient} heapWorker + * @param {snapshotId} id + */ +const readSnapshot = exports.readSnapshot = +TaskCache.declareCacheableTask({ + getCacheKey(_, id) { + return id; + }, + + task: function* (heapWorker, id, removeFromCache, dispatch, getState) { + const snapshot = getSnapshot(getState(), id); + assert([states.SAVED, states.IMPORTING].includes(snapshot.state), + `Should only read a snapshot once. Found snapshot in state ${snapshot.state}`); + + let creationTime; + + dispatch({ type: actions.READ_SNAPSHOT_START, id }); + try { + yield heapWorker.readHeapSnapshot(snapshot.path); + creationTime = yield 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 + */ + let thisTakeCensusTaskId = ++takeCensusTaskCounter; + return TaskCache.declareCacheableTask({ + getCacheKey(_, id) { + return `take-census-task-${thisTakeCensusTaskId}-${id}`; + }, + + task: function* (heapWorker, id, removeFromCache, dispatch, getState) { + const snapshot = getSnapshot(getState(), id); + if (!snapshot) { + removeFromCache(); + return; + } + + // Assert that snapshot is in a valid state + assert(canTakeCensus(snapshot), + `Attempting to take a census when the snapshot is not in a ready state. snapshot.state = ${snapshot.state}, census.state = ${(getCensus(snapshot) || { state: null }).state}`); + + let report, parentMap; + let display = getDisplay(getState()); + let filter = getFilter(getState()); + + // If display, filter and inversion haven't changed, don't do anything. + if (censusIsUpToDate(filter, display, getCensus(snapshot))) { + removeFromCache(); + return; + } + + // Keep taking a census if the display changes while our request is in + // flight. Recheck that the display used for the census is the same as the + // state's display. + do { + display = getDisplay(getState()); + filter = getState().filter; + + dispatch({ + type: beginAction, + id, + filter, + display + }); + + let opts = display.inverted + ? { asInvertedTreeNode: true } + : { asTreeNode: true }; + + opts.filter = filter || null; + + try { + ({ report, parentMap } = yield 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 + }); + + telemetry.countCensus({ filter, display }); + } + }); +} + +/** + * 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. + */ +const focusIndividual = 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 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)) { + yield dispatch(computeAndFetchDominatorTree(heapWorker, id)); + } + + const snapshot_ = getSnapshot(getState(), id); + assert(snapshot_.dominatorTree && 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 && labelDisplay.breakdown && labelDisplay.breakdown.by, + `Should have a breakdown to label nodes with, got: ${uneval(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 } = yield 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 + */ +const refreshIndividuals = exports.refreshIndividuals = function (heapWorker) { + return 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; + } + + yield 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 + */ +const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) { + return function* (dispatch, getState) { + let 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) { + yield dispatch(takeCensus(heapWorker, snapshot.id)); + } + }; +}; + +/** + * Refresh the selected snapshot's tree map data, if need be (for example, + * display configuration changed). + * + * @param {HeapAnalysesClient} heapWorker + */ +const refreshSelectedTreeMap = exports.refreshSelectedTreeMap = function (heapWorker) { + return function* (dispatch, getState) { + let 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) { + yield dispatch(takeTreeMap(heapWorker, snapshot.id)); + } + }; +}; + +/** + * Request that the `HeapAnalysesWorker` compute the dominator tree for the + * snapshot with the given `id`. + * + * @param {HeapAnalysesClient} heapWorker + * @param {SnapshotId} id + * + * @returns {Promise<DominatorTreeId>} + */ +const computeDominatorTree = exports.computeDominatorTree = +TaskCache.declareCacheableTask({ + getCacheKey(_, id) { + return id; + }, + + task: function* (heapWorker, id, removeFromCache, dispatch, getState) { + const snapshot = getSnapshot(getState(), id); + assert(!(snapshot.dominatorTree && snapshot.dominatorTree.dominatorTreeId), + "Should not re-compute dominator trees"); + + dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_START, id }); + + let dominatorTreeId; + try { + dominatorTreeId = yield heapWorker.computeDominatorTree(snapshot.path); + } catch (error) { + removeFromCache(); + reportException("actions/snapshot/computeDominatorTree", error); + dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error }); + return null; + } + + removeFromCache(); + dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_END, id, dominatorTreeId }); + return dominatorTreeId; + } +}); + +/** + * Get the partial subtree, starting from the root, of the + * snapshot-with-the-given-id's dominator tree. + * + * @param {HeapAnalysesClient} heapWorker + * @param {SnapshotId} id + * + * @returns {Promise<DominatorTreeNode>} + */ +const fetchDominatorTree = exports.fetchDominatorTree = +TaskCache.declareCacheableTask({ + getCacheKey(_, id) { + return id; + }, + + task: function* (heapWorker, id, removeFromCache, dispatch, getState) { + const snapshot = getSnapshot(getState(), id); + assert(dominatorTreeIsComputed(snapshot), + "Should have dominator tree model and it should be computed"); + + let display; + let root; + do { + display = getState().labelDisplay; + assert(display && display.breakdown, + `Should have a breakdown to describe nodes with, got: ${uneval(display)}`); + + dispatch({ type: actions.FETCH_DOMINATOR_TREE_START, id, display }); + + try { + root = yield 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 }); + telemetry.countDominatorTree({ display }); + 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 + */ +const fetchImmediatelyDominated = exports.fetchImmediatelyDominated = +TaskCache.declareCacheableTask({ + getCacheKey(_, id, lazyChildren) { + return `${id}-${lazyChildren.key()}`; + }, + + task: function* (heapWorker, id, lazyChildren, removeFromCache, dispatch, getState) { + const snapshot = getSnapshot(getState(), id); + assert(snapshot.dominatorTree, "Should have dominator tree model"); + assert(snapshot.dominatorTree.state === dominatorTreeState.LOADED || + snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING, + "Cannot fetch immediately dominated nodes in a dominator tree unless " + + " the dominator tree has already been computed"); + + let display; + let response; + do { + display = getState().labelDisplay; + assert(display, "Should have a display to describe nodes with."); + + dispatch({ type: actions.FETCH_IMMEDIATELY_DOMINATED_START, id }); + + try { + response = yield 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 null; + } + } + while (display !== getState().labelDisplay); + + removeFromCache(); + dispatch({ + type: actions.FETCH_IMMEDIATELY_DOMINATED_END, + id, + path: response.path, + nodes: response.nodes, + moreChildrenAvailable: response.moreChildrenAvailable, + }); + } +}); + +/** + * Compute and then fetch the dominator tree of the snapshot with the given + * `id`. + * + * @param {HeapAnalysesClient} heapWorker + * @param {SnapshotId} id + * + * @returns {Promise<DominatorTreeNode>} + */ +const computeAndFetchDominatorTree = exports.computeAndFetchDominatorTree = +TaskCache.declareCacheableTask({ + getCacheKey(_, id) { + return id; + }, + + task: function* (heapWorker, id, removeFromCache, dispatch, getState) { + const dominatorTreeId = yield dispatch(computeDominatorTree(heapWorker, id)); + if (dominatorTreeId === null) { + removeFromCache(); + return null; + } + + const root = yield dispatch(fetchDominatorTree(heapWorker, id)); + removeFromCache(); + + if (!root) { + return null; + } + + return root; + } +}); + +/** + * Update the currently selected snapshot's dominator tree. + * + * @param {HeapAnalysesClient} heapWorker + */ +const refreshSelectedDominatorTree = exports.refreshSelectedDominatorTree = function (heapWorker) { + return function* (dispatch, getState) { + let 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; + } + + if (snapshot.state === states.READ) { + if (snapshot.dominatorTree) { + yield dispatch(fetchDominatorTree(heapWorker, snapshot.id)); + } else { + yield dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id)); + } + } else { + // If there was an error, we can't continue. If we are still saving or + // reading the snapshot, then takeSnapshotAndCensus will finish the job + // for us. + return; + } + }; +}; + +/** + * 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 + */ +const clearSnapshots = exports.clearSnapshots = function (heapWorker) { + return function* (dispatch, getState) { + let snapshots = getState().snapshots.filter(s => { + let snapshotReady = s.state === states.READ || s.state === states.ERROR; + let censusReady = (s.treeMap && s.treeMap.state === treeMapState.SAVED) || + (s.census && s.census.state === censusState.SAVED); + + return snapshotReady && censusReady; + }); + + let 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()); + } + + yield 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 + */ +const deleteSnapshot = exports.deleteSnapshot = function (heapWorker, snapshot) { + return function* (dispatch, getState) { + dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids: [snapshot.id] }); + + try { + yield 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 + */ +const expandCensusNode = 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 + */ +const collapseCensusNode = 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 + */ +const focusCensusNode = 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 + */ +const expandDominatorTreeNode = 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 + */ +const collapseDominatorTreeNode = 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 + */ +const focusDominatorTreeNode = 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 000000000..14e44d0c3 --- /dev/null +++ b/devtools/client/memory/actions/task-cache.js @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { assert } = require("devtools/shared/DevToolsUtils"); + +/** + * The `TaskCache` allows for re-using active tasks when spawning a second task + * would simply duplicate work and is unnecessary. It maps from a task's unique + * key to the promise of its result. + */ +const TaskCache = module.exports = class TaskCache { + constructor() { + this._cache = new Map(); + } + + /** + * Get the promise keyed by the given unique `key`, if one exists. + * + * @param {Any} key + * @returns {Promise<Any> | undefined} + */ + get(key) { + return this._cache.get(key); + } + + /** + * Put the task result promise in the cache and associate it with the given + * `key` which must not already have an entry in the cache. + * + * @param {Any} key + * @param {Promise<Any>} promise + */ + put(key, promise) { + assert(!this._cache.has(key), + "We should not override extant entries"); + + this._cache.set(key, promise); + } + + /** + * Remove the cache entry with the given key. + * + * @param {Any} key + */ + remove(key) { + assert(this._cache.has(key), + `Should have an extant entry for key = ${key}`); + + this._cache.delete(key); + } +}; + +/** + * Create a new action-orchestrating task that is automatically cached. The + * tasks themselves are responsible from removing themselves from the cache. + * + * @param {Function(...args) -> Any} getCacheKey + * @param {Generator(...args) -> Any} task + * + * @returns Cacheable, Action-Creating Task + */ +TaskCache.declareCacheableTask = function ({ getCacheKey, task }) { + const cache = new TaskCache(); + + return function (...args) { + return 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(function* () { + try { + args.push(() => cache.remove(key), dispatch, getState); + return yield* task(...args); + } catch (error) { + // Don't perma-cache errors. + if (cache.get(key)) { + cache.remove(key); + } + throw error; + } + })); + + return yield 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 000000000..e4ab9988c --- /dev/null +++ b/devtools/client/memory/actions/tree-map-display.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { assert } = require("devtools/shared/DevToolsUtils"); +const { actions } = require("../constants"); +const { refresh } = require("./refresh"); +/** + * Sets the tree map display as the current display and refreshes the tree map + * census. + */ +exports.setTreeMapAndRefresh = function (heapWorker, display) { + return function* (dispatch, getState) { + dispatch(setTreeMap(display)); + yield 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: ${uneval(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 000000000..5ff508840 --- /dev/null +++ b/devtools/client/memory/actions/view.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { assert } = require("devtools/shared/DevToolsUtils"); +const { actions } = require("../constants"); +const { findSelectedSnapshot } = require("../utils"); +const refresh = require("./refresh"); + +/** + * Change the currently selected view. + * + * @param {viewState} view + */ +const changeView = exports.changeView = function (view) { + return function (dispatch, getState) { + dispatch({ + type: actions.CHANGE_VIEW, + newViewState: view, + oldDiffing: getState().diffing, + oldSelected: findSelectedSnapshot(getState()), + }); + }; +}; + +/** + * Given that we are in the INDIVIDUALS view state, go back to the state we were + * in before. + */ +const popView = exports.popView = function () { + return function (dispatch, getState) { + const { previous } = getState().view; + assert(previous); + dispatch({ + type: actions.POP_VIEW, + previousView: previous, + }); + }; +}; + +/** + * Change the currently selected view and ensure all our data is up to date from + * the heap worker. + * + * @param {viewState} view + * @param {HeapAnalysesClient} heapWorker + */ +exports.changeViewAndRefresh = function (view, heapWorker) { + return function* (dispatch, getState) { + dispatch(changeView(view)); + yield 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 function* (dispatch, getState) { + dispatch(popView()); + yield dispatch(refresh.refresh(heapWorker)); + }; +}; |