diff options
Diffstat (limited to 'devtools/client/memory/components/heap.js')
-rw-r--r-- | devtools/client/memory/components/heap.js | 455 |
1 files changed, 455 insertions, 0 deletions
diff --git a/devtools/client/memory/components/heap.js b/devtools/client/memory/components/heap.js new file mode 100644 index 000000000..786f37ae1 --- /dev/null +++ b/devtools/client/memory/components/heap.js @@ -0,0 +1,455 @@ +/* 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/. */ + +const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react"); +const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils"); +const Census = createFactory(require("./census")); +const CensusHeader = createFactory(require("./census-header")); +const DominatorTree = createFactory(require("./dominator-tree")); +const DominatorTreeHeader = createFactory(require("./dominator-tree-header")); +const TreeMap = createFactory(require("./tree-map")); +const HSplitBox = createFactory(require("devtools/client/shared/components/h-split-box")); +const Individuals = createFactory(require("./individuals")); +const IndividualsHeader = createFactory(require("./individuals-header")); +const ShortestPaths = createFactory(require("./shortest-paths")); +const { getStatusTextFull, L10N } = require("../utils"); +const { + snapshotState: states, + diffingState, + viewState, + censusState, + treeMapState, + dominatorTreeState, + individualsState, +} = require("../constants"); +const models = require("../models"); +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. + */ +const Heap = module.exports = createClass({ + displayName: "Heap", + + propTypes: { + 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, + }, + + render() { + let { + 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); + }, + + /** + * 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-toolbarbutton take-snapshot", + onClick: onSnapshotClick, + "data-standalone": true, + "data-text-only": 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 msg = diffing ? L10N.getStr("heapview.no-difference") + : census.filter ? L10N.getStr("heapview.none-match") + : L10N.getStr("heapview.empty"); + 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 && 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") + ), + 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, + }) + ); + }, +}); |