/* 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, 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} 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: Services.prefs.getIntPref( "devtools.memory.max-retaining-paths" ), maxIndividuals: Services.prefs.getIntPref( "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} */ 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} */ 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: Services.prefs.getIntPref( "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: Services.prefs.getIntPref( "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} */ const computeAndFetchDominatorTree = (exports.computeAndFetchDominatorTree = TaskCache.declareCacheableTask({ getCacheKey(_, id) { return id; }, async task(heapWorker, id, removeFromCache, dispatch) { 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 }) { 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, }; };