--- a/devtools/client/locales/en-US/memory.properties
+++ b/devtools/client/locales/en-US/memory.properties
@@ -5,72 +5,88 @@
# LOCALIZATION NOTE These strings are used inside the Memory Tools
# which is available from the Web Developer sub-menu -> 'Memory'.
# The correct localization of this file might be to keep it in
# English, or another language commonly spoken among web developers.
# You want to make that choice consistent across the developer tools.
# A good criteria is the language in which you'd find the best
# documentation on web development on the web.
-# LOCALIZATION NOTE (memory.label):
-# This string is displayed in the title of the tab when the memory tool is
-# displayed inside the developer tools window and in the Developer Tools Menu.
+# LOCALIZATION NOTE (memory.label): This string is displayed in the title of the
+# tab when the memory tool is displayed inside the developer tools window and in
+# the Developer Tools Menu.
memory.label=Memory
-# LOCALIZATION NOTE (memory.panelLabel):
-# This is used as the label for the toolbox panel.
+# LOCALIZATION NOTE (memory.panelLabel): This is used as the label for the
+# toolbox panel.
memory.panelLabel=Memory Panel
-# LOCALIZATION NOTE (memory.tooltip):
-# This string is displayed in the tooltip of the tab when the memory tool is
-# displayed inside the developer tools window.
+# LOCALIZATION NOTE (memory.tooltip): This string is displayed in the tooltip of
+# the tab when the memory tool is displayed inside the developer tools window.
memory.tooltip=Memory
-# LOCALIZATION NOTE (snapshot.io.save): The label for the link that saves a snapshot
-# to disk.
+# LOCALIZATION NOTE (snapshot.io.save): The label for the link that saves a
+# snapshot to disk.
snapshot.io.save=Save
-# LOCALIZATION NOTE (snapshot.io.save.window): The title for the window displayed when
-# saving a snapshot to disk.
+# LOCALIZATION NOTE (snapshot.io.save.window): The title for the window
+# displayed when saving a snapshot to disk.
snapshot.io.save.window=Save Heap Snapshot
-# LOCALIZATION NOTE (snapshot.io.import.window): The title for the window displayed when
-# importing a snapshot form disk.
+# LOCALIZATION NOTE (snapshot.io.import.window): The title for the window
+# displayed when importing a snapshot form disk.
snapshot.io.import.window=Import Heap Snapshot
# LOCALIZATION NOTE (snapshot.io.filter): The title for the filter used to
# filter file types (*.fxsnapshot)
snapshot.io.filter=Firefox Heap Snapshots
-# LOCALIZATION NOTE (aggregate.mb): The label annotating the number of bytes (in megabytes)
-# in a snapshot. %S represents the value, rounded to 2 decimal points.
+# LOCALIZATION NOTE (aggregate.mb): The label annotating the number of bytes (in
+# megabytes) in a snapshot. %S represents the value, rounded to 2 decimal
+# points.
aggregate.mb=%S MB
# LOCALIZATION NOTE (snapshot-title.loading): The title for a snapshot before
# it has a creation time to display.
snapshot-title.loading=Processing…
# LOCALIZATION NOTE (checkbox.invertTree): The label describing the boolean
# checkbox whether or not to invert the tree.
checkbox.invertTree=Invert tree
-# LOCALIZATION NOTE (checkbox.recordAllocationStacks): The label describing the boolean
-# checkbox whether or not to record allocation stacks.
+# LOCALIZATION NOTE (checkbox.recordAllocationStacks): The label describing the
+# boolean checkbox whether or not to record allocation stacks.
checkbox.recordAllocationStacks=Record allocation stacks
# LOCALIZATION NOTE (toolbar.breakdownBy): The label describing the select menu
# options of the breakdown options.
toolbar.breakdownBy=Group by:
-# LOCALIZATION NOTE (take-snapshot): The label describing the button that initiates
-# taking a snapshot, either as the main label, or a tooltip.
+# LOCALIZATION NOTE (toolbar.labelBy): The label describing the select menu
+# options of the label options.
+toolbar.labelBy=Label by:
+
+# LOCALIZATION NOTE (toolbar.view): The label for the view selector in the
+# toolbar.
+toolbar.view=View:
+
+# LOCALIZATION NOTE (toolbar.view.census): The label for the census view option
+# in the toolbar.
+toolbar.view.census=Aggregate
+
+# LOCALIZATION NOTE (toolbar.view.dominators): The label for the dominators view
+# option in the toolbar.
+toolbar.view.dominators=Dominators
+
+# LOCALIZATION NOTE (take-snapshot): The label describing the button that
+# initiates taking a snapshot, either as the main label, or a tooltip.
take-snapshot=Take snapshot
-# LOCALIZATION NOTE (import-snapshot): The label describing the button that initiates
-# importing a snapshot.
+# LOCALIZATION NOTE (import-snapshot): The label describing the button that
+# initiates importing a snapshot.
import-snapshot=Import…
# LOCALIZATION NOTE (diff-snapshots): The label for the button that initiates
# selecting two snapshots to diff with each other.
diff-snapshots=+/-
# LOCALIZATION NOTE (diff-snapshots.tooltip): The tooltip for the button that
# initiates selecting two snapshots to diff with each other.
@@ -80,16 +96,20 @@ diff-snapshots.tooltip=Compare snapshots
# memory tool's filter search box.
filter.placeholder=Filter
# LOCALIZATION NOTE (viewsourceindebugger): The label for the tooltip when hovering over
# a link in the heap tree to jump to the debugger view.
# %S represents the URL to match in the debugger.
viewsourceindebugger=View source in Debugger → %S
+# LOCALIZATION NOTE (tree-item.load-more): The label for the links to fetch the
+# lazily loaded sub trees in the dominator tree view.
+tree-item.load-more=Load more…
+
# LOCALIZATION NOTE (tree-item.nostack): The label describing the row in the heap tree
# that represents a row broken down by allocation stack when no stack was available.
tree-item.nostack=(no stack available)
# LOCALIZATION NOTE (tree-item.nofilename): The label describing the row in the
# heap tree that represents a row broken down by filename when no filename was
# available.
tree-item.nofilename=(no filename available)
@@ -120,17 +140,17 @@ diffing.prompt.selectComparison=Select t
# LOCALIZATION NOTE (diffing.state.error): The label describing the diffing
# state ERROR, used in the snapshot list when an error occurs while diffing two
# snapshots.
diffing.state.error=Error
# LOCALIZATION NOTE (diffing.state.error.full): The text describing the diffing
# state ERROR, used in the main view when an error occurs while diffing two
# snapshots.
-diffing.state.error.full=There was an error while comparing snapshots
+diffing.state.error.full=There was an error while comparing snapshots.
# LOCALIZATION NOTE (diffing.state.taking-diff): The label describing the diffin
# state TAKING_DIFF, used in the snapshots list when computing the difference
# between two snapshots.
diffing.state.taking-diff=Computing difference…
# LOCALIZATION NOTE (diffing.state.taking-diff.full): The label describing the
# diffing state TAKING_DIFF, used in the main view when computing the difference
@@ -141,16 +161,48 @@ diffing.state.taking-diff.full=Computing difference…
# state SELECTING.
diffing.state.selecting=Select two snapshots to compare
# LOCALIZATION NOTE (diffing.state.selecting.full): The label describing the
# diffing state SELECTING, used in the main view when selecting snapshots to
# diff.
diffing.state.selecting.full=Select two snapshots to compare
+# LOCALIZATION NOTE (dominatorTree.state.computing): The label describing the
+# dominator tree state COMPUTING.
+dominatorTree.state.computing=Computing dominators…
+
+# LOCALIZATION NOTE (dominatorTree.state.computing): The label describing the
+# dominator tree state COMPUTING, used in the dominator tree view.
+dominatorTree.state.computing.full=Computing dominator tree…
+
+# LOCALIZATION NOTE (dominatorTree.state.fetching): The label describing the
+# dominator tree state FETCHING.
+dominatorTree.state.fetching=Computing sizes…
+
+# LOCALIZATION NOTE (dominatorTree.state.fetching): The label describing the
+# dominator tree state FETCHING, used in the dominator tree view.
+dominatorTree.state.fetching.full=Computing retained sizes…
+
+# LOCALIZATION NOTE (dominatorTree.state.incrementalFetching): The label
+# describing the dominator tree state INCREMENTAL_FETCHING.
+dominatorTree.state.incrementalFetching=Fetching…
+
+# LOCALIZATION NOTE (dominatorTree.state.incrementalFetching): The label describing the
+# dominator tree state INCREMENTAL_FETCHING, used in the dominator tree view.
+dominatorTree.state.incrementalFetching.full=Fetching subtree…
+
+# LOCALIZATION NOTE (dominatorTree.state.error): The label describing the
+# dominator tree state ERROR.
+dominatorTree.state.error=Error
+
+# LOCALIZATION NOTE (dominatorTree.state.error): The label describing the
+# dominator tree state ERROR, used in the dominator tree view.
+dominatorTree.state.error.full=There was an error while processing the dominator tree
+
# LOCALIZATION NOTE (snapshot.state.saving.full): The label describing the
# snapshot state SAVING, used in the main heap view.
snapshot.state.saving.full=Saving snapshot…
# LOCALIZATION NOTE (snapshot.state.importing.full): The label describing the
# snapshot state IMPORTING, used in the main heap view.
snapshot.state.importing.full=Importing…
@@ -188,16 +240,24 @@ snapshot.state.saving-census=Saving census…
# state ERROR, used in the snapshot list view.
snapshot.state.error=Error
# LOCALIZATION NOTE (heapview.noAllocationStacks): The message displayed to
# users when selecting a breakdown by "allocation stack" but no allocation
# stacks were recorded in the heap snapshot.
heapview.noAllocationStacks=No allocation stacks found. Record allocation stacks before taking a heap snapshot.
+# LOCALIZATION NOTE (heapview.field.retainedSize): The name of the column in the
+# dominator tree view for retained byte sizes.
+heapview.field.retainedSize=Retained Size (Bytes)
+
+# LOCALIZATION NOTE (heapview.field.shallowSize): The name of the column in the
+# dominator tree view for shallow byte sizes.
+heapview.field.shallowSize=Shallow Size (Bytes)
+
# LOCALIZATION NOTE (heapview.field.bytes): The name of the column in the heap view for bytes.
heapview.field.bytes=Bytes
# LOCALIZATION NOTE (heapview.field.count): The name of the column in the heap view for count.
heapview.field.count=Count
# LOCALIZATION NOTE (heapview.field.totalbytes): The name of the column in the heap view for total bytes.
heapview.field.totalbytes=Total Bytes
--- a/devtools/client/memory/actions/diffing.js
+++ b/devtools/client/memory/actions/diffing.js
@@ -1,27 +1,32 @@
/* 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 { assert, reportException } = require("devtools/shared/DevToolsUtils");
-const { actions, diffingState } = require("../constants");
+const { actions, diffingState, viewState } = require("../constants");
const {
breakdownEquals,
getSnapshot,
censusIsUpToDate,
snapshotIsDiffable
} = require("../utils");
/**
* Toggle diffing mode on or off.
*/
const toggleDiffing = exports.toggleDiffing = function () {
- return { type: actions.TOGGLE_DIFFING };
+ return function(dispatch, getState) {
+ dispatch({
+ type: actions.CHANGE_VIEW,
+ view: getState().diffing ? viewState.CENSUS : viewState.DIFFING,
+ });
+ };
};
/**
* Select the given snapshot for diffing.
*
* @param {snapshotModel} snapshot
*/
const selectSnapshotForDiffing = exports.selectSnapshotForDiffing = function (snapshot) {
@@ -147,8 +152,44 @@ const refreshDiffing = exports.refreshDi
const selectSnapshotForDiffingAndRefresh = exports.selectSnapshotForDiffingAndRefresh = function (heapWorker, snapshot) {
return function*(dispatch, getState) {
assert(getState().diffing,
"If we are selecting for diffing, we must be in diffing mode");
dispatch(selectSnapshotForDiffing(snapshot));
yield dispatch(refreshDiffing(heapWorker));
};
};
+
+/**
+ * Expand the given node in the diffing's census's delta-report.
+ *
+ * @param {CensusTreeNode} node
+ */
+const expandDiffingCensusNode = exports.expandDiffingCensusNode = function (node) {
+ return {
+ type: actions.EXPAND_DIFFING_CENSUS_NODE,
+ node,
+ };
+};
+
+/**
+ * Collapse the given node in the diffing's census's delta-report.
+ *
+ * @param {CensusTreeNode} node
+ */
+const collapseDiffingCensusNode = exports.collapseDiffingCensusNode = function (node) {
+ return {
+ type: actions.COLLAPSE_DIFFING_CENSUS_NODE,
+ node,
+ };
+};
+
+/**
+ * Focus the given node in the snapshot's census's report.
+ *
+ * @param {DominatorTreeNode} node
+ */
+const focusDiffingCensusNode = exports.focusDiffingCensusNode = function (node) {
+ return {
+ type: actions.FOCUS_DIFFING_CENSUS_NODE,
+ node,
+ };
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/dominatorTreeBreakdown.js
@@ -0,0 +1,37 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { breakdownEquals, createSnapshot } = require("../utils");
+const { actions, snapshotState: states } = require("../constants");
+const { refresh } = require("./refresh");
+
+const setDominatorTreeBreakdownAndRefresh =
+ exports.setDominatorTreeBreakdownAndRefresh =
+ function (heapWorker, breakdown) {
+ return function *(dispatch, getState) {
+ // Clears out all stored census data and sets the breakdown.
+ dispatch(setDominatorTreeBreakdown(breakdown));
+ yield dispatch(refresh(heapWorker));
+ };
+ };
+
+/**
+ * Clears out all census data in the snapshots and sets
+ * a new breakdown.
+ *
+ * @param {Breakdown} breakdown
+ */
+const setDominatorTreeBreakdown = exports.setDominatorTreeBreakdown = function (breakdown) {
+ assert(typeof breakdown === "object"
+ && breakdown
+ && breakdown.by,
+ `Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(breakdown)}`);
+
+ return {
+ type: actions.SET_DOMINATOR_TREE_BREAKDOWN,
+ breakdown,
+ };
+};
--- a/devtools/client/memory/actions/io.js
+++ b/devtools/client/memory/actions/io.js
@@ -59,17 +59,17 @@ const pickFileAndImportSnapshotAndCensus
}
yield dispatch(importSnapshotAndCensus(heapWorker, input.path));
};
};
const importSnapshotAndCensus = exports.importSnapshotAndCensus = function (heapWorker, path) {
return function* (dispatch, getState) {
- const snapshot = immutableUpdate(createSnapshot(), {
+ const snapshot = immutableUpdate(createSnapshot(getState()), {
path,
state: states.IMPORTING,
imported: true,
});
const id = snapshot.id;
dispatch({ type: actions.IMPORT_SNAPSHOT_START, snapshot });
dispatch(selectSnapshot(snapshot.id));
--- a/devtools/client/memory/actions/moz.build
+++ b/devtools/client/memory/actions/moz.build
@@ -2,14 +2,16 @@
# 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/.
DevToolsModules(
'allocations.js',
'breakdown.js',
'diffing.js',
+ 'dominatorTreeBreakdown.js',
'filter.js',
'inverted.js',
'io.js',
'refresh.js',
'snapshot.js',
+ 'view.js',
)
--- a/devtools/client/memory/actions/refresh.js
+++ b/devtools/client/memory/actions/refresh.js
@@ -1,22 +1,36 @@
/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { viewState } = require("../constants");
const { refreshDiffing } = require("./diffing");
-const { refreshSelectedCensus } = require("./snapshot");
+const snapshot = require("./snapshot");
/**
* Refresh the main thread's data from the heap analyses worker, if needed.
*
* @param {HeapAnalysesWorker} heapWorker
*/
exports.refresh = function (heapWorker) {
return function* (dispatch, getState) {
- if (getState().diffing) {
- yield dispatch(refreshDiffing(heapWorker));
- } else {
- yield dispatch(refreshSelectedCensus(heapWorker));
+ switch (getState().view) {
+ case viewState.DIFFING:
+ assert(getState().diffing, "Should have diffing state if in diffing view");
+ yield dispatch(refreshDiffing(heapWorker));
+ return;
+
+ case viewState.CENSUS:
+ yield dispatch(snapshot.refreshSelectedCensus(heapWorker));
+ return;
+
+ case viewState.DOMINATOR_TREE:
+ yield dispatch(snapshot.refreshSelectedDominatorTree(heapWorker));
+ return;
+
+ default:
+ assert(false, `Unexpected view state: ${getState().view}`);
}
};
};
--- a/devtools/client/memory/actions/snapshot.js
+++ b/devtools/client/memory/actions/snapshot.js
@@ -1,17 +1,24 @@
/* 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 { assert, reportException } = require("devtools/shared/DevToolsUtils");
-const { censusIsUpToDate, getSnapshot, breakdownEquals, createSnapshot } = require("../utils");
-const { actions, snapshotState: states } = require("../constants");
-const { toggleDiffing } = require("./diffing");
+const {
+ censusIsUpToDate,
+ getSnapshot,
+ breakdownEquals,
+ createSnapshot,
+ dominatorTreeIsComputed,
+} = require("../utils");
+const { actions, snapshotState: states, viewState, dominatorTreeState } = require("../constants");
+const view = require("./view");
+const refresh = require("./refresh");
/**
* A series of actions are fired from this task to save, read and generate the
* initial census from a snapshot.
*
* @param {MemoryFront}
* @param {HeapAnalysesClient}
* @param {Object}
@@ -21,51 +28,55 @@ const takeSnapshotAndCensus = exports.ta
const id = yield dispatch(takeSnapshot(front));
if (id === null) {
return;
}
yield dispatch(readSnapshot(heapWorker, id));
if (getSnapshot(getState(), id).state === states.READ) {
yield dispatch(takeCensus(heapWorker, id));
+
+ if (getState().view === viewState.DOMINATOR_TREE) {
+ yield dispatch(computeAndFetchDominatorTree(heapWorker, id));
+ }
}
};
};
/**
* Selects a snapshot and if the snapshot's census is using a different
* breakdown, take a new census.
*
* @param {HeapAnalysesClient} heapWorker
* @param {snapshotId} id
*/
const selectSnapshotAndRefresh = exports.selectSnapshotAndRefresh = function (heapWorker, id) {
return function *(dispatch, getState) {
if (getState().diffing) {
- dispatch(toggleDiffing());
+ dispatch(view.changeView(viewState.CENSUS));
}
dispatch(selectSnapshot(id));
- yield dispatch(refreshSelectedCensus(heapWorker));
+ yield dispatch(refresh.refresh(heapWorker));
};
};
/**
* Take a snapshot and return its id on success, or null on failure.
*
* @param {MemoryFront} front
* @returns {Number|null}
*/
const takeSnapshot = exports.takeSnapshot = function (front) {
return function *(dispatch, getState) {
if (getState().diffing) {
- dispatch(toggleDiffing());
+ dispatch(view.changeView(viewState.CENSUS));
}
- const snapshot = createSnapshot();
+ const snapshot = createSnapshot(getState());
const id = snapshot.id;
dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot });
dispatch(selectSnapshot(id));
let path;
try {
path = yield front.saveHeapSnapshot();
} catch (error) {
@@ -192,19 +203,285 @@ const refreshSelectedCensus = exports.re
// task action will follow through and ensure that a census is taken.
if (snapshot && snapshot.state === states.SAVED_CENSUS) {
yield dispatch(takeCensus(heapWorker, snapshot.id));
}
};
};
/**
+ * Request that the `HeapAnalysesWorker` compute the dominator tree for the
+ * snapshot with the given `id`.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ *
+ * @returns {Promise<DominatorTreeId>}
+ */
+const computeDominatorTree = exports.computeDominatorTree = function (heapWorker, id) {
+ return function*(dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(!(snapshot.dominatorTree && snapshot.dominatorTree.dominatorTreeId),
+ "Should not re-compute dominator trees");
+
+ dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_START, id });
+
+ let dominatorTreeId;
+ try {
+ dominatorTreeId = yield heapWorker.computeDominatorTree(snapshot.path);
+ } catch (error) {
+ reportException("actions/snapshot/computeDominatorTree", error);
+ dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
+ return null;
+ }
+
+ dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_END, id, dominatorTreeId });
+ return dominatorTreeId;
+ };
+};
+
+/**
+ * Get the partial subtree, starting from the root, of the
+ * snapshot-with-the-given-id's dominator tree.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ *
+ * @returns {Promise<DominatorTreeNode>}
+ */
+const fetchDominatorTree = exports.fetchDominatorTree = function (heapWorker, id) {
+ return function*(dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(dominatorTreeIsComputed(snapshot),
+ "Should have dominator tree model and it should be computed");
+
+ let breakdown;
+ let root;
+ do {
+ breakdown = getState().dominatorTreeBreakdown;
+ assert(breakdown, "Should have a breakdown to describe nodes with.");
+
+ dispatch({ type: actions.FETCH_DOMINATOR_TREE_START, id, breakdown });
+
+ try {
+ root = yield heapWorker.getDominatorTree({
+ dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
+ breakdown,
+ });
+ } catch (error) {
+ reportException("actions/snapshot/fetchDominatorTree", error);
+ dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
+ return null;
+ }
+ }
+ while (!breakdownEquals(breakdown, getState().dominatorTreeBreakdown));
+
+ dispatch({ type: actions.FETCH_DOMINATOR_TREE_END, id, root });
+ return root;
+ };
+};
+
+/**
+ * Fetch the immediately dominated children represented by the placeholder
+ * `lazyChildren` from snapshot-with-the-given-id's dominator tree.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ * @param {DominatorTreeLazyChildren} lazyChildren
+ */
+const fetchImmediatelyDominated = exports.fetchImmediatelyDominated = function (heapWorker, id, lazyChildren) {
+ return function*(dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ assert(snapshot.dominatorTree, "Should have dominator tree model");
+ assert(snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
+ snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
+ "Cannot fetch immediately dominated nodes in a dominator tree unless " +
+ " the dominator tree has already been computed");
+
+ let breakdown;
+ let response;
+ do {
+ breakdown = getState().dominatorTreeBreakdown;
+ assert(breakdown, "Should have a breakdown to describe nodes with.");
+
+ dispatch({ type: actions.FETCH_IMMEDIATELY_DOMINATED_START, id });
+
+ try {
+ response = yield heapWorker.getImmediatelyDominated({
+ dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
+ breakdown,
+ nodeId: lazyChildren.parentNodeId(),
+ startIndex: lazyChildren.siblingIndex(),
+ });
+ } catch (error) {
+ reportException("actions/snapshot/fetchImmediatelyDominated", error);
+ dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
+ return null;
+ }
+ }
+ while (!breakdownEquals(breakdown, getState().dominatorTreeBreakdown));
+
+ dispatch({
+ type: actions.FETCH_IMMEDIATELY_DOMINATED_END,
+ id,
+ path: response.path,
+ nodes: response.nodes,
+ moreChildrenAvailable: response.moreChildrenAvailable,
+ });
+ };
+};
+
+/**
+ * Compute and then fetch the dominator tree of the snapshot with the given
+ * `id`.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {SnapshotId} id
+ *
+ * @returns {Promise<DominatorTreeNode>}
+ */
+const computeAndFetchDominatorTree = exports.computeAndFetchDominatorTree = function (heapWorker, id) {
+ return function*(dispatch, getState) {
+ const dominatorTreeId = yield dispatch(computeDominatorTree(heapWorker, id));
+ if (dominatorTreeId === null) {
+ return null;
+ }
+
+ const root = yield dispatch(fetchDominatorTree(heapWorker, id));
+ if (!root) {
+ return null;
+ }
+
+ return root;
+ };
+};
+
+/**
+ * Update the currently selected snapshot's dominator tree.
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshSelectedDominatorTree = exports.refreshSelectedDominatorTree = function (heapWorker) {
+ return function*(dispatch, getState) {
+ let snapshot = getState().snapshots.find(s => s.selected);
+ if (!snapshot) {
+ return;
+ }
+
+ if (snapshot.dominatorTree &&
+ !(snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
+ snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
+ snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING)) {
+ return;
+ }
+
+ switch (snapshot.state) {
+ case states.READ:
+ case states.SAVING_CENSUS:
+ case states.SAVED_CENSUS:
+ if (snapshot.dominatorTree) {
+ yield dispatch(fetchDominatorTree(heapWorker, snapshot.id));
+ } else {
+ yield dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
+ }
+ return;
+
+ default:
+ // If there was an error, we can't continue. If we are still saving or
+ // reading the snapshot, then takeSnapshotAndCensus will finish the job
+ // for us.
+ return;
+ }
+ };
+};
+
+/**
* Select the snapshot with the given id.
*
* @param {snapshotId} id
* @see {Snapshot} model defined in devtools/client/memory/models.js
*/
const selectSnapshot = exports.selectSnapshot = function (id) {
return {
type: actions.SELECT_SNAPSHOT,
id
};
};
+
+/**
+ * Expand the given node in the snapshot's census report.
+ *
+ * @param {CensusTreeNode} node
+ */
+const expandCensusNode = exports.expandCensusNode = function (id, node) {
+ return {
+ type: actions.EXPAND_CENSUS_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Collapse the given node in the snapshot's census report.
+ *
+ * @param {CensusTreeNode} node
+ */
+const collapseCensusNode = exports.collapseCensusNode = function (id, node) {
+ return {
+ type: actions.COLLAPSE_CENSUS_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Focus the given node in the snapshot's census's report.
+ *
+ * @param {SnapshotId} id
+ * @param {DominatorTreeNode} node
+ */
+const focusCensusNode = exports.focusCensusNode = function (id, node) {
+ return {
+ type: actions.FOCUS_CENSUS_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Expand the given node in the snapshot's dominator tree.
+ *
+ * @param {DominatorTreeTreeNode} node
+ */
+const expandDominatorTreeNode = exports.expandDominatorTreeNode = function (id, node) {
+ return {
+ type: actions.EXPAND_DOMINATOR_TREE_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Collapse the given node in the snapshot's dominator tree.
+ *
+ * @param {DominatorTreeTreeNode} node
+ */
+const collapseDominatorTreeNode = exports.collapseDominatorTreeNode = function (id, node) {
+ return {
+ type: actions.COLLAPSE_DOMINATOR_TREE_NODE,
+ id,
+ node,
+ };
+};
+
+/**
+ * Focus the given node in the snapshot's dominator tree.
+ *
+ * @param {SnapshotId} id
+ * @param {DominatorTreeNode} node
+ */
+const focusDominatorTreeNode = exports.focusDominatorTreeNode = function (id, node) {
+ return {
+ type: actions.FOCUS_DOMINATOR_TREE_NODE,
+ id,
+ node,
+ };
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/view.js
@@ -0,0 +1,32 @@
+/* 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 { actions } = require("../constants");
+const refresh = require("./refresh");
+
+/**
+ * Change the currently selected view.
+ *
+ * @param {viewState} view
+ */
+const changeView = exports.changeView = function (view) {
+ return {
+ type: actions.CHANGE_VIEW,
+ view
+ };
+};
+
+/**
+ * Change the currently selected view and ensure all our data is up to date from
+ * the heap worker.
+ *
+ * @param {viewState} view
+ */
+exports.changeViewAndRefresh = function (view, heapWorker) {
+ return function* (dispatch, getState) {
+ dispatch(changeView(view));
+ yield dispatch(refresh.refresh(heapWorker));
+ };
+};
--- a/devtools/client/memory/app.js
+++ b/devtools/client/memory/app.js
@@ -1,31 +1,55 @@
/* 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 { assert } = require("devtools/shared/DevToolsUtils");
const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { breakdowns, diffingState } = require("./constants");
+const { breakdowns, diffingState, viewState } = require("./constants");
const { toggleRecordingAllocationStacks } = require("./actions/allocations");
const { setBreakdownAndRefresh } = require("./actions/breakdown");
-const { selectSnapshotForDiffingAndRefresh, toggleDiffing } = require("./actions/diffing");
+const { setDominatorTreeBreakdownAndRefresh } = require("./actions/dominatorTreeBreakdown");
+const {
+ selectSnapshotForDiffingAndRefresh,
+ toggleDiffing,
+ expandDiffingCensusNode,
+ collapseDiffingCensusNode,
+ focusDiffingCensusNode,
+} = require("./actions/diffing");
const { toggleInvertedAndRefresh } = require("./actions/inverted");
const { setFilterStringAndRefresh } = require("./actions/filter");
const { pickFileAndExportSnapshot, pickFileAndImportSnapshotAndCensus } = require("./actions/io");
-const { selectSnapshotAndRefresh, takeSnapshotAndCensus } = require("./actions/snapshot");
-const { breakdownNameToSpec, getBreakdownDisplayData } = require("./utils");
+const {
+ selectSnapshotAndRefresh,
+ takeSnapshotAndCensus,
+ fetchImmediatelyDominated,
+ expandCensusNode,
+ collapseCensusNode,
+ focusCensusNode,
+ expandDominatorTreeNode,
+ collapseDominatorTreeNode,
+ focusDominatorTreeNode,
+} = require("./actions/snapshot");
+const { changeViewAndRefresh } = require("./actions/view");
+const {
+ breakdownNameToSpec,
+ getBreakdownDisplayData,
+ dominatorTreeBreakdownNameToSpec,
+ getDominatorTreeBreakdownDisplayData,
+} = require("./utils");
const Toolbar = createFactory(require("./components/toolbar"));
const List = createFactory(require("./components/list"));
const SnapshotListItem = createFactory(require("./components/snapshot-list-item"));
-const HeapView = createFactory(require("./components/heap"));
+const Heap = createFactory(require("./components/heap"));
const { app: appModel } = require("./models");
-const App = createClass({
- displayName: "memory-tool",
+const MemoryApp = createClass({
+ displayName: "MemoryApp",
propTypes: appModel,
getDefaultProps() {
return {};
},
childContextTypes: {
@@ -34,41 +58,46 @@ const App = createClass({
toolbox: PropTypes.any,
},
getChildContext() {
return {
front: this.props.front,
heapWorker: this.props.heapWorker,
toolbox: this.props.toolbox,
- }
+ };
},
render() {
let {
dispatch,
snapshots,
front,
heapWorker,
breakdown,
allocations,
inverted,
toolbox,
filter,
- diffing
+ diffing,
+ view,
+ dominatorTreeBreakdown
} = this.props;
const selectedSnapshot = snapshots.find(s => s.selected);
const onClickSnapshotListItem = diffing && diffing.state === diffingState.SELECTING
? snapshot => dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, snapshot))
: snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot.id));
return (
- dom.div({ id: "memory-tool" },
+ dom.div(
+ {
+ id: "memory-tool"
+ },
Toolbar({
snapshots,
breakdowns: getBreakdownDisplayData(),
onImportClick: () => dispatch(pickFileAndImportSnapshotAndCensus(heapWorker)),
onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)),
onBreakdownChange: breakdown =>
dispatch(setBreakdownAndRefresh(heapWorker, breakdownNameToSpec(breakdown))),
@@ -77,41 +106,116 @@ const App = createClass({
allocations,
inverted,
onToggleInverted: () =>
dispatch(toggleInvertedAndRefresh(heapWorker)),
filter,
setFilterString: filterString =>
dispatch(setFilterStringAndRefresh(filterString, heapWorker)),
diffing,
- onToggleDiffing: () => dispatch(toggleDiffing())
+ onToggleDiffing: () => dispatch(toggleDiffing()),
+ view,
+ dominatorTreeBreakdowns: getDominatorTreeBreakdownDisplayData(),
+ onDominatorTreeBreakdownChange: breakdown => {
+ const spec = dominatorTreeBreakdownNameToSpec(breakdown);
+ assert(spec, "Should have a breakdown spec");
+ dispatch(setDominatorTreeBreakdownAndRefresh(heapWorker, spec));
+ },
+ onViewChange: v => dispatch(changeViewAndRefresh(v, heapWorker)),
}),
- dom.div({ id: "memory-tool-container" },
+ dom.div(
+ {
+ id: "memory-tool-container"
+ },
+
List({
itemComponent: SnapshotListItem,
items: snapshots,
onSave: snapshot => dispatch(pickFileAndExportSnapshot(snapshot)),
onClick: onClickSnapshotListItem,
diffing,
}),
- HeapView({
+ Heap({
snapshot: selectedSnapshot,
diffing,
- onSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)),
onViewSourceInDebugger: frame => toolbox.viewSourceInDebugger(frame.source, frame.line),
+ onSnapshotClick: () =>
+ dispatch(takeSnapshotAndCensus(front, heapWorker)),
+ onLoadMoreSiblings: lazyChildren =>
+ dispatch(fetchImmediatelyDominated(heapWorker,
+ selectedSnapshot.id,
+ lazyChildren)),
+ onCensusExpand: (census, node) => {
+ if (diffing) {
+ assert(diffing.census === census,
+ "Should only expand active census");
+ dispatch(expandDiffingCensusNode(node));
+ } else {
+ assert(selectedSnapshot && selectedSnapshot.census === census,
+ "If not diffing, should be expanding on selected snapshot's census");
+ dispatch(expandCensusNode(selectedSnapshot.id, node));
+ }
+ },
+ onCensusCollapse: (census, node) => {
+ if (diffing) {
+ assert(diffing.census === census,
+ "Should only collapse active census");
+ dispatch(collapseDiffingCensusNode(node));
+ } else {
+ assert(selectedSnapshot && selectedSnapshot.census === census,
+ "If not diffing, should be collapsing on selected snapshot's census");
+ dispatch(collapseCensusNode(selectedSnapshot.id, node));
+ }
+ },
+ onCensusFocus: (census, node) => {
+ if (diffing) {
+ assert(diffing.census === census,
+ "Should only focus nodes in active census");
+ dispatch(focusDiffingCensusNode(node));
+ } else {
+ assert(selectedSnapshot && selectedSnapshot.census === census,
+ "If not diffing, should be focusing on nodes in selected snapshot's census");
+ dispatch(focusCensusNode(selectedSnapshot.id, node));
+ }
+ },
+ onDominatorTreeExpand: node => {
+ assert(view === viewState.DOMINATOR_TREE,
+ "If expanding dominator tree nodes, should be in dominator tree view");
+ assert(selectedSnapshot, "...and we should have a selected snapshot");
+ assert(selectedSnapshot.dominatorTree,
+ "...and that snapshot should have a dominator tree");
+ dispatch(expandDominatorTreeNode(selectedSnapshot.id, node));
+ },
+ onDominatorTreeCollapse: node => {
+ assert(view === viewState.DOMINATOR_TREE,
+ "If collapsing dominator tree nodes, should be in dominator tree view");
+ assert(selectedSnapshot, "...and we should have a selected snapshot");
+ assert(selectedSnapshot.dominatorTree,
+ "...and that snapshot should have a dominator tree");
+ dispatch(collapseDominatorTreeNode(selectedSnapshot.id, node));
+ },
+ onDominatorTreeFocus: node => {
+ assert(view === viewState.DOMINATOR_TREE,
+ "If focusing dominator tree nodes, should be in dominator tree view");
+ assert(selectedSnapshot, "...and we should have a selected snapshot");
+ assert(selectedSnapshot.dominatorTree,
+ "...and that snapshot should have a dominator tree");
+ dispatch(focusDominatorTreeNode(selectedSnapshot.id, node));
+ },
+ view,
})
)
)
);
},
});
/**
* Passed into react-redux's `connect` method that is called on store change
* and passed to components.
*/
function mapStateToProps (state) {
return state;
}
-module.exports = connect(mapStateToProps)(App);
+module.exports = connect(mapStateToProps)(MemoryApp);
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/census-header.js
@@ -0,0 +1,35 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+
+const CensusHeader = module.exports = createClass({
+ displayName: "CensusHeader",
+
+ propTypes: { },
+
+ render() {
+ return dom.div(
+ {
+ className: "header"
+ },
+
+ dom.span({ className: "heap-tree-item-bytes" },
+ L10N.getStr("heapview.field.bytes")),
+
+ dom.span({ className: "heap-tree-item-count" },
+ L10N.getStr("heapview.field.count")),
+
+ dom.span({ className: "heap-tree-item-total-bytes" },
+ L10N.getStr("heapview.field.totalbytes")),
+
+ dom.span({ className: "heap-tree-item-total-count" },
+ L10N.getStr("heapview.field.totalcount")),
+
+ dom.span({ className: "heap-tree-item-name" },
+ L10N.getStr("heapview.field.name"))
+ );
+ }
+});
rename from devtools/client/memory/components/tree-item.js
rename to devtools/client/memory/components/census-tree-item.js
--- a/devtools/client/memory/components/tree-item.js
+++ b/devtools/client/memory/components/census-tree-item.js
@@ -1,28 +1,21 @@
/* 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 { isSavedFrame } = require("devtools/shared/DevToolsUtils");
-const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const { DOM: dom, createClass, createFactory } = require("devtools/client/shared/vendor/react");
const { L10N } = require("../utils");
-const FrameView = createFactory(require("devtools/client/shared/components/frame"));
+const Frame = createFactory(require("devtools/client/shared/components/frame"));
const unknownSourceString = L10N.getStr("unknownSource");
-
-const INDENT = 10;
-const MAX_SOURCE_LENGTH = 200;
+const { TREE_ROW_HEIGHT } = require("../constants");
-
-/**
- * An arrow that displays whether its node is expanded (â–¼) or collapsed
- * (â–¶). When its node has no children, it is hidden.
- */
-const TreeItem = module.exports = createClass({
- displayName: "tree-item",
+const CensusTreeItem = module.exports = createClass({
+ displayName: "CensusTreeItem",
formatPercent(showSign, percent) {
return L10N.getFormatStr("tree-item.percent",
this.formatNumber(showSign, percent));
},
formatNumber(showSign, number) {
const rounded = Math.round(number);
@@ -79,28 +72,29 @@ const TreeItem = module.exports = create
dom.span({ className: "heap-tree-number" }, count),
dom.span({ className: "heap-tree-percent" }, percentCount)),
dom.span({ className: "heap-tree-item-field heap-tree-item-total-bytes" },
dom.span({ className: "heap-tree-number" }, totalBytes),
dom.span({ className: "heap-tree-percent" }, percentTotalBytes)),
dom.span({ className: "heap-tree-item-field heap-tree-item-total-count" },
dom.span({ className: "heap-tree-number" }, totalCount),
dom.span({ className: "heap-tree-percent" }, percentTotalCount)),
- dom.span({ className: "heap-tree-item-field heap-tree-item-name", style: { marginLeft: depth * INDENT }},
+ dom.span({ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginLeft: depth * TREE_ROW_HEIGHT }},
arrow,
this.toLabel(item.name, onViewSourceInDebugger)
)
);
},
toLabel(name, linkToDebugger) {
if (isSavedFrame(name)) {
let onClickTooltipString =
L10N.getFormatStr("viewsourceindebugger",`${name.source}:${name.line}:${name.column}`);
- return FrameView({
+ return Frame({
frame: name,
onClick: () => linkToDebugger(name),
onClickTooltipString,
unknownSourceString
});
}
if (name === null) {
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/census.js
@@ -0,0 +1,75 @@
+/* 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 Tree = createFactory(require("devtools/client/shared/components/tree"));
+const CensusTreeItem = createFactory(require("./census-tree-item"));
+const { createParentMap } = require("../utils");
+const { TREE_ROW_HEIGHT } = require("../constants");
+const { censusModel, diffingModel } = require("../models");
+
+const Census = module.exports = createClass({
+ displayName: "Census",
+
+ propTypes: {
+ census: censusModel,
+ onExpand: PropTypes.func.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ diffing: diffingModel,
+ },
+
+ render() {
+ let {
+ census,
+ onExpand,
+ onCollapse,
+ onFocus,
+ diffing,
+ onViewSourceInDebugger,
+ } = this.props;
+
+ const report = census.report;
+ let parentMap = createParentMap(report);
+ const { totalBytes, totalCount } = report;
+
+ const getPercentBytes = totalBytes === 0
+ ? _ => 0
+ : bytes => (bytes / totalBytes) * 100;
+
+ const getPercentCount = totalCount === 0
+ ? _ => 0
+ : count => (count / totalCount) * 100;
+
+ return Tree({
+ autoExpandDepth: 0,
+ focused: census.focused,
+ getParent: node => {
+ const parent = parentMap[node.id];
+ return parent === report ? null : parent;
+ },
+ getChildren: node => node.children || [],
+ isExpanded: node => census.expanded.has(node.id),
+ onExpand,
+ onCollapse,
+ onFocus,
+ renderItem: (item, depth, focused, arrow, expanded) =>
+ new CensusTreeItem({
+ onViewSourceInDebugger,
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ getPercentBytes,
+ getPercentCount,
+ showSign: !!diffing,
+ }),
+ getRoots: () => report.children || [],
+ getKey: node => node.id,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/dominator-tree-header.js
@@ -0,0 +1,29 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+
+const DominatorTreeHeader = module.exports = createClass({
+ displayName: "DominatorTreeHeader",
+
+ propTypes: { },
+
+ render() {
+ return dom.div(
+ {
+ className: "header"
+ },
+
+ dom.span({ className: "heap-tree-item-bytes" },
+ L10N.getStr("heapview.field.retainedSize")),
+
+ dom.span({ className: "heap-tree-item-bytes" },
+ L10N.getStr("heapview.field.shallowSize")),
+
+ dom.span({ className: "heap-tree-item-name" },
+ L10N.getStr("heapview.field.name"))
+ );
+ }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/dominator-tree-item.js
@@ -0,0 +1,148 @@
+/* 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 { assert, isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+const Frame = createFactory(require("devtools/client/shared/components/frame"));
+const { TREE_ROW_HEIGHT } = require("../constants");
+
+const Separator = createFactory(createClass({
+ displayName: "Separator",
+
+ render() {
+ return dom.span({ className: "separator" }, "›");
+ }
+}));
+
+const DominatorTreeItem = module.exports = createClass({
+ displayName: "DominatorTreeItem",
+
+ propTypes: {
+ item: PropTypes.object.isRequired,
+ depth: PropTypes.number.isRequired,
+ arrow: PropTypes.object.isRequired,
+ focused: PropTypes.bool.isRequired,
+ getPercentSize: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ },
+
+ formatPercent(percent) {
+ return L10N.getFormatStr("tree-item.percent",
+ this.formatNumber(percent));
+ },
+
+ formatNumber(number) {
+ const rounded = Math.round(number);
+ if (rounded === 0 || rounded === -0) {
+ return "0";
+ }
+
+ return String(Math.abs(rounded));
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.item != nextProps.item
+ || this.props.depth != nextProps.depth
+ || this.props.focused != nextProps.focused;
+ },
+
+ render() {
+ let {
+ item,
+ depth,
+ arrow,
+ focused,
+ getPercentSize,
+ onViewSourceInDebugger,
+ } = this.props;
+
+ const retainedSize = this.formatNumber(item.retainedSize);
+ const percentRetainedSize = this.formatPercent(getPercentSize(item.retainedSize));
+
+ const shallowSize = this.formatNumber(item.shallowSize);
+ const percentShallowSize = this.formatPercent(getPercentSize(item.shallowSize));
+
+ // Build up our label UI as an array of each label piece, which is either a
+ // string or a frame, and separators in between them.
+
+ assert(item.label.length > 0,
+ "Our label should not be empty");
+ const label = Array(item.label.length * 2 - 1);
+ label.fill(undefined);
+
+ for (let i = 0, length = item.label.length; i < length; i++) {
+ const piece = item.label[i];
+ const key = `${item.nodeId}-label-${i}`;
+
+ // `i` is the index of the label piece we are rendering, `label[i*2]` is
+ // where the rendered label piece belngs, and `label[i*2+1]` (if it isn't
+ // out of bounds) is where the separator belongs.
+
+ if (isSavedFrame(piece)) {
+ label[i * 2] = Frame({
+ key,
+ onClick: () => onViewSourceInDebugger(piece),
+ frame: piece
+ });
+ } else if (piece === "noStack") {
+ label[i * 2] = dom.span({ key, className: "not-available" },
+ L10N.getStr("tree-item.nostack"));
+ } else if (piece === "noFilename") {
+ label[i * 2] = dom.span({ key, className: "not-available" },
+ L10N.getStr("tree-item.nofilename"));
+ } else {
+ label[i * 2] = piece;
+ }
+
+ // If this is not the last piece of the label, add a separator.
+ if (i < length - 1) {
+ label[i * 2 + 1] = Separator({ key: `${item.nodeId}-separator-${i}` });
+ }
+ }
+
+ return dom.div(
+ {
+ className: `heap-tree-item ${focused ? "focused" : ""} node-${item.nodeId}`
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-bytes"
+ },
+ dom.span(
+ {
+ className: "heap-tree-number"
+ },
+ retainedSize
+ ),
+ dom.span({ className: "heap-tree-percent" }, percentRetainedSize)
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-bytes"
+ },
+ dom.span(
+ {
+ className: "heap-tree-number"
+ },
+ shallowSize
+ ),
+ dom.span({ className: "heap-tree-percent" }, percentShallowSize)
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginLeft: depth * TREE_ROW_HEIGHT }
+ },
+ arrow,
+ label,
+ dom.span({ className: "heap-tree-item-address" },
+ `@ 0x${item.nodeId.toString(16)}`)
+ )
+ );
+ },
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/dominator-tree.js
@@ -0,0 +1,217 @@
+/* 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 Tree = createFactory(require("devtools/client/shared/components/tree"));
+const DominatorTreeItem = createFactory(require("./dominator-tree-item"));
+const { createParentMap, L10N } = require("../utils");
+const { TREE_ROW_HEIGHT, dominatorTreeState } = require("../constants");
+const { dominatorTreeModel } = require("../models");
+const DominatorTreeLazyChildren = require("../dominator-tree-lazy-children");
+
+const DOMINATOR_TREE_AUTO_EXPAND_DEPTH = 3;
+
+/**
+ * A throbber that represents a subtree in the dominator tree that is actively
+ * being incrementally loaded and fetched from the `HeapAnalysesWorker`.
+ */
+const DominatorTreeSubtreeFetching = createFactory(createClass({
+ displayName: "DominatorTreeSubtreeFetching",
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.depth !== nextProps.depth
+ || this.props.focused !== nextProps.focused;
+ },
+
+ propTypes: {
+ depth: PropTypes.number.isRequired,
+ focused: PropTypes.bool.isRequired,
+ },
+
+ render() {
+ let {
+ depth,
+ focused,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: `heap-tree-item subtree-fetching ${focused ? "focused" : ""}`
+ },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({
+ className: "heap-tree-item-field heap-tree-item-name devtools-throbber",
+ style: { marginLeft: depth * TREE_ROW_HEIGHT }
+ })
+ );
+ }
+}));
+
+/**
+ * A link to fetch and load more siblings in the dominator tree, when there are
+ * already many loaded above.
+ */
+const DominatorTreeSiblingLink = createFactory(createClass({
+ displayName: "DominatorTreeSiblingLink",
+
+ propTypes: {
+ depth: PropTypes.number.isRequired,
+ focused: PropTypes.bool.isRequired,
+ item: PropTypes.instanceOf(DominatorTreeLazyChildren).isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.depth !== nextProps.depth
+ || this.props.focused !== nextProps.focused;
+ },
+
+ render() {
+ let {
+ depth,
+ focused,
+ item,
+ onLoadMoreSiblings,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: `heap-tree-item more-children ${focused ? "focused" : ""}`
+ },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginLeft: depth * TREE_ROW_HEIGHT }
+ },
+ dom.a(
+ {
+ onClick: () => onLoadMoreSiblings(item)
+ },
+ L10N.getStr("tree-item.load-more")
+ )
+ )
+ );
+ }
+}));
+
+/**
+ * The actual dominator tree rendered as an expandable and collapsible tree.
+ */
+const DominatorTree = module.exports = createClass({
+ displayName: "DominatorTree",
+
+ propTypes: {
+ dominatorTree: dominatorTreeModel.isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onExpand: PropTypes.func.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ // Safe to use referential equality here because all of our mutations on
+ // dominator tree models use immutableUpdate in a persistent manner. The
+ // exception to the rule are mutations of the expanded set, however we take
+ // care that the dominatorTree model itself is still re-allocated when
+ // mutations to the expanded set occur. Because of the re-allocations, we
+ // can continue using referential equality here.
+ return this.props.dominatorTree !== nextProps.dominatorTree;
+ },
+
+ render() {
+ const { dominatorTree, onViewSourceInDebugger, onLoadMoreSiblings } = this.props;
+
+ const parentMap = createParentMap(dominatorTree.root, node => node.nodeId);
+
+ return Tree({
+ key: "dominator-tree-tree",
+ autoExpandDepth: DOMINATOR_TREE_AUTO_EXPAND_DEPTH,
+ focused: dominatorTree.focused,
+ getParent: node =>
+ node instanceof DominatorTreeLazyChildren
+ ? parentMap[node.parentNodeId()]
+ : parentMap[node.nodeId],
+ getChildren: node => {
+ const children = node.children ? node.children.slice() : [];
+ if (node.moreChildrenAvailable) {
+ children.push(new DominatorTreeLazyChildren(node.nodeId, children.length));
+ }
+ return children;
+ },
+ isExpanded: node => {
+ return node instanceof DominatorTreeLazyChildren
+ ? false
+ : dominatorTree.expanded.has(node.nodeId)
+ },
+ onExpand: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ if (item.moreChildrenAvailable && (!item.children || !item.children.length)) {
+ const startIndex = item.children ? item.children.length : 0;
+ onLoadMoreSiblings(new DominatorTreeLazyChildren(item.nodeId, startIndex));
+ }
+
+ this.props.onExpand(item);
+ },
+ onCollapse: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ this.props.onCollapse(item);
+ },
+ onFocus: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ this.props.onFocus(item);
+ },
+ renderItem: (item, depth, focused, arrow, expanded) => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ if (item.isFirstChild()) {
+ assert(dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
+ "If we are displaying a throbber for loading a subtree, " +
+ "then we should be INCREMENTAL_FETCHING those children right now");
+ return DominatorTreeSubtreeFetching({
+ key: item.key(),
+ depth,
+ focused,
+ });
+ }
+
+ return DominatorTreeSiblingLink({
+ key: item.key(),
+ item,
+ depth,
+ focused,
+ onLoadMoreSiblings,
+ });
+ }
+
+ return DominatorTreeItem({
+ item,
+ depth,
+ focused,
+ arrow,
+ getPercentSize: size => (size / dominatorTree.root.retainedSize) * 100,
+ onViewSourceInDebugger,
+ })
+ },
+ getRoots: () => [dominatorTree.root],
+ getKey: node =>
+ node instanceof DominatorTreeLazyChildren ? node.key() : node.nodeId,
+ itemHeight: TREE_ROW_HEIGHT,
+ // We can't cache traversals because incremental fetching of children
+ // means the traversal might not be valid.
+ reuseCachedTraversal: _ => false,
+ });
+ }
+});
--- a/devtools/client/memory/components/heap.js
+++ b/devtools/client/memory/components/heap.js
@@ -1,203 +1,291 @@
/* 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 Tree = createFactory(require("devtools/client/shared/components/tree"));
-const TreeItem = createFactory(require("./tree-item"));
+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 { getStatusTextFull, L10N } = require("../utils");
-const { snapshotState: states, diffingState } = require("../constants");
+const { snapshotState: states, diffingState, viewState, dominatorTreeState } = require("../constants");
const { snapshot: snapshotModel, diffingModel } = require("../models");
-// If HEAP_TREE_ROW_HEIGHT changes, be sure to change `var(--heap-tree-row-height)`
-// in `devtools/client/themes/memory.css`
-const HEAP_TREE_ROW_HEIGHT = 14;
/**
- * Creates a hash map mapping node IDs to its parent node.
+ * Get the app state's current state atom.
+ *
+ * @see the relevant state string constants in `../constants.js`.
*
- * @param {CensusTreeNode} node
- * @param {Object<number, CensusTreeNode>} aggregator
+ * @param {viewState} view
+ * @param {snapshotModel} snapshot
+ * @param {diffingModel} diffing
*
- * @return {Object<number, CensusTreeNode>}
+ * @return {snapshotState|diffingState|dominatorTreeState}
*/
-function createParentMap (node, aggregator=Object.create(null)) {
- for (let child of (node.children || [])) {
- aggregator[child.id] = node;
- createParentMap(child, aggregator);
+function getState(view, snapshot, diffing) {
+ switch (view) {
+ case viewState.CENSUS:
+ return snapshot.state;
+
+ case viewState.DIFFING:
+ return diffing.state;
+
+ case viewState.DOMINATOR_TREE:
+ return snapshot.dominatorTree
+ ? snapshot.dominatorTree.state
+ : snapshot.state;
}
- return aggregator;
+ assert(false, `Unexpected view state: ${view}`);
+ 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 {viewState} 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 states.READ:
+ case states.SAVING_CENSUS:
+ case diffingState.SELECTING:
+ case diffingState.TAKING_DIFF:
+ case dominatorTreeState.COMPUTING:
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ return true;
+ }
+ return view === viewState.DOMINATOR_TREE && !snapshot.dominatorTree;
}
/**
- * Creates properties to be passed into the Tree component.
+ * Get the status text to display for the given state.
*
- * @param {censusModel} census
- * @param {Function} onViewSourceInDebugger
- * @param {Object} diffing
- * @return {Object}
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {diffingModel} diffing
+ *
+ * @returns {String}
*/
-function createTreeProperties(census, onViewSourceInDebugger, diffing) {
- const report = census.report;
- let map = createParentMap(report);
- const { totalBytes, totalCount } = report;
-
- const getPercentBytes = totalBytes === 0
- ? _ => 0
- : bytes => (bytes / totalBytes) * 100;
-
- const getPercentCount = totalCount === 0
- ? _ => 0
- : count => (count / totalCount) * 100;
+function getStateStatusText(state, diffing) {
+ if (state === diffingState.SELECTING) {
+ return L10N.getStr(diffing.firstSnapshotId === null
+ ? "diffing.prompt.selectBaseline"
+ : "diffing.prompt.selectComparison");
+ }
- return {
- autoExpandDepth: 0,
- getParent: node => {
- const parent = map[node.id];
- return parent === report ? null : parent;
- },
- getChildren: node => node.children || [],
- renderItem: (item, depth, focused, arrow, expanded) =>
- new TreeItem({
- onViewSourceInDebugger,
- item,
- depth,
- focused,
- arrow,
- expanded,
- getPercentBytes,
- getPercentCount,
- showSign: !!diffing,
- }),
- getRoots: () => report.children || [],
- getKey: node => node.id,
- itemHeight: HEAP_TREE_ROW_HEIGHT,
- // Because we never add or remove children when viewing the same census, we
- // can always reuse a cached traversal if one is available.
- reuseCachedTraversal: _ => true,
- };
+ 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;
}
/**
- * Main view for the memory tool -- contains several panels for different states;
- * an initial state of only a button to take a snapshot, loading states, and the
- * heap view tree.
+ * Get the current state's error, or return null if there is none.
+ *
+ * @param {snapshotModel} snapshot
+ * @param {diffingModel} diffing
+ *
+ * @returns {Error|null}
*/
+function getError(snapshot, diffing) {
+ if (diffing && diffing.state === diffingState.ERROR) {
+ return diffing.error;
+ }
+ if (snapshot) {
+ if (snapshot.state === states.ERROR) {
+ return snapshot.error;
+ }
+
+ if (snapshot.dominatorTree &&
+ snapshot.dominatorTree.state === dominatorTreeState.ERROR) {
+ return snapshot.dominatorTree.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-view",
+ 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,
snapshot: snapshotModel,
onViewSourceInDebugger: PropTypes.func.isRequired,
diffing: diffingModel,
+ view: PropTypes.string.isRequired,
},
render() {
- let { snapshot, diffing, onSnapshotClick, onViewSourceInDebugger } = this.props;
+ let {
+ snapshot,
+ diffing,
+ onSnapshotClick,
+ onLoadMoreSiblings,
+ onViewSourceInDebugger,
+ view,
+ } = this.props;
+
+ if (!diffing && !snapshot) {
+ return this._renderInitial(onSnapshotClick);
+ }
+
+ const state = getState(view, snapshot, diffing);
+ const statusText = getStateStatusText(state, diffing);
- let census;
- let state;
- let statusText;
- let error;
- if (diffing) {
- census = diffing.census;
- state = diffing.state;
+ if (shouldDisplayStatus(state, view, snapshot)) {
+ return this._renderStatus(state, statusText, diffing);
+ }
- if (diffing.state === diffingState.SELECTING) {
- statusText = L10N.getStr(diffing.firstSnapshotId === null
- ? "diffing.prompt.selectBaseline"
- : "diffing.prompt.selectComparison");
- } else {
- statusText = getStatusTextFull(diffing.state);
- }
+ const error = getError(snapshot, diffing);
+ if (error) {
+ return this._renderError(state, statusText, error);
+ }
+
+ if (view === viewState.CENSUS || view === viewState.DIFFING) {
+ const census = view === viewState.CENSUS
+ ? snapshot.census
+ : diffing.census;
+ return this._renderCensus(state, census, diffing, onViewSourceInDebugger);
+ }
- if (diffing.error) {
- error = diffing.error;
- }
- } else {
- census = snapshot ? snapshot.census : null;
- state = snapshot ? snapshot.state : "initial";
- statusText = snapshot ? getStatusTextFull(snapshot.state) : "";
- if (snapshot && snapshot.error) {
- error = snapshot.error;
- }
- }
- assert(census !== undefined, "census should have been set");
- assert(state !== undefined, "state should have been set");
- assert(statusText !== undefined, "statusText should have been set");
+ assert(view === 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);
+ },
- let content;
- switch (state) {
- case "initial":
- content = [dom.button({
- className: "devtools-toolbarbutton take-snapshot",
- onClick: onSnapshotClick,
- // Want to use the [standalone] tag to leverage our styles,
- // but React hates that evidently
- "data-standalone": true,
- "data-text-only": true,
- }, L10N.getStr("take-snapshot"))];
- break;
+ /**
+ * 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
+ )
+ );
+ },
- case diffingState.ERROR:
- case states.ERROR:
- content = [
- dom.span({ className: "snapshot-status error" }, statusText),
- dom.pre({}, safeErrorString(error))
- ];
- break;
+ _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")
+ ));
+ },
- case diffingState.SELECTING:
- case diffingState.TAKING_DIFF:
- case states.IMPORTING:
- case states.SAVING:
- case states.SAVED:
- case states.READING:
- case states.READ:
- case states.SAVING_CENSUS:
- const throbber = state === diffingState.SELECTING
- ? ""
- : "devtools-throbber";
- content = [dom.span({ className: `snapshot-status ${throbber}` },
- statusText)];
- break;
+ _renderStatus(state, statusText, diffing) {
+ let throbber = "";
+ if (shouldDisplayThrobber(diffing)) {
+ throbber = "devtools-throbber";
+ }
- case diffingState.TOOK_DIFF:
- case states.SAVED_CENSUS:
- content = [];
+ 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))
+ );
+ },
- if (census.breakdown.by === "allocationStack"
- && census.report.children.length === 1
- && census.report.children[0].name === "noStack") {
- content.push(dom.div({ className: "error no-allocation-stacks" },
- L10N.getStr("heapview.noAllocationStacks")));
- }
+ _renderCensus(state, census, diffing, onViewSourceInDebugger) {
+ const contents = [];
+
+ if (census.breakdown.by === "allocationStack"
+ && census.report.children.length === 1
+ && census.report.children[0].name === "noStack") {
+ contents.push(dom.div({ className: "error no-allocation-stacks" },
+ L10N.getStr("heapview.noAllocationStacks")));
+ }
- content.push(
- dom.div({ className: "header" },
- dom.span({ className: "heap-tree-item-bytes" }, L10N.getStr("heapview.field.bytes")),
- dom.span({ className: "heap-tree-item-count" }, L10N.getStr("heapview.field.count")),
- dom.span({ className: "heap-tree-item-total-bytes" }, L10N.getStr("heapview.field.totalbytes")),
- dom.span({ className: "heap-tree-item-total-count" }, L10N.getStr("heapview.field.totalcount")),
- dom.span({ className: "heap-tree-item-name" }, L10N.getStr("heapview.field.name"))
- ),
- Tree(createTreeProperties(census, onViewSourceInDebugger, diffing))
- );
- break;
+ contents.push(CensusHeader());
+ contents.push(Census({
+ onViewSourceInDebugger,
+ 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);
+ },
- default:
- assert(false, "Unexpected state: ${state}");
- }
- assert(!!content, "Should have set content in the above switch block");
-
- let pane = dom.div({ className: "heap-view-panel", "data-state": state },
- ...content);
-
- return (
- dom.div({ id: "heap-view", "data-state": state }, pane)
+ _renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
+ return this._renderHeapView(
+ state,
+ DominatorTreeHeader(),
+ DominatorTree({
+ onViewSourceInDebugger,
+ dominatorTree,
+ onLoadMoreSiblings,
+ onExpand: this.props.onDominatorTreeExpand,
+ onCollapse: this.props.onDominatorTreeCollapse,
+ onFocus: this.props.onDominatorTreeFocus,
+ })
);
- }
+ },
});
--- a/devtools/client/memory/components/list.js
+++ b/devtools/client/memory/components/list.js
@@ -5,17 +5,17 @@
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
/**
* Generic list component that takes another react component to represent
* the children nodes as `itemComponent`, and a list of items to render
* as that component with a click handler.
*/
const List = module.exports = createClass({
- displayName: "list",
+ displayName: "List",
propTypes: {
itemComponent: PropTypes.any.isRequired,
onClick: PropTypes.func,
items: PropTypes.array.isRequired,
},
render() {
--- a/devtools/client/memory/components/moz.build
+++ b/devtools/client/memory/components/moz.build
@@ -1,12 +1,17 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
+ 'census-header.js',
+ 'census-tree-item.js',
+ 'census.js',
+ 'dominator-tree-header.js',
+ 'dominator-tree-item.js',
+ 'dominator-tree.js',
'heap.js',
'list.js',
'snapshot-list-item.js',
'toolbar.js',
- 'tree-item.js',
)
--- a/devtools/client/memory/components/snapshot-list-item.js
+++ b/devtools/client/memory/components/snapshot-list-item.js
@@ -10,17 +10,17 @@ const {
getSnapshotTotals,
getStatusText,
snapshotIsDiffable
} = require("../utils");
const { snapshotState: states, diffingState } = require("../constants");
const { snapshot: snapshotModel } = require("../models");
const SnapshotListItem = module.exports = createClass({
- displayName: "snapshot-list-item",
+ displayName: "SnapshotListItem",
propTypes: {
onClick: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
item: snapshotModel.isRequired,
index: PropTypes.number.isRequired,
},
--- a/devtools/client/memory/components/toolbar.js
+++ b/devtools/client/memory/components/toolbar.js
@@ -1,118 +1,213 @@
/* 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 { assert } = require("devtools/shared/DevToolsUtils");
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
const { L10N } = require("../utils");
const models = require("../models");
+const { viewState } = require("../constants");
const Toolbar = module.exports = createClass({
- displayName: "toolbar",
+ displayName: "Toolbar",
propTypes: {
breakdowns: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
})).isRequired,
onTakeSnapshotClick: PropTypes.func.isRequired,
onImportClick: PropTypes.func.isRequired,
onBreakdownChange: PropTypes.func.isRequired,
onToggleRecordAllocationStacks: PropTypes.func.isRequired,
allocations: models.allocations,
onToggleInverted: PropTypes.func.isRequired,
inverted: PropTypes.bool.isRequired,
filterString: PropTypes.string,
setFilterString: PropTypes.func.isRequired,
diffing: models.diffingModel,
onToggleDiffing: PropTypes.func.isRequired,
+ view: PropTypes.string.isRequired,
+ onViewChange: PropTypes.func.isRequired,
+ dominatorTreeBreakdowns: PropTypes.arrayOf(PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ displayName: PropTypes.string.isRequired,
+ })).isRequired,
+ onDominatorTreeBreakdownChange: PropTypes.func.isRequired,
+ snapshots: PropTypes.arrayOf(models.snapshot).isRequired,
},
render() {
let {
onTakeSnapshotClick,
onImportClick,
onBreakdownChange,
breakdowns,
+ dominatorTreeBreakdowns,
+ onDominatorTreeBreakdownChange,
onToggleRecordAllocationStacks,
allocations,
onToggleInverted,
inverted,
filterString,
setFilterString,
snapshots,
diffing,
onToggleDiffing,
+ view,
+ onViewChange,
} = this.props;
+ let viewToolbarOptions;
+ if (view == viewState.CENSUS || view === viewState.DIFFING) {
+ viewToolbarOptions = dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.label(
+ { className: "breakdown-by" },
+ L10N.getStr("toolbar.breakdownBy"),
+ dom.select(
+ {
+ id: "select-breakdown",
+ className: "select-breakdown",
+ onChange: e => onBreakdownChange(e.target.value),
+ },
+ breakdowns.map(({ name, displayName }) => dom.option(
+ {
+ key: name,
+ value: name
+ },
+ displayName
+ ))
+ )
+ ),
+
+ dom.label(
+ {},
+ dom.input({
+ id: "invert-tree-checkbox",
+ type: "checkbox",
+ checked: inverted,
+ onChange: onToggleInverted,
+ }),
+ L10N.getStr("checkbox.invertTree")
+ ),
+
+ dom.div({ id: "toolbar-spacer", className: "spacer" }),
+
+ dom.input({
+ id: "filter",
+ type: "search",
+ className: "devtools-searchinput",
+ placeholder: L10N.getStr("filter.placeholder"),
+ onChange: event => setFilterString(event.target.value),
+ value: !!filterString ? filterString : undefined,
+ })
+ );
+ } else {
+ assert(view === viewState.DOMINATOR_TREE);
+
+ viewToolbarOptions = dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.label(
+ { className: "label-by" },
+ L10N.getStr("toolbar.labelBy"),
+ dom.select(
+ {
+ id: "select-dominator-tree-breakdown",
+ onChange: e => onDominatorTreeBreakdownChange(e.target.value),
+ },
+ dominatorTreeBreakdowns.map(({ name, displayName }) => dom.option(
+ {
+ key: name,
+ value: name
+ },
+ displayName
+ ))
+ )
+ )
+ );
+ }
+
+ let viewSelect;
+ if (view !== viewState.DIFFING) {
+ viewSelect = dom.label(
+ {},
+ L10N.getStr("toolbar.view"),
+ dom.select(
+ {
+ id: "select-view",
+ onChange: e => onViewChange(e.target.value),
+ defaultValue: viewState.CENSUS,
+ },
+ dom.option({ value: viewState.CENSUS },
+ L10N.getStr("toolbar.view.census")),
+ dom.option({ value: viewState.DOMINATOR_TREE },
+ L10N.getStr("toolbar.view.dominators"))
+ )
+ );
+ }
+
return (
- dom.div({ className: "devtools-toolbar" },
- dom.div({ className: "toolbar-group" },
+ dom.div(
+ {
+ className: "devtools-toolbar"
+ },
+
+ dom.div(
+ {
+ className: "toolbar-group"
+ },
+
dom.button({
id: "take-snapshot",
className: "take-snapshot devtools-button",
onClick: onTakeSnapshotClick,
title: L10N.getStr("take-snapshot")
}),
- dom.button({
- id: "diff-snapshots",
- className: "devtools-button devtools-monospace" + (!!diffing ? " checked" : ""),
- disabled: snapshots.length < 2,
- onClick: onToggleDiffing,
- title: L10N.getStr("diff-snapshots.tooltip"),
- }, L10N.getStr("diff-snapshots")),
-
- dom.button({
- id: "import-snapshot",
- className: "devtools-toolbarbutton import-snapshot devtools-button",
- onClick: onImportClick,
- title: L10N.getStr("import-snapshot"),
- "data-text-only": true,
- }, L10N.getStr("import-snapshot"))
- ),
-
- dom.div({ className: "toolbar-group" },
- dom.label({ className: "breakdown-by" },
- L10N.getStr("toolbar.breakdownBy"),
- dom.select({
- id: "select-breakdown",
- className: "select-breakdown",
- onChange: e => onBreakdownChange(e.target.value),
- }, ...breakdowns.map(({ name, displayName }) => dom.option({ key: name, value: name }, displayName)))
+ dom.button(
+ {
+ id: "diff-snapshots",
+ className: "devtools-button devtools-monospace" + (!!diffing ? " checked" : ""),
+ disabled: snapshots.length < 2,
+ onClick: onToggleDiffing,
+ title: L10N.getStr("diff-snapshots.tooltip"),
+ },
+ L10N.getStr("diff-snapshots")
),
- dom.label({},
- dom.input({
- id: "invert-tree-checkbox",
- type: "checkbox",
- checked: inverted,
- onChange: onToggleInverted,
- }),
- L10N.getStr("checkbox.invertTree")
- ),
+ dom.button(
+ {
+ id: "import-snapshot",
+ className: "devtools-toolbarbutton import-snapshot devtools-button",
+ onClick: onImportClick,
+ title: L10N.getStr("import-snapshot"),
+ "data-text-only": true,
+ },
+ L10N.getStr("import-snapshot")
+ )
+ ),
- dom.label({},
- dom.input({
- id: "record-allocation-stacks-checkbox",
- type: "checkbox",
- checked: allocations.recording,
- disabled: allocations.togglingInProgress,
- onChange: onToggleRecordAllocationStacks,
- }),
- L10N.getStr("checkbox.recordAllocationStacks")
- ),
+ dom.label(
+ {},
+ dom.input({
+ id: "record-allocation-stacks-checkbox",
+ type: "checkbox",
+ checked: allocations.recording,
+ disabled: allocations.togglingInProgress,
+ onChange: onToggleRecordAllocationStacks,
+ }),
+ L10N.getStr("checkbox.recordAllocationStacks")
+ ),
- dom.div({ id: "toolbar-spacer", className: "spacer" }),
-
- dom.input({
- id: "filter",
- type: "search",
- className: "devtools-searchinput",
- placeholder: L10N.getStr("filter.placeholder"),
- onChange: event => setFilterString(event.target.value),
- value: !!filterString ? filterString : undefined,
- })
- )
+ viewSelect,
+ viewToolbarOptions
)
);
}
});
--- a/devtools/client/memory/constants.js
+++ b/devtools/client/memory/constants.js
@@ -5,16 +5,20 @@
"use strict";
// Options passed to MemoryFront's startRecordingAllocations never change.
exports.ALLOCATION_RECORDING_OPTIONS = {
probability: 1,
maxLogLength: 1
};
+// If TREE_ROW_HEIGHT changes, be sure to change `var(--heap-tree-row-height)`
+// in `devtools/client/themes/memory.css`
+exports.TREE_ROW_HEIGHT = 14;
+
/*** Actions ******************************************************************/
const actions = exports.actions = {};
// Fired by UI to request a snapshot from the actor.
actions.TAKE_SNAPSHOT_START = "take-snapshot-start";
actions.TAKE_SNAPSHOT_END = "take-snapshot-end";
@@ -44,36 +48,60 @@ actions.IMPORT_SNAPSHOT_END = "import-sn
actions.IMPORT_SNAPSHOT_ERROR = "import-snapshot-error";
// Fired by UI to select a snapshot to view.
actions.SELECT_SNAPSHOT = "select-snapshot";
// Fired to toggle tree inversion on or off.
actions.TOGGLE_INVERTED = "toggle-inverted";
-// Fired to toggle diffing mode on or off.
-actions.TOGGLE_DIFFING = "toggle-diffing";
-
// Fired when a snapshot is selected for diffing.
actions.SELECT_SNAPSHOT_FOR_DIFFING = "select-snapshot-for-diffing";
// Fired when taking a census diff.
actions.TAKE_CENSUS_DIFF_START = "take-census-diff-start";
actions.TAKE_CENSUS_DIFF_END = "take-census-diff-end";
actions.DIFFING_ERROR = "diffing-error";
// Fired to set a new breakdown.
actions.SET_BREAKDOWN = "set-breakdown";
+// Fired to change the breakdown that controls the dominator tree labels.
+actions.SET_DOMINATOR_TREE_BREAKDOWN = "set-dominator-tree-breakdown";
+
+// Fired when changing between census or dominators view.
+actions.CHANGE_VIEW = "change-view";
+
// Fired when there is an error processing a snapshot or taking a census.
actions.SNAPSHOT_ERROR = "snapshot-error";
// Fired when there is a new filter string set.
actions.SET_FILTER_STRING = "set-filter-string";
+// Fired to expand or collapse nodes in census reports.
+actions.EXPAND_CENSUS_NODE = "expand-census-node";
+actions.EXPAND_DIFFING_CENSUS_NODE = "expand-diffing-census-node";
+actions.COLLAPSE_CENSUS_NODE = "collapse-census-node";
+actions.COLLAPSE_DIFFING_CENSUS_NODE = "collapse-diffing-census-node";
+
+// Fired when nodes in various trees are focused.
+actions.FOCUS_CENSUS_NODE = "focus-census-node";
+actions.FOCUS_DIFFING_CENSUS_NODE = "focus-diffing-census-node";
+actions.FOCUS_DOMINATOR_TREE_NODE = "focus-dominator-tree-node";
+
+actions.COMPUTE_DOMINATOR_TREE_START = "compute-dominator-tree-start";
+actions.COMPUTE_DOMINATOR_TREE_END = "compute-dominator-tree-end";
+actions.FETCH_DOMINATOR_TREE_START = "fetch-dominator-tree-start";
+actions.FETCH_DOMINATOR_TREE_END = "fetch-dominator-tree-end";
+actions.DOMINATOR_TREE_ERROR = "dominator-tree-error";
+actions.FETCH_IMMEDIATELY_DOMINATED_START = "fetch-immediately-dominated-start";
+actions.FETCH_IMMEDIATELY_DOMINATED_END = "fetch-immediately-dominated-end";
+actions.EXPAND_DOMINATOR_TREE_NODE = "expand-dominator-tree-node";
+actions.COLLAPSE_DOMINATOR_TREE_NODE = "collapse-dominator-tree-node";
+
/*** Breakdowns ***************************************************************/
const COUNT = { by: "count", count: true, bytes: true };
const INTERNAL_TYPE = { by: "internalType", then: COUNT };
const ALLOCATION_STACK = { by: "allocationStack", then: COUNT, noStack: COUNT };
const OBJECT_CLASS = { by: "objectClass", then: COUNT, other: COUNT };
const breakdowns = exports.breakdowns = {
@@ -103,27 +131,73 @@ const breakdowns = exports.breakdowns =
},
internalType: {
displayName: "Internal Type",
breakdown: INTERNAL_TYPE,
},
};
+const DOMINATOR_TREE_LABEL_COARSE_TYPE = {
+ by: "coarseType",
+ objects: OBJECT_CLASS,
+ scripts: {
+ by: "internalType",
+ then: {
+ by: "filename",
+ then: COUNT,
+ noFilename: COUNT,
+ },
+ },
+ strings: INTERNAL_TYPE,
+ other: INTERNAL_TYPE,
+};
+
+const dominatorTreeBreakdowns = exports.dominatorTreeBreakdowns = {
+ coarseType: {
+ displayName: "Coarse Type",
+ breakdown: DOMINATOR_TREE_LABEL_COARSE_TYPE
+ },
+
+ allocationStack: {
+ displayName: "Allocation Stack",
+ breakdown: {
+ by: "allocationStack",
+ then: DOMINATOR_TREE_LABEL_COARSE_TYPE,
+ noStack: DOMINATOR_TREE_LABEL_COARSE_TYPE,
+ },
+ },
+
+ internalType: {
+ displayName: "Internal Type",
+ breakdown: INTERNAL_TYPE,
+ },
+};
+
+/*** View States **************************************************************/
+
+/**
+ * The various main views that the tool can be in.
+ */
+const viewState = exports.viewState = Object.create(null);
+viewState.CENSUS = "view-state-census";
+viewState.DIFFING = "view-state-diffing";
+viewState.DOMINATOR_TREE = "view-state-dominator-tree";
+
/*** Snapshot States **********************************************************/
-const snapshotState = exports.snapshotState = {};
+const snapshotState = exports.snapshotState = Object.create(null);
/**
* Various states a snapshot can be in.
* An FSM describing snapshot states:
*
- * SAVING -> SAVED -> READING -> READ SAVED_CENSUS
- * IMPORTING ↗ ↘ ↑ ↓
- * SAVING_CENSUS
+ * SAVING -> SAVED -> READING -> READ SAVED_CENSUS
+ * ↗ ↘ ↑ ↓
+ * IMPORTING SAVING_CENSUS
*
* Any of these states may go to the ERROR state, from which they can never
* leave (mwah ha ha ha!)
*/
snapshotState.ERROR = "snapshot-state-error";
snapshotState.IMPORTING = "snapshot-state-importing";
snapshotState.SAVING = "snapshot-state-saving";
snapshotState.SAVED = "snapshot-state-saved";
@@ -150,8 +224,25 @@ diffingState.SELECTING = "diffing-state-
// Currently computing the diff between the two selected snapshots.
diffingState.TAKING_DIFF = "diffing-state-taking-diff";
// Have the diff between the two selected snapshots.
diffingState.TOOK_DIFF = "diffing-state-took-diff";
// An error occurred while computing the diff.
diffingState.ERROR = "diffing-state-error";
+
+/*** Dominator Tree States ****************************************************/
+
+/*
+ * Various states the dominator tree model can be in.
+ *
+ * COMPUTING -> COMPUTED -> FETCHING -> LOADED <--> INCREMENTAL_FETCHING
+ *
+ * Any state may lead to the ERROR state, from which it can never leave.
+ */
+const dominatorTreeState = exports.dominatorTreeState = Object.create(null);
+dominatorTreeState.COMPUTING = "dominator-tree-state-computing";
+dominatorTreeState.COMPUTED = "dominator-tree-state-computed";
+dominatorTreeState.FETCHING = "dominator-tree-state-fetching";
+dominatorTreeState.LOADED = "dominator-tree-state-loaded";
+dominatorTreeState.INCREMENTAL_FETCHING = "dominator-tree-state-incremental-fetching";
+dominatorTreeState.ERROR = "dominator-tree-state-error";
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/dominator-tree-lazy-children.js
@@ -0,0 +1,58 @@
+/* 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/. */
+
+/**
+ * The `DominatorTreeLazyChildren` is a placeholder that represents a future
+ * subtree in an existing `DominatorTreeNode` tree that is currently being
+ * incrementally fetched from the `HeapAnalysesWorker`.
+ *
+ * @param {NodeId} parentNodeId
+ * @param {Number} siblingIndex
+ */
+function DominatorTreeLazyChildren(parentNodeId, siblingIndex) {
+ this._parentNodeId = parentNodeId;
+ this._siblingIndex = siblingIndex;
+}
+
+/**
+ * Generate a unique key for this `DominatorTreeLazyChildren` instance. This can
+ * be used as the key in a hash table or as the `key` property for a React
+ * component, for example.
+ *
+ * @returns {String}
+ */
+DominatorTreeLazyChildren.prototype.key = function () {
+ return `dominator-tree-lazy-children-${this._parentNodeId}-${this._siblingIndex}`;
+};
+
+/**
+ * Return true if this is a placeholder for the first child of its
+ * parent. Return false if it is a placeholder for loading more of its parent's
+ * children.
+ *
+ * @returns {Boolean}
+ */
+DominatorTreeLazyChildren.prototype.isFirstChild = function () {
+ return this._siblingIndex === 0;
+};
+
+/**
+ * Get this subtree's parent node's identifier.
+ *
+ * @returns {NodeId}
+ */
+DominatorTreeLazyChildren.prototype.parentNodeId = function () {
+ return this._parentNodeId;
+};
+
+/**
+ * Get this subtree's index in its parent's children array.
+ *
+ * @returns {Number}
+ */
+DominatorTreeLazyChildren.prototype.siblingIndex = function () {
+ return this._siblingIndex;
+};
+
+module.exports = DominatorTreeLazyChildren;
--- a/devtools/client/memory/models.js
+++ b/devtools/client/memory/models.js
@@ -1,17 +1,53 @@
/* 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 { assert } = require("devtools/shared/DevToolsUtils");
const { MemoryFront } = require("devtools/server/actors/memory");
const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
const { PropTypes } = require("devtools/client/shared/vendor/react");
-const { snapshotState: states, diffingState } = require("./constants");
+const {
+ snapshotState: states,
+ diffingState,
+ dominatorTreeState,
+ viewState
+} = require("./constants");
+
+/**
+ * ONLY USE THIS FOR MODEL VALIDATORS IN CONJUCTION WITH assert()!
+ *
+ * React checks that the returned values from validator functions are instances
+ * of Error, but because React is loaded in its own global, that check is always
+ * false and always results in a warning.
+ *
+ * To work around this and still get model validation, just call assert() inside
+ * a function passed to catchAndIgnore. The assert() function will still report
+ * assertion failures, but this funciton will swallow the errors so that React
+ * doesn't go crazy and drown out the real error in irrelevant and incorrect
+ * warnings.
+ *
+ * Example usage:
+ *
+ * const MyModel = PropTypes.shape({
+ * someProperty: catchAndIgnore(function (model) {
+ * assert(someInvariant(model.someProperty), "Should blah blah");
+ * })
+ * });
+ */
+function catchAndIgnore(fn) {
+ return function (...args) {
+ try {
+ fn(...args);
+ } catch (err) { }
+
+ return null;
+ };
+}
/**
* The breakdown object DSL describing how we want
* the census data to be.
* @see `js/src/doc/Debugger/Debugger.Memory.md`
*/
let breakdownModel = exports.breakdown = PropTypes.shape({
by: PropTypes.oneOf(["coarseType", "allocationStack", "objectClass", "internalType"]).isRequired,
@@ -22,16 +58,107 @@ let censusModel = exports.censusModel =
report: PropTypes.object,
// The breakdown used to generate the current census
breakdown: breakdownModel,
// Whether the currently cached report tree is inverted or not.
inverted: PropTypes.bool,
// If present, the currently cached report's filter string used for pruning
// the tree items.
filter: PropTypes.string,
+ // The Set<CensusTreeNode.id> of expanded node ids in the report tree.
+ expanded: catchAndIgnore(function (census) {
+ if (census.report) {
+ assert(census.expanded,
+ "If we have a report, we should also have the set of expanded nodes");
+ }
+ }),
+ // If a node is currently focused in the report tree, then this is it.
+ focused: PropTypes.object,
+});
+
+/**
+ * Dominator tree model.
+ */
+let dominatorTreeModel = exports.dominatorTreeModel = PropTypes.shape({
+ // The id of this dominator tree.
+ dominatorTreeId: PropTypes.number,
+
+ // The root DominatorTreeNode of this dominator tree.
+ root: PropTypes.object,
+
+ // The Set<NodeId> of expanded nodes in this dominator tree.
+ expanded: PropTypes.object,
+
+ // If a node is currently focused in the dominator tree, then this is it.
+ focused: PropTypes.object,
+
+ // If an error was thrown while getting this dominator tree, the `Error`
+ // instance (or an error string message) is attached here.
+ error: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.object,
+ ]),
+
+ // The breakdown used to generate descriptive labels of nodes in this
+ // dominator tree.
+ breakdown: breakdownModel,
+
+ // The number of active requests to incrementally fetch subtrees. This should
+ // only be non-zero when the state is INCREMENTAL_FETCHING.
+ activeFetchRequestCount: PropTypes.number,
+
+ // The dominatorTreeState that this domintor tree is currently in.
+ state: catchAndIgnore(function (dominatorTree) {
+ switch (dominatorTree.state) {
+ case dominatorTreeState.COMPUTING:
+ assert(dominatorTree.dominatorTreeId == null,
+ "Should not have a dominator tree id yet");
+ assert(!dominatorTree.root,
+ "Should not have the root of the tree yet");
+ assert(!dominatorTree.error,
+ "Should not have an error");
+ break;
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ assert(dominatorTree.dominatorTreeId != null,
+ "Should have a dominator tree id");
+ assert(!dominatorTree.root,
+ "Should not have the root of the tree yet");
+ assert(!dominatorTree.error,
+ "Should not have an error");
+ break;
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ assert(typeof dominatorTree.activeFetchRequestCount === "number",
+ "The active fetch request count is a number when we are in the " +
+ "INCREMENTAL_FETCHING state");
+ assert(dominatorTree.activeFetchRequestCount > 0,
+ "We are keeping track of how many active requests are in flight.");
+ // Fall through...
+ case dominatorTreeState.LOADED:
+ assert(dominatorTree.dominatorTreeId != null,
+ "Should have a dominator tree id");
+ assert(dominatorTree.root,
+ "Should have the root of the tree");
+ assert(dominatorTree.expanded,
+ "Should have an expanded set");
+ assert(!dominatorTree.error,
+ "Should not have an error");
+ break;
+
+ case dominatorTreeState.ERROR:
+ assert(dominatorTree.error, "Should have an error");
+ break;
+
+ default:
+ assert(false,
+ `Unexpected dominator tree state: ${dominatorTree.state}`);
+ }
+ }),
});
/**
* Snapshot model.
*/
let stateKeys = Object.keys(states).map(state => states[state]);
const snapshotId = PropTypes.number;
let snapshotModel = exports.snapshot = PropTypes.shape({
@@ -39,27 +166,29 @@ let snapshotModel = exports.snapshot = P
id: snapshotId.isRequired,
// Whether or not this snapshot is currently selected.
selected: PropTypes.bool.isRequired,
// Filesystem path to where the snapshot is stored; used to identify the
// snapshot for HeapAnalysesClient.
path: PropTypes.string,
// Current census data for this snapshot.
census: censusModel,
+ // Current dominator tree data for this snapshot.
+ dominatorTree: dominatorTreeModel,
// If an error was thrown while processing this snapshot, the `Error` instance
// is attached here.
error: PropTypes.object,
// Boolean indicating whether or not this snapshot was imported.
imported: PropTypes.bool.isRequired,
// The creation time of the snapshot; required after the snapshot has been
// read.
creationTime: PropTypes.number,
// The current state the snapshot is in.
// @see ./constants.js
- state: function (snapshot, propName) {
+ state: catchAndIgnore(function (snapshot, propName) {
let current = snapshot.state;
let shouldHavePath = [states.IMPORTING, states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
let shouldHaveCreationTime = [states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
let shouldHaveCensus = [states.SAVED_CENSUS];
if (!stateKeys.includes(current)) {
throw new Error(`Snapshot state must be one of ${stateKeys}.`);
}
@@ -67,50 +196,50 @@ let snapshotModel = exports.snapshot = P
throw new Error(`Snapshots in state ${current} must have a snapshot path.`);
}
if (shouldHaveCensus.includes(current) && (!snapshot.census || !snapshot.census.breakdown)) {
throw new Error(`Snapshots in state ${current} must have a census and breakdown.`);
}
if (shouldHaveCreationTime.includes(current) && !snapshot.creationTime) {
throw new Error(`Snapshots in state ${current} must have a creation time.`);
}
- },
+ }),
});
let allocationsModel = exports.allocations = PropTypes.shape({
// True iff we are recording allocation stacks right now.
recording: PropTypes.bool.isRequired,
// True iff we are in the process of toggling the recording of allocation
// stacks on or off right now.
togglingInProgress: PropTypes.bool.isRequired,
});
let diffingModel = exports.diffingModel = PropTypes.shape({
// The id of the first snapshot to diff.
firstSnapshotId: snapshotId,
// The id of the second snapshot to diff.
- secondSnapshotId: function (diffing, propName) {
+ secondSnapshotId: catchAndIgnore(function (diffing, propName) {
if (diffing.secondSnapshotId && !diffing.firstSnapshotId) {
throw new Error("Cannot have second snapshot without already having " +
"first snapshot");
}
return snapshotId(diffing, propName);
- },
+ }),
// The current census data for the diffing.
census: censusModel,
// If an error was thrown while diffing, the `Error` instance is attached
// here.
error: PropTypes.object,
// The current state the diffing is in.
// @see ./constants.js
- state: function (diffing) {
+ state: catchAndIgnore(function (diffing) {
switch (diffing.state) {
case diffingState.TOOK_DIFF:
assert(diffing.census, "If we took a diff, we should have a census");
// Fall through...
case diffingState.TAKING_DIFF:
assert(diffing.firstSnapshotId, "Should have first snapshot");
assert(diffing.secondSnapshotId, "Should have second snapshot");
break;
@@ -120,31 +249,63 @@ let diffingModel = exports.diffingModel
case diffingState.ERROR:
assert(diffing.error, "Should have error");
break;
default:
assert(false, `Bad diffing state: ${diffing.state}`);
}
- }
+ }),
});
let appModel = exports.app = {
// {MemoryFront} Used to communicate with platform
front: PropTypes.instanceOf(MemoryFront),
+
// Allocations recording related data.
allocations: allocationsModel.isRequired,
+
// {HeapAnalysesClient} Used to interface with snapshots
heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
+
// The breakdown object DSL describing how we want
// the census data to be.
// @see `js/src/doc/Debugger/Debugger.Memory.md`
breakdown: breakdownModel.isRequired,
+
+ // The breakdown object DSL describing how we want
+ // the dominator tree labels to be computed.
+ // @see `js/src/doc/Debugger/Debugger.Memory.md`
+ dominatorTreeBreakdown: breakdownModel.isRequired,
+
// List of reference to all snapshots taken
snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
+
// True iff we want the tree displayed inverted.
inverted: PropTypes.bool.isRequired,
+
// If present, a filter string for pruning the tree items.
filter: PropTypes.string,
+
// If present, the current diffing state.
diffing: diffingModel,
+
+ // The current type of view.
+ view: catchAndIgnore(function (app) {
+ switch (app.view) {
+ case viewState.CENSUS:
+ assert(!app.diffing, "Should not be diffing");
+ break;
+
+ case viewState.DIFFING:
+ assert(app.diffing, "Should be diffing");
+ break;
+
+ case viewState.DOMINATOR_TREE:
+ assert(!app.diffing, "Should not be diffing");
+ break;
+
+ default:
+ assert(false, `Unexpected type of view: ${app.view}`);
+ }
+ }),
};
--- a/devtools/client/memory/moz.build
+++ b/devtools/client/memory/moz.build
@@ -10,18 +10,20 @@ DIRS += [
'actions',
'components',
'reducers',
]
DevToolsModules(
'app.js',
'constants.js',
+ 'dominator-tree-lazy-children.js',
'initializer.js',
'models.js',
'panel.js',
'reducers.js',
'store.js',
'utils.js',
)
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
--- a/devtools/client/memory/reducers.js
+++ b/devtools/client/memory/reducers.js
@@ -1,12 +1,14 @@
/* 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";
exports.allocations = require("./reducers/allocations");
exports.breakdown = require("./reducers/breakdown");
exports.diffing = require("./reducers/diffing");
+exports.dominatorTreeBreakdown = require("./reducers/dominatorTreeBreakdown");
exports.errors = require("./reducers/errors");
exports.filter = require("./reducers/filter");
exports.inverted = require("./reducers/inverted");
exports.snapshots = require("./reducers/snapshots");
+exports.view = require("./reducers/view");
--- a/devtools/client/memory/reducers/diffing.js
+++ b/devtools/client/memory/reducers/diffing.js
@@ -1,30 +1,31 @@
/* 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 { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
-const { actions, diffingState } = require("../constants");
+const { actions, diffingState, viewState } = require("../constants");
const { snapshotIsDiffable } = require("../utils");
const handlers = Object.create(null);
-handlers[actions.TOGGLE_DIFFING] = function (diffing, action) {
- if (diffing) {
- return null;
+handlers[actions.CHANGE_VIEW] = function (diffing, { view }) {
+ if (view === viewState.DIFFING) {
+ assert(!diffing, "Should not switch to diffing view when already diffing");
+ return Object.freeze({
+ firstSnapshotId: null,
+ secondSnapshotId: null,
+ census: null,
+ state: diffingState.SELECTING,
+ });
}
- return Object.freeze({
- firstSnapshotId: null,
- secondSnapshotId: null,
- census: null,
- state: diffingState.SELECTING,
- });
+ return null;
};
handlers[actions.SELECT_SNAPSHOT_FOR_DIFFING] = function (diffing, { snapshot }) {
assert(diffing,
"Should never select a snapshot for diffing when we aren't diffing " +
"anything");
assert(diffing.state === diffingState.SELECTING,
"Can't select when not in SELECTING state");
@@ -75,26 +76,67 @@ handlers[actions.TAKE_CENSUS_DIFF_END] =
"First snapshot's id should match");
assert(action.second.id === diffing.secondSnapshotId,
"Second snapshot's id should match");
return immutableUpdate(diffing, {
state: diffingState.TOOK_DIFF,
census: {
report: action.report,
+ expanded: new Set(),
inverted: action.inverted,
filter: action.filter,
breakdown: action.breakdown,
}
});
};
-handlers[actions.DIFFFING_ERROR] = function (diffing, action) {
+handlers[actions.DIFFING_ERROR] = function (diffing, action) {
return {
state: diffingState.ERROR,
error: action.error
};
};
+handlers[actions.EXPAND_DIFFING_CENSUS_NODE] = function (diffing, { node }) {
+ assert(diffing, "Should be diffing if expanding diffing's census nodes");
+ assert(diffing.state === diffingState.TOOK_DIFF,
+ "Should have taken the census diff if expanding nodes");
+ assert(diffing.census, "Should have a census");
+ assert(diffing.census.report, "Should have a census report");
+ assert(diffing.census.expanded, "Should have a census's expanded set");
+
+ // Warning: mutable operations couched in an immutable update ahead :'( This
+ // at least lets us use referential equality on the census model itself,
+ // even though the expanded set is mutated in place.
+ const expanded = diffing.census.expanded;
+ expanded.add(node.id);
+ const census = immutableUpdate(diffing.census, { expanded });
+ return immutableUpdate(diffing, { census });
+};
+
+handlers[actions.COLLAPSE_DIFFING_CENSUS_NODE] = function (diffing, { node }) {
+ assert(diffing, "Should be diffing if expanding diffing's census nodes");
+ assert(diffing.state === diffingState.TOOK_DIFF,
+ "Should have taken the census diff if expanding nodes");
+ assert(diffing.census, "Should have a census");
+ assert(diffing.census.report, "Should have a census report");
+ assert(diffing.census.expanded, "Should have a census's expanded set");
+
+ // Warning: mutable operations couched in an immutable update ahead :'( See
+ // above comment in the EXPAND_DIFFING_CENSUS_NODE handler.
+ const expanded = diffing.census.expanded;
+ expanded.delete(node.id);
+ const census = immutableUpdate(diffing.census, { expanded });
+ return immutableUpdate(diffing, { census });
+};
+
+handlers[actions.FOCUS_DIFFING_CENSUS_NODE] = function (diffing, { node }) {
+ assert(diffing, "Should be diffing.");
+ assert(diffing.census, "Should have a census");
+ const census = immutableUpdate(diffing.census, { focused: node });
+ return immutableUpdate(diffing, { census });
+};
+
module.exports = function (diffing = null, action) {
const handler = handlers[action.type];
return handler ? handler(diffing, action) : diffing;
};
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers/dominatorTreeBreakdown.js
@@ -0,0 +1,19 @@
+/* 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 { actions, dominatorTreeBreakdowns } = require("../constants");
+const DEFAULT_BREAKDOWN = dominatorTreeBreakdowns.coarseType.breakdown;
+
+const handlers = Object.create(null);
+
+handlers[actions.SET_DOMINATOR_TREE_BREAKDOWN] = function (_, { breakdown }) {
+ return breakdown;
+};
+
+module.exports = function (state = DEFAULT_BREAKDOWN, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(state, action) : state;
+};
--- a/devtools/client/memory/reducers/moz.build
+++ b/devtools/client/memory/reducers/moz.build
@@ -2,13 +2,15 @@
# 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/.
DevToolsModules(
'allocations.js',
'breakdown.js',
'diffing.js',
+ 'dominatorTreeBreakdown.js',
'errors.js',
'filter.js',
'inverted.js',
'snapshots.js',
+ 'view.js',
)
--- a/devtools/client/memory/reducers/snapshots.js
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -1,15 +1,21 @@
/* 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 { immutableUpdate } = require("devtools/shared/DevToolsUtils");
-const { actions, snapshotState: states } = require("../constants");
+const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
+const {
+ actions,
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("../constants");
+const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode");
const handlers = Object.create(null);
handlers[actions.SNAPSHOT_ERROR] = function (snapshots, { id, error }) {
return snapshots.map(snapshot => {
return snapshot.id === id
? immutableUpdate(snapshot, { state: states.ERROR, error })
: snapshot;
@@ -59,34 +65,301 @@ handlers[actions.TAKE_CENSUS_START] = fu
? immutableUpdate(snapshot, { state: states.SAVING_CENSUS, census })
: snapshot;
});
};
handlers[actions.TAKE_CENSUS_END] = function (snapshots, { id, report, breakdown, inverted, filter }) {
const census = {
report,
+ expanded: new Set(),
breakdown,
inverted,
filter,
};
return snapshots.map(snapshot => {
return snapshot.id === id
? immutableUpdate(snapshot, { state: states.SAVED_CENSUS, census })
: snapshot;
});
};
+handlers[actions.EXPAND_CENSUS_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.census, "Should have a census");
+ assert(snapshot.census.report, "Should have a census report");
+ assert(snapshot.census.expanded, "Should have a census's expanded set");
+
+ // Warning: mutable operations couched in an immutable update ahead :'( This
+ // at least lets us use referential equality on the census model itself,
+ // even though the expanded set is mutated in place.
+ const expanded = snapshot.census.expanded;
+ expanded.add(node.id);
+ const census = immutableUpdate(snapshot.census, { expanded });
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.COLLAPSE_CENSUS_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.census, "Should have a census");
+ assert(snapshot.census.report, "Should have a census report");
+ assert(snapshot.census.expanded, "Should have a census's expanded set");
+
+ // Warning: mutable operations couched in an immutable update ahead :'( See
+ // above comment in the EXPAND_CENSUS_NODE handler.
+ const expanded = snapshot.census.expanded;
+ expanded.delete(node.id);
+ const census = immutableUpdate(snapshot.census, { expanded });
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.FOCUS_CENSUS_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.census, "Should have a census");
+ const census = immutableUpdate(snapshot.census, { focused: node });
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
handlers[actions.SELECT_SNAPSHOT] = function (snapshots, { id }) {
return snapshots.map(s => immutableUpdate(s, { selected: s.id === id }));
};
-handlers[actions.TOGGLE_DIFFING] = function (snapshots, action) {
- return snapshots.map(s => immutableUpdate(s, { selected: false }));
+handlers[actions.CHANGE_VIEW] = function (snapshots, { view }) {
+ return view === viewState.DIFFING
+ ? snapshots.map(s => immutableUpdate(s, { selected: false }))
+ : snapshots;
+};
+
+handlers[actions.COMPUTE_DOMINATOR_TREE_START] = function (snapshots, { id }) {
+ const dominatorTree = Object.freeze({
+ state: dominatorTreeState.COMPUTING,
+ dominatorTreeId: undefined,
+ root: undefined,
+ });
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(!snapshot.dominatorTree,
+ "Should not have a dominator tree model");
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.COMPUTE_DOMINATOR_TREE_END] = function (snapshots, { id, dominatorTreeId }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.state == dominatorTreeState.COMPUTING,
+ "Should be in the COMPUTING state");
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.COMPUTED,
+ dominatorTreeId,
+ });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_DOMINATOR_TREE_START] = function (snapshots, { id, breakdown }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.state !== dominatorTreeState.COMPUTING &&
+ snapshot.dominatorTree.state !== dominatorTreeState.ERROR,
+ `Should have already computed the dominator tree, found state = ${snapshot.dominatorTree.state}`);
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.FETCHING,
+ root: undefined,
+ breakdown,
+ });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_DOMINATOR_TREE_END] = function (snapshots, { id, root }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.state == dominatorTreeState.FETCHING,
+ "Should be in the FETCHING state");
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.LOADED,
+ root,
+ expanded: new Set(),
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.EXPAND_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+ assert(snapshot.dominatorTree.expanded,
+ "Should have the dominator tree's expanded set");
+
+ // Warning: mutable operations couched in an immutable update ahead :'( This
+ // at least lets us use referential equality on the dominatorTree model itself,
+ // even though the expanded set is mutated in place.
+ const expanded = snapshot.dominatorTree.expanded;
+ expanded.add(node.nodeId);
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.COLLAPSE_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+ assert(snapshot.dominatorTree.expanded,
+ "Should have the dominator tree's expanded set");
+
+ // Warning: mutable operations couched in an immutable update ahead :'( See
+ // above comment in the EXPAND_DOMINATOR_TREE_NODE handler.
+ const expanded = snapshot.dominatorTree.expanded;
+ expanded.delete(node.nodeId);
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FOCUS_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree");
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, { focused: node });
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_IMMEDIATELY_DOMINATED_START] = function (snapshots, { id }) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.state == dominatorTreeState.INCREMENTAL_FETCHING ||
+ snapshot.dominatorTree.state == dominatorTreeState.LOADED,
+ "The dominator tree should be loaded if we are going to " +
+ "incrementally fetch children.");
+
+ const activeFetchRequestCount = snapshot.dominatorTree.activeFetchRequestCount
+ ? snapshot.dominatorTree.activeFetchRequestCount + 1
+ : 1;
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state: dominatorTreeState.INCREMENTAL_FETCHING,
+ activeFetchRequestCount,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+};
+
+handlers[actions.FETCH_IMMEDIATELY_DOMINATED_END] =
+ function (snapshots, { id, path, nodes, moreChildrenAvailable}) {
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ assert(snapshot.dominatorTree, "Should have a dominator tree model");
+ assert(snapshot.dominatorTree.root, "Should have a dominator tree model root");
+ assert(snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
+ "The dominator tree state should be INCREMENTAL_FETCHING");
+
+ const root = DominatorTreeNode.insert(snapshot.dominatorTree.root,
+ path,
+ nodes,
+ moreChildrenAvailable);
+
+ const focused = snapshot.dominatorTree.focused
+ ? DominatorTreeNode.getNodeByIdAlongPath(snapshot.dominatorTree.focused.nodeId,
+ root,
+ path)
+ : undefined;
+
+ const activeFetchRequestCount = snapshot.dominatorTree.activeFetchRequestCount === 1
+ ? undefined
+ : snapshot.dominatorTree.activeFetchRequestCount - 1;
+
+ // If there are still outstanding requests, we need to stay in the
+ // INCREMENTAL_FETCHING state until they complete.
+ const state = activeFetchRequestCount
+ ? dominatorTreeState.INCREMENTAL_FETCHING
+ : dominatorTreeState.LOADED;
+
+ const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
+ state,
+ root,
+ focused,
+ activeFetchRequestCount,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
+ };
+
+handlers[actions.DOMINATOR_TREE_ERROR] = function (snapshots, { id, error }) {
+ assert(error, "actions with DOMINATOR_TREE_ERROR should have an error");
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ const dominatorTree = Object.freeze({
+ state: dominatorTreeState.ERROR,
+ error,
+ });
+
+ return immutableUpdate(snapshot, { dominatorTree });
+ });
};
module.exports = function (snapshots = [], action) {
const handler = handlers[action.type];
if (handler) {
return handler(snapshots, action);
}
return snapshots;
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers/view.js
@@ -0,0 +1,17 @@
+/* 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 { actions, viewState } = require("../constants");
+
+const handlers = Object.create(null);
+
+handlers[actions.CHANGE_VIEW] = function (_, { view }) {
+ return view;
+};
+
+module.exports = function (view = viewState.CENSUS, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(view, action) : view;
+};
--- a/devtools/client/memory/store.js
+++ b/devtools/client/memory/store.js
@@ -1,27 +1,32 @@
/* 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 { combineReducers } = require("../shared/vendor/redux");
const createStore = require("../shared/redux/create-store");
const reducers = require("./reducers");
+const { viewState } = require("./constants");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
module.exports = function () {
let shouldLog = false;
let history;
// If testing, store the action history in an array
// we'll later attach to the store
if (DevToolsUtils.testing) {
history = [];
+ shouldLog = true;
}
- let store = createStore({ log: shouldLog, history })(combineReducers(reducers), {});
+ let store = createStore({
+ log: shouldLog,
+ history
+ })(combineReducers(reducers), {});
if (history) {
store.history = history;
}
return store;
};
--- a/devtools/client/memory/test/browser/browser.ini
+++ b/devtools/client/memory/test/browser/browser.ini
@@ -1,20 +1,22 @@
[DEFAULT]
tags = devtools devtools-memory
subsuite = devtools
support-files =
head.js
+ doc_big_tree.html
doc_steady_allocation.html
[browser_memory_allocationStackBreakdown_01.js]
[browser_memory-breakdowns-01.js]
skip-if = debug # bug 1219554
[browser_memory_diff_01.js]
skip-if = debug # bug 1219554
+[browser_memory_dominator_trees_01.js]
[browser_memory_filter_01.js]
skip-if = debug # bug 1219554
[browser_memory_no_allocation_stacks.js]
[browser_memory_no_auto_expand.js]
skip-if = debug # bug 1219554
[browser_memory_percents_01.js]
[browser_memory-simple-01.js]
skip-if = debug # bug 1219554
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test for dominator trees, their focused nodes, and keyboard navigating
+// through nodes across incrementally fetching subtrees.
+
+"use strict";
+
+const {
+ dominatorTreeState,
+ snapshotState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ expandDominatorTreeNode,
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const { toggleInverted } = require("devtools/client/memory/actions/inverted");
+const { changeView } = require("devtools/client/memory/actions/view");
+
+const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_big_tree.html";
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ // Taking snapshots and computing dominator trees is slow :-/
+ requestLongerTimeout(4);
+
+ const heapWorker = panel.panelWin.gHeapAnalysesClient;
+ const front = panel.panelWin.gFront;
+ const store = panel.panelWin.gStore;
+ const { getState, dispatch } = store;
+ const doc = panel.panelWin.document;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ // Take a snapshot.
+
+ const takeSnapshotButton = doc.getElementById("take-snapshot");
+ EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
+
+ // Wait for the dominator tree to be computed and fetched.
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "Computed and fetched the dominator tree.");
+
+ // Expand all the dominator tree nodes that are eagerly fetched, except for
+ // the leaves which will trigger fetching their lazily loaded subtrees.
+
+ const id = getState().snapshots[0].id;
+ const root = getState().snapshots[0].dominatorTree.root;
+ (function expandAllEagerlyFetched(node = root) {
+ if (!node.moreChildrenAvailable || node.children) {
+ dispatch(expandDominatorTreeNode(id, node));
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ expandAllEagerlyFetched(child);
+ }
+ }
+ }());
+
+ // Find the deepest eagerly loaded node: one which has more children but none
+ // of them are loaded.
+
+ const deepest = (function findDeepest(node = root) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findDeepest(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }());
+
+ ok(deepest, "Found the deepest node");
+ ok(!getState().snapshots[0].dominatorTree.expanded.has(deepest.nodeId),
+ "The deepest node should not be expanded");
+
+ // Select the deepest node.
+
+ EventUtils.synthesizeMouseAtCenter(doc.querySelector(`.node-${deepest.nodeId}`),
+ {},
+ panel.panelWin);
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.focused.nodeId === deepest.nodeId);
+ ok(doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
+ "The deepest node should be focused now");
+
+ // Expand the deepest node, which triggers an incremental fetch of its lazily
+ // loaded subtree.
+
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.expanded.has(deepest.nodeId));
+ is(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Expanding the deepest node should start an incremental fetch of its subtree");
+ ok(doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
+ "The deepest node should still be focused after expansion");
+
+ // Wait for the incremental fetch to complete.
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "And the incremental fetch completes.");
+ ok(doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
+ "The deepest node should still be focused after we have loaded its children");
+
+ // Find the most up-to-date version of the node whose children we just
+ // incrementally fetched.
+
+ const newDeepest = (function findNewDeepest(node = getState().snapshots[0].dominatorTree.root) {
+ if (node.nodeId === deepest.nodeId) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNewDeepest(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }());
+
+ ok(newDeepest, "We found the up-to-date version of deepest");
+ ok(newDeepest.children, "And its children are loaded");
+ ok(newDeepest.children.length, "And there are more than 0 children");
+
+ const firstChild = newDeepest.children[0];
+ ok(firstChild, "deepest should have a first child");
+ ok(doc.querySelector(`.node-${firstChild.nodeId}`),
+ "and the first child should exist in the dom");
+
+ // Select the newly loaded first child by pressing the right arrow once more.
+
+ EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.focused === firstChild);
+ ok(doc.querySelector(`.node-${firstChild.nodeId}`).classList.contains("focused"),
+ "The first child should now be focused");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/browser/doc_big_tree.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <script>
+ window.big = (function makeBig(depth = 0) {
+ var big = Array(5);
+ big.fill(undefined);
+ if (depth < 5) {
+ big = big.map(_ => makeBig(depth + 1));
+ }
+ return big;
+ }());
+ </script>
+ </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/chrome.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_DominatorTree_01.html]
+[test_DominatorTree_02.html]
+[test_Heap_01.html]
+[test_Heap_02.html]
+[test_Heap_03.html]
+[test_Toolbar_01.html]
+[test_Toolbar_02.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/head.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://testing-common/Assert.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+Cu.import("resource://devtools/client/shared/browser-loader.js");
+var { require } = BrowserLoader("resource://devtools/client/memory/", this);
+
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+DevToolsUtils.testing = true;
+var { immutableUpdate } = DevToolsUtils;
+
+var constants = require("devtools/client/memory/constants");
+var {
+ diffingState,
+ dominatorTreeBreakdowns,
+ dominatorTreeState,
+ snapshotState,
+ viewState
+} = constants;
+
+const {
+ getBreakdownDisplayData,
+ getDominatorTreeBreakdownDisplayData,
+} = require("devtools/client/memory/utils");
+
+var models = require("devtools/client/memory/models");
+
+var React = require("devtools/client/shared/vendor/react");
+var ReactDOM = require("devtools/client/shared/vendor/react-dom");
+var Heap = React.createFactory(require("devtools/client/memory/components/heap"));
+var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree"));
+var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
+
+// All tests are asynchronous.
+SimpleTest.waitForExplicitFinish();
+
+var noop = () => {};
+
+// Counter for mock DominatorTreeNode ids.
+var TEST_NODE_ID_COUNTER = 0;
+
+/**
+ * Create a mock DominatorTreeNode for testing, with sane defaults. Override any
+ * property by providing it on `opts`. Optionally pass child nodes as well.
+ *
+ * @param {Object} opts
+ * @param {Array<DominatorTreeNode>?} children
+ *
+ * @returns {DominatorTreeNode}
+ */
+function makeTestDominatorTreeNode(opts, children) {
+ const nodeId = TEST_NODE_ID_COUNTER++;
+
+ const node = Object.assign({
+ nodeId,
+ label: ["other", "SomeType"],
+ shallowSize: 1,
+ retainedSize: (children || []).reduce((size, c) => size + c.retainedSize, 1),
+ parentId: undefined,
+ children,
+ moreChildrenAvailable: true,
+ }, opts);
+
+ if (children && children.length) {
+ children.map(c => c.parentId = node.nodeId);
+ }
+
+ return node;
+}
+
+var TEST_DOMINATOR_TREE = Object.freeze({
+ dominatorTreeId: 666,
+ root: (function makeTree(depth = 0) {
+ let children;
+ if (depth <= 3) {
+ children = [
+ makeTree(depth + 1),
+ makeTree(depth + 1),
+ makeTree(depth + 1),
+ ];
+ }
+ return makeTestDominatorTreeNode({}, children);
+ }()),
+ expanded: new Set(),
+ focused: null,
+ error: null,
+ breakdown: dominatorTreeBreakdowns.coarseType.breakdown,
+ activeFetchRequestCount: null,
+ state: dominatorTreeState.LOADED,
+});
+
+var TEST_DOMINATOR_TREE_PROPS = Object.freeze({
+ dominatorTree: TEST_DOMINATOR_TREE,
+ onLoadMoreSiblings: noop,
+ onViewSourceInDebugger: noop,
+ onExpand: noop,
+ onCollapse: noop,
+});
+
+var TEST_HEAP_PROPS = Object.freeze({
+ onSnapshotClick: noop,
+ onLoadMoreSiblings: noop,
+ onCensusExpand: noop,
+ onCensusCollapse: noop,
+ onDominatorTreeExpand: noop,
+ onDominatorTreeCollapse: noop,
+ onCensusFocus: noop,
+ onDominatorTreeFocus: noop,
+ onViewSourceInDebugger: noop,
+ diffing: null,
+ view: viewState.CENSUS,
+ snapshot: Object.freeze({
+ id: 1337,
+ selected: true,
+ path: "/fake/path/to/snapshot",
+ census: Object.freeze({
+ report: Object.freeze({
+ objects: Object.freeze({ count: 4, bytes: 400 }),
+ scripts: Object.freeze({ count: 3, bytes: 300 }),
+ strings: Object.freeze({ count: 2, bytes: 200 }),
+ other: Object.freeze({ count: 1, bytes: 100 }),
+ }),
+ breakdown: Object.freeze({
+ by: "coarseType",
+ objects: Object.freeze({ by: "count", count: true, bytes: true }),
+ scripts: Object.freeze({ by: "count", count: true, bytes: true }),
+ strings: Object.freeze({ by: "count", count: true, bytes: true }),
+ other: Object.freeze({ by: "count", count: true, bytes: true }),
+ }),
+ inverted: false,
+ filter: null,
+ expanded: new Set(),
+ focused: null,
+ }),
+ dominatorTree: TEST_DOMINATOR_TREE,
+ error: null,
+ imported: false,
+ creationTime: 0,
+ state: snapshotState.SAVED_CENSUS,
+ }),
+});
+
+var TEST_TOOLBAR_PROPS = Object.freeze({
+ breakdowns: getBreakdownDisplayData(),
+ onTakeSnapshotClick: noop,
+ onImportClick: noop,
+ onBreakdownChange: noop,
+ onToggleRecordAllocationStacks: noop,
+ allocations: models.allocations,
+ onToggleInverted: noop,
+ inverted: false,
+ filterString: null,
+ setFilterString: noop,
+ diffing: null,
+ onToggleDiffing: noop,
+ view: viewState.CENSUS,
+ onViewChange: noop,
+ dominatorTreeBreakdowns: getDominatorTreeBreakdownDisplayData(),
+ onDominatorTreeBreakdownChange: noop,
+ snapshots: [],
+});
+
+function onNextAnimationFrame(fn) {
+ return () =>
+ requestAnimationFrame(() =>
+ requestAnimationFrame(fn));
+}
+
+function renderComponent(component, container) {
+ return new Promise(resolve => {
+ ReactDOM.render(component, container, onNextAnimationFrame(() => {
+ dumpn("Rendered = " + container.innerHTML);
+ resolve();
+ }));
+ });
+}
+
+function setState(component, newState) {
+ return new Promise(resolve => {
+ component.setState(newState, onNextAnimationFrame(resolve));
+ });
+}
+
+function setProps(component, newProps) {
+ return new Promise(resolve => {
+ component.setProps(newProps, onNextAnimationFrame(resolve));
+ });
+}
+
+function dumpn(msg) {
+ dump(`MEMORY-TEST: ${msg}\n`);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTree_01.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a place holder for a subtree we are lazily fetching.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true});
+ ok(!root.children);
+
+ const expanded = new Set();
+ expanded.add(root.nodeId);
+
+ yield renderComponent(DominatorTreeComponent(immutableUpdate(TEST_DOMINATOR_TREE_PROPS, {
+ dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, {
+ expanded,
+ root,
+ state: dominatorTreeState.INCREMENTAL_FETCHING,
+ activeFetchRequestCount: 1,
+ }),
+ })), container);
+
+ ok(container.querySelector(".subtree-fetching"),
+ "Expanded nodes with more children available, but no children " +
+ "loaded, should get a placeholder");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_DominatorTree_02.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a link to load more children when some (but not all) are loaded.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <!-- Give the container height so that the whole tree is rendered. -->
+ <div id="container" style="height: 900px;"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true }, [
+ makeTestDominatorTreeNode({}),
+ ]);
+ ok(root.children);
+ ok(root.moreChildrenAvailable);
+
+ const expanded = new Set();
+ expanded.add(root.nodeId);
+
+ yield renderComponent(DominatorTreeComponent(immutableUpdate(TEST_DOMINATOR_TREE_PROPS, {
+ dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, {
+ expanded,
+ root,
+ }),
+ })), container);
+
+ ok(container.querySelector(".more-children"),
+ "Should get a link to load more children");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_01.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that rendering a dominator tree error is handled correctly.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ ok(React, "Should get React");
+ ok(Heap, "Should get Heap");
+
+ const errorMessage = "Something went wrong!";
+ const container = document.getElementById("container");
+
+ const props = immutableUpdate(TEST_HEAP_PROPS, {
+ view: viewState.DOMINATOR_TREE,
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: {
+ error: new Error(errorMessage),
+ state: dominatorTreeState.ERROR,
+ }
+ })
+ });
+
+ yield renderComponent(Heap(props), container);
+
+ ok(container.querySelector(".error"), "Should render an error view");
+ ok(container.textContent.indexOf(errorMessage) !== -1,
+ "Should see our error message");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_02.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the currently selected view is rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ ok(React, "Should get React");
+ ok(Heap, "Should get Heap");
+
+ const errorMessage = "Something went wrong!";
+ const container = document.getElementById("container");
+
+ // Dominator tree view.
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: viewState.DOMINATOR_TREE,
+ })), container);
+
+ ok(container.querySelector(`[data-state=${dominatorTreeState.LOADED}]`),
+ "Should render the dominator tree.");
+
+ // Census view.
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: viewState.CENSUS,
+ })), container);
+
+ ok(container.querySelector(`[data-state=${snapshotState.SAVED_CENSUS}]`),
+ "Should render the census.");
+
+ // Diffing view.
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: viewState.DIFFING,
+ snapshot: null,
+ diffing: {
+ firstSnapshotId: null,
+ secondSnapshotId: null,
+ census: null,
+ error: null,
+ state: diffingState.SELECTING,
+ },
+ })), container);
+
+ ok(container.querySelector(`[data-state=${diffingState.SELECTING}]`),
+ "Should render the diffing.");
+
+ // Initial view.
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ snapshot: null,
+ diffing: null,
+ })), container);
+
+ ok(container.querySelector("[data-state=initial]"),
+ "With no snapshot, nor a diffing, should render initial prompt.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Heap_03.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we show a throbber while computing and fetching dominator trees,
+but not in other dominator tree states.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ for (let state of [dominatorTreeState.COMPUTING, dominatorTreeState.FETCHING]) {
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: viewState.DOMINATOR_TREE,
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
+ state,
+ root: null,
+ dominatorTreeId: state === dominatorTreeState.FETCHING ? 1 : null,
+ }),
+ }),
+ })), container);
+
+ ok(container.querySelector(".devtools-throbber"),
+ `Should show a throbber for state = ${state}`);
+ }
+
+ for (let state of [dominatorTreeState.LOADED, dominatorTreeState.INCREMENTAL_FETCHING]) {
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: viewState.DOMINATOR_TREE,
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
+ state,
+ activeFetchRequestCount: state === dominatorTreeState.INCREMENTAL_FETCHING ? 1 : undefined,
+ }),
+ }),
+ })), container);
+
+ ok(!container.querySelector(".devtools-throbber"),
+ `Should not show a throbber for state = ${state}`);
+ }
+
+ yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
+ view: viewState.DOMINATOR_TREE,
+ snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
+ dominatorTree: {
+ state: dominatorTreeState.ERROR,
+ error: new Error("example error for testing"),
+ },
+ }),
+ })), container);
+
+ ok(!container.querySelector(".devtools-throbber"),
+ `Should not show a throbber for ERROR state`);
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Toolbar_01.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the Toolbar component shows the view switcher only at the appropriate times.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ // Census and dominator tree views.
+
+ for (let view of [viewState.CENSUS, viewState.DOMINATOR_TREE]) {
+ yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
+ view,
+ })), container);
+
+ ok(container.querySelector("#select-view"),
+ `The view selector is shown in view = ${view}`);
+ }
+
+ yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
+ view: viewState.DIFFING,
+ })), container);
+
+ ok(!container.querySelector("#select-view"),
+ "The view selector is NOT shown in the DIFFING view");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_Toolbar_02.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the Toolbar component shows the tree inversion checkbox only at the appropriate times.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+ <pre id="test">
+ <script src="head.js" type="application/javascript;version=1.8"></script>
+ <script type="application/javascript;version=1.8">
+ window.onload = Task.async(function* () {
+ try {
+ const container = document.getElementById("container");
+
+ // Census and dominator tree views.
+
+ for (let view of [viewState.CENSUS, viewState.DIFFING]) {
+ yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
+ view,
+ })), container);
+
+ ok(container.querySelector("#invert-tree-checkbox"),
+ `The invert checkbox is shown in view = ${view}`);
+ }
+
+ yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
+ view: viewState.DOMINATOR_TREE,
+ })), container);
+
+ ok(!container.querySelector("#invert-tree-checkbox"),
+ "The invert checkbox is NOT shown in the DOMINATOR_TREE view");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+ });
+ </script>
+ </pre>
+</body>
+</html>
--- a/devtools/client/memory/test/unit/head.js
+++ b/devtools/client/memory/test/unit/head.js
@@ -6,16 +6,18 @@
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
var { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
var { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
var { console } = Cu.import("resource://gre/modules/Console.jsm", {});
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
DevToolsUtils.testing = true;
+DevToolsUtils.dumpn.wantLogging = true;
+DevToolsUtils.dumpv.wantLogging = true;
var { OS } = require("resource://gre/modules/osfile.jsm");
var { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
var { TargetFactory } = require("devtools/client/framework/target");
var promise = require("promise");
var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
var { expectState } = require("devtools/server/actors/common");
var HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
--- a/devtools/client/memory/test/unit/test_action_diffing_05.js
+++ b/devtools/client/memory/test/unit/test_action_diffing_05.js
@@ -45,17 +45,17 @@ add_task(function *() {
const s3 = yield dispatch(takeSnapshot(front, heapWorker));
dispatch(readSnapshot(heapWorker, s1));
dispatch(readSnapshot(heapWorker, s2));
dispatch(readSnapshot(heapWorker, s3));
yield waitUntilSnapshotState(store, [snapshotState.READ,
snapshotState.READ,
snapshotState.READ]);
- dispatch(toggleDiffing());
+ yield dispatch(toggleDiffing());
dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
getState().snapshots[0]));
dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
getState().snapshots[1]));
yield waitUntilState(store,
state => state.diffing.state === diffingState.TOOK_DIFF);
const shouldTriggerRecompute = [
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_01.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can compute and fetch the dominator tree for a snapshot.
+
+let {
+ snapshotState: states,
+ dominatorTreeState,
+} = require("devtools/client/memory/constants");
+let {
+ takeSnapshotAndCensus,
+ computeAndFetchDominatorTree,
+} = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ ok(!getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view.");
+
+ // Change to the dominator tree view.
+ dispatch(computeAndFetchDominatorTree(heapWorker, getState().snapshots[0].id));
+ ok(getState().snapshots[0].dominatorTree,
+ "Should now have a dominator tree model for the selected snapshot");
+
+ // Wait for the dominator tree to start being computed.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.COMPUTING);
+ ok(true, "The dominator tree started computing");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root");
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true, "The dominator tree started fetching");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was fetched");
+ ok(getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_02.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that selecting the dominator tree view automatically kicks off fetching
+// and computing dominator trees.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeViewAndRefresh
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ ok(!getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view.");
+
+ dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
+ ok(getState().snapshots[0].dominatorTree,
+ "Should now have a dominator tree model for the selected snapshot");
+
+ // Wait for the dominator tree to start being computed.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.COMPUTING);
+ ok(true, "The dominator tree started computing");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root");
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true, "The dominator tree started fetching");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was fetched");
+ ok(getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_03.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that selecting the dominator tree view and then taking a snapshot
+// properly kicks off fetching and computing dominator trees.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ equal(getState().view, viewState.DOMINATOR_TREE,
+ "We should now be in the DOMINATOR_TREE view");
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to start being computed.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] && state.snapshots[0].dominatorTree);
+ equal(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.COMPUTING,
+ "The dominator tree started computing");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root");
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true, "The dominator tree started fetching");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was fetched");
+ ok(getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_04.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that selecting the dominator tree view while in the middle of taking a
+// snapshot properly kicks off fetching and computing dominator trees.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+ changeView
+} = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+
+ for (let intermediateSnapshotState of [states.SAVING,
+ states.READING,
+ states.SAVING_CENSUS]) {
+ dumpn(`Testing switching to the DOMINATOR_TREE view in the middle of the ${intermediateSnapshotState} snapshot state`);
+
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [intermediateSnapshotState]);
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ equal(getState().view, viewState.DOMINATOR_TREE,
+ "We should now be in the DOMINATOR_TREE view");
+
+ // Wait for the dominator tree to start being computed.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] && state.snapshots[0].dominatorTree);
+ equal(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.COMPUTING,
+ "The dominator tree started computing");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is computing, we should not have its root");
+
+ // Wait for the dominator tree to finish computing and start being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true, "The dominator tree started fetching");
+ ok(!getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is fetching, we should not have its root");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The dominator tree was fetched");
+ ok(getState().snapshots[0].dominatorTree.root,
+ "When the dominator tree is loaded, we should have its root");
+ }
+
+ heapWorker.destroy();
+ yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_05.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that changing the currently selected snapshot to a snapshot that does
+// not have a dominator tree will automatically compute and fetch one for it.
+
+let {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+let {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+} = require("devtools/client/memory/actions/snapshot");
+
+let { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+ states.SAVED_CENSUS]);
+
+ ok(getState().snapshots[1].selected, "The second snapshot is selected");
+
+ // Change to the dominator tree view.
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[1].dominatorTree &&
+ state.snapshots[1].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The second snapshot's dominator tree was fetched");
+
+ // Select the first snapshot.
+ dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[0].id));
+
+ // And now the first snapshot should have its dominator tree fetched and
+ // computed because of the new selection.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true, "The first snapshot's dominator tree was fetched");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_06.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can incrementally fetch a subtree of a dominator tree.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+ fetchImmediatelyDominated,
+} = require("devtools/client/memory/actions/snapshot");
+const DominatorTreeLazyChildren
+ = require("devtools/client/memory/dominator-tree-lazy-children");
+
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(getState().snapshots[0].dominatorTree.root,
+ "The dominator tree was fetched");
+
+ // Find a node that has children, but none of them are loaded.
+
+ function findNode(node) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNode(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const oldRoot = getState().snapshots[0].dominatorTree.root;
+ const oldNode = findNode(oldRoot);
+ ok(oldNode,
+ "Should have found a node with children that are not loaded since we " +
+ "only send partial dominator trees across initially and load the rest " +
+ "on demand");
+ ok(oldNode !== oldRoot, "But the node should not be the root");
+
+ const lazyChildren = new DominatorTreeLazyChildren(oldNode.nodeId, 0);
+ dispatch(fetchImmediatelyDominated(heapWorker, getState().snapshots[0].id, lazyChildren));
+
+ equal(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Fetching immediately dominated children should put us in the " +
+ "INCREMENTAL_FETCHING state");
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true,
+ "The dominator tree should go back to LOADED after the incremental " +
+ "fetching is done.");
+
+ const newRoot = getState().snapshots[0].dominatorTree.root;
+ ok(oldRoot !== newRoot,
+ "When we insert new nodes, we get a new tree");
+ equal(oldRoot.children.length, newRoot.children.length,
+ "The new tree's root should have the same number of children as the " +
+ "old root's");
+
+ let differentChildrenCount = 0;
+ for (let i = 0; i < oldRoot.children.length; i++) {
+ if (oldRoot.children[i] !== newRoot.children[i]) {
+ differentChildrenCount++;
+ }
+ }
+ equal(differentChildrenCount, 1,
+ "All subtrees except the subtree we inserted incrementally fetched " +
+ "children into should be the same because we use persistent updates");
+
+ // Find the new node which has the children inserted.
+
+ function findNewNode(node) {
+ if (node.nodeId === oldNode.nodeId) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNewNode(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const newNode = findNewNode(newRoot);
+ ok(newNode, "Should find the node in the new tree again");
+ ok(newNode !== oldNode, "We did not mutate the old node in place, instead created a new node");
+ ok(newNode.children, "And the new node should have the children attached");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_07.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can incrementally fetch two subtrees in the same dominator tree
+// concurrently. This exercises the activeFetchRequestCount machinery.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+} = require("devtools/client/memory/constants");
+const {
+ takeSnapshotAndCensus,
+ selectSnapshotAndRefresh,
+ fetchImmediatelyDominated,
+} = require("devtools/client/memory/actions/snapshot");
+const DominatorTreeLazyChildren
+ = require("devtools/client/memory/dominator-tree-lazy-children");
+
+const { changeView } = require("devtools/client/memory/actions/view");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(getState().snapshots[0].dominatorTree.root,
+ "The dominator tree was fetched");
+
+ // Find a node that has more children.
+
+ function findNode(node) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNode(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const oldRoot = getState().snapshots[0].dominatorTree.root;
+ const oldNode = findNode(oldRoot);
+ ok(oldNode, "Should have found a node with more children.");
+
+ // Find another node that has more children.
+ function findNodeRev(node) {
+ if (node.moreChildrenAvailable && !node.children) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children.slice().reverse()) {
+ const found = findNodeRev(child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const oldNode2 = findNodeRev(oldRoot);
+ ok(oldNode2, "Should have found another node with more children.");
+ ok(oldNode !== oldNode2,
+ "The second node should not be the same as the first one");
+
+ // Fetch both subtrees concurrently.
+ dispatch(fetchImmediatelyDominated(heapWorker, getState().snapshots[0].id,
+ new DominatorTreeLazyChildren(oldNode.nodeId, 0)));
+ dispatch(fetchImmediatelyDominated(heapWorker, getState().snapshots[0].id,
+ new DominatorTreeLazyChildren(oldNode2.nodeId, 0)));
+
+ equal(getState().snapshots[0].dominatorTree.state,
+ dominatorTreeState.INCREMENTAL_FETCHING,
+ "Fetching immediately dominated children should put us in the " +
+ "INCREMENTAL_FETCHING state");
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ ok(true,
+ "The dominator tree should go back to LOADED after the incremental " +
+ "fetching is done.");
+
+ const newRoot = getState().snapshots[0].dominatorTree.root;
+ ok(oldRoot !== newRoot,
+ "When we insert new nodes, we get a new tree");
+
+ // Find the new node which has the children inserted.
+
+ function findNodeWithId(id, node) {
+ if (node.nodeId === id) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = findNodeWithId(id, child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const newNode = findNodeWithId(oldNode.nodeId, newRoot);
+ ok(newNode, "Should find the node in the new tree again");
+ ok(newNode !== oldNode,
+ "We did not mutate the old node in place, instead created a new node");
+ ok(newNode.children.length,
+ "And the new node should have the new children attached");
+
+ const newNode2 = findNodeWithId(oldNode2.nodeId, newRoot);
+ ok(newNode2, "Should find the second node in the new tree again");
+ ok(newNode2 !== oldNode2,
+ "We did not mutate the second old node in place, instead created a new node");
+ ok(newNode2.children,
+ "And the new node should have the new children attached");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_08.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can change the breakdown with which we describe a dominator tree
+// and that the dominator tree is re-fetched.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+ dominatorTreeBreakdowns,
+} = require("devtools/client/memory/constants");
+const {
+ setDominatorTreeBreakdownAndRefresh
+} = require("devtools/client/memory/actions/dominatorTreeBreakdown");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+const {
+ takeSnapshotAndCensus,
+ computeAndFetchDominatorTree,
+} = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ ok(!getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view.");
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+
+ ok(getState().dominatorTreeBreakdown,
+ "We have a default breakdown for describing nodes in a dominator tree");
+ equal(getState().dominatorTreeBreakdown,
+ dominatorTreeBreakdowns.coarseType.breakdown,
+ "and the default is coarse type");
+ equal(getState().dominatorTreeBreakdown,
+ getState().snapshots[0].dominatorTree.breakdown,
+ "and the newly computed dominator tree has that breakdown");
+
+ // Switch to the internalType breakdown.
+ dispatch(setDominatorTreeBreakdownAndRefresh(
+ heapWorker,
+ dominatorTreeBreakdowns.internalType.breakdown));
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+ ok(true,
+ "switching breakdown types caused the dominator tree to be fetched " +
+ "again.");
+
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+ equal(getState().snapshots[0].dominatorTree.breakdown,
+ dominatorTreeBreakdowns.internalType.breakdown,
+ "The new dominator tree's breakdown is internalType");
+ equal(getState().dominatorTreeBreakdown,
+ dominatorTreeBreakdowns.internalType.breakdown,
+ "as is our requested dominator tree breakdown");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_09.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can change the breakdown with which we describe a dominator tree
+// while the dominator tree is in the middle of being fetched.
+
+const {
+ snapshotState: states,
+ dominatorTreeState,
+ viewState,
+ dominatorTreeBreakdowns,
+} = require("devtools/client/memory/constants");
+const {
+ setDominatorTreeBreakdownAndRefresh
+} = require("devtools/client/memory/actions/dominatorTreeBreakdown");
+const {
+ changeView,
+} = require("devtools/client/memory/actions/view");
+const {
+ takeSnapshotAndCensus,
+ computeAndFetchDominatorTree,
+} = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let front = new StubbedMemoryFront();
+ let heapWorker = new HeapAnalysesClient();
+ yield front.attach();
+ let store = Store();
+ let { getState, dispatch } = store;
+
+ dispatch(changeView(viewState.DOMINATOR_TREE));
+
+ dispatch(takeSnapshotAndCensus(front, heapWorker));
+ yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ ok(!getState().snapshots[0].dominatorTree,
+ "There shouldn't be a dominator tree model yet since it is not computed " +
+ "until we switch to the dominators view.");
+
+ // Wait for the dominator tree to start fetching.
+ yield waitUntilState(store, state =>
+ state.snapshots[0] &&
+ state.snapshots[0].dominatorTree &&
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+
+ ok(getState().dominatorTreeBreakdown,
+ "We have a default breakdown for describing nodes in a dominator tree");
+ equal(getState().dominatorTreeBreakdown,
+ dominatorTreeBreakdowns.coarseType.breakdown,
+ "and the default is coarse type");
+ equal(getState().dominatorTreeBreakdown,
+ getState().snapshots[0].dominatorTree.breakdown,
+ "and the newly computed dominator tree has that breakdown");
+
+ // Switch to the internalType breakdown while we are still fetching the
+ // dominator tree.
+ dispatch(setDominatorTreeBreakdownAndRefresh(
+ heapWorker,
+ dominatorTreeBreakdowns.internalType.breakdown));
+
+ // Wait for the dominator tree to finish being fetched.
+ yield waitUntilState(store, state =>
+ state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+
+ equal(getState().snapshots[0].dominatorTree.breakdown,
+ dominatorTreeBreakdowns.internalType.breakdown,
+ "The new dominator tree's breakdown is internalType");
+ equal(getState().dominatorTreeBreakdown,
+ dominatorTreeBreakdowns.internalType.breakdown,
+ "as is our requested dominator tree breakdown");
+
+ heapWorker.destroy();
+ yield front.detach();
+});
--- a/devtools/client/memory/test/unit/test_utils.js
+++ b/devtools/client/memory/test/unit/test_utils.js
@@ -28,18 +28,18 @@ add_task(function *() {
}), "utils.breakdownEquals() fails when deep properties do not match");
ok(!utils.breakdownEquals(breakdowns.allocationStack.breakdown, {
by: "allocationStack",
then: { by: "count", bytes: true },
noStack: { by: "count", count: true, bytes: true },
}), "utils.breakdownEquals() fails when deep properties are missing.");
- let s1 = utils.createSnapshot();
- let s2 = utils.createSnapshot();
+ let s1 = utils.createSnapshot({});
+ let s2 = utils.createSnapshot({});
equal(s1.state, states.SAVING, "utils.createSnapshot() creates snapshot in saving state");
ok(s1.id !== s2.id, "utils.createSnapshot() creates snapshot with unique ids");
ok(utils.breakdownEquals(utils.breakdownNameToSpec("coarseType"), breakdowns.coarseType.breakdown),
"utils.breakdownNameToSpec() works for presets");
ok(utils.breakdownEquals(utils.breakdownNameToSpec("coarseType"), breakdowns.coarseType.breakdown),
"utils.breakdownNameToSpec() works for presets");
--- a/devtools/client/memory/test/unit/xpcshell.ini
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -21,10 +21,19 @@ skip-if = toolkit == 'android' || toolki
[test_action-set-breakdown-and-refresh-02.js]
[test_action-take-census.js]
[test_action-take-snapshot.js]
[test_action-take-snapshot-and-census.js]
[test_action-toggle-inverted.js]
[test_action-toggle-inverted-and-refresh-01.js]
[test_action-toggle-inverted-and-refresh-02.js]
[test_action-toggle-recording-allocations.js]
+[test_dominator_trees_01.js]
+[test_dominator_trees_02.js]
+[test_dominator_trees_03.js]
+[test_dominator_trees_04.js]
+[test_dominator_trees_05.js]
+[test_dominator_trees_06.js]
+[test_dominator_trees_07.js]
+[test_dominator_trees_08.js]
+[test_dominator_trees_09.js]
[test_utils.js]
[test_utils-get-snapshot-totals.js]
--- a/devtools/client/memory/utils.js
+++ b/devtools/client/memory/utils.js
@@ -8,18 +8,25 @@ Cu.import("resource://devtools/client/sh
const STRINGS_URI = "chrome://devtools/locale/memory.properties"
const L10N = exports.L10N = new ViewHelpers.L10N(STRINGS_URI);
const { URL } = require("sdk/url");
const { OS } = require("resource://gre/modules/osfile.jsm");
const { assert } = require("devtools/shared/DevToolsUtils");
const { Preferences } = require("resource://gre/modules/Preferences.jsm");
const CUSTOM_BREAKDOWN_PREF = "devtools.memory.custom-breakdowns";
+const CUSTOM_DOMINATOR_TREE_BREAKDOWN_PREF = "devtools.memory.custom-dominator-tree-breakdowns";
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
-const { snapshotState: states, diffingState, breakdowns } = require("./constants");
+const {
+ snapshotState: states,
+ diffingState,
+ breakdowns,
+ dominatorTreeBreakdowns,
+ dominatorTreeState
+} = require("./constants");
/**
* Takes a snapshot object and returns the
* localized form of its timestamp to be used as a title.
*
* @param {Snapshot} snapshot
* @return {String}
*/
@@ -90,17 +97,16 @@ exports.getCustomBreakdowns = function (
/**
* Converts a breakdown preset name, like "allocationStack", and returns the
* spec for the breakdown. Also checks properties of keys in the `devtools.memory.custom-breakdowns`
* pref. If not found, returns an empty object.
*
* @param {String} name
* @return {Object}
*/
-
exports.breakdownNameToSpec = function (name) {
let customBreakdowns = exports.getCustomBreakdowns();
// If breakdown is already a breakdown, use it
if (typeof name === "object") {
return name;
}
// If it's in our custom breakdowns, use it
@@ -110,16 +116,91 @@ exports.breakdownNameToSpec = function (
// If breakdown name is in our presets, use that
else if (name in breakdowns) {
return breakdowns[name].breakdown;
}
return Object.create(null);
};
/**
+ * Returns an array of objects with the unique key `name` and `displayName` for
+ * each breakdown for dominator trees.
+ *
+ * @return {Array<Object>}
+ */
+exports.getDominatorTreeBreakdownDisplayData = function () {
+ return exports.getDominatorTreeBreakdownNames().map(name => {
+ // If it's a preset use the display name value
+ let preset = dominatorTreeBreakdowns[name];
+ let displayName = name;
+ if (preset && preset.displayName) {
+ displayName = preset.displayName;
+ }
+ return { name, displayName };
+ });
+};
+
+/**
+ * Returns an array of the unique names for each breakdown in
+ * presets and custom pref.
+ *
+ * @return {Array<Breakdown>}
+ */
+exports.getDominatorTreeBreakdownNames = function () {
+ let custom = exports.getCustomDominatorTreeBreakdowns();
+ return Object.keys(Object.assign({}, dominatorTreeBreakdowns, custom));
+};
+
+/**
+ * Returns custom breakdowns defined in `devtools.memory.custom-dominator-tree-breakdowns` pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomDominatorTreeBreakdowns = function () {
+ let customBreakdowns = Object.create(null);
+ try {
+ customBreakdowns = JSON.parse(Preferences.get(CUSTOM_DOMINATOR_TREE_BREAKDOWN_PREF)) || Object.create(null);
+ } catch (e) {
+ DevToolsUtils.reportException(
+ `String stored in "${CUSTOM_BREAKDOWN_PREF}" pref cannot be parsed by \`JSON.parse()\`.`);
+ }
+ return customBreakdowns;
+}
+
+/**
+ * Converts a dominator tree breakdown preset name, like "allocationStack", and
+ * returns the spec for the breakdown. Also checks properties of keys in the
+ * `devtools.memory.custom-breakdowns` pref. If not found, returns an empty
+ * object.
+ *
+ * @param {String} name
+ * @return {Object}
+ */
+exports.dominatorTreeBreakdownNameToSpec = function (name) {
+ let customBreakdowns = exports.getCustomDominatorTreeBreakdowns();
+
+ // If breakdown is already a breakdown, use it.
+ if (typeof name === "object") {
+ return name;
+ }
+
+ // If it's in our custom breakdowns, use it.
+ if (name in customBreakdowns) {
+ return customBreakdowns[name];
+ }
+
+ // If breakdown name is in our presets, use that.
+ if (name in dominatorTreeBreakdowns) {
+ return dominatorTreeBreakdowns[name].breakdown;
+ }
+
+ return Object.create(null);
+};
+
+/**
* Returns a string representing a readable form of the snapshot's state. More
* concise than `getStatusTextFull`.
*
* @param {snapshotState | diffingState} state
* @return {String}
*/
exports.getStatusText = function (state) {
assert(state, "Must have a state");
@@ -145,18 +226,32 @@ exports.getStatusText = function (state)
return L10N.getStr("snapshot.state.saving-census");
case diffingState.TAKING_DIFF:
return L10N.getStr("diffing.state.taking-diff");
case diffingState.SELECTING:
return L10N.getStr("diffing.state.selecting");
+ case dominatorTreeState.COMPUTING:
+ return L10N.getStr("dominatorTree.state.computing");
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ return L10N.getStr("dominatorTree.state.fetching");
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ return L10N.getStr("dominatorTree.state.incrementalFetching");
+
+ case dominatorTreeState.ERROR:
+ return L10N.getStr("dominatorTree.state.error");
+
// These states do not have any message to show as other content will be
// displayed.
+ case dominatorTreeState.LOADED:
case diffingState.TOOK_DIFF:
case states.READ:
case states.SAVED_CENSUS:
return "";
default:
assert(false, `Unexpected state: ${state}`);
return "";
@@ -194,18 +289,32 @@ exports.getStatusTextFull = function (st
return L10N.getStr("snapshot.state.saving-census.full");
case diffingState.TAKING_DIFF:
return L10N.getStr("diffing.state.taking-diff.full");
case diffingState.SELECTING:
return L10N.getStr("diffing.state.selecting.full");
+ case dominatorTreeState.COMPUTING:
+ return L10N.getStr("dominatorTree.state.computing.full");
+
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ return L10N.getStr("dominatorTree.state.fetching.full");
+
+ case dominatorTreeState.INCREMENTAL_FETCHING:
+ return L10N.getStr("dominatorTree.state.incrementalFetching.full");
+
+ case dominatorTreeState.ERROR:
+ return L10N.getStr("dominatorTree.state.error.full");
+
// These states do not have any full message to show as other content will
// be displayed.
+ case dominatorTreeState.LOADED:
case diffingState.TOOK_DIFF:
case states.READ:
case states.SAVED_CENSUS:
return "";
default:
assert(false, `Unexpected state: ${state}`);
return "";
@@ -231,30 +340,42 @@ exports.snapshotIsDiffable = function sn
* the snapshot passed in.
*
* @param {appModel} state
* @param {snapshotId} id
* @return {snapshotModel|null}
*/
exports.getSnapshot = function getSnapshot (state, id) {
const found = state.snapshots.find(s => s.id === id);
- assert(found, `No matching snapshot found for ${id}`);
+ assert(found, `No matching snapshot found with id = ${id}`);
return found;
};
/**
* Creates a new snapshot object.
*
+ * @param {appModel} state
* @return {Snapshot}
*/
let ID_COUNTER = 0;
-exports.createSnapshot = function createSnapshot() {
+exports.createSnapshot = function createSnapshot(state) {
+ let dominatorTree = null;
+ if (state.view === dominatorTreeState.DOMINATOR_TREE) {
+ dominatorTree = Object.freeze({
+ dominatorTreeId: null,
+ root: null,
+ error: null,
+ state: dominatorTreeState.COMPUTING,
+ });
+ }
+
return Object.freeze({
id: ++ID_COUNTER,
state: states.SAVING,
+ dominatorTree,
census: null,
path: null,
imported: false,
selected: false,
error: null,
});
};
@@ -312,16 +433,30 @@ const breakdownEquals = exports.breakdow
exports.censusIsUpToDate = function (inverted, filter, breakdown, census) {
return census
&& inverted === census.inverted
&& filter === census.filter
&& breakdownEquals(breakdown, census.breakdown);
};
/**
+ * Returns true if the given snapshot's dominator tree has been computed, false
+ * otherwise.
+ *
+ * @param {SnapshotModel} snapshot
+ * @returns {Boolean}
+ */
+exports.dominatorTreeIsComputed = function (snapshot) {
+ return snapshot.dominatorTree &&
+ (snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
+ snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
+ snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING);
+};
+
+/**
* Takes a snapshot and returns the total bytes and total count that this
* snapshot represents.
*
* @param {CensusModel} census
* @return {Object}
*/
exports.getSnapshotTotals = function (census) {
let bytes = 0;
@@ -376,8 +511,29 @@ exports.openFilePicker = function({ titl
resolve(null);
return;
}
resolve(fp.file);
}
});
});
};
+
+/**
+ * Creates a hash map mapping node IDs to its parent node.
+ *
+ * @param {CensusTreeNode} node
+ * @param {Object<number, TreeNode>} aggregator
+ *
+ * @return {Object<number, TreeNode>}
+ */
+const createParentMap = exports.createParentMap = function (node,
+ getId = node => node.id,
+ aggregator = Object.create(null)) {
+ if (node.children) {
+ for (let child of node.children) {
+ aggregator[getId(child)] = node;
+ createParentMap(child, getId, aggregator);
+ }
+ }
+
+ return aggregator;
+};
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -98,16 +98,17 @@ pref("devtools.debugger.ui.panes-visible
pref("devtools.debugger.ui.variables-sorting-enabled", true);
pref("devtools.debugger.ui.variables-only-enum-visible", false);
pref("devtools.debugger.ui.variables-searchbox-visible", false);
// Enable the Memory tools
pref("devtools.memory.enabled", false);
pref("devtools.memory.custom-breakdowns", "{}");
+pref("devtools.memory.custom-dominator-tree-breakdowns", "{}");
// Enable the Performance tools
pref("devtools.performance.enabled", true);
// The default Performance UI settings
pref("devtools.performance.memory.sample-probability", "0.05");
// Can't go higher than this without causing internal allocation overflows while
// serializing the allocations data over the RDP.
--- a/devtools/client/shared/components/frame.js
+++ b/devtools/client/shared/components/frame.js
@@ -1,17 +1,17 @@
/* 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 } = require("devtools/client/shared/vendor/react");
const { getSourceNames } = require("devtools/client/shared/source-utils");
const Frame = module.exports = createClass({
- displayName: "frame-view",
+ displayName: "Frame",
propTypes: {
// SavedFrame
frame: PropTypes.object.isRequired,
// Clicking on the frame link -- probably should link to the debugger.
onClick: PropTypes.func.isRequired,
// Tooltip to display when hovering over the link to the frame;
// Something like "View source in debugger -> http://foo.com/file.js:100:2".
--- a/devtools/client/shared/components/test/mochitest/head.js
+++ b/devtools/client/shared/components/test/mochitest/head.js
@@ -3,25 +3,16 @@
"use strict";
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://testing-common/Assert.jsm");
Cu.import("resource://gre/modules/Task.jsm");
var { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
-// Disable logging for all the tests. Both the debugger server and frontend will
-// be affected by this pref.
-var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
-Services.prefs.setBoolPref("devtools.debugger.log", false);
-
-// Enable the memory tool for all tests.
-var gMemoryToolEnabled = Services.prefs.getBoolPref("devtools.memory.enabled");
-Services.prefs.setBoolPref("devtools.memory.enabled", true);
-
var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
var { require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
var { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
var { DebuggerServer } = require("devtools/server/main");
var { DebuggerClient } = require("devtools/shared/client/main");
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
var { TargetFactory } = require("devtools/client/framework/target");
@@ -78,121 +69,40 @@ var TEST_TREE = {
H: "C",
I: "C",
J: "D",
K: "E",
L: "E",
M: null,
N: "M",
O: "N"
- }
+ },
+ expanded: new Set(),
};
var TEST_TREE_INTERFACE = {
getParent: x => TEST_TREE.parent[x],
getChildren: x => TEST_TREE.children[x],
renderItem: (x, depth, focused, arrow) => "-".repeat(depth) + x + ":" + focused + "\n",
getRoots: () => ["A", "M"],
getKey: x => "key-" + x,
itemHeight: 1,
+ onExpand: x => TEST_TREE.expanded.add(x),
+ onCollapse: x => TEST_TREE.expanded.delete(x),
+ isExpanded: x => TEST_TREE.expanded.has(x),
};
+function forceRender(tree) {
+ return setState(tree, {})
+ .then(() => setState(tree, {}));
+}
+
// All tests are asynchronous.
SimpleTest.waitForExplicitFinish();
-SimpleTest.registerCleanupFunction(() => {
- info("finish() was called, cleaning up...");
- Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
- Services.prefs.setBoolPref("devtools.memory.enabled", gMemoryToolEnabled);
-});
-
-function addTab(url) {
- info("Adding tab: " + url);
-
- var deferred = promise.defer();
- var tab = gBrowser.selectedTab = gBrowser.addTab(url);
- var linkedBrowser = tab.linkedBrowser;
-
- linkedBrowser.addEventListener("load", function onLoad() {
- linkedBrowser.removeEventListener("load", onLoad, true);
- info("Tab added and finished loading: " + url);
- deferred.resolve(tab);
- }, true);
-
- return deferred.promise;
-}
-
-function removeTab(tab) {
- info("Removing tab.");
-
- var deferred = promise.defer();
- var tabContainer = gBrowser.tabContainer;
-
- tabContainer.addEventListener("TabClose", function onClose(aEvent) {
- tabContainer.removeEventListener("TabClose", onClose, false);
- info("Tab removed and finished closing.");
- deferred.resolve();
- }, false);
-
- gBrowser.removeTab(tab);
- return deferred.promise;
-}
-
-var withTab = Task.async(function* (url, generator) {
- var tab = yield addTab(url);
- try {
- yield* generator(tab);
- } finally {
- yield removeTab(tab);
- }
-});
-
-var openMemoryTool = Task.async(function* (tab) {
- info("Initializing a memory panel.");
-
- var target = TargetFactory.forTab(tab);
- var debuggee = target.window.wrappedJSObject;
-
- yield target.makeRemote();
-
- var toolbox = yield gDevTools.showToolbox(target, "memory");
- var panel = toolbox.getCurrentPanel();
- return [target, debuggee, panel];
-});
-
-var closeMemoryTool = Task.async(function* (panel) {
- info("Closing a memory panel");
- yield panel._toolbox.destroy();
-});
-
-var withMemoryTool = Task.async(function* (tab, generator) {
- var [target, debuggee, panel] = yield openMemoryTool(tab);
- try {
- yield* generator(target, debuggee, panel);
- } finally {
- yield closeMemoryTool(panel);
- }
-});
-
-var withTabAndMemoryTool = Task.async(function* (url, generator) {
- yield withTab(url, function* (tab) {
- yield withMemoryTool(tab, function* (target, debuggee, panel) {
- yield* generator(tab, target, debuggee, panel);
- });
- });
-});
-
-function reload(target) {
- info("Reloading tab.");
- var deferred = promise.defer();
- target.once("navigate", deferred.resolve);
- target.activeTab.reload();
- return deferred.promise;
-}
-
function onNextAnimationFrame(fn) {
return () =>
requestAnimationFrame(() =>
requestAnimationFrame(fn));
}
function setState(component, newState) {
var deferred = promise.defer();
--- a/devtools/client/shared/components/test/mochitest/test_tree_01.html
+++ b/devtools/client/shared/components/test/mochitest/test_tree_01.html
@@ -24,19 +24,18 @@ window.onload = Task.async(function* ()
ok(Tree, "Should get Tree");
const t = Tree(TEST_TREE_INTERFACE);
ok(t, "Should be able to create Tree instances");
const tree = ReactDOM.render(t, window.document.body);
ok(tree, "Should be able to mount Tree instances");
- yield setState(tree, {
- expanded: new Set("ABCDEFGHIJKLMNO".split(""))
- });
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:false",
"---K:false",
"---L:false",
"--F:false",
--- a/devtools/client/shared/components/test/mochitest/test_tree_02.html
+++ b/devtools/client/shared/components/test/mochitest/test_tree_02.html
@@ -16,19 +16,18 @@ Test that collapsed subtrees aren't rend
window.onload = Task.async(function* () {
try {
let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
let React = browserRequire("devtools/client/shared/vendor/react");
let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
- yield setState(tree, {
- expanded: new Set("MNO".split(""))
- });
+ TEST_TREE.expanded = new Set("MNO".split(""));
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"M:false",
"-N:false",
"--O:false",
], "Collapsed subtrees shouldn't be rendered");
} catch(e) {
--- a/devtools/client/shared/components/test/mochitest/test_tree_04.html
+++ b/devtools/client/shared/components/test/mochitest/test_tree_04.html
@@ -15,18 +15,18 @@ Test that we only render visible tree it
<script type="application/javascript;version=1.8">
window.onload = Task.async(function* () {
try {
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
const React = browserRequire("devtools/client/shared/vendor/react");
const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
yield setState(tree, {
- expanded: new Set("ABCDEFGHIJKLMNO".split("")),
height: 3,
scroll: 1
});
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:false",
--- a/devtools/client/shared/components/test/mochitest/test_tree_05.html
+++ b/devtools/client/shared/components/test/mochitest/test_tree_05.html
@@ -15,21 +15,23 @@ Test focusing with the Tree component.
<script type="application/javascript;version=1.8">
window.onload = Task.async(function* () {
try {
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
const React = browserRequire("devtools/client/shared/vendor/react");
const { Simulate } = React.addons.TestUtils;
const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
- const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
- yield setState(tree, {
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield setProps(tree, {
focused: "G",
- expanded: new Set("ABCDEFGHIJKLMNO".split(""))
});
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:false",
"---K:false",
"---L:false",
@@ -42,19 +44,17 @@ window.onload = Task.async(function* ()
"--J:false",
"M:false",
"-N:false",
"--O:false",
], "G should be focused");
// Click the first tree node
Simulate.click(document.querySelector(".tree-node"));
-
- // Let the next render happen.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:true",
"-B:false",
"--E:false",
"---K:false",
"---L:false",
"--F:false",
--- a/devtools/client/shared/components/test/mochitest/test_tree_06.html
+++ b/devtools/client/shared/components/test/mochitest/test_tree_06.html
@@ -14,32 +14,31 @@ Test keyboard navigation with the Tree c
<script src="head.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript;version=1.8">
window.onload = Task.async(function* () {
try {
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
const React = browserRequire("devtools/client/shared/vendor/react");
const { Simulate } = React.addons.TestUtils;
const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
- const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
- yield setState(tree, {
- expanded: new Set("ABCDEFGHIJKLMNO".split(""))
- });
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
// UP ----------------------------------------------------------------------
info("Up to the previous sibling.");
- yield setState(tree, {
+ yield setProps(tree, {
focused: "L"
});
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:false",
"---K:true",
"---L:false",
"--F:false",
@@ -52,18 +51,17 @@ window.onload = Task.async(function* ()
"M:false",
"-N:false",
"--O:false",
], "After the UP, K should be focused.");
info("Up to the parent.");
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:true",
"---K:false",
"---L:false",
"--F:false",
@@ -75,22 +73,21 @@ window.onload = Task.async(function* ()
"--J:false",
"M:false",
"-N:false",
"--O:false",
], "After the UP, E should be focused.");
info("Try and navigate up, past the first item.");
- yield setState(tree, {
+ yield setProps(tree, {
focused: "A"
});
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:true",
"-B:false",
"--E:false",
"---K:false",
"---L:false",
"--F:false",
@@ -102,25 +99,24 @@ window.onload = Task.async(function* ()
"--J:false",
"M:false",
"-N:false",
"--O:false",
], "After the UP, A should be focused and we shouldn't have overflowed past it.");
// DOWN --------------------------------------------------------------------
- yield setState(tree, {
+ yield setProps(tree, {
focused: "K"
});
info("Down to next sibling.");
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:false",
"---K:false",
"---L:true",
"--F:false",
@@ -133,18 +129,17 @@ window.onload = Task.async(function* ()
"M:false",
"-N:false",
"--O:false",
], "After the DOWN, L should be focused.");
info("Down to parent's next sibling.");
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:false",
"---K:false",
"---L:false",
"--F:true",
@@ -156,22 +151,21 @@ window.onload = Task.async(function* ()
"--J:false",
"M:false",
"-N:false",
"--O:false",
], "After the DOWN, F should be focused.");
info("Try and go down past the last item.");
- yield setState(tree, {
+ yield setProps(tree, {
focused: "O"
});
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:false",
"---K:false",
"---L:false",
"--F:false",
@@ -185,22 +179,21 @@ window.onload = Task.async(function* ()
"-N:false",
"--O:true",
], "After the DOWN, O should still be focused and we shouldn't have overflowed past it.");
// LEFT --------------------------------------------------------------------
info("Left to go to parent.");
- yield setState(tree, {
+ yield setProps(tree, {
focused: "L"
})
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:true",
"---K:false",
"---L:false",
"--F:false",
@@ -213,18 +206,17 @@ window.onload = Task.async(function* ()
"M:false",
"-N:false",
"--O:false",
], "After the LEFT, E should be focused.");
info("Left to collapse children.");
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:true",
"--F:false",
"--G:false",
"-C:false",
@@ -237,18 +229,17 @@ window.onload = Task.async(function* ()
"--O:false",
], "After the LEFT, E's children should be collapsed.");
// RIGHT -------------------------------------------------------------------
info("Right to expand children.");
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:true",
"---K:false",
"---L:false",
"--F:false",
@@ -261,18 +252,17 @@ window.onload = Task.async(function* ()
"M:false",
"-N:false",
"--O:false",
], "After the RIGHT, E's children should be expanded again.");
info("Right to go to next item.");
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
- // Let the component re-render.
- yield setState(tree, {});
+ yield forceRender(tree);
isRenderedTree(document.body.textContent, [
"A:false",
"-B:false",
"--E:false",
"---K:true",
"---L:false",
"--F:false",
--- a/devtools/client/shared/components/test/mochitest/test_tree_07.html
+++ b/devtools/client/shared/components/test/mochitest/test_tree_07.html
@@ -29,34 +29,31 @@ window.onload = Task.async(function* ()
style: { marginLeft: depth * 16 + "px" }
},
arrow,
item
);
}
});
- yield setState(tree, {
- expanded: new Set("ABCDEFGHIJKLMNO".split(""))
- });
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield forceRender(tree);
let arrows = document.querySelectorAll(".arrow");
for (let a of arrows) {
ok(a.classList.contains("open"), "Every arrow should be open.");
}
- yield setState(tree, {
- expanded: new Set()
- });
+ TEST_TREE.expanded = new Set();
+ yield forceRender(tree);
arrows = document.querySelectorAll(".arrow");
for (let a of arrows) {
ok(!a.classList.contains("open"), "Every arrow should be closed.");
}
-
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
});
</script>
</pre>
--- a/devtools/client/shared/components/test/mochitest/test_tree_08.html
+++ b/devtools/client/shared/components/test/mochitest/test_tree_08.html
@@ -16,27 +16,28 @@ other inputs.
<script src="head.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript;version=1.8">
window.onload = Task.async(function* () {
try {
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
const React = browserRequire("devtools/client/shared/vendor/react");
const { Simulate } = React.addons.TestUtils;
const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
- const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
const input = document.createElement("input");
document.body.appendChild(input);
input.focus();
is(document.activeElement, input, "The text input should be focused.");
Simulate.click(document.querySelector(".tree-node"));
- // Let the tree re-render, because focus is dealt with on componentDidUpdate.
- yield setState(tree, {});
+ yield forceRender(tree);
isnot(document.activeElement, input,
"The input should have had it's focus stolen by clicking on a tree item.");
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
--- a/devtools/client/shared/components/test/mochitest/test_tree_09.html
+++ b/devtools/client/shared/components/test/mochitest/test_tree_09.html
@@ -22,44 +22,48 @@ window.onload = Task.async(function* ()
const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
let numberOfExpands = 0;
let lastExpandedItem = null;
let numberOfCollapses = 0;
let lastCollapsedItem = null;
- const tree = ReactDOM.render(Tree(Object.assign({
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ autoExpandDepth: 0,
onExpand: item => {
lastExpandedItem = item;
numberOfExpands++;
+ TEST_TREE.expanded.add(item);
},
onCollapse: item => {
lastCollapsedItem = item;
numberOfCollapses++;
+ TEST_TREE.expanded.delete(item);
},
- }, TEST_TREE_INTERFACE)), window.document.body);
+ onFocus: item => setProps(tree, { focused: item }),
+ })), window.document.body);
- yield setState(tree, {
+ yield setProps(tree, {
focused: "A"
});
is(lastExpandedItem, null);
is(lastCollapsedItem, null);
// Expand "A" via the keyboard and then let the component re-render.
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
- yield setState(tree, {});
+ yield forceRender(tree);
is(lastExpandedItem, "A", "Our onExpand callback should have been fired.");
is(numberOfExpands, 1);
// Now collapse "A" via the keyboard and then let the component re-render.
Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
- yield setState(tree, {});
+ yield forceRender(tree);
is(lastCollapsedItem, "A", "Our onCollapsed callback should have been fired.");
is(numberOfCollapses, 1);
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
--- a/devtools/client/shared/components/test/mochitest/test_tree_10.html
+++ b/devtools/client/shared/components/test/mochitest/test_tree_10.html
@@ -20,17 +20,17 @@ window.onload = Task.async(function* ()
const React = browserRequire("devtools/client/shared/vendor/react");
const { Simulate } = React.addons.TestUtils;
const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
const tree = ReactDOM.render(Tree(Object.assign({
autoExpandDepth: 1
}, TEST_TREE_INTERFACE)), window.document.body);
- yield setState(tree, {
+ yield setProps(tree, {
focused: "A"
});
isRenderedTree(document.body.textContent, [
"A:true",
"-B:false",
"-C:false",
"-D:false",
--- a/devtools/client/shared/components/tree.js
+++ b/devtools/client/shared/components/tree.js
@@ -38,16 +38,22 @@ const ArrowExpander = createFactory(crea
};
}
return dom.div(attrs);
}
}));
const TreeNode = createFactory(createClass({
+ componentDidMount() {
+ if (this.props.focused) {
+ this.refs.button.focus();
+ }
+ },
+
componentDidUpdate() {
if (this.props.focused) {
this.refs.button.focus();
}
},
render() {
const arrow = ArrowExpander({
@@ -140,50 +146,52 @@ const Tree = module.exports = createClas
getChildren: PropTypes.func.isRequired,
// A function which takes an item and ArrowExpander and returns a
// component.
renderItem: PropTypes.func.isRequired,
// A function which returns the roots of the tree (forest).
getRoots: PropTypes.func.isRequired,
// A function to get a unique key for the given item.
getKey: PropTypes.func.isRequired,
+ // A function to get whether an item is expanded or not. If an item is not
+ // expanded, then it must be collapsed.
+ isExpanded: PropTypes.func.isRequired,
// The height of an item in the tree including margin and padding, in
// pixels.
itemHeight: PropTypes.number.isRequired,
// Optional props
+ // The currently focused item, if any such item exists.
+ focused: PropTypes.any,
+ // Handle when a new item is focused.
+ onFocus: PropTypes.func,
// The depth to which we should automatically expand new items.
autoExpandDepth: PropTypes.number,
// A predicate that returns true if the last DFS traversal that was cached
// can be reused, false otherwise. The predicate function is passed the
// cached traversal as an array of nodes.
reuseCachedTraversal: PropTypes.func,
// Optional event handlers for when items are expanded or collapsed.
onExpand: PropTypes.func,
onCollapse: PropTypes.func,
},
getDefaultProps() {
return {
- expanded: new Set(),
- seen: new Set(),
- focused: undefined,
autoExpandDepth: AUTO_EXPAND_DEPTH,
reuseCachedTraversal: null,
};
},
getInitialState() {
return {
scroll: 0,
height: window.innerHeight,
- expanded: new Set(),
seen: new Set(),
- focused: undefined,
cachedTraversal: undefined,
};
},
componentDidMount() {
window.addEventListener("resize", this._updateHeight);
this._autoExpand();
this._updateHeight();
@@ -206,17 +214,17 @@ const Tree = module.exports = createClas
// not use the usual DFS infrastructure because we don't want to ignore
// collapsed nodes.
const autoExpand = (item, currentDepth) => {
if (currentDepth >= this.props.autoExpandDepth ||
this.state.seen.has(item)) {
return;
}
- this.state.expanded.add(item);
+ this.props.onExpand(item);
this.state.seen.add(item);
for (let child of this.props.getChildren(item)) {
autoExpand(item, currentDepth + 1);
}
};
for (let root of this.props.getRoots()) {
@@ -247,22 +255,22 @@ const Tree = module.exports = createClas
for (let i = 0; i < toRender.length; i++) {
let { item, depth } = toRender[i];
nodes.push(TreeNode({
key: this.props.getKey(item),
item: item,
depth: depth,
renderItem: this.props.renderItem,
- focused: this.state.focused === item,
- expanded: this.state.expanded.has(item),
+ focused: this.props.focused === item,
+ expanded: this.props.isExpanded(item),
hasChildren: !!this.props.getChildren(item).length,
onExpand: this._onExpand,
onCollapse: this._onCollapse,
- onFocus: () => this._onFocus(item)
+ onFocus: () => this._focus(item)
}));
}
nodes.push(dom.div({
key: "bottom-spacer",
style: {
padding: 0,
margin: 0,
@@ -295,17 +303,17 @@ const Tree = module.exports = createClas
},
/**
* Perform a pre-order depth-first search from item.
*/
_dfs(item, maxDepth = Infinity, traversal = [], _depth = 0) {
traversal.push({ item, depth: _depth });
- if (!this.state.expanded.has(item)) {
+ if (!this.props.isExpanded(item)) {
return traversal;
}
const nextDepth = _depth + 1;
if (nextDepth > maxDepth) {
return traversal;
}
@@ -343,75 +351,63 @@ const Tree = module.exports = createClas
/**
* Expands current row.
*
* @param {Object} item
* @param {Boolean} expandAllChildren
*/
_onExpand: oncePerAnimationFrame(function (item, expandAllChildren) {
- this.state.expanded.add(item);
-
if (this.props.onExpand) {
this.props.onExpand(item);
- }
- if (expandAllChildren) {
- for (let { item: child } of this._dfs(item)) {
- this.state.expanded.add(child);
-
- if (this.props.onExpand) {
+ if (expandAllChildren) {
+ for (let { item: child } of this._dfs(item)) {
this.props.onExpand(child);
}
}
}
this.setState({
- expanded: this.state.expanded,
cachedTraversal: null,
});
}),
/**
* Collapses current row.
*
* @param {Object} item
*/
_onCollapse: oncePerAnimationFrame(function (item) {
- this.state.expanded.delete(item);
-
if (this.props.onCollapse) {
this.props.onCollapse(item);
}
this.setState({
- expanded: this.state.expanded,
cachedTraversal: null,
});
}),
/**
* Sets the passed in item to be the focused item.
*
* @param {Object} item
*/
- _onFocus: oncePerAnimationFrame(function (item) {
- this.setState({
- focused: item
- });
- }),
+ _focus: function (item) {
+ if (this.props.onFocus) {
+ this.props.onFocus(item);
+ }
+ },
/**
* Sets the state to have no focused item.
*/
- _onBlur: oncePerAnimationFrame(function () {
- this.setState({
- focused: undefined
- });
- }),
+ _onBlur: function () {
+ this._focus(undefined);
+ },
/**
* Fired on a scroll within the tree's container, updates
* the stored position of the view port to handle virtual view rendering.
*
* @param {Event} e
*/
_onScroll: oncePerAnimationFrame(function (e) {
@@ -422,17 +418,17 @@ const Tree = module.exports = createClas
}),
/**
* Handles key down events in the tree's container.
*
* @param {Event} e
*/
_onKeyDown(e) {
- if (this.state.focused == null) {
+ if (this.props.focused == null) {
return;
}
// Prevent scrolling when pressing navigation keys. Guard against mocked
// events received when testing.
if (e.nativeEvent && e.nativeEvent.preventDefault) {
ViewHelpers.preventScrolling(e.nativeEvent);
}
@@ -442,27 +438,27 @@ const Tree = module.exports = createClas
this._focusPrevNode();
return false;
case "ArrowDown":
this._focusNextNode();
return false;
case "ArrowLeft":
- if (this.state.expanded.has(this.state.focused)
- && this.props.getChildren(this.state.focused).length) {
- this._onCollapse(this.state.focused);
+ if (this.props.isExpanded(this.props.focused)
+ && this.props.getChildren(this.props.focused).length) {
+ this._onCollapse(this.props.focused);
} else {
this._focusParentNode();
}
return false;
case "ArrowRight":
- if (!this.state.expanded.has(this.state.focused)) {
- this._onExpand(this.state.focused);
+ if (!this.props.isExpanded(this.props.focused)) {
+ this._onExpand(this.props.focused);
} else {
this._focusNextNode();
}
return false;
}
},
/**
@@ -470,62 +466,56 @@ const Tree = module.exports = createClas
*/
_focusPrevNode: oncePerAnimationFrame(function () {
// Start a depth first search and keep going until we reach the currently
// focused node. Focus the previous node in the DFS, if it exists. If it
// doesn't exist, we're at the first node already.
let prev;
for (let { item } of this._dfsFromRoots()) {
- if (item === this.state.focused) {
+ if (item === this.props.focused) {
break;
}
prev = item;
}
if (prev === undefined) {
return;
}
- this.setState({
- focused: prev
- });
+ this._focus(prev);
}),
/**
* Handles the down arrow key which will focus either the next child
* or sibling row.
*/
_focusNextNode: oncePerAnimationFrame(function () {
// Start a depth first search and keep going until we reach the currently
// focused node. Focus the next node in the DFS, if it exists. If it
// doesn't exist, we're at the last node already.
const traversal = this._dfsFromRoots();
let i = 0;
for (let { item } of traversal) {
- if (item === this.state.focused) {
+ if (item === this.props.focused) {
break;
}
i++;
}
if (i + 1 < traversal.length) {
- this.setState({
- focused: traversal[i + 1].item
- });
+ this._focus(traversal[i + 1].item);
}
}),
/**
* Handles the left arrow key, going back up to the current rows'
* parent row.
*/
_focusParentNode: oncePerAnimationFrame(function () {
- const parent = this.props.getParent(this.state.focused);
+ const parent = this.props.getParent(this.props.focused);
if (parent) {
- this.setState({
- focused: parent
- });
+ this._focus(parent);
}
}),
});
--- a/devtools/client/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -85,16 +85,24 @@ html, body, #app, #memory-tool {
align-items: center;
margin-inline-end: 5px;
}
.devtools-toolbar > .toolbar-group > label.breakdown-by > span {
margin-inline-end: 5px;
}
+.devtools-toolbar > label {
+ margin-inline-end: 5px;
+}
+
+#select-view {
+ margin-inline-start: 5px;
+}
+
#take-snapshot::before {
background-image: url(images/command-screenshot.png);
}
@media (min-resolution: 1.1dppx) {
#take-snapshot::before {
background-image: url(images/command-screenshot@2x.png);
}
}
@@ -409,16 +417,24 @@ html, body, #app, #memory-tool {
.theme-light .error::before {
background-image: url(chrome://devtools/skin/images/webconsole.svg#light-icons);
}
/**
* Frame View components
*/
+.separator,
+.not-available,
+.heap-tree-item-address {
+ opacity: .5;
+ margin-left: .5em;
+ margin-right: .5em;
+}
+
.focused .frame-link-filename,
.focused .frame-link-column,
.focused .frame-link-line,
.focused .frame-link-host,
.focused .frame-link-colon {
color: var(--theme-selection-color);
}