Bug 1249788 - Implement the census individuals view; r=jsantell

This adds the INDIVIDUALS view state to the memory panel, renames "dominator
tree display" to "label display", and adds a view for listing the individual
nodes in a census group and inspecting each nodes' retaining paths.
This commit is contained in:
Nick Fitzgerald
2016-04-11 18:04:31 -07:00
parent 1d5b9a6db5
commit f2a6ddf449
63 changed files with 1922 additions and 238 deletions

View File

@@ -69,6 +69,8 @@ registerCleanupFunction(() => {
Services.obs.removeObserver(ConsoleObserver, "console-api-log-event");
});
var waitForTime = DevToolsUtils.waitForTime;
function getFrameScript() {
let mm = gBrowser.selectedBrowser.messageManager;
let frameURL = "chrome://devtools/content/shared/frame-script-utils.js";

View File

@@ -65,6 +65,15 @@ toolbar.displayBy=Group by:
# describing the select menu options of the display options.
toolbar.displayBy.tooltip=Change how objects are grouped
# TODO FITZGEN
toolbar.pop-view=
# TODO FITZGEN
toolbar.pop-view.label=Go back to aggregates
# TODO FITZGEN
toolbar.viewing-individuals=⁂ Viewing individuals in group
# LOCALIZATION NOTE (censusDisplays.coarseType.tooltip): The tooltip for the
# "coarse type" display option.
censusDisplays.coarseType.tooltip=Group items by their type
@@ -169,6 +178,9 @@ filter.placeholder=Filter
# tool's filter search box.
filter.tooltip=Filter the contents of the heap snapshot
# TODO FITZGEN
tree-item.view-individuals.tooltip=View individual nodes in this group and their retaining paths
# LOCALIZATION NOTE (tree-item.load-more): The label for the links to fetch the
# lazily loaded sub trees in the dominator tree view.
tree-item.load-more=Load more…
@@ -295,6 +307,24 @@ snapshot.state.saving-tree-map.full=Saving tree map…
# snapshot state ERROR, used in the main heap view.
snapshot.state.error.full=There was an error processing this snapshot.
# TODO FITZGEN
individuals.state.error=Error
# TODO FITZGEN
individuals.state.error.full=There was an error while fetching individuals in the group
# TODO FITZGEN
individuals.state.fetching=Fetching…
# TODO FITZGEN
individuals.state.fetching.full=Fetching individuals in group…
# TODO FITZGEN
individuals.field.node=Node
# TODO FITZGEN
individuals.field.node.tooltip=The individual node in the snapshot
# LOCALIZATION NOTE (snapshot.state.saving): The label describing the snapshot
# state SAVING, used in the snapshot list view
snapshot.state.saving=Saving snapshot…

View File

@@ -9,6 +9,7 @@ const { refresh } = require("./refresh");
exports.setCensusDisplayAndRefresh = function(heapWorker, display) {
return function*(dispatch, getState) {
console.log("FITZGEN: setCensusDisplayAndRefresh", display);
dispatch(setCensusDisplay(display));
yield dispatch(refresh(heapWorker));
};

View File

@@ -9,7 +9,8 @@ const telemetry = require("../telemetry");
const {
getSnapshot,
censusIsUpToDate,
snapshotIsDiffable
snapshotIsDiffable,
findSelectedSnapshot,
} = require("../utils");
// This is a circular dependency, so do not destructure the needed properties.
const snapshotActions = require("./snapshot");
@@ -21,7 +22,9 @@ const toggleDiffing = exports.toggleDiffing = function () {
return function(dispatch, getState) {
dispatch({
type: actions.CHANGE_VIEW,
view: getState().diffing ? viewState.CENSUS : viewState.DIFFING,
newViewState: getState().diffing ? viewState.CENSUS : viewState.DIFFING,
oldDiffing: getState().diffing,
oldSelected: findSelectedSnapshot(getState()),
});
};
};

View File

@@ -7,21 +7,24 @@ const { assert } = require("devtools/shared/DevToolsUtils");
const { actions } = require("../constants");
const { refresh } = require("./refresh");
exports.setDominatorTreeDisplayAndRefresh = function(heapWorker, display) {
/**
* Change the display we use for labeling individual nodes and refresh the
* current data.
*/
exports.setLabelDisplayAndRefresh = function(heapWorker, display) {
return function*(dispatch, getState) {
// Clears out all stored census data and sets the display.
dispatch(setDominatorTreeDisplay(display));
dispatch(setLabelDisplay(display));
yield dispatch(refresh(heapWorker));
};
};
/**
* Clears out all census data in the snapshots and sets
* a new display.
* Change the display we use for labeling individual nodes.
*
* @param {dominatorTreeDisplayModel} display
* @param {labelDisplayModel} display
*/
const setDominatorTreeDisplay = exports.setDominatorTreeDisplay = function (display) {
const setLabelDisplay = exports.setLabelDisplay = function (display) {
assert(typeof display === "object"
&& display
&& display.breakdown
@@ -29,7 +32,7 @@ const setDominatorTreeDisplay = exports.setDominatorTreeDisplay = function (disp
`Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(display)}`);
return {
type: actions.SET_DOMINATOR_TREE_DISPLAY,
type: actions.SET_LABEL_DISPLAY,
display,
};
};

View File

@@ -7,12 +7,13 @@ DevToolsModules(
'allocations.js',
'census-display.js',
'diffing.js',
'dominator-tree-display.js',
'filter.js',
'io.js',
'label-display.js',
'refresh.js',
'sizes.js',
'snapshot.js',
'task-cache.js',
'tree-map-display.js',
'view.js',
)

View File

@@ -14,8 +14,9 @@ const snapshot = require("./snapshot");
* @param {HeapAnalysesWorker} heapWorker
*/
exports.refresh = function (heapWorker) {
console.log("FITZGEN: refresh");
return function* (dispatch, getState) {
switch (getState().view) {
switch (getState().view.state) {
case viewState.DIFFING:
assert(getState().diffing, "Should have diffing state if in diffing view");
yield dispatch(refreshDiffing(heapWorker));
@@ -33,8 +34,12 @@ exports.refresh = function (heapWorker) {
yield dispatch(snapshot.refreshSelectedTreeMap(heapWorker));
return;
case viewState.INDIVIDUALS:
yield dispatch(snapshot.refreshIndividuals(heapWorker));
return;
default:
assert(false, `Unexpected view state: ${getState().view}`);
assert(false, `Unexpected view state: ${getState().view.state}`);
}
};
};

View File

@@ -3,7 +3,8 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { assert, reportException } = require("devtools/shared/DevToolsUtils");
const { Preferences } = require("resource://gre/modules/Preferences.jsm");
const { assert, reportException, isSet } = require("devtools/shared/DevToolsUtils");
const {
censusIsUpToDate,
getSnapshot,
@@ -16,12 +17,14 @@ const {
viewState,
censusState,
treeMapState,
dominatorTreeState
dominatorTreeState,
individualsState,
} = require("../constants");
const telemetry = require("../telemetry");
const view = require("./view");
const refresh = require("./refresh");
const diffing = require("./diffing");
const TaskCache = require("./task-cache");
/**
* A series of actions are fired from this task to save, read and generate the
@@ -56,16 +59,16 @@ const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function (front, h
* @param {snapshotId} id
*/
const computeSnapshotData = exports.computeSnapshotData = function(heapWorker, id) {
return function* (dispatch, getState) {
return function*(dispatch, getState) {
if (getSnapshot(getState(), id).state !== states.READ) {
return;
}
// Decide which type of census to take.
const censusTaker = getCurrentCensusTaker(getState().view);
const censusTaker = getCurrentCensusTaker(getState().view.state);
yield dispatch(censusTaker(heapWorker, id));
if (getState().view === viewState.DOMINATOR_TREE &&
if (getState().view.state === viewState.DOMINATOR_TREE &&
!getSnapshot(getState(), id).dominatorTree) {
yield dispatch(computeAndFetchDominatorTree(heapWorker, id));
}
@@ -81,7 +84,7 @@ const computeSnapshotData = exports.computeSnapshotData = function(heapWorker, i
*/
const selectSnapshotAndRefresh = exports.selectSnapshotAndRefresh = function (heapWorker, id) {
return function *(dispatch, getState) {
if (getState().diffing) {
if (getState().diffing || getState().individuals) {
dispatch(view.changeView(viewState.CENSUS));
}
@@ -100,7 +103,7 @@ const takeSnapshot = exports.takeSnapshot = function (front) {
return function *(dispatch, getState) {
telemetry.countTakeSnapshot();
if (getState().diffing) {
if (getState().diffing || getState().individuals) {
dispatch(view.changeView(viewState.CENSUS));
}
@@ -130,11 +133,18 @@ const takeSnapshot = exports.takeSnapshot = function (front) {
* @param {HeapAnalysesClient} heapWorker
* @param {snapshotId} id
*/
const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, id) {
return function *(dispatch, getState) {
const readSnapshot = exports.readSnapshot =
TaskCache.declareCacheableTask({
getCacheKey(_, id) {
return id;
},
task: function*(heapWorker, id, removeFromCache, dispatch, getState) {
console.log("FITZGEN: readSnapshot");
const snapshot = getSnapshot(getState(), id);
assert([states.SAVED, states.IMPORTING].includes(snapshot.state),
`Should only read a snapshot once. Found snapshot in state ${snapshot.state}`);
`Should only read a snapshot once. Found snapshot in state ${snapshot.state}`);
let creationTime;
@@ -143,14 +153,20 @@ const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, i
yield heapWorker.readHeapSnapshot(snapshot.path);
creationTime = yield heapWorker.getCreationTime(snapshot.path);
} catch (error) {
console.log("FITZGEN: readSnapshot: error", error);
removeFromCache();
reportException("readSnapshot", error);
dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
return;
}
console.log("FITZGEN: readSnapshot: done reading");
removeFromCache();
dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime });
};
};
}
});
let takeCensusTaskCounter = 0;
/**
* Census and tree maps both require snapshots. This function shares the logic
@@ -173,18 +189,24 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
* @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) {
let thisTakeCensusTaskId = ++takeCensusTaskCounter;
return TaskCache.declareCacheableTask({
getCacheKey(_, id) {
return `take-census-task-${thisTakeCensusTaskId}-${id}`;
},
task: function*(heapWorker, id, removeFromCache, dispatch, getState) {
console.log("FITZGEN: takeCensus");
const snapshot = getSnapshot(getState(), id);
if (!snapshot) {
console.log("FITZGEN: no snapshot");
removeFromCache();
return;
}
// 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. snapshot.state = ${snapshot.state},
census.state = ${(getCensus(snapshot) || {}).state}`);
`Attempting to take a census when the snapshot is not in a ready state. snapshot.state = ${snapshot.state}, census.state = ${(getCensus(snapshot) || { state: null }).state}`);
let report, parentMap;
let display = getDisplay(getState());
@@ -192,6 +214,8 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
// If display, filter and inversion haven't changed, don't do anything.
if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
console.log("FITZGEN: census is up to date");
removeFromCache();
return;
}
@@ -202,6 +226,7 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
display = getDisplay(getState());
filter = getState().filter;
console.log("FITZGEN: taking census with display =", display.displayName);
dispatch({
type: beginAction,
id,
@@ -221,6 +246,8 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
{ breakdown: display.breakdown },
opts));
} catch (error) {
console.log("FITZGEN: error taking census: " + error + "\n" + error.stack);
removeFromCache();
reportException("takeCensus", error);
dispatch({ type: errorAction, id, error });
return;
@@ -229,6 +256,9 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
while (filter !== getState().filter ||
display !== getDisplay(getState()));
console.log("FITZGEN: done taking census");
removeFromCache();
dispatch({
type: endAction,
id,
@@ -239,8 +269,8 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
});
telemetry.countCensus({ filter, display });
};
};
}
});
}
/**
@@ -287,7 +317,7 @@ const defaultCensusTaker = takeTreeMap;
*
* @param {string} value from viewState
*/
const getCurrentCensusTaker = exports.getCurrentCensusTaker = function (currentView) {
const getCurrentCensusTaker = exports.getCurrentCensusTaker = function(currentView) {
switch (currentView) {
case viewState.TREE_MAP:
return takeTreeMap;
@@ -298,6 +328,136 @@ const getCurrentCensusTaker = exports.getCurrentCensusTaker = function (currentV
}
};
/**
* Focus the given node in the individuals view.
*
* @param {DominatorTreeNode} node.
*/
const focusIndividual = exports.focusIndividual = function(node) {
return {
type: actions.FOCUS_INDIVIDUAL,
node,
};
};
/**
* Fetch the individual `DominatorTreeNodes` for the census group specified by
* `censusBreakdown` and `reportLeafIndex`.
*
* @param {HeapAnalysesClient} heapWorker
* @param {SnapshotId} id
* @param {Object} censusBreakdown
* @param {Set<Number> | Number} reportLeafIndex
*/
const fetchIndividuals = exports.fetchIndividuals =
function(heapWorker, id, censusBreakdown, reportLeafIndex) {
return function*(dispatch, getState) {
if (getState().view.state !== viewState.INDIVIDUALS) {
dispatch(view.changeView(viewState.INDIVIDUALS));
}
const snapshot = getSnapshot(getState(), id);
assert(snapshot && snapshot.state === states.READ,
"The snapshot should already be read into memory");
if (!dominatorTreeIsComputed(snapshot)) {
yield dispatch(computeAndFetchDominatorTree(heapWorker, id));
}
const snapshot_ = getSnapshot(getState(), id);
assert(snapshot_.dominatorTree && snapshot_.dominatorTree.root,
"Should have a dominator tree with a root.");
const dominatorTreeId = snapshot_.dominatorTree.dominatorTreeId;
const indices = isSet(reportLeafIndex)
? reportLeafIndex
: new Set([reportLeafIndex]);
let labelDisplay;
let nodes;
do {
labelDisplay = getState().labelDisplay;
assert(labelDisplay && labelDisplay.breakdown && labelDisplay.breakdown.by,
`Should have a breakdown to label nodes with, got: ${uneval(labelDisplay)}`);
if (getState().view.state !== viewState.INDIVIDUALS) {
// We switched views while in the process of fetching individuals -- any
// further work is useless.
return;
}
dispatch({ type: actions.FETCH_INDIVIDUALS_START });
try {
({ nodes } = yield heapWorker.getCensusIndividuals({
dominatorTreeId,
indices,
censusBreakdown,
labelBreakdown: labelDisplay.breakdown,
maxRetainingPaths: Preferences.get("devtools.memory.max-retaining-paths"),
maxIndividuals: Preferences.get("devtools.memory.max-individuals"),
}));
} catch (error) {
reportException("actions/snapshot/fetchIndividuals", error);
dispatch({ type: actions.INDIVIDUALS_ERROR, error });
return;
}
}
while (labelDisplay !== getState().labelDisplay);
dispatch({
type: actions.FETCH_INDIVIDUALS_END,
id,
censusBreakdown,
indices,
labelDisplay,
nodes,
dominatorTree: snapshot_.dominatorTree,
});
};
};
/**
* Refresh the current individuals view.
*
* @param {HeapAnalysesClient} heapWorker
*/
const refreshIndividuals = exports.refreshIndividuals = function(heapWorker) {
return function*(dispatch, getState) {
assert(getState().view.state === viewState.INDIVIDUALS,
"Should be in INDIVIDUALS view.");
const { individuals } = getState();
switch (individuals.state) {
case individualsState.COMPUTING_DOMINATOR_TREE:
case individualsState.FETCHING:
// Nothing to do here.
return;
case individualsState.FETCHED:
if (getState().individuals.labelDisplay === getState().labelDisplay) {
return;
}
break;
case individualsState.ERROR:
// Doesn't hurt to retry: maybe we won't get an error this time around?
break;
default:
assert(false, `Unexpected individuals state: ${individuals.state}`);
return;
}
yield dispatch(fetchIndividuals(heapWorker,
individuals.id,
individuals.censusBreakdown,
individuals.indices));
};
};
/**
* Refresh the selected snapshot's census data, if need be (for example,
* display configuration changed).
@@ -305,9 +465,11 @@ const getCurrentCensusTaker = exports.getCurrentCensusTaker = function (currentV
* @param {HeapAnalysesClient} heapWorker
*/
const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) {
console.log("FITZGEN: refreshSelectedCensus");
return function*(dispatch, getState) {
let snapshot = getState().snapshots.find(s => s.selected);
if (!snapshot || snapshot.state !== states.READ) {
console.log("FITZGEN: nothing to do");
return;
}
@@ -319,6 +481,7 @@ const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWork
// task action will follow through and ensure that a census is taken.
if ((snapshot.census && snapshot.census.state === censusState.SAVED) ||
!snapshot.census) {
console.log("FITZGEN: taking census");
yield dispatch(takeCensus(heapWorker, snapshot.id));
}
};
@@ -357,8 +520,13 @@ const refreshSelectedTreeMap = exports.refreshSelectedTreeMap = function (heapWo
*
* @returns {Promise<DominatorTreeId>}
*/
const computeDominatorTree = exports.computeDominatorTree = function (heapWorker, id) {
return function*(dispatch, getState) {
const computeDominatorTree = exports.computeDominatorTree =
TaskCache.declareCacheableTask({
getCacheKey(_, id) {
return id;
},
task: function*(heapWorker, id, removeFromCache, dispatch, getState) {
const snapshot = getSnapshot(getState(), id);
assert(!(snapshot.dominatorTree && snapshot.dominatorTree.dominatorTreeId),
"Should not re-compute dominator trees");
@@ -369,15 +537,17 @@ const computeDominatorTree = exports.computeDominatorTree = function (heapWorker
try {
dominatorTreeId = yield heapWorker.computeDominatorTree(snapshot.path);
} catch (error) {
removeFromCache();
reportException("actions/snapshot/computeDominatorTree", error);
dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
return null;
}
removeFromCache();
dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_END, id, dominatorTreeId });
return dominatorTreeId;
};
};
}
});
/**
* Get the partial subtree, starting from the root, of the
@@ -388,8 +558,13 @@ const computeDominatorTree = exports.computeDominatorTree = function (heapWorker
*
* @returns {Promise<DominatorTreeNode>}
*/
const fetchDominatorTree = exports.fetchDominatorTree = function (heapWorker, id) {
return function*(dispatch, getState) {
const fetchDominatorTree = exports.fetchDominatorTree =
TaskCache.declareCacheableTask({
getCacheKey(_, id) {
return id;
},
task: function*(heapWorker, id, removeFromCache, dispatch, getState) {
const snapshot = getSnapshot(getState(), id);
assert(dominatorTreeIsComputed(snapshot),
"Should have dominator tree model and it should be computed");
@@ -397,7 +572,7 @@ const fetchDominatorTree = exports.fetchDominatorTree = function (heapWorker, id
let display;
let root;
do {
display = getState().dominatorTreeDisplay;
display = getState().labelDisplay;
assert(display && display.breakdown,
`Should have a breakdown to describe nodes with, got: ${uneval(display)}`);
@@ -407,20 +582,23 @@ const fetchDominatorTree = exports.fetchDominatorTree = function (heapWorker, id
root = yield heapWorker.getDominatorTree({
dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
breakdown: display.breakdown,
maxRetainingPaths: Preferences.get("devtools.memory.max-retaining-paths"),
});
} catch (error) {
removeFromCache();
reportException("actions/snapshot/fetchDominatorTree", error);
dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
return null;
}
}
while (display !== getState().dominatorTreeDisplay);
while (display !== getState().labelDisplay);
removeFromCache();
dispatch({ type: actions.FETCH_DOMINATOR_TREE_END, id, root });
telemetry.countDominatorTree({ display });
return root;
};
};
}
});
/**
* Fetch the immediately dominated children represented by the placeholder
@@ -430,8 +608,13 @@ const fetchDominatorTree = exports.fetchDominatorTree = function (heapWorker, id
* @param {SnapshotId} id
* @param {DominatorTreeLazyChildren} lazyChildren
*/
const fetchImmediatelyDominated = exports.fetchImmediatelyDominated = function (heapWorker, id, lazyChildren) {
return function*(dispatch, getState) {
const fetchImmediatelyDominated = exports.fetchImmediatelyDominated =
TaskCache.declareCacheableTask({
getCacheKey(_, id, lazyChildren) {
return `${id}-${lazyChildren.key()}`;
},
task: function*(heapWorker, id, lazyChildren, removeFromCache, dispatch, getState) {
const snapshot = getSnapshot(getState(), id);
assert(snapshot.dominatorTree, "Should have dominator tree model");
assert(snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
@@ -442,7 +625,7 @@ const fetchImmediatelyDominated = exports.fetchImmediatelyDominated = function (
let display;
let response;
do {
display = getState().dominatorTreeDisplay;
display = getState().labelDisplay;
assert(display, "Should have a display to describe nodes with.");
dispatch({ type: actions.FETCH_IMMEDIATELY_DOMINATED_START, id });
@@ -453,15 +636,18 @@ const fetchImmediatelyDominated = exports.fetchImmediatelyDominated = function (
breakdown: display.breakdown,
nodeId: lazyChildren.parentNodeId(),
startIndex: lazyChildren.siblingIndex(),
maxRetainingPaths: Preferences.get("devtools.memory.max-retaining-paths"),
});
} catch (error) {
removeFromCache();
reportException("actions/snapshot/fetchImmediatelyDominated", error);
dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
return null;
}
}
while (display !== getState().dominatorTreeDisplay);
while (display !== getState().labelDisplay);
removeFromCache();
dispatch({
type: actions.FETCH_IMMEDIATELY_DOMINATED_END,
id,
@@ -469,8 +655,8 @@ const fetchImmediatelyDominated = exports.fetchImmediatelyDominated = function (
nodes: response.nodes,
moreChildrenAvailable: response.moreChildrenAvailable,
});
};
};
}
});
/**
* Compute and then fetch the dominator tree of the snapshot with the given
@@ -481,21 +667,29 @@ const fetchImmediatelyDominated = exports.fetchImmediatelyDominated = function (
*
* @returns {Promise<DominatorTreeNode>}
*/
const computeAndFetchDominatorTree = exports.computeAndFetchDominatorTree = function (heapWorker, id) {
return function*(dispatch, getState) {
const computeAndFetchDominatorTree = exports.computeAndFetchDominatorTree =
TaskCache.declareCacheableTask({
getCacheKey(_, id) {
return id;
},
task: function*(heapWorker, id, removeFromCache, dispatch, getState) {
const dominatorTreeId = yield dispatch(computeDominatorTree(heapWorker, id));
if (dominatorTreeId === null) {
removeFromCache();
return null;
}
const root = yield dispatch(fetchDominatorTree(heapWorker, id));
removeFromCache();
if (!root) {
return null;
}
return root;
};
};
}
});
/**
* Update the currently selected snapshot's dominator tree.
@@ -556,7 +750,7 @@ const clearSnapshots = exports.clearSnapshots = function (heapWorker) {
let censusReady = (s.treeMap && s.treeMap.state === treeMapState.SAVED) ||
(s.census && s.census.state === censusState.SAVED);
return snapshotReady && censusReady
return snapshotReady && censusReady;
});
let ids = snapshots.map(s => s.id);
@@ -566,6 +760,9 @@ const clearSnapshots = exports.clearSnapshots = function (heapWorker) {
if (getState().diffing) {
dispatch(diffing.toggleDiffing());
}
if (getState().individuals) {
dispatch(view.popView());
}
yield Promise.all(snapshots.map(snapshot => {
return heapWorker.deleteHeapSnapshot(snapshot.path).catch(error => {

View File

@@ -0,0 +1,103 @@
/* 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");
/**
* The `TaskCache` allows for re-using active tasks when spawning a second task
* would simply duplicate work and is unnecessary. It maps from a task's unique
* key to the promise of its result.
*/
const TaskCache = module.exports = class TaskCache {
constructor() {
this._cache = new Map();
}
/**
* Get the promise keyed by the given unique `key`, if one exists.
*
* @param {Any} key
* @returns {Promise<Any> | undefined}
*/
get(key) {
return this._cache.get(key);
}
/**
* Put the task result promise in the cache and associate it with the given
* `key` which must not already have an entry in the cache.
*
* @param {Any} key
* @param {Promise<Any>} promise
*/
put(key, promise) {
assert(!this._cache.has(key),
"We should not override extant entries");
this._cache.set(key, promise);
}
/**
* Remove the cache entry with the given key.
*
* @param {Any} key
*/
remove(key) {
console.log("FITZGEN: removing task from cache with keky =", key);
assert(this._cache.has(key),
`Should have an extant entry for key = ${key}`);
this._cache.delete(key);
}
};
/**
* Create a new action-orchestrating task that is automatically cached. The
* tasks themselves are responsible from removing themselves from the cache.
*
* @param {Function(...args) -> Any} getCacheKey
* @param {Generator(...args) -> Any} task
*
* @returns Cacheable, Action-Creating Task
*/
TaskCache.declareCacheableTask = function({ getCacheKey, task }) {
const cache = new TaskCache();
return function(...args) {
return function*(dispatch, getState) {
const key = getCacheKey(...args);
const extantResult = cache.get(key);
if (extantResult) {
console.log("FITZGEN: re-using task with cache key =", key);
return extantResult;
}
console.log("FITZGEN: creating new task with cache key =", key);
// Ensure that we have our new entry in the cache *before* dispatching the
// task!
let resolve;
cache.put(key, new Promise(r => {
resolve = r;
}));
resolve(dispatch(function*() {
try {
args.push(() => cache.remove(key), dispatch, getState);
return yield* task(...args);
} catch (error) {
// Don't perma-cache errors.
if (cache.get(key)) {
cache.remove(key);
}
throw error;
}
}));
return yield cache.get(key);
};
};
};

View File

@@ -3,7 +3,9 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { assert } = require("devtools/shared/DevToolsUtils");
const { actions } = require("../constants");
const { findSelectedSnapshot } = require("../utils");
const refresh = require("./refresh");
/**
@@ -12,9 +14,28 @@ const refresh = require("./refresh");
* @param {viewState} view
*/
const changeView = exports.changeView = function (view) {
return {
type: actions.CHANGE_VIEW,
view
return function(dispatch, getState) {
dispatch({
type: actions.CHANGE_VIEW,
newViewState: view,
oldDiffing: getState().diffing,
oldSelected: findSelectedSnapshot(getState()),
});
};
};
/**
* Given that we are in the INDIVIDUALS view state, go back to the state we were
* in before.
*/
const popView = exports.popView = function () {
return function(dispatch, getState) {
const { previous } = getState().view;
assert(previous);
dispatch({
type: actions.POP_VIEW,
previousView: previous,
});
};
};
@@ -23,6 +44,7 @@ const changeView = exports.changeView = function (view) {
* the heap worker.
*
* @param {viewState} view
* @param {HeapAnalysesClient} heapWorker
*/
exports.changeViewAndRefresh = function (view, heapWorker) {
return function* (dispatch, getState) {
@@ -30,3 +52,16 @@ exports.changeViewAndRefresh = function (view, heapWorker) {
yield dispatch(refresh.refresh(heapWorker));
};
};
/**
* Given that we are in the INDIVIDUALS view state, go back to the state we were
* previously in and refresh our data.
*
* @param {HeapAnalysesClient} heapWorker
*/
exports.popViewAndRefresh = function(heapWorker) {
return function* (dispatch, getState) {
dispatch(popView());
yield dispatch(refresh.refresh(heapWorker));
};
};

View File

@@ -6,15 +6,15 @@ 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, treeMapDisplays, diffingState, viewState } = require("./constants");
const { censusDisplays, labelDisplays, treeMapDisplays, diffingState, viewState } = require("./constants");
const { toggleRecordingAllocationStacks } = require("./actions/allocations");
const { setCensusDisplayAndRefresh } = require("./actions/census-display");
const { setDominatorTreeDisplayAndRefresh } = require("./actions/dominator-tree-display");
const { setLabelDisplayAndRefresh } = require("./actions/label-display");
const { setTreeMapDisplayAndRefresh } = require("./actions/tree-map-display");
const {
getCustomCensusDisplays,
getCustomDominatorTreeDisplays,
getCustomLabelDisplays,
getCustomTreeMapDisplays,
} = require("devtools/client/memory/utils");
const {
@@ -37,8 +37,10 @@ const {
expandDominatorTreeNode,
collapseDominatorTreeNode,
focusDominatorTreeNode,
fetchIndividuals,
focusIndividual,
} = require("./actions/snapshot");
const { changeViewAndRefresh } = require("./actions/view");
const { changeViewAndRefresh, popViewAndRefresh } = require("./actions/view");
const { resizeShortestPaths } = require("./actions/sizes");
const Toolbar = createFactory(require("./components/toolbar"));
const List = createFactory(require("./components/list"));
@@ -117,16 +119,16 @@ const MemoryApp = createClass({
].concat(custom);
},
_getDominatorTreeDisplays() {
const customDisplays = getCustomDominatorTreeDisplays();
_getLabelDisplays() {
const customDisplays = getCustomLabelDisplays();
const custom = Object.keys(customDisplays).reduce((arr, key) => {
arr.push(customDisplays[key]);
return arr;
}, []);
return [
dominatorTreeDisplays.coarseType,
dominatorTreeDisplays.allocationStack,
labelDisplays.coarseType,
labelDisplays.allocationStack,
].concat(custom);
},
@@ -154,6 +156,9 @@ const MemoryApp = createClass({
diffing,
view,
sizes,
censusDisplay,
labelDisplay,
individuals,
} = this.props;
const selectedSnapshot = snapshots.find(s => s.selected);
@@ -171,6 +176,7 @@ const MemoryApp = createClass({
Toolbar({
snapshots,
censusDisplays: this._getCensusDisplays(),
censusDisplay,
onCensusDisplayChange: newDisplay =>
dispatch(setCensusDisplayAndRefresh(heapWorker, newDisplay)),
onImportClick: () => dispatch(pickFileAndImportSnapshotAndCensus(heapWorker)),
@@ -185,9 +191,10 @@ const MemoryApp = createClass({
diffing,
onToggleDiffing: () => dispatch(toggleDiffing()),
view,
dominatorTreeDisplays: this._getDominatorTreeDisplays(),
onDominatorTreeDisplayChange: newDisplay =>
dispatch(setDominatorTreeDisplayAndRefresh(heapWorker, newDisplay)),
labelDisplays: this._getLabelDisplays(),
labelDisplay,
onLabelDisplayChange: newDisplay =>
dispatch(setLabelDisplayAndRefresh(heapWorker, newDisplay)),
treeMapDisplays: this._getTreeMapDisplays(),
onTreeMapDisplayChange: newDisplay =>
dispatch(setTreeMapDisplayAndRefresh(heapWorker, newDisplay)),
@@ -217,6 +224,22 @@ const MemoryApp = createClass({
dispatch(fetchImmediatelyDominated(heapWorker,
selectedSnapshot.id,
lazyChildren)),
onPopView: () => dispatch(popViewAndRefresh(heapWorker)),
individuals,
onViewIndividuals: node => {
const snapshotId = diffing
? diffing.secondSnapshotId
: selectedSnapshot.id;
dispatch(fetchIndividuals(heapWorker,
snapshotId,
censusDisplay.breakdown,
node.reportLeafIndex));
},
onFocusIndividual: node => {
assert(view.state === viewState.INDIVIDUALS,
"Should be in the individuals view");
dispatch(focusIndividual(node));
},
onCensusExpand: (census, node) => {
if (diffing) {
assert(diffing.census === census,
@@ -251,7 +274,7 @@ const MemoryApp = createClass({
}
},
onDominatorTreeExpand: node => {
assert(view === viewState.DOMINATOR_TREE,
assert(view.state === viewState.DOMINATOR_TREE,
"If expanding dominator tree nodes, should be in dominator tree view");
assert(selectedSnapshot, "...and we should have a selected snapshot");
assert(selectedSnapshot.dominatorTree,
@@ -259,7 +282,7 @@ const MemoryApp = createClass({
dispatch(expandDominatorTreeNode(selectedSnapshot.id, node));
},
onDominatorTreeCollapse: node => {
assert(view === viewState.DOMINATOR_TREE,
assert(view.state === viewState.DOMINATOR_TREE,
"If collapsing dominator tree nodes, should be in dominator tree view");
assert(selectedSnapshot, "...and we should have a selected snapshot");
assert(selectedSnapshot.dominatorTree,
@@ -267,7 +290,7 @@ const MemoryApp = createClass({
dispatch(collapseDominatorTreeNode(selectedSnapshot.id, node));
},
onDominatorTreeFocus: node => {
assert(view === viewState.DOMINATOR_TREE,
assert(view.state === viewState.DOMINATOR_TREE,
"If focusing dominator tree nodes, should be in dominator tree view");
assert(selectedSnapshot, "...and we should have a selected snapshot");
assert(selectedSnapshot.dominatorTree,

View File

@@ -4,13 +4,23 @@
const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
const { L10N } = require("../utils");
const models = require("../models");
const CensusHeader = module.exports = createClass({
displayName: "CensusHeader",
propTypes: { },
propTypes: {
diffing: models.diffingModel,
},
render() {
let individualsCell;
if (!this.props.diffing) {
individualsCell = dom.span({
className: "heap-tree-item-field heap-tree-item-individuals"
});
}
return dom.div(
{
className: "header"
@@ -48,6 +58,8 @@ const CensusHeader = module.exports = createClass({
L10N.getStr("heapview.field.totalcount")
),
individualsCell,
dom.span(
{
className: "heap-tree-item-name",

View File

@@ -1,6 +1,7 @@
/* 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 { isSavedFrame } = require("devtools/shared/DevToolsUtils");
const { DOM: dom, createClass, createFactory } = require("devtools/client/shared/vendor/react");
@@ -16,7 +17,7 @@ const CensusTreeItem = module.exports = createClass({
|| this.props.depth != nextProps.depth
|| this.props.expanded != nextProps.expanded
|| this.props.focused != nextProps.focused
|| this.props.showSign != nextProps.showSign;
|| this.props.diffing != nextProps.diffing;
},
render() {
@@ -27,22 +28,23 @@ const CensusTreeItem = module.exports = createClass({
focused,
getPercentBytes,
getPercentCount,
showSign,
diffing,
onViewSourceInDebugger,
onViewIndividuals,
inverted,
} = this.props;
const bytes = formatNumber(item.bytes, showSign);
const percentBytes = formatPercent(getPercentBytes(item.bytes), showSign);
const bytes = formatNumber(item.bytes, !!diffing);
const percentBytes = formatPercent(getPercentBytes(item.bytes), !!diffing);
const count = formatNumber(item.count, showSign);
const percentCount = formatPercent(getPercentCount(item.count), showSign);
const count = formatNumber(item.count, !!diffing);
const percentCount = formatPercent(getPercentCount(item.count), !!diffing);
const totalBytes = formatNumber(item.totalBytes, showSign);
const percentTotalBytes = formatPercent(getPercentBytes(item.totalBytes), showSign);
const totalBytes = formatNumber(item.totalBytes, !!diffing);
const percentTotalBytes = formatPercent(getPercentBytes(item.totalBytes), !!diffing);
const totalCount = formatNumber(item.totalCount, showSign);
const percentTotalCount = formatPercent(getPercentCount(item.totalCount), showSign);
const totalCount = formatNumber(item.totalCount, !!diffing);
const percentTotalCount = formatPercent(getPercentCount(item.totalCount), !!diffing);
let pointer;
if (inverted && depth > 0) {
@@ -51,7 +53,35 @@ const CensusTreeItem = module.exports = createClass({
pointer = dom.span({ className: "children-pointer" }, "↘");
}
return dom.div({ className: `heap-tree-item ${focused ? "focused" : ""}` },
let individualsCell;
if (!diffing) {
let individualsButton;
if (item.reportLeafIndex !== undefined) {
individualsButton = dom.button(
{
key: `individuals-button-${item.id}`,
title: L10N.getStr("tree-item.view-individuals.tooltip"),
className: "devtools-button individuals-button",
onClick: e => {
// Don't let the event bubble up to cause this item to focus after
// we have switched views, which would lead to assertion failures.
e.preventDefault();
e.stopPropagation();
onViewIndividuals(item);
},
},
"⁂"
);
}
individualsCell = dom.span(
{ className: "heap-tree-item-field heap-tree-item-individuals" },
individualsButton
);
}
return dom.div(
{ className: `heap-tree-item ${focused ? "focused" : ""}` },
dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" },
dom.span({ className: "heap-tree-number" }, bytes),
dom.span({ className: "heap-tree-percent" }, percentBytes)),
@@ -64,8 +94,12 @@ const CensusTreeItem = module.exports = createClass({
dom.span({ className: "heap-tree-item-field heap-tree-item-total-count" },
dom.span({ className: "heap-tree-number" }, totalCount),
dom.span({ className: "heap-tree-percent" }, percentTotalCount)),
dom.span({ className: "heap-tree-item-field heap-tree-item-name",
style: { marginLeft: depth * TREE_ROW_HEIGHT }},
individualsCell,
dom.span(
{
className: "heap-tree-item-field heap-tree-item-name",
style: { marginLeft: depth * TREE_ROW_HEIGHT }
},
arrow,
pointer,
this.toLabel(item.name, onViewSourceInDebugger)

View File

@@ -18,6 +18,7 @@ const Census = module.exports = createClass({
onCollapse: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onViewSourceInDebugger: PropTypes.func.isRequired,
onViewIndividuals: PropTypes.func.isRequired,
diffing: diffingModel,
},
@@ -29,6 +30,7 @@ const Census = module.exports = createClass({
onFocus,
diffing,
onViewSourceInDebugger,
onViewIndividuals,
} = this.props;
const report = census.report;
@@ -65,8 +67,9 @@ const Census = module.exports = createClass({
expanded,
getPercentBytes,
getPercentCount,
showSign: !!diffing,
diffing,
inverted: census.display.inverted,
onViewIndividuals,
}),
getRoots: () => report.children || [],
getKey: node => node.id,

View File

@@ -22,7 +22,7 @@ const DominatorTreeItem = module.exports = createClass({
propTypes: {
item: PropTypes.object.isRequired,
depth: PropTypes.number.isRequired,
arrow: PropTypes.object.isRequired,
arrow: PropTypes.object,
focused: PropTypes.bool.isRequired,
getPercentSize: PropTypes.func.isRequired,
onViewSourceInDebugger: PropTypes.func.isRequired,

View File

@@ -10,6 +10,8 @@ const DominatorTree = createFactory(require("./dominator-tree"));
const DominatorTreeHeader = createFactory(require("./dominator-tree-header"));
const TreeMap = createFactory(require("./tree-map"));
const HSplitBox = createFactory(require("devtools/client/shared/components/h-split-box"));
const Individuals = createFactory(require("./individuals"));
const IndividualsHeader = createFactory(require("./individuals-header"));
const ShortestPaths = createFactory(require("./shortest-paths"));
const { getStatusTextFull, L10N } = require("../utils");
const {
@@ -18,23 +20,26 @@ const {
viewState,
censusState,
treeMapState,
dominatorTreeState
dominatorTreeState,
individualsState,
} = require("../constants");
const { snapshot: snapshotModel, diffingModel } = require("../models");
const models = require("../models");
const { snapshot: snapshotModel, diffingModel } = models;
/**
* Get the app state's current state atom.
*
* @see the relevant state string constants in `../constants.js`.
*
* @param {viewState} view
* @param {models.view} view
* @param {snapshotModel} snapshot
* @param {diffingModel} diffing
* @param {individualsModel} individuals
*
* @return {snapshotState|diffingState|dominatorTreeState}
*/
function getState(view, snapshot, diffing) {
switch (view) {
function getState(view, snapshot, diffing, individuals) {
switch (view.state) {
case viewState.CENSUS:
return snapshot.census
? snapshot.census.state
@@ -52,9 +57,12 @@ function getState(view, snapshot, diffing) {
return snapshot.dominatorTree
? snapshot.dominatorTree.state
: snapshot.state;
case viewState.INDIVIDUALS:
return individuals.state;
}
assert(false, `Unexpected view state: ${view}`);
assert(false, `Unexpected view state: ${view.state}`);
return null;
}
@@ -63,7 +71,7 @@ function getState(view, snapshot, diffing) {
* state. Return false otherwise.
*
* @param {snapshotState|diffingState|dominatorTreeState} state
* @param {viewState} view
* @param {models.view} view
* @param {snapshotModel} snapshot
*
* @returns {Boolean}
@@ -81,9 +89,11 @@ function shouldDisplayStatus(state, view, snapshot) {
case dominatorTreeState.COMPUTING:
case dominatorTreeState.COMPUTED:
case dominatorTreeState.FETCHING:
case individualsState.COMPUTING_DOMINATOR_TREE:
case individualsState.FETCHING:
return true;
}
return view === viewState.DOMINATOR_TREE && !snapshot.dominatorTree;
return view.state === viewState.DOMINATOR_TREE && !snapshot.dominatorTree;
}
/**
@@ -121,10 +131,11 @@ function shouldDisplayThrobber(diffing) {
*
* @param {snapshotModel} snapshot
* @param {diffingModel} diffing
* @param {individualsModel} individuals
*
* @returns {Error|null}
*/
function getError(snapshot, diffing) {
function getError(snapshot, diffing, individuals) {
if (diffing) {
if (diffing.state === diffingState.ERROR) {
return diffing.error;
@@ -153,6 +164,10 @@ function getError(snapshot, diffing) {
}
}
if (individuals && individuals.state === individualsState.ERROR) {
return individuals.error;
}
return null;
}
@@ -178,8 +193,12 @@ const Heap = module.exports = createClass({
onShortestPathsResize: PropTypes.func.isRequired,
snapshot: snapshotModel,
onViewSourceInDebugger: PropTypes.func.isRequired,
onPopView: PropTypes.func.isRequired,
individuals: models.individuals,
onViewIndividuals: PropTypes.func.isRequired,
onFocusIndividual: PropTypes.func.isRequired,
diffing: diffingModel,
view: PropTypes.string.isRequired,
view: models.view.isRequired,
sizes: PropTypes.object.isRequired,
},
@@ -190,40 +209,52 @@ const Heap = module.exports = createClass({
onSnapshotClick,
onLoadMoreSiblings,
onViewSourceInDebugger,
onViewIndividuals,
individuals,
view,
} = this.props;
if (!diffing && !snapshot) {
if (!diffing && !snapshot && !individuals) {
return this._renderInitial(onSnapshotClick);
}
const state = getState(view, snapshot, diffing);
const state = getState(view, snapshot, diffing, individuals);
const statusText = getStateStatusText(state, diffing);
if (shouldDisplayStatus(state, view, snapshot)) {
return this._renderStatus(state, statusText, diffing);
}
const error = getError(snapshot, diffing);
const error = getError(snapshot, diffing, individuals);
if (error) {
return this._renderError(state, statusText, error);
}
if (view === viewState.CENSUS || view === viewState.DIFFING) {
const census = view === viewState.CENSUS
if (view.state === viewState.CENSUS || view.state === viewState.DIFFING) {
const census = view.state === viewState.CENSUS
? snapshot.census
: diffing.census;
if (!census) {
return this._renderStatus(state, statusText, diffing);
}
return this._renderCensus(state, census, diffing, onViewSourceInDebugger);
return this._renderCensus(state, census, diffing, onViewSourceInDebugger,
onViewIndividuals);
}
if (view === viewState.TREE_MAP) {
if (view.state === viewState.TREE_MAP) {
return this._renderTreeMap(state, snapshot.treeMap);
}
assert(view === viewState.DOMINATOR_TREE,
if (view.state === viewState.INDIVIDUALS) {
assert(individuals.state === individualsState.FETCHED,
"Should have fetched the individuals -- other states are rendered as statuses");
return this._renderIndividuals(state, individuals,
individuals.dominatorTree,
onViewSourceInDebugger);
}
assert(view.state === viewState.DOMINATOR_TREE,
"If we aren't in progress, looking at a census, or diffing, then we " +
"must be looking at a dominator tree");
assert(!diffing, "Should not have diffing");
@@ -290,7 +321,7 @@ const Heap = module.exports = createClass({
);
},
_renderCensus(state, census, diffing, onViewSourceInDebugger) {
_renderCensus(state, census, diffing, onViewSourceInDebugger, onViewIndividuals) {
assert(census.report, "Should not render census that does not have a report");
if (!census.report.children) {
@@ -310,9 +341,10 @@ const Heap = module.exports = createClass({
L10N.getStr("heapview.noAllocationStacks")));
}
contents.push(CensusHeader());
contents.push(CensusHeader({ diffing }));
contents.push(Census({
onViewSourceInDebugger,
onViewIndividuals,
diffing,
census,
onExpand: node => this.props.onCensusExpand(census, node),
@@ -330,6 +362,61 @@ const Heap = module.exports = createClass({
);
},
_renderIndividuals(state, individuals, dominatorTree, onViewSourceInDebugger) {
assert(individuals.state === individualsState.FETCHED,
"Should have fetched individuals");
assert(dominatorTree && dominatorTree.root,
"Should have a dominator tree and its root");
const tree = dom.div(
{
className: "vbox",
style: {
overflowY: "auto"
}
},
IndividualsHeader(),
Individuals({
individuals,
dominatorTree,
onViewSourceInDebugger,
onFocus: this.props.onFocusIndividual
})
);
const shortestPaths = ShortestPaths({
graph: individuals.focused
? individuals.focused.shortestPaths
: null
});
return this._renderHeapView(
state,
dom.div(
{ className: "hbox devtools-toolbar" },
dom.label(
{ id: "pop-view-button-label" },
dom.button(
{
id: "pop-view-button",
className: "devtools-button",
onClick: this.props.onPopView,
},
L10N.getStr("toolbar.pop-view")
),
L10N.getStr("toolbar.pop-view.label")
),
L10N.getStr("toolbar.viewing-individuals")
),
HSplitBox({
start: tree,
end: shortestPaths,
startWidth: this.props.sizes.shortestPathsSize,
onResize: this.props.onShortestPathsResize,
})
);
},
_renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
const tree = dom.div(
{

View File

@@ -0,0 +1,44 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
const { L10N } = require("../utils");
const IndividualsHeader = module.exports = createClass({
displayName: "IndividualsHeader",
propTypes: { },
render() {
return dom.div(
{
className: "header"
},
dom.span(
{
className: "heap-tree-item-bytes",
title: L10N.getStr("heapview.field.retainedSize.tooltip"),
},
L10N.getStr("heapview.field.retainedSize")
),
dom.span(
{
className: "heap-tree-item-bytes",
title: L10N.getStr("heapview.field.shallowSize.tooltip"),
},
L10N.getStr("heapview.field.shallowSize")
),
dom.span(
{
className: "heap-tree-item-name",
title: L10N.getStr("individuals.field.node.tooltip"),
},
L10N.getStr("individuals.field.node")
)
);
}
});

View File

@@ -0,0 +1,61 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
const { assert } = require("devtools/shared/DevToolsUtils");
const { createParentMap } = require("devtools/shared/heapsnapshot/CensusUtils");
const Tree = createFactory(require("devtools/client/shared/components/tree"));
const DominatorTreeItem = createFactory(require("./dominator-tree-item"));
const { L10N } = require("../utils");
const { TREE_ROW_HEIGHT } = require("../constants");
const models = require("../models");
/**
* The list of individuals in a census group.
*/
const Individuals = module.exports = createClass({
displayName: "Individuals",
propTypes: {
onViewSourceInDebugger: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
individuals: models.individuals,
dominatorTree: models.dominatorTreeModel,
},
render() {
const {
individuals,
dominatorTree,
onViewSourceInDebugger,
onFocus,
} = this.props;
return Tree({
key: "individuals-tree",
autoExpandDepth: 0,
focused: individuals.focused,
getParent: node => null,
getChildren: node => [],
isExpanded: node => false,
onExpand: () => {},
onCollapse: () => {},
onFocus,
renderItem: (item, depth, focused, _, expanded) => {
return DominatorTreeItem({
item,
depth,
focused,
arrow: undefined,
expanded,
getPercentSize: size => (size / dominatorTree.root.retainedSize) * 100,
onViewSourceInDebugger,
});
},
getRoots: () => individuals.nodes,
getKey: node => node.nodeId,
itemHeight: TREE_ROW_HEIGHT,
});
}
});

View File

@@ -15,6 +15,8 @@ DevToolsModules(
'dominator-tree-item.js',
'dominator-tree.js',
'heap.js',
'individuals-header.js',
'individuals.js',
'list.js',
'shortest-paths.js',
'snapshot-list-item.js',

View File

@@ -11,10 +11,14 @@ const { viewState } = require("../constants");
module.exports = createClass({
displayName: "Toolbar",
propTypes: {
censusDisplays: PropTypes.arrayOf(PropTypes.shape({
displayName: PropTypes.string.isRequired,
})).isRequired,
censusDisplay: PropTypes.shape({
displayName: PropTypes.string.isRequired,
}).isRequired,
onTakeSnapshotClick: PropTypes.func.isRequired,
onImportClick: PropTypes.func.isRequired,
onClearSnapshotsClick: PropTypes.func.isRequired,
@@ -25,12 +29,15 @@ module.exports = createClass({
setFilterString: PropTypes.func.isRequired,
diffing: models.diffingModel,
onToggleDiffing: PropTypes.func.isRequired,
view: PropTypes.string.isRequired,
view: models.view.isRequired,
onViewChange: PropTypes.func.isRequired,
dominatorTreeDisplays: PropTypes.arrayOf(PropTypes.shape({
labelDisplays: PropTypes.arrayOf(PropTypes.shape({
displayName: PropTypes.string.isRequired,
})).isRequired,
onDominatorTreeDisplayChange: PropTypes.func.isRequired,
labelDisplay: PropTypes.shape({
displayName: PropTypes.string.isRequired,
}).isRequired,
onLabelDisplayChange: PropTypes.func.isRequired,
treeMapDisplays: PropTypes.arrayOf(PropTypes.shape({
displayName: PropTypes.string.isRequired,
})).isRequired,
@@ -45,8 +52,10 @@ module.exports = createClass({
onClearSnapshotsClick,
onCensusDisplayChange,
censusDisplays,
dominatorTreeDisplays,
onDominatorTreeDisplayChange,
censusDisplay,
labelDisplays,
labelDisplay,
onLabelDisplayChange,
treeMapDisplays,
onTreeMapDisplayChange,
onToggleRecordAllocationStacks,
@@ -61,7 +70,7 @@ module.exports = createClass({
} = this.props;
let viewToolbarOptions;
if (view == viewState.CENSUS || view === viewState.DIFFING) {
if (view.state == viewState.CENSUS || view.state === viewState.DIFFING) {
viewToolbarOptions = dom.div(
{
className: "toolbar-group"
@@ -82,6 +91,7 @@ module.exports = createClass({
censusDisplays.find(b => b.displayName === e.target.value);
onCensusDisplayChange(newDisplay);
},
value: censusDisplay.displayName,
},
censusDisplays.map(({ tooltip, displayName }) => dom.option(
{
@@ -106,7 +116,7 @@ module.exports = createClass({
value: filterString || undefined,
})
);
} else if (view == viewState.TREE_MAP) {
} else if (view.state == viewState.TREE_MAP) {
assert(treeMapDisplays.length >= 1,
"Should always have at least one tree map display");
@@ -145,7 +155,8 @@ module.exports = createClass({
)
: null;
} else {
assert(view === viewState.DOMINATOR_TREE);
assert(view.state === viewState.DOMINATOR_TREE ||
view.state === viewState.INDIVIDUALS);
viewToolbarOptions = dom.div(
{
@@ -160,16 +171,17 @@ module.exports = createClass({
L10N.getStr("toolbar.labelBy"),
dom.select(
{
id: "select-dominator-tree-display",
id: "select-label-display",
onChange: e => {
const newDisplay =
dominatorTreeDisplays.find(b => b.displayName === e.target.value);
onDominatorTreeDisplayChange(newDisplay);
labelDisplays.find(b => b.displayName === e.target.value);
onLabelDisplayChange(newDisplay);
},
value: labelDisplay.displayName,
},
dominatorTreeDisplays.map(({ tooltip, displayName }) => dom.option(
labelDisplays.map(({ tooltip, displayName }) => dom.option(
{
key: `dominator-tree-display-${displayName}`,
key: `label-display-${displayName}`,
value: displayName,
title: tooltip,
},
@@ -181,7 +193,7 @@ module.exports = createClass({
}
let viewSelect;
if (view !== viewState.DIFFING) {
if (view.state !== viewState.DIFFING && view.state !== viewState.INDIVIDUALS) {
viewSelect = dom.label(
{
title: L10N.getStr("toolbar.view.tooltip"),
@@ -192,12 +204,12 @@ module.exports = createClass({
id: "select-view",
onChange: e => onViewChange(e.target.value),
defaultValue: view,
value: view.state,
},
dom.option(
{
value: viewState.TREE_MAP,
title: L10N.getStr("toolbar.view.treemap.tooltip"),
selected: view
},
L10N.getStr("toolbar.view.treemap")
),
@@ -268,6 +280,7 @@ module.exports = createClass({
dom.label(
{
id: "record-allocation-stacks-label",
title: L10N.getStr("checkbox.recordAllocationStacks.tooltip"),
},
dom.input({

View File

@@ -12,7 +12,7 @@ exports.ALLOCATION_RECORDING_OPTIONS = {
// If TREE_ROW_HEIGHT changes, be sure to change `var(--heap-tree-row-height)`
// in `devtools/client/themes/memory.css`
exports.TREE_ROW_HEIGHT = 14;
exports.TREE_ROW_HEIGHT = 18;
/*** Actions ******************************************************************/
@@ -75,13 +75,14 @@ actions.DIFFING_ERROR = "diffing-error";
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";
actions.SET_LABEL_DISPLAY = "set-label-display";
// Fired to set a tree map display
actions.SET_TREE_MAP_DISPLAY = "set-tree-map-display";
// Fired when changing between census or dominators view.
actions.CHANGE_VIEW = "change-view";
actions.POP_VIEW = "pop-view";
// Fired when there is an error processing a snapshot or taking a census.
actions.SNAPSHOT_ERROR = "snapshot-error";
@@ -100,6 +101,11 @@ actions.FOCUS_CENSUS_NODE = "focus-census-node";
actions.FOCUS_DIFFING_CENSUS_NODE = "focus-diffing-census-node";
actions.FOCUS_DOMINATOR_TREE_NODE = "focus-dominator-tree-node";
actions.FOCUS_INDIVIDUAL = "focus-individual";
actions.FETCH_INDIVIDUALS_START = "fetch-individuals-start";
actions.FETCH_INDIVIDUALS_END = "fetch-individuals-end";
actions.INDIVIDUALS_ERROR = "individuals-error";
actions.COMPUTE_DOMINATOR_TREE_START = "compute-dominator-tree-start";
actions.COMPUTE_DOMINATOR_TREE_END = "compute-dominator-tree-end";
actions.FETCH_DOMINATOR_TREE_START = "fetch-dominator-tree-start";
@@ -179,7 +185,7 @@ const DOMINATOR_TREE_LABEL_COARSE_TYPE = Object.freeze({
other: INTERNAL_TYPE,
});
exports.dominatorTreeDisplays = Object.freeze({
exports.labelDisplays = Object.freeze({
coarseType: Object.freeze({
displayName: "Type",
get tooltip() {
@@ -225,6 +231,7 @@ viewState.CENSUS = "view-state-census";
viewState.DIFFING = "view-state-diffing";
viewState.DOMINATOR_TREE = "view-state-dominator-tree";
viewState.TREE_MAP = "view-state-tree-map";
viewState.INDIVIDUALS = "view-state-individuals";
/*** Snapshot States **********************************************************/
@@ -318,3 +325,18 @@ dominatorTreeState.FETCHING = "dominator-tree-state-fetching";
dominatorTreeState.LOADED = "dominator-tree-state-loaded";
dominatorTreeState.INCREMENTAL_FETCHING = "dominator-tree-state-incremental-fetching";
dominatorTreeState.ERROR = "dominator-tree-state-error";
/*** States for Individuals Model *********************************************/
/*
* Various states the individuals model can be in.
*
* COMPUTING_DOMINATOR_TREE -> FETCHING -> FETCHED
*
* Any state may lead to the ERROR state, from which it can never leave.
*/
const individualsState = exports.individualsState = Object.create(null);
individualsState.COMPUTING_DOMINATOR_TREE = "individuals-state-computing-dominator-tree";
individualsState.FETCHING = "individuals-state-fetching";
individualsState.FETCHED = "individuals-state-fetched";
individualsState.ERROR = "individuals-state-error";

View File

@@ -10,7 +10,8 @@ const {
snapshotState: states,
diffingState,
dominatorTreeState,
viewState
viewState,
individualsState,
} = require("./constants");
/**
@@ -65,7 +66,7 @@ const censusDisplayModel = exports.censusDisplay = PropTypes.shape({
*
* @see `js/src/doc/Debugger/Debugger.Memory.md`
*/
const dominatorTreeDisplayModel = exports.dominatorTreeDisplay = PropTypes.shape({
const labelDisplayModel = exports.labelDisplay = PropTypes.shape({
displayName: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
breakdown: PropTypes.shape({
@@ -190,7 +191,7 @@ let dominatorTreeModel = exports.dominatorTreeModel = PropTypes.shape({
// The display used to generate descriptive labels of nodes in this dominator
// tree.
display: dominatorTreeDisplayModel,
display: labelDisplayModel,
// The number of active requests to incrementally fetch subtrees. This should
// only be non-zero when the state is INCREMENTAL_FETCHING.
@@ -346,6 +347,102 @@ let diffingModel = exports.diffingModel = PropTypes.shape({
}),
});
let previousViewModel = exports.previousView = PropTypes.shape({
state: catchAndIgnore(function (previous) {
switch (previous.state) {
case viewState.DIFFING:
assert(previous.diffing, "Should have previous diffing state.");
assert(!previous.selected, "Should not have a previously selected snapshot.")
break;
case viewState.CENSUS:
case viewState.DOMINATOR_TREE:
case viewState.TREE_MAP:
assert(previous.selected, "Should have a previously selected snapshot.");
break;
case viewState.INDIVIDUALS:
default:
assert(false, `Unexpected previous view state: ${previous.state}.`);
}
}),
// The previous diffing state, if any.
diffing: diffingModel,
// The previously selected snapshot, if any.
selected: snapshotId,
});
let viewModel = exports.view = PropTypes.shape({
// The current view state.
state: catchAndIgnore(function (view) {
switch (view.state) {
case viewState.DIFFING:
case viewState.CENSUS:
case viewState.DOMINATOR_TREE:
case viewState.INDIVIDUALS:
case viewState.TREE_MAP:
break;
default:
assert(false, `Unexpected type of view: ${view.state}`);
}
}),
// The previous view state.
previous: previousViewModel,
});
const individualsModel = exports.individuals = PropTypes.shape({
error: PropTypes.object,
nodes: PropTypes.arrayOf(PropTypes.object),
dominatorTree: dominatorTreeModel,
id: snapshotId,
censusBreakdown: PropTypes.object,
indices: PropTypes.object,
labelDisplay: labelDisplayModel,
focused: PropTypes.object,
state: catchAndIgnore(function(individuals) {
switch (individuals.state) {
case individualsState.COMPUTING_DOMINATOR_TREE:
case individualsState.FETCHING:
assert(!individuals.nodes, "Should not have individual nodes");
assert(!individuals.dominatorTree, "Should not have dominator tree");
assert(!individuals.id, "Should not have an id");
assert(!individuals.censusBreakdown, "Should not have a censusBreakdown");
assert(!individuals.indices, "Should not have indices");
assert(!individuals.labelDisplay, "Should not have a labelDisplay");
break;
case individualsState.FETCHED:
assert(individuals.nodes, "Should have individual nodes");
assert(individuals.dominatorTree, "Should have dominator tree");
assert(individuals.id, "Should have an id");
assert(individuals.censusBreakdown, "Should have a censusBreakdown");
assert(individuals.indices, "Should have indices");
assert(individuals.labelDisplay, "Should have a labelDisplay");
break;
case individualsState.ERROR:
assert(individuals.error, "Should have an error object");
break;
default:
assert(false, `Unexpected individuals state: ${individuals.state}`);
break;
}
}),
});
let appModel = exports.app = {
// {MemoryFront} Used to communicate with platform
front: PropTypes.instanceOf(MemoryFront),
@@ -361,7 +458,7 @@ let appModel = exports.app = {
// The display data describing how we want the dominator tree labels to be
// computed.
dominatorTreeDisplay: dominatorTreeDisplayModel.isRequired,
labelDisplay: labelDisplayModel.isRequired,
// The display data describing how we want the dominator tree labels to be
// computed.
@@ -376,27 +473,47 @@ let appModel = exports.app = {
// If present, the current diffing state.
diffing: diffingModel,
// If present, the current individuals state.
individuals: individualsModel,
// The current type of view.
view: catchAndIgnore(function (app) {
switch (app.view) {
case viewState.CENSUS:
assert(!app.diffing, "Should not be diffing");
break;
view: function(app) {
viewModel.isRequired(app, "view");
case viewState.DIFFING:
assert(app.diffing, "Should be diffing");
break;
catchAndIgnore(function(app) {
switch (app.view.state) {
case viewState.DIFFING:
assert(app.diffing, "Should be diffing");
break;
case viewState.DOMINATOR_TREE:
assert(!app.diffing, "Should not be diffing");
break;
case viewState.INDIVIDUALS:
case viewState.CENSUS:
case viewState.DOMINATOR_TREE:
case viewState.TREE_MAP:
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: ${view.state}`);
}
})(app);
default:
assert(false, `Unexpected type of view: ${app.view}`);
}
}),
catchAndIgnore(function(app) {
switch (app.view.state) {
case viewState.INDIVIDUALS:
assert(app.individuals, "Should have individuals state");
break;
case viewState.DIFFING:
case viewState.CENSUS:
case viewState.DOMINATOR_TREE:
case viewState.TREE_MAP:
assert(!app.individuals, "Should not have individuals state");
break;
default:
assert(false, `Unexpected type of view: ${view.state}`);
}
})(app);
},
};

View File

@@ -6,7 +6,8 @@
exports.allocations = require("./reducers/allocations");
exports.censusDisplay = require("./reducers/census-display");
exports.diffing = require("./reducers/diffing");
exports.dominatorTreeDisplay = require("./reducers/dominator-tree-display");
exports.individuals = require("./reducers/individuals");
exports.labelDisplay = require("./reducers/label-display");
exports.treeMapDisplay = require("./reducers/tree-map-display");
exports.errors = require("./reducers/errors");
exports.filter = require("./reducers/filter");

View File

@@ -10,8 +10,17 @@ const { snapshotIsDiffable } = require("../utils");
const handlers = Object.create(null);
handlers[actions.CHANGE_VIEW] = function (diffing, { view }) {
if (view === viewState.DIFFING) {
handlers[actions.POP_VIEW] = function (diffing, { previousView }) {
if (previousView.state === viewState.DIFFING) {
assert(previousView.diffing, "Should have previousView.diffing");
return previousView.diffing;
}
return null;
};
handlers[actions.CHANGE_VIEW] = function (diffing, { newViewState }) {
if (newViewState === viewState.DIFFING) {
assert(!diffing, "Should not switch to diffing view when already diffing");
return Object.freeze({
firstSnapshotId: null,

View File

@@ -0,0 +1,73 @@
/* 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, immutableUpdate } = require("devtools/shared/DevToolsUtils");
const { actions, individualsState, viewState } = require("../constants");
const handlers = Object.create(null);
handlers[actions.POP_VIEW] = function(_state, _action) {
return null;
};
handlers[actions.CHANGE_VIEW] = function(individuals, { newViewState }) {
if (newViewState === viewState.INDIVIDUALS) {
assert(!individuals,
"Should not switch to individuals view when already in individuals view");
return Object.freeze({
state: individualsState.COMPUTING_DOMINATOR_TREE,
});
}
return null;
};
handlers[actions.FOCUS_INDIVIDUAL] = function(individuals, { node }) {
assert(individuals, "Should have individuals");
return immutableUpdate(individuals, { focused: node });
};
handlers[actions.FETCH_INDIVIDUALS_START] = function(individuals, action) {
assert(individuals, "Should have individuals");
return Object.freeze({
state: individualsState.FETCHING,
focused: individuals.focused,
});
};
handlers[actions.FETCH_INDIVIDUALS_END] = function(individuals, action) {
assert(individuals, "Should have individuals");
assert(!individuals.nodes, "Should not have nodes");
assert(individuals.state === individualsState.FETCHING,
"Should only end fetching individuals after starting.");
const focused = individuals.focused
? action.nodes.find(n => n.nodeId === individuals.focused.nodeId)
: null;
return Object.freeze({
state: individualsState.FETCHED,
nodes: action.nodes,
id: action.id,
censusBreakdown: action.censusBreakdown,
indices: action.indices,
labelDisplay: action.labelDisplay,
focused,
dominatorTree: action.dominatorTree,
});
};
handlers[actions.INDIVIDUALS_ERROR] = function(_, { error }) {
return Object.freeze({
error,
nodes: null,
state: individualsState.ERROR,
});
};
module.exports = function (individuals = null, action) {
const handler = handlers[action.type];
return handler ? handler(individuals, action) : individuals;
};

View File

@@ -4,16 +4,16 @@
"use strict";
const { actions, dominatorTreeDisplays } = require("../constants");
const DEFAULT_DOMINATOR_TREE_DISPLAY = dominatorTreeDisplays.coarseType;
const { actions, labelDisplays } = require("../constants");
const DEFAULT_LABEL_DISPLAY = labelDisplays.coarseType;
const handlers = Object.create(null);
handlers[actions.SET_DOMINATOR_TREE_DISPLAY] = function (_, { display }) {
handlers[actions.SET_LABEL_DISPLAY] = function (_, { display }) {
return display;
};
module.exports = function (state = DEFAULT_DOMINATOR_TREE_DISPLAY, action) {
module.exports = function (state = DEFAULT_LABEL_DISPLAY, action) {
const handler = handlers[action.type];
return handler ? handler(state, action) : state;
};

View File

@@ -7,9 +7,10 @@ DevToolsModules(
'allocations.js',
'census-display.js',
'diffing.js',
'dominator-tree-display.js',
'errors.js',
'filter.js',
'individuals.js',
'label-display.js',
'sizes.js',
'snapshots.js',
'tree-map-display.js',

View File

@@ -210,12 +210,18 @@ handlers[actions.DELETE_SNAPSHOTS_END] = function (snapshots) {
return snapshots;
};
handlers[actions.CHANGE_VIEW] = function (snapshots, { view }) {
return view === viewState.DIFFING
handlers[actions.CHANGE_VIEW] = function (snapshots, { newViewState }) {
return newViewState === viewState.DIFFING
? snapshots.map(s => immutableUpdate(s, { selected: false }))
: snapshots;
};
handlers[actions.POP_VIEW] = function (snapshots, { previousView }) {
return snapshots.map(s => immutableUpdate(s, {
selected: s.id === previousView.selected
}));
};
handlers[actions.COMPUTE_DOMINATOR_TREE_START] = function (snapshots, { id }) {
const dominatorTree = Object.freeze({
state: dominatorTreeState.COMPUTING,

View File

@@ -3,15 +3,47 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { assert } = require("devtools/shared/DevToolsUtils");
const { actions, viewState } = require("../constants");
const handlers = Object.create(null);
handlers[actions.CHANGE_VIEW] = function (_, { view }) {
return view;
handlers[actions.POP_VIEW] = function (view, _) {
assert(view.previous, "Had better have a previous view state when POP_VIEW");
return Object.freeze({
state: view.previous.state,
previous: null,
});
};
module.exports = function (view = viewState.TREE_MAP, action) {
handlers[actions.CHANGE_VIEW] = function (view, action) {
const { newViewState, oldDiffing, oldSelected } = action;
assert(newViewState);
if (newViewState === viewState.INDIVIDUALS) {
assert(oldDiffing || oldSelected);
return Object.freeze({
state: newViewState,
previous: Object.freeze({
state: view.state,
selected: oldSelected,
diffing: oldDiffing,
}),
});
}
return Object.freeze({
state: newViewState,
previous: null,
});
};
const DEFAULT_VIEW = {
state: viewState.TREE_MAP,
previous: null,
};
module.exports = function (view = DEFAULT_VIEW, action) {
const handler = handlers[action.type];
return handler ? handler(view, action) : view;
};

View File

@@ -10,7 +10,7 @@
const { telemetry } = require("Services");
const { makeInfallible, immutableUpdate } = require("devtools/shared/DevToolsUtils");
const { dominatorTreeDisplays, treeMapDisplays, censusDisplays } = require("./constants");
const { labelDisplays, treeMapDisplays, censusDisplays } = require("./constants");
exports.countTakeSnapshot = makeInfallible(function () {
const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_TAKE_SNAPSHOT_COUNT");
@@ -81,9 +81,9 @@ exports.countDominatorTree = makeInfallible(function ({ display }) {
histogram.add(1);
histogram = telemetry.getKeyedHistogramById("DEVTOOLS_MEMORY_BREAKDOWN_DOMINATOR_TREE_COUNT");
if (display === dominatorTreeDisplays.coarseType) {
if (display === labelDisplays.coarseType) {
histogram.add(COARSE_TYPE);
} else if (display === dominatorTreeDisplays.allocationStack) {
} else if (display === labelDisplays.allocationStack) {
histogram.add(ALLOCATION_STACK);
} else {
histogram.add(CUSTOM);

View File

@@ -16,6 +16,7 @@ support-files =
[browser_memory_dominator_trees_01.js]
[browser_memory_dominator_trees_02.js]
[browser_memory_filter_01.js]
[browser_memory_individuals_01.js]
[browser_memory_keyboard.js]
[browser_memory_keyboard-snapshot-list.js]
[browser_memory_no_allocation_stacks.js]

View File

@@ -5,7 +5,6 @@
"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");

View File

@@ -5,7 +5,6 @@
"use strict";
const { waitForTime } = require("devtools/shared/DevToolsUtils");
const {
snapshotState,
diffingState,

View File

@@ -0,0 +1,67 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Sanity test that we can show census group individuals, and then go back to
// the previous view.
"use strict";
const {
individualsState,
viewState,
censusState,
} = require("devtools/client/memory/constants");
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));
// Take a snapshot and wait for the census to finish.
const takeSnapshotButton = doc.getElementById("take-snapshot");
EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
yield waitUntilState(store, state => {
return state.snapshots.length === 1 &&
state.snapshots[0].census &&
state.snapshots[0].census.state === censusState.SAVED;
});
// Click on the first individuals button found, and wait for the individuals
// to be fetched.
const individualsButton = doc.querySelector(".individuals-button");
EventUtils.synthesizeMouseAtCenter(individualsButton, {}, panel.panelWin);
yield waitUntilState(store, state => {
return state.view.state === viewState.INDIVIDUALS &&
state.individuals &&
state.individuals.state === individualsState.FETCHED;
});
ok(doc.getElementById("shortest-paths"),
"Should be showing the shortest paths component");
ok(doc.querySelector(".heap-tree-item"),
"Should be showing the individuals");
// Go back to the previous view.
const popViewButton = doc.getElementById("pop-view-button");
ok(popViewButton, "Should be showing the #pop-view-button");
EventUtils.synthesizeMouseAtCenter(popViewButton, {}, panel.panelWin);
yield waitUntilState(store, state => {
return state.view.state === viewState.CENSUS;
});
ok(!doc.getElementById("shortest-paths"),
"Should not be showing the shortest paths component anymore");
});

View File

@@ -193,9 +193,10 @@ function waitUntilCensusState (store, getCensus, expected) {
(census && census.state === state);
});
};
info(`Waiting for snapshots' censuses to be of state: ${expected}`);
info(`Waiting for snapshot censuses to be of state: ${expected}`);
return waitUntilState(store, predicate);
}
/**
* Mock out the requestAnimationFrame.
*

View File

@@ -31,7 +31,7 @@ var constants = require("devtools/client/memory/constants");
var {
censusDisplays,
diffingState,
dominatorTreeDisplays,
labelDisplays,
dominatorTreeState,
snapshotState,
viewState,
@@ -135,7 +135,7 @@ var TEST_DOMINATOR_TREE = Object.freeze({
expanded: new Set(),
focused: null,
error: null,
display: dominatorTreeDisplays.coarseType,
display: labelDisplays.coarseType,
activeFetchRequestCount: null,
state: dominatorTreeState.LOADED,
});
@@ -174,7 +174,7 @@ var TEST_HEAP_PROPS = Object.freeze({
onDominatorTreeFocus: noop,
onViewSourceInDebugger: noop,
diffing: null,
view: viewState.CENSUS,
view: { state: viewState.CENSUS, },
snapshot: Object.freeze({
id: 1337,
selected: true,
@@ -221,6 +221,7 @@ var TEST_TOOLBAR_PROPS = Object.freeze({
censusDisplays.allocationStack,
censusDisplays.invertedAllocationStack,
],
censusDisplay: censusDisplays.coarseType,
onTakeSnapshotClick: noop,
onImportClick: noop,
onCensusDisplayChange: noop,
@@ -232,13 +233,14 @@ var TEST_TOOLBAR_PROPS = Object.freeze({
setFilterString: noop,
diffing: null,
onToggleDiffing: noop,
view: viewState.CENSUS,
view: { state: viewState.CENSUS, },
onViewChange: noop,
dominatorTreeDisplays: [
dominatorTreeDisplays.coarseType,
dominatorTreeDisplays.allocationStack,
labelDisplays: [
labelDisplays.coarseType,
labelDisplays.allocationStack,
],
onDominatorTreeDisplayChange: noop,
labelDisplay: labelDisplays.coarseType,
onLabelDisplayChange: noop,
snapshots: [],
});

View File

@@ -13,6 +13,7 @@ Test that rendering a dominator tree error is handled correctly.
<div id="container"></div>
<pre id="test">
<script src="head.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript;version=1.8">
window.onload = Task.async(function* () {
try {
@@ -23,7 +24,7 @@ Test that rendering a dominator tree error is handled correctly.
const container = document.getElementById("container");
const props = immutableUpdate(TEST_HEAP_PROPS, {
view: viewState.DOMINATOR_TREE,
view: { state: viewState.DOMINATOR_TREE, },
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
dominatorTree: {
error: new Error(errorMessage),

View File

@@ -25,7 +25,7 @@ Test that the currently selected view is rendered.
// Dominator tree view.
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
view: viewState.DOMINATOR_TREE,
view: { state: viewState.DOMINATOR_TREE, },
})), container);
ok(container.querySelector(`[data-state=${dominatorTreeState.LOADED}]`),
@@ -34,7 +34,7 @@ Test that the currently selected view is rendered.
// Census view.
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
view: viewState.CENSUS,
view: { state: viewState.CENSUS, },
})), container);
ok(container.querySelector(`[data-state=${censusState.SAVED}]`),
@@ -43,7 +43,7 @@ Test that the currently selected view is rendered.
// Diffing view.
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
view: viewState.DIFFING,
view: { state: viewState.DIFFING, },
snapshot: null,
diffing: {
firstSnapshotId: null,

View File

@@ -21,7 +21,7 @@ but not in other dominator tree states.
for (let state of [dominatorTreeState.COMPUTING, dominatorTreeState.FETCHING]) {
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
view: viewState.DOMINATOR_TREE,
view: { state: viewState.DOMINATOR_TREE, },
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
state,
@@ -37,7 +37,7 @@ but not in other dominator tree states.
for (let state of [dominatorTreeState.LOADED, dominatorTreeState.INCREMENTAL_FETCHING]) {
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
view: viewState.DOMINATOR_TREE,
view: { state: viewState.DOMINATOR_TREE, },
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
state,
@@ -51,7 +51,7 @@ but not in other dominator tree states.
}
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
view: viewState.DOMINATOR_TREE,
view: { state: viewState.DOMINATOR_TREE, },
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
dominatorTree: {
state: dominatorTreeState.ERROR,

View File

@@ -92,7 +92,7 @@ Test that we show a message when the census results are empty.
// Empty Diffing Report
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
view: viewState.DIFFING,
view: { state: viewState.DIFFING, },
diffing: {
firstSnapshotId: 1,
secondSnapshotId: 2,

View File

@@ -22,7 +22,7 @@ Test that the Toolbar component shows the view switcher only at the appropriate
for (let view of [viewState.CENSUS, viewState.DOMINATOR_TREE]) {
yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
view,
view: { state: view },
})), container);
ok(container.querySelector("#select-view"),
@@ -30,7 +30,7 @@ Test that the Toolbar component shows the view switcher only at the appropriate
}
yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
view: viewState.DIFFING,
view: { state: viewState.DIFFING, },
})), container);
ok(!container.querySelector("#select-view"),

View File

@@ -80,6 +80,23 @@ function waitUntilSnapshotState (store, expected) {
return waitUntilState(store, predicate);
}
function findReportLeafIndex(node, name = null) {
if (node.reportLeafIndex && (!name || node.name === name)) {
return node.reportLeafIndex;
}
if (node.children) {
for (let child of node.children) {
const found = findReportLeafIndex(child);
if (found) {
return found;
}
}
}
return null;
}
function waitUntilCensusState (store, getCensus, expected) {
let predicate = () => {
let snapshots = store.getState().snapshots;

View File

@@ -25,7 +25,7 @@ add_task(function* () {
let { subscribe, dispatch, getState } = store;
dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
equal(getState().view, viewState.DOMINATOR_TREE,
equal(getState().view.state, viewState.DOMINATOR_TREE,
"We should now be in the DOMINATOR_TREE view");
let i = 0;

View File

@@ -52,6 +52,9 @@ add_task(function *() {
yield waitUntilCensusState(store, snapshot => snapshot.census,
[censusState.SAVED]);
equal(getState().snapshots[0].census.display, censusDisplays.allocationStack,
"New snapshot's census uses correct display");
// Updates when changing display during `SAVING`
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilCensusState(store, snapshot => snapshot.census,
@@ -59,6 +62,8 @@ add_task(function *() {
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
yield waitUntilCensusState(store, snapshot => snapshot.census,
[censusState.SAVED, censusState.SAVED]);
equal(getState().snapshots[1].census.display, censusDisplays.coarseType,
"Changing display while saving a snapshot results in a census using the new display");
// Updates when changing display during `SAVING_CENSUS`
@@ -72,13 +77,13 @@ add_task(function *() {
[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].census.state === censusState.SAVING);
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");
@@ -88,12 +93,12 @@ add_task(function *() {
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");
// Does not update unselected censuses.
ok(!getState().snapshots[1].selected, "Second snapshot selected 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
// Updates to current display when switching to stale snapshot.
dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
yield waitUntilCensusState(store, snapshot => snapshot.census,
[censusState.SAVED,

View File

@@ -38,6 +38,8 @@ add_task(function *() {
// Test error case of wrong state.
store.dispatch(actions.takeCensus(heapWorker, snapshot.id));
yield waitUntilState(store, () => store.getState().errors.length === 1);
dumpn("Found error: " + store.getState().errors[0]);
ok(/Assertion failure/.test(store.getState().errors[0]),
"Error thrown when taking a census of a snapshot that has not been read.");

View File

@@ -28,7 +28,7 @@ add_task(function *() {
let { getState, dispatch } = store;
dispatch(changeView(viewState.DOMINATOR_TREE));
equal(getState().view, viewState.DOMINATOR_TREE,
equal(getState().view.state, viewState.DOMINATOR_TREE,
"We should now be in the DOMINATOR_TREE view");
dispatch(takeSnapshotAndCensus(front, heapWorker));

View File

@@ -37,7 +37,7 @@ add_task(function *() {
yield waitUntilSnapshotState(store, [intermediateSnapshotState]);
dispatch(changeView(viewState.DOMINATOR_TREE));
equal(getState().view, viewState.DOMINATOR_TREE,
equal(getState().view.state, viewState.DOMINATOR_TREE,
"We should now be in the DOMINATOR_TREE view");
// Wait for the dominator tree to start being computed.

View File

@@ -8,12 +8,12 @@ const {
snapshotState: states,
dominatorTreeState,
viewState,
dominatorTreeDisplays,
labelDisplays,
treeMapState
} = require("devtools/client/memory/constants");
const {
setDominatorTreeDisplayAndRefresh
} = require("devtools/client/memory/actions/dominator-tree-display");
setLabelDisplayAndRefresh
} = require("devtools/client/memory/actions/label-display");
const {
changeView,
} = require("devtools/client/memory/actions/view");
@@ -47,19 +47,19 @@ add_task(function *() {
state.snapshots[0].dominatorTree &&
state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
ok(getState().dominatorTreeDisplay,
ok(getState().labelDisplay,
"We have a default display for describing nodes in a dominator tree");
equal(getState().dominatorTreeDisplay,
dominatorTreeDisplays.coarseType,
equal(getState().labelDisplay,
labelDisplays.coarseType,
"and the default is coarse type");
equal(getState().dominatorTreeDisplay,
equal(getState().labelDisplay,
getState().snapshots[0].dominatorTree.display,
"and the newly computed dominator tree has that display");
// Switch to the allocationStack display.
dispatch(setDominatorTreeDisplayAndRefresh(
dispatch(setLabelDisplayAndRefresh(
heapWorker,
dominatorTreeDisplays.allocationStack));
labelDisplays.allocationStack));
yield waitUntilState(store, state =>
state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
@@ -70,10 +70,10 @@ add_task(function *() {
yield waitUntilState(store, state =>
state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
equal(getState().snapshots[0].dominatorTree.display,
dominatorTreeDisplays.allocationStack,
labelDisplays.allocationStack,
"The new dominator tree's display is allocationStack");
equal(getState().dominatorTreeDisplay,
dominatorTreeDisplays.allocationStack,
equal(getState().labelDisplay,
labelDisplays.allocationStack,
"as is our requested dominator tree display");
heapWorker.destroy();

View File

@@ -8,12 +8,12 @@ const {
snapshotState: states,
dominatorTreeState,
viewState,
dominatorTreeDisplays,
labelDisplays,
treeMapState,
} = require("devtools/client/memory/constants");
const {
setDominatorTreeDisplayAndRefresh
} = require("devtools/client/memory/actions/dominator-tree-display");
setLabelDisplayAndRefresh
} = require("devtools/client/memory/actions/label-display");
const {
changeView,
} = require("devtools/client/memory/actions/view");
@@ -47,30 +47,30 @@ add_task(function *() {
state.snapshots[0].dominatorTree &&
state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
ok(getState().dominatorTreeDisplay,
ok(getState().labelDisplay,
"We have a default display for describing nodes in a dominator tree");
equal(getState().dominatorTreeDisplay,
dominatorTreeDisplays.coarseType,
equal(getState().labelDisplay,
labelDisplays.coarseType,
"and the default is coarse type");
equal(getState().dominatorTreeDisplay,
equal(getState().labelDisplay,
getState().snapshots[0].dominatorTree.display,
"and the newly computed dominator tree has that display");
// Switch to the allocationStack display while we are still fetching the
// dominator tree.
dispatch(setDominatorTreeDisplayAndRefresh(
dispatch(setLabelDisplayAndRefresh(
heapWorker,
dominatorTreeDisplays.allocationStack));
labelDisplays.allocationStack));
// Wait for the dominator tree to finish being fetched.
yield waitUntilState(store, state =>
state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
equal(getState().snapshots[0].dominatorTree.display,
dominatorTreeDisplays.allocationStack,
labelDisplays.allocationStack,
"The new dominator tree's display is allocationStack");
equal(getState().dominatorTreeDisplay,
dominatorTreeDisplays.allocationStack,
equal(getState().labelDisplay,
labelDisplays.allocationStack,
"as is our requested dominator tree display");
heapWorker.destroy();

View File

@@ -7,7 +7,7 @@
let {
snapshotState: states,
dominatorTreeState,
dominatorTreeDisplays,
labelDisplays,
viewState,
} = require("devtools/client/memory/constants");
let {
@@ -18,8 +18,8 @@ const {
changeView,
} = require("devtools/client/memory/actions/view");
const {
setDominatorTreeDisplayAndRefresh,
} = require("devtools/client/memory/actions/dominator-tree-display");
setLabelDisplayAndRefresh,
} = require("devtools/client/memory/actions/label-display");
function run_test() {
run_next_test();
@@ -49,12 +49,12 @@ add_task(function *() {
equal(root, getState().snapshots[0].dominatorTree.focused,
"The root should be focused.");
equal(getState().dominatorTreeDisplay, dominatorTreeDisplays.coarseType,
"Using dominatorTreeDisplays.coarseType by default");
dispatch(setDominatorTreeDisplayAndRefresh(heapWorker,
dominatorTreeDisplays.allocationStack));
equal(getState().dominatorTreeDisplay, dominatorTreeDisplays.allocationStack,
"Using dominatorTreeDisplays.allocationStack now");
equal(getState().labelDisplay, labelDisplays.coarseType,
"Using labelDisplays.coarseType by default");
dispatch(setLabelDisplayAndRefresh(heapWorker,
labelDisplays.allocationStack));
equal(getState().labelDisplay, labelDisplays.allocationStack,
"Using labelDisplays.allocationStack now");
yield waitUntilState(store, state =>
state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);

View File

@@ -0,0 +1,76 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Basic test for switching to the individuals view.
const {
censusState,
viewState,
individualsState,
} = require("devtools/client/memory/constants");
const {
fetchIndividuals,
takeSnapshotAndCensus,
} = require("devtools/client/memory/actions/snapshot");
const {
changeView,
} = require("devtools/client/memory/actions/view");
function run_test() {
run_next_test();
}
const EXPECTED_INDIVIDUAL_STATES = [
individualsState.COMPUTING_DOMINATOR_TREE,
individualsState.FETCHING,
individualsState.FETCHED,
];
add_task(function*() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
equal(getState().individuals, null,
"no individuals state by default");
dispatch(changeView(viewState.CENSUS));
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
const root = getState().snapshots[0].census.report;
ok(root, "Should have a census");
const reportLeafIndex = findReportLeafIndex(root);
ok(reportLeafIndex, "Should get a reportLeafIndex");
const snapshotId = getState().snapshots[0].id;
ok(snapshotId, "Should have a snapshot id");
const breakdown = getState().snapshots[0].census.display.breakdown;
ok(breakdown, "Should have a breakdown");
dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
reportLeafIndex));
// Wait for each expected state.
for (let state of EXPECTED_INDIVIDUAL_STATES) {
yield waitUntilState(store, s => {
return s.view.state === viewState.INDIVIDUALS &&
s.individuals &&
s.individuals.state === state;
});
ok(true, `Reached state = ${state}`);
}
ok(getState().individuals, "Should have individuals state");
ok(getState().individuals.nodes, "Should have individuals nodes");
ok(getState().individuals.nodes.length > 0,
"Should have a positive number of nodes");
heapWorker.destroy();
yield front.detach();
});

View File

@@ -0,0 +1,88 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test switching to the individuals view when we are in the middle of computing
// a dominator tree.
const {
censusState,
dominatorTreeState,
viewState,
individualsState,
} = require("devtools/client/memory/constants");
const {
fetchIndividuals,
takeSnapshotAndCensus,
computeDominatorTree,
} = require("devtools/client/memory/actions/snapshot");
const {
changeView,
} = require("devtools/client/memory/actions/view");
function run_test() {
run_next_test();
}
const EXPECTED_INDIVIDUAL_STATES = [
individualsState.COMPUTING_DOMINATOR_TREE,
individualsState.FETCHING,
individualsState.FETCHED,
];
add_task(function*() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
equal(getState().individuals, null,
"no individuals state by default");
dispatch(changeView(viewState.CENSUS));
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
const root = getState().snapshots[0].census.report;
ok(root, "Should have a census");
const reportLeafIndex = findReportLeafIndex(root);
ok(reportLeafIndex, "Should get a reportLeafIndex");
const snapshotId = getState().snapshots[0].id;
ok(snapshotId, "Should have a snapshot id");
const breakdown = getState().snapshots[0].census.display.breakdown;
ok(breakdown, "Should have a breakdown");
// Start computing a dominator tree.
dispatch(computeDominatorTree(heapWorker, snapshotId));
equal(getState().snapshots[0].dominatorTree.state,
dominatorTreeState.COMPUTING,
"Should be computing dominator tree");
// Fetch individuals in the middle of computing the dominator tree.
dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
reportLeafIndex));
// Wait for each expected state.
for (let state of EXPECTED_INDIVIDUAL_STATES) {
yield waitUntilState(store, s => {
return s.view.state === viewState.INDIVIDUALS &&
s.individuals &&
s.individuals.state === state;
});
ok(true, `Reached state = ${state}`);
}
ok(getState().individuals, "Should have individuals state");
ok(getState().individuals.nodes, "Should have individuals nodes");
ok(getState().individuals.nodes.length > 0,
"Should have a positive number of nodes");
heapWorker.destroy();
yield front.detach();
});

View File

@@ -0,0 +1,106 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test switching to the individuals view when we are in the diffing view.
const {
censusState,
diffingState,
viewState,
individualsState,
} = require("devtools/client/memory/constants");
const {
fetchIndividuals,
takeSnapshotAndCensus,
} = require("devtools/client/memory/actions/snapshot");
const {
changeView,
popViewAndRefresh,
} = require("devtools/client/memory/actions/view");
const {
selectSnapshotForDiffingAndRefresh,
} = require("devtools/client/memory/actions/diffing");
function run_test() {
run_next_test();
}
const EXPECTED_INDIVIDUAL_STATES = [
individualsState.COMPUTING_DOMINATOR_TREE,
individualsState.FETCHING,
individualsState.FETCHED,
];
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));
// Take two snapshots and diff them from each other.
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
censusState.SAVED]);
dispatch(changeView(viewState.DIFFING));
dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[0]));
dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[1]));
yield waitUntilState(store, state => {
return state.diffing &&
state.diffing.state === diffingState.TOOK_DIFF;
});
ok(getState().diffing.census);
// Fetch individuals.
const root = getState().diffing.census.report;
ok(root, "Should have a census");
const reportLeafIndex = findReportLeafIndex(root);
ok(reportLeafIndex, "Should get a reportLeafIndex");
const snapshotId = getState().diffing.secondSnapshotId;
ok(snapshotId, "Should have a snapshot id");
const breakdown = getState().censusDisplay.breakdown;
ok(breakdown, "Should have a breakdown");
dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
reportLeafIndex));
for (let state of EXPECTED_INDIVIDUAL_STATES) {
yield waitUntilState(store, s => {
return s.view.state === viewState.INDIVIDUALS &&
s.individuals &&
s.individuals.state === state;
});
ok(true, `Reached state = ${state}`);
}
ok(getState().individuals, "Should have individuals state");
ok(getState().individuals.nodes, "Should have individuals nodes");
ok(getState().individuals.nodes.length > 0,
"Should have a positive number of nodes");
// Pop the view back to the diffing.
dispatch(popViewAndRefresh(heapWorker));
yield waitUntilState(store, state => {
return state.diffing &&
state.diffing.state === diffingState.TOOK_DIFF;
});
ok(getState().diffing.census.report,
"We have our census diff again after popping back to the last view");
heapWorker.destroy();
yield front.detach();
});

View File

@@ -0,0 +1,89 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test showing individual Array objects.
const {
censusState,
viewState,
individualsState,
} = require("devtools/client/memory/constants");
const {
fetchIndividuals,
takeSnapshotAndCensus,
} = require("devtools/client/memory/actions/snapshot");
const {
changeView,
} = require("devtools/client/memory/actions/view");
const {
setFilterString,
} = require("devtools/client/memory/actions/filter");
function run_test() {
run_next_test();
}
const EXPECTED_INDIVIDUAL_STATES = [
individualsState.COMPUTING_DOMINATOR_TREE,
individualsState.FETCHING,
individualsState.FETCHED,
];
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));
dispatch(setFilterString("Array"));
// Take a snapshot and wait for the census to finish.
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
// Fetch individuals.
const root = getState().snapshots[0].census.report;
ok(root, "Should have a census");
const reportLeafIndex = findReportLeafIndex(root, "Array");
ok(reportLeafIndex, "Should get a reportLeafIndex for Array");
const snapshotId = getState().snapshots[0].id;
ok(snapshotId, "Should have a snapshot id");
const breakdown = getState().censusDisplay.breakdown;
ok(breakdown, "Should have a breakdown");
dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
reportLeafIndex));
for (let state of EXPECTED_INDIVIDUAL_STATES) {
yield waitUntilState(store, s => {
return s.view.state === viewState.INDIVIDUALS &&
s.individuals &&
s.individuals.state === state;
});
ok(true, `Reached state = ${state}`);
}
ok(getState().individuals, "Should have individuals state");
ok(getState().individuals.nodes, "Should have individuals nodes");
ok(getState().individuals.nodes.length > 0,
"Should have a positive number of nodes");
// Assert that all the individuals are `Array`s.
for (let node of getState().individuals.nodes) {
dumpn("Checking node: " + node.label.join(" > "));
ok(node.label.find(part => part === "Array"),
"The node should be an Array node");
}
heapWorker.destroy();
yield front.detach();
});

View File

@@ -0,0 +1,82 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test showing individual objects that do not have allocation stacks.
const {
censusState,
viewState,
individualsState,
censusDisplays,
} = require("devtools/client/memory/constants");
const {
fetchIndividuals,
takeSnapshotAndCensus,
} = require("devtools/client/memory/actions/snapshot");
const {
changeView,
} = require("devtools/client/memory/actions/view");
const {
setCensusDisplay,
} = require("devtools/client/memory/actions/census-display");
function run_test() {
run_next_test();
}
const EXPECTED_INDIVIDUAL_STATES = [
individualsState.COMPUTING_DOMINATOR_TREE,
individualsState.FETCHING,
individualsState.FETCHED,
];
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));
dispatch(setCensusDisplay(censusDisplays.invertedAllocationStack));
// Take a snapshot and wait for the census to finish.
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
// Fetch individuals.
const root = getState().snapshots[0].census.report;
ok(root, "Should have a census");
const reportLeafIndex = findReportLeafIndex(root, "noStack");
ok(reportLeafIndex, "Should get a reportLeafIndex for noStack");
const snapshotId = getState().snapshots[0].id;
ok(snapshotId, "Should have a snapshot id");
const breakdown = getState().censusDisplay.breakdown;
ok(breakdown, "Should have a breakdown");
dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
reportLeafIndex));
for (let state of EXPECTED_INDIVIDUAL_STATES) {
yield waitUntilState(store, s => {
return s.view.state === viewState.INDIVIDUALS &&
s.individuals &&
s.individuals.state === state;
});
ok(true, `Reached state = ${state}`);
}
ok(getState().individuals, "Should have individuals state");
ok(getState().individuals.nodes, "Should have individuals nodes");
ok(getState().individuals.nodes.length > 0,
"Should have a positive number of nodes");
heapWorker.destroy();
yield front.detach();
});

View File

@@ -0,0 +1,84 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that clearing the current individuals' snapshot leaves the individuals
// view.
const {
censusState,
viewState,
individualsState,
} = require("devtools/client/memory/constants");
const {
fetchIndividuals,
takeSnapshotAndCensus,
clearSnapshots,
} = require("devtools/client/memory/actions/snapshot");
const {
changeView,
} = require("devtools/client/memory/actions/view");
function run_test() {
run_next_test();
}
const EXPECTED_INDIVIDUAL_STATES = [
individualsState.COMPUTING_DOMINATOR_TREE,
individualsState.FETCHING,
individualsState.FETCHED,
];
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));
// Take a snapshot and wait for the census to finish.
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
// Fetch individuals.
const root = getState().snapshots[0].census.report;
ok(root, "Should have a census");
const reportLeafIndex = findReportLeafIndex(root);
ok(reportLeafIndex, "Should get a reportLeafIndex");
const snapshotId = getState().snapshots[0].id;
ok(snapshotId, "Should have a snapshot id");
const breakdown = getState().censusDisplay.breakdown;
ok(breakdown, "Should have a breakdown");
dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
reportLeafIndex));
for (let state of EXPECTED_INDIVIDUAL_STATES) {
yield waitUntilState(store, s => {
return s.view.state === viewState.INDIVIDUALS &&
s.individuals &&
s.individuals.state === state;
});
ok(true, `Reached state = ${state}`);
}
ok(getState().individuals, "Should have individuals state");
ok(getState().individuals.nodes, "Should have individuals nodes");
ok(getState().individuals.nodes.length > 0,
"Should have a positive number of nodes");
dispatch(clearSnapshots(heapWorker));
equal(getState().view.state, viewState.CENSUS,
"Went back to census view");
heapWorker.destroy();
yield front.detach();
});

View File

@@ -0,0 +1,81 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test popping views from each intermediate individuals model state.
const {
censusState,
viewState,
individualsState,
} = require("devtools/client/memory/constants");
const {
fetchIndividuals,
takeSnapshotAndCensus,
} = require("devtools/client/memory/actions/snapshot");
const {
changeView,
popViewAndRefresh,
} = require("devtools/client/memory/actions/view");
function run_test() {
run_next_test();
}
const TEST_STATES = [
individualsState.COMPUTING_DOMINATOR_TREE,
individualsState.FETCHING,
individualsState.FETCHED,
];
add_task(function*() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
equal(getState().individuals, null,
"no individuals state by default");
dispatch(changeView(viewState.CENSUS));
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
const root = getState().snapshots[0].census.report;
ok(root, "Should have a census");
const reportLeafIndex = findReportLeafIndex(root);
ok(reportLeafIndex, "Should get a reportLeafIndex");
const snapshotId = getState().snapshots[0].id;
ok(snapshotId, "Should have a snapshot id");
const breakdown = getState().snapshots[0].census.display.breakdown;
ok(breakdown, "Should have a breakdown");
for (let state of TEST_STATES) {
dumpn(`Testing popping back to the old view from state = ${state}`);
dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown,
reportLeafIndex));
// Wait for the expected test state.
yield waitUntilState(store, s => {
return s.view.state === viewState.INDIVIDUALS &&
s.individuals &&
s.individuals.state === state;
});
ok(true, `Reached state = ${state}`);
// Pop back to the CENSUS state.
dispatch(popViewAndRefresh(heapWorker));
yield waitUntilState(store, s => {
return s.view.state === viewState.CENSUS;
});
ok(!getState().individuals, "Should no longer have individuals");
}
heapWorker.destroy();
yield front.detach();
});

View File

@@ -9,7 +9,7 @@
*/
let utils = require("devtools/client/memory/utils");
let { snapshotState: states, censusDisplays } = require("devtools/client/memory/constants");
let { snapshotState: states, viewState } = require("devtools/client/memory/constants");
let { Preferences } = require("resource://gre/modules/Preferences.jsm");
function run_test() {
@@ -17,8 +17,8 @@ function run_test() {
}
add_task(function *() {
let s1 = utils.createSnapshot({});
let s2 = utils.createSnapshot({});
let s1 = utils.createSnapshot({ view: { state: viewState.CENSUS } });
let s2 = utils.createSnapshot({ view: { state: viewState.CENSUS } });
equal(s1.state, states.SAVING, "utils.createSnapshot() creates snapshot in saving state");
ok(s1.id !== s2.id, "utils.createSnapshot() creates snapshot with unique ids");

View File

@@ -43,6 +43,13 @@ skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_dominator_trees_08.js]
[test_dominator_trees_09.js]
[test_dominator_trees_10.js]
[test_individuals_01.js]
[test_individuals_02.js]
[test_individuals_03.js]
[test_individuals_04.js]
[test_individuals_05.js]
[test_individuals_06.js]
[test_pop_view_01.js]
[test_tree-map-01.js]
[test_tree-map-02.js]
[test_utils.js]

View File

@@ -12,7 +12,7 @@ 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_LABEL_DISPLAY_PREF = "devtools.memory.custom-label-displays";
const CUSTOM_TREE_MAP_DISPLAY_PREF = "devtools.memory.custom-tree-map-displays";
const BYTES = 1024;
const KILOBYTES = Math.pow(BYTES, 2);
@@ -23,9 +23,8 @@ const {
diffingState,
censusState,
treeMapState,
censusDisplays,
dominatorTreeDisplays,
dominatorTreeState
dominatorTreeState,
individualsState,
} = require("./constants");
/**
@@ -77,12 +76,12 @@ exports.getCustomCensusDisplays = function () {
/**
* Returns custom displays defined in
* `devtools.memory.custom-dominator-tree-displays` pref.
* `devtools.memory.custom-label-displays` pref.
*
* @return {Object}
*/
exports.getCustomDominatorTreeDisplays = function () {
return getCustomDisplaysHelper(CUSTOM_DOMINATOR_TREE_DISPLAY_PREF);
exports.getCustomLabelDisplays = function () {
return getCustomDisplaysHelper(CUSTOM_LABEL_DISPLAY_PREF);
};
/**
@@ -135,6 +134,7 @@ exports.getStatusText = function (state) {
return L10N.getStr("diffing.state.selecting");
case dominatorTreeState.COMPUTING:
case individualsState.COMPUTING_DOMINATOR_TREE:
return L10N.getStr("dominatorTree.state.computing");
case dominatorTreeState.COMPUTED:
@@ -147,6 +147,12 @@ exports.getStatusText = function (state) {
case dominatorTreeState.ERROR:
return L10N.getStr("dominatorTree.state.error");
case individualsState.ERROR:
return L10N.getStr("individuals.state.error");
case individualsState.FETCHING:
return L10N.getStr("individuals.state.fetching");
// These states do not have any message to show as other content will be
// displayed.
case dominatorTreeState.LOADED:
@@ -154,6 +160,7 @@ exports.getStatusText = function (state) {
case states.READ:
case censusState.SAVED:
case treeMapState.SAVED:
case individualsState.FETCHED:
return "";
default:
@@ -202,6 +209,7 @@ exports.getStatusTextFull = function (state) {
return L10N.getStr("diffing.state.selecting.full");
case dominatorTreeState.COMPUTING:
case individualsState.COMPUTING_DOMINATOR_TREE:
return L10N.getStr("dominatorTree.state.computing.full");
case dominatorTreeState.COMPUTED:
@@ -214,6 +222,12 @@ exports.getStatusTextFull = function (state) {
case dominatorTreeState.ERROR:
return L10N.getStr("dominatorTree.state.error.full");
case individualsState.ERROR:
return L10N.getStr("individuals.state.error.full");
case individualsState.FETCHING:
return L10N.getStr("individuals.state.fetching.full");
// These states do not have any full message to show as other content will
// be displayed.
case dominatorTreeState.LOADED:
@@ -221,6 +235,7 @@ exports.getStatusTextFull = function (state) {
case states.READ:
case censusState.SAVED:
case treeMapState.SAVED:
case individualsState.FETCHED:
return "";
default:
@@ -257,6 +272,16 @@ exports.getSnapshot = function getSnapshot (state, id) {
return found;
};
/**
* Get the ID of the selected snapshot, if one is selected, null otherwise.
*
* @returns {SnapshotId|null}
*/
exports.findSelectedSnapshot = function (state) {
const found = state.snapshots.find(s => s.selected);
return found ? found.id : null;
};
/**
* Creates a new snapshot object.
*
@@ -266,7 +291,7 @@ exports.getSnapshot = function getSnapshot (state, id) {
let ID_COUNTER = 0;
exports.createSnapshot = function createSnapshot(state) {
let dominatorTree = null;
if (state.view === dominatorTreeState.DOMINATOR_TREE) {
if (state.view.state === dominatorTreeState.DOMINATOR_TREE) {
dominatorTree = Object.freeze({
dominatorTreeId: null,
root: null,

View File

@@ -105,9 +105,12 @@ pref("devtools.debugger.ui.variables-searchbox-visible", false);
pref("devtools.memory.enabled", false);
pref("devtools.memory.custom-census-displays", "{}");
pref("devtools.memory.custom-dominator-tree-displays", "{}");
pref("devtools.memory.custom-label-displays", "{}");
pref("devtools.memory.custom-tree-map-displays", "{}");
pref("devtools.memory.max-individuals", 1000);
pref("devtools.memory.max-retaining-paths", 10);
// Enable the Performance tools
pref("devtools.performance.enabled", true);

View File

@@ -36,8 +36,8 @@ html, body, #app, #memory-tool {
* If --heap-tree-row-height changes, be sure to change HEAP_TREE_ROW_HEIGHT
* in `devtools/client/memory/components/heap.js`.
*/
--heap-tree-row-height: 14px;
--heap-tree-header-height: 17px;
--heap-tree-row-height: 18px;
--heap-tree-header-height: 18px;
}
/**
@@ -115,7 +115,15 @@ html, body, #app, #memory-tool {
background-image: url(chrome://devtools/skin/images/diff.svg);
}
#import-snapshot {
#record-allocation-stacks-label,
#pop-view-button-label {
border-inline-end: 1px solid var(--theme-splitter-color);
padding-inline-end: 5px;
}
#import-snapshot,
#clear-snapshots {
-moz-box-align: center;
flex-grow: 1;
}
@@ -399,6 +407,7 @@ html, body, #app, #memory-tool {
color: var(--theme-selection-background);
}
.heap-tree-item-individuals,
.heap-tree-item-bytes,
.heap-tree-item-count,
.heap-tree-item-total-bytes,
@@ -485,6 +494,20 @@ html, body, #app, #memory-tool {
color: inherit;
}
.heap-tree-item-individuals {
width: 38px;
min-width: 20px;
overflow: hidden;
margin: 0;
}
.heap-tree-item-individuals > button {
height: 10px;
width: 32px;
margin: 0 auto;
padding: 0;
}
/**
* Tree map
*/
@@ -531,6 +554,10 @@ html, body, #app, #memory-tool {
margin-right: .5em;
}
.heap-tree-item-address {
font-family: monospace;
}
.no-allocation-stacks {
border-color: var(--theme-splitter-color);
border-style: solid;

View File

@@ -231,6 +231,8 @@ HeapAnalysesClient.prototype.computeDominatorTree = function (snapshotFilePath)
* - {Number} maxSiblings
* The maximum number of siblings to visit within each traversed node's
* children.
* - {Number} maxRetainingPaths
* The maximum number of retaining paths to find for each node.
*
* @returns {Promise<DominatorTreeNode>}
*/