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/. */
5 /* global treeMapState, censusState */
6 /* eslint no-shadow: ["error", { "allow": ["app"] }] */
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.js");
15 snapshotState: states,
20 } = require("resource://devtools/client/memory/constants.js");
23 * ONLY USE THIS FOR MODEL VALIDATORS IN CONJUCTION WITH assert()!
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.
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
37 * const MyModel = PropTypes.shape({
38 * someProperty: catchAndIgnore(function (model) {
39 * assert(someInvariant(model.someProperty), "Should blah blah");
43 function catchAndIgnore(fn) {
44 return function (...args) {
48 // continue regardless of error
56 * The data describing the census report's shape, and its associated metadata.
58 * @see `js/src/doc/Debugger/Debugger.Memory.md`
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,
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.
74 * @see `js/src/doc/Debugger/Debugger.Memory.md`
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,
85 * The data describing the tree map's shape, and its associated metadata.
87 * @see `js/src/doc/Debugger/Debugger.Memory.md`
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,
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");
114 case treeMapState.SAVED:
115 assert(treeMap.report, "Should have a report");
116 assert(!treeMap.error, "Should not have an error");
119 case treeMapState.ERROR:
120 assert(treeMap.error, "Should have an error");
124 assert(false, `Unexpected treeMap state: ${treeMap.state}`);
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
138 filter: PropTypes.string,
139 // The Immutable.Set<CensusTreeNode.id> of expanded node ids in the report
141 expanded: catchAndIgnore(function (census) {
145 "If we have a report, we should also have the set of expanded nodes"
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");
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");
168 case censusState.ERROR:
169 assert(!census.report, "Should not have a report");
170 assert(census.error, "Should have an error");
174 assert(false, `Unexpected census state: ${census.state}`);
180 * Dominator tree model.
182 const dominatorTreeModel = (exports.dominatorTreeModel = PropTypes.shape({
183 // The id of this dominator tree.
184 dominatorTreeId: PropTypes.number,
186 // The root DominatorTreeNode of this dominator tree.
187 root: PropTypes.object,
189 // The Set<NodeId> of expanded nodes in this dominator tree.
190 expanded: PropTypes.object,
192 // If a node is currently focused in the dominator tree, then this is it.
193 focused: PropTypes.object,
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]),
199 // The display used to generate descriptive labels of nodes in this dominator
201 display: labelDisplayModel,
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,
207 // The dominatorTreeState that this domintor tree is currently in.
208 state: catchAndIgnore(function (dominatorTree) {
209 switch (dominatorTree.state) {
210 case dominatorTreeState.COMPUTING:
212 dominatorTree.dominatorTreeId == null,
213 "Should not have a dominator tree id yet"
215 assert(!dominatorTree.root, "Should not have the root of the tree yet");
216 assert(!dominatorTree.error, "Should not have an error");
219 case dominatorTreeState.COMPUTED:
220 case dominatorTreeState.FETCHING:
222 dominatorTree.dominatorTreeId != null,
223 "Should have a dominator tree id"
225 assert(!dominatorTree.root, "Should not have the root of the tree yet");
226 assert(!dominatorTree.error, "Should not have an error");
229 case dominatorTreeState.INCREMENTAL_FETCHING:
231 typeof dominatorTree.activeFetchRequestCount === "number",
232 "The active fetch request count is a number when we are in the " +
233 "INCREMENTAL_FETCHING state"
236 dominatorTree.activeFetchRequestCount > 0,
237 "We are keeping track of how many active requests are in flight."
240 case dominatorTreeState.LOADED:
242 dominatorTree.dominatorTreeId != null,
243 "Should have a dominator tree id"
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");
250 case dominatorTreeState.ERROR:
251 assert(dominatorTree.error, "Should have an error");
257 `Unexpected dominator tree state: ${dominatorTree.state}`
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.
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
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
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];
297 if (!stateKeys.includes(current)) {
298 throw new Error(`Snapshot state must be one of ${stateKeys}.`);
300 if (shouldHavePath.includes(current) && !snapshot.path) {
302 `Snapshots in state ${current} must have a snapshot path.`
305 if (shouldHaveCreationTime.includes(current) && !snapshot.creationTime) {
307 `Snapshots in state ${current} must have a creation time.`
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,
321 const diffingModel = (exports.diffingModel = PropTypes.shape({
322 // The id of the first snapshot to diff.
323 firstSnapshotId: snapshotId,
325 // The id of the second snapshot to diff.
326 secondSnapshotId: catchAndIgnore(function (diffing, propName) {
327 if (diffing.secondSnapshotId && !diffing.firstSnapshotId) {
329 "Cannot have second snapshot without already having " + "first snapshot"
332 return snapshotId(diffing, propName);
335 // The current census data for the diffing.
338 // If an error was thrown while diffing, the `Error` instance is attached
340 error: PropTypes.object,
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");
349 case diffingState.TAKING_DIFF:
350 assert(diffing.firstSnapshotId, "Should have first snapshot");
351 assert(diffing.secondSnapshotId, "Should have second snapshot");
354 case diffingState.SELECTING:
357 case diffingState.ERROR:
358 assert(diffing.error, "Should have error");
362 assert(false, `Bad diffing state: ${diffing.state}`);
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.");
374 "Should not have a previously selected snapshot."
378 case viewState.CENSUS:
379 case viewState.DOMINATOR_TREE:
380 case viewState.TREE_MAP:
383 "Should have a previously selected snapshot."
387 case viewState.INDIVIDUALS:
389 assert(false, `Unexpected previous view state: ${previous.state}.`);
393 // The previous diffing state, if any.
394 diffing: diffingModel,
396 // The previously selected snapshot, if any.
397 selected: snapshotId,
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:
412 assert(false, `Unexpected type of view: ${view.state}`);
416 // The previous view state.
417 previous: previousViewModel,
420 const individualsModel = (exports.individuals = PropTypes.shape({
421 error: PropTypes.object,
423 nodes: PropTypes.arrayOf(PropTypes.object),
425 dominatorTree: dominatorTreeModel,
429 censusBreakdown: PropTypes.object,
431 indices: PropTypes.object,
433 labelDisplay: labelDisplayModel,
435 focused: PropTypes.object,
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");
445 !individuals.censusBreakdown,
446 "Should not have a censusBreakdown"
448 assert(!individuals.indices, "Should not have indices");
449 assert(!individuals.labelDisplay, "Should not have a labelDisplay");
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");
461 case individualsState.ERROR:
462 assert(individuals.error, "Should have an error object");
466 assert(false, `Unexpected individuals state: ${individuals.state}`);
473 // {Commands} Used to communicate with the backend
474 commands: PropTypes.object,
476 // {MemoryFront} Used to communicate with platform
477 front: PropTypes.instanceOf(MemoryFront),
479 // Allocations recording related data.
480 allocations: allocationsModel.isRequired,
482 // {HeapAnalysesClient} Used to interface with snapshots
483 heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
485 // The display data describing how we want the census data to be.
486 censusDisplay: censusDisplayModel.isRequired,
488 // The display data describing how we want the dominator tree labels to be
490 labelDisplay: labelDisplayModel.isRequired,
492 // The display data describing how we want the dominator tree labels to be
494 treeMapDisplay: treeMapDisplayModel.isRequired,
496 // List of reference to all snapshots taken
497 snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
499 // If present, a filter string for pruning the tree items.
500 filter: PropTypes.string,
502 // If present, the current diffing state.
503 diffing: diffingModel,
505 // If present, the current individuals state.
506 individuals: individualsModel,
508 // The current type of view.
510 catchAndIgnore(function (app) {
511 switch (app.view.state) {
512 case viewState.DIFFING:
513 assert(app.diffing, "Should be diffing");
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");
524 assert(false, `Unexpected type of view: ${app.view.state}`);
528 catchAndIgnore(function (app) {
529 switch (app.view.state) {
530 case viewState.INDIVIDUALS:
531 assert(app.individuals, "Should have individuals state");
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");
542 assert(false, `Unexpected type of view: ${app.view.state}`);