/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { Component, createFactory, } = require("resource://devtools/client/shared/vendor/react.js"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); const { assert, safeErrorString, } = require("resource://devtools/shared/DevToolsUtils.js"); const Census = createFactory( require("resource://devtools/client/memory/components/Census.js") ); const CensusHeader = createFactory( require("resource://devtools/client/memory/components/CensusHeader.js") ); const DominatorTree = createFactory( require("resource://devtools/client/memory/components/DominatorTree.js") ); const DominatorTreeHeader = createFactory( require("resource://devtools/client/memory/components/DominatorTreeHeader.js") ); const TreeMap = createFactory( require("resource://devtools/client/memory/components/TreeMap.js") ); const HSplitBox = createFactory( require("resource://devtools/client/shared/components/HSplitBox.js") ); const Individuals = createFactory( require("resource://devtools/client/memory/components/Individuals.js") ); const IndividualsHeader = createFactory( require("resource://devtools/client/memory/components/IndividualsHeader.js") ); const ShortestPaths = createFactory( require("resource://devtools/client/memory/components/ShortestPaths.js") ); const { getStatusTextFull, L10N, } = require("resource://devtools/client/memory/utils.js"); const { snapshotState: states, diffingState, viewState, censusState, treeMapState, dominatorTreeState, individualsState, } = require("resource://devtools/client/memory/constants.js"); const models = require("resource://devtools/client/memory/models.js"); const { snapshot: snapshotModel, diffingModel } = models; /** * Get the app state's current state atom. * * @see the relevant state string constants in `../constants.js`. * * @param {models.view} view * @param {snapshotModel} snapshot * @param {diffingModel} diffing * @param {individualsModel} individuals * * @return {snapshotState|diffingState|dominatorTreeState} */ function getState(view, snapshot, diffing, individuals) { switch (view.state) { case viewState.CENSUS: return snapshot.census ? snapshot.census.state : snapshot.state; case viewState.DIFFING: return diffing.state; case viewState.TREE_MAP: return snapshot.treeMap ? snapshot.treeMap.state : snapshot.state; case viewState.DOMINATOR_TREE: return snapshot.dominatorTree ? snapshot.dominatorTree.state : snapshot.state; case viewState.INDIVIDUALS: return individuals.state; } assert(false, `Unexpected view state: ${view.state}`); return null; } /** * Return true if we should display a status message when we are in the given * state. Return false otherwise. * * @param {snapshotState|diffingState|dominatorTreeState} state * @param {models.view} view * @param {snapshotModel} snapshot * * @returns {Boolean} */ function shouldDisplayStatus(state, view, snapshot) { switch (state) { case states.IMPORTING: case states.SAVING: case states.SAVED: case states.READING: case censusState.SAVING: case treeMapState.SAVING: case diffingState.SELECTING: case diffingState.TAKING_DIFF: case dominatorTreeState.COMPUTING: case dominatorTreeState.COMPUTED: case dominatorTreeState.FETCHING: case individualsState.COMPUTING_DOMINATOR_TREE: case individualsState.FETCHING: return true; } return view.state === viewState.DOMINATOR_TREE && !snapshot.dominatorTree; } /** * Get the status text to display for the given state. * * @param {snapshotState|diffingState|dominatorTreeState} state * @param {diffingModel} diffing * * @returns {String} */ function getStateStatusText(state, diffing) { if (state === diffingState.SELECTING) { return L10N.getStr( diffing.firstSnapshotId === null ? "diffing.prompt.selectBaseline" : "diffing.prompt.selectComparison" ); } return getStatusTextFull(state); } /** * Given that we should display a status message, return true if we should also * display a throbber along with the status message. Return false otherwise. * * @param {diffingModel} diffing * * @returns {Boolean} */ function shouldDisplayThrobber(diffing) { return !diffing || diffing.state !== diffingState.SELECTING; } /** * Get the current state's error, or return null if there is none. * * @param {snapshotModel} snapshot * @param {diffingModel} diffing * @param {individualsModel} individuals * * @returns {Error|null} */ function getError(snapshot, diffing, individuals) { if (diffing) { if (diffing.state === diffingState.ERROR) { return diffing.error; } if (diffing.census === censusState.ERROR) { return diffing.census.error; } } if (snapshot) { if (snapshot.state === states.ERROR) { return snapshot.error; } if (snapshot.census === censusState.ERROR) { return snapshot.census.error; } if (snapshot.treeMap === treeMapState.ERROR) { return snapshot.treeMap.error; } if ( snapshot.dominatorTree && snapshot.dominatorTree.state === dominatorTreeState.ERROR ) { return snapshot.dominatorTree.error; } } if (individuals && individuals.state === individualsState.ERROR) { return individuals.error; } return null; } /** * Main view for the memory tool. * * The Heap component contains several panels for different states; an initial * state of only a button to take a snapshot, loading states, the census view * tree, the dominator tree, etc. */ class Heap extends Component { static get propTypes() { return { onSnapshotClick: PropTypes.func.isRequired, onLoadMoreSiblings: PropTypes.func.isRequired, onCensusExpand: PropTypes.func.isRequired, onCensusCollapse: PropTypes.func.isRequired, onDominatorTreeExpand: PropTypes.func.isRequired, onDominatorTreeCollapse: PropTypes.func.isRequired, onCensusFocus: PropTypes.func.isRequired, onDominatorTreeFocus: PropTypes.func.isRequired, onShortestPathsResize: PropTypes.func.isRequired, snapshot: snapshotModel, onViewSourceInDebugger: PropTypes.func.isRequired, onPopView: PropTypes.func.isRequired, individuals: models.individuals, onViewIndividuals: PropTypes.func.isRequired, onFocusIndividual: PropTypes.func.isRequired, diffing: diffingModel, view: models.view.isRequired, sizes: PropTypes.object.isRequired, }; } constructor(props) { super(props); this._renderHeapView = this._renderHeapView.bind(this); this._renderInitial = this._renderInitial.bind(this); this._renderStatus = this._renderStatus.bind(this); this._renderError = this._renderError.bind(this); this._renderCensus = this._renderCensus.bind(this); this._renderTreeMap = this._renderTreeMap.bind(this); this._renderIndividuals = this._renderIndividuals.bind(this); this._renderDominatorTree = this._renderDominatorTree.bind(this); } /** * Render the heap view's container panel with the given contents inside of * it. * * @param {snapshotState|diffingState|dominatorTreeState} state * @param {...Any} contents */ _renderHeapView(state, ...contents) { return dom.div( { id: "heap-view", "data-state": state, }, dom.div( { className: "heap-view-panel", "data-state": state, }, ...contents ) ); } _renderInitial(onSnapshotClick) { return this._renderHeapView( "initial", dom.button( { className: "devtools-button take-snapshot", onClick: onSnapshotClick, "data-standalone": true, }, L10N.getStr("take-snapshot") ) ); } _renderStatus(state, statusText, diffing) { let throbber = ""; if (shouldDisplayThrobber(diffing)) { throbber = "devtools-throbber"; } return this._renderHeapView( state, dom.span( { className: `snapshot-status ${throbber}`, }, statusText ) ); } _renderError(state, statusText, error) { return this._renderHeapView( state, dom.span({ className: "snapshot-status error" }, statusText), dom.pre({}, safeErrorString(error)) ); } _renderCensus( state, census, diffing, onViewSourceInDebugger, onViewIndividuals ) { assert( census.report, "Should not render census that does not have a report" ); if (!census.report.children) { const censusFilterMsg = census.filter ? L10N.getStr("heapview.none-match") : L10N.getStr("heapview.empty"); const msg = diffing ? L10N.getStr("heapview.no-difference") : censusFilterMsg; return this._renderHeapView(state, dom.div({ className: "empty" }, msg)); } const contents = []; if ( census.display.breakdown.by === "allocationStack" && census.report.children && census.report.children.length === 1 && census.report.children[0].name === "noStack" ) { contents.push( dom.div( { className: "error no-allocation-stacks" }, L10N.getStr("heapview.noAllocationStacks") ) ); } contents.push(CensusHeader({ diffing })); contents.push( Census({ onViewSourceInDebugger, onViewIndividuals, diffing, census, onExpand: node => this.props.onCensusExpand(census, node), onCollapse: node => this.props.onCensusCollapse(census, node), onFocus: node => this.props.onCensusFocus(census, node), }) ); return this._renderHeapView(state, ...contents); } _renderTreeMap(state, treeMap) { return this._renderHeapView(state, TreeMap({ treeMap })); } _renderIndividuals( state, individuals, dominatorTree, onViewSourceInDebugger ) { assert( individuals.state === individualsState.FETCHED, "Should have fetched individuals" ); assert(dominatorTree?.root, "Should have a dominator tree and its root"); const tree = dom.div( { className: "vbox", style: { overflowY: "auto", }, }, IndividualsHeader(), Individuals({ individuals, dominatorTree, onViewSourceInDebugger, onFocus: this.props.onFocusIndividual, }) ); const shortestPaths = ShortestPaths({ graph: individuals.focused ? individuals.focused.shortestPaths : null, }); return this._renderHeapView( state, dom.div( { className: "hbox devtools-toolbar" }, dom.label( { id: "pop-view-button-label" }, dom.button( { id: "pop-view-button", className: "devtools-button", onClick: this.props.onPopView, }, L10N.getStr("toolbar.pop-view") ), L10N.getStr("toolbar.pop-view.label") ), dom.span( { className: "toolbar-text" }, L10N.getStr("toolbar.viewing-individuals") ) ), HSplitBox({ start: tree, end: shortestPaths, startWidth: this.props.sizes.shortestPathsSize, onResize: this.props.onShortestPathsResize, }) ); } _renderDominatorTree( state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings ) { const tree = dom.div( { className: "vbox", style: { overflowY: "auto", }, }, DominatorTreeHeader(), DominatorTree({ onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings, onExpand: this.props.onDominatorTreeExpand, onCollapse: this.props.onDominatorTreeCollapse, onFocus: this.props.onDominatorTreeFocus, }) ); const shortestPaths = ShortestPaths({ graph: dominatorTree.focused ? dominatorTree.focused.shortestPaths : null, }); return this._renderHeapView( state, HSplitBox({ start: tree, end: shortestPaths, startWidth: this.props.sizes.shortestPathsSize, onResize: this.props.onShortestPathsResize, }) ); } render() { const { snapshot, diffing, onSnapshotClick, onLoadMoreSiblings, onViewSourceInDebugger, onViewIndividuals, individuals, view, } = this.props; if (!diffing && !snapshot && !individuals) { return this._renderInitial(onSnapshotClick); } const state = getState(view, snapshot, diffing, individuals); const statusText = getStateStatusText(state, diffing); if (shouldDisplayStatus(state, view, snapshot)) { return this._renderStatus(state, statusText, diffing); } const error = getError(snapshot, diffing, individuals); if (error) { return this._renderError(state, statusText, error); } if (view.state === viewState.CENSUS || view.state === viewState.DIFFING) { const census = view.state === viewState.CENSUS ? snapshot.census : diffing.census; if (!census) { return this._renderStatus(state, statusText, diffing); } return this._renderCensus( state, census, diffing, onViewSourceInDebugger, onViewIndividuals ); } if (view.state === viewState.TREE_MAP) { return this._renderTreeMap(state, snapshot.treeMap); } if (view.state === viewState.INDIVIDUALS) { assert( individuals.state === individualsState.FETCHED, "Should have fetched the individuals -- " + "other states are rendered as statuses" ); return this._renderIndividuals( state, individuals, individuals.dominatorTree, onViewSourceInDebugger ); } assert( view.state === viewState.DOMINATOR_TREE, "If we aren't in progress, looking at a census, or diffing, then we " + "must be looking at a dominator tree" ); assert(!diffing, "Should not have diffing"); assert(snapshot.dominatorTree, "Should have a dominator tree"); return this._renderDominatorTree( state, onViewSourceInDebugger, snapshot.dominatorTree, onLoadMoreSiblings ); } } module.exports = Heap;