/* 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} 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} */ 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} */ 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} */ 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, }; };