tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

snapshot.js (26403B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 const {
      7  assert,
      8  reportException,
      9  isSet,
     10 } = require("resource://devtools/shared/DevToolsUtils.js");
     11 const {
     12  censusIsUpToDate,
     13  getSnapshot,
     14  createSnapshot,
     15  dominatorTreeIsComputed,
     16 } = require("resource://devtools/client/memory/utils.js");
     17 const {
     18  actions,
     19  snapshotState: states,
     20  viewState,
     21  censusState,
     22  treeMapState,
     23  dominatorTreeState,
     24  individualsState,
     25 } = require("resource://devtools/client/memory/constants.js");
     26 const view = require("resource://devtools/client/memory/actions/view.js");
     27 const refresh = require("resource://devtools/client/memory/actions/refresh.js");
     28 const diffing = require("resource://devtools/client/memory/actions/diffing.js");
     29 const TaskCache = require("resource://devtools/client/memory/actions/task-cache.js");
     30 
     31 /**
     32 * A series of actions are fired from this task to save, read and generate the
     33 * initial census from a snapshot.
     34 *
     35 * @param {MemoryFront}
     36 * @param {HeapAnalysesClient}
     37 * @param {object}
     38 */
     39 exports.takeSnapshotAndCensus = function (front, heapWorker) {
     40  return async function ({ dispatch, getState }) {
     41    const id = await dispatch(takeSnapshot(front));
     42    if (id === null) {
     43      return;
     44    }
     45 
     46    await dispatch(readSnapshot(heapWorker, id));
     47    if (getSnapshot(getState(), id).state !== states.READ) {
     48      return;
     49    }
     50 
     51    await dispatch(computeSnapshotData(heapWorker, id));
     52  };
     53 };
     54 
     55 /**
     56 * Create the census for the snapshot with the provided snapshot id. If the
     57 * current view is the DOMINATOR_TREE view, create the dominator tree for this
     58 * snapshot as well.
     59 *
     60 * @param {HeapAnalysesClient} heapWorker
     61 * @param {snapshotId} id
     62 */
     63 const computeSnapshotData = (exports.computeSnapshotData = function (
     64  heapWorker,
     65  id
     66 ) {
     67  return async function ({ dispatch, getState }) {
     68    if (getSnapshot(getState(), id).state !== states.READ) {
     69      return;
     70    }
     71 
     72    // Decide which type of census to take.
     73    const censusTaker = getCurrentCensusTaker(getState().view.state);
     74    await dispatch(censusTaker(heapWorker, id));
     75 
     76    if (
     77      getState().view.state === viewState.DOMINATOR_TREE &&
     78      !getSnapshot(getState(), id).dominatorTree
     79    ) {
     80      await dispatch(computeAndFetchDominatorTree(heapWorker, id));
     81    }
     82  };
     83 });
     84 
     85 /**
     86 * Selects a snapshot and if the snapshot's census is using a different
     87 * display, take a new census.
     88 *
     89 * @param {HeapAnalysesClient} heapWorker
     90 * @param {snapshotId} id
     91 */
     92 exports.selectSnapshotAndRefresh = function (heapWorker, id) {
     93  return async function ({ dispatch, getState }) {
     94    if (getState().diffing || getState().individuals) {
     95      dispatch(view.changeView(viewState.CENSUS));
     96    }
     97 
     98    dispatch(selectSnapshot(id));
     99    await dispatch(refresh.refresh(heapWorker));
    100  };
    101 };
    102 
    103 /**
    104 * Take a snapshot and return its id on success, or null on failure.
    105 *
    106 * @param {MemoryFront} front
    107 * @returns {number | null}
    108 */
    109 const takeSnapshot = (exports.takeSnapshot = function (front) {
    110  return async function ({ dispatch, getState }) {
    111    if (getState().diffing || getState().individuals) {
    112      dispatch(view.changeView(viewState.CENSUS));
    113    }
    114 
    115    const snapshot = createSnapshot(getState());
    116    const id = snapshot.id;
    117    dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot });
    118    dispatch(selectSnapshot(id));
    119 
    120    let path;
    121    try {
    122      path = await front.saveHeapSnapshot();
    123    } catch (error) {
    124      reportException("takeSnapshot", error);
    125      dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
    126      return null;
    127    }
    128 
    129    dispatch({ type: actions.TAKE_SNAPSHOT_END, id, path });
    130    return snapshot.id;
    131  };
    132 });
    133 
    134 /**
    135 * Reads a snapshot into memory; necessary to do before taking
    136 * a census on the snapshot. May only be called once per snapshot.
    137 *
    138 * @param {HeapAnalysesClient} heapWorker
    139 * @param {snapshotId} id
    140 */
    141 const readSnapshot = (exports.readSnapshot = TaskCache.declareCacheableTask({
    142  getCacheKey(_, id) {
    143    return id;
    144  },
    145 
    146  async task(heapWorker, id, removeFromCache, dispatch, getState) {
    147    const snapshot = getSnapshot(getState(), id);
    148    assert(
    149      [states.SAVED, states.IMPORTING].includes(snapshot.state),
    150      `Should only read a snapshot once. Found snapshot in state ${snapshot.state}`
    151    );
    152 
    153    let creationTime;
    154 
    155    dispatch({ type: actions.READ_SNAPSHOT_START, id });
    156    try {
    157      await heapWorker.readHeapSnapshot(snapshot.path);
    158      creationTime = await heapWorker.getCreationTime(snapshot.path);
    159    } catch (error) {
    160      removeFromCache();
    161      reportException("readSnapshot", error);
    162      dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
    163      return;
    164    }
    165 
    166    removeFromCache();
    167    dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime });
    168  },
    169 }));
    170 
    171 let takeCensusTaskCounter = 0;
    172 
    173 /**
    174 * Census and tree maps both require snapshots. This function shares the logic
    175 * of creating snapshots, but is configurable with specific actions for the
    176 * individual census types.
    177 *
    178 * @param {getDisplay} Get the display object from the state.
    179 * @param {getCensus} Get the census from the snapshot.
    180 * @param {beginAction} Action to send at the beginning of a heap snapshot.
    181 * @param {endAction} Action to send at the end of a heap snapshot.
    182 * @param {errorAction} Action to send if a snapshot has an error.
    183 */
    184 function makeTakeCensusTask({
    185  getDisplay,
    186  getFilter,
    187  getCensus,
    188  beginAction,
    189  endAction,
    190  errorAction,
    191  canTakeCensus,
    192 }) {
    193  /**
    194   * @param {HeapAnalysesClient} heapWorker
    195   * @param {snapshotId} id
    196   *
    197   * @see {Snapshot} model defined in devtools/client/memory/models.js
    198   * @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
    199   * @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
    200   */
    201  const thisTakeCensusTaskId = ++takeCensusTaskCounter;
    202  return TaskCache.declareCacheableTask({
    203    getCacheKey(_, id) {
    204      return `take-census-task-${thisTakeCensusTaskId}-${id}`;
    205    },
    206 
    207    async task(heapWorker, id, removeFromCache, dispatch, getState) {
    208      const snapshot = getSnapshot(getState(), id);
    209      if (!snapshot) {
    210        removeFromCache();
    211        return;
    212      }
    213 
    214      // Assert that snapshot is in a valid state
    215      assert(
    216        canTakeCensus(snapshot),
    217        "Attempting to take a census when the snapshot is not in a ready state. " +
    218          `snapshot.state = ${snapshot.state}, ` +
    219          `census.state = ${(getCensus(snapshot) || { state: null }).state}`
    220      );
    221 
    222      let report, parentMap;
    223      let display = getDisplay(getState());
    224      let filter = getFilter(getState());
    225 
    226      // If display, filter and inversion haven't changed, don't do anything.
    227      if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
    228        removeFromCache();
    229        return;
    230      }
    231 
    232      // Keep taking a census if the display changes while our request is in
    233      // flight. Recheck that the display used for the census is the same as the
    234      // state's display.
    235      do {
    236        display = getDisplay(getState());
    237        filter = getState().filter;
    238 
    239        dispatch({
    240          type: beginAction,
    241          id,
    242          filter,
    243          display,
    244        });
    245 
    246        const opts = display.inverted
    247          ? { asInvertedTreeNode: true }
    248          : { asTreeNode: true };
    249 
    250        opts.filter = filter || null;
    251 
    252        try {
    253          ({ report, parentMap } = await heapWorker.takeCensus(
    254            snapshot.path,
    255            { breakdown: display.breakdown },
    256            opts
    257          ));
    258        } catch (error) {
    259          removeFromCache();
    260          reportException("takeCensus", error);
    261          dispatch({ type: errorAction, id, error });
    262          return;
    263        }
    264      } while (
    265        filter !== getState().filter ||
    266        display !== getDisplay(getState())
    267      );
    268 
    269      removeFromCache();
    270      dispatch({
    271        type: endAction,
    272        id,
    273        display,
    274        filter,
    275        report,
    276        parentMap,
    277      });
    278    },
    279  });
    280 }
    281 
    282 /**
    283 * Take a census.
    284 */
    285 const takeCensus = (exports.takeCensus = makeTakeCensusTask({
    286  getDisplay: state => state.censusDisplay,
    287  getFilter: state => state.filter,
    288  getCensus: snapshot => snapshot.census,
    289  beginAction: actions.TAKE_CENSUS_START,
    290  endAction: actions.TAKE_CENSUS_END,
    291  errorAction: actions.TAKE_CENSUS_ERROR,
    292  canTakeCensus: snapshot =>
    293    snapshot.state === states.READ &&
    294    (!snapshot.census || snapshot.census.state === censusState.SAVED),
    295 }));
    296 
    297 /**
    298 * Take a census for the treemap.
    299 */
    300 const takeTreeMap = (exports.takeTreeMap = makeTakeCensusTask({
    301  getDisplay: state => state.treeMapDisplay,
    302  getFilter: () => null,
    303  getCensus: snapshot => snapshot.treeMap,
    304  beginAction: actions.TAKE_TREE_MAP_START,
    305  endAction: actions.TAKE_TREE_MAP_END,
    306  errorAction: actions.TAKE_TREE_MAP_ERROR,
    307  canTakeCensus: snapshot =>
    308    snapshot.state === states.READ &&
    309    (!snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED),
    310 }));
    311 
    312 /**
    313 * Define what should be the default mode for taking a census based on the
    314 * default view of the tool.
    315 */
    316 const defaultCensusTaker = takeTreeMap;
    317 
    318 /**
    319 * Pick the default census taker when taking a snapshot. This should be
    320 * determined by the current view. If the view doesn't include a census, then
    321 * use the default one defined above. Some census information is always needed
    322 * to display some basic information about a snapshot.
    323 *
    324 * @param {string} value from viewState
    325 */
    326 const getCurrentCensusTaker = (exports.getCurrentCensusTaker = function (
    327  currentView
    328 ) {
    329  switch (currentView) {
    330    case viewState.TREE_MAP:
    331      return takeTreeMap;
    332    case viewState.CENSUS:
    333      return takeCensus;
    334    default:
    335      return defaultCensusTaker;
    336  }
    337 });
    338 
    339 /**
    340 * Focus the given node in the individuals view.
    341 *
    342 * @param {DominatorTreeNode} node.
    343 */
    344 exports.focusIndividual = function (node) {
    345  return {
    346    type: actions.FOCUS_INDIVIDUAL,
    347    node,
    348  };
    349 };
    350 
    351 /**
    352 * Fetch the individual `DominatorTreeNodes` for the census group specified by
    353 * `censusBreakdown` and `reportLeafIndex`.
    354 *
    355 * @param {HeapAnalysesClient} heapWorker
    356 * @param {SnapshotId} id
    357 * @param {object} censusBreakdown
    358 * @param {Set<number> | number} reportLeafIndex
    359 */
    360 const fetchIndividuals = (exports.fetchIndividuals = function (
    361  heapWorker,
    362  id,
    363  censusBreakdown,
    364  reportLeafIndex
    365 ) {
    366  return async function ({ dispatch, getState }) {
    367    if (getState().view.state !== viewState.INDIVIDUALS) {
    368      dispatch(view.changeView(viewState.INDIVIDUALS));
    369    }
    370 
    371    const snapshot = getSnapshot(getState(), id);
    372    assert(
    373      snapshot && snapshot.state === states.READ,
    374      "The snapshot should already be read into memory"
    375    );
    376 
    377    if (!dominatorTreeIsComputed(snapshot)) {
    378      await dispatch(computeAndFetchDominatorTree(heapWorker, id));
    379    }
    380 
    381    const snapshot_ = getSnapshot(getState(), id);
    382    assert(
    383      snapshot_.dominatorTree?.root,
    384      "Should have a dominator tree with a root."
    385    );
    386 
    387    const dominatorTreeId = snapshot_.dominatorTree.dominatorTreeId;
    388 
    389    const indices = isSet(reportLeafIndex)
    390      ? reportLeafIndex
    391      : new Set([reportLeafIndex]);
    392 
    393    let labelDisplay;
    394    let nodes;
    395    do {
    396      labelDisplay = getState().labelDisplay;
    397      assert(
    398        labelDisplay?.breakdown?.by,
    399        `Should have a breakdown to label nodes with, got: ${JSON.stringify(
    400          labelDisplay
    401        )}`
    402      );
    403 
    404      if (getState().view.state !== viewState.INDIVIDUALS) {
    405        // We switched views while in the process of fetching individuals -- any
    406        // further work is useless.
    407        return;
    408      }
    409 
    410      dispatch({ type: actions.FETCH_INDIVIDUALS_START });
    411 
    412      try {
    413        ({ nodes } = await heapWorker.getCensusIndividuals({
    414          dominatorTreeId,
    415          indices,
    416          censusBreakdown,
    417          labelBreakdown: labelDisplay.breakdown,
    418          maxRetainingPaths: Services.prefs.getIntPref(
    419            "devtools.memory.max-retaining-paths"
    420          ),
    421          maxIndividuals: Services.prefs.getIntPref(
    422            "devtools.memory.max-individuals"
    423          ),
    424        }));
    425      } catch (error) {
    426        reportException("actions/snapshot/fetchIndividuals", error);
    427        dispatch({ type: actions.INDIVIDUALS_ERROR, error });
    428        return;
    429      }
    430    } while (labelDisplay !== getState().labelDisplay);
    431 
    432    dispatch({
    433      type: actions.FETCH_INDIVIDUALS_END,
    434      id,
    435      censusBreakdown,
    436      indices,
    437      labelDisplay,
    438      nodes,
    439      dominatorTree: snapshot_.dominatorTree,
    440    });
    441  };
    442 });
    443 
    444 /**
    445 * Refresh the current individuals view.
    446 *
    447 * @param {HeapAnalysesClient} heapWorker
    448 */
    449 exports.refreshIndividuals = function (heapWorker) {
    450  return async function ({ dispatch, getState }) {
    451    assert(
    452      getState().view.state === viewState.INDIVIDUALS,
    453      "Should be in INDIVIDUALS view."
    454    );
    455 
    456    const { individuals } = getState();
    457 
    458    switch (individuals.state) {
    459      case individualsState.COMPUTING_DOMINATOR_TREE:
    460      case individualsState.FETCHING:
    461        // Nothing to do here.
    462        return;
    463 
    464      case individualsState.FETCHED:
    465        if (getState().individuals.labelDisplay === getState().labelDisplay) {
    466          return;
    467        }
    468        break;
    469 
    470      case individualsState.ERROR:
    471        // Doesn't hurt to retry: maybe we won't get an error this time around?
    472        break;
    473 
    474      default:
    475        assert(false, `Unexpected individuals state: ${individuals.state}`);
    476        return;
    477    }
    478 
    479    await dispatch(
    480      fetchIndividuals(
    481        heapWorker,
    482        individuals.id,
    483        individuals.censusBreakdown,
    484        individuals.indices
    485      )
    486    );
    487  };
    488 };
    489 
    490 /**
    491 * Refresh the selected snapshot's census data, if need be (for example,
    492 * display configuration changed).
    493 *
    494 * @param {HeapAnalysesClient} heapWorker
    495 */
    496 exports.refreshSelectedCensus = function (heapWorker) {
    497  return async function ({ dispatch, getState }) {
    498    const snapshot = getState().snapshots.find(s => s.selected);
    499    if (!snapshot || snapshot.state !== states.READ) {
    500      return;
    501    }
    502 
    503    // Intermediate snapshot states will get handled by the task action that is
    504    // orchestrating them. For example, if the snapshot census's state is
    505    // SAVING, then the takeCensus action will keep taking a census until
    506    // the inverted property matches the inverted state. If the snapshot is
    507    // still in the process of being saved or read, the takeSnapshotAndCensus
    508    // task action will follow through and ensure that a census is taken.
    509    if (
    510      (snapshot.census && snapshot.census.state === censusState.SAVED) ||
    511      !snapshot.census
    512    ) {
    513      await dispatch(takeCensus(heapWorker, snapshot.id));
    514    }
    515  };
    516 };
    517 
    518 /**
    519 * Refresh the selected snapshot's tree map data, if need be (for example,
    520 * display configuration changed).
    521 *
    522 * @param {HeapAnalysesClient} heapWorker
    523 */
    524 exports.refreshSelectedTreeMap = function (heapWorker) {
    525  return async function ({ dispatch, getState }) {
    526    const snapshot = getState().snapshots.find(s => s.selected);
    527    if (!snapshot || snapshot.state !== states.READ) {
    528      return;
    529    }
    530 
    531    // Intermediate snapshot states will get handled by the task action that is
    532    // orchestrating them. For example, if the snapshot census's state is
    533    // SAVING, then the takeCensus action will keep taking a census until
    534    // the inverted property matches the inverted state. If the snapshot is
    535    // still in the process of being saved or read, the takeSnapshotAndCensus
    536    // task action will follow through and ensure that a census is taken.
    537    if (
    538      (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
    539      !snapshot.treeMap
    540    ) {
    541      await dispatch(takeTreeMap(heapWorker, snapshot.id));
    542    }
    543  };
    544 };
    545 
    546 /**
    547 * Request that the `HeapAnalysesWorker` compute the dominator tree for the
    548 * snapshot with the given `id`.
    549 *
    550 * @param {HeapAnalysesClient} heapWorker
    551 * @param {SnapshotId} id
    552 *
    553 * @returns {Promise<DominatorTreeId>}
    554 */
    555 const computeDominatorTree = (exports.computeDominatorTree =
    556  TaskCache.declareCacheableTask({
    557    getCacheKey(_, id) {
    558      return id;
    559    },
    560 
    561    async task(heapWorker, id, removeFromCache, dispatch, getState) {
    562      const snapshot = getSnapshot(getState(), id);
    563      assert(
    564        !snapshot.dominatorTree?.dominatorTreeId,
    565        "Should not re-compute dominator trees"
    566      );
    567 
    568      dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_START, id });
    569 
    570      let dominatorTreeId;
    571      try {
    572        dominatorTreeId = await heapWorker.computeDominatorTree(snapshot.path);
    573      } catch (error) {
    574        removeFromCache();
    575        reportException("actions/snapshot/computeDominatorTree", error);
    576        dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
    577        return null;
    578      }
    579 
    580      removeFromCache();
    581      dispatch({
    582        type: actions.COMPUTE_DOMINATOR_TREE_END,
    583        id,
    584        dominatorTreeId,
    585      });
    586      return dominatorTreeId;
    587    },
    588  }));
    589 
    590 /**
    591 * Get the partial subtree, starting from the root, of the
    592 * snapshot-with-the-given-id's dominator tree.
    593 *
    594 * @param {HeapAnalysesClient} heapWorker
    595 * @param {SnapshotId} id
    596 *
    597 * @returns {Promise<DominatorTreeNode>}
    598 */
    599 const fetchDominatorTree = (exports.fetchDominatorTree =
    600  TaskCache.declareCacheableTask({
    601    getCacheKey(_, id) {
    602      return id;
    603    },
    604 
    605    async task(heapWorker, id, removeFromCache, dispatch, getState) {
    606      const snapshot = getSnapshot(getState(), id);
    607      assert(
    608        dominatorTreeIsComputed(snapshot),
    609        "Should have dominator tree model and it should be computed"
    610      );
    611 
    612      let display;
    613      let root;
    614      do {
    615        display = getState().labelDisplay;
    616        assert(
    617          display?.breakdown,
    618          `Should have a breakdown to describe nodes with, got: ${JSON.stringify(
    619            display
    620          )}`
    621        );
    622 
    623        dispatch({ type: actions.FETCH_DOMINATOR_TREE_START, id, display });
    624 
    625        try {
    626          root = await heapWorker.getDominatorTree({
    627            dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
    628            breakdown: display.breakdown,
    629            maxRetainingPaths: Services.prefs.getIntPref(
    630              "devtools.memory.max-retaining-paths"
    631            ),
    632          });
    633        } catch (error) {
    634          removeFromCache();
    635          reportException("actions/snapshot/fetchDominatorTree", error);
    636          dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
    637          return null;
    638        }
    639      } while (display !== getState().labelDisplay);
    640 
    641      removeFromCache();
    642      dispatch({ type: actions.FETCH_DOMINATOR_TREE_END, id, root });
    643      return root;
    644    },
    645  }));
    646 
    647 /**
    648 * Fetch the immediately dominated children represented by the placeholder
    649 * `lazyChildren` from snapshot-with-the-given-id's dominator tree.
    650 *
    651 * @param {HeapAnalysesClient} heapWorker
    652 * @param {SnapshotId} id
    653 * @param {DominatorTreeLazyChildren} lazyChildren
    654 */
    655 exports.fetchImmediatelyDominated = TaskCache.declareCacheableTask({
    656  getCacheKey(_, id, lazyChildren) {
    657    return `${id}-${lazyChildren.key()}`;
    658  },
    659 
    660  async task(
    661    heapWorker,
    662    id,
    663    lazyChildren,
    664    removeFromCache,
    665    dispatch,
    666    getState
    667  ) {
    668    const snapshot = getSnapshot(getState(), id);
    669    assert(snapshot.dominatorTree, "Should have dominator tree model");
    670    assert(
    671      snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
    672        snapshot.dominatorTree.state ===
    673          dominatorTreeState.INCREMENTAL_FETCHING,
    674      "Cannot fetch immediately dominated nodes in a dominator tree unless " +
    675        " the dominator tree has already been computed"
    676    );
    677 
    678    let display;
    679    let response;
    680    do {
    681      display = getState().labelDisplay;
    682      assert(display, "Should have a display to describe nodes with.");
    683 
    684      dispatch({ type: actions.FETCH_IMMEDIATELY_DOMINATED_START, id });
    685 
    686      try {
    687        response = await heapWorker.getImmediatelyDominated({
    688          dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
    689          breakdown: display.breakdown,
    690          nodeId: lazyChildren.parentNodeId(),
    691          startIndex: lazyChildren.siblingIndex(),
    692          maxRetainingPaths: Services.prefs.getIntPref(
    693            "devtools.memory.max-retaining-paths"
    694          ),
    695        });
    696      } catch (error) {
    697        removeFromCache();
    698        reportException("actions/snapshot/fetchImmediatelyDominated", error);
    699        dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
    700        return;
    701      }
    702    } while (display !== getState().labelDisplay);
    703 
    704    removeFromCache();
    705    dispatch({
    706      type: actions.FETCH_IMMEDIATELY_DOMINATED_END,
    707      id,
    708      path: response.path,
    709      nodes: response.nodes,
    710      moreChildrenAvailable: response.moreChildrenAvailable,
    711    });
    712  },
    713 });
    714 
    715 /**
    716 * Compute and then fetch the dominator tree of the snapshot with the given
    717 * `id`.
    718 *
    719 * @param {HeapAnalysesClient} heapWorker
    720 * @param {SnapshotId} id
    721 *
    722 * @returns {Promise<DominatorTreeNode>}
    723 */
    724 const computeAndFetchDominatorTree = (exports.computeAndFetchDominatorTree =
    725  TaskCache.declareCacheableTask({
    726    getCacheKey(_, id) {
    727      return id;
    728    },
    729 
    730    async task(heapWorker, id, removeFromCache, dispatch) {
    731      const dominatorTreeId = await dispatch(
    732        computeDominatorTree(heapWorker, id)
    733      );
    734      if (dominatorTreeId === null) {
    735        removeFromCache();
    736        return null;
    737      }
    738 
    739      const root = await dispatch(fetchDominatorTree(heapWorker, id));
    740      removeFromCache();
    741 
    742      if (!root) {
    743        return null;
    744      }
    745 
    746      return root;
    747    },
    748  }));
    749 
    750 /**
    751 * Update the currently selected snapshot's dominator tree.
    752 *
    753 * @param {HeapAnalysesClient} heapWorker
    754 */
    755 exports.refreshSelectedDominatorTree = function (heapWorker) {
    756  return async function ({ dispatch, getState }) {
    757    const snapshot = getState().snapshots.find(s => s.selected);
    758    if (!snapshot) {
    759      return;
    760    }
    761 
    762    if (
    763      snapshot.dominatorTree &&
    764      !(
    765        snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
    766        snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
    767        snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING
    768      )
    769    ) {
    770      return;
    771    }
    772 
    773    // We need to check for the snapshot state because if there was an error,
    774    // we can't continue and if we are still saving or reading the snapshot,
    775    // then takeSnapshotAndCensus will finish the job for us
    776    if (snapshot.state === states.READ) {
    777      if (snapshot.dominatorTree) {
    778        await dispatch(fetchDominatorTree(heapWorker, snapshot.id));
    779      } else {
    780        await dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
    781      }
    782    }
    783  };
    784 };
    785 
    786 /**
    787 * Select the snapshot with the given id.
    788 *
    789 * @param {snapshotId} id
    790 * @see {Snapshot} model defined in devtools/client/memory/models.js
    791 */
    792 const selectSnapshot = (exports.selectSnapshot = function (id) {
    793  return {
    794    type: actions.SELECT_SNAPSHOT,
    795    id,
    796  };
    797 });
    798 
    799 /**
    800 * Delete all snapshots that are in the READ or ERROR state
    801 *
    802 * @param {HeapAnalysesClient} heapWorker
    803 */
    804 exports.clearSnapshots = function (heapWorker) {
    805  return async function ({ dispatch, getState }) {
    806    const snapshots = getState().snapshots.filter(s => {
    807      const snapshotReady = s.state === states.READ || s.state === states.ERROR;
    808      const censusReady =
    809        (s.treeMap && s.treeMap.state === treeMapState.SAVED) ||
    810        (s.census && s.census.state === censusState.SAVED);
    811 
    812      return snapshotReady && censusReady;
    813    });
    814 
    815    const ids = snapshots.map(s => s.id);
    816 
    817    dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids });
    818 
    819    if (getState().diffing) {
    820      dispatch(diffing.toggleDiffing());
    821    }
    822    if (getState().individuals) {
    823      dispatch(view.popView());
    824    }
    825 
    826    await Promise.all(
    827      snapshots.map(snapshot => {
    828        return heapWorker.deleteHeapSnapshot(snapshot.path).catch(error => {
    829          reportException("clearSnapshots", error);
    830          dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
    831        });
    832      })
    833    );
    834 
    835    dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids });
    836  };
    837 };
    838 
    839 /**
    840 * Delete a snapshot
    841 *
    842 * @param {HeapAnalysesClient} heapWorker
    843 * @param {snapshotModel} snapshot
    844 */
    845 exports.deleteSnapshot = function (heapWorker, snapshot) {
    846  return async function ({ dispatch }) {
    847    dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids: [snapshot.id] });
    848 
    849    try {
    850      await heapWorker.deleteHeapSnapshot(snapshot.path);
    851    } catch (error) {
    852      reportException("deleteSnapshot", error);
    853      dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
    854    }
    855 
    856    dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids: [snapshot.id] });
    857  };
    858 };
    859 
    860 /**
    861 * Expand the given node in the snapshot's census report.
    862 *
    863 * @param {CensusTreeNode} node
    864 */
    865 exports.expandCensusNode = function (id, node) {
    866  return {
    867    type: actions.EXPAND_CENSUS_NODE,
    868    id,
    869    node,
    870  };
    871 };
    872 
    873 /**
    874 * Collapse the given node in the snapshot's census report.
    875 *
    876 * @param {CensusTreeNode} node
    877 */
    878 exports.collapseCensusNode = function (id, node) {
    879  return {
    880    type: actions.COLLAPSE_CENSUS_NODE,
    881    id,
    882    node,
    883  };
    884 };
    885 
    886 /**
    887 * Focus the given node in the snapshot's census's report.
    888 *
    889 * @param {SnapshotId} id
    890 * @param {DominatorTreeNode} node
    891 */
    892 exports.focusCensusNode = function (id, node) {
    893  return {
    894    type: actions.FOCUS_CENSUS_NODE,
    895    id,
    896    node,
    897  };
    898 };
    899 
    900 /**
    901 * Expand the given node in the snapshot's dominator tree.
    902 *
    903 * @param {DominatorTreeTreeNode} node
    904 */
    905 exports.expandDominatorTreeNode = function (id, node) {
    906  return {
    907    type: actions.EXPAND_DOMINATOR_TREE_NODE,
    908    id,
    909    node,
    910  };
    911 };
    912 
    913 /**
    914 * Collapse the given node in the snapshot's dominator tree.
    915 *
    916 * @param {DominatorTreeTreeNode} node
    917 */
    918 exports.collapseDominatorTreeNode = function (id, node) {
    919  return {
    920    type: actions.COLLAPSE_DOMINATOR_TREE_NODE,
    921    id,
    922    node,
    923  };
    924 };
    925 
    926 /**
    927 * Focus the given node in the snapshot's dominator tree.
    928 *
    929 * @param {SnapshotId} id
    930 * @param {DominatorTreeNode} node
    931 */
    932 exports.focusDominatorTreeNode = function (id, node) {
    933  return {
    934    type: actions.FOCUS_DOMINATOR_TREE_NODE,
    935    id,
    936    node,
    937  };
    938 };