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 };