models.js 17 KB

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