snapshot.js (26403B)
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 "use strict"; 5 6 const { 7 assert, 8 reportException, 9 isSet, 10 } = require("resource://devtools/shared/DevToolsUtils.js"); 11 const { 12 censusIsUpToDate, 13 getSnapshot, 14 createSnapshot, 15 dominatorTreeIsComputed, 16 } = require("resource://devtools/client/memory/utils.js"); 17 const { 18 actions, 19 snapshotState: states, 20 viewState, 21 censusState, 22 treeMapState, 23 dominatorTreeState, 24 individualsState, 25 } = require("resource://devtools/client/memory/constants.js"); 26 const view = require("resource://devtools/client/memory/actions/view.js"); 27 const refresh = require("resource://devtools/client/memory/actions/refresh.js"); 28 const diffing = require("resource://devtools/client/memory/actions/diffing.js"); 29 const TaskCache = require("resource://devtools/client/memory/actions/task-cache.js"); 30 31 /** 32 * A series of actions are fired from this task to save, read and generate the 33 * initial census from a snapshot. 34 * 35 * @param {MemoryFront} 36 * @param {HeapAnalysesClient} 37 * @param {object} 38 */ 39 exports.takeSnapshotAndCensus = function (front, heapWorker) { 40 return async function ({ dispatch, getState }) { 41 const id = await dispatch(takeSnapshot(front)); 42 if (id === null) { 43 return; 44 } 45 46 await dispatch(readSnapshot(heapWorker, id)); 47 if (getSnapshot(getState(), id).state !== states.READ) { 48 return; 49 } 50 51 await dispatch(computeSnapshotData(heapWorker, id)); 52 }; 53 }; 54 55 /** 56 * Create the census for the snapshot with the provided snapshot id. If the 57 * current view is the DOMINATOR_TREE view, create the dominator tree for this 58 * snapshot as well. 59 * 60 * @param {HeapAnalysesClient} heapWorker 61 * @param {snapshotId} id 62 */ 63 const computeSnapshotData = (exports.computeSnapshotData = function ( 64 heapWorker, 65 id 66 ) { 67 return async function ({ dispatch, getState }) { 68 if (getSnapshot(getState(), id).state !== states.READ) { 69 return; 70 } 71 72 // Decide which type of census to take. 73 const censusTaker = getCurrentCensusTaker(getState().view.state); 74 await dispatch(censusTaker(heapWorker, id)); 75 76 if ( 77 getState().view.state === viewState.DOMINATOR_TREE && 78 !getSnapshot(getState(), id).dominatorTree 79 ) { 80 await dispatch(computeAndFetchDominatorTree(heapWorker, id)); 81 } 82 }; 83 }); 84 85 /** 86 * Selects a snapshot and if the snapshot's census is using a different 87 * display, take a new census. 88 * 89 * @param {HeapAnalysesClient} heapWorker 90 * @param {snapshotId} id 91 */ 92 exports.selectSnapshotAndRefresh = function (heapWorker, id) { 93 return async function ({ dispatch, getState }) { 94 if (getState().diffing || getState().individuals) { 95 dispatch(view.changeView(viewState.CENSUS)); 96 } 97 98 dispatch(selectSnapshot(id)); 99 await dispatch(refresh.refresh(heapWorker)); 100 }; 101 }; 102 103 /** 104 * Take a snapshot and return its id on success, or null on failure. 105 * 106 * @param {MemoryFront} front 107 * @returns {number | null} 108 */ 109 const takeSnapshot = (exports.takeSnapshot = function (front) { 110 return async function ({ dispatch, getState }) { 111 if (getState().diffing || getState().individuals) { 112 dispatch(view.changeView(viewState.CENSUS)); 113 } 114 115 const snapshot = createSnapshot(getState()); 116 const id = snapshot.id; 117 dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot }); 118 dispatch(selectSnapshot(id)); 119 120 let path; 121 try { 122 path = await front.saveHeapSnapshot(); 123 } catch (error) { 124 reportException("takeSnapshot", error); 125 dispatch({ type: actions.SNAPSHOT_ERROR, id, error }); 126 return null; 127 } 128 129 dispatch({ type: actions.TAKE_SNAPSHOT_END, id, path }); 130 return snapshot.id; 131 }; 132 }); 133 134 /** 135 * Reads a snapshot into memory; necessary to do before taking 136 * a census on the snapshot. May only be called once per snapshot. 137 * 138 * @param {HeapAnalysesClient} heapWorker 139 * @param {snapshotId} id 140 */ 141 const readSnapshot = (exports.readSnapshot = TaskCache.declareCacheableTask({ 142 getCacheKey(_, id) { 143 return id; 144 }, 145 146 async task(heapWorker, id, removeFromCache, dispatch, getState) { 147 const snapshot = getSnapshot(getState(), id); 148 assert( 149 [states.SAVED, states.IMPORTING].includes(snapshot.state), 150 `Should only read a snapshot once. Found snapshot in state ${snapshot.state}` 151 ); 152 153 let creationTime; 154 155 dispatch({ type: actions.READ_SNAPSHOT_START, id }); 156 try { 157 await heapWorker.readHeapSnapshot(snapshot.path); 158 creationTime = await heapWorker.getCreationTime(snapshot.path); 159 } catch (error) { 160 removeFromCache(); 161 reportException("readSnapshot", error); 162 dispatch({ type: actions.SNAPSHOT_ERROR, id, error }); 163 return; 164 } 165 166 removeFromCache(); 167 dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime }); 168 }, 169 })); 170 171 let takeCensusTaskCounter = 0; 172 173 /** 174 * Census and tree maps both require snapshots. This function shares the logic 175 * of creating snapshots, but is configurable with specific actions for the 176 * individual census types. 177 * 178 * @param {getDisplay} Get the display object from the state. 179 * @param {getCensus} Get the census from the snapshot. 180 * @param {beginAction} Action to send at the beginning of a heap snapshot. 181 * @param {endAction} Action to send at the end of a heap snapshot. 182 * @param {errorAction} Action to send if a snapshot has an error. 183 */ 184 function makeTakeCensusTask({ 185 getDisplay, 186 getFilter, 187 getCensus, 188 beginAction, 189 endAction, 190 errorAction, 191 canTakeCensus, 192 }) { 193 /** 194 * @param {HeapAnalysesClient} heapWorker 195 * @param {snapshotId} id 196 * 197 * @see {Snapshot} model defined in devtools/client/memory/models.js 198 * @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js` 199 * @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details 200 */ 201 const thisTakeCensusTaskId = ++takeCensusTaskCounter; 202 return TaskCache.declareCacheableTask({ 203 getCacheKey(_, id) { 204 return `take-census-task-${thisTakeCensusTaskId}-${id}`; 205 }, 206 207 async task(heapWorker, id, removeFromCache, dispatch, getState) { 208 const snapshot = getSnapshot(getState(), id); 209 if (!snapshot) { 210 removeFromCache(); 211 return; 212 } 213 214 // Assert that snapshot is in a valid state 215 assert( 216 canTakeCensus(snapshot), 217 "Attempting to take a census when the snapshot is not in a ready state. " + 218 `snapshot.state = ${snapshot.state}, ` + 219 `census.state = ${(getCensus(snapshot) || { state: null }).state}` 220 ); 221 222 let report, parentMap; 223 let display = getDisplay(getState()); 224 let filter = getFilter(getState()); 225 226 // If display, filter and inversion haven't changed, don't do anything. 227 if (censusIsUpToDate(filter, display, getCensus(snapshot))) { 228 removeFromCache(); 229 return; 230 } 231 232 // Keep taking a census if the display changes while our request is in 233 // flight. Recheck that the display used for the census is the same as the 234 // state's display. 235 do { 236 display = getDisplay(getState()); 237 filter = getState().filter; 238 239 dispatch({ 240 type: beginAction, 241 id, 242 filter, 243 display, 244 }); 245 246 const opts = display.inverted 247 ? { asInvertedTreeNode: true } 248 : { asTreeNode: true }; 249 250 opts.filter = filter || null; 251 252 try { 253 ({ report, parentMap } = await heapWorker.takeCensus( 254 snapshot.path, 255 { breakdown: display.breakdown }, 256 opts 257 )); 258 } catch (error) { 259 removeFromCache(); 260 reportException("takeCensus", error); 261 dispatch({ type: errorAction, id, error }); 262 return; 263 } 264 } while ( 265 filter !== getState().filter || 266 display !== getDisplay(getState()) 267 ); 268 269 removeFromCache(); 270 dispatch({ 271 type: endAction, 272 id, 273 display, 274 filter, 275 report, 276 parentMap, 277 }); 278 }, 279 }); 280 } 281 282 /** 283 * Take a census. 284 */ 285 const takeCensus = (exports.takeCensus = makeTakeCensusTask({ 286 getDisplay: state => state.censusDisplay, 287 getFilter: state => state.filter, 288 getCensus: snapshot => snapshot.census, 289 beginAction: actions.TAKE_CENSUS_START, 290 endAction: actions.TAKE_CENSUS_END, 291 errorAction: actions.TAKE_CENSUS_ERROR, 292 canTakeCensus: snapshot => 293 snapshot.state === states.READ && 294 (!snapshot.census || snapshot.census.state === censusState.SAVED), 295 })); 296 297 /** 298 * Take a census for the treemap. 299 */ 300 const takeTreeMap = (exports.takeTreeMap = makeTakeCensusTask({ 301 getDisplay: state => state.treeMapDisplay, 302 getFilter: () => null, 303 getCensus: snapshot => snapshot.treeMap, 304 beginAction: actions.TAKE_TREE_MAP_START, 305 endAction: actions.TAKE_TREE_MAP_END, 306 errorAction: actions.TAKE_TREE_MAP_ERROR, 307 canTakeCensus: snapshot => 308 snapshot.state === states.READ && 309 (!snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED), 310 })); 311 312 /** 313 * Define what should be the default mode for taking a census based on the 314 * default view of the tool. 315 */ 316 const defaultCensusTaker = takeTreeMap; 317 318 /** 319 * Pick the default census taker when taking a snapshot. This should be 320 * determined by the current view. If the view doesn't include a census, then 321 * use the default one defined above. Some census information is always needed 322 * to display some basic information about a snapshot. 323 * 324 * @param {string} value from viewState 325 */ 326 const getCurrentCensusTaker = (exports.getCurrentCensusTaker = function ( 327 currentView 328 ) { 329 switch (currentView) { 330 case viewState.TREE_MAP: 331 return takeTreeMap; 332 case viewState.CENSUS: 333 return takeCensus; 334 default: 335 return defaultCensusTaker; 336 } 337 }); 338 339 /** 340 * Focus the given node in the individuals view. 341 * 342 * @param {DominatorTreeNode} node. 343 */ 344 exports.focusIndividual = function (node) { 345 return { 346 type: actions.FOCUS_INDIVIDUAL, 347 node, 348 }; 349 }; 350 351 /** 352 * Fetch the individual `DominatorTreeNodes` for the census group specified by 353 * `censusBreakdown` and `reportLeafIndex`. 354 * 355 * @param {HeapAnalysesClient} heapWorker 356 * @param {SnapshotId} id 357 * @param {object} censusBreakdown 358 * @param {Set<number> | number} reportLeafIndex 359 */ 360 const fetchIndividuals = (exports.fetchIndividuals = function ( 361 heapWorker, 362 id, 363 censusBreakdown, 364 reportLeafIndex 365 ) { 366 return async function ({ dispatch, getState }) { 367 if (getState().view.state !== viewState.INDIVIDUALS) { 368 dispatch(view.changeView(viewState.INDIVIDUALS)); 369 } 370 371 const snapshot = getSnapshot(getState(), id); 372 assert( 373 snapshot && snapshot.state === states.READ, 374 "The snapshot should already be read into memory" 375 ); 376 377 if (!dominatorTreeIsComputed(snapshot)) { 378 await dispatch(computeAndFetchDominatorTree(heapWorker, id)); 379 } 380 381 const snapshot_ = getSnapshot(getState(), id); 382 assert( 383 snapshot_.dominatorTree?.root, 384 "Should have a dominator tree with a root." 385 ); 386 387 const dominatorTreeId = snapshot_.dominatorTree.dominatorTreeId; 388 389 const indices = isSet(reportLeafIndex) 390 ? reportLeafIndex 391 : new Set([reportLeafIndex]); 392 393 let labelDisplay; 394 let nodes; 395 do { 396 labelDisplay = getState().labelDisplay; 397 assert( 398 labelDisplay?.breakdown?.by, 399 `Should have a breakdown to label nodes with, got: ${JSON.stringify( 400 labelDisplay 401 )}` 402 ); 403 404 if (getState().view.state !== viewState.INDIVIDUALS) { 405 // We switched views while in the process of fetching individuals -- any 406 // further work is useless. 407 return; 408 } 409 410 dispatch({ type: actions.FETCH_INDIVIDUALS_START }); 411 412 try { 413 ({ nodes } = await heapWorker.getCensusIndividuals({ 414 dominatorTreeId, 415 indices, 416 censusBreakdown, 417 labelBreakdown: labelDisplay.breakdown, 418 maxRetainingPaths: Services.prefs.getIntPref( 419 "devtools.memory.max-retaining-paths" 420 ), 421 maxIndividuals: Services.prefs.getIntPref( 422 "devtools.memory.max-individuals" 423 ), 424 })); 425 } catch (error) { 426 reportException("actions/snapshot/fetchIndividuals", error); 427 dispatch({ type: actions.INDIVIDUALS_ERROR, error }); 428 return; 429 } 430 } while (labelDisplay !== getState().labelDisplay); 431 432 dispatch({ 433 type: actions.FETCH_INDIVIDUALS_END, 434 id, 435 censusBreakdown, 436 indices, 437 labelDisplay, 438 nodes, 439 dominatorTree: snapshot_.dominatorTree, 440 }); 441 }; 442 }); 443 444 /** 445 * Refresh the current individuals view. 446 * 447 * @param {HeapAnalysesClient} heapWorker 448 */ 449 exports.refreshIndividuals = function (heapWorker) { 450 return async function ({ dispatch, getState }) { 451 assert( 452 getState().view.state === viewState.INDIVIDUALS, 453 "Should be in INDIVIDUALS view." 454 ); 455 456 const { individuals } = getState(); 457 458 switch (individuals.state) { 459 case individualsState.COMPUTING_DOMINATOR_TREE: 460 case individualsState.FETCHING: 461 // Nothing to do here. 462 return; 463 464 case individualsState.FETCHED: 465 if (getState().individuals.labelDisplay === getState().labelDisplay) { 466 return; 467 } 468 break; 469 470 case individualsState.ERROR: 471 // Doesn't hurt to retry: maybe we won't get an error this time around? 472 break; 473 474 default: 475 assert(false, `Unexpected individuals state: ${individuals.state}`); 476 return; 477 } 478 479 await dispatch( 480 fetchIndividuals( 481 heapWorker, 482 individuals.id, 483 individuals.censusBreakdown, 484 individuals.indices 485 ) 486 ); 487 }; 488 }; 489 490 /** 491 * Refresh the selected snapshot's census data, if need be (for example, 492 * display configuration changed). 493 * 494 * @param {HeapAnalysesClient} heapWorker 495 */ 496 exports.refreshSelectedCensus = function (heapWorker) { 497 return async function ({ dispatch, getState }) { 498 const snapshot = getState().snapshots.find(s => s.selected); 499 if (!snapshot || snapshot.state !== states.READ) { 500 return; 501 } 502 503 // Intermediate snapshot states will get handled by the task action that is 504 // orchestrating them. For example, if the snapshot census's state is 505 // SAVING, then the takeCensus action will keep taking a census until 506 // the inverted property matches the inverted state. If the snapshot is 507 // still in the process of being saved or read, the takeSnapshotAndCensus 508 // task action will follow through and ensure that a census is taken. 509 if ( 510 (snapshot.census && snapshot.census.state === censusState.SAVED) || 511 !snapshot.census 512 ) { 513 await dispatch(takeCensus(heapWorker, snapshot.id)); 514 } 515 }; 516 }; 517 518 /** 519 * Refresh the selected snapshot's tree map data, if need be (for example, 520 * display configuration changed). 521 * 522 * @param {HeapAnalysesClient} heapWorker 523 */ 524 exports.refreshSelectedTreeMap = function (heapWorker) { 525 return async function ({ dispatch, getState }) { 526 const snapshot = getState().snapshots.find(s => s.selected); 527 if (!snapshot || snapshot.state !== states.READ) { 528 return; 529 } 530 531 // Intermediate snapshot states will get handled by the task action that is 532 // orchestrating them. For example, if the snapshot census's state is 533 // SAVING, then the takeCensus action will keep taking a census until 534 // the inverted property matches the inverted state. If the snapshot is 535 // still in the process of being saved or read, the takeSnapshotAndCensus 536 // task action will follow through and ensure that a census is taken. 537 if ( 538 (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) || 539 !snapshot.treeMap 540 ) { 541 await dispatch(takeTreeMap(heapWorker, snapshot.id)); 542 } 543 }; 544 }; 545 546 /** 547 * Request that the `HeapAnalysesWorker` compute the dominator tree for the 548 * snapshot with the given `id`. 549 * 550 * @param {HeapAnalysesClient} heapWorker 551 * @param {SnapshotId} id 552 * 553 * @returns {Promise<DominatorTreeId>} 554 */ 555 const computeDominatorTree = (exports.computeDominatorTree = 556 TaskCache.declareCacheableTask({ 557 getCacheKey(_, id) { 558 return id; 559 }, 560 561 async task(heapWorker, id, removeFromCache, dispatch, getState) { 562 const snapshot = getSnapshot(getState(), id); 563 assert( 564 !snapshot.dominatorTree?.dominatorTreeId, 565 "Should not re-compute dominator trees" 566 ); 567 568 dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_START, id }); 569 570 let dominatorTreeId; 571 try { 572 dominatorTreeId = await heapWorker.computeDominatorTree(snapshot.path); 573 } catch (error) { 574 removeFromCache(); 575 reportException("actions/snapshot/computeDominatorTree", error); 576 dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error }); 577 return null; 578 } 579 580 removeFromCache(); 581 dispatch({ 582 type: actions.COMPUTE_DOMINATOR_TREE_END, 583 id, 584 dominatorTreeId, 585 }); 586 return dominatorTreeId; 587 }, 588 })); 589 590 /** 591 * Get the partial subtree, starting from the root, of the 592 * snapshot-with-the-given-id's dominator tree. 593 * 594 * @param {HeapAnalysesClient} heapWorker 595 * @param {SnapshotId} id 596 * 597 * @returns {Promise<DominatorTreeNode>} 598 */ 599 const fetchDominatorTree = (exports.fetchDominatorTree = 600 TaskCache.declareCacheableTask({ 601 getCacheKey(_, id) { 602 return id; 603 }, 604 605 async task(heapWorker, id, removeFromCache, dispatch, getState) { 606 const snapshot = getSnapshot(getState(), id); 607 assert( 608 dominatorTreeIsComputed(snapshot), 609 "Should have dominator tree model and it should be computed" 610 ); 611 612 let display; 613 let root; 614 do { 615 display = getState().labelDisplay; 616 assert( 617 display?.breakdown, 618 `Should have a breakdown to describe nodes with, got: ${JSON.stringify( 619 display 620 )}` 621 ); 622 623 dispatch({ type: actions.FETCH_DOMINATOR_TREE_START, id, display }); 624 625 try { 626 root = await heapWorker.getDominatorTree({ 627 dominatorTreeId: snapshot.dominatorTree.dominatorTreeId, 628 breakdown: display.breakdown, 629 maxRetainingPaths: Services.prefs.getIntPref( 630 "devtools.memory.max-retaining-paths" 631 ), 632 }); 633 } catch (error) { 634 removeFromCache(); 635 reportException("actions/snapshot/fetchDominatorTree", error); 636 dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error }); 637 return null; 638 } 639 } while (display !== getState().labelDisplay); 640 641 removeFromCache(); 642 dispatch({ type: actions.FETCH_DOMINATOR_TREE_END, id, root }); 643 return root; 644 }, 645 })); 646 647 /** 648 * Fetch the immediately dominated children represented by the placeholder 649 * `lazyChildren` from snapshot-with-the-given-id's dominator tree. 650 * 651 * @param {HeapAnalysesClient} heapWorker 652 * @param {SnapshotId} id 653 * @param {DominatorTreeLazyChildren} lazyChildren 654 */ 655 exports.fetchImmediatelyDominated = TaskCache.declareCacheableTask({ 656 getCacheKey(_, id, lazyChildren) { 657 return `${id}-${lazyChildren.key()}`; 658 }, 659 660 async task( 661 heapWorker, 662 id, 663 lazyChildren, 664 removeFromCache, 665 dispatch, 666 getState 667 ) { 668 const snapshot = getSnapshot(getState(), id); 669 assert(snapshot.dominatorTree, "Should have dominator tree model"); 670 assert( 671 snapshot.dominatorTree.state === dominatorTreeState.LOADED || 672 snapshot.dominatorTree.state === 673 dominatorTreeState.INCREMENTAL_FETCHING, 674 "Cannot fetch immediately dominated nodes in a dominator tree unless " + 675 " the dominator tree has already been computed" 676 ); 677 678 let display; 679 let response; 680 do { 681 display = getState().labelDisplay; 682 assert(display, "Should have a display to describe nodes with."); 683 684 dispatch({ type: actions.FETCH_IMMEDIATELY_DOMINATED_START, id }); 685 686 try { 687 response = await heapWorker.getImmediatelyDominated({ 688 dominatorTreeId: snapshot.dominatorTree.dominatorTreeId, 689 breakdown: display.breakdown, 690 nodeId: lazyChildren.parentNodeId(), 691 startIndex: lazyChildren.siblingIndex(), 692 maxRetainingPaths: Services.prefs.getIntPref( 693 "devtools.memory.max-retaining-paths" 694 ), 695 }); 696 } catch (error) { 697 removeFromCache(); 698 reportException("actions/snapshot/fetchImmediatelyDominated", error); 699 dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error }); 700 return; 701 } 702 } while (display !== getState().labelDisplay); 703 704 removeFromCache(); 705 dispatch({ 706 type: actions.FETCH_IMMEDIATELY_DOMINATED_END, 707 id, 708 path: response.path, 709 nodes: response.nodes, 710 moreChildrenAvailable: response.moreChildrenAvailable, 711 }); 712 }, 713 }); 714 715 /** 716 * Compute and then fetch the dominator tree of the snapshot with the given 717 * `id`. 718 * 719 * @param {HeapAnalysesClient} heapWorker 720 * @param {SnapshotId} id 721 * 722 * @returns {Promise<DominatorTreeNode>} 723 */ 724 const computeAndFetchDominatorTree = (exports.computeAndFetchDominatorTree = 725 TaskCache.declareCacheableTask({ 726 getCacheKey(_, id) { 727 return id; 728 }, 729 730 async task(heapWorker, id, removeFromCache, dispatch) { 731 const dominatorTreeId = await dispatch( 732 computeDominatorTree(heapWorker, id) 733 ); 734 if (dominatorTreeId === null) { 735 removeFromCache(); 736 return null; 737 } 738 739 const root = await dispatch(fetchDominatorTree(heapWorker, id)); 740 removeFromCache(); 741 742 if (!root) { 743 return null; 744 } 745 746 return root; 747 }, 748 })); 749 750 /** 751 * Update the currently selected snapshot's dominator tree. 752 * 753 * @param {HeapAnalysesClient} heapWorker 754 */ 755 exports.refreshSelectedDominatorTree = function (heapWorker) { 756 return async function ({ dispatch, getState }) { 757 const snapshot = getState().snapshots.find(s => s.selected); 758 if (!snapshot) { 759 return; 760 } 761 762 if ( 763 snapshot.dominatorTree && 764 !( 765 snapshot.dominatorTree.state === dominatorTreeState.COMPUTED || 766 snapshot.dominatorTree.state === dominatorTreeState.LOADED || 767 snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING 768 ) 769 ) { 770 return; 771 } 772 773 // We need to check for the snapshot state because if there was an error, 774 // we can't continue and if we are still saving or reading the snapshot, 775 // then takeSnapshotAndCensus will finish the job for us 776 if (snapshot.state === states.READ) { 777 if (snapshot.dominatorTree) { 778 await dispatch(fetchDominatorTree(heapWorker, snapshot.id)); 779 } else { 780 await dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id)); 781 } 782 } 783 }; 784 }; 785 786 /** 787 * Select the snapshot with the given id. 788 * 789 * @param {snapshotId} id 790 * @see {Snapshot} model defined in devtools/client/memory/models.js 791 */ 792 const selectSnapshot = (exports.selectSnapshot = function (id) { 793 return { 794 type: actions.SELECT_SNAPSHOT, 795 id, 796 }; 797 }); 798 799 /** 800 * Delete all snapshots that are in the READ or ERROR state 801 * 802 * @param {HeapAnalysesClient} heapWorker 803 */ 804 exports.clearSnapshots = function (heapWorker) { 805 return async function ({ dispatch, getState }) { 806 const snapshots = getState().snapshots.filter(s => { 807 const snapshotReady = s.state === states.READ || s.state === states.ERROR; 808 const censusReady = 809 (s.treeMap && s.treeMap.state === treeMapState.SAVED) || 810 (s.census && s.census.state === censusState.SAVED); 811 812 return snapshotReady && censusReady; 813 }); 814 815 const ids = snapshots.map(s => s.id); 816 817 dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids }); 818 819 if (getState().diffing) { 820 dispatch(diffing.toggleDiffing()); 821 } 822 if (getState().individuals) { 823 dispatch(view.popView()); 824 } 825 826 await Promise.all( 827 snapshots.map(snapshot => { 828 return heapWorker.deleteHeapSnapshot(snapshot.path).catch(error => { 829 reportException("clearSnapshots", error); 830 dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error }); 831 }); 832 }) 833 ); 834 835 dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids }); 836 }; 837 }; 838 839 /** 840 * Delete a snapshot 841 * 842 * @param {HeapAnalysesClient} heapWorker 843 * @param {snapshotModel} snapshot 844 */ 845 exports.deleteSnapshot = function (heapWorker, snapshot) { 846 return async function ({ dispatch }) { 847 dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids: [snapshot.id] }); 848 849 try { 850 await heapWorker.deleteHeapSnapshot(snapshot.path); 851 } catch (error) { 852 reportException("deleteSnapshot", error); 853 dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error }); 854 } 855 856 dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids: [snapshot.id] }); 857 }; 858 }; 859 860 /** 861 * Expand the given node in the snapshot's census report. 862 * 863 * @param {CensusTreeNode} node 864 */ 865 exports.expandCensusNode = function (id, node) { 866 return { 867 type: actions.EXPAND_CENSUS_NODE, 868 id, 869 node, 870 }; 871 }; 872 873 /** 874 * Collapse the given node in the snapshot's census report. 875 * 876 * @param {CensusTreeNode} node 877 */ 878 exports.collapseCensusNode = function (id, node) { 879 return { 880 type: actions.COLLAPSE_CENSUS_NODE, 881 id, 882 node, 883 }; 884 }; 885 886 /** 887 * Focus the given node in the snapshot's census's report. 888 * 889 * @param {SnapshotId} id 890 * @param {DominatorTreeNode} node 891 */ 892 exports.focusCensusNode = function (id, node) { 893 return { 894 type: actions.FOCUS_CENSUS_NODE, 895 id, 896 node, 897 }; 898 }; 899 900 /** 901 * Expand the given node in the snapshot's dominator tree. 902 * 903 * @param {DominatorTreeTreeNode} node 904 */ 905 exports.expandDominatorTreeNode = function (id, node) { 906 return { 907 type: actions.EXPAND_DOMINATOR_TREE_NODE, 908 id, 909 node, 910 }; 911 }; 912 913 /** 914 * Collapse the given node in the snapshot's dominator tree. 915 * 916 * @param {DominatorTreeTreeNode} node 917 */ 918 exports.collapseDominatorTreeNode = function (id, node) { 919 return { 920 type: actions.COLLAPSE_DOMINATOR_TREE_NODE, 921 id, 922 node, 923 }; 924 }; 925 926 /** 927 * Focus the given node in the snapshot's dominator tree. 928 * 929 * @param {SnapshotId} id 930 * @param {DominatorTreeNode} node 931 */ 932 exports.focusDominatorTreeNode = function (id, node) { 933 return { 934 type: actions.FOCUS_DOMINATOR_TREE_NODE, 935 id, 936 node, 937 }; 938 };