fune/devtools/client/memory/components/heap.js
2016-12-21 15:21:24 +01:00

459 lines
13 KiB
JavaScript

/* 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 { 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.
*/
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 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 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 && 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,
})
);
},
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);
},
});