Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / devtools / client / memory / models.js
blob9429fdae4a3e3abcdb45bd5a3fd3a34ebd538e80
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"] }] */
8 "use strict";
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");
14 const {
15   snapshotState: states,
16   diffingState,
17   dominatorTreeState,
18   viewState,
19   individualsState,
20 } = require("resource://devtools/client/memory/constants.js");
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     }
51     return null;
52   };
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 }));
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 }));
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 }));
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;
114       case treeMapState.SAVED:
115         assert(treeMap.report, "Should have a report");
116         assert(!treeMap.error, "Should not have an error");
117         break;
119       case treeMapState.ERROR:
120         assert(treeMap.error, "Should have an error");
121         break;
123       default:
124         assert(false, `Unexpected treeMap state: ${treeMap.state}`);
125     }
126   }),
127 }));
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 Immutable.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;
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;
168       case censusState.ERROR:
169         assert(!census.report, "Should not have a report");
170         assert(census.error, "Should have an error");
171         break;
173       default:
174         assert(false, `Unexpected census state: ${census.state}`);
175     }
176   }),
177 }));
180  * Dominator tree model.
181  */
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
200   // tree.
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:
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;
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;
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;
250       case dominatorTreeState.ERROR:
251         assert(dominatorTree.error, "Should have an error");
252         break;
254       default:
255         assert(
256           false,
257           `Unexpected dominator tree state: ${dominatorTree.state}`
258         );
259     }
260   }),
261 }));
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];
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 }));
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 }));
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) {
328       throw new Error(
329         "Cannot have second snapshot without already having " + "first snapshot"
330       );
331     }
332     return snapshotId(diffing, propName);
333   }),
335   // The current census data for the diffing.
336   census: censusModel,
338   // If an error was thrown while diffing, the `Error` instance is attached
339   // here.
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");
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;
354       case diffingState.SELECTING:
355         break;
357       case diffingState.ERROR:
358         assert(diffing.error, "Should have error");
359         break;
361       default:
362         assert(false, `Bad diffing state: ${diffing.state}`);
363     }
364   }),
365 }));
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;
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;
387       case viewState.INDIVIDUALS:
388       default:
389         assert(false, `Unexpected previous view state: ${previous.state}.`);
390     }
391   }),
393   // The previous diffing state, if any.
394   diffing: diffingModel,
396   // The previously selected snapshot, if any.
397   selected: snapshotId,
398 }));
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;
411       default:
412         assert(false, `Unexpected type of view: ${view.state}`);
413     }
414   }),
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,
427   id: snapshotId,
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");
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;
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;
461       case individualsState.ERROR:
462         assert(individuals.error, "Should have an error object");
463         break;
465       default:
466         assert(false, `Unexpected individuals state: ${individuals.state}`);
467         break;
468     }
469   }),
470 }));
472 exports.app = {
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
489   // computed.
490   labelDisplay: labelDisplayModel.isRequired,
492   // The display data describing how we want the dominator tree labels to be
493   // computed.
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.
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;
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;
523         default:
524           assert(false, `Unexpected type of view: ${app.view.state}`);
525       }
526     })(app);
528     catchAndIgnore(function (app) {
529       switch (app.view.state) {
530         case viewState.INDIVIDUALS:
531           assert(app.individuals, "Should have individuals state");
532           break;
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;
541         default:
542           assert(false, `Unexpected type of view: ${app.view.state}`);
543       }
544     })(app);
545   },