summaryrefslogtreecommitdiffstats
path: root/devtools/client/memory/components/Heap.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/memory/components/Heap.js')
-rw-r--r--devtools/client/memory/components/Heap.js547
1 files changed, 547 insertions, 0 deletions
diff --git a/devtools/client/memory/components/Heap.js b/devtools/client/memory/components/Heap.js
new file mode 100644
index 0000000000..2c78a63749
--- /dev/null
+++ b/devtools/client/memory/components/Heap.js
@@ -0,0 +1,547 @@
+/* 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;