Heap.js (14803B)
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 "use strict"; 6 7 const { 8 Component, 9 createFactory, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 13 const { 14 assert, 15 safeErrorString, 16 } = require("resource://devtools/shared/DevToolsUtils.js"); 17 const Census = createFactory( 18 require("resource://devtools/client/memory/components/Census.js") 19 ); 20 const CensusHeader = createFactory( 21 require("resource://devtools/client/memory/components/CensusHeader.js") 22 ); 23 const DominatorTree = createFactory( 24 require("resource://devtools/client/memory/components/DominatorTree.js") 25 ); 26 const DominatorTreeHeader = createFactory( 27 require("resource://devtools/client/memory/components/DominatorTreeHeader.js") 28 ); 29 const TreeMap = createFactory( 30 require("resource://devtools/client/memory/components/TreeMap.js") 31 ); 32 const HSplitBox = createFactory( 33 require("resource://devtools/client/shared/components/HSplitBox.js") 34 ); 35 const Individuals = createFactory( 36 require("resource://devtools/client/memory/components/Individuals.js") 37 ); 38 const IndividualsHeader = createFactory( 39 require("resource://devtools/client/memory/components/IndividualsHeader.js") 40 ); 41 const ShortestPaths = createFactory( 42 require("resource://devtools/client/memory/components/ShortestPaths.js") 43 ); 44 const { 45 getStatusTextFull, 46 L10N, 47 } = require("resource://devtools/client/memory/utils.js"); 48 const { 49 snapshotState: states, 50 diffingState, 51 viewState, 52 censusState, 53 treeMapState, 54 dominatorTreeState, 55 individualsState, 56 } = require("resource://devtools/client/memory/constants.js"); 57 const models = require("resource://devtools/client/memory/models.js"); 58 const { snapshot: snapshotModel, diffingModel } = models; 59 60 /** 61 * Get the app state's current state atom. 62 * 63 * @see the relevant state string constants in `../constants.js`. 64 * 65 * @param {models.view} view 66 * @param {snapshotModel} snapshot 67 * @param {diffingModel} diffing 68 * @param {individualsModel} individuals 69 * 70 * @return {snapshotState|diffingState|dominatorTreeState} 71 */ 72 function getState(view, snapshot, diffing, individuals) { 73 switch (view.state) { 74 case viewState.CENSUS: 75 return snapshot.census ? snapshot.census.state : snapshot.state; 76 77 case viewState.DIFFING: 78 return diffing.state; 79 80 case viewState.TREE_MAP: 81 return snapshot.treeMap ? snapshot.treeMap.state : snapshot.state; 82 83 case viewState.DOMINATOR_TREE: 84 return snapshot.dominatorTree 85 ? snapshot.dominatorTree.state 86 : snapshot.state; 87 88 case viewState.INDIVIDUALS: 89 return individuals.state; 90 } 91 92 assert(false, `Unexpected view state: ${view.state}`); 93 return null; 94 } 95 96 /** 97 * Return true if we should display a status message when we are in the given 98 * state. Return false otherwise. 99 * 100 * @param {snapshotState|diffingState|dominatorTreeState} state 101 * @param {models.view} view 102 * @param {snapshotModel} snapshot 103 * 104 * @returns {boolean} 105 */ 106 function shouldDisplayStatus(state, view, snapshot) { 107 switch (state) { 108 case states.IMPORTING: 109 case states.SAVING: 110 case states.SAVED: 111 case states.READING: 112 case censusState.SAVING: 113 case treeMapState.SAVING: 114 case diffingState.SELECTING: 115 case diffingState.TAKING_DIFF: 116 case dominatorTreeState.COMPUTING: 117 case dominatorTreeState.COMPUTED: 118 case dominatorTreeState.FETCHING: 119 case individualsState.COMPUTING_DOMINATOR_TREE: 120 case individualsState.FETCHING: 121 return true; 122 } 123 return view.state === viewState.DOMINATOR_TREE && !snapshot.dominatorTree; 124 } 125 126 /** 127 * Get the status text to display for the given state. 128 * 129 * @param {snapshotState|diffingState|dominatorTreeState} state 130 * @param {diffingModel} diffing 131 * 132 * @returns {string} 133 */ 134 function getStateStatusText(state, diffing) { 135 if (state === diffingState.SELECTING) { 136 return L10N.getStr( 137 diffing.firstSnapshotId === null 138 ? "diffing.prompt.selectBaseline" 139 : "diffing.prompt.selectComparison" 140 ); 141 } 142 143 return getStatusTextFull(state); 144 } 145 146 /** 147 * Given that we should display a status message, return true if we should also 148 * display a throbber along with the status message. Return false otherwise. 149 * 150 * @param {diffingModel} diffing 151 * 152 * @returns {boolean} 153 */ 154 function shouldDisplayThrobber(diffing) { 155 return !diffing || diffing.state !== diffingState.SELECTING; 156 } 157 158 /** 159 * Get the current state's error, or return null if there is none. 160 * 161 * @param {snapshotModel} snapshot 162 * @param {diffingModel} diffing 163 * @param {individualsModel} individuals 164 * 165 * @returns {Error|null} 166 */ 167 function getError(snapshot, diffing, individuals) { 168 if (diffing) { 169 if (diffing.state === diffingState.ERROR) { 170 return diffing.error; 171 } 172 if (diffing.census === censusState.ERROR) { 173 return diffing.census.error; 174 } 175 } 176 177 if (snapshot) { 178 if (snapshot.state === states.ERROR) { 179 return snapshot.error; 180 } 181 182 if (snapshot.census === censusState.ERROR) { 183 return snapshot.census.error; 184 } 185 186 if (snapshot.treeMap === treeMapState.ERROR) { 187 return snapshot.treeMap.error; 188 } 189 190 if ( 191 snapshot.dominatorTree && 192 snapshot.dominatorTree.state === dominatorTreeState.ERROR 193 ) { 194 return snapshot.dominatorTree.error; 195 } 196 } 197 198 if (individuals && individuals.state === individualsState.ERROR) { 199 return individuals.error; 200 } 201 202 return null; 203 } 204 205 /** 206 * Main view for the memory tool. 207 * 208 * The Heap component contains several panels for different states; an initial 209 * state of only a button to take a snapshot, loading states, the census view 210 * tree, the dominator tree, etc. 211 */ 212 class Heap extends Component { 213 static get propTypes() { 214 return { 215 onSnapshotClick: PropTypes.func.isRequired, 216 onLoadMoreSiblings: PropTypes.func.isRequired, 217 onCensusExpand: PropTypes.func.isRequired, 218 onCensusCollapse: PropTypes.func.isRequired, 219 onDominatorTreeExpand: PropTypes.func.isRequired, 220 onDominatorTreeCollapse: PropTypes.func.isRequired, 221 onCensusFocus: PropTypes.func.isRequired, 222 onDominatorTreeFocus: PropTypes.func.isRequired, 223 onShortestPathsResize: PropTypes.func.isRequired, 224 snapshot: snapshotModel, 225 onViewSourceInDebugger: PropTypes.func.isRequired, 226 onPopView: PropTypes.func.isRequired, 227 individuals: models.individuals, 228 onViewIndividuals: PropTypes.func.isRequired, 229 onFocusIndividual: PropTypes.func.isRequired, 230 diffing: diffingModel, 231 view: models.view.isRequired, 232 sizes: PropTypes.object.isRequired, 233 }; 234 } 235 236 constructor(props) { 237 super(props); 238 this._renderHeapView = this._renderHeapView.bind(this); 239 this._renderInitial = this._renderInitial.bind(this); 240 this._renderStatus = this._renderStatus.bind(this); 241 this._renderError = this._renderError.bind(this); 242 this._renderCensus = this._renderCensus.bind(this); 243 this._renderTreeMap = this._renderTreeMap.bind(this); 244 this._renderIndividuals = this._renderIndividuals.bind(this); 245 this._renderDominatorTree = this._renderDominatorTree.bind(this); 246 } 247 248 /** 249 * Render the heap view's container panel with the given contents inside of 250 * it. 251 * 252 * @param {snapshotState|diffingState|dominatorTreeState} state 253 * @param {...Any} contents 254 */ 255 _renderHeapView(state, ...contents) { 256 return dom.div( 257 { 258 id: "heap-view", 259 "data-state": state, 260 }, 261 dom.div( 262 { 263 className: "heap-view-panel", 264 "data-state": state, 265 }, 266 ...contents 267 ) 268 ); 269 } 270 271 _renderInitial(onSnapshotClick) { 272 return this._renderHeapView( 273 "initial", 274 dom.button( 275 { 276 className: "devtools-button devtools-button-standalone take-snapshot", 277 onClick: onSnapshotClick, 278 }, 279 L10N.getStr("take-snapshot") 280 ) 281 ); 282 } 283 284 _renderStatus(state, statusText, diffing) { 285 let throbber = ""; 286 if (shouldDisplayThrobber(diffing)) { 287 throbber = "devtools-throbber"; 288 } 289 290 return this._renderHeapView( 291 state, 292 dom.span( 293 { 294 className: `snapshot-status ${throbber}`, 295 }, 296 statusText 297 ) 298 ); 299 } 300 301 _renderError(state, statusText, error) { 302 return this._renderHeapView( 303 state, 304 dom.span({ className: "snapshot-status error" }, statusText), 305 dom.pre({}, safeErrorString(error)) 306 ); 307 } 308 309 _renderCensus( 310 state, 311 census, 312 diffing, 313 onViewSourceInDebugger, 314 onViewIndividuals 315 ) { 316 assert( 317 census.report, 318 "Should not render census that does not have a report" 319 ); 320 321 if (!census.report.children) { 322 const censusFilterMsg = census.filter 323 ? L10N.getStr("heapview.none-match") 324 : L10N.getStr("heapview.empty"); 325 const msg = diffing 326 ? L10N.getStr("heapview.no-difference") 327 : censusFilterMsg; 328 return this._renderHeapView(state, dom.div({ className: "empty" }, msg)); 329 } 330 331 const contents = []; 332 333 if ( 334 census.display.breakdown.by === "allocationStack" && 335 census.report.children && 336 census.report.children.length === 1 && 337 census.report.children[0].name === "noStack" 338 ) { 339 contents.push( 340 dom.div( 341 { className: "error no-allocation-stacks" }, 342 L10N.getStr("heapview.noAllocationStacks") 343 ) 344 ); 345 } 346 347 contents.push(CensusHeader({ diffing })); 348 contents.push( 349 Census({ 350 onViewSourceInDebugger, 351 onViewIndividuals, 352 diffing, 353 census, 354 onExpand: node => this.props.onCensusExpand(census, node), 355 onCollapse: node => this.props.onCensusCollapse(census, node), 356 onFocus: node => this.props.onCensusFocus(census, node), 357 }) 358 ); 359 360 return this._renderHeapView(state, ...contents); 361 } 362 363 _renderTreeMap(state, treeMap) { 364 return this._renderHeapView(state, TreeMap({ treeMap })); 365 } 366 367 _renderIndividuals( 368 state, 369 individuals, 370 dominatorTree, 371 onViewSourceInDebugger 372 ) { 373 assert( 374 individuals.state === individualsState.FETCHED, 375 "Should have fetched individuals" 376 ); 377 assert(dominatorTree?.root, "Should have a dominator tree and its root"); 378 379 const tree = dom.div( 380 { 381 className: "vbox", 382 style: { 383 overflowY: "auto", 384 }, 385 }, 386 IndividualsHeader(), 387 Individuals({ 388 individuals, 389 dominatorTree, 390 onViewSourceInDebugger, 391 onFocus: this.props.onFocusIndividual, 392 }) 393 ); 394 395 const shortestPaths = ShortestPaths({ 396 graph: individuals.focused ? individuals.focused.shortestPaths : null, 397 }); 398 399 return this._renderHeapView( 400 state, 401 dom.div( 402 { className: "hbox devtools-toolbar" }, 403 dom.label( 404 { id: "pop-view-button-label" }, 405 dom.button( 406 { 407 id: "pop-view-button", 408 className: "devtools-button", 409 onClick: this.props.onPopView, 410 }, 411 L10N.getStr("toolbar.pop-view") 412 ), 413 L10N.getStr("toolbar.pop-view.label") 414 ), 415 dom.span( 416 { className: "toolbar-text" }, 417 L10N.getStr("toolbar.viewing-individuals") 418 ) 419 ), 420 HSplitBox({ 421 start: tree, 422 end: shortestPaths, 423 startWidth: this.props.sizes.shortestPathsSize, 424 onResize: this.props.onShortestPathsResize, 425 }) 426 ); 427 } 428 429 _renderDominatorTree( 430 state, 431 onViewSourceInDebugger, 432 dominatorTree, 433 onLoadMoreSiblings 434 ) { 435 const tree = dom.div( 436 { 437 className: "vbox", 438 style: { 439 overflowY: "auto", 440 }, 441 }, 442 DominatorTreeHeader(), 443 DominatorTree({ 444 onViewSourceInDebugger, 445 dominatorTree, 446 onLoadMoreSiblings, 447 onExpand: this.props.onDominatorTreeExpand, 448 onCollapse: this.props.onDominatorTreeCollapse, 449 onFocus: this.props.onDominatorTreeFocus, 450 }) 451 ); 452 453 const shortestPaths = ShortestPaths({ 454 graph: dominatorTree.focused ? dominatorTree.focused.shortestPaths : null, 455 }); 456 457 return this._renderHeapView( 458 state, 459 HSplitBox({ 460 start: tree, 461 end: shortestPaths, 462 startWidth: this.props.sizes.shortestPathsSize, 463 onResize: this.props.onShortestPathsResize, 464 }) 465 ); 466 } 467 468 render() { 469 const { 470 snapshot, 471 diffing, 472 onSnapshotClick, 473 onLoadMoreSiblings, 474 onViewSourceInDebugger, 475 onViewIndividuals, 476 individuals, 477 view, 478 } = this.props; 479 480 if (!diffing && !snapshot && !individuals) { 481 return this._renderInitial(onSnapshotClick); 482 } 483 484 const state = getState(view, snapshot, diffing, individuals); 485 const statusText = getStateStatusText(state, diffing); 486 487 if (shouldDisplayStatus(state, view, snapshot)) { 488 return this._renderStatus(state, statusText, diffing); 489 } 490 491 const error = getError(snapshot, diffing, individuals); 492 if (error) { 493 return this._renderError(state, statusText, error); 494 } 495 496 if (view.state === viewState.CENSUS || view.state === viewState.DIFFING) { 497 const census = 498 view.state === viewState.CENSUS ? snapshot.census : diffing.census; 499 if (!census) { 500 return this._renderStatus(state, statusText, diffing); 501 } 502 return this._renderCensus( 503 state, 504 census, 505 diffing, 506 onViewSourceInDebugger, 507 onViewIndividuals 508 ); 509 } 510 511 if (view.state === viewState.TREE_MAP) { 512 return this._renderTreeMap(state, snapshot.treeMap); 513 } 514 515 if (view.state === viewState.INDIVIDUALS) { 516 assert( 517 individuals.state === individualsState.FETCHED, 518 "Should have fetched the individuals -- " + 519 "other states are rendered as statuses" 520 ); 521 return this._renderIndividuals( 522 state, 523 individuals, 524 individuals.dominatorTree, 525 onViewSourceInDebugger 526 ); 527 } 528 529 assert( 530 view.state === viewState.DOMINATOR_TREE, 531 "If we aren't in progress, looking at a census, or diffing, then we " + 532 "must be looking at a dominator tree" 533 ); 534 assert(!diffing, "Should not have diffing"); 535 assert(snapshot.dominatorTree, "Should have a dominator tree"); 536 537 return this._renderDominatorTree( 538 state, 539 onViewSourceInDebugger, 540 snapshot.dominatorTree, 541 onLoadMoreSiblings 542 ); 543 } 544 } 545 546 module.exports = Heap;