547 lines
14 KiB
JavaScript
547 lines
14 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 {
|
|
Component,
|
|
createFactory,
|
|
} = require("resource://devtools/client/shared/vendor/react.mjs");
|
|
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
|
|
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
|
|
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;
|