summaryrefslogtreecommitdiffstats
path: root/devtools/client/memory/models.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/memory/models.js')
-rw-r--r--devtools/client/memory/models.js519
1 files changed, 519 insertions, 0 deletions
diff --git a/devtools/client/memory/models.js b/devtools/client/memory/models.js
new file mode 100644
index 000000000..7624f7acc
--- /dev/null
+++ b/devtools/client/memory/models.js
@@ -0,0 +1,519 @@
+/* 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/. */
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { MemoryFront } = require("devtools/shared/fronts/memory");
+const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
+const { PropTypes } = require("devtools/client/shared/vendor/react");
+const {
+ snapshotState: states,
+ diffingState,
+ dominatorTreeState,
+ viewState,
+ individualsState,
+} = require("./constants");
+
+/**
+ * ONLY USE THIS FOR MODEL VALIDATORS IN CONJUCTION WITH assert()!
+ *
+ * React checks that the returned values from validator functions are instances
+ * of Error, but because React is loaded in its own global, that check is always
+ * false and always results in a warning.
+ *
+ * To work around this and still get model validation, just call assert() inside
+ * a function passed to catchAndIgnore. The assert() function will still report
+ * assertion failures, but this funciton will swallow the errors so that React
+ * doesn't go crazy and drown out the real error in irrelevant and incorrect
+ * warnings.
+ *
+ * Example usage:
+ *
+ * const MyModel = PropTypes.shape({
+ * someProperty: catchAndIgnore(function (model) {
+ * assert(someInvariant(model.someProperty), "Should blah blah");
+ * })
+ * });
+ */
+function catchAndIgnore(fn) {
+ return function (...args) {
+ try {
+ fn(...args);
+ } catch (err) { }
+
+ return null;
+ };
+}
+
+/**
+ * The data describing the census report's shape, and its associated metadata.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const censusDisplayModel = exports.censusDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ inverted: PropTypes.bool.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ })
+});
+
+/**
+ * How we want to label nodes in the dominator tree, and associated
+ * metadata. The notable difference from `censusDisplayModel` is the lack of
+ * an `inverted` property.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const labelDisplayModel = exports.labelDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ })
+});
+
+/**
+ * The data describing the tree map's shape, and its associated metadata.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const treeMapDisplayModel = exports.treeMapDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ inverted: PropTypes.bool.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ })
+});
+
+/**
+ * Tree map model.
+ */
+const treeMapModel = exports.treeMapModel = PropTypes.shape({
+ // The current census report data.
+ report: PropTypes.object,
+ // The display data used to generate the current census.
+ display: treeMapDisplayModel,
+ // The current treeMapState this is in
+ state: catchAndIgnore(function (treeMap) {
+ switch (treeMap.state) {
+ case treeMapState.SAVING:
+ assert(!treeMap.report, "Should not have a report");
+ assert(!treeMap.error, "Should not have an error");
+ break;
+ case treeMapState.SAVED:
+ assert(treeMap.report, "Should have a report");
+ assert(!treeMap.error, "Should not have an error");
+ break;
+
+ case treeMapState.ERROR:
+ assert(treeMap.error, "Should have an error");
+ break;
+
+ default:
+ assert(false, `Unexpected treeMap state: ${treeMap.state}`);
+ }
+ })
+});
+
+let censusModel = exports.censusModel = PropTypes.shape({
+ // The current census report data.
+ report: PropTypes.object,
+ // The parent map for the report.
+ parentMap: PropTypes.object,
+ // The display data used to generate the current census.
+ display: censusDisplayModel,
+ // If present, the currently cached report's filter string used for pruning
+ // the tree items.
+ filter: PropTypes.string,
+ // The Immutable.Set<CensusTreeNode.id> of expanded node ids in the report
+ // tree.
+ expanded: catchAndIgnore(function (census) {
+ if (census.report) {
+ assert(census.expanded,
+ "If we have a report, we should also have the set of expanded nodes");
+ }
+ }),
+ // If a node is currently focused in the report tree, then this is it.
+ focused: PropTypes.object,
+ // The censusModelState that this census is currently in.
+ state: catchAndIgnore(function (census) {
+ switch (census.state) {
+ case censusState.SAVING:
+ assert(!census.report, "Should not have a report");
+ assert(!census.parentMap, "Should not have a parent map");
+ assert(census.expanded, "Should not have an expanded set");
+ assert(!census.error, "Should not have an error");
+ break;
+
+ case censusState.SAVED:
+ assert(census.report, "Should have a report");
+ assert(census.parentMap, "Should have a parent map");
+ assert(census.expanded, "Should have an expanded set");
+ assert(!census.error, "Should not have an error");
+ break;
+
+ case censusState.ERROR:
+ assert(!census.report, "Should not have a report");
+ assert(census.error, "Should have an error");
+ break;
+
+ default:
+ assert(false, `Unexpected census state: ${census.state}`);
+ }
+ })
+});
+
+/**
+ * Dominator tree model.
+ */
+let dominatorTreeModel = exports.dominatorTreeModel = PropTypes.shape({
+ // The id of this dominator tree.
+ dominatorTreeId: PropTypes.number,
+
+ // The root DominatorTreeNode of this dominator tree.
+ root: PropTypes.object,
+
+ // The Set<NodeId> of expanded nodes in this dominator tree.
+ expanded: PropTypes.object,
+
+ // If a node is currently focused in the dominator tree, then this is it.
+ focused: PropTypes.object,
+
+ // If an error was thrown while getting this dominator tree, the `Error`
+ // instance (or an error string message) is attached here.
+ error: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.object,
+ ]),
+
+ // The display used to generate descriptive labels of nodes in this dominator
+ // tree.
+ display: labelDisplayModel,
+
+ // The number of active requests to incrementally fetch subtrees. This should
+ // only be non-zero when the state is INCREMENTAL_FETCHING.
+ activeFetchRequestCount: PropTypes.number,
+
+ // The dominatorTreeState that this domintor tree is currently in.
+ state: catchAndIgnore(function (dominatorTree) {
+ switch (dominatorTree.state) {
+ case dominatorTreeState.COMPUTING:
+ assert(dominatorTree.dominatorTreeId == null,
+ "Should not have a dominator tree id yet");
+ assert(!dominatorTree.root,
+ "Should not have the root of the tree yet");
+ assert(!dominatorTree.error,
+ "Should not have an error");
+ break;
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ assert(dominatorTree.dominatorTreeId != null,
+ "Should have a dominator tree id");
+ assert(!dominatorTree.root,
+ "Should not have the root of the tree yet");
+ assert(!dominatorTree.error,
+ "Should not have an error");
+ break;
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ assert(typeof dominatorTree.activeFetchRequestCount === "number",
+ "The active fetch request count is a number when we are in the " +
+ "INCREMENTAL_FETCHING state");
+ assert(dominatorTree.activeFetchRequestCount > 0,
+ "We are keeping track of how many active requests are in flight.");
+ // Fall through...
+ case dominatorTreeState.LOADED:
+ assert(dominatorTree.dominatorTreeId != null,
+ "Should have a dominator tree id");
+ assert(dominatorTree.root,
+ "Should have the root of the tree");
+ assert(dominatorTree.expanded,
+ "Should have an expanded set");
+ assert(!dominatorTree.error,
+ "Should not have an error");
+ break;
+
+ case dominatorTreeState.ERROR:
+ assert(dominatorTree.error, "Should have an error");
+ break;
+
+ default:
+ assert(false,
+ `Unexpected dominator tree state: ${dominatorTree.state}`);
+ }
+ }),
+});
+
+/**
+ * Snapshot model.
+ */
+let stateKeys = Object.keys(states).map(state => states[state]);
+const snapshotId = PropTypes.number;
+let snapshotModel = exports.snapshot = PropTypes.shape({
+ // Unique ID for a snapshot
+ id: snapshotId.isRequired,
+ // Whether or not this snapshot is currently selected.
+ selected: PropTypes.bool.isRequired,
+ // Filesystem path to where the snapshot is stored; used to identify the
+ // snapshot for HeapAnalysesClient.
+ path: PropTypes.string,
+ // Current census data for this snapshot.
+ census: censusModel,
+ // Current dominator tree data for this snapshot.
+ dominatorTree: dominatorTreeModel,
+ // Current tree map data for this snapshot.
+ treeMap: treeMapModel,
+ // If an error was thrown while processing this snapshot, the `Error` instance
+ // is attached here.
+ error: PropTypes.object,
+ // Boolean indicating whether or not this snapshot was imported.
+ imported: PropTypes.bool.isRequired,
+ // The creation time of the snapshot; required after the snapshot has been
+ // read.
+ creationTime: PropTypes.number,
+ // The current state the snapshot is in.
+ // @see ./constants.js
+ state: catchAndIgnore(function (snapshot, propName) {
+ let current = snapshot.state;
+ let shouldHavePath = [states.IMPORTING, states.SAVED, states.READ];
+ let shouldHaveCreationTime = [states.READ];
+
+ if (!stateKeys.includes(current)) {
+ throw new Error(`Snapshot state must be one of ${stateKeys}.`);
+ }
+ if (shouldHavePath.includes(current) && !snapshot.path) {
+ throw new Error(`Snapshots in state ${current} must have a snapshot path.`);
+ }
+ if (shouldHaveCreationTime.includes(current) && !snapshot.creationTime) {
+ throw new Error(`Snapshots in state ${current} must have a creation time.`);
+ }
+ }),
+});
+
+let allocationsModel = exports.allocations = PropTypes.shape({
+ // True iff we are recording allocation stacks right now.
+ recording: PropTypes.bool.isRequired,
+ // True iff we are in the process of toggling the recording of allocation
+ // stacks on or off right now.
+ togglingInProgress: PropTypes.bool.isRequired,
+});
+
+let diffingModel = exports.diffingModel = PropTypes.shape({
+ // The id of the first snapshot to diff.
+ firstSnapshotId: snapshotId,
+
+ // The id of the second snapshot to diff.
+ secondSnapshotId: catchAndIgnore(function (diffing, propName) {
+ if (diffing.secondSnapshotId && !diffing.firstSnapshotId) {
+ throw new Error("Cannot have second snapshot without already having " +
+ "first snapshot");
+ }
+ return snapshotId(diffing, propName);
+ }),
+
+ // The current census data for the diffing.
+ census: censusModel,
+
+ // If an error was thrown while diffing, the `Error` instance is attached
+ // here.
+ error: PropTypes.object,
+
+ // The current state the diffing is in.
+ // @see ./constants.js
+ state: catchAndIgnore(function (diffing) {
+ switch (diffing.state) {
+ case diffingState.TOOK_DIFF:
+ assert(diffing.census, "If we took a diff, we should have a census");
+ // Fall through...
+ case diffingState.TAKING_DIFF:
+ assert(diffing.firstSnapshotId, "Should have first snapshot");
+ assert(diffing.secondSnapshotId, "Should have second snapshot");
+ break;
+
+ case diffingState.SELECTING:
+ break;
+
+ case diffingState.ERROR:
+ assert(diffing.error, "Should have error");
+ break;
+
+ default:
+ assert(false, `Bad diffing state: ${diffing.state}`);
+ }
+ }),
+});
+
+let previousViewModel = exports.previousView = PropTypes.shape({
+ state: catchAndIgnore(function (previous) {
+ switch (previous.state) {
+ case viewState.DIFFING:
+ assert(previous.diffing, "Should have previous diffing state.");
+ assert(!previous.selected, "Should not have a previously selected snapshot.");
+ break;
+
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.TREE_MAP:
+ assert(previous.selected, "Should have a previously selected snapshot.");
+ break;
+
+ case viewState.INDIVIDUALS:
+ default:
+ assert(false, `Unexpected previous view state: ${previous.state}.`);
+ }
+ }),
+
+ // The previous diffing state, if any.
+ diffing: diffingModel,
+
+ // The previously selected snapshot, if any.
+ selected: snapshotId,
+});
+
+let viewModel = exports.view = PropTypes.shape({
+ // The current view state.
+ state: catchAndIgnore(function (view) {
+ switch (view.state) {
+ case viewState.DIFFING:
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.INDIVIDUALS:
+ case viewState.TREE_MAP:
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${view.state}`);
+ }
+ }),
+
+ // The previous view state.
+ previous: previousViewModel,
+});
+
+const individualsModel = exports.individuals = PropTypes.shape({
+ error: PropTypes.object,
+
+ nodes: PropTypes.arrayOf(PropTypes.object),
+
+ dominatorTree: dominatorTreeModel,
+
+ id: snapshotId,
+
+ censusBreakdown: PropTypes.object,
+
+ indices: PropTypes.object,
+
+ labelDisplay: labelDisplayModel,
+
+ focused: PropTypes.object,
+
+ state: catchAndIgnore(function (individuals) {
+ switch (individuals.state) {
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ case individualsState.FETCHING:
+ assert(!individuals.nodes, "Should not have individual nodes");
+ assert(!individuals.dominatorTree, "Should not have dominator tree");
+ assert(!individuals.id, "Should not have an id");
+ assert(!individuals.censusBreakdown, "Should not have a censusBreakdown");
+ assert(!individuals.indices, "Should not have indices");
+ assert(!individuals.labelDisplay, "Should not have a labelDisplay");
+ break;
+
+ case individualsState.FETCHED:
+ assert(individuals.nodes, "Should have individual nodes");
+ assert(individuals.dominatorTree, "Should have dominator tree");
+ assert(individuals.id, "Should have an id");
+ assert(individuals.censusBreakdown, "Should have a censusBreakdown");
+ assert(individuals.indices, "Should have indices");
+ assert(individuals.labelDisplay, "Should have a labelDisplay");
+ break;
+
+ case individualsState.ERROR:
+ assert(individuals.error, "Should have an error object");
+ break;
+
+ default:
+ assert(false, `Unexpected individuals state: ${individuals.state}`);
+ break;
+ }
+ }),
+});
+
+let appModel = exports.app = {
+ // {MemoryFront} Used to communicate with platform
+ front: PropTypes.instanceOf(MemoryFront),
+
+ // Allocations recording related data.
+ allocations: allocationsModel.isRequired,
+
+ // {HeapAnalysesClient} Used to interface with snapshots
+ heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
+
+ // The display data describing how we want the census data to be.
+ censusDisplay: censusDisplayModel.isRequired,
+
+ // The display data describing how we want the dominator tree labels to be
+ // computed.
+ labelDisplay: labelDisplayModel.isRequired,
+
+ // The display data describing how we want the dominator tree labels to be
+ // computed.
+ treeMapDisplay: treeMapDisplayModel.isRequired,
+
+ // List of reference to all snapshots taken
+ snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
+
+ // If present, a filter string for pruning the tree items.
+ filter: PropTypes.string,
+
+ // If present, the current diffing state.
+ diffing: diffingModel,
+
+ // If present, the current individuals state.
+ individuals: individualsModel,
+
+ // The current type of view.
+ view: function (app) {
+ viewModel.isRequired(app, "view");
+
+ catchAndIgnore(function (app) {
+ switch (app.view.state) {
+ case viewState.DIFFING:
+ assert(app.diffing, "Should be diffing");
+ break;
+
+ case viewState.INDIVIDUALS:
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.TREE_MAP:
+ assert(!app.diffing, "Should not be diffing");
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${view.state}`);
+ }
+ })(app);
+
+ catchAndIgnore(function (app) {
+ switch (app.view.state) {
+ case viewState.INDIVIDUALS:
+ assert(app.individuals, "Should have individuals state");
+ break;
+
+ case viewState.DIFFING:
+ case viewState.CENSUS:
+ case viewState.DOMINATOR_TREE:
+ case viewState.TREE_MAP:
+ assert(!app.individuals, "Should not have individuals state");
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${view.state}`);
+ }
+ })(app);
+ },
+};