forked from mirrors/gecko-dev
459 lines
13 KiB
JavaScript
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);
|
|
},
|
|
});
|