tor-browser

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

models.js (17519B)


      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 
      5 /* global treeMapState, censusState */
      6 /* eslint no-shadow: ["error", { "allow": ["app"] }] */
      7 
      8 "use strict";
      9 
     10 const { assert } = require("resource://devtools/shared/DevToolsUtils.js");
     11 const { MemoryFront } = require("resource://devtools/client/fronts/memory.js");
     12 const HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js");
     13 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     14 const {
     15  snapshotState: states,
     16  diffingState,
     17  dominatorTreeState,
     18  viewState,
     19  individualsState,
     20 } = require("resource://devtools/client/memory/constants.js");
     21 
     22 /**
     23 * ONLY USE THIS FOR MODEL VALIDATORS IN CONJUCTION WITH assert()!
     24 *
     25 * React checks that the returned values from validator functions are instances
     26 * of Error, but because React is loaded in its own global, that check is always
     27 * false and always results in a warning.
     28 *
     29 * To work around this and still get model validation, just call assert() inside
     30 * a function passed to catchAndIgnore. The assert() function will still report
     31 * assertion failures, but this funciton will swallow the errors so that React
     32 * doesn't go crazy and drown out the real error in irrelevant and incorrect
     33 * warnings.
     34 *
     35 * Example usage:
     36 *
     37 *     const MyModel = PropTypes.shape({
     38 *       someProperty: catchAndIgnore(function (model) {
     39 *         assert(someInvariant(model.someProperty), "Should blah blah");
     40 *       })
     41 *     });
     42 */
     43 function catchAndIgnore(fn) {
     44  return function (...args) {
     45    try {
     46      fn(...args);
     47    } catch (err) {
     48      // continue regardless of error
     49    }
     50 
     51    return null;
     52  };
     53 }
     54 
     55 /**
     56 * The data describing the census report's shape, and its associated metadata.
     57 *
     58 * @see `js/src/doc/Debugger/Debugger.Memory.md`
     59 */
     60 const censusDisplayModel = (exports.censusDisplay = PropTypes.shape({
     61  displayName: PropTypes.string.isRequired,
     62  tooltip: PropTypes.string.isRequired,
     63  inverted: PropTypes.bool.isRequired,
     64  breakdown: PropTypes.shape({
     65    by: PropTypes.string.isRequired,
     66  }),
     67 }));
     68 
     69 /**
     70 * How we want to label nodes in the dominator tree, and associated
     71 * metadata. The notable difference from `censusDisplayModel` is the lack of
     72 * an `inverted` property.
     73 *
     74 * @see `js/src/doc/Debugger/Debugger.Memory.md`
     75 */
     76 const labelDisplayModel = (exports.labelDisplay = PropTypes.shape({
     77  displayName: PropTypes.string.isRequired,
     78  tooltip: PropTypes.string.isRequired,
     79  breakdown: PropTypes.shape({
     80    by: PropTypes.string.isRequired,
     81  }),
     82 }));
     83 
     84 /**
     85 * The data describing the tree map's shape, and its associated metadata.
     86 *
     87 * @see `js/src/doc/Debugger/Debugger.Memory.md`
     88 */
     89 const treeMapDisplayModel = (exports.treeMapDisplay = PropTypes.shape({
     90  displayName: PropTypes.string.isRequired,
     91  tooltip: PropTypes.string.isRequired,
     92  inverted: PropTypes.bool.isRequired,
     93  breakdown: PropTypes.shape({
     94    by: PropTypes.string.isRequired,
     95  }),
     96 }));
     97 
     98 /**
     99 * Tree map model.
    100 */
    101 const treeMapModel = (exports.treeMapModel = PropTypes.shape({
    102  // The current census report data.
    103  report: PropTypes.object,
    104  // The display data used to generate the current census.
    105  display: treeMapDisplayModel,
    106  // The current treeMapState this is in
    107  state: catchAndIgnore(function (treeMap) {
    108    switch (treeMap.state) {
    109      case treeMapState.SAVING:
    110        assert(!treeMap.report, "Should not have a report");
    111        assert(!treeMap.error, "Should not have an error");
    112        break;
    113 
    114      case treeMapState.SAVED:
    115        assert(treeMap.report, "Should have a report");
    116        assert(!treeMap.error, "Should not have an error");
    117        break;
    118 
    119      case treeMapState.ERROR:
    120        assert(treeMap.error, "Should have an error");
    121        break;
    122 
    123      default:
    124        assert(false, `Unexpected treeMap state: ${treeMap.state}`);
    125    }
    126  }),
    127 }));
    128 
    129 const censusModel = (exports.censusModel = PropTypes.shape({
    130  // The current census report data.
    131  report: PropTypes.object,
    132  // The parent map for the report.
    133  parentMap: PropTypes.object,
    134  // The display data used to generate the current census.
    135  display: censusDisplayModel,
    136  // If present, the currently cached report's filter string used for pruning
    137  // the tree items.
    138  filter: PropTypes.string,
    139  // The Set<CensusTreeNode.id> of expanded node ids in the report
    140  // tree.
    141  expanded: catchAndIgnore(function (census) {
    142    if (census.report) {
    143      assert(
    144        census.expanded,
    145        "If we have a report, we should also have the set of expanded nodes"
    146      );
    147    }
    148  }),
    149  // If a node is currently focused in the report tree, then this is it.
    150  focused: PropTypes.object,
    151  // The censusModelState that this census is currently in.
    152  state: catchAndIgnore(function (census) {
    153    switch (census.state) {
    154      case censusState.SAVING:
    155        assert(!census.report, "Should not have a report");
    156        assert(!census.parentMap, "Should not have a parent map");
    157        assert(census.expanded, "Should not have an expanded set");
    158        assert(!census.error, "Should not have an error");
    159        break;
    160 
    161      case censusState.SAVED:
    162        assert(census.report, "Should have a report");
    163        assert(census.parentMap, "Should have a parent map");
    164        assert(census.expanded, "Should have an expanded set");
    165        assert(!census.error, "Should not have an error");
    166        break;
    167 
    168      case censusState.ERROR:
    169        assert(!census.report, "Should not have a report");
    170        assert(census.error, "Should have an error");
    171        break;
    172 
    173      default:
    174        assert(false, `Unexpected census state: ${census.state}`);
    175    }
    176  }),
    177 }));
    178 
    179 /**
    180 * Dominator tree model.
    181 */
    182 const dominatorTreeModel = (exports.dominatorTreeModel = PropTypes.shape({
    183  // The id of this dominator tree.
    184  dominatorTreeId: PropTypes.number,
    185 
    186  // The root DominatorTreeNode of this dominator tree.
    187  root: PropTypes.object,
    188 
    189  // The Set<NodeId> of expanded nodes in this dominator tree.
    190  expanded: PropTypes.object,
    191 
    192  // If a node is currently focused in the dominator tree, then this is it.
    193  focused: PropTypes.object,
    194 
    195  // If an error was thrown while getting this dominator tree, the `Error`
    196  // instance (or an error string message) is attached here.
    197  error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
    198 
    199  // The display used to generate descriptive labels of nodes in this dominator
    200  // tree.
    201  display: labelDisplayModel,
    202 
    203  // The number of active requests to incrementally fetch subtrees. This should
    204  // only be non-zero when the state is INCREMENTAL_FETCHING.
    205  activeFetchRequestCount: PropTypes.number,
    206 
    207  // The dominatorTreeState that this domintor tree is currently in.
    208  state: catchAndIgnore(function (dominatorTree) {
    209    switch (dominatorTree.state) {
    210      case dominatorTreeState.COMPUTING:
    211        assert(
    212          dominatorTree.dominatorTreeId == null,
    213          "Should not have a dominator tree id yet"
    214        );
    215        assert(!dominatorTree.root, "Should not have the root of the tree yet");
    216        assert(!dominatorTree.error, "Should not have an error");
    217        break;
    218 
    219      case dominatorTreeState.COMPUTED:
    220      case dominatorTreeState.FETCHING:
    221        assert(
    222          dominatorTree.dominatorTreeId != null,
    223          "Should have a dominator tree id"
    224        );
    225        assert(!dominatorTree.root, "Should not have the root of the tree yet");
    226        assert(!dominatorTree.error, "Should not have an error");
    227        break;
    228 
    229      case dominatorTreeState.INCREMENTAL_FETCHING:
    230        assert(
    231          typeof dominatorTree.activeFetchRequestCount === "number",
    232          "The active fetch request count is a number when we are in the " +
    233            "INCREMENTAL_FETCHING state"
    234        );
    235        assert(
    236          dominatorTree.activeFetchRequestCount > 0,
    237          "We are keeping track of how many active requests are in flight."
    238        );
    239      // Fall through...
    240      case dominatorTreeState.LOADED:
    241        assert(
    242          dominatorTree.dominatorTreeId != null,
    243          "Should have a dominator tree id"
    244        );
    245        assert(dominatorTree.root, "Should have the root of the tree");
    246        assert(dominatorTree.expanded, "Should have an expanded set");
    247        assert(!dominatorTree.error, "Should not have an error");
    248        break;
    249 
    250      case dominatorTreeState.ERROR:
    251        assert(dominatorTree.error, "Should have an error");
    252        break;
    253 
    254      default:
    255        assert(
    256          false,
    257          `Unexpected dominator tree state: ${dominatorTree.state}`
    258        );
    259    }
    260  }),
    261 }));
    262 
    263 /**
    264 * Snapshot model.
    265 */
    266 const stateKeys = Object.keys(states).map(state => states[state]);
    267 const snapshotId = PropTypes.number;
    268 const snapshotModel = (exports.snapshot = PropTypes.shape({
    269  // Unique ID for a snapshot
    270  id: snapshotId.isRequired,
    271  // Whether or not this snapshot is currently selected.
    272  selected: PropTypes.bool.isRequired,
    273  // Filesystem path to where the snapshot is stored; used to identify the
    274  // snapshot for HeapAnalysesClient.
    275  path: PropTypes.string,
    276  // Current census data for this snapshot.
    277  census: censusModel,
    278  // Current dominator tree data for this snapshot.
    279  dominatorTree: dominatorTreeModel,
    280  // Current tree map data for this snapshot.
    281  treeMap: treeMapModel,
    282  // If an error was thrown while processing this snapshot, the `Error` instance
    283  // is attached here.
    284  error: PropTypes.object,
    285  // Boolean indicating whether or not this snapshot was imported.
    286  imported: PropTypes.bool.isRequired,
    287  // The creation time of the snapshot; required after the snapshot has been
    288  // read.
    289  creationTime: PropTypes.number,
    290  // The current state the snapshot is in.
    291  // @see ./constants.js
    292  state: catchAndIgnore(function (snapshot) {
    293    const current = snapshot.state;
    294    const shouldHavePath = [states.IMPORTING, states.SAVED, states.READ];
    295    const shouldHaveCreationTime = [states.READ];
    296 
    297    if (!stateKeys.includes(current)) {
    298      throw new Error(`Snapshot state must be one of ${stateKeys}.`);
    299    }
    300    if (shouldHavePath.includes(current) && !snapshot.path) {
    301      throw new Error(
    302        `Snapshots in state ${current} must have a snapshot path.`
    303      );
    304    }
    305    if (shouldHaveCreationTime.includes(current) && !snapshot.creationTime) {
    306      throw new Error(
    307        `Snapshots in state ${current} must have a creation time.`
    308      );
    309    }
    310  }),
    311 }));
    312 
    313 const allocationsModel = (exports.allocations = PropTypes.shape({
    314  // True iff we are recording allocation stacks right now.
    315  recording: PropTypes.bool.isRequired,
    316  // True iff we are in the process of toggling the recording of allocation
    317  // stacks on or off right now.
    318  togglingInProgress: PropTypes.bool.isRequired,
    319 }));
    320 
    321 const diffingModel = (exports.diffingModel = PropTypes.shape({
    322  // The id of the first snapshot to diff.
    323  firstSnapshotId: snapshotId,
    324 
    325  // The id of the second snapshot to diff.
    326  secondSnapshotId: catchAndIgnore(function (diffing, propName) {
    327    if (diffing.secondSnapshotId && !diffing.firstSnapshotId) {
    328      throw new Error(
    329        "Cannot have second snapshot without already having " + "first snapshot"
    330      );
    331    }
    332    return snapshotId(diffing, propName);
    333  }),
    334 
    335  // The current census data for the diffing.
    336  census: censusModel,
    337 
    338  // If an error was thrown while diffing, the `Error` instance is attached
    339  // here.
    340  error: PropTypes.object,
    341 
    342  // The current state the diffing is in.
    343  // @see ./constants.js
    344  state: catchAndIgnore(function (diffing) {
    345    switch (diffing.state) {
    346      case diffingState.TOOK_DIFF:
    347        assert(diffing.census, "If we took a diff, we should have a census");
    348      // Fall through...
    349      case diffingState.TAKING_DIFF:
    350        assert(diffing.firstSnapshotId, "Should have first snapshot");
    351        assert(diffing.secondSnapshotId, "Should have second snapshot");
    352        break;
    353 
    354      case diffingState.SELECTING:
    355        break;
    356 
    357      case diffingState.ERROR:
    358        assert(diffing.error, "Should have error");
    359        break;
    360 
    361      default:
    362        assert(false, `Bad diffing state: ${diffing.state}`);
    363    }
    364  }),
    365 }));
    366 
    367 const previousViewModel = (exports.previousView = PropTypes.shape({
    368  state: catchAndIgnore(function (previous) {
    369    switch (previous.state) {
    370      case viewState.DIFFING:
    371        assert(previous.diffing, "Should have previous diffing state.");
    372        assert(
    373          !previous.selected,
    374          "Should not have a previously selected snapshot."
    375        );
    376        break;
    377 
    378      case viewState.CENSUS:
    379      case viewState.DOMINATOR_TREE:
    380      case viewState.TREE_MAP:
    381        assert(
    382          previous.selected,
    383          "Should have a previously selected snapshot."
    384        );
    385        break;
    386 
    387      case viewState.INDIVIDUALS:
    388      default:
    389        assert(false, `Unexpected previous view state: ${previous.state}.`);
    390    }
    391  }),
    392 
    393  // The previous diffing state, if any.
    394  diffing: diffingModel,
    395 
    396  // The previously selected snapshot, if any.
    397  selected: snapshotId,
    398 }));
    399 
    400 exports.view = PropTypes.shape({
    401  // The current view state.
    402  state: catchAndIgnore(function (view) {
    403    switch (view.state) {
    404      case viewState.DIFFING:
    405      case viewState.CENSUS:
    406      case viewState.DOMINATOR_TREE:
    407      case viewState.INDIVIDUALS:
    408      case viewState.TREE_MAP:
    409        break;
    410 
    411      default:
    412        assert(false, `Unexpected type of view: ${view.state}`);
    413    }
    414  }),
    415 
    416  // The previous view state.
    417  previous: previousViewModel,
    418 });
    419 
    420 const individualsModel = (exports.individuals = PropTypes.shape({
    421  error: PropTypes.object,
    422 
    423  nodes: PropTypes.arrayOf(PropTypes.object),
    424 
    425  dominatorTree: dominatorTreeModel,
    426 
    427  id: snapshotId,
    428 
    429  censusBreakdown: PropTypes.object,
    430 
    431  indices: PropTypes.object,
    432 
    433  labelDisplay: labelDisplayModel,
    434 
    435  focused: PropTypes.object,
    436 
    437  state: catchAndIgnore(function (individuals) {
    438    switch (individuals.state) {
    439      case individualsState.COMPUTING_DOMINATOR_TREE:
    440      case individualsState.FETCHING:
    441        assert(!individuals.nodes, "Should not have individual nodes");
    442        assert(!individuals.dominatorTree, "Should not have dominator tree");
    443        assert(!individuals.id, "Should not have an id");
    444        assert(
    445          !individuals.censusBreakdown,
    446          "Should not have a censusBreakdown"
    447        );
    448        assert(!individuals.indices, "Should not have indices");
    449        assert(!individuals.labelDisplay, "Should not have a labelDisplay");
    450        break;
    451 
    452      case individualsState.FETCHED:
    453        assert(individuals.nodes, "Should have individual nodes");
    454        assert(individuals.dominatorTree, "Should have dominator tree");
    455        assert(individuals.id, "Should have an id");
    456        assert(individuals.censusBreakdown, "Should have a censusBreakdown");
    457        assert(individuals.indices, "Should have indices");
    458        assert(individuals.labelDisplay, "Should have a labelDisplay");
    459        break;
    460 
    461      case individualsState.ERROR:
    462        assert(individuals.error, "Should have an error object");
    463        break;
    464 
    465      default:
    466        assert(false, `Unexpected individuals state: ${individuals.state}`);
    467        break;
    468    }
    469  }),
    470 }));
    471 
    472 exports.app = {
    473  // {Commands} Used to communicate with the backend
    474  commands: PropTypes.object,
    475 
    476  // {MemoryFront} Used to communicate with platform
    477  front: PropTypes.instanceOf(MemoryFront),
    478 
    479  // Allocations recording related data.
    480  allocations: allocationsModel.isRequired,
    481 
    482  // {HeapAnalysesClient} Used to interface with snapshots
    483  heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
    484 
    485  // The display data describing how we want the census data to be.
    486  censusDisplay: censusDisplayModel.isRequired,
    487 
    488  // The display data describing how we want the dominator tree labels to be
    489  // computed.
    490  labelDisplay: labelDisplayModel.isRequired,
    491 
    492  // The display data describing how we want the dominator tree labels to be
    493  // computed.
    494  treeMapDisplay: treeMapDisplayModel.isRequired,
    495 
    496  // List of reference to all snapshots taken
    497  snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
    498 
    499  // If present, a filter string for pruning the tree items.
    500  filter: PropTypes.string,
    501 
    502  // If present, the current diffing state.
    503  diffing: diffingModel,
    504 
    505  // If present, the current individuals state.
    506  individuals: individualsModel,
    507 
    508  // The current type of view.
    509  view(app) {
    510    catchAndIgnore(function (app) {
    511      switch (app.view.state) {
    512        case viewState.DIFFING:
    513          assert(app.diffing, "Should be diffing");
    514          break;
    515 
    516        case viewState.INDIVIDUALS:
    517        case viewState.CENSUS:
    518        case viewState.DOMINATOR_TREE:
    519        case viewState.TREE_MAP:
    520          assert(!app.diffing, "Should not be diffing");
    521          break;
    522 
    523        default:
    524          assert(false, `Unexpected type of view: ${app.view.state}`);
    525      }
    526    })(app);
    527 
    528    catchAndIgnore(function (app) {
    529      switch (app.view.state) {
    530        case viewState.INDIVIDUALS:
    531          assert(app.individuals, "Should have individuals state");
    532          break;
    533 
    534        case viewState.DIFFING:
    535        case viewState.CENSUS:
    536        case viewState.DOMINATOR_TREE:
    537        case viewState.TREE_MAP:
    538          assert(!app.individuals, "Should not have individuals state");
    539          break;
    540 
    541        default:
    542          assert(false, `Unexpected type of view: ${app.view.state}`);
    543      }
    544    })(app);
    545  },
    546 };