--- a/devtools/client/locales/en-US/memory.properties
+++ b/devtools/client/locales/en-US/memory.properties
@@ -72,16 +72,20 @@ censusDisplays.coarseType.tooltip=Group
# LOCALIZATION NOTE (censusDisplays.allocationStack.tooltip): The tooltip for
# the "allocation stack" display option.
censusDisplays.allocationStack.tooltip=Group items by the JavaScript stack recorded when the object was allocated
# LOCALIZATION NOTE (censusDisplays.invertedAllocationStack.tooltip): The
# tooltip for the "inverted allocation stack" display option.
censusDisplays.invertedAllocationStack.tooltip=Group items by the inverted JavaScript call stack recorded when the object was created
+# LOCALIZATION NOTE (breakdowns.treeMap.tooltip): The tooltip for the "tree map"
+# breakdown option.
+censusDisplays.treeMap.tooltip=Visualize memory usage: larger blocks account for a larger percent of memory usage
+
# LOCALIZATION NOTE (censusDisplays.objectClass.tooltip): The tooltip for the
# "object class" display option.
censusDisplays.objectClass.tooltip=Group items by their JavaScript Object [[class]] name
# LOCALIZATION NOTE (censusDisplays.internalType.tooltip): The tooltip for the
# "internal type" display option.
censusDisplays.internalType.tooltip=Group items by their internal C++ type
@@ -100,16 +104,20 @@ dominatorTreeDisplays.coarseType.tooltip
# LOCALIZATION NOTE (dominatorTreeDisplays.allocationStack.tooltip): The
# tooltip for the "allocation stack" dominator tree display option.
dominatorTreeDisplays.allocationStack.tooltip=Label objects by the JavaScript stack recorded when it was allocated
# LOCALIZATION NOTE (dominatorTreeDisplays.internalType.tooltip): The
# tooltip for the "internal type" dominator tree display option.
dominatorTreeDisplays.internalType.tooltip=Label objects by their internal C++ type name
+# LOCALIZATION NOTE (treeMapDisplays.coarseType.tooltip): The tooltip for
+# the "coarse type" tree map display option.
+treeMapDisplays.coarseType.tooltip=Label objects by the broad categories they fit in
+
# LOCALIZATION NOTE (toolbar.view): The label for the view selector in the
# toolbar.
toolbar.view=View:
# LOCALIZATION NOTE (toolbar.view.tooltip): The tooltip for the label for the
# view selector in the toolbar.
toolbar.view.tooltip=Change the view of the heap snapshot
@@ -124,16 +132,24 @@ toolbar.view.census.tooltip=View a summary of the heap snapshot’s contents by aggregating objects into groups
# LOCALIZATION NOTE (toolbar.view.dominators): The label for the dominators view
# option in the toolbar.
toolbar.view.dominators=Dominators
# LOCALIZATION NOTE (toolbar.view.dominators.tooltip): The tooltip for the label
# for the dominators view option in the toolbar.
toolbar.view.dominators.tooltip=View the dominator tree and surface the largest structures in the heap snapshot
+# LOCALIZATION NOTE (toolbar.view.treemap): The label for the tree map option
+# in the toolbar.
+toolbar.view.treemap=Tree Map
+
+# LOCALIZATION NOTE (toolbar.view.treemap.tooltip): The tooltip for the label for
+# the tree map view option in the toolbar.
+toolbar.view.treemap.tooltip=Visualize memory usage: larger blocks account for a larger percent of memory usage
+
# 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.
import-snapshot=Import…
@@ -266,16 +282,20 @@ snapshot.state.importing.full=Importing…
# snapshot state READING, and SAVED, due to these states being combined
# visually, used in the main heap view.
snapshot.state.reading.full=Reading snapshot…
# LOCALIZATION NOTE (snapshot.state.saving-census.full): The label describing
# the snapshot state SAVING, used in the main heap view.
snapshot.state.saving-census.full=Saving census…
+# LOCALIZATION NOTE (snapshot.state.saving-tree-map.full): The label describing
+# the snapshot state SAVING, used in the main heap view.
+snapshot.state.saving-tree-map.full=Saving tree map…
+
# LOCALIZATION NOTE (snapshot.state.error.full): The label describing the
# snapshot state ERROR, used in the main heap view.
snapshot.state.error.full=There was an error processing this snapshot.
# LOCALIZATION NOTE (snapshot.state.saving): The label describing the snapshot
# state SAVING, used in the snapshot list view
snapshot.state.saving=Saving snapshot…
@@ -287,16 +307,20 @@ snapshot.state.importing=Importing snapshot…
# state READING, and SAVED, due to these states being combined visually, used in
# the snapshot list view.
snapshot.state.reading=Reading snapshot…
# LOCALIZATION NOTE (snapshot.state.saving-census): The label describing the
# snapshot state SAVING, used in snapshot list view.
snapshot.state.saving-census=Saving census…
+# LOCALIZATION NOTE (snapshot.state.saving-census): The label describing the
+# snapshot state SAVING, used in snapshot list view.
+snapshot.state.saving-tree-map=Saving tree map…
+
# LOCALIZATION NOTE (snapshot.state.error): The label describing the snapshot
# state ERROR, used in the snapshot list view.
snapshot.state.error=Error
# LOCALIZATION NOTE (heapview.no-difference): Message displayed when there is no
# difference between two snapshots.
heapview.no-difference=No difference between the baseline and comparison.
@@ -379,8 +403,12 @@ heapview.field.name.tooltip=The name of
# LOCALIZATION NOTE (shortest-paths.header): The header label for the shortest
# paths pane.
shortest-paths.header=Retaining Paths from GC Roots
# LOCALIZATION NOTE (shortest-paths.select-node): The message displayed in the
# shortest paths pane when a node is not yet selected.
shortest-paths.select-node=Select a node to view its retaining paths
+
+# LOCALIZATION NOTE (tree-map.node-count): The label for the count value of a
+# node in the tree map
+tree-map.node-count=count
--- a/devtools/client/memory/actions/diffing.js
+++ b/devtools/client/memory/actions/diffing.js
@@ -6,16 +6,18 @@
const { assert, reportException } = require("devtools/shared/DevToolsUtils");
const { actions, diffingState, viewState } = require("../constants");
const telemetry = require("../telemetry");
const {
getSnapshot,
censusIsUpToDate,
snapshotIsDiffable
} = require("../utils");
+// This is a circular dependency, so do not destructure the needed properties.
+const snapshotActions = require("./snapshot");
/**
* Toggle diffing mode on or off.
*/
const toggleDiffing = exports.toggleDiffing = function () {
return function(dispatch, getState) {
dispatch({
type: actions.CHANGE_VIEW,
--- a/devtools/client/memory/actions/io.js
+++ b/devtools/client/memory/actions/io.js
@@ -1,20 +1,26 @@
/* 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, reportException, assert } = require("devtools/shared/DevToolsUtils");
-const { snapshotState: states, actions } = require("../constants");
+const { snapshotState: states, actions, viewState } = require("../constants");
const { L10N, openFilePicker, createSnapshot } = require("../utils");
const telemetry = require("../telemetry");
-const { selectSnapshot, computeSnapshotData, readSnapshot } = require("./snapshot");
const { OS } = require("resource://gre/modules/osfile.jsm");
-const VALID_EXPORT_STATES = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
+const {
+ selectSnapshot,
+ computeSnapshotData,
+ readSnapshot,
+ takeCensus,
+ takeTreeMap
+} = require("./snapshot");
+const VALID_EXPORT_STATES = [states.SAVED, states.READ];
exports.pickFileAndExportSnapshot = function (snapshot) {
return function* (dispatch, getState) {
let outputFile = yield openFilePicker({
title: L10N.getFormatStr("snapshot.io.save.window"),
defaultName: OS.Path.basename(snapshot.path),
filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]],
mode: "save",
--- a/devtools/client/memory/actions/moz.build
+++ b/devtools/client/memory/actions/moz.build
@@ -8,10 +8,11 @@ DevToolsModules(
'census-display.js',
'diffing.js',
'dominator-tree-display.js',
'filter.js',
'io.js',
'refresh.js',
'sizes.js',
'snapshot.js',
+ 'tree-map-display.js',
'view.js',
)
--- a/devtools/client/memory/actions/refresh.js
+++ b/devtools/client/memory/actions/refresh.js
@@ -24,13 +24,17 @@ exports.refresh = function (heapWorker)
case viewState.CENSUS:
yield dispatch(snapshot.refreshSelectedCensus(heapWorker));
return;
case viewState.DOMINATOR_TREE:
yield dispatch(snapshot.refreshSelectedDominatorTree(heapWorker));
return;
+ case viewState.TREE_MAP:
+ yield dispatch(snapshot.refreshSelectedTreeMap(heapWorker));
+ return;
+
default:
assert(false, `Unexpected view state: ${getState().view}`);
}
};
};
--- a/devtools/client/memory/actions/snapshot.js
+++ b/devtools/client/memory/actions/snapshot.js
@@ -4,18 +4,26 @@
"use strict";
const { assert, reportException } = require("devtools/shared/DevToolsUtils");
const {
censusIsUpToDate,
getSnapshot,
createSnapshot,
dominatorTreeIsComputed,
+ canTakeCensus
} = require("../utils");
-const { actions, snapshotState: states, viewState, dominatorTreeState } = require("../constants");
+const {
+ actions,
+ snapshotState: states,
+ viewState,
+ censusState,
+ treeMapState,
+ dominatorTreeState
+} = require("../constants");
const telemetry = require("../telemetry");
const view = require("./view");
const refresh = require("./refresh");
const diffing = require("./diffing");
/**
* A series of actions are fired from this task to save, read and generate the
* initial census from a snapshot.
@@ -45,17 +53,20 @@ const takeSnapshotAndCensus = exports.ta
* @param {snapshotId} id
*/
const computeSnapshotData = exports.computeSnapshotData = function(heapWorker, id) {
return function* (dispatch, getState) {
if (getSnapshot(getState(), id).state !== states.READ) {
return;
}
- yield dispatch(takeCensus(heapWorker, id));
+ // Decide which type of census to take.
+ const censusTaker = getCurrentCensusTaker(getState().view);
+ yield dispatch(censusTaker(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
@@ -133,107 +144,200 @@ const readSnapshot = exports.readSnapsho
return;
}
dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime });
};
};
/**
- * @param {HeapAnalysesClient} heapWorker
- * @param {snapshotId} id
+ * Census and tree maps both require snapshots. This function shares the logic
+ * of creating snapshots, but is configurable with specific actions for the
+ * individual census types.
*
- * @see {Snapshot} model defined in devtools/client/memory/models.js
- * @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
- * @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
+ * @param {getDisplay} Get the display object from the state.
+ * @param {getCensus} Get the census from the snapshot.
+ * @param {beginAction} Action to send at the beginning of a heap snapshot.
+ * @param {endAction} Action to send at the end of a heap snapshot.
+ * @param {errorAction} Action to send if a snapshot has an error.
*/
-const takeCensus = exports.takeCensus = function (heapWorker, id) {
- return function *(dispatch, getState) {
- const snapshot = getSnapshot(getState(), id);
- assert([states.READ, states.SAVED_CENSUS].includes(snapshot.state),
- `Can only take census of snapshots in READ or SAVED_CENSUS state, found ${snapshot.state}`);
+function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
+ endAction, errorAction }) {
+ /**
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotId} id
+ *
+ * @see {Snapshot} model defined in devtools/client/memory/models.js
+ * @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
+ * @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
+ */
+ return function (heapWorker, id) {
+ return function *(dispatch, getState) {
+ const snapshot = getSnapshot(getState(), id);
+ // Assert that snapshot is in a valid state
+
+ assert(canTakeCensus(snapshot),
+ `Attempting to take a census when the snapshot is not in a ready state.`);
+
+ let report, parentMap;
+ let display = getDisplay(getState());
+ let filter = getFilter(getState());
- let report, parentMap;
- let display = getState().censusDisplay;
- let filter = getState().filter;
+ // If display, filter and inversion haven't changed, don't do anything.
+ if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
+ return;
+ }
+
+ // Keep taking a census if the display changes while our request is in
+ // flight. Recheck that the display used for the census is the same as the
+ // state's display.
+ do {
+ display = getDisplay(getState());
+ filter = getState().filter;
+
+ dispatch({
+ type: beginAction,
+ id,
+ filter,
+ display
+ });
- // If display, filter and inversion haven't changed, don't do anything.
- if (censusIsUpToDate(filter, display, snapshot.census)) {
- return;
- }
+ let opts = display.inverted
+ ? { asInvertedTreeNode: true }
+ : { asTreeNode: true };
+
+ opts.filter = filter || null;
- // Keep taking a census if the display changes while our request is in
- // flight. Recheck that the display used for the census is the same as the
- // state's display.
- do {
- display = getState().censusDisplay;
- filter = getState().filter;
+ try {
+ ({ report, parentMap } = yield heapWorker.takeCensus(
+ snapshot.path,
+ { breakdown: display.breakdown },
+ opts));
+ } catch (error) {
+ reportException("takeCensus", error);
+ dispatch({ type: errorAction, id, error });
+ return;
+ }
+ }
+ while (filter !== getState().filter ||
+ display !== getDisplay(getState()));
dispatch({
- type: actions.TAKE_CENSUS_START,
+ type: endAction,
id,
+ display,
filter,
- display
+ report,
+ parentMap
});
- let opts = display.inverted
- ? { asInvertedTreeNode: true }
- : { asTreeNode: true };
- opts.filter = filter || null;
+ telemetry.countCensus({ filter, display });
+ };
+ };
+}
+
+/**
+ * Take a census.
+ */
+const takeCensus = exports.takeCensus = makeTakeCensusTask({
+ getDisplay: (state) => state.censusDisplay,
+ getFilter: (state) => state.filter,
+ getCensus: (snapshot) => snapshot.census,
+ beginAction: actions.TAKE_CENSUS_START,
+ endAction: actions.TAKE_CENSUS_END,
+ errorAction: actions.TAKE_CENSUS_ERROR
+});
- try {
- ({ report, parentMap } = yield heapWorker.takeCensus(
- snapshot.path,
- { breakdown: display.breakdown },
- opts));
- } catch (error) {
- reportException("takeCensus", error);
- dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
- return;
- }
- }
- while (filter !== getState().filter ||
- display !== getState().censusDisplay);
+/**
+ * Take a census for the treemap.
+ */
+const takeTreeMap = exports.takeTreeMap = makeTakeCensusTask({
+ getDisplay: (state) => state.treeMapDisplay,
+ getFilter: () => null,
+ getCensus: (snapshot) => snapshot.treeMap,
+ beginAction: actions.TAKE_TREE_MAP_START,
+ endAction: actions.TAKE_TREE_MAP_END,
+ errorAction: actions.TAKE_TREE_MAP_ERROR
+});
+
+/**
+ * Define what should be the default mode for taking a census based on the
+ * default view of the tool.
+ */
+const defaultCensusTaker = takeTreeMap;
- dispatch({
- type: actions.TAKE_CENSUS_END,
- id,
- display,
- filter,
- report,
- parentMap
- });
-
- telemetry.countCensus({ filter, display });
- };
+/**
+ * Pick the default census taker when taking a snapshot. This should be
+ * determined by the current view. If the view doesn't include a census, then
+ * use the default one defined above. Some census information is always needed
+ * to display some basic information about a snapshot.
+ *
+ * @param {string} value from viewState
+ */
+const getCurrentCensusTaker = exports.getCurrentCensusTaker = function (currentView) {
+ switch (currentView) {
+ case viewState.TREE_MAP:
+ return takeTreeMap;
+ break;
+ case viewState.CENSUS:
+ return takeCensus;
+ break;
+ }
+ return defaultCensusTaker;
};
/**
* Refresh the selected snapshot's census data, if need be (for example,
* display configuration changed).
*
* @param {HeapAnalysesClient} heapWorker
*/
const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) {
return function*(dispatch, getState) {
let snapshot = getState().snapshots.find(s => s.selected);
// Intermediate snapshot states will get handled by the task action that is
- // orchestrating them. For example, if the snapshot's state is
- // SAVING_CENSUS, then the takeCensus action will keep taking a census until
+ // orchestrating them. For example, if the snapshot census's state is
+ // SAVING, then the takeCensus action will keep taking a census until
// the inverted property matches the inverted state. If the snapshot is
// still in the process of being saved or read, the takeSnapshotAndCensus
// task action will follow through and ensure that a census is taken.
- if (snapshot && snapshot.state === states.SAVED_CENSUS) {
+ if (snapshot &&
+ (snapshot.census && snapshot.census.state === censusState.SAVED) ||
+ !snapshot.census) {
yield dispatch(takeCensus(heapWorker, snapshot.id));
}
};
};
/**
+ * Refresh the selected snapshot's tree map data, if need be (for example,
+ * display configuration changed).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshSelectedTreeMap = exports.refreshSelectedTreeMap = function (heapWorker) {
+ return function*(dispatch, getState) {
+ let snapshot = getState().snapshots.find(s => s.selected);
+
+ // Intermediate snapshot states will get handled by the task action that is
+ // orchestrating them. For example, if the snapshot census's state is
+ // SAVING, then the takeCensus action will keep taking a census until
+ // the inverted property matches the inverted state. If the snapshot is
+ // still in the process of being saved or read, the takeSnapshotAndCensus
+ // task action will follow through and ensure that a census is taken.
+ if (snapshot &&
+ (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
+ !snapshot.treeMap) {
+ yield dispatch(takeTreeMap(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>}
*/
@@ -391,28 +495,23 @@ const refreshSelectedDominatorTree = exp
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 (snapshot.state === states.READ) {
+ if (snapshot.dominatorTree) {
+ yield dispatch(fetchDominatorTree(heapWorker, snapshot.id));
+ } else {
+ yield dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
+ }
+ } else {
// 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;
}
};
};
@@ -425,24 +524,29 @@ const refreshSelectedDominatorTree = exp
const selectSnapshot = exports.selectSnapshot = function (id) {
return {
type: actions.SELECT_SNAPSHOT,
id
};
};
/**
- * Delete all snapshots that are in the SAVED_CENSUS or ERROR state
+ * Delete all snapshots that are in the READ or ERROR state
*
* @param {HeapAnalysesClient} heapWorker
*/
const clearSnapshots = exports.clearSnapshots = function (heapWorker) {
return function*(dispatch, getState) {
- let snapshots = getState().snapshots.filter(
- s => s.state === states.SAVED_CENSUS || s.state === states.ERROR);
+ let snapshots = getState().snapshots.filter(s => {
+ let snapshotReady = s.state === states.READ || s.state === states.ERROR;
+ let censusReady = (s.treeMap && s.treeMap.state === treeMapState.SAVED) ||
+ (s.census && s.census.state === censusState.SAVED);
+
+ return snapshotReady && censusReady
+ });
let ids = snapshots.map(s => s.id);
dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids });
if (getState().diffing) {
dispatch(diffing.toggleDiffing());
}
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/tree-map-display.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 { actions } = require("../constants");
+const { refresh } = require("./refresh");
+/**
+ * Sets the tree map display as the current display and refreshes the tree map
+ * census.
+ */
+exports.setTreeMapAndRefresh = function(heapWorker, display) {
+ return function*(dispatch, getState) {
+ dispatch(setTreeMap(display));
+ yield dispatch(refresh(heapWorker));
+ };
+};
+
+/**
+ * Clears out all cached census data in the snapshots and sets new display data
+ * for tree maps.
+ *
+ * @param {treeMapModel} display
+ */
+const setTreeMap = exports.setTreeMap = function(display) {
+ assert(typeof display === "object"
+ && display
+ && display.breakdown
+ && display.breakdown.by,
+ `Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(display)}`);
+
+ return {
+ type: actions.SET_TREE_MAP_DISPLAY,
+ display,
+ };
+};
--- a/devtools/client/memory/app.js
+++ b/devtools/client/memory/app.js
@@ -1,23 +1,26 @@
/* 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 { appinfo } = require("Services");
const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { censusDisplays, dominatorTreeDisplays, diffingState, viewState } = require("./constants");
+const { censusDisplays, dominatorTreeDisplays, treeMapDisplays, diffingState, viewState } = require("./constants");
const { toggleRecordingAllocationStacks } = require("./actions/allocations");
const { setCensusDisplayAndRefresh } = require("./actions/census-display");
const { setDominatorTreeDisplayAndRefresh } = require("./actions/dominator-tree-display");
+const { setTreeMapDisplayAndRefresh } = require("./actions/tree-map-display");
+
const {
getCustomCensusDisplays,
getCustomDominatorTreeDisplays,
+ getCustomTreeMapDisplays,
} = require("devtools/client/memory/utils");
const {
selectSnapshotForDiffingAndRefresh,
toggleDiffing,
expandDiffingCensusNode,
collapseDiffingCensusNode,
focusDiffingCensusNode,
} = require("./actions/diffing");
@@ -122,16 +125,28 @@ const MemoryApp = createClass({
}, []);
return [
dominatorTreeDisplays.coarseType,
dominatorTreeDisplays.allocationStack,
].concat(custom);
},
+ _getTreeMapDisplays() {
+ const customDisplays = getCustomTreeMapDisplays();
+ const custom = Object.keys(customDisplays).reduce((arr, key) => {
+ arr.push(customDisplays[key]);
+ return arr;
+ }, []);
+
+ return [
+ treeMapDisplays.coarseType
+ ].concat(custom);
+ },
+
render() {
let {
dispatch,
snapshots,
front,
heapWorker,
allocations,
toolbox,
@@ -168,16 +183,19 @@ const MemoryApp = createClass({
setFilterString: filterString =>
dispatch(setFilterStringAndRefresh(filterString, heapWorker)),
diffing,
onToggleDiffing: () => dispatch(toggleDiffing()),
view,
dominatorTreeDisplays: this._getDominatorTreeDisplays(),
onDominatorTreeDisplayChange: newDisplay =>
dispatch(setDominatorTreeDisplayAndRefresh(heapWorker, newDisplay)),
+ treeMapDisplays: this._getTreeMapDisplays(),
+ onTreeMapDisplayChange: newDisplay =>
+ dispatch(setTreeMapDisplayAndRefresh(heapWorker, newDisplay)),
onViewChange: v => dispatch(changeViewAndRefresh(v, heapWorker)),
}),
dom.div(
{
id: "memory-tool-container"
},
--- a/devtools/client/memory/components/heap.js
+++ b/devtools/client/memory/components/heap.js
@@ -3,41 +3,56 @@
* 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 Census = createFactory(require("./census"));
const CensusHeader = createFactory(require("./census-header"));
const DominatorTree = createFactory(require("./dominator-tree"));
const DominatorTreeHeader = createFactory(require("./dominator-tree-header"));
+const TreeMap = createFactory(require("./tree-map"));
const HSplitBox = createFactory(require("devtools/client/shared/components/h-split-box"));
const ShortestPaths = createFactory(require("./shortest-paths"));
const { getStatusTextFull, L10N } = require("../utils");
-const { snapshotState: states, diffingState, viewState, dominatorTreeState } = require("../constants");
+const {
+ snapshotState: states,
+ diffingState,
+ viewState,
+ censusState,
+ treeMapState,
+ dominatorTreeState
+} = require("../constants");
const { snapshot: snapshotModel, diffingModel } = require("../models");
/**
* Get the app state's current state atom.
*
* @see the relevant state string constants in `../constants.js`.
*
* @param {viewState} view
* @param {snapshotModel} snapshot
* @param {diffingModel} diffing
*
* @return {snapshotState|diffingState|dominatorTreeState}
*/
function getState(view, snapshot, diffing) {
switch (view) {
case viewState.CENSUS:
- return snapshot.state;
+ return snapshot.census
+ ? snapshot.census.state
+ : snapshot.state;
case viewState.DIFFING:
return diffing.state;
+ case viewState.TREE_MAP:
+ return snapshot.treeMap
+ ? snapshot.treeMap.state
+ : snapshot.state;
+
case viewState.DOMINATOR_TREE:
return snapshot.dominatorTree
? snapshot.dominatorTree.state
: snapshot.state;
}
assert(false, `Unexpected view state: ${view}`);
return null;
@@ -54,18 +69,18 @@ function getState(view, snapshot, diffin
* @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 censusState.SAVING:
+ case treeMapState.SAVING:
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;
@@ -105,25 +120,38 @@ function shouldDisplayThrobber(diffing)
* 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 (diffing) {
+ if (diffing.state === diffingState.ERROR) {
+ return diffing.error;
+ }
+ if (diffing.census === censusState.ERROR) {
+ return diffing.census.error;
+ }
}
if (snapshot) {
if (snapshot.state === states.ERROR) {
return snapshot.error;
}
+ if (snapshot.census === censusState.ERROR) {
+ return snapshot.census.error;
+ }
+
+ if (snapshot.treeMap === treeMapState.ERROR) {
+ return snapshot.treeMap.error;
+ }
+
if (snapshot.dominatorTree &&
snapshot.dominatorTree.state === dominatorTreeState.ERROR) {
return snapshot.dominatorTree.error;
}
}
return null;
}
@@ -180,19 +208,26 @@ const Heap = module.exports = createClas
if (error) {
return this._renderError(state, statusText, error);
}
if (view === viewState.CENSUS || view === viewState.DIFFING) {
const census = view === viewState.CENSUS
? snapshot.census
: diffing.census;
+ if (!census) {
+ return this._renderStatus(state, statusText, diffing);
+ }
return this._renderCensus(state, census, diffing, onViewSourceInDebugger);
}
+ if (view === viewState.TREE_MAP) {
+ return this._renderTreeMap(state, snapshot.treeMap);
+ }
+
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);
@@ -283,16 +318,23 @@ const Heap = module.exports = createClas
onExpand: node => this.props.onCensusExpand(census, node),
onCollapse: node => this.props.onCensusCollapse(census, node),
onFocus: node => this.props.onCensusFocus(census, node),
}));
return this._renderHeapView(state, ...contents);
},
+ _renderTreeMap(state, treeMap) {
+ return this._renderHeapView(
+ state,
+ TreeMap({ treeMap })
+ );
+ },
+
_renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
const tree = dom.div(
{
className: "vbox",
style: {
overflowY: "auto"
}
},
--- a/devtools/client/memory/components/moz.build
+++ b/devtools/client/memory/components/moz.build
@@ -1,18 +1,23 @@
# 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/.
+DIRS += [
+ 'tree-map',
+]
+
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',
'shortest-paths.js',
'snapshot-list-item.js',
'toolbar.js',
+ 'tree-map.js',
)
--- a/devtools/client/memory/components/snapshot-list-item.js
+++ b/devtools/client/memory/components/snapshot-list-item.js
@@ -4,19 +4,25 @@
const { assert } = require("devtools/shared/DevToolsUtils");
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
const {
L10N,
getSnapshotTitle,
getSnapshotTotals,
getStatusText,
- snapshotIsDiffable
+ snapshotIsDiffable,
+ getSavedCensus
} = require("../utils");
-const { snapshotState: states, diffingState } = require("../constants");
+const {
+ snapshotState: states,
+ diffingState,
+ censusState,
+ treeMapState
+} = require("../constants");
const { snapshot: snapshotModel } = require("../models");
const SnapshotListItem = module.exports = createClass({
displayName: "SnapshotListItem",
propTypes: {
onClick: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
@@ -56,24 +62,31 @@ const SnapshotListItem = module.exports
}
if (selectedForDiffing || diffing.state == diffingState.SELECTING) {
checkbox = dom.input(checkboxAttrs);
}
}
let details;
- if (!selectedForDiffing && snapshot.state === states.SAVED_CENSUS) {
- let { bytes } = getSnapshotTotals(snapshot.census);
- let formatBytes = L10N.getFormatStr("aggregate.mb", L10N.numberWithDecimals(bytes / 1000000, 2));
+ if (!selectedForDiffing) {
+ // See if a tree map or census is in the read state.
+ let census = getSavedCensus(snapshot);
- details = dom.span({ className: "snapshot-totals" },
- dom.span({ className: "total-bytes" }, formatBytes)
- );
- } else {
+ // If there is census data, fill in the total bytes.
+ if (census) {
+ let { bytes } = getSnapshotTotals(census);
+ let formatBytes = L10N.getFormatStr("aggregate.mb", L10N.numberWithDecimals(bytes / 1000000, 2));
+
+ details = dom.span({ className: "snapshot-totals" },
+ dom.span({ className: "total-bytes" }, formatBytes)
+ );
+ }
+ }
+ if (!details) {
details = dom.span({ className: "snapshot-state" }, statusText);
}
let saveLink = !snapshot.path ? void 0 : dom.a({
onClick: () => onSave(snapshot),
className: "save",
}, L10N.getFormatStr("snapshot.io.save"));
--- a/devtools/client/memory/components/toolbar.js
+++ b/devtools/client/memory/components/toolbar.js
@@ -26,28 +26,34 @@ module.exports = createClass({
diffing: models.diffingModel,
onToggleDiffing: PropTypes.func.isRequired,
view: PropTypes.string.isRequired,
onViewChange: PropTypes.func.isRequired,
dominatorTreeDisplays: PropTypes.arrayOf(PropTypes.shape({
displayName: PropTypes.string.isRequired,
})).isRequired,
onDominatorTreeDisplayChange: PropTypes.func.isRequired,
+ treeMapDisplays: PropTypes.arrayOf(PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ })).isRequired,
+ onTreeMapDisplayChange: PropTypes.func.isRequired,
snapshots: PropTypes.arrayOf(models.snapshot).isRequired,
},
render() {
let {
onTakeSnapshotClick,
onImportClick,
onClearSnapshotsClick,
onCensusDisplayChange,
censusDisplays,
dominatorTreeDisplays,
onDominatorTreeDisplayChange,
+ treeMapDisplays,
+ onTreeMapDisplayChange,
onToggleRecordAllocationStacks,
allocations,
filterString,
setFilterString,
snapshots,
diffing,
onToggleDiffing,
view,
@@ -95,16 +101,54 @@ module.exports = createClass({
type: "search",
className: "devtools-searchinput",
placeholder: L10N.getStr("filter.placeholder"),
title: L10N.getStr("filter.tooltip"),
onChange: event => setFilterString(event.target.value),
value: filterString || undefined,
})
);
+ } else if (view == viewState.TREE_MAP) {
+ assert(treeMapDisplays.length >= 1,
+ "Should always have at least one tree map display");
+
+ // Only show the dropdown if there are multiple display options
+ viewToolbarOptions = treeMapDisplays.length > 1
+ ? dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.label(
+ {
+ className: "display-by",
+ title: L10N.getStr("toolbar.displayBy.tooltip"),
+ },
+ L10N.getStr("toolbar.displayBy"),
+ dom.select(
+ {
+ id: "select-tree-map-display",
+ onChange: e => {
+ const newDisplay =
+ treeMapDisplays.find(b => b.displayName === e.target.value);
+ onTreeMapDisplayChange(newDisplay);
+ },
+ },
+ treeMapDisplays.map(({ tooltip, displayName }) => dom.option(
+ {
+ key: `tree-map-display-${displayName}`,
+ value: displayName,
+ title: tooltip,
+ },
+ displayName
+ ))
+ )
+ )
+ )
+ : null;
} else {
assert(view === viewState.DOMINATOR_TREE);
viewToolbarOptions = dom.div(
{
className: "toolbar-group"
},
@@ -142,20 +186,28 @@ module.exports = createClass({
{
title: L10N.getStr("toolbar.view.tooltip"),
},
L10N.getStr("toolbar.view"),
dom.select(
{
id: "select-view",
onChange: e => onViewChange(e.target.value),
- defaultValue: viewState.CENSUS,
+ defaultValue: view,
},
dom.option(
{
+ value: viewState.TREE_MAP,
+ title: L10N.getStr("toolbar.view.treemap.tooltip"),
+ selected: view
+ },
+ L10N.getStr("toolbar.view.treemap")
+ ),
+ dom.option(
+ {
value: viewState.CENSUS,
title: L10N.getStr("toolbar.view.census.tooltip"),
},
L10N.getStr("toolbar.view.census")
),
dom.option(
{
value: viewState.DOMINATOR_TREE,
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
+const { treeMapModel } = require("../models");
+const startVisualization = require("./tree-map/start");
+
+module.exports = createClass({
+ propTypes: {
+ treeMap: treeMapModel
+ },
+
+ displayName: "TreeMap",
+
+ getInitialState() {
+ return {};
+ },
+
+ componentDidMount() {
+ const { treeMap } = this.props;
+ if (treeMap && treeMap.report) {
+ this._startVisualization();
+ }
+ },
+
+ shouldComponentUpdate(nextProps) {
+ const oldTreeMap = this.props.treeMap;
+ const newTreeMap = nextProps.treeMap;
+ return oldTreeMap !== newTreeMap;
+ },
+
+ componentDidUpdate(prevProps) {
+ this._stopVisualization();
+
+ if (this.props.treeMap && this.props.treeMap.report) {
+ this._startVisualization();
+ }
+ },
+
+ componentWillUnmount() {
+ if (this.state.stopVisualization) {
+ this.state.stopVisualization();
+ }
+ },
+
+ _stopVisualization() {
+ if (this.state.stopVisualization) {
+ this.state.stopVisualization();
+ this.setState({ stopVisualization: null });
+ }
+ },
+
+ _startVisualization() {
+ const { container } = this.refs;
+ const { report } = this.props.treeMap;
+ const stopVisualization = startVisualization(container, report);
+ this.setState({ stopVisualization });
+ },
+
+ render() {
+ return dom.div(
+ {
+ ref: "container",
+ className: "tree-map-container"
+ }
+ );
+ }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/canvas-utils.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+/**
+ * Create 2 canvases and contexts for drawing onto, 1 main canvas, and 1 zoom
+ * canvas. The main canvas dimensions match the parent div, but the CSS can be
+ * transformed to be zoomed and dragged around (potentially creating a blurry
+ * canvas once zoomed in). The zoom canvas is a zoomed in section that matches
+ * the parent div's dimensions and is kept in place through CSS. A zoomed in
+ * view of the visualization is drawn onto this canvas, providing a crisp zoomed
+ * in view of the tree map.
+ */
+const { debounce } = require("sdk/lang/functional");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const FULLSCREEN_STYLE = {
+ width: "100%",
+ height: "100%",
+ position: "absolute",
+};
+
+/**
+ * Create the canvases, resize handlers, and return references to them all
+ *
+ * @param {HTMLDivElement} parentEl
+ * @param {Number} debounceRate
+ * @return {Object}
+ */
+function Canvases(parentEl, debounceRate) {
+ EventEmitter.decorate(this)
+ this.container = createContainingDiv(parentEl);
+
+ // This canvas contains all of the treemap
+ this.main = createCanvas(this.container, "main");
+ // This canvas contains only the zoomed in portion, overlaying the main canvas
+ this.zoom = createCanvas(this.container, "zoom");
+
+ this.removeHandlers = handleResizes(this, debounceRate);
+}
+
+Canvases.prototype = {
+
+ /**
+ * Remove the handlers and elements
+ *
+ * @return {type} description
+ */
+ destroy : function() {
+ this.removeHandlers();
+ this.container.removeChild(this.main.canvas);
+ this.container.removeChild(this.zoom.canvas);
+ }
+}
+
+module.exports = Canvases;
+
+/**
+ * Create the containing div
+ *
+ * @param {HTMLDivElement} parentEl
+ * @return {HTMLDivElement}
+ */
+function createContainingDiv(parentEl) {
+ let div = parentEl.ownerDocument.createElementNS(HTML_NS, "div");
+ Object.assign(div.style, FULLSCREEN_STYLE);
+ parentEl.appendChild(div);
+ return div;
+}
+
+/**
+ * Create a canvas and context
+ *
+ * @param {HTMLDivElement} container
+ * @param {String} className
+ * @return {Object} { canvas, ctx }
+ */
+function createCanvas(container, className) {
+ let window = container.ownerDocument.defaultView;
+ let canvas = container.ownerDocument.createElementNS(HTML_NS, "canvas");
+ container.appendChild(canvas);
+ canvas.width = container.offsetWidth * window.devicePixelRatio;
+ canvas.height = container.offsetHeight * window.devicePixelRatio;
+ canvas.className = className;
+
+ Object.assign(canvas.style, FULLSCREEN_STYLE, {
+ pointerEvents: "none"
+ });
+
+ let ctx = canvas.getContext("2d");
+
+ return { canvas, ctx };
+}
+
+/**
+ * Resize the canvases' resolutions, and fires out the onResize callback
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} canvases
+ * @param {Number} debounceRate
+ */
+function handleResizes(canvases, debounceRate) {
+ let { container, main, zoom } = canvases;
+ let window = container.ownerDocument.defaultView;
+
+ function resize() {
+ let width = container.offsetWidth * window.devicePixelRatio;
+ let height = container.offsetHeight * window.devicePixelRatio;
+
+ main.canvas.width = width;
+ main.canvas.height = height;
+ zoom.canvas.width = width;
+ zoom.canvas.height = height;
+
+ canvases.emit('resize');
+ }
+
+ // Tests may not need debouncing
+ let debouncedResize = debounceRate > 0
+ ? debounce(resize, debounceRate)
+ : resize;
+
+ window.addEventListener("resize", debouncedResize, false);
+ resize();
+
+ return function removeResizeHandlers() {
+ window.removeEventListener("resize", debouncedResize, false);
+ };
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/color-coarse-type.js
@@ -0,0 +1,70 @@
+/* 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";
+
+/**
+ * Color the boxes in the treemap
+ */
+
+const TYPES = [ "objects", "other", "strings", "scripts" ];
+
+// The factors determine how much the hue shifts
+const TYPE_FACTOR = TYPES.length * 3;
+const DEPTH_FACTOR = -10;
+const H = 0.5;
+const S = 0.6;
+const L = 0.9;
+
+/**
+ * Recursively find the index of the coarse type of a node
+ *
+ * @param {Object} node
+ * d3 treemap
+ * @return {Integer}
+ * index
+ */
+function findCoarseTypeIndex(node) {
+ let index = TYPES.indexOf(node.name);
+
+ if (node.parent) {
+ return index === -1 ? findCoarseTypeIndex(node.parent) : index;
+ }
+
+ return TYPES.indexOf("other");
+}
+
+/**
+ * Decide a color value for depth to be used in the HSL computation
+ *
+ * @param {Object} node
+ * @return {Number}
+ */
+function depthColorFactor(node) {
+ return Math.min(1, node.depth / DEPTH_FACTOR);
+}
+
+/**
+ * Decide a color value for type to be used in the HSL computation
+ *
+ * @param {Object} node
+ * @return {Number}
+ */
+function typeColorFactor(node) {
+ return findCoarseTypeIndex(node) / TYPE_FACTOR;
+}
+
+/**
+ * Color a node
+ *
+ * @param {Object} node
+ * @return {Array} HSL values ranged 0-1
+ */
+module.exports = function colorCoarseType(node) {
+ let h = Math.min(1, H + typeColorFactor(node));
+ let s = Math.min(1, S);
+ let l = Math.min(1, L + depthColorFactor(node));
+
+ return [h, s, l];
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/drag-zoom.js
@@ -0,0 +1,330 @@
+/* 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 { debounce } = require("sdk/lang/functional");
+const { lerp } = require("devtools/client/memory/utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const LERP_SPEED = 0.5;
+const ZOOM_SPEED = 0.01;
+const TRANSLATE_EPSILON = 1;
+const ZOOM_EPSILON = 0.001;
+const LINE_SCROLL_MODE = 1;
+const SCROLL_LINE_SIZE = 15;
+
+/**
+ * DragZoom is a constructor that contains the state of the current dragging and
+ * zooming behavior. It sets the scrolling and zooming behaviors.
+ *
+ * @param {HTMLElement} container description
+ * The container for the canvases
+ */
+function DragZoom(container, debounceRate, requestAnimationFrame) {
+ EventEmitter.decorate(this);
+
+ this.isDragging = false;
+
+ // The current mouse position
+ this.mouseX = container.offsetWidth / 2;
+ this.mouseY = container.offsetHeight / 2;
+
+ // The total size of the visualization after being zoomed, in pixels
+ this.zoomedWidth = container.offsetWidth;
+ this.zoomedHeight = container.offsetHeight;
+
+ // How much the visualization has been zoomed in
+ this.zoom = 0;
+
+ // The offset of visualization from the container. This is applied after
+ // the zoom, and the visualization by default is centered
+ this.translateX = 0;
+ this.translateY = 0;
+
+ // The size of the offset between the top/left of the container, and the
+ // top/left of the containing element. This value takes into account
+ // the devicePixelRatio for canvas draws.
+ this.offsetX = 0;
+ this.offsetY = 0;
+
+ // The smoothed values that are animated and eventually match the target
+ // values. The values are updated by the update loop
+ this.smoothZoom = 0;
+ this.smoothTranslateX = 0;
+ this.smoothTranslateY = 0;
+
+ // Add the constant values for testing purposes
+ this.ZOOM_SPEED = ZOOM_SPEED;
+ this.ZOOM_EPSILON = ZOOM_EPSILON;
+
+ let update = createUpdateLoop(container, this, requestAnimationFrame);
+
+ this.destroy = setHandlers(this, container, update, debounceRate);
+}
+
+module.exports = DragZoom;
+
+/**
+ * Returns an update loop. This loop smoothly updates the visualization when
+ * actions are performed. Once the animations have reached their target values
+ * the animation loop is stopped.
+ *
+ * Any value in the `dragZoom` object that starts with "smooth" is the
+ * smoothed version of a value that is interpolating toward the target value.
+ * For instance `dragZoom.smoothZoom` approaches `dragZoom.zoom` on each
+ * iteration of the update loop until it's sufficiently close as defined by
+ * the epsilon values.
+ *
+ * Only these smoothed values and the container CSS are updated by the loop.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ * The values that represent the current dragZoom state
+ * @param {Function} requestAnimationFrame
+ */
+function createUpdateLoop(container, dragZoom, requestAnimationFrame) {
+ let isLooping = false;
+
+ function update() {
+ let isScrollChanging = (
+ Math.abs(dragZoom.smoothZoom - dragZoom.zoom) > ZOOM_EPSILON
+ );
+ let isTranslateChanging = (
+ Math.abs(dragZoom.smoothTranslateX - dragZoom.translateX)
+ > TRANSLATE_EPSILON ||
+ Math.abs(dragZoom.smoothTranslateY - dragZoom.translateY)
+ > TRANSLATE_EPSILON
+ );
+
+ isLooping = isScrollChanging || isTranslateChanging;
+
+ if (isScrollChanging) {
+ dragZoom.smoothZoom = lerp(dragZoom.smoothZoom, dragZoom.zoom,
+ LERP_SPEED);
+ } else {
+ dragZoom.smoothZoom = dragZoom.zoom;
+ }
+
+ if (isTranslateChanging) {
+ dragZoom.smoothTranslateX = lerp(dragZoom.smoothTranslateX,
+ dragZoom.translateX, LERP_SPEED);
+ dragZoom.smoothTranslateY = lerp(dragZoom.smoothTranslateY,
+ dragZoom.translateY, LERP_SPEED);
+ } else {
+ dragZoom.smoothTranslateX = dragZoom.translateX;
+ dragZoom.smoothTranslateY = dragZoom.translateY;
+ }
+
+ let zoom = 1 + dragZoom.smoothZoom;
+ let x = dragZoom.smoothTranslateX;
+ let y = dragZoom.smoothTranslateY;
+ container.style.transform = `translate(${x}px, ${y}px) scale(${zoom})`;
+
+ if (isLooping) {
+ requestAnimationFrame(update);
+ }
+ }
+
+ // Go ahead and start the update loop
+ update();
+
+ return function restartLoopingIfStopped() {
+ if (!isLooping) {
+ update();
+ }
+ };
+}
+
+/**
+ * Set the various event listeners and return a function to remove them
+ *
+ * @param {Object} dragZoom
+ * @param {HTMLElement} container
+ * @param {Function} update
+ * @return {Function} The function to remove the handlers
+ */
+function setHandlers(dragZoom, container, update, debounceRate) {
+ let emitChanged = debounce(() => dragZoom.emit("change"), debounceRate);
+
+ let removeDragHandlers =
+ setDragHandlers(container, dragZoom, emitChanged, update);
+ let removeScrollHandlers =
+ setScrollHandlers(container, dragZoom, emitChanged, update);
+
+ return function removeHandlers() {
+ removeDragHandlers();
+ removeScrollHandlers();
+ };
+}
+
+/**
+ * Sets handlers for when the user drags on the canvas. It will update dragZoom
+ * object with new translate and offset values.
+ *
+ * @param {HTMLElement} container
+ * @param {Object} dragZoom
+ * @param {Function} changed
+ * @param {Function} update
+ */
+function setDragHandlers(container, dragZoom, emitChanged, update) {
+ let parentEl = container.parentElement;
+
+ function startDrag() {
+ dragZoom.isDragging = true;
+ container.style.cursor = "grabbing";
+ }
+
+ function stopDrag() {
+ dragZoom.isDragging = false;
+ container.style.cursor = "grab";
+ }
+
+ function drag(event) {
+ let prevMouseX = dragZoom.mouseX;
+ let prevMouseY = dragZoom.mouseY;
+
+ dragZoom.mouseX = event.clientX - parentEl.offsetLeft;
+ dragZoom.mouseY = event.clientY - parentEl.offsetTop;
+
+ if (!dragZoom.isDragging) {
+ return;
+ }
+
+ dragZoom.translateX += dragZoom.mouseX - prevMouseX;
+ dragZoom.translateY += dragZoom.mouseY - prevMouseY;
+
+ keepInView(container, dragZoom);
+
+ emitChanged();
+ update();
+ }
+
+ parentEl.addEventListener("mousedown", startDrag, false);
+ parentEl.addEventListener("mouseup", stopDrag, false);
+ parentEl.addEventListener("mouseout", stopDrag, false);
+ parentEl.addEventListener("mousemove", drag, false);
+
+ return function removeListeners() {
+ parentEl.removeEventListener("mousedown", startDrag, false);
+ parentEl.removeEventListener("mouseup", stopDrag, false);
+ parentEl.removeEventListener("mouseout", stopDrag, false);
+ parentEl.removeEventListener("mousemove", drag, false);
+ };
+}
+
+/**
+ * Sets the handlers for when the user scrolls. It updates the dragZoom object
+ * and keeps the canvases all within the view. After changing values update
+ * loop is called, and the changed event is emitted.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ * @param {Function} changed
+ * @param {Function} update
+ */
+function setScrollHandlers(container, dragZoom, emitChanged, update) {
+ let window = container.ownerDocument.defaultView;
+
+ function handleWheel(event) {
+ event.preventDefault();
+
+ if (dragZoom.isDragging) {
+ return;
+ }
+
+ // Update the zoom level
+ let scrollDelta = getScrollDelta(event, window);
+ let prevZoom = dragZoom.zoom;
+ dragZoom.zoom = Math.max(0, dragZoom.zoom - scrollDelta * ZOOM_SPEED);
+ let deltaZoom = dragZoom.zoom - prevZoom;
+
+ // Calculate the updated width and height
+ let prevWidth = container.offsetWidth * (1 + prevZoom);
+ let prevHeight = container.offsetHeight * (1 + prevZoom);
+ dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
+ dragZoom.height = container.offsetHeight * (1 + dragZoom.zoom);
+ let deltaWidth = dragZoom.zoomedWidth - prevWidth;
+ let deltaHeight = dragZoom.height - prevHeight;
+
+ // The ratio of where the center of the zoom is in regards to the total
+ // zoomed width/height
+ let ratioZoomX = (dragZoom.zoomedWidth / 2 - dragZoom.translateX)
+ / dragZoom.zoomedWidth;
+ let ratioZoomY = (dragZoom.height / 2 - dragZoom.translateY)
+ / dragZoom.height;
+
+ // Distribute the change in width and height based on the above ratio
+ dragZoom.translateX -= lerp(-deltaWidth / 2, deltaWidth / 2, ratioZoomX);
+ dragZoom.translateY -= lerp(-deltaHeight / 2, deltaHeight / 2, ratioZoomY);
+
+ // The ratio of mouse position to total zoomeed width/height, ranged [-1, 1]
+ let mouseRatioX, mouseRatioY;
+ if (deltaZoom > 0) {
+ // Zoom in towards the mouse
+ mouseRatioX = 2 * (dragZoom.mouseX - container.offsetWidth / 2)
+ / dragZoom.zoomedWidth;
+ mouseRatioY = 2 * (dragZoom.mouseY - container.offsetHeight / 2)
+ / dragZoom.height;
+ } else {
+ // Zoom out centering the screen
+ mouseRatioX = 0;
+ mouseRatioY = 0;
+ }
+ // Adjust the translate to zoom towards the mouse
+ dragZoom.translateX -= deltaWidth * mouseRatioX;
+ dragZoom.translateY -= deltaHeight * mouseRatioY;
+
+ // Keep the canvas in range of the container
+ keepInView(container, dragZoom);
+ emitChanged();
+ update();
+ }
+
+ window.addEventListener("wheel", handleWheel, false);
+
+ return function removeListener() {
+ window.removeEventListener("wheel", handleWheel, false);
+ };
+}
+
+/**
+ * Account for the various mouse wheel event types, per pixel or per line
+ *
+ * @param {WheelEvent} event
+ * @param {Window} window
+ * @return {Number} The scroll size in pixels
+ */
+function getScrollDelta(event, window) {
+ if (event.deltaMode === LINE_SCROLL_MODE) {
+ // Update by a fixed arbitrary value to normalize scroll types
+ return event.deltaY * SCROLL_LINE_SIZE;
+ }
+ return event.deltaY;
+}
+
+/**
+ * Keep the dragging and zooming within the view by updating the values in the
+ * `dragZoom` object.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ */
+function keepInView(container, dragZoom) {
+ let { devicePixelRatio } = container.ownerDocument.defaultView;
+ let overdrawX = (dragZoom.zoomedWidth - container.offsetWidth) / 2;
+ let overdrawY = (dragZoom.height - container.offsetHeight) / 2;
+
+ dragZoom.translateX = Math.max(-overdrawX,
+ Math.min(overdrawX, dragZoom.translateX));
+ dragZoom.translateY = Math.max(-overdrawY,
+ Math.min(overdrawY, dragZoom.translateY));
+
+ dragZoom.offsetX = devicePixelRatio * (
+ (dragZoom.zoomedWidth - container.offsetWidth) / 2 - dragZoom.translateX
+ );
+ dragZoom.offsetY = devicePixelRatio * (
+ (dragZoom.height - container.offsetHeight) / 2 - dragZoom.translateY
+ );
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/draw.js
@@ -0,0 +1,285 @@
+/* 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";
+/**
+ * Draw the treemap into the provided canvases using the 2d context. The treemap
+ * layout is computed with d3. There are 2 canvases provided, each matching
+ * the resolution of the window. The main canvas is a fully drawn version of
+ * the treemap that is positioned and zoomed using css. It gets blurry the more
+ * you zoom in as it doesn't get redrawn when zooming. The zoom canvas is
+ * repositioned absolutely after every change in the dragZoom object, and then
+ * redrawn to provide a full-resolution (non-blurry) view of zoomed in segment
+ * of the treemap.
+ */
+
+const colorCoarseType = require("./color-coarse-type");
+const {
+ hslToStyle,
+ formatAbbreviatedBytes,
+ L10N
+} = require("devtools/client/memory/utils");
+
+// A constant fully zoomed out dragZoom object for the main canvas
+const NO_SCROLL = {
+ translateX: 0,
+ translateY: 0,
+ zoom: 0,
+ offsetX: 0,
+ offsetY: 0
+};
+
+// Drawing constants
+const ELLIPSIS = "...";
+const TEXT_MARGIN = 2;
+const TEXT_COLOR = "#000000";
+const TEXT_LIGHT_COLOR = "rgba(0,0,0,0.5)";
+const LINE_WIDTH = 1;
+const FONT_SIZE = 10;
+const FONT_LINE_HEIGHT = 2;
+const PADDING = [5, 5, 5, 5];
+const COUNT_LABEL = L10N.getStr("tree-map.node-count");
+
+/**
+ * Setup and start drawing the treemap visualization
+ *
+ * @param {Object} report
+ * @param {Object} canvases
+ * A CanvasUtils object that contains references to the main and zoom
+ * canvases and contexts
+ * @param {Object} dragZoom
+ * A DragZoom object representing the current state of the dragging
+ * and zooming behavior
+ */
+exports.setupDraw = function(report, canvases, dragZoom) {
+ let getTreemap = configureD3Treemap.bind(null, canvases.main.canvas);
+
+ let treemap, nodes;
+
+ function drawFullTreemap() {
+ treemap = getTreemap();
+ nodes = treemap(report);
+ drawTreemap(canvases.main, nodes, NO_SCROLL);
+ drawTreemap(canvases.zoom, nodes, dragZoom);
+ }
+
+ function drawZoomedTreemap() {
+ drawTreemap(canvases.zoom, nodes, dragZoom);
+ positionZoomedCanvas(canvases.zoom.canvas, dragZoom);
+ }
+
+ drawFullTreemap();
+ canvases.on("resize", drawFullTreemap);
+ dragZoom.on("change", drawZoomedTreemap);
+};
+
+/**
+ * Returns a configured d3 treemap function
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @return {Function}
+ */
+const configureD3Treemap = exports.configureD3Treemap = function(canvas) {
+ let window = canvas.ownerDocument.defaultView;
+ let ratio = window.devicePixelRatio;
+ let treemap = window.d3.layout.treemap()
+ .size([canvas.width, canvas.height])
+ .sticky(true)
+ .padding([
+ (PADDING[0] + FONT_SIZE) * ratio,
+ PADDING[1] * ratio,
+ PADDING[2] * ratio,
+ PADDING[3] * ratio,
+ ])
+ .value(d => d.bytes);
+
+ /**
+ * Create treemap nodes from a census report that are sorted by depth
+ *
+ * @param {Object} report
+ * @return {Array} An array of d3 treemap nodes
+ * // https://github.com/mbostock/d3/wiki/Treemap-Layout
+ * parent - the parent node, or null for the root.
+ * children - the array of child nodes, or null for leaf nodes.
+ * value - the node value, as returned by the value accessor.
+ * depth - the depth of the node, starting at 0 for the root.
+ * area - the computed pixel area of this node.
+ * x - the minimum x-coordinate of the node position.
+ * y - the minimum y-coordinate of the node position.
+ * z - the orientation of this cell’s subdivision, if any.
+ * dx - the x-extent of the node position.
+ * dy - the y-extent of the node position.
+ */
+ return function depthSortedNodes(report) {
+ let nodes = treemap(report);
+ nodes.sort((a, b) => a.depth - b.depth);
+ return nodes;
+ };
+};
+
+/**
+ * Draw the text, cut it in half every time it doesn't fit until it fits or
+ * it's smaller than the "..." text.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} x
+ * the position of the text
+ * @param {Number} y
+ * the position of the text
+ * @param {Number} innerWidth
+ * the inner width of the containing treemap cell
+ * @param {Text} name
+ */
+const drawTruncatedName = exports.drawTruncatedName = function(ctx, x, y,
+ innerWidth,
+ name) {
+ let truncated = name.substr(0, Math.floor(name.length / 2));
+ let formatted = truncated + ELLIPSIS;
+
+ if (ctx.measureText(formatted).width > innerWidth) {
+ drawTruncatedName(ctx, x, y, innerWidth, truncated);
+ } else {
+ ctx.fillText(formatted, x, y);
+ }
+};
+
+/**
+ * Fit and draw the text in a node with the following strategies to shrink
+ * down the text size:
+ *
+ * Function 608KB 9083 count
+ * Function
+ * Func...
+ * Fu...
+ * ...
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Object} node
+ * @param {Number} borderWidth
+ * @param {Number} ratio
+ * @param {Object} dragZoom
+ */
+const drawText = exports.drawText = function(ctx, node, borderWidth, ratio,
+ dragZoom) {
+ let { dx, dy, name, totalBytes, totalCount } = node;
+ let scale = dragZoom.zoom + 1;
+ dx *= scale;
+ dy *= scale;
+
+ // Start checking to see how much text we can fit in, optimizing for the
+ // common case of lots of small leaf nodes
+ if (FONT_SIZE * FONT_LINE_HEIGHT < dy) {
+ let margin = borderWidth(node) * 1.5 + ratio * TEXT_MARGIN;
+ let x = margin + node.x * scale - dragZoom.offsetX;
+ let y = margin + node.y * scale - dragZoom.offsetY;
+ let innerWidth = dx - margin * 2;
+ let nameSize = ctx.measureText(name).width;
+
+ if (ctx.measureText(ELLIPSIS).width > innerWidth) {
+ return;
+ }
+
+ ctx.fillStyle = TEXT_COLOR;
+
+ if (nameSize > innerWidth) {
+ // The name is too long - halve the name as an expediant way to shorten it
+ drawTruncatedName(ctx, x, y, innerWidth, name);
+ } else {
+ let bytesFormatted = formatAbbreviatedBytes(totalBytes);
+ let countFormatted = `${totalCount} ${COUNT_LABEL}`;
+ let byteSize = ctx.measureText(bytesFormatted).width;
+ let countSize = ctx.measureText(countFormatted).width;
+ let spaceSize = ctx.measureText(" ").width;
+
+ if (nameSize + byteSize + countSize + spaceSize * 3 > innerWidth) {
+ // The full name will fit
+ ctx.fillText(`${name}`, x, y);
+ } else {
+ // The full name plus the byte information will fit
+ ctx.fillText(name, x, y);
+ ctx.fillStyle = TEXT_LIGHT_COLOR;
+ ctx.fillText(`${bytesFormatted} ${countFormatted}`,
+ x + nameSize + spaceSize, y);
+ }
+ }
+ }
+};
+
+/**
+ * Draw a box given a node
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Object} node
+ * @param {Number} borderWidth
+ * @param {Number} ratio
+ * @param {Object} dragZoom
+ */
+const drawBox = exports.drawBox = function(ctx, node, borderWidth, dragZoom) {
+ let border = borderWidth(node);
+ let fillHSL = colorCoarseType(node);
+ let strokeHSL = [fillHSL[0], fillHSL[1], fillHSL[2] * 0.5];
+ let scale = 1 + dragZoom.zoom;
+
+ // Offset the draw so that box strokes don't overlap
+ let x = scale * node.x - dragZoom.offsetX + border / 2;
+ let y = scale * node.y - dragZoom.offsetY + border / 2;
+ let dx = scale * node.dx - border;
+ let dy = scale * node.dy - border;
+
+ ctx.fillStyle = hslToStyle(...fillHSL);
+ ctx.fillRect(x, y, dx, dy);
+
+ ctx.strokeStyle = hslToStyle(...strokeHSL);
+ ctx.lineWidth = border;
+ ctx.strokeRect(x, y, dx, dy);
+};
+
+/**
+ * Draw the overall treemap
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Array} nodes
+ * @param {Objbect} dragZoom
+ */
+const drawTreemap = exports.drawTreemap = function({canvas, ctx}, nodes,
+ dragZoom) {
+ let window = canvas.ownerDocument.defaultView;
+ let ratio = window.devicePixelRatio;
+ let canvasArea = canvas.width * canvas.height;
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.font = `${FONT_SIZE * ratio}px sans-serif`;
+ ctx.textBaseline = "top";
+
+ function borderWidth(node) {
+ let areaRatio = Math.sqrt(node.area / canvasArea);
+ return ratio * Math.max(1, LINE_WIDTH * areaRatio);
+ }
+
+ for (let i = 0; i < nodes.length; i++) {
+ let node = nodes[i];
+ if (node.parent === undefined) {
+ continue;
+ }
+
+ drawBox(ctx, node, borderWidth, dragZoom);
+ drawText(ctx, node, borderWidth, ratio, dragZoom);
+ }
+};
+
+/**
+ * Set the position of the zoomed in canvas. It always take up 100% of the view
+ * window, but is transformed relative to the zoomed in containing element,
+ * essentially reversing the transform of the containing element.
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @param {Object} dragZoom
+ */
+const positionZoomedCanvas = function(canvas, dragZoom) {
+ let scale = 1 / (1 + dragZoom.zoom);
+ let x = -dragZoom.translateX;
+ let y = -dragZoom.translateY;
+ canvas.style.transform = `scale(${scale}) translate(${x}px, ${y}px)`;
+};
+
+exports.positionZoomedCanvas = positionZoomedCanvas;
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/moz.build
@@ -0,0 +1,12 @@
+# 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(
+ 'canvas-utils.js',
+ 'color-coarse-type.js',
+ 'drag-zoom.js',
+ 'draw.js',
+ 'start.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/start.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 { setupDraw } = require("./draw");
+const DragZoom = require("./drag-zoom");
+const CanvasUtils = require("./canvas-utils");
+
+/**
+ * Start the tree map visualization
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} report
+ * the report from a census
+ * @param {Number} debounceRate
+ */
+module.exports = function startVisualization(parentEl, report,
+ debounceRate = 100) {
+ let window = parentEl.ownerDocument.defaultView;
+ let canvases = new CanvasUtils(parentEl, debounceRate);
+ let dragZoom = new DragZoom(canvases.container, debounceRate,
+ window.requestAnimationFrame);
+
+ setupDraw(report, canvases, dragZoom);
+
+ return function stopVisualization() {
+ canvases.destroy();
+ dragZoom.destroy();
+ };
+};
--- a/devtools/client/memory/constants.js
+++ b/devtools/client/memory/constants.js
@@ -25,16 +25,22 @@ actions.TAKE_SNAPSHOT_END = "take-snapsh
// When a heap snapshot is read into memory -- only fired
// once per snapshot.
actions.READ_SNAPSHOT_START = "read-snapshot-start";
actions.READ_SNAPSHOT_END = "read-snapshot-end";
// When a census is being performed on a heap snapshot
actions.TAKE_CENSUS_START = "take-census-start";
actions.TAKE_CENSUS_END = "take-census-end";
+actions.TAKE_CENSUS_ERROR = "take-census-error";
+
+// When a tree map is being calculated on a heap snapshot
+actions.TAKE_TREE_MAP_START = "take-tree-map-start";
+actions.TAKE_TREE_MAP_END = "take-tree-map-end";
+actions.TAKE_TREE_MAP_ERROR = "take-tree-map-error";
// When requesting that the server start/stop recording allocation stacks.
actions.TOGGLE_RECORD_ALLOCATION_STACKS_START = "toggle-record-allocation-stacks-start";
actions.TOGGLE_RECORD_ALLOCATION_STACKS_END = "toggle-record-allocation-stacks-end";
// When a heap snapshot is being saved to a user-specified
// location on disk.
actions.EXPORT_SNAPSHOT_START = "export-snapshot-start";
@@ -66,16 +72,19 @@ actions.TAKE_CENSUS_DIFF_END = "take-cen
actions.DIFFING_ERROR = "diffing-error";
// Fired to set a new census display.
actions.SET_CENSUS_DISPLAY = "set-census-display";
// Fired to change the display that controls the dominator tree labels.
actions.SET_DOMINATOR_TREE_DISPLAY = "set-dominator-tree-display";
+// Fired to set a tree map display
+actions.SET_TREEMAP_DISPLAY = "set-treemap-display";
+
// 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";
@@ -104,38 +113,39 @@ actions.COLLAPSE_DOMINATOR_TREE_NODE = "
actions.RESIZE_SHORTEST_PATHS = "resize-shortest-paths";
/*** Census Displays ***************************************************************/
const COUNT = Object.freeze({ by: "count", count: true, bytes: true });
const INTERNAL_TYPE = Object.freeze({ by: "internalType", then: COUNT });
const ALLOCATION_STACK = Object.freeze({ by: "allocationStack", then: COUNT, noStack: COUNT });
const OBJECT_CLASS = Object.freeze({ by: "objectClass", then: COUNT, other: COUNT });
+const COARSE_TYPE = Object.freeze({
+ by: "coarseType",
+ objects: OBJECT_CLASS,
+ strings: COUNT,
+ scripts: {
+ by: "filename",
+ then: INTERNAL_TYPE,
+ noFilename: INTERNAL_TYPE
+ },
+ other: INTERNAL_TYPE,
+});
exports.censusDisplays = Object.freeze({
coarseType: Object.freeze({
displayName: "Type",
get tooltip() {
// Importing down here is necessary because of the circular dependency
// this introduces with `./utils.js`.
const { L10N } = require("./utils");
return L10N.getStr("censusDisplays.coarseType.tooltip");
},
inverted: true,
- breakdown: Object.freeze({
- by: "coarseType",
- objects: OBJECT_CLASS,
- strings: COUNT,
- scripts: {
- by: "filename",
- then: INTERNAL_TYPE,
- noFilename: INTERNAL_TYPE
- },
- other: INTERNAL_TYPE,
- })
+ breakdown: COARSE_TYPE
}),
allocationStack: Object.freeze({
displayName: "Call Stack",
get tooltip() {
const { L10N } = require("./utils");
return L10N.getStr("censusDisplays.allocationStack.tooltip");
},
@@ -188,49 +198,90 @@ exports.dominatorTreeDisplays = Object.f
breakdown: Object.freeze({
by: "allocationStack",
then: DOMINATOR_TREE_LABEL_COARSE_TYPE,
noStack: DOMINATOR_TREE_LABEL_COARSE_TYPE,
}),
}),
});
+exports.treeMapDisplays = Object.freeze({
+ coarseType: Object.freeze({
+ displayName: "Type",
+ get tooltip() {
+ const { L10N } = require("./utils");
+ return L10N.getStr("treeMapDisplays.coarseType.tooltip");
+ },
+ breakdown: COARSE_TYPE,
+ inverted: false,
+ })
+});
+
/*** 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";
+viewState.TREE_MAP = "view-state-tree-map";
/*** Snapshot States **********************************************************/
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
+ * ↗
+ * IMPORTING
*
* 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";
snapshotState.READING = "snapshot-state-reading";
snapshotState.READ = "snapshot-state-read";
-snapshotState.SAVING_CENSUS = "snapshot-state-saving-census";
-snapshotState.SAVED_CENSUS = "snapshot-state-saved-census";
+
+/*
+ * Various states the census model can be in.
+ *
+ * SAVING <-> SAVED
+ * |
+ * V
+ * ERROR
+ */
+
+const censusState = exports.censusState = Object.create(null);
+
+censusState.SAVING = "census-state-saving";
+censusState.SAVED = "census-state-saved";
+censusState.ERROR = "census-state-error";
+
+/*
+ * Various states the tree map model can be in.
+ *
+ * SAVING <-> SAVED
+ * |
+ * V
+ * ERROR
+ */
+
+const treeMapState = exports.treeMapState = Object.create(null);
+
+treeMapState.SAVING = "tree-map-state-saving";
+treeMapState.SAVED = "tree-map-state-saved";
+treeMapState.ERROR = "tree-map-state-error";
/*** Diffing States ***********************************************************/
/*
* Various states the diffing model can be in.
*
* SELECTING --> TAKING_DIFF <---> TOOK_DIFF
* |
--- a/devtools/client/memory/models.js
+++ b/devtools/client/memory/models.js
@@ -68,16 +68,60 @@ const censusDisplayModel = exports.censu
const dominatorTreeDisplayModel = exports.dominatorTreeDisplay = PropTypes.shape({
displayName: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
breakdown: PropTypes.shape({
by: PropTypes.string.isRequired,
})
});
+/**
+ * The data describing the tree map's shape, and its associated metadata.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const treeMapDisplayModel = exports.treeMapDisplay = PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ inverted: PropTypes.bool.isRequired,
+ breakdown: PropTypes.shape({
+ by: PropTypes.string.isRequired,
+ })
+});
+
+/**
+ * Tree map model.
+ */
+const treeMapModel = exports.treeMapModel = PropTypes.shape({
+ // The current census report data.
+ report: PropTypes.object,
+ // The display data used to generate the current census.
+ display: treeMapDisplayModel,
+ // The current treeMapState this is in
+ state: catchAndIgnore(function (treeMap) {
+ switch (treeMap.state) {
+ case treeMapState.SAVING:
+ assert(!treeMap.report, "Should not have a report");
+ assert(!treeMap.error, "Should not have an error");
+ break;
+ case treeMapState.SAVED:
+ assert(treeMap.report, "Should have a report");
+ assert(!treeMap.error, "Should not have an error");
+ break;
+
+ case treeMapState.ERROR:
+ assert(treeMap.error, "Should have an error");
+ break;
+
+ default:
+ assert(false, `Unexpected treeMap state: ${treeMap.state}`);
+ }
+ })
+});
+
let censusModel = exports.censusModel = PropTypes.shape({
// The current census report data.
report: PropTypes.object,
// The parent map for the report.
parentMap: PropTypes.object,
// The display data used to generate the current census.
display: censusDisplayModel,
// If present, the currently cached report's filter string used for pruning
@@ -87,16 +131,42 @@ let censusModel = exports.censusModel =
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,
+ // The censusModelState that this census is currently in.
+ state: catchAndIgnore(function (census) {
+ switch (census.state) {
+ case censusState.SAVING:
+ assert(!census.report, "Should not have a report");
+ assert(!census.parentMap, "Should not have a parent map");
+ assert(census.expanded, "Should not have an expanded set");
+ assert(!census.error, "Should not have an error");
+ break;
+
+ case censusState.SAVED:
+ assert(census.report, "Should have a report");
+ assert(census.parentMap, "Should have a parent map");
+ assert(census.expanded, "Should have an expanded set");
+ assert(!census.error, "Should not have an error");
+ break;
+
+ case censusState.ERROR:
+ assert(!census.report, "Should not have a report");
+ assert(census.error, "Should have an error");
+ break;
+
+ default:
+ assert(false, `Unexpected census state: ${census.state}`);
+ }
+ })
});
/**
* Dominator tree model.
*/
let dominatorTreeModel = exports.dominatorTreeModel = PropTypes.shape({
// The id of this dominator tree.
dominatorTreeId: PropTypes.number,
@@ -188,42 +258,39 @@ let snapshotModel = exports.snapshot = P
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,
+ // Current tree map data for this snapshot.
+ treeMap: treeMapModel,
// 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: 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];
+ let shouldHavePath = [states.IMPORTING, states.SAVED, states.READ];
+ let shouldHaveCreationTime = [states.READ];
if (!stateKeys.includes(current)) {
throw new Error(`Snapshot state must be one of ${stateKeys}.`);
}
if (shouldHavePath.includes(current) && !snapshot.path) {
throw new Error(`Snapshots in state ${current} must have a snapshot path.`);
}
- if (shouldHaveCensus.includes(current) &&
- (!snapshot.census || !snapshot.census.display || !snapshot.census.display.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.
@@ -290,16 +357,20 @@ let appModel = exports.app = {
// The display data describing how we want the census data to be.
censusDisplay: censusDisplayModel.isRequired,
// The display data describing how we want the dominator tree labels to be
// computed.
dominatorTreeDisplay: dominatorTreeDisplayModel.isRequired,
+ // The display data describing how we want the dominator tree labels to be
+ // computed.
+ treeMapDisplay: treeMapDisplayModel.isRequired,
+
// List of reference to all snapshots taken
snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
// If present, a filter string for pruning the tree items.
filter: PropTypes.string,
// If present, the current diffing state.
diffing: diffingModel,
@@ -314,13 +385,17 @@ let appModel = exports.app = {
case viewState.DIFFING:
assert(app.diffing, "Should be diffing");
break;
case viewState.DOMINATOR_TREE:
assert(!app.diffing, "Should not be diffing");
break;
+ case viewState.TREE_MAP:
+ assert(!app.diffing, "Should not be diffing");
+ break;
+
default:
assert(false, `Unexpected type of view: ${app.view}`);
}
}),
};
--- a/devtools/client/memory/reducers.js
+++ b/devtools/client/memory/reducers.js
@@ -2,13 +2,14 @@
* 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.censusDisplay = require("./reducers/census-display");
exports.diffing = require("./reducers/diffing");
exports.dominatorTreeDisplay = require("./reducers/dominator-tree-display");
+exports.treeMapDisplay = require("./reducers/tree-map-display");
exports.errors = require("./reducers/errors");
exports.filter = require("./reducers/filter");
exports.sizes = require("./reducers/sizes");
exports.snapshots = require("./reducers/snapshots");
exports.view = require("./reducers/view");
--- a/devtools/client/memory/reducers/moz.build
+++ b/devtools/client/memory/reducers/moz.build
@@ -7,10 +7,11 @@ DevToolsModules(
'allocations.js',
'census-display.js',
'diffing.js',
'dominator-tree-display.js',
'errors.js',
'filter.js',
'sizes.js',
'snapshots.js',
+ 'tree-map-display.js',
'view.js',
)
--- a/devtools/client/memory/reducers/snapshots.js
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -3,16 +3,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Immutable = require("devtools/client/shared/vendor/immutable");
const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
const {
actions,
snapshotState: states,
+ censusState,
+ treeMapState,
dominatorTreeState,
viewState,
} = require("../constants");
const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode");
const handlers = Object.create(null);
handlers[actions.SNAPSHOT_ERROR] = function (snapshots, { id, error }) {
@@ -53,45 +55,110 @@ handlers[actions.READ_SNAPSHOT_END] = fu
});
};
handlers[actions.TAKE_CENSUS_START] = function (snapshots, { id, display, filter }) {
const census = {
report: null,
display,
filter,
+ state: censusState.SAVING
};
return snapshots.map(snapshot => {
return snapshot.id === id
- ? immutableUpdate(snapshot, { state: states.SAVING_CENSUS, census })
+ ? immutableUpdate(snapshot, { census })
: snapshot;
});
};
handlers[actions.TAKE_CENSUS_END] = function (snapshots, { id,
report,
parentMap,
display,
filter }) {
const census = {
report,
parentMap,
expanded: Immutable.Set(),
display,
filter,
+ state: censusState.SAVED
};
return snapshots.map(snapshot => {
return snapshot.id === id
- ? immutableUpdate(snapshot, { state: states.SAVED_CENSUS, census })
+ ? immutableUpdate(snapshot, { census })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_CENSUS_ERROR] = function (snapshots, { id, error }) {
+ assert(error, "actions with TAKE_CENSUS_ERROR should have an error");
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ const census = Object.freeze({
+ state: censusState.ERROR,
+ error,
+ });
+
+ return immutableUpdate(snapshot, { census });
+ });
+};
+
+handlers[actions.TAKE_TREE_MAP_START] = function (snapshots, { id, display }) {
+ const treeMap = {
+ report: null,
+ display,
+ state: treeMapState.SAVING
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { treeMap })
: snapshot;
});
};
+handlers[actions.TAKE_TREE_MAP_END] = function (snapshots, action) {
+ const { id, report, display } = action;
+ const treeMap = {
+ report,
+ display,
+ state: treeMapState.SAVED
+ };
+
+ return snapshots.map(snapshot => {
+ return snapshot.id === id
+ ? immutableUpdate(snapshot, { treeMap })
+ : snapshot;
+ });
+};
+
+handlers[actions.TAKE_TREE_MAP_ERROR] = function (snapshots, { id, error }) {
+ assert(error, "actions with TAKE_TREE_MAP_ERROR should have an error");
+
+ return snapshots.map(snapshot => {
+ if (snapshot.id !== id) {
+ return snapshot;
+ }
+
+ const treeMap = Object.freeze({
+ state: treeMapState.ERROR,
+ error,
+ });
+
+ return immutableUpdate(snapshot, { treeMap });
+ });
+};
+
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");
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers/tree-map-display.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, treeMapDisplays } = require("../constants");
+const DEFAULT_TREE_MAP_DISPLAY = treeMapDisplays.coarseType;
+
+const handlers = Object.create(null);
+
+handlers[actions.SET_TREE_MAP_DISPLAY] = function (_, { display }) {
+ return display;
+};
+
+module.exports = function (state = DEFAULT_TREE_MAP_DISPLAY, action) {
+ const handler = handlers[action.type];
+ return handler ? handler(state, action) : state;
+};
--- a/devtools/client/memory/reducers/view.js
+++ b/devtools/client/memory/reducers/view.js
@@ -6,12 +6,12 @@
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) {
+module.exports = function (view = viewState.TREE_MAP, action) {
const handler = handlers[action.type];
return handler ? handler(view, action) : view;
};
--- a/devtools/client/memory/telemetry.js
+++ b/devtools/client/memory/telemetry.js
@@ -5,17 +5,17 @@
// This module exports methods to record telemetry data for memory tool usage.
//
// NB: Ensure that *every* exported function is wrapped in `makeInfallible` so
// that our probes don't accidentally break code that actually does productive
// work for the user!
const { telemetry } = require("Services");
const { makeInfallible, immutableUpdate } = require("devtools/shared/DevToolsUtils");
-const { dominatorTreeDisplays, censusDisplays } = require("./constants");
+const { dominatorTreeDisplays, treeMapDisplays, censusDisplays } = require("./constants");
exports.countTakeSnapshot = makeInfallible(function () {
const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_TAKE_SNAPSHOT_COUNT");
histogram.add(1);
}, "devtools/client/memory/telemetry#countTakeSnapshot");
exports.countImportSnapshot = makeInfallible(function () {
const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_IMPORT_SNAPSHOT_COUNT");
--- a/devtools/client/memory/test/browser/browser.ini
+++ b/devtools/client/memory/test/browser/browser.ini
@@ -17,8 +17,10 @@ support-files =
[browser_memory_keyboard.js]
[browser_memory_keyboard-snapshot-list.js]
[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]
[browser_memory_transferHeapSnapshot_e10s_01.js]
+[browser_memory_tree_map-01.js]
+[browser_memory_tree_map-02.js]
--- a/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js
@@ -4,25 +4,29 @@
// Sanity test that we can show allocation stack displays in the tree.
"use strict";
const { waitForTime } = require("devtools/shared/DevToolsUtils");
const { toggleRecordingAllocationStacks } = require("devtools/client/memory/actions/allocations");
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
const censusDisplayActions = require("devtools/client/memory/actions/census-display");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
const heapWorker = panel.panelWin.gHeapAnalysesClient;
const front = panel.panelWin.gFront;
const { getState, dispatch } = panel.panelWin.gStore;
const doc = panel.panelWin.document;
+ dispatch(changeView(viewState.CENSUS));
+
dispatch(censusDisplayActions.setCensusDisplay(censusDisplays.invertedAllocationStack));
is(getState().censusDisplay.breakdown.by, "allocationStack");
yield dispatch(toggleRecordingAllocationStacks(front));
ok(getState().allocations.recording);
// Let some allocations build up.
yield waitForTime(500);
--- a/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
+++ b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
@@ -1,32 +1,34 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests taking and then clearing snapshots.
*/
+ const { treeMapState } = require("devtools/client/memory/constants");
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
const { gStore, document } = panel.panelWin;
const { getState, dispatch } = gStore;
let snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
is(getState().snapshots.length, 0, "Starts with no snapshots in store");
is(snapshotEls.length, 0, "No snapshots visible");
info("Take two snapshots");
takeSnapshot(panel.panelWin);
takeSnapshot(panel.panelWin);
yield waitUntilState(gStore, state =>
state.snapshots.length === 2 &&
- state.snapshots[0].state === states.SAVED_CENSUS &&
- state.snapshots[1].state === states.SAVED_CENSUS);
+ state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
+ state.snapshots[0].treeMap.state === treeMapState.SAVED &&
+ state.snapshots[1].treeMap.state === treeMapState.SAVED);
snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
is(snapshotEls.length, 2, "Two snapshots visible");
info("Click on Clear Snapshots");
yield clearSnapshots(panel.panelWin);
is(getState().snapshots.length, 0, "No snapshots in store");
snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
--- a/devtools/client/memory/test/browser/browser_memory_diff_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_diff_01.js
@@ -3,17 +3,18 @@
// Test diffing.
"use strict";
const { waitForTime } = require("devtools/shared/DevToolsUtils");
const {
snapshotState,
- diffingState
+ diffingState,
+ treeMapState
} = require("devtools/client/memory/constants");
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
const heapWorker = panel.panelWin.gHeapAnalysesClient;
const front = panel.panelWin.gFront;
const store = panel.panelWin.gStore;
@@ -35,18 +36,19 @@ this.test = makeMemoryTest(TEST_URL, fun
state =>
!!state.diffing &&
state.diffing.state === diffingState.SELECTING);
ok(true, "Clicking the diffing button put us into the diffing state.");
is(getDisplayedSnapshotStatus(doc), "Select the baseline snapshot");
yield waitUntilState(store, state =>
state.snapshots.length === 2 &&
- state.snapshots[0].state === snapshotState.SAVED_CENSUS &&
- state.snapshots[1].state === snapshotState.SAVED_CENSUS);
+ state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
+ state.snapshots[0].treeMap.state === treeMapState.SAVED &&
+ state.snapshots[1].treeMap.state === treeMapState.SAVED);
const listItems = [...doc.querySelectorAll(".snapshot-list-item")];
is(listItems.length, 2, "Should have two snapshot list items");
// Select the first snapshot.
EventUtils.synthesizeMouseAtCenter(listItems[0], {}, panel.panelWin);
yield waitUntilState(store,
state =>
--- a/devtools/client/memory/test/browser/browser_memory_displays_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_displays_01.js
@@ -2,28 +2,34 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the heap tree renders rows based on the display
*/
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+const { viewState, censusState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
const { gStore, document } = panel.panelWin;
+ const { dispatch } = panel.panelWin.gStore;
+
function $$(selector) {
return [...document.querySelectorAll(selector)];
}
+ dispatch(changeView(viewState.CENSUS));
yield takeSnapshot(panel.panelWin);
yield waitUntilState(gStore, state =>
- state.snapshots[0].state === states.SAVED_CENSUS);
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED);
info("Check coarse type heap view");
["Function", "js::Shape", "Object", "strings"].forEach(findNameCell);
yield setCensusDisplay(panel.panelWin, censusDisplays.allocationStack);
info("Check allocation stack heap view");
[L10N.getStr("tree-item.nostack")].forEach(findNameCell);
--- a/devtools/client/memory/test/browser/browser_memory_filter_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_filter_01.js
@@ -4,47 +4,53 @@
// Sanity test that we can show allocation stack displays in the tree.
"use strict";
const {
dominatorTreeState,
snapshotState,
viewState,
+ censusState,
} = require("devtools/client/memory/constants");
-const { changeViewAndRefresh } = require("devtools/client/memory/actions/view");
+const { changeViewAndRefresh, changeView } = require("devtools/client/memory/actions/view");
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
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.CENSUS));
+
const takeSnapshotButton = doc.getElementById("take-snapshot");
EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
yield waitUntilState(store, state =>
state.snapshots.length === 1 &&
- state.snapshots[0].state === snapshotState.SAVED_CENSUS);
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVING);
let filterInput = doc.getElementById("filter");
EventUtils.synthesizeMouseAtCenter(filterInput, {}, panel.panelWin);
EventUtils.sendString("js::Shape", panel.panelWin);
yield waitUntilState(store, state =>
state.snapshots.length === 1 &&
- state.snapshots[0].state === snapshotState.SAVING_CENSUS);
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVING);
ok(true, "adding a filter string should trigger census recompute");
yield waitUntilState(store, state =>
state.snapshots.length === 1 &&
- state.snapshots[0].state === snapshotState.SAVED_CENSUS);
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED);
let nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
ok(nameElem, "Should get a tree item row with a name");
is(nameElem.textContent.trim(), "js::Shape", "the tree item should be the one we filtered for");
is(filterInput.value, "js::Shape",
"and filter input contains the user value");
// Now switch the dominator view, then switch back to census view
@@ -56,17 +62,18 @@ this.test = makeMemoryTest(TEST_URL, fun
yield waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]);
ok(true, "computed and fetched the dominator tree.");
dispatch(changeViewAndRefresh(viewState.CENSUS, heapWorker));
ok(true, "change view back to census");
yield waitUntilState(store, state =>
state.snapshots.length === 1 &&
- state.snapshots[0].state === snapshotState.SAVED_CENSUS);
+ state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED);
nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
filterInput = doc.getElementById("filter");
ok(nameElem, "Should still get a tree item row with a name");
is(nameElem.textContent.trim(), "js::Shape",
"the tree item should still be the one we filtered for");
is(filterInput.value, "js::Shape",
--- a/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js
@@ -1,44 +1,49 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that using ACCEL+UP/DOWN, the user can navigate between snapshots.
"use strict";
const {
- snapshotState
+ snapshotState,
+ viewState,
+ censusState
} = require("devtools/client/memory/constants");
const {
takeSnapshotAndCensus
} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
this.test = makeMemoryTest(TEST_URL, function* ({ panel }) {
// Creating snapshots already takes ~25 seconds on linux 32 debug machines
// which makes the test very likely to go over the allowed timeout
requestLongerTimeout(2);
const heapWorker = panel.panelWin.gHeapAnalysesClient;
const front = panel.panelWin.gFront;
const store = panel.panelWin.gStore;
const { dispatch } = store;
const doc = panel.panelWin.document;
+ dispatch(changeView(viewState.CENSUS));
+
info("Take 3 snapshots");
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilState(store, state =>
state.snapshots.length == 3 &&
- state.snapshots.every(s => s.state === snapshotState.SAVED_CENSUS));
- ok(true, "All snapshots are in SAVED_CENSUS state");
+ state.snapshots.every(s => s.census && s.census.state === censusState.SAVED));
+ ok(true, "All snapshots censuses are in SAVED state");
yield waitUntilSnapshotSelected(store, 2);
ok(true, "Third snapshot selected after creating all snapshots.");
info("Press ACCEL+UP key, expect second snapshot selected.");
EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
yield waitUntilSnapshotSelected(store, 1);
ok(true, "Second snapshot selected after alt+UP.");
--- a/devtools/client/memory/test/browser/browser_memory_keyboard.js
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard.js
@@ -2,29 +2,32 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
// Bug 1246570 - Check that when pressing on LEFT arrow, the parent tree node
// gets focused.
"use strict";
const {
- snapshotState
+ snapshotState,
+ censusState,
+ viewState
} = require("devtools/client/memory/constants");
const {
takeSnapshotAndCensus
} = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
function waitUntilFocused(store, node) {
return waitUntilState(store, state =>
state.snapshots.length === 1 &&
- state.snapshots[0].state === snapshotState.SAVED_CENSUS &&
state.snapshots[0].census &&
+ state.snapshots[0].census.state === censusState.SAVED &&
state.snapshots[0].census.focused &&
state.snapshots[0].census.focused === node
);
}
function waitUntilExpanded(store, node) {
return waitUntilState(store, state =>
state.snapshots[0] &&
@@ -34,17 +37,20 @@ function waitUntilExpanded(store, node)
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
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.CENSUS));
+
is(getState().censusDisplay.breakdown.by, "coarseType");
+
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
let census = getState().snapshots[0].census;
let root1 = census.report.children[0];
let root2 = census.report.children[0];
let root3 = census.report.children[0];
let root4 = census.report.children[0];
let child1 = root1.children[0];
--- a/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js
+++ b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js
@@ -2,25 +2,29 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
// Sanity test that we can show allocation stack displays in the tree.
"use strict";
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
const censusDisplayActions = require("devtools/client/memory/actions/census-display");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
const heapWorker = panel.panelWin.gHeapAnalysesClient;
const front = panel.panelWin.gFront;
const { getState, dispatch } = panel.panelWin.gStore;
const doc = panel.panelWin.document;
+ dispatch(changeView(viewState.CENSUS));
+
ok(!getState().allocations.recording,
"Should not be recording allocagtions");
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
yield dispatch(censusDisplayActions.setCensusDisplayAndRefresh(
heapWorker,
censusDisplays.allocationStack));
--- a/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
+++ b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
@@ -2,25 +2,29 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
// Bug 1221150 - Ensure that census trees do not accidentally auto expand
// when clicking on the allocation stacks checkbox.
"use strict";
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
const heapWorker = panel.panelWin.gHeapAnalysesClient;
const front = panel.panelWin.gFront;
const { getState, dispatch } = panel.panelWin.gStore;
const doc = panel.panelWin.document;
+ dispatch(changeView(viewState.CENSUS));
+
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
is(getState().allocations.recording, false);
const recordingCheckbox = doc.getElementById("record-allocation-stacks-checkbox");
EventUtils.synthesizeMouseAtCenter(recordingCheckbox, {}, panel.panelWin);
is(getState().allocations.recording, true);
const nameElems = [...doc.querySelectorAll(".heap-tree-item-field.heap-tree-item-name")];
--- a/devtools/client/memory/test/browser/browser_memory_percents_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_percents_01.js
@@ -1,16 +1,18 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Sanity test that we calculate percentages in the tree.
"use strict";
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
function checkCells(cells) {
ok(cells.length > 1, "Should have found some");
// Ignore the first header cell.
for (let cell of cells.slice(1)) {
const percent = cell.querySelector(".heap-tree-percent");
@@ -20,16 +22,18 @@ function checkCells(cells) {
}
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
const heapWorker = panel.panelWin.gHeapAnalysesClient;
const front = panel.panelWin.gFront;
const { getState, dispatch } = panel.panelWin.gStore;
const doc = panel.panelWin.document;
+ dispatch(changeView(viewState.CENSUS));
+
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
is(getState().censusDisplay.breakdown.by, "coarseType",
"Should be using coarse type breakdown");
const bytesCells = [...doc.querySelectorAll(".heap-tree-item-bytes")];
checkCells(bytesCells);
const totalBytesCells = [...doc.querySelectorAll(".heap-tree-item-total-bytes")];
--- a/devtools/client/memory/test/browser/browser_memory_simple_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_simple_01.js
@@ -1,21 +1,25 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests taking snapshots and default states.
*/
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+const { viewState, censusState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
const { gStore, document } = panel.panelWin;
const { getState, dispatch } = gStore;
+ dispatch(changeView(viewState.CENSUS));
+
let snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
is(getState().snapshots.length, 0, "Starts with no snapshots in store");
is(snapshotEls.length, 0, "No snapshots rendered");
yield takeSnapshot(panel.panelWin);
snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
is(getState().snapshots.length, 1, "One snapshot was created in store");
is(snapshotEls.length, 1, "One snapshot was rendered");
@@ -23,15 +27,14 @@ this.test = makeMemoryTest(TEST_URL, fun
yield takeSnapshot(panel.panelWin);
snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
is(getState().snapshots.length, 2, "Two snapshots created in store");
is(snapshotEls.length, 2, "Two snapshots rendered");
ok(!snapshotEls[0].classList.contains("selected"), "First snapshot no longer has `selected` class");
ok(snapshotEls[1].classList.contains("selected"), "Second snapshot has `selected` class");
- yield waitUntilState(gStore, state =>
- state.snapshots[0].state === states.SAVED_CENSUS &&
- state.snapshots[1].state === states.SAVED_CENSUS);
+ yield waitUntilCensusState(gStore, s => s.census, [censusState.SAVED,
+ censusState.SAVED]);
ok(document.querySelector(".heap-tree-item-name"),
"Should have rendered some tree items");
});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_tree_map-01.js
@@ -0,0 +1,102 @@
+/* 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/. */
+
+// Make sure the canvases are created correctly
+
+"use strict";
+
+const CanvasUtils = require("devtools/client/memory/components/tree-map/canvas-utils");
+const D3_SCRIPT = '<script type="application/javascript" ' +
+ 'src="chrome://devtools/content/shared/vendor/d3.js>';
+const TEST_URL = `data:text/html,<html><body>${D3_SCRIPT}</body></html>`;
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const document = panel.panelWin.document;
+ const window = panel.panelWin;
+ const div = document.createElement("div");
+
+ Object.assign(div.style, {
+ width: "100px",
+ height: "200px",
+ position: "absolute"
+ });
+
+ document.body.appendChild(div);
+
+ info("Create the canvases");
+
+ let canvases = new CanvasUtils(div, 0);
+
+ info("Test the shape of the returned object");
+
+ is(typeof canvases, "object", "Canvases create an object");
+ is(typeof canvases.emit, "function", "Decorated with an EventEmitter");
+ is(typeof canvases.on, "function", "Decorated with an EventEmitter");
+ is(div.children[0], canvases.container, "Div has the container");
+ ok(canvases.main.canvas instanceof window.HTMLCanvasElement,
+ "Creates the main canvas");
+ ok(canvases.zoom.canvas instanceof window.HTMLCanvasElement,
+ "Creates the zoom canvas");
+ ok(canvases.main.ctx instanceof window.CanvasRenderingContext2D,
+ "Creates the main canvas context");
+ ok(canvases.zoom.ctx instanceof window.CanvasRenderingContext2D,
+ "Creates the zoom canvas context");
+
+ info("Test resizing");
+
+ let timesResizeCalled = 0;
+ canvases.on('resize', function() {
+ timesResizeCalled++;
+ });
+
+ let main = canvases.main.canvas;
+ let zoom = canvases.zoom.canvas;
+ let ratio = window.devicePixelRatio;
+
+ is(main.width, 100 * ratio,
+ "Main canvas width is the same as the parent div");
+ is(main.height, 200 * ratio,
+ "Main canvas height is the same as the parent div");
+ is(zoom.width, 100 * ratio,
+ "Zoom canvas width is the same as the parent div");
+ is(zoom.height, 200 * ratio,
+ "Zoom canvas height is the same as the parent div");
+ is(timesResizeCalled, 0,
+ "Resize was not emitted");
+
+ div.style.width = "500px";
+ div.style.height = "700px";
+
+ window.dispatchEvent(new Event("resize"));
+
+ is(main.width, 500 * ratio,
+ "Main canvas width is resized to be the same as the parent div");
+ is(main.height, 700 * ratio,
+ "Main canvas height is resized to be the same as the parent div");
+ is(zoom.width, 500 * ratio,
+ "Zoom canvas width is resized to be the same as the parent div");
+ is(zoom.height, 700 * ratio,
+ "Zoom canvas height is resized to be the same as the parent div");
+ is(timesResizeCalled, 1,
+ "'resize' was emitted was emitted");
+
+ div.style.width = "1100px";
+ div.style.height = "1300px";
+
+ canvases.destroy();
+ window.dispatchEvent(new Event("resize"));
+
+ is(main.width, 500 * ratio,
+ "Main canvas width is not resized after destroy");
+ is(main.height, 700 * ratio,
+ "Main canvas height is not resized after destroy");
+ is(zoom.width, 500 * ratio,
+ "Zoom canvas width is not resized after destroy");
+ is(zoom.height, 700 * ratio,
+ "Zoom canvas height is not resized after destroy");
+ is(timesResizeCalled, 1,
+ "onResize was not called again");
+
+ document.body.removeChild(div);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_tree_map-02.js
@@ -0,0 +1,129 @@
+/* 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/. */
+
+// Test the drag and zooming behavior
+
+"use strict";
+
+const CanvasUtils = require("devtools/client/memory/components/tree-map/canvas-utils");
+const DragZoom = require("devtools/client/memory/components/tree-map/drag-zoom");
+
+const TEST_URL = `data:text/html,<html><body></body></html>`;
+const PIXEL_SCROLL_MODE = 0;
+const PIXEL_DELTA = 10;
+const MAX_RAF_LOOP = 1000;
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+ const panelWin = panel.panelWin;
+ const panelDoc = panelWin.document;
+ const div = panelDoc.createElement("div");
+
+ Object.assign(div.style, {
+ width: "100px",
+ height: "200px",
+ position: "absolute",
+ left:0,
+ top:0
+ });
+
+ let rafMock = createRAFMock();
+
+ panelDoc.body.appendChild(div);
+
+ let canvases = new CanvasUtils(div, 0);
+ let dragZoom = new DragZoom(canvases.container, 0, rafMock.raf);
+
+ info("Check initial state of dragZoom");
+ {
+ is(dragZoom.zoom, 0, "Zooming starts at 0");
+ is(dragZoom.smoothZoom, 0, "Smoothed zooming starts at 0");
+ is(rafMock.timesCalled, 0, "No RAFs have been queued");
+
+ panelWin.dispatchEvent(new WheelEvent("wheel", {
+ deltaY: -PIXEL_DELTA,
+ deltaMode: PIXEL_SCROLL_MODE
+ }));
+
+ is(dragZoom.zoom, PIXEL_DELTA * dragZoom.ZOOM_SPEED,
+ "The zoom was increased");
+ ok(dragZoom.smoothZoom < dragZoom.zoom && dragZoom.smoothZoom > 0,
+ "The smooth zoom is between the initial value and the target");
+ is(rafMock.timesCalled, 1, "A RAF has been queued");
+ }
+
+ info("RAF will eventually stop once the smooth values approach the target");
+ {
+ let i;
+ let lastCallCount;
+ for (i = 0; i < MAX_RAF_LOOP; i++) {
+ if (lastCallCount === rafMock.timesCalled) {
+ break;
+ }
+ lastCallCount = rafMock.timesCalled;
+ rafMock.nextFrame();
+ }
+
+ is(dragZoom.zoom, dragZoom.smoothZoom,
+ "The smooth and target zoom values match");
+ isnot(MAX_RAF_LOOP, i,
+ "The RAF loop correctly stopped");
+ }
+
+ info("Dragging correctly translates the div");
+ {
+ let initialX = dragZoom.translateX;
+ let initialY = dragZoom.translateY;
+ div.dispatchEvent(new MouseEvent("mousemove", {
+ clientX: 10,
+ clientY: 10,
+ }));
+ div.dispatchEvent(new MouseEvent("mousedown"));
+ div.dispatchEvent(new MouseEvent("mousemove", {
+ clientX: 20,
+ clientY: 20,
+ }));
+ div.dispatchEvent(new MouseEvent("mouseup"));
+
+ ok(dragZoom.translateX - initialX > 0,
+ "Translate X moved by some pixel amount");
+ ok(dragZoom.translateY - initialY > 0,
+ "Translate Y moved by some pixel amount");
+ }
+
+ dragZoom.destroy();
+
+ info("Scroll isn't tracked after destruction");
+ {
+ let previousZoom = dragZoom.zoom;
+ let previousSmoothZoom = dragZoom.smoothZoom;
+
+ panelWin.dispatchEvent(new WheelEvent("wheel", {
+ deltaY: -PIXEL_DELTA,
+ deltaMode: PIXEL_SCROLL_MODE
+ }));
+
+ is(dragZoom.zoom, previousZoom,
+ "The zoom stayed the same");
+ is(dragZoom.smoothZoom, previousSmoothZoom,
+ "The smooth zoom stayed the same");
+ }
+
+ info("Translation isn't tracked after destruction");
+ {
+ let initialX = dragZoom.translateX;
+ let initialY = dragZoom.translateY;
+
+ div.dispatchEvent(new MouseEvent("mousedown"));
+ div.dispatchEvent(new MouseEvent("mousemove"), {
+ clientX: 40,
+ clientY: 40,
+ });
+ div.dispatchEvent(new MouseEvent("mouseup"));
+ is(dragZoom.translateX, initialX,
+ "The translationX didn't change");
+ is(dragZoom.translateY, initialY,
+ "The translationY didn't change");
+ }
+ panelDoc.body.removeChild(div);
+});
--- a/devtools/client/memory/test/browser/head.js
+++ b/devtools/client/memory/test/browser/head.js
@@ -107,17 +107,17 @@ function takeSnapshot (window) {
document.querySelector(".devtools-toolbar .take-snapshot").click();
return waitUntilState(gStore, () => gStore.getState().snapshots.length === snapshotCount + 1);
}
function clearSnapshots (window) {
let { gStore, document } = window;
document.querySelector(".devtools-toolbar .clear-snapshots").click();
return waitUntilState(gStore, () => gStore.getState().snapshots.every(
- (snapshot) => snapshot.state !== states.SAVED_CENSUS)
+ (snapshot) => snapshot.state !== states.READ)
);
}
/**
* Sets the current requested display and waits for the selected snapshot to use
* it and complete the new census that entails.
*/
function setCensusDisplay(window, display) {
@@ -126,17 +126,19 @@ function setCensusDisplay(window, displa
// XXX: Should handle this via clicking the DOM, but React doesn't
// fire the onChange event, so just change it in the store.
// window.document.querySelector(`.select-display`).value = type;
gStore.dispatch(require("devtools/client/memory/actions/census-display")
.setCensusDisplayAndRefresh(gHeapAnalysesClient, display));
return waitUntilState(window.gStore, () => {
let selected = window.gStore.getState().snapshots.find(s => s.selected);
- return selected.state === states.SAVED_CENSUS &&
+ return selected.state === states.READ &&
+ selected.census &&
+ selected.census.state === censusState.SAVED &&
selected.census.display === display;
});
}
/**
* Get the snapshot tatus text currently displayed, or null if none is
* displayed.
*
@@ -164,8 +166,62 @@ function getSelectedSnapshotIndex(store)
*
* @return {Promise}
*/
function waitUntilSnapshotSelected(store, snapshotIndex) {
return waitUntilState(store, state =>
state.snapshots[snapshotIndex] &&
state.snapshots[snapshotIndex].selected === true);
}
+
+
+/**
+ * Wait until the state has censuses in a certain state.
+ *
+ * @return {Promise}
+ */
+function waitUntilCensusState (store, getCensus, expected) {
+ let predicate = () => {
+ let snapshots = store.getState().snapshots;
+
+ info('Current census state:' +
+ snapshots.map(x => getCensus(x) ? getCensus(x).state : null ));
+
+ return snapshots.length === expected.length &&
+ expected.every((state, i) => {
+ let census = getCensus(snapshots[i]);
+ return (state === "*") ||
+ (!census && !state) ||
+ (census && census.state === state);
+ });
+ };
+ info(`Waiting for snapshots' censuses to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+/**
+ * Mock out the requestAnimationFrame.
+ *
+ * @return {Object}
+ * @function nextFrame
+ * Call the last queued function
+ * @function raf
+ * The mocked raf function
+ * @function timesCalled
+ * How many times the RAF has been called
+ */
+function createRAFMock() {
+ let queuedFns = [];
+ let mock = { timesCalled: 0 };
+
+ mock.nextFrame = function() {
+ let thisQueue = queuedFns;
+ queuedFns = [];
+ for(var i = 0; i < thisQueue.length; i++) {
+ thisQueue[i]();
+ }
+ };
+
+ mock.raf = function(fn) {
+ mock.timesCalled++;
+ queuedFns.push(fn);
+ };
+ return mock;
+}
--- a/devtools/client/memory/test/chrome/chrome.ini
+++ b/devtools/client/memory/test/chrome/chrome.ini
@@ -10,8 +10,9 @@ support-files =
[test_Heap_01.html]
[test_Heap_02.html]
[test_Heap_03.html]
[test_Heap_04.html]
[test_Heap_05.html]
[test_ShortestPaths_01.html]
[test_ShortestPaths_02.html]
[test_Toolbar_01.html]
+[test_TreeMap_01.html]
--- a/devtools/client/memory/test/chrome/head.js
+++ b/devtools/client/memory/test/chrome/head.js
@@ -20,32 +20,34 @@ var { immutableUpdate } = DevToolsUtils;
var constants = require("devtools/client/memory/constants");
var {
censusDisplays,
diffingState,
dominatorTreeDisplays,
dominatorTreeState,
snapshotState,
- viewState
+ viewState,
+ censusState
} = constants;
const {
L10N,
} = 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 CensusTreeItem = React.createFactory(require("devtools/client/memory/components/census-tree-item"));
var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree"));
var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item"));
var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths"));
+var TreeMap = React.createFactory(require("devtools/client/memory/components/tree-map"));
var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
// All tests are asynchronous.
SimpleTest.waitForExplicitFinish();
var noop = () => {};
var TEST_CENSUS_TREE_ITEM_PROPS = Object.freeze({
@@ -181,26 +183,28 @@ var TEST_HEAP_PROPS = Object.freeze({
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 }),
}),
}),
+ state: censusState.SAVED,
inverted: false,
filter: null,
expanded: new Set(),
focused: null,
+ parentMap: Object.freeze(Object.create(null))
}),
dominatorTree: TEST_DOMINATOR_TREE,
error: null,
imported: false,
creationTime: 0,
- state: snapshotState.SAVED_CENSUS,
+ state: snapshotState.READ,
}),
sizes: Object.freeze({ shortestPathsSize: .5 }),
onShortestPathsResize: noop,
});
var TEST_TOOLBAR_PROPS = Object.freeze({
censusDisplays: [
censusDisplays.coarseType,
@@ -223,16 +227,57 @@ var TEST_TOOLBAR_PROPS = Object.freeze({
dominatorTreeDisplays: [
dominatorTreeDisplays.coarseType,
dominatorTreeDisplays.allocationStack,
],
onDominatorTreeDisplayChange: noop,
snapshots: [],
});
+function makeTestCensusNode() {
+ return {
+ name: "Function",
+ bytes: 100,
+ totalBytes: 100,
+ count: 100,
+ totalCount: 100,
+ children: []
+ };
+}
+
+var TEST_TREE_MAP_PROPS = Object.freeze({
+ treeMap: Object.freeze({
+ report: {
+ name: null,
+ bytes: 0,
+ totalBytes: 400,
+ count: 0,
+ totalCount: 400,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 200,
+ children: [ makeTestCensusNode(), makeTestCensusNode() ]
+ },
+ {
+ name: "other",
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 200,
+ children: [ makeTestCensusNode(), makeTestCensusNode() ],
+ }
+ ]
+ }
+ })
+});
+
function onNextAnimationFrame(fn) {
return () =>
requestAnimationFrame(() =>
requestAnimationFrame(fn));
}
/**
* Render the provided ReactElement in the provided HTML container.
--- a/devtools/client/memory/test/chrome/test_Heap_02.html
+++ b/devtools/client/memory/test/chrome/test_Heap_02.html
@@ -32,17 +32,17 @@ Test that the currently selected view is
"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}]`),
+ ok(container.querySelector(`[data-state=${censusState.SAVED}]`),
"Should render the census.");
// Diffing view.
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
view: viewState.DIFFING,
snapshot: null,
diffing: {
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_TreeMap_01.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the Tree Map correctly renders onto 2 managed canvases.
+-->
+<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">
+
+ <script type="application/javascript"
+ src="chrome://devtools/content/shared/vendor/d3.js">
+ </script>
+</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");
+
+ yield renderComponent(TreeMap(TEST_TREE_MAP_PROPS), container);
+
+ let treeMapContainer = container.querySelector(".tree-map-container");
+ ok(treeMapContainer, "Component creates a container");
+
+ let canvases = treeMapContainer.querySelectorAll("canvas");
+ is(canvases.length, 2, "Creates 2 canvases");
+
+ } 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
@@ -68,16 +68,35 @@ function waitUntilSnapshotState (store,
do_print(snapshots.map(x => x.state));
return snapshots.length === expected.length &&
expected.every((state, i) => state === "*" || snapshots[i].state === state);
};
do_print(`Waiting for snapshots to be of state: ${expected}`);
return waitUntilState(store, predicate);
}
+function waitUntilCensusState (store, getCensus, expected) {
+ let predicate = () => {
+ let snapshots = store.getState().snapshots;
+
+ do_print('Current census state:' +
+ snapshots.map(x => getCensus(x) ? getCensus(x).state : null ));
+
+ return snapshots.length === expected.length &&
+ expected.every((state, i) => {
+ let census = getCensus(snapshots[i]);
+ return (state === "*") ||
+ (!census && !state) ||
+ (census && census.state === state);
+ });
+ };
+ do_print(`Waiting for snapshots' censuses to be of state: ${expected}`);
+ return waitUntilState(store, predicate);
+}
+
function *createTempFile () {
let file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
let destPath = file.path;
let stat = yield OS.File.stat(destPath);
ok(stat.size === 0, "new file is 0 bytes at start");
return destPath;
}
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js
@@ -1,29 +1,30 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-// Test clearSnapshots deletes snapshots with state SAVED_CENSUS
+// Test clearSnapshots deletes snapshots with READ censuses
let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+const { treeMapState } = require("devtools/client/memory/constants");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
ok(true, "snapshot created");
ok(true, "dispatch clearSnapshots action");
let deleteEvents = Promise.all([
waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
]);
dispatch(clearSnapshots(heapWorker));
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js
@@ -1,43 +1,47 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
-// Test clearSnapshots preserves snapshots with state != SAVED_CENSUS or ERROR
+// Test clearSnapshots preserves snapshots with state != READ or ERROR
let { takeSnapshotAndCensus, clearSnapshots, takeSnapshot } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
- ok(true, "create a snapshot in SAVED_CENSUS state");
+ ok(true, "create a snapshot with a census in SAVED state");
dispatch(takeSnapshotAndCensus(front, heapWorker));
ok(true, "create a snapshot in SAVED state");
dispatch(takeSnapshot(front));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED]);
+ yield waitUntilSnapshotState(store, [states.SAVED, states.SAVED]);
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED, null]);
ok(true, "snapshots created with expected states");
ok(true, "dispatch clearSnapshots action");
let deleteEvents = Promise.all([
waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
]);
dispatch(clearSnapshots(heapWorker));
yield deleteEvents;
ok(true, "received delete snapshots events");
equal(getState().snapshots.length, 1, "one snapshot remaining");
let remainingSnapshot = getState().snapshots[0];
- notEqual(remainingSnapshot.state, states.SAVED_CENSUS,
- "remaining snapshot doesn't have the SAVED_CENSUS state");
+ equal(remainingSnapshot.treeMap, undefined,
+ "remaining snapshot doesn't have a treeMap property");
+ equal(remainingSnapshot.census, undefined,
+ "remaining snapshot doesn't have a census property");
heapWorker.destroy();
yield front.detach();
});
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js
@@ -1,31 +1,34 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test clearSnapshots deletes snapshots with state ERROR
let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
- ok(true, "create a snapshot with SAVED_CENSUS state");
+ ok(true, "create a snapshot with a treeMap");
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
- ok(true, "snapshot created with SAVED_CENSUS state");
+ yield waitUntilSnapshotState(store, [states.SAVED]);
+ ok(true, "snapshot created with a SAVED state");
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED]);
+ ok(true, "treeMap created with a SAVED state");
ok(true, "set snapshot state to error");
let id = getState().snapshots[0].id;
dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") });
yield waitUntilSnapshotState(store, [states.ERROR]);
ok(true, "snapshot set to error state");
ok(true, "dispatch clearSnapshots action");
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js
@@ -1,40 +1,41 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test clearSnapshots deletes several snapshots
let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
- ok(true, "create 3 snapshots in SAVED_CENSUS state");
+ ok(true, "create 3 snapshots with a saved census");
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
- ok(true, "snapshots created in SAVED_CENSUS state");
- yield waitUntilSnapshotState(store,
- [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED, treeMapState.SAVED,
+ treeMapState.SAVED]);
+ ok(true, "snapshots created with a saved census");
ok(true, "set first snapshot state to error");
let id = getState().snapshots[0].id;
dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") });
yield waitUntilSnapshotState(store,
- [states.ERROR, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+ [states.ERROR, states.READ, states.READ]);
ok(true, "first snapshot set to error state");
ok(true, "dispatch clearSnapshots action");
let deleteEvents = Promise.all([
waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
]);
dispatch(clearSnapshots(heapWorker));
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js
@@ -1,32 +1,33 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test clearSnapshots deletes several snapshots
let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
- ok(true, "create 3 snapshots in SAVED_CENSUS state");
+ ok(true, "create 2 snapshots with a saved census");
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
- ok(true, "snapshots created in SAVED_CENSUS state");
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS]);
+ ok(true, "snapshots created with a saved census");
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED, treeMapState.SAVED]);
let errorHeapWorker = {
deleteHeapSnapshot: function() {
return Promise.reject("_");
}
};
ok(true, "dispatch clearSnapshots action");
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js
@@ -2,42 +2,45 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that clearSnapshots disables diffing when deleting snapshots
const {
takeSnapshotAndCensus,
- clearSnapshots } = require("devtools/client/memory/actions/snapshot");
+ clearSnapshots
+} = require("devtools/client/memory/actions/snapshot");
const {
snapshotState: states,
- actions } = require("devtools/client/memory/constants");
+ actions,
+ treeMapState
+} = require("devtools/client/memory/constants");
const {
toggleDiffing,
selectSnapshotForDiffingAndRefresh
} = require("devtools/client/memory/actions/diffing");
function run_test() {
run_next_test();
}
add_task(function* () {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
- ok(true, "Create 2 snapshots in SAVED_CENSUS state");
+ ok(true, "create 2 snapshots with a saved census");
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
- ok(true, "Snapshots created in SAVED_CENSUS state");
- yield waitUntilSnapshotState(store,
- [states.SAVED_CENSUS, states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED, treeMapState.SAVED]);
+ ok(true, "snapshots created with a saved census");
dispatch(toggleDiffing());
dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
getState().snapshots[0]));
dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
getState().snapshots[1]));
ok(getState().diffing, "We should be in diffing view");
--- a/devtools/client/memory/test/unit/test_action-export-snapshot.js
+++ b/devtools/client/memory/test/unit/test_action-export-snapshot.js
@@ -1,31 +1,32 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test exporting a snapshot to a user specified location on disk.
let { exportSnapshot } = require("devtools/client/memory/actions/io");
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
let destPath = yield createTempFile();
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+ [treeMapState.SAVED]);
let exportEvents = Promise.all([
waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
waitUntilAction(store, actions.EXPORT_SNAPSHOT_END)
]);
dispatch(exportSnapshot(getState().snapshots[0], destPath));
yield exportEvents;
--- a/devtools/client/memory/test/unit/test_action-filter-02.js
+++ b/devtools/client/memory/test/unit/test_action-filter-02.js
@@ -1,65 +1,74 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that changing filter state properly refreshes the selected census.
-let { snapshotState: states } = require("devtools/client/memory/constants");
+let { snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants");
let { setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter");
let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
+let { setCensusDisplay } = require("devtools/client/memory/actions/census-display");
+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(changeView(viewState.CENSUS));
+
equal(getState().filter, null, "no filter by default");
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVED_CENSUS,
- states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
ok(true, "saved 3 snapshots and took a census of each of them");
dispatch(setFilterStringAndRefresh("str", heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVED_CENSUS,
- states.SAVING_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVING]);
ok(true, "setting filter string should recompute the selected snapshot's census");
equal(getState().filter, "str", "now inverted");
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVED_CENSUS,
- states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
equal(getState().snapshots[0].census.filter, null);
equal(getState().snapshots[1].census.filter, null);
equal(getState().snapshots[2].census.filter, "str");
dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVING_CENSUS,
- states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVING,
+ censusState.SAVED]);
ok(true, "selecting non-inverted census should trigger a recompute");
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVED_CENSUS,
- states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
equal(getState().snapshots[0].census.filter, null);
equal(getState().snapshots[1].census.filter, "str");
equal(getState().snapshots[2].census.filter, "str");
heapWorker.destroy();
yield front.detach();
});
--- a/devtools/client/memory/test/unit/test_action-filter-03.js
+++ b/devtools/client/memory/test/unit/test_action-filter-03.js
@@ -1,46 +1,52 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that changing filter state in the middle of taking a snapshot results in
// the properly fitered census.
-let { snapshotState: states } = require("devtools/client/memory/constants");
+let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
let { setFilterString, setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter");
let { takeSnapshotAndCensus } = 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(changeView(viewState.CENSUS));
+
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilSnapshotState(store, [states.SAVING]);
dispatch(setFilterString("str"));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED]);
equal(getState().filter, "str",
"should want filtered trees");
equal(getState().snapshots[0].census.filter, "str",
"snapshot-we-were-in-the-middle-of-saving's census should be filtered");
dispatch(setFilterStringAndRefresh("", heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVING]);
ok(true, "changing filter string retriggers census");
ok(!getState().filter, "no longer filtering");
dispatch(setFilterString("obj"));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED]);
equal(getState().filter, "obj", "filtering for obj now");
equal(getState().snapshots[0].census.filter, "obj",
"census-we-were-in-the-middle-of-recomputing should be filtered again");
heapWorker.destroy();
yield front.detach();
});
--- a/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js
+++ b/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js
@@ -2,77 +2,90 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests the task creator `importSnapshotAndCensus()` for the whole flow of
* importing a snapshot, and its sub-actions.
*/
-let { actions, snapshotState: states } = require("devtools/client/memory/constants");
+let { actions, snapshotState: states, treeMapState } = require("devtools/client/memory/constants");
let { exportSnapshot, importSnapshotAndCensus } = require("devtools/client/memory/actions/io");
let { takeSnapshotAndCensus } = 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 { subscribe, dispatch, getState } = store;
let destPath = yield createTempFile();
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
let exportEvents = Promise.all([
waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
waitUntilAction(store, actions.EXPORT_SNAPSHOT_END)
]);
dispatch(exportSnapshot(getState().snapshots[0], destPath));
yield exportEvents;
// Now import our freshly exported snapshot
- let i = 0;
- let expected = ["IMPORTING", "READING", "READ", "SAVING_CENSUS", "SAVED_CENSUS"];
+ let snapshotI = 0;
+ let censusI = 0;
+ let snapshotStates = ["IMPORTING", "READING", "READ"];
+ let censusStates = ["SAVING", "SAVED"];
let expectStates = () => {
let snapshot = getState().snapshots[1];
if (!snapshot) {
return;
}
- let isCorrectState = snapshot.state === states[expected[i]];
- if (isCorrectState) {
- ok(true, `Found expected state ${expected[i]}`);
- i++;
+ if (snapshotI < snapshotStates.length) {
+ let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]];
+ if (isCorrectState) {
+ ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
+ snapshotI++;
+ }
+ }
+ if (snapshot.treeMap && censusI < censusStates.length) {
+ if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
+ ok(true, `Found expected census state ${censusStates[censusI]}`);
+ censusI++;
+ }
}
};
let unsubscribe = subscribe(expectStates);
dispatch(importSnapshotAndCensus(heapWorker, destPath));
- yield waitUntilState(store, () => i === expected.length);
+ yield waitUntilState(store, () => { return snapshotI === snapshotStates.length &&
+ censusI === censusStates.length });
unsubscribe();
- equal(i, expected.length, "importSnapshotAndCensus() produces the correct sequence of states in a snapshot");
- equal(getState().snapshots[1].state, states.SAVED_CENSUS, "imported snapshot is in SAVED_CENSUS state");
+ equal(snapshotI, snapshotStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a snapshot");
+ equal(getState().snapshots[1].state, states.READ, "imported snapshot is in READ state");
+ equal(censusI, censusStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a census");
+ equal(getState().snapshots[1].treeMap.state, treeMapState.SAVED, "imported snapshot is in READ state");
ok(getState().snapshots[1].selected, "imported snapshot is selected");
// Check snapshot data
let snapshot1 = getState().snapshots[0];
let snapshot2 = getState().snapshots[1];
- equal(snapshot1.census.display, snapshot2.census.display,
+ equal(snapshot1.treeMap.display, snapshot2.treeMap.display,
"imported snapshot has correct display");
// Clone the census data so we can destructively remove the ID/parents to compare
// equal census data
- let census1 = stripUnique(JSON.parse(JSON.stringify(snapshot1.census.report)));
- let census2 = stripUnique(JSON.parse(JSON.stringify(snapshot2.census.report)));
+ let census1 = stripUnique(JSON.parse(JSON.stringify(snapshot1.treeMap.report)));
+ let census2 = stripUnique(JSON.parse(JSON.stringify(snapshot2.treeMap.report)));
equal(JSON.stringify(census1), JSON.stringify(census2), "Imported snapshot has correct census");
function stripUnique (obj) {
let children = obj.children || [];
for (let child of children) {
delete child.id;
delete child.parent;
--- a/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js
+++ b/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js
@@ -3,17 +3,17 @@
"use strict";
/**
* Tests `importSnapshotAndCensus()` when importing snapshots from the dominator
* tree view. The snapshot is expected to be loaded and its dominator tree
* should be computed.
*/
-let { snapshotState, dominatorTreeState, viewState, } =
+let { snapshotState, dominatorTreeState, viewState, treeMapState } =
require("devtools/client/memory/constants");
let { importSnapshotAndCensus } = require("devtools/client/memory/actions/io");
let { changeViewAndRefresh } = require("devtools/client/memory/actions/view");
function run_test() {
run_next_test();
}
@@ -28,18 +28,18 @@ add_task(function* () {
equal(getState().view, viewState.DOMINATOR_TREE,
"We should now be in the DOMINATOR_TREE view");
let i = 0;
let expected = [
"IMPORTING",
"READING",
"READ",
- "SAVING_CENSUS",
- "SAVED_CENSUS",
+ "treeMap:SAVING",
+ "treeMap:SAVED",
"dominatorTree:COMPUTING",
"dominatorTree:FETCHING",
"dominatorTree:LOADED",
];
let expectStates = () => {
let snapshot = getState().snapshots[0];
if (snapshot && hasExpectedState(snapshot, expected[i])) {
ok(true, `Found expected state ${expected[i]}`);
@@ -68,11 +68,17 @@ add_task(function* () {
*/
function hasExpectedState(snapshot, expectedState) {
let isDominatorState = expectedState.indexOf("dominatorTree:") === 0;
if (isDominatorState) {
let state = dominatorTreeState[expectedState.replace("dominatorTree:", "")];
return snapshot.dominatorTree && snapshot.dominatorTree.state === state;
}
+ let isTreeMapState = expectedState.indexOf("treeMap:") === 0;
+ if (isTreeMapState) {
+ let state = treeMapState[expectedState.replace("treeMap:", "")];
+ return snapshot.treeMap && snapshot.treeMap.state === state;
+ }
+
let state = snapshotState[expectedState];
return snapshot.state === state;
}
--- a/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js
+++ b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js
@@ -4,31 +4,34 @@
/**
* Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for display
* changing. We test this rather than `setCensusDisplayAndRefresh` directly, as
* we use the refresh action in the app itself composed from
* `setCensusDisplayAndRefresh`.
*/
-let { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
+let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = 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.CENSUS));
+
// Test default display with no snapshots
equal(getState().censusDisplay.breakdown.by, "coarseType",
"default coarseType display selected at start.");
dispatch(setCensusDisplayAndRefresh(heapWorker,
censusDisplays.allocationStack));
equal(getState().censusDisplay.breakdown.by, "allocationStack",
"display changed with no snapshots");
@@ -38,56 +41,70 @@ add_task(function *() {
yield waitUntilState(store, () => getState().errors.length === 1);
ok(true, "Emits an error action when passing in an invalid display object");
equal(getState().censusDisplay.breakdown.by, "allocationStack",
"current display unchanged when passing invalid display");
// Test new snapshots
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
-
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED]);
// Updates when changing display during `SAVING`
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED, censusState.SAVING]);
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED, censusState.SAVED]);
// Updates when changing display during `SAVING_CENSUS`
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVING_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVING]);
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
equal(getState().snapshots[2].census.display, censusDisplays.allocationStack,
"Display can be changed while saving census, stores updated display in snapshot");
// Updates census on currently selected snapshot when changing display
ok(getState().snapshots[2].selected, "Third snapshot currently selected");
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
- yield waitUntilState(store, state => state.snapshots[2].state === states.SAVED_CENSUS);
+ yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED);
equal(getState().snapshots[2].census.display, censusDisplays.coarseType,
"Snapshot census updated when changing displays after already generating one census");
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
- yield waitUntilState(store, state => state.snapshots[2].state === states.SAVED_CENSUS);
+ yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED);
equal(getState().snapshots[2].census.display, censusDisplays.allocationStack,
"Snapshot census updated when changing displays after already generating one census");
// Does not update unselected censuses
ok(!getState().snapshots[1].selected, "Second snapshot unselected currently");
equal(getState().snapshots[1].census.display, censusDisplays.coarseType,
"Second snapshot using `coarseType` display still and not yet updated to correct display");
// Updates to current display when switching to stale snapshot
dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING_CENSUS, states.SAVED_CENSUS]);
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVING,
+ censusState.SAVED]);
+ yield waitUntilCensusState(store, snapshot => snapshot.census,
+ [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
ok(getState().snapshots[1].selected, "Second snapshot selected currently");
equal(getState().snapshots[1].census.display, censusDisplays.allocationStack,
"Second snapshot using `allocationStack` display and updated to correct display");
heapWorker.destroy();
yield front.detach();
});
--- a/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js
+++ b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js
@@ -2,19 +2,20 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for custom
* displays.
*/
-let { snapshotState: states } = require("devtools/client/memory/constants");
+let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+let { changeView } = require("devtools/client/memory/actions/view");
let CUSTOM = {
displayName: "Custom",
tooltip: "Custom tooltip",
inverted: false,
breakdown: {
by: "internalType",
then: { by: "count", bytes: true, count: false }
@@ -27,22 +28,23 @@ function run_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.CENSUS));
dispatch(setCensusDisplayAndRefresh(heapWorker, CUSTOM));
equal(getState().censusDisplay, CUSTOM,
"CUSTOM display stored in display state.");
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
equal(getState().snapshots[0].census.display, CUSTOM,
"New snapshot stored CUSTOM display when done taking census");
ok(getState().snapshots[0].census.report.children.length, "Census has some children");
// Ensure we don't have `count` in any results
ok(getState().snapshots[0].census.report.children.every(c => !c.count),
"Census used CUSTOM display without counts");
// Ensure we do have `bytes` in the results
--- a/devtools/client/memory/test/unit/test_action-set-display.js
+++ b/devtools/client/memory/test/unit/test_action-set-display.js
@@ -3,31 +3,34 @@
"use strict";
/**
* Tests the action creator `setCensusDisplay()` for display changing. Does not
* test refreshing the census information, check `setCensusDisplayAndRefresh`
* action for that.
*/
-let { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
+let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
let { setCensusDisplay } = require("devtools/client/memory/actions/census-display");
let { 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.CENSUS));
+
// Test default display with no snapshots
equal(getState().censusDisplay.breakdown.by, "coarseType",
"default coarseType display selected at start.");
dispatch(setCensusDisplay(censusDisplays.allocationStack));
equal(getState().censusDisplay.breakdown.by, "allocationStack",
"display changed with no snapshots");
@@ -38,12 +41,12 @@ add_task(function*() {
} catch (e) {
ok(true, "Throws when passing in an invalid display object");
}
equal(getState().censusDisplay.breakdown.by, "allocationStack",
"current display unchanged when passing invalid display");
// Test new snapshots
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
equal(getState().snapshots[0].census.display, censusDisplays.allocationStack,
"New snapshots use the current, non-default display");
});
--- a/devtools/client/memory/test/unit/test_action-take-census.js
+++ b/devtools/client/memory/test/unit/test_action-take-census.js
@@ -1,28 +1,32 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the async reducer responding to the action `takeCensus(heapWorker, snapshot)`
*/
-var { snapshotState: states, censusDisplays } = require("devtools/client/memory/constants");
+var { snapshotState: states, censusDisplays, censusState, censusState, viewState } = require("devtools/client/memory/constants");
var actions = require("devtools/client/memory/actions/snapshot");
+var { 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();
+ store.dispatch(changeView(viewState.CENSUS));
+
store.dispatch(actions.takeSnapshot(front));
yield waitUntilState(store, () => {
let snapshots = store.getState().snapshots;
return snapshots.length === 1 && snapshots[0].state === states.SAVED;
});
let snapshot = store.getState().snapshots[0];
equal(snapshot.census, null, "No census data exists yet on the snapshot.");
@@ -32,17 +36,18 @@ add_task(function *() {
yield waitUntilState(store, () => store.getState().errors.length === 1);
ok(/Assertion failure/.test(store.getState().errors[0]),
"Error thrown when taking a census of a snapshot that has not been read.");
store.dispatch(actions.readSnapshot(heapWorker, snapshot.id));
yield waitUntilState(store, () => store.getState().snapshots[0].state === states.READ);
store.dispatch(actions.takeCensus(heapWorker, snapshot.id));
- yield waitUntilState(store, () => store.getState().snapshots[0].state === states.SAVING_CENSUS);
- yield waitUntilState(store, () => store.getState().snapshots[0].state === states.SAVED_CENSUS);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
snapshot = store.getState().snapshots[0];
ok(snapshot.census, "Snapshot has census after saved census");
ok(snapshot.census.report.children.length, "Census is in tree node form");
equal(snapshot.census.display, censusDisplays.coarseType,
"Snapshot stored correct display used for the census");
+
});
--- a/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js
+++ b/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js
@@ -1,46 +1,58 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the task creator `takeSnapshotAndCensus()` for the whole flow of
* taking a snapshot, and its sub-actions.
*/
-let { snapshotState: states } = require("devtools/client/memory/constants");
+let { snapshotState: states, treeMapState } = require("devtools/client/memory/constants");
let actions = 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 i = 0;
- let expected = ["SAVING", "SAVED", "READING", "READ", "SAVING_CENSUS", "SAVED_CENSUS"];
+ let snapshotI = 0;
+ let censusI = 0;
+ let snapshotStates = ["SAVING", "SAVED", "READING", "READ"];
+ let censusStates = ["SAVING", "SAVED"];
let expectStates = () => {
- if (i >= expected.length) { return false; }
+ let snapshot = store.getState().snapshots[0];
+ if (!snapshot) {
+ return;
+ }
+ if (snapshotI < snapshotStates.length) {
+ let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]];
+ if (isCorrectState) {
+ ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
+ snapshotI++;
+ }
+ }
+ if (snapshot.treeMap && censusI < censusStates.length) {
+ if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
+ ok(true, `Found expected census state ${censusStates[censusI]}`);
+ censusI++;
+ }
+ }
+ };
- let snapshot = store.getState().snapshots[0] || {};
- let isCorrectState = snapshot.state === states[expected[i]];
- if (isCorrectState) {
- ok(true, `Found expected state ${expected[i]}`);
- i++;
- }
- return isCorrectState;
- };
let unsubscribe = store.subscribe(expectStates);
store.dispatch(actions.takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilState(store, () => i === 6);
+ yield waitUntilState(store, () => { return snapshotI === snapshotStates.length &&
+ censusI === censusStates.length });
unsubscribe();
ok(true, "takeSnapshotAndCensus() produces the correct sequence of states in a snapshot");
let snapshot = store.getState().snapshots[0];
- ok(snapshot.census, "snapshot has census data");
+ ok(snapshot.treeMap, "snapshot has tree map census data");
ok(snapshot.selected, "snapshot is selected");
});
--- a/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js
@@ -2,76 +2,81 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that changing displays with different inverted state properly
// refreshes the selected census.
const {
censusDisplays,
snapshotState: states,
+ censusState,
+ viewState
} = require("devtools/client/memory/constants");
const {
setCensusDisplayAndRefresh
} = require("devtools/client/memory/actions/census-display");
const {
takeSnapshotAndCensus,
selectSnapshotAndRefresh,
} = 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.CENSUS));
+
// Select a non-inverted display.
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
equal(getState().censusDisplay.inverted, false, "not inverted by default");
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVED_CENSUS,
- states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
ok(true, "saved 3 snapshots and took a census of each of them");
// Select an inverted display.
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVED_CENSUS,
- states.SAVING_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVING]);
ok(true, "toggling inverted should recompute the selected snapshot's census");
equal(getState().censusDisplay.inverted, true, "now inverted");
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVED_CENSUS,
- states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
equal(getState().snapshots[0].census.display.inverted, false);
equal(getState().snapshots[1].census.display.inverted, false);
equal(getState().snapshots[2].census.display.inverted, true);
dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVING_CENSUS,
- states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVING,
+ censusState.SAVED]);
ok(true, "selecting non-inverted census should trigger a recompute");
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
- states.SAVED_CENSUS,
- states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
equal(getState().snapshots[0].census.display.inverted, false);
equal(getState().snapshots[1].census.display.inverted, true);
equal(getState().snapshots[2].census.display.inverted, true);
heapWorker.destroy();
yield front.detach();
});
--- a/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js
@@ -1,50 +1,54 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that changing inverted state in the middle of taking a snapshot results
// in an inverted census.
-const { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
+const { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
const { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
+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.CENSUS));
+
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
equal(getState().censusDisplay.inverted, false);
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilSnapshotState(store, [states.SAVING]);
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
ok(getState().censusDisplay.inverted,
"should want inverted trees");
ok(getState().snapshots[0].census.display.inverted,
"snapshot-we-were-in-the-middle-of-saving's census should be inverted");
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
- yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
ok(true, "toggling inverted retriggers census");
ok(!getState().censusDisplay.inverted, "no longer inverted");
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
ok(getState().censusDisplay.inverted, "inverted again");
ok(getState().snapshots[0].census.display.inverted,
"census-we-were-in-the-middle-of-recomputing should be inverted again");
heapWorker.destroy();
yield front.detach();
});
--- a/devtools/client/memory/test/unit/test_action_diffing_02.js
+++ b/devtools/client/memory/test/unit/test_action_diffing_02.js
@@ -1,36 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that toggling diffing unselects all snapshots.
-const { snapshotState } = require("devtools/client/memory/constants");
+const { snapshotState, censusState, viewState } = require("devtools/client/memory/constants");
const { toggleDiffing } = require("devtools/client/memory/actions/diffing");
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();
const { getState, dispatch } = store;
+ dispatch(changeView(viewState.CENSUS));
+
equal(getState().diffing, null, "not diffing by default");
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [snapshotState.SAVED_CENSUS,
- snapshotState.SAVED_CENSUS,
- snapshotState.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+ censusState.SAVED,
+ censusState.SAVED]);
ok(getState().snapshots.some(s => s.selected),
"One of the new snapshots is selected");
dispatch(toggleDiffing());
ok(getState().diffing, "now diffing after toggling");
for (let s of getState().snapshots) {
--- a/devtools/client/memory/test/unit/test_action_diffing_03.js
+++ b/devtools/client/memory/test/unit/test_action_diffing_03.js
@@ -1,36 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test selecting snapshots for diffing.
-const { diffingState, snapshotState } = require("devtools/client/memory/constants");
+const { diffingState, snapshotState, viewState } = require("devtools/client/memory/constants");
const {
toggleDiffing,
selectSnapshotForDiffing
} = require("devtools/client/memory/actions/diffing");
const { takeSnapshot } = 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();
const { getState, dispatch } = store;
+ dispatch(changeView(viewState.CENSUS));
equal(getState().diffing, null, "not diffing by default");
dispatch(takeSnapshot(front, heapWorker));
dispatch(takeSnapshot(front, heapWorker));
dispatch(takeSnapshot(front, heapWorker));
+
yield waitUntilSnapshotState(store, [snapshotState.SAVED,
snapshotState.SAVED,
snapshotState.SAVED]);
dispatch(takeSnapshot(front));
// Start diffing.
dispatch(toggleDiffing());
ok(getState().diffing, "now diffing after toggling");
--- a/devtools/client/memory/test/unit/test_action_diffing_04.js
+++ b/devtools/client/memory/test/unit/test_action_diffing_04.js
@@ -1,36 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that we compute census diffs.
const {
diffingState,
- snapshotState
+ snapshotState,
+ viewState
} = require("devtools/client/memory/constants");
const {
toggleDiffing,
selectSnapshotForDiffingAndRefresh
} = require("devtools/client/memory/actions/diffing");
const {
takeSnapshot,
readSnapshot
} = 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();
const { getState, dispatch } = store;
+ dispatch(changeView(viewState.CENSUS));
equal(getState().diffing, null, "not diffing by default");
const s1 = yield dispatch(takeSnapshot(front, heapWorker));
const s2 = yield dispatch(takeSnapshot(front, heapWorker));
const s3 = yield dispatch(takeSnapshot(front, heapWorker));
dispatch(readSnapshot(heapWorker, s1));
dispatch(readSnapshot(heapWorker, s2));
--- a/devtools/client/memory/test/unit/test_action_diffing_05.js
+++ b/devtools/client/memory/test/unit/test_action_diffing_05.js
@@ -2,42 +2,45 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that we recompute census diffs at the appropriate times.
const {
diffingState,
snapshotState,
censusDisplays,
+ viewState,
} = require("devtools/client/memory/constants");
const {
setCensusDisplayAndRefresh,
} = require("devtools/client/memory/actions/census-display");
const {
toggleDiffing,
selectSnapshotForDiffingAndRefresh,
} = require("devtools/client/memory/actions/diffing");
const {
setFilterStringAndRefresh,
} = require("devtools/client/memory/actions/filter");
const {
takeSnapshot,
readSnapshot,
} = 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();
const { getState, dispatch } = store;
+ dispatch(changeView(viewState.CENSUS));
yield dispatch(setCensusDisplayAndRefresh(heapWorker,
censusDisplays.allocationStack));
equal(getState().censusDisplay.inverted, false,
"not inverted at start");
equal(getState().diffing, null, "not diffing by default");
--- a/devtools/client/memory/test/unit/test_dominator_trees_01.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_01.js
@@ -1,16 +1,17 @@
/* 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,
+ treeMapState,
} = require("devtools/client/memory/constants");
let {
takeSnapshotAndCensus,
computeAndFetchDominatorTree,
} = require("devtools/client/memory/actions/snapshot");
function run_test() {
run_next_test();
@@ -19,17 +20,17 @@ function run_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]);
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
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");
--- a/devtools/client/memory/test/unit/test_dominator_trees_02.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_02.js
@@ -3,16 +3,17 @@
// Test that selecting the dominator tree view automatically kicks off fetching
// and computing dominator trees.
const {
snapshotState: states,
dominatorTreeState,
viewState,
+ treeMapState,
} = require("devtools/client/memory/constants");
const {
takeSnapshotAndCensus,
} = require("devtools/client/memory/actions/snapshot");
const {
changeViewAndRefresh
} = require("devtools/client/memory/actions/view");
@@ -23,17 +24,17 @@ function run_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]);
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
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");
--- a/devtools/client/memory/test/unit/test_dominator_trees_04.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_04.js
@@ -22,17 +22,17 @@ function run_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]) {
+ states.READ]) {
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]);
--- a/devtools/client/memory/test/unit/test_dominator_trees_05.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_05.js
@@ -3,16 +3,17 @@
// 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,
+ treeMapState,
} = require("devtools/client/memory/constants");
let {
takeSnapshotAndCensus,
selectSnapshotAndRefresh,
} = require("devtools/client/memory/actions/snapshot");
let { changeView } = require("devtools/client/memory/actions/view");
@@ -24,18 +25,18 @@ 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]);
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED,
+ treeMapState.SAVED]);
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 =>
--- a/devtools/client/memory/test/unit/test_dominator_trees_08.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_08.js
@@ -4,16 +4,17 @@
// Test that we can change the display with which we describe a dominator tree
// and that the dominator tree is re-fetched.
const {
snapshotState: states,
dominatorTreeState,
viewState,
dominatorTreeDisplays,
+ treeMapState
} = require("devtools/client/memory/constants");
const {
setDominatorTreeDisplayAndRefresh
} = require("devtools/client/memory/actions/dominator-tree-display");
const {
changeView,
} = require("devtools/client/memory/actions/view");
const {
@@ -30,17 +31,17 @@ add_task(function *() {
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]);
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
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 &&
--- a/devtools/client/memory/test/unit/test_dominator_trees_09.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_09.js
@@ -4,16 +4,17 @@
// Test that we can change the display with which we describe a dominator tree
// while the dominator tree is in the middle of being fetched.
const {
snapshotState: states,
dominatorTreeState,
viewState,
dominatorTreeDisplays,
+ treeMapState,
} = require("devtools/client/memory/constants");
const {
setDominatorTreeDisplayAndRefresh
} = require("devtools/client/memory/actions/dominator-tree-display");
const {
changeView,
} = require("devtools/client/memory/actions/view");
const {
@@ -30,17 +31,17 @@ add_task(function *() {
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]);
+ yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
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 &&
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_tree-map-01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { drawBox } = require("devtools/client/memory/components/tree-map/draw");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function *() {
+ let fillRectValues, strokeRectValues;
+ let ctx = {
+ fillRect: (...args) => fillRectValues = args,
+ strokeRect: (...args) => strokeRectValues = args
+ };
+ let node = {
+ x: 20,
+ y: 30,
+ dx: 50,
+ dy: 70,
+ type: "other",
+ depth: 2
+ };
+ let borderWidth = () => 1;
+ let dragZoom = {
+ offsetX: 0,
+ offsetY: 0,
+ zoom: 0
+ };
+
+ drawBox(ctx, node, borderWidth, dragZoom);
+ ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
+ equal(ctx.fillStyle, "hsl(210,60%,70%)", "The fillStyle is set");
+ equal(ctx.strokeStyle, "hsl(210,60%,35%)", "The strokeStyle is set");
+ equal(ctx.lineWidth, 1, "The lineWidth is set");
+ deepEqual(fillRectValues, [20.5,30.5,49,69], "Draws a filled rectangle");
+ deepEqual(strokeRectValues, [20.5,30.5,49,69], "Draws a stroked rectangle");
+
+
+ dragZoom.zoom = 0.5;
+
+ drawBox(ctx, node, borderWidth, dragZoom);
+ ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
+ deepEqual(fillRectValues, [30.5,45.5,74,104],
+ "Draws a zoomed filled rectangle");
+ deepEqual(strokeRectValues, [30.5,45.5,74,104],
+ "Draws a zoomed stroked rectangle");
+
+ dragZoom.offsetX = 110;
+ dragZoom.offsetY = 130;
+
+ drawBox(ctx, node, borderWidth, dragZoom);
+ deepEqual(fillRectValues, [-79.5,-84.5,74,104],
+ "Draws a zoomed and offset filled rectangle");
+ deepEqual(strokeRectValues, [-79.5,-84.5,74,104],
+ "Draws a zoomed and offset stroked rectangle");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_tree-map-02.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { drawText } = require("devtools/client/memory/components/tree-map/draw");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function*() {
+ // Mock out the Canvas2dContext
+ let ctx = {
+ fillText: (...args) => fillTextValues.push(args),
+ measureText: (text) => {
+ let width = text ? text.length * 10 : 0;
+ return { width };
+ }
+ };
+ let node = {
+ x: 20,
+ y: 30,
+ dx: 500,
+ dy: 70,
+ name: "Example Node",
+ totalBytes: 1200,
+ totalCount: 100
+ };
+ let ratio = 0;
+ let borderWidth = () => 1;
+ let dragZoom = {
+ offsetX: 0,
+ offsetY: 0,
+ zoom: 0
+ };
+ let fillTextValues = [];
+
+ drawText(ctx, node, borderWidth, ratio, dragZoom);
+ deepEqual(fillTextValues[0], ["Example Node", 21.5, 31.5],
+ "Fills in the full node name");
+ deepEqual(fillTextValues[1], ["1KiB 100 count", 151.5, 31.5],
+ "Includes the full byte and count information");
+
+ fillTextValues = [];
+ node.dx = 250;
+ drawText(ctx, node, borderWidth, ratio, dragZoom);
+
+ deepEqual(fillTextValues[0], ["Example Node", 21.5, 31.5],
+ "Fills in the full node name");
+ deepEqual(fillTextValues[1], undefined,
+ "Drops off the byte and count information if not enough room");
+
+ fillTextValues = [];
+ node.dx = 100;
+ drawText(ctx, node, borderWidth, ratio, dragZoom);
+
+ deepEqual(fillTextValues[0], ["Exampl...", 21.5, 31.5],
+ "Cuts the name with ellipsis");
+ deepEqual(fillTextValues[1], undefined,
+ "Drops off the byte and count information if not enough room");
+
+ fillTextValues = [];
+ node.dx = 40;
+ drawText(ctx, node, borderWidth, ratio, dragZoom);
+
+ deepEqual(fillTextValues[0], ["...", 21.5, 31.5],
+ "Shows only ellipsis when smaller");
+ deepEqual(fillTextValues[1], undefined,
+ "Drops off the byte and count information if not enough room");
+
+ fillTextValues = [];
+ node.dx = 20;
+ drawText(ctx, node, borderWidth, ratio, dragZoom);
+
+ deepEqual(fillTextValues[0], undefined,
+ "Draw nothing when not enough room");
+ deepEqual(fillTextValues[1], undefined,
+ "Drops off the byte and count information if not enough room");
+});
--- a/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js
+++ b/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js
@@ -2,37 +2,40 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that we use the correct snapshot aggregate value
* in `utils.getSnapshotTotals(snapshot)`
*/
-const { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
+const { censusDisplays, snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants");
const { getSnapshotTotals } = require("devtools/client/memory/utils");
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
const { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
+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.CENSUS));
+
yield dispatch(setCensusDisplayAndRefresh(heapWorker,
censusDisplays.allocationStack));
dispatch(takeSnapshotAndCensus(front, heapWorker));
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
ok(!getState().snapshots[0].census.display.inverted, "Snapshot is not inverted");
let census = getState().snapshots[0].census;
let result = aggregate(census.report);
let totalBytes = result.bytes;
let totalCount = result.count;
@@ -40,18 +43,19 @@ add_task(function *() {
ok(totalCount > 0, "counted up count in the census");
result = getSnapshotTotals(getState().snapshots[0].census);
equal(totalBytes, result.bytes, "getSnapshotTotals reuslted in correct bytes");
equal(totalCount, result.count, "getSnapshotTotals reuslted in correct count");
dispatch(setCensusDisplayAndRefresh(heapWorker,
censusDisplays.invertedAllocationStack));
- yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
- yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+ yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
ok(getState().snapshots[0].census.display.inverted, "Snapshot is inverted");
result = getSnapshotTotals(getState().snapshots[0].census);
equal(totalBytes, result.bytes,
"getSnapshotTotals reuslted in correct bytes when inverted");
equal(totalCount, result.count,
"getSnapshotTotals reuslted in correct count when inverted");
});
--- a/devtools/client/memory/test/unit/test_utils.js
+++ b/devtools/client/memory/test/unit/test_utils.js
@@ -46,9 +46,25 @@ add_task(function *() {
"formatNumber can display number sign");
equal(utils.formatNumber(-12, true), "-12",
"formatNumber can display number sign (negative)");
ok(true, "test formatPercent util functions");
equal(utils.formatPercent(12), "12%", "formatPercent returns 12% for 12");
equal(utils.formatPercent(12345), "12 345%",
"formatPercent returns 12 345% for 12345");
+
+ equal(utils.formatAbbreviatedBytes(12), "12B", "Formats bytes");
+ equal(utils.formatAbbreviatedBytes(12345), "12KiB", "Formats kilobytes");
+ equal(utils.formatAbbreviatedBytes(12345678), "11MiB", "Formats megabytes");
+ equal(utils.formatAbbreviatedBytes(12345678912), "11GiB", "Formats gigabytes");
+
+ equal(utils.hslToStyle(0.5, 0.6, 0.7),
+ "hsl(180,60%,70%)", "hslToStyle converts an array to a style string");
+ equal(utils.hslToStyle(0, 0, 0),
+ "hsl(0,0%,0%)", "hslToStyle converts an array to a style string");
+ equal(utils.hslToStyle(1, 1, 1),
+ "hsl(360,100%,100%)", "hslToStyle converts an array to a style string");
+
+ equal(utils.lerp(5, 7, 0), 5, "lerp return first number for 0");
+ equal(utils.lerp(5, 7, 1), 7, "lerp return second number for 1");
+ equal(utils.lerp(5, 7, 0.5), 6, "lerp interpolates the numbers for 0.5");
});
--- a/devtools/client/memory/test/unit/xpcshell.ini
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -37,10 +37,12 @@ skip-if = toolkit == 'android' || toolki
[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_tree-map-01.js]
+[test_tree-map-02.js]
[test_utils.js]
[test_utils-get-snapshot-totals.js]
--- a/devtools/client/memory/utils.js
+++ b/devtools/client/memory/utils.js
@@ -8,20 +8,26 @@ const { LocalizationHelper } = require("
const STRINGS_URI = "chrome://devtools/locale/memory.properties"
const L10N = exports.L10N = new LocalizationHelper(STRINGS_URI);
const { OS } = require("resource://gre/modules/osfile.jsm");
const { assert } = require("devtools/shared/DevToolsUtils");
const { Preferences } = require("resource://gre/modules/Preferences.jsm");
const CUSTOM_CENSUS_DISPLAY_PREF = "devtools.memory.custom-census-displays";
const CUSTOM_DOMINATOR_TREE_DISPLAY_PREF = "devtools.memory.custom-dominator-tree-displays";
+const CUSTOM_TREE_MAP_DISPLAY_PREF = "devtools.memory.custom-tree-map-displays";
+const BYTES = 1024;
+const KILOBYTES = Math.pow(BYTES, 2);
+const MEGABYTES = Math.pow(BYTES, 3);
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const {
snapshotState: states,
diffingState,
+ censusState,
+ treeMapState,
censusDisplays,
dominatorTreeDisplays,
dominatorTreeState
} = require("./constants");
/**
* Takes a snapshot object and returns the localized form of its timestamp to be
* used as a title.
@@ -75,16 +81,26 @@ exports.getCustomCensusDisplays = functi
*
* @return {Object}
*/
exports.getCustomDominatorTreeDisplays = function () {
return getCustomDisplaysHelper(CUSTOM_DOMINATOR_TREE_DISPLAY_PREF);
};
/**
+ * Returns custom displays defined in
+ * `devtools.memory.custom-tree-map-displays` pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomTreeMapDisplays = function () {
+ return getCustomDisplaysHelper(CUSTOM_TREE_MAP_DISPLAY_PREF);
+};
+
+/**
* 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");
@@ -101,19 +117,22 @@ exports.getStatusText = function (state)
case states.IMPORTING:
return L10N.getStr("snapshot.state.importing");
case states.SAVED:
case states.READING:
return L10N.getStr("snapshot.state.reading");
- case states.SAVING_CENSUS:
+ case censusState.SAVING:
return L10N.getStr("snapshot.state.saving-census");
+ case treeMapState.SAVING:
+ return L10N.getStr("snapshot.state.saving-tree-map");
+
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");
@@ -128,17 +147,18 @@ exports.getStatusText = function (state)
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:
+ case censusState.SAVED:
+ case treeMapState.SAVED:
return "";
default:
assert(false, `Unexpected state: ${state}`);
return "";
}
};
@@ -164,19 +184,22 @@ exports.getStatusTextFull = function (st
case states.IMPORTING:
return L10N.getStr("snapshot.state.importing");
case states.SAVED:
case states.READING:
return L10N.getStr("snapshot.state.reading.full");
- case states.SAVING_CENSUS:
+ case censusState.SAVING:
return L10N.getStr("snapshot.state.saving-census.full");
+ case treeMapState.SAVING:
+ return L10N.getStr("snapshot.state.saving-tree-map.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");
@@ -191,34 +214,35 @@ exports.getStatusTextFull = function (st
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:
+ case censusState.SAVED:
+ case treeMapState.SAVED:
return "";
default:
assert(false, `Unexpected state: ${state}`);
return "";
}
};
/**
* Return true if the snapshot is in a diffable state, false otherwise.
*
* @param {snapshotModel} snapshot
* @returns {Boolean}
*/
exports.snapshotIsDiffable = function snapshotIsDiffable(snapshot) {
- return snapshot.state === states.SAVED_CENSUS
- || snapshot.state === states.SAVING_CENSUS
+ return (snapshot.census && snapshot.census.state === censusState.SAVED)
+ || (snapshot.census && snapshot.census.state === censusState.SAVING)
|| snapshot.state === states.SAVED
|| snapshot.state === states.READ;
};
/**
* Takes an array of snapshots and a snapshot and returns
* the snapshot instance in `snapshots` that matches
* the snapshot passed in.
@@ -251,16 +275,17 @@ exports.createSnapshot = function create
});
}
return Object.freeze({
id: ++ID_COUNTER,
state: states.SAVING,
dominatorTree,
census: null,
+ treeMap: null,
path: null,
imported: false,
selected: false,
error: null,
});
};
/**
@@ -270,35 +295,67 @@ exports.createSnapshot = function create
* @param {String} filter
* @param {censusDisplayModel} display
* @param {censusModel} census
*
* @returns {Boolean}
*/
exports.censusIsUpToDate = function (filter, display, census) {
return census
- && filter === census.filter
+ // Filter could be null == undefined so use loose equality.
+ && filter == census.filter
&& display === census.display;
};
+
+/**
+ * Check to see if the snapshot is in a state that it can take a census.
+ *
+ * @param {SnapshotModel} A snapshot to check.
+ * @param {Boolean} Assert that the snapshot must be in a ready state.
+ * @returns {Boolean}
+ */
+exports.canTakeCensus = function (snapshot) {
+ return snapshot.state === states.READ &&
+ (!snapshot.census || snapshot.census.state === censusState.SAVED) &&
+ (!snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED);
+};
+
/**
* 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);
};
/**
+ * Find the first SAVED census, either from the tree map or the normal
+ * census.
+ *
+ * @param {SnapshotModel} snapshot
+ * @returns {Object|null} Either the census, or null if one hasn't completed
+ */
+exports.getSavedCensus = function (snapshot) {
+ if (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) {
+ return snapshot.treeMap;
+ }
+ if (snapshot.census && snapshot.census.state === censusState.SAVED) {
+ return snapshot.census;
+ }
+ return null;
+};
+
+/**
* 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;
@@ -389,8 +446,59 @@ exports.formatNumber = function(number,
*
* @param {Number} percent
* @param {Boolean} showSign (defaults to false)
*/
exports.formatPercent = function(percent, showSign = false) {
return exports.L10N.getFormatStr("tree-item.percent",
exports.formatNumber(percent, showSign));
};
+
+/**
+ * Change an HSL color array with values ranged 0-1 to a properly formatted
+ * ctx.fillStyle string.
+ *
+ * @param {Number} h
+ * hue values ranged between [0 - 1]
+ * @param {Number} s
+ * hue values ranged between [0 - 1]
+ * @param {Number} l
+ * hue values ranged between [0 - 1]
+ * @return {type}
+ */
+exports.hslToStyle = function(h, s, l) {
+ h = parseInt(h * 360, 10);
+ s = parseInt(s * 100, 10);
+ l = parseInt(l * 100, 10);
+
+ return `hsl(${h},${s}%,${l}%)`;
+};
+
+/**
+ * Linearly interpolate between 2 numbers.
+ *
+ * @param {Number} a
+ * @param {Number} b
+ * @param {Number} t
+ * A value of 0 returns a, and 1 returns b
+ * @return {Number}
+ */
+exports.lerp = function(a, b, t) {
+ return a * (1 - t) + b * t;
+};
+
+/**
+ * Format a number of bytes as human readable, e.g. 13434 => '13KiB'.
+ *
+ * @param {Number} n
+ * Number of bytes
+ * @return {String}
+ */
+exports.formatAbbreviatedBytes = function(n) {
+ if (n < BYTES) {
+ return n + "B";
+ } else if (n < KILOBYTES) {
+ return Math.floor(n / BYTES) + "KiB";
+ } else if (n < MEGABYTES) {
+ return Math.floor(n / KILOBYTES) + "MiB";
+ }
+ return Math.floor(n / MEGABYTES) + "GiB";
+};
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -104,16 +104,17 @@ pref("devtools.debugger.ui.variables-sor
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-census-displays", "{}");
pref("devtools.memory.custom-dominator-tree-displays", "{}");
+pref("devtools.memory.custom-tree-map-displays", "{}");
// 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/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -490,16 +490,29 @@ html, body, #app, #memory-tool {
}
.heap-tree-item.focused .heap-tree-number,
.heap-tree-item.focused .heap-tree-percent {
color: inherit;
}
/**
+ * Tree map
+ */
+
+.tree-map-container {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+ margin-top: -1em;
+ padding-bottom: 1em;
+}
+
+/**
* Heap tree errors.
*/
.error::before {
content: "";
display: inline-block;
width: 12px;
height: 12px;
--- a/devtools/docs/memory-panel.md
+++ b/devtools/docs/memory-panel.md
@@ -199,21 +199,25 @@ restricted to within these tasks.
### Snapshots State
On the JavaScript side, the snapshots represent a reference to the underlying
heap dump and the various analyses. The following diagram represents a finite
state machine describing the snapshot states. Any of these states may go to the
ERROR state, from which they can never leave.
```
-SAVING → SAVED → READING → READ SAVED_CENSUS
- ↗ ↘ ↑ ↓
- IMPORTING SAVING_CENSUS
+SAVING → SAVED → READING → READ
+ ↗
+ IMPORTING
```
+Each of the report types (census, diffing, tree maps, dominators) have their own states as well, and are documented at `devtools/client/memory/constants.js`.
+These report states are updated as the various filtering and selecting options
+are updated in the UI.
+
### Testing the Frontend
Unit tests for React components are in `devtools/client/memory/test/chrome/*`.
Unit tests for actions, reducers, and state changes are in
`devtools/client/memory/test/unit/*`.
Holistic integration tests for the frontend and the whole memory tool are in