mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Bug 1238695 - Render census data as a treemap. r=fitzgen, r=vporof
This commit is contained in:
parent
4fdd179921
commit
bffbcbb869
@ -77,6 +77,10 @@ censusDisplays.allocationStack.tooltip=Group items by the JavaScript stack recor
|
||||
# tooltip for the "inverted allocation stack" display option.
|
||||
censusDisplays.invertedAllocationStack.tooltip=Group items by the inverted JavaScript call stack recorded when the object was created
|
||||
|
||||
# LOCALIZATION NOTE (breakdowns.treeMap.tooltip): The tooltip for the "tree map"
|
||||
# breakdown option.
|
||||
censusDisplays.treeMap.tooltip=Visualize memory usage: larger blocks account for a larger percent of memory usage
|
||||
|
||||
# LOCALIZATION NOTE (censusDisplays.objectClass.tooltip): The tooltip for the
|
||||
# "object class" display option.
|
||||
censusDisplays.objectClass.tooltip=Group items by their JavaScript Object [[class]] name
|
||||
@ -105,6 +109,10 @@ dominatorTreeDisplays.allocationStack.tooltip=Label objects by the JavaScript st
|
||||
# tooltip for the "internal type" dominator tree display option.
|
||||
dominatorTreeDisplays.internalType.tooltip=Label objects by their internal C++ type name
|
||||
|
||||
# LOCALIZATION NOTE (treeMapDisplays.coarseType.tooltip): The tooltip for
|
||||
# the "coarse type" tree map display option.
|
||||
treeMapDisplays.coarseType.tooltip=Label objects by the broad categories they fit in
|
||||
|
||||
# LOCALIZATION NOTE (toolbar.view): The label for the view selector in the
|
||||
# toolbar.
|
||||
toolbar.view=View:
|
||||
@ -129,6 +137,14 @@ toolbar.view.dominators=Dominators
|
||||
# for the dominators view option in the toolbar.
|
||||
toolbar.view.dominators.tooltip=View the dominator tree and surface the largest structures in the heap snapshot
|
||||
|
||||
# LOCALIZATION NOTE (toolbar.view.treemap): The label for the tree map option
|
||||
# in the toolbar.
|
||||
toolbar.view.treemap=Tree Map
|
||||
|
||||
# LOCALIZATION NOTE (toolbar.view.treemap.tooltip): The tooltip for the label for
|
||||
# the tree map view option in the toolbar.
|
||||
toolbar.view.treemap.tooltip=Visualize memory usage: larger blocks account for a larger percent of memory usage
|
||||
|
||||
# LOCALIZATION NOTE (take-snapshot): The label describing the button that
|
||||
# initiates taking a snapshot, either as the main label, or a tooltip.
|
||||
take-snapshot=Take snapshot
|
||||
@ -271,6 +287,10 @@ snapshot.state.reading.full=Reading snapshot…
|
||||
# the snapshot state SAVING, used in the main heap view.
|
||||
snapshot.state.saving-census.full=Saving census…
|
||||
|
||||
# LOCALIZATION NOTE (snapshot.state.saving-tree-map.full): The label describing
|
||||
# the snapshot state SAVING, used in the main heap view.
|
||||
snapshot.state.saving-tree-map.full=Saving tree map…
|
||||
|
||||
# LOCALIZATION NOTE (snapshot.state.error.full): The label describing the
|
||||
# snapshot state ERROR, used in the main heap view.
|
||||
snapshot.state.error.full=There was an error processing this snapshot.
|
||||
@ -292,6 +312,10 @@ snapshot.state.reading=Reading snapshot…
|
||||
# snapshot state SAVING, used in snapshot list view.
|
||||
snapshot.state.saving-census=Saving census…
|
||||
|
||||
# LOCALIZATION NOTE (snapshot.state.saving-census): The label describing the
|
||||
# snapshot state SAVING, used in snapshot list view.
|
||||
snapshot.state.saving-tree-map=Saving tree map…
|
||||
|
||||
# LOCALIZATION NOTE (snapshot.state.error): The label describing the snapshot
|
||||
# state ERROR, used in the snapshot list view.
|
||||
snapshot.state.error=Error
|
||||
@ -384,3 +408,7 @@ shortest-paths.header=Retaining Paths from GC Roots
|
||||
# LOCALIZATION NOTE (shortest-paths.select-node): The message displayed in the
|
||||
# shortest paths pane when a node is not yet selected.
|
||||
shortest-paths.select-node=Select a node to view its retaining paths
|
||||
|
||||
# LOCALIZATION NOTE (tree-map.node-count): The label for the count value of a
|
||||
# node in the tree map
|
||||
tree-map.node-count=count
|
||||
|
@ -11,6 +11,8 @@ const {
|
||||
censusIsUpToDate,
|
||||
snapshotIsDiffable
|
||||
} = require("../utils");
|
||||
// This is a circular dependency, so do not destructure the needed properties.
|
||||
const snapshotActions = require("./snapshot");
|
||||
|
||||
/**
|
||||
* Toggle diffing mode on or off.
|
||||
|
@ -4,12 +4,18 @@
|
||||
"use strict";
|
||||
|
||||
const { immutableUpdate, reportException, assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { snapshotState: states, actions } = require("../constants");
|
||||
const { snapshotState: states, actions, viewState } = require("../constants");
|
||||
const { L10N, openFilePicker, createSnapshot } = require("../utils");
|
||||
const telemetry = require("../telemetry");
|
||||
const { selectSnapshot, computeSnapshotData, readSnapshot } = require("./snapshot");
|
||||
const { OS } = require("resource://gre/modules/osfile.jsm");
|
||||
const VALID_EXPORT_STATES = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
|
||||
const {
|
||||
selectSnapshot,
|
||||
computeSnapshotData,
|
||||
readSnapshot,
|
||||
takeCensus,
|
||||
takeTreeMap
|
||||
} = require("./snapshot");
|
||||
const VALID_EXPORT_STATES = [states.SAVED, states.READ];
|
||||
|
||||
exports.pickFileAndExportSnapshot = function (snapshot) {
|
||||
return function* (dispatch, getState) {
|
||||
|
@ -13,5 +13,6 @@ DevToolsModules(
|
||||
'refresh.js',
|
||||
'sizes.js',
|
||||
'snapshot.js',
|
||||
'tree-map-display.js',
|
||||
'view.js',
|
||||
)
|
||||
|
@ -29,6 +29,10 @@ exports.refresh = function (heapWorker) {
|
||||
yield dispatch(snapshot.refreshSelectedDominatorTree(heapWorker));
|
||||
return;
|
||||
|
||||
case viewState.TREE_MAP:
|
||||
yield dispatch(snapshot.refreshSelectedTreeMap(heapWorker));
|
||||
return;
|
||||
|
||||
default:
|
||||
assert(false, `Unexpected view state: ${getState().view}`);
|
||||
}
|
||||
|
@ -9,8 +9,16 @@ const {
|
||||
getSnapshot,
|
||||
createSnapshot,
|
||||
dominatorTreeIsComputed,
|
||||
canTakeCensus
|
||||
} = require("../utils");
|
||||
const { actions, snapshotState: states, viewState, dominatorTreeState } = require("../constants");
|
||||
const {
|
||||
actions,
|
||||
snapshotState: states,
|
||||
viewState,
|
||||
censusState,
|
||||
treeMapState,
|
||||
dominatorTreeState
|
||||
} = require("../constants");
|
||||
const telemetry = require("../telemetry");
|
||||
const view = require("./view");
|
||||
const refresh = require("./refresh");
|
||||
@ -50,7 +58,10 @@ const computeSnapshotData = exports.computeSnapshotData = function(heapWorker, i
|
||||
return;
|
||||
}
|
||||
|
||||
yield dispatch(takeCensus(heapWorker, id));
|
||||
// Decide which type of census to take.
|
||||
const censusTaker = getCurrentCensusTaker(getState().view);
|
||||
yield dispatch(censusTaker(heapWorker, id));
|
||||
|
||||
if (getState().view === viewState.DOMINATOR_TREE) {
|
||||
yield dispatch(computeAndFetchDominatorTree(heapWorker, id));
|
||||
}
|
||||
@ -138,72 +149,139 @@ const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, i
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {snapshotId} id
|
||||
* Census and tree maps both require snapshots. This function shares the logic
|
||||
* of creating snapshots, but is configurable with specific actions for the
|
||||
* individual census types.
|
||||
*
|
||||
* @see {Snapshot} model defined in devtools/client/memory/models.js
|
||||
* @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
|
||||
* @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
|
||||
* @param {getDisplay} Get the display object from the state.
|
||||
* @param {getCensus} Get the census from the snapshot.
|
||||
* @param {beginAction} Action to send at the beginning of a heap snapshot.
|
||||
* @param {endAction} Action to send at the end of a heap snapshot.
|
||||
* @param {errorAction} Action to send if a snapshot has an error.
|
||||
*/
|
||||
const takeCensus = exports.takeCensus = function (heapWorker, id) {
|
||||
return function *(dispatch, getState) {
|
||||
const snapshot = getSnapshot(getState(), id);
|
||||
assert([states.READ, states.SAVED_CENSUS].includes(snapshot.state),
|
||||
`Can only take census of snapshots in READ or SAVED_CENSUS state, found ${snapshot.state}`);
|
||||
function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
|
||||
endAction, errorAction }) {
|
||||
/**
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {snapshotId} id
|
||||
*
|
||||
* @see {Snapshot} model defined in devtools/client/memory/models.js
|
||||
* @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
|
||||
* @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
|
||||
*/
|
||||
return function (heapWorker, id) {
|
||||
return function *(dispatch, getState) {
|
||||
const snapshot = getSnapshot(getState(), id);
|
||||
// Assert that snapshot is in a valid state
|
||||
|
||||
let report, parentMap;
|
||||
let display = getState().censusDisplay;
|
||||
let filter = getState().filter;
|
||||
assert(canTakeCensus(snapshot),
|
||||
`Attempting to take a census when the snapshot is not in a ready state.`);
|
||||
|
||||
// If display, filter and inversion haven't changed, don't do anything.
|
||||
if (censusIsUpToDate(filter, display, snapshot.census)) {
|
||||
return;
|
||||
}
|
||||
let report, parentMap;
|
||||
let display = getDisplay(getState());
|
||||
let filter = getFilter(getState());
|
||||
|
||||
// Keep taking a census if the display changes while our request is in
|
||||
// flight. Recheck that the display used for the census is the same as the
|
||||
// state's display.
|
||||
do {
|
||||
display = getState().censusDisplay;
|
||||
filter = getState().filter;
|
||||
|
||||
dispatch({
|
||||
type: actions.TAKE_CENSUS_START,
|
||||
id,
|
||||
filter,
|
||||
display
|
||||
});
|
||||
|
||||
let opts = display.inverted
|
||||
? { asInvertedTreeNode: true }
|
||||
: { asTreeNode: true };
|
||||
opts.filter = filter || null;
|
||||
|
||||
try {
|
||||
({ report, parentMap } = yield heapWorker.takeCensus(
|
||||
snapshot.path,
|
||||
{ breakdown: display.breakdown },
|
||||
opts));
|
||||
} catch (error) {
|
||||
reportException("takeCensus", error);
|
||||
dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
|
||||
// If display, filter and inversion haven't changed, don't do anything.
|
||||
if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (filter !== getState().filter ||
|
||||
display !== getState().censusDisplay);
|
||||
|
||||
dispatch({
|
||||
type: actions.TAKE_CENSUS_END,
|
||||
id,
|
||||
display,
|
||||
filter,
|
||||
report,
|
||||
parentMap
|
||||
});
|
||||
// Keep taking a census if the display changes while our request is in
|
||||
// flight. Recheck that the display used for the census is the same as the
|
||||
// state's display.
|
||||
do {
|
||||
display = getDisplay(getState());
|
||||
filter = getState().filter;
|
||||
|
||||
telemetry.countCensus({ filter, display });
|
||||
dispatch({
|
||||
type: beginAction,
|
||||
id,
|
||||
filter,
|
||||
display
|
||||
});
|
||||
|
||||
let opts = display.inverted
|
||||
? { asInvertedTreeNode: true }
|
||||
: { asTreeNode: true };
|
||||
|
||||
opts.filter = filter || null;
|
||||
|
||||
try {
|
||||
({ report, parentMap } = yield heapWorker.takeCensus(
|
||||
snapshot.path,
|
||||
{ breakdown: display.breakdown },
|
||||
opts));
|
||||
} catch (error) {
|
||||
reportException("takeCensus", error);
|
||||
dispatch({ type: errorAction, id, error });
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (filter !== getState().filter ||
|
||||
display !== getDisplay(getState()));
|
||||
|
||||
dispatch({
|
||||
type: endAction,
|
||||
id,
|
||||
display,
|
||||
filter,
|
||||
report,
|
||||
parentMap
|
||||
});
|
||||
|
||||
telemetry.countCensus({ filter, display });
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a census.
|
||||
*/
|
||||
const takeCensus = exports.takeCensus = makeTakeCensusTask({
|
||||
getDisplay: (state) => state.censusDisplay,
|
||||
getFilter: (state) => state.filter,
|
||||
getCensus: (snapshot) => snapshot.census,
|
||||
beginAction: actions.TAKE_CENSUS_START,
|
||||
endAction: actions.TAKE_CENSUS_END,
|
||||
errorAction: actions.TAKE_CENSUS_ERROR
|
||||
});
|
||||
|
||||
/**
|
||||
* Take a census for the treemap.
|
||||
*/
|
||||
const takeTreeMap = exports.takeTreeMap = makeTakeCensusTask({
|
||||
getDisplay: (state) => state.treeMapDisplay,
|
||||
getFilter: () => null,
|
||||
getCensus: (snapshot) => snapshot.treeMap,
|
||||
beginAction: actions.TAKE_TREE_MAP_START,
|
||||
endAction: actions.TAKE_TREE_MAP_END,
|
||||
errorAction: actions.TAKE_TREE_MAP_ERROR
|
||||
});
|
||||
|
||||
/**
|
||||
* Define what should be the default mode for taking a census based on the
|
||||
* default view of the tool.
|
||||
*/
|
||||
const defaultCensusTaker = takeTreeMap;
|
||||
|
||||
/**
|
||||
* Pick the default census taker when taking a snapshot. This should be
|
||||
* determined by the current view. If the view doesn't include a census, then
|
||||
* use the default one defined above. Some census information is always needed
|
||||
* to display some basic information about a snapshot.
|
||||
*
|
||||
* @param {string} value from viewState
|
||||
*/
|
||||
const getCurrentCensusTaker = exports.getCurrentCensusTaker = function (currentView) {
|
||||
switch (currentView) {
|
||||
case viewState.TREE_MAP:
|
||||
return takeTreeMap;
|
||||
break;
|
||||
case viewState.CENSUS:
|
||||
return takeCensus;
|
||||
break;
|
||||
}
|
||||
return defaultCensusTaker;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -217,17 +295,43 @@ const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWork
|
||||
let snapshot = getState().snapshots.find(s => s.selected);
|
||||
|
||||
// Intermediate snapshot states will get handled by the task action that is
|
||||
// orchestrating them. For example, if the snapshot's state is
|
||||
// SAVING_CENSUS, then the takeCensus action will keep taking a census until
|
||||
// orchestrating them. For example, if the snapshot census's state is
|
||||
// SAVING, then the takeCensus action will keep taking a census until
|
||||
// the inverted property matches the inverted state. If the snapshot is
|
||||
// still in the process of being saved or read, the takeSnapshotAndCensus
|
||||
// task action will follow through and ensure that a census is taken.
|
||||
if (snapshot && snapshot.state === states.SAVED_CENSUS) {
|
||||
if (snapshot &&
|
||||
(snapshot.census && snapshot.census.state === censusState.SAVED) ||
|
||||
!snapshot.census) {
|
||||
yield dispatch(takeCensus(heapWorker, snapshot.id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh the selected snapshot's tree map data, if need be (for example,
|
||||
* display configuration changed).
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
*/
|
||||
const refreshSelectedTreeMap = exports.refreshSelectedTreeMap = function (heapWorker) {
|
||||
return function*(dispatch, getState) {
|
||||
let snapshot = getState().snapshots.find(s => s.selected);
|
||||
|
||||
// Intermediate snapshot states will get handled by the task action that is
|
||||
// orchestrating them. For example, if the snapshot census's state is
|
||||
// SAVING, then the takeCensus action will keep taking a census until
|
||||
// the inverted property matches the inverted state. If the snapshot is
|
||||
// still in the process of being saved or read, the takeSnapshotAndCensus
|
||||
// task action will follow through and ensure that a census is taken.
|
||||
if (snapshot &&
|
||||
(snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
|
||||
!snapshot.treeMap) {
|
||||
yield dispatch(takeTreeMap(heapWorker, snapshot.id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request that the `HeapAnalysesWorker` compute the dominator tree for the
|
||||
* snapshot with the given `id`.
|
||||
@ -396,18 +500,13 @@ const refreshSelectedDominatorTree = exports.refreshSelectedDominatorTree = func
|
||||
return;
|
||||
}
|
||||
|
||||
switch (snapshot.state) {
|
||||
case states.READ:
|
||||
case states.SAVING_CENSUS:
|
||||
case states.SAVED_CENSUS:
|
||||
if (snapshot.dominatorTree) {
|
||||
yield dispatch(fetchDominatorTree(heapWorker, snapshot.id));
|
||||
} else {
|
||||
yield dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
|
||||
}
|
||||
return;
|
||||
|
||||
default:
|
||||
if (snapshot.state === states.READ) {
|
||||
if (snapshot.dominatorTree) {
|
||||
yield dispatch(fetchDominatorTree(heapWorker, snapshot.id));
|
||||
} else {
|
||||
yield dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
|
||||
}
|
||||
} else {
|
||||
// If there was an error, we can't continue. If we are still saving or
|
||||
// reading the snapshot, then takeSnapshotAndCensus will finish the job
|
||||
// for us.
|
||||
@ -430,14 +529,19 @@ const selectSnapshot = exports.selectSnapshot = function (id) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete all snapshots that are in the SAVED_CENSUS or ERROR state
|
||||
* Delete all snapshots that are in the READ or ERROR state
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
*/
|
||||
const clearSnapshots = exports.clearSnapshots = function (heapWorker) {
|
||||
return function*(dispatch, getState) {
|
||||
let snapshots = getState().snapshots.filter(
|
||||
s => s.state === states.SAVED_CENSUS || s.state === states.ERROR);
|
||||
let snapshots = getState().snapshots.filter(s => {
|
||||
let snapshotReady = s.state === states.READ || s.state === states.ERROR;
|
||||
let censusReady = (s.treeMap && s.treeMap.state === treeMapState.SAVED) ||
|
||||
(s.census && s.census.state === censusState.SAVED);
|
||||
|
||||
return snapshotReady && censusReady
|
||||
});
|
||||
|
||||
let ids = snapshots.map(s => s.id);
|
||||
|
||||
|
37
devtools/client/memory/actions/tree-map-display.js
Normal file
37
devtools/client/memory/actions/tree-map-display.js
Normal file
@ -0,0 +1,37 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { actions } = require("../constants");
|
||||
const { refresh } = require("./refresh");
|
||||
/**
|
||||
* Sets the tree map display as the current display and refreshes the tree map
|
||||
* census.
|
||||
*/
|
||||
exports.setTreeMapAndRefresh = function(heapWorker, display) {
|
||||
return function*(dispatch, getState) {
|
||||
dispatch(setTreeMap(display));
|
||||
yield dispatch(refresh(heapWorker));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears out all cached census data in the snapshots and sets new display data
|
||||
* for tree maps.
|
||||
*
|
||||
* @param {treeMapModel} display
|
||||
*/
|
||||
const setTreeMap = exports.setTreeMap = function(display) {
|
||||
assert(typeof display === "object"
|
||||
&& display
|
||||
&& display.breakdown
|
||||
&& display.breakdown.by,
|
||||
`Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(display)}`);
|
||||
|
||||
return {
|
||||
type: actions.SET_TREE_MAP_DISPLAY,
|
||||
display,
|
||||
};
|
||||
};
|
@ -6,13 +6,16 @@ const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { appinfo } = require("Services");
|
||||
const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
||||
const { censusDisplays, dominatorTreeDisplays, diffingState, viewState } = require("./constants");
|
||||
const { censusDisplays, dominatorTreeDisplays, treeMapDisplays, diffingState, viewState } = require("./constants");
|
||||
const { toggleRecordingAllocationStacks } = require("./actions/allocations");
|
||||
const { setCensusDisplayAndRefresh } = require("./actions/census-display");
|
||||
const { setDominatorTreeDisplayAndRefresh } = require("./actions/dominator-tree-display");
|
||||
const { setTreeMapDisplayAndRefresh } = require("./actions/tree-map-display");
|
||||
|
||||
const {
|
||||
getCustomCensusDisplays,
|
||||
getCustomDominatorTreeDisplays,
|
||||
getCustomTreeMapDisplays,
|
||||
} = require("devtools/client/memory/utils");
|
||||
const {
|
||||
selectSnapshotForDiffingAndRefresh,
|
||||
@ -127,6 +130,18 @@ const MemoryApp = createClass({
|
||||
].concat(custom);
|
||||
},
|
||||
|
||||
_getTreeMapDisplays() {
|
||||
const customDisplays = getCustomTreeMapDisplays();
|
||||
const custom = Object.keys(customDisplays).reduce((arr, key) => {
|
||||
arr.push(customDisplays[key]);
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
return [
|
||||
treeMapDisplays.coarseType
|
||||
].concat(custom);
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
dispatch,
|
||||
@ -173,6 +188,9 @@ const MemoryApp = createClass({
|
||||
dominatorTreeDisplays: this._getDominatorTreeDisplays(),
|
||||
onDominatorTreeDisplayChange: newDisplay =>
|
||||
dispatch(setDominatorTreeDisplayAndRefresh(heapWorker, newDisplay)),
|
||||
treeMapDisplays: this._getTreeMapDisplays(),
|
||||
onTreeMapDisplayChange: newDisplay =>
|
||||
dispatch(setTreeMapDisplayAndRefresh(heapWorker, newDisplay)),
|
||||
onViewChange: v => dispatch(changeViewAndRefresh(v, heapWorker)),
|
||||
}),
|
||||
|
||||
|
@ -8,10 +8,18 @@ const Census = createFactory(require("./census"));
|
||||
const CensusHeader = createFactory(require("./census-header"));
|
||||
const DominatorTree = createFactory(require("./dominator-tree"));
|
||||
const DominatorTreeHeader = createFactory(require("./dominator-tree-header"));
|
||||
const TreeMap = createFactory(require("./tree-map"));
|
||||
const HSplitBox = createFactory(require("devtools/client/shared/components/h-split-box"));
|
||||
const ShortestPaths = createFactory(require("./shortest-paths"));
|
||||
const { getStatusTextFull, L10N } = require("../utils");
|
||||
const { snapshotState: states, diffingState, viewState, dominatorTreeState } = require("../constants");
|
||||
const {
|
||||
snapshotState: states,
|
||||
diffingState,
|
||||
viewState,
|
||||
censusState,
|
||||
treeMapState,
|
||||
dominatorTreeState
|
||||
} = require("../constants");
|
||||
const { snapshot: snapshotModel, diffingModel } = require("../models");
|
||||
|
||||
/**
|
||||
@ -28,11 +36,18 @@ const { snapshot: snapshotModel, diffingModel } = require("../models");
|
||||
function getState(view, snapshot, diffing) {
|
||||
switch (view) {
|
||||
case viewState.CENSUS:
|
||||
return snapshot.state;
|
||||
return snapshot.census
|
||||
? snapshot.census.state
|
||||
: snapshot.state;
|
||||
|
||||
case viewState.DIFFING:
|
||||
return diffing.state;
|
||||
|
||||
case viewState.TREE_MAP:
|
||||
return snapshot.treeMap
|
||||
? snapshot.treeMap.state
|
||||
: snapshot.state;
|
||||
|
||||
case viewState.DOMINATOR_TREE:
|
||||
return snapshot.dominatorTree
|
||||
? snapshot.dominatorTree.state
|
||||
@ -59,8 +74,8 @@ function shouldDisplayStatus(state, view, snapshot) {
|
||||
case states.SAVING:
|
||||
case states.SAVED:
|
||||
case states.READING:
|
||||
case states.READ:
|
||||
case states.SAVING_CENSUS:
|
||||
case censusState.SAVING:
|
||||
case treeMapState.SAVING:
|
||||
case diffingState.SELECTING:
|
||||
case diffingState.TAKING_DIFF:
|
||||
case dominatorTreeState.COMPUTING:
|
||||
@ -110,8 +125,13 @@ function shouldDisplayThrobber(diffing) {
|
||||
* @returns {Error|null}
|
||||
*/
|
||||
function getError(snapshot, diffing) {
|
||||
if (diffing && diffing.state === diffingState.ERROR) {
|
||||
return diffing.error;
|
||||
if (diffing) {
|
||||
if (diffing.state === diffingState.ERROR) {
|
||||
return diffing.error;
|
||||
}
|
||||
if (diffing.census === censusState.ERROR) {
|
||||
return diffing.census.error;
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot) {
|
||||
@ -119,6 +139,14 @@ function getError(snapshot, diffing) {
|
||||
return snapshot.error;
|
||||
}
|
||||
|
||||
if (snapshot.census === censusState.ERROR) {
|
||||
return snapshot.census.error;
|
||||
}
|
||||
|
||||
if (snapshot.treeMap === treeMapState.ERROR) {
|
||||
return snapshot.treeMap.error;
|
||||
}
|
||||
|
||||
if (snapshot.dominatorTree &&
|
||||
snapshot.dominatorTree.state === dominatorTreeState.ERROR) {
|
||||
return snapshot.dominatorTree.error;
|
||||
@ -185,9 +213,16 @@ const Heap = module.exports = createClass({
|
||||
const census = view === viewState.CENSUS
|
||||
? snapshot.census
|
||||
: diffing.census;
|
||||
if (!census) {
|
||||
return this._renderStatus(state, statusText, diffing);
|
||||
}
|
||||
return this._renderCensus(state, census, diffing, onViewSourceInDebugger);
|
||||
}
|
||||
|
||||
if (view === viewState.TREE_MAP) {
|
||||
return this._renderTreeMap(state, snapshot.treeMap);
|
||||
}
|
||||
|
||||
assert(view === viewState.DOMINATOR_TREE,
|
||||
"If we aren't in progress, looking at a census, or diffing, then we " +
|
||||
"must be looking at a dominator tree");
|
||||
@ -288,6 +323,13 @@ const Heap = module.exports = createClass({
|
||||
return this._renderHeapView(state, ...contents);
|
||||
},
|
||||
|
||||
_renderTreeMap(state, treeMap) {
|
||||
return this._renderHeapView(
|
||||
state,
|
||||
TreeMap({ treeMap })
|
||||
);
|
||||
},
|
||||
|
||||
_renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
|
||||
const tree = dom.div(
|
||||
{
|
||||
|
@ -3,6 +3,10 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += [
|
||||
'tree-map',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'census-header.js',
|
||||
'census-tree-item.js',
|
||||
@ -15,4 +19,5 @@ DevToolsModules(
|
||||
'shortest-paths.js',
|
||||
'snapshot-list-item.js',
|
||||
'toolbar.js',
|
||||
'tree-map.js',
|
||||
)
|
||||
|
@ -9,9 +9,15 @@ const {
|
||||
getSnapshotTitle,
|
||||
getSnapshotTotals,
|
||||
getStatusText,
|
||||
snapshotIsDiffable
|
||||
snapshotIsDiffable,
|
||||
getSavedCensus
|
||||
} = require("../utils");
|
||||
const { snapshotState: states, diffingState } = require("../constants");
|
||||
const {
|
||||
snapshotState: states,
|
||||
diffingState,
|
||||
censusState,
|
||||
treeMapState
|
||||
} = require("../constants");
|
||||
const { snapshot: snapshotModel } = require("../models");
|
||||
|
||||
const SnapshotListItem = module.exports = createClass({
|
||||
@ -61,14 +67,21 @@ const SnapshotListItem = module.exports = createClass({
|
||||
}
|
||||
|
||||
let details;
|
||||
if (!selectedForDiffing && snapshot.state === states.SAVED_CENSUS) {
|
||||
let { bytes } = getSnapshotTotals(snapshot.census);
|
||||
let formatBytes = L10N.getFormatStr("aggregate.mb", L10N.numberWithDecimals(bytes / 1000000, 2));
|
||||
if (!selectedForDiffing) {
|
||||
// See if a tree map or census is in the read state.
|
||||
let census = getSavedCensus(snapshot);
|
||||
|
||||
details = dom.span({ className: "snapshot-totals" },
|
||||
dom.span({ className: "total-bytes" }, formatBytes)
|
||||
);
|
||||
} else {
|
||||
// If there is census data, fill in the total bytes.
|
||||
if (census) {
|
||||
let { bytes } = getSnapshotTotals(census);
|
||||
let formatBytes = L10N.getFormatStr("aggregate.mb", L10N.numberWithDecimals(bytes / 1000000, 2));
|
||||
|
||||
details = dom.span({ className: "snapshot-totals" },
|
||||
dom.span({ className: "total-bytes" }, formatBytes)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!details) {
|
||||
details = dom.span({ className: "snapshot-state" }, statusText);
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,10 @@ module.exports = createClass({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
onDominatorTreeDisplayChange: PropTypes.func.isRequired,
|
||||
treeMapDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
onTreeMapDisplayChange: PropTypes.func.isRequired,
|
||||
snapshots: PropTypes.arrayOf(models.snapshot).isRequired,
|
||||
},
|
||||
|
||||
@ -43,6 +47,8 @@ module.exports = createClass({
|
||||
censusDisplays,
|
||||
dominatorTreeDisplays,
|
||||
onDominatorTreeDisplayChange,
|
||||
treeMapDisplays,
|
||||
onTreeMapDisplayChange,
|
||||
onToggleRecordAllocationStacks,
|
||||
allocations,
|
||||
filterString,
|
||||
@ -100,6 +106,44 @@ module.exports = createClass({
|
||||
value: filterString || undefined,
|
||||
})
|
||||
);
|
||||
} else if (view == viewState.TREE_MAP) {
|
||||
assert(treeMapDisplays.length >= 1,
|
||||
"Should always have at least one tree map display");
|
||||
|
||||
// Only show the dropdown if there are multiple display options
|
||||
viewToolbarOptions = treeMapDisplays.length > 1
|
||||
? dom.div(
|
||||
{
|
||||
className: "toolbar-group"
|
||||
},
|
||||
|
||||
dom.label(
|
||||
{
|
||||
className: "display-by",
|
||||
title: L10N.getStr("toolbar.displayBy.tooltip"),
|
||||
},
|
||||
L10N.getStr("toolbar.displayBy"),
|
||||
dom.select(
|
||||
{
|
||||
id: "select-tree-map-display",
|
||||
onChange: e => {
|
||||
const newDisplay =
|
||||
treeMapDisplays.find(b => b.displayName === e.target.value);
|
||||
onTreeMapDisplayChange(newDisplay);
|
||||
},
|
||||
},
|
||||
treeMapDisplays.map(({ tooltip, displayName }) => dom.option(
|
||||
{
|
||||
key: `tree-map-display-${displayName}`,
|
||||
value: displayName,
|
||||
title: tooltip,
|
||||
},
|
||||
displayName
|
||||
))
|
||||
)
|
||||
)
|
||||
)
|
||||
: null;
|
||||
} else {
|
||||
assert(view === viewState.DOMINATOR_TREE);
|
||||
|
||||
@ -147,8 +191,16 @@ module.exports = createClass({
|
||||
{
|
||||
id: "select-view",
|
||||
onChange: e => onViewChange(e.target.value),
|
||||
defaultValue: viewState.CENSUS,
|
||||
defaultValue: view,
|
||||
},
|
||||
dom.option(
|
||||
{
|
||||
value: viewState.TREE_MAP,
|
||||
title: L10N.getStr("toolbar.view.treemap.tooltip"),
|
||||
selected: view
|
||||
},
|
||||
L10N.getStr("toolbar.view.treemap")
|
||||
),
|
||||
dom.option(
|
||||
{
|
||||
value: viewState.CENSUS,
|
||||
|
71
devtools/client/memory/components/tree-map.js
Normal file
71
devtools/client/memory/components/tree-map.js
Normal file
@ -0,0 +1,71 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
|
||||
const { treeMapModel } = require("../models");
|
||||
const startVisualization = require("./tree-map/start");
|
||||
|
||||
module.exports = createClass({
|
||||
propTypes: {
|
||||
treeMap: treeMapModel
|
||||
},
|
||||
|
||||
displayName: "TreeMap",
|
||||
|
||||
getInitialState() {
|
||||
return {};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
const { treeMap } = this.props;
|
||||
if (treeMap && treeMap.report) {
|
||||
this._startVisualization();
|
||||
}
|
||||
},
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const oldTreeMap = this.props.treeMap;
|
||||
const newTreeMap = nextProps.treeMap;
|
||||
return oldTreeMap !== newTreeMap;
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this._stopVisualization();
|
||||
|
||||
if (this.props.treeMap && this.props.treeMap.report) {
|
||||
this._startVisualization();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.stopVisualization) {
|
||||
this.state.stopVisualization();
|
||||
}
|
||||
},
|
||||
|
||||
_stopVisualization() {
|
||||
if (this.state.stopVisualization) {
|
||||
this.state.stopVisualization();
|
||||
this.setState({ stopVisualization: null });
|
||||
}
|
||||
},
|
||||
|
||||
_startVisualization() {
|
||||
const { container } = this.refs;
|
||||
const { report } = this.props.treeMap;
|
||||
const stopVisualization = startVisualization(container, report);
|
||||
this.setState({ stopVisualization });
|
||||
},
|
||||
|
||||
render() {
|
||||
return dom.div(
|
||||
{
|
||||
ref: "container",
|
||||
className: "tree-map-container"
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
134
devtools/client/memory/components/tree-map/canvas-utils.js
Normal file
134
devtools/client/memory/components/tree-map/canvas-utils.js
Normal file
@ -0,0 +1,134 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Create 2 canvases and contexts for drawing onto, 1 main canvas, and 1 zoom
|
||||
* canvas. The main canvas dimensions match the parent div, but the CSS can be
|
||||
* transformed to be zoomed and dragged around (potentially creating a blurry
|
||||
* canvas once zoomed in). The zoom canvas is a zoomed in section that matches
|
||||
* the parent div's dimensions and is kept in place through CSS. A zoomed in
|
||||
* view of the visualization is drawn onto this canvas, providing a crisp zoomed
|
||||
* in view of the tree map.
|
||||
*/
|
||||
const { debounce } = require("sdk/lang/functional");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const FULLSCREEN_STYLE = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the canvases, resize handlers, and return references to them all
|
||||
*
|
||||
* @param {HTMLDivElement} parentEl
|
||||
* @param {Number} debounceRate
|
||||
* @return {Object}
|
||||
*/
|
||||
function Canvases(parentEl, debounceRate) {
|
||||
EventEmitter.decorate(this)
|
||||
this.container = createContainingDiv(parentEl);
|
||||
|
||||
// This canvas contains all of the treemap
|
||||
this.main = createCanvas(this.container, "main");
|
||||
// This canvas contains only the zoomed in portion, overlaying the main canvas
|
||||
this.zoom = createCanvas(this.container, "zoom");
|
||||
|
||||
this.removeHandlers = handleResizes(this, debounceRate);
|
||||
}
|
||||
|
||||
Canvases.prototype = {
|
||||
|
||||
/**
|
||||
* Remove the handlers and elements
|
||||
*
|
||||
* @return {type} description
|
||||
*/
|
||||
destroy : function() {
|
||||
this.removeHandlers();
|
||||
this.container.removeChild(this.main.canvas);
|
||||
this.container.removeChild(this.zoom.canvas);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Canvases;
|
||||
|
||||
/**
|
||||
* Create the containing div
|
||||
*
|
||||
* @param {HTMLDivElement} parentEl
|
||||
* @return {HTMLDivElement}
|
||||
*/
|
||||
function createContainingDiv(parentEl) {
|
||||
let div = parentEl.ownerDocument.createElementNS(HTML_NS, "div");
|
||||
Object.assign(div.style, FULLSCREEN_STYLE);
|
||||
parentEl.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a canvas and context
|
||||
*
|
||||
* @param {HTMLDivElement} container
|
||||
* @param {String} className
|
||||
* @return {Object} { canvas, ctx }
|
||||
*/
|
||||
function createCanvas(container, className) {
|
||||
let window = container.ownerDocument.defaultView;
|
||||
let canvas = container.ownerDocument.createElementNS(HTML_NS, "canvas");
|
||||
container.appendChild(canvas);
|
||||
canvas.width = container.offsetWidth * window.devicePixelRatio;
|
||||
canvas.height = container.offsetHeight * window.devicePixelRatio;
|
||||
canvas.className = className;
|
||||
|
||||
Object.assign(canvas.style, FULLSCREEN_STYLE, {
|
||||
pointerEvents: "none"
|
||||
});
|
||||
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
return { canvas, ctx };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the canvases' resolutions, and fires out the onResize callback
|
||||
*
|
||||
* @param {HTMLDivElement} container
|
||||
* @param {Object} canvases
|
||||
* @param {Number} debounceRate
|
||||
*/
|
||||
function handleResizes(canvases, debounceRate) {
|
||||
let { container, main, zoom } = canvases;
|
||||
let window = container.ownerDocument.defaultView;
|
||||
|
||||
function resize() {
|
||||
let width = container.offsetWidth * window.devicePixelRatio;
|
||||
let height = container.offsetHeight * window.devicePixelRatio;
|
||||
|
||||
main.canvas.width = width;
|
||||
main.canvas.height = height;
|
||||
zoom.canvas.width = width;
|
||||
zoom.canvas.height = height;
|
||||
|
||||
canvases.emit('resize');
|
||||
}
|
||||
|
||||
// Tests may not need debouncing
|
||||
let debouncedResize = debounceRate > 0
|
||||
? debounce(resize, debounceRate)
|
||||
: resize;
|
||||
|
||||
window.addEventListener("resize", debouncedResize, false);
|
||||
resize();
|
||||
|
||||
return function removeResizeHandlers() {
|
||||
window.removeEventListener("resize", debouncedResize, false);
|
||||
};
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Color the boxes in the treemap
|
||||
*/
|
||||
|
||||
const TYPES = [ "objects", "other", "strings", "scripts" ];
|
||||
|
||||
// The factors determine how much the hue shifts
|
||||
const TYPE_FACTOR = TYPES.length * 3;
|
||||
const DEPTH_FACTOR = -10;
|
||||
const H = 0.5;
|
||||
const S = 0.6;
|
||||
const L = 0.9;
|
||||
|
||||
/**
|
||||
* Recursively find the index of the coarse type of a node
|
||||
*
|
||||
* @param {Object} node
|
||||
* d3 treemap
|
||||
* @return {Integer}
|
||||
* index
|
||||
*/
|
||||
function findCoarseTypeIndex(node) {
|
||||
let index = TYPES.indexOf(node.name);
|
||||
|
||||
if (node.parent) {
|
||||
return index === -1 ? findCoarseTypeIndex(node.parent) : index;
|
||||
}
|
||||
|
||||
return TYPES.indexOf("other");
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide a color value for depth to be used in the HSL computation
|
||||
*
|
||||
* @param {Object} node
|
||||
* @return {Number}
|
||||
*/
|
||||
function depthColorFactor(node) {
|
||||
return Math.min(1, node.depth / DEPTH_FACTOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide a color value for type to be used in the HSL computation
|
||||
*
|
||||
* @param {Object} node
|
||||
* @return {Number}
|
||||
*/
|
||||
function typeColorFactor(node) {
|
||||
return findCoarseTypeIndex(node) / TYPE_FACTOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Color a node
|
||||
*
|
||||
* @param {Object} node
|
||||
* @return {Array} HSL values ranged 0-1
|
||||
*/
|
||||
module.exports = function colorCoarseType(node) {
|
||||
let h = Math.min(1, H + typeColorFactor(node));
|
||||
let s = Math.min(1, S);
|
||||
let l = Math.min(1, L + depthColorFactor(node));
|
||||
|
||||
return [h, s, l];
|
||||
};
|
330
devtools/client/memory/components/tree-map/drag-zoom.js
Normal file
330
devtools/client/memory/components/tree-map/drag-zoom.js
Normal file
@ -0,0 +1,330 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { debounce } = require("sdk/lang/functional");
|
||||
const { lerp } = require("devtools/client/memory/utils");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
const LERP_SPEED = 0.5;
|
||||
const ZOOM_SPEED = 0.01;
|
||||
const TRANSLATE_EPSILON = 1;
|
||||
const ZOOM_EPSILON = 0.001;
|
||||
const LINE_SCROLL_MODE = 1;
|
||||
const SCROLL_LINE_SIZE = 15;
|
||||
|
||||
/**
|
||||
* DragZoom is a constructor that contains the state of the current dragging and
|
||||
* zooming behavior. It sets the scrolling and zooming behaviors.
|
||||
*
|
||||
* @param {HTMLElement} container description
|
||||
* The container for the canvases
|
||||
*/
|
||||
function DragZoom(container, debounceRate, requestAnimationFrame) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.isDragging = false;
|
||||
|
||||
// The current mouse position
|
||||
this.mouseX = container.offsetWidth / 2;
|
||||
this.mouseY = container.offsetHeight / 2;
|
||||
|
||||
// The total size of the visualization after being zoomed, in pixels
|
||||
this.zoomedWidth = container.offsetWidth;
|
||||
this.zoomedHeight = container.offsetHeight;
|
||||
|
||||
// How much the visualization has been zoomed in
|
||||
this.zoom = 0;
|
||||
|
||||
// The offset of visualization from the container. This is applied after
|
||||
// the zoom, and the visualization by default is centered
|
||||
this.translateX = 0;
|
||||
this.translateY = 0;
|
||||
|
||||
// The size of the offset between the top/left of the container, and the
|
||||
// top/left of the containing element. This value takes into account
|
||||
// the devicePixelRatio for canvas draws.
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
|
||||
// The smoothed values that are animated and eventually match the target
|
||||
// values. The values are updated by the update loop
|
||||
this.smoothZoom = 0;
|
||||
this.smoothTranslateX = 0;
|
||||
this.smoothTranslateY = 0;
|
||||
|
||||
// Add the constant values for testing purposes
|
||||
this.ZOOM_SPEED = ZOOM_SPEED;
|
||||
this.ZOOM_EPSILON = ZOOM_EPSILON;
|
||||
|
||||
let update = createUpdateLoop(container, this, requestAnimationFrame);
|
||||
|
||||
this.destroy = setHandlers(this, container, update, debounceRate);
|
||||
}
|
||||
|
||||
module.exports = DragZoom;
|
||||
|
||||
/**
|
||||
* Returns an update loop. This loop smoothly updates the visualization when
|
||||
* actions are performed. Once the animations have reached their target values
|
||||
* the animation loop is stopped.
|
||||
*
|
||||
* Any value in the `dragZoom` object that starts with "smooth" is the
|
||||
* smoothed version of a value that is interpolating toward the target value.
|
||||
* For instance `dragZoom.smoothZoom` approaches `dragZoom.zoom` on each
|
||||
* iteration of the update loop until it's sufficiently close as defined by
|
||||
* the epsilon values.
|
||||
*
|
||||
* Only these smoothed values and the container CSS are updated by the loop.
|
||||
*
|
||||
* @param {HTMLDivElement} container
|
||||
* @param {Object} dragZoom
|
||||
* The values that represent the current dragZoom state
|
||||
* @param {Function} requestAnimationFrame
|
||||
*/
|
||||
function createUpdateLoop(container, dragZoom, requestAnimationFrame) {
|
||||
let isLooping = false;
|
||||
|
||||
function update() {
|
||||
let isScrollChanging = (
|
||||
Math.abs(dragZoom.smoothZoom - dragZoom.zoom) > ZOOM_EPSILON
|
||||
);
|
||||
let isTranslateChanging = (
|
||||
Math.abs(dragZoom.smoothTranslateX - dragZoom.translateX)
|
||||
> TRANSLATE_EPSILON ||
|
||||
Math.abs(dragZoom.smoothTranslateY - dragZoom.translateY)
|
||||
> TRANSLATE_EPSILON
|
||||
);
|
||||
|
||||
isLooping = isScrollChanging || isTranslateChanging;
|
||||
|
||||
if (isScrollChanging) {
|
||||
dragZoom.smoothZoom = lerp(dragZoom.smoothZoom, dragZoom.zoom,
|
||||
LERP_SPEED);
|
||||
} else {
|
||||
dragZoom.smoothZoom = dragZoom.zoom;
|
||||
}
|
||||
|
||||
if (isTranslateChanging) {
|
||||
dragZoom.smoothTranslateX = lerp(dragZoom.smoothTranslateX,
|
||||
dragZoom.translateX, LERP_SPEED);
|
||||
dragZoom.smoothTranslateY = lerp(dragZoom.smoothTranslateY,
|
||||
dragZoom.translateY, LERP_SPEED);
|
||||
} else {
|
||||
dragZoom.smoothTranslateX = dragZoom.translateX;
|
||||
dragZoom.smoothTranslateY = dragZoom.translateY;
|
||||
}
|
||||
|
||||
let zoom = 1 + dragZoom.smoothZoom;
|
||||
let x = dragZoom.smoothTranslateX;
|
||||
let y = dragZoom.smoothTranslateY;
|
||||
container.style.transform = `translate(${x}px, ${y}px) scale(${zoom})`;
|
||||
|
||||
if (isLooping) {
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
}
|
||||
|
||||
// Go ahead and start the update loop
|
||||
update();
|
||||
|
||||
return function restartLoopingIfStopped() {
|
||||
if (!isLooping) {
|
||||
update();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the various event listeners and return a function to remove them
|
||||
*
|
||||
* @param {Object} dragZoom
|
||||
* @param {HTMLElement} container
|
||||
* @param {Function} update
|
||||
* @return {Function} The function to remove the handlers
|
||||
*/
|
||||
function setHandlers(dragZoom, container, update, debounceRate) {
|
||||
let emitChanged = debounce(() => dragZoom.emit("change"), debounceRate);
|
||||
|
||||
let removeDragHandlers =
|
||||
setDragHandlers(container, dragZoom, emitChanged, update);
|
||||
let removeScrollHandlers =
|
||||
setScrollHandlers(container, dragZoom, emitChanged, update);
|
||||
|
||||
return function removeHandlers() {
|
||||
removeDragHandlers();
|
||||
removeScrollHandlers();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets handlers for when the user drags on the canvas. It will update dragZoom
|
||||
* object with new translate and offset values.
|
||||
*
|
||||
* @param {HTMLElement} container
|
||||
* @param {Object} dragZoom
|
||||
* @param {Function} changed
|
||||
* @param {Function} update
|
||||
*/
|
||||
function setDragHandlers(container, dragZoom, emitChanged, update) {
|
||||
let parentEl = container.parentElement;
|
||||
|
||||
function startDrag() {
|
||||
dragZoom.isDragging = true;
|
||||
container.style.cursor = "grabbing";
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
dragZoom.isDragging = false;
|
||||
container.style.cursor = "grab";
|
||||
}
|
||||
|
||||
function drag(event) {
|
||||
let prevMouseX = dragZoom.mouseX;
|
||||
let prevMouseY = dragZoom.mouseY;
|
||||
|
||||
dragZoom.mouseX = event.clientX - parentEl.offsetLeft;
|
||||
dragZoom.mouseY = event.clientY - parentEl.offsetTop;
|
||||
|
||||
if (!dragZoom.isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragZoom.translateX += dragZoom.mouseX - prevMouseX;
|
||||
dragZoom.translateY += dragZoom.mouseY - prevMouseY;
|
||||
|
||||
keepInView(container, dragZoom);
|
||||
|
||||
emitChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
parentEl.addEventListener("mousedown", startDrag, false);
|
||||
parentEl.addEventListener("mouseup", stopDrag, false);
|
||||
parentEl.addEventListener("mouseout", stopDrag, false);
|
||||
parentEl.addEventListener("mousemove", drag, false);
|
||||
|
||||
return function removeListeners() {
|
||||
parentEl.removeEventListener("mousedown", startDrag, false);
|
||||
parentEl.removeEventListener("mouseup", stopDrag, false);
|
||||
parentEl.removeEventListener("mouseout", stopDrag, false);
|
||||
parentEl.removeEventListener("mousemove", drag, false);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the handlers for when the user scrolls. It updates the dragZoom object
|
||||
* and keeps the canvases all within the view. After changing values update
|
||||
* loop is called, and the changed event is emitted.
|
||||
*
|
||||
* @param {HTMLDivElement} container
|
||||
* @param {Object} dragZoom
|
||||
* @param {Function} changed
|
||||
* @param {Function} update
|
||||
*/
|
||||
function setScrollHandlers(container, dragZoom, emitChanged, update) {
|
||||
let window = container.ownerDocument.defaultView;
|
||||
|
||||
function handleWheel(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (dragZoom.isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the zoom level
|
||||
let scrollDelta = getScrollDelta(event, window);
|
||||
let prevZoom = dragZoom.zoom;
|
||||
dragZoom.zoom = Math.max(0, dragZoom.zoom - scrollDelta * ZOOM_SPEED);
|
||||
let deltaZoom = dragZoom.zoom - prevZoom;
|
||||
|
||||
// Calculate the updated width and height
|
||||
let prevWidth = container.offsetWidth * (1 + prevZoom);
|
||||
let prevHeight = container.offsetHeight * (1 + prevZoom);
|
||||
dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
|
||||
dragZoom.height = container.offsetHeight * (1 + dragZoom.zoom);
|
||||
let deltaWidth = dragZoom.zoomedWidth - prevWidth;
|
||||
let deltaHeight = dragZoom.height - prevHeight;
|
||||
|
||||
// The ratio of where the center of the zoom is in regards to the total
|
||||
// zoomed width/height
|
||||
let ratioZoomX = (dragZoom.zoomedWidth / 2 - dragZoom.translateX)
|
||||
/ dragZoom.zoomedWidth;
|
||||
let ratioZoomY = (dragZoom.height / 2 - dragZoom.translateY)
|
||||
/ dragZoom.height;
|
||||
|
||||
// Distribute the change in width and height based on the above ratio
|
||||
dragZoom.translateX -= lerp(-deltaWidth / 2, deltaWidth / 2, ratioZoomX);
|
||||
dragZoom.translateY -= lerp(-deltaHeight / 2, deltaHeight / 2, ratioZoomY);
|
||||
|
||||
// The ratio of mouse position to total zoomeed width/height, ranged [-1, 1]
|
||||
let mouseRatioX, mouseRatioY;
|
||||
if (deltaZoom > 0) {
|
||||
// Zoom in towards the mouse
|
||||
mouseRatioX = 2 * (dragZoom.mouseX - container.offsetWidth / 2)
|
||||
/ dragZoom.zoomedWidth;
|
||||
mouseRatioY = 2 * (dragZoom.mouseY - container.offsetHeight / 2)
|
||||
/ dragZoom.height;
|
||||
} else {
|
||||
// Zoom out centering the screen
|
||||
mouseRatioX = 0;
|
||||
mouseRatioY = 0;
|
||||
}
|
||||
// Adjust the translate to zoom towards the mouse
|
||||
dragZoom.translateX -= deltaWidth * mouseRatioX;
|
||||
dragZoom.translateY -= deltaHeight * mouseRatioY;
|
||||
|
||||
// Keep the canvas in range of the container
|
||||
keepInView(container, dragZoom);
|
||||
emitChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
window.addEventListener("wheel", handleWheel, false);
|
||||
|
||||
return function removeListener() {
|
||||
window.removeEventListener("wheel", handleWheel, false);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Account for the various mouse wheel event types, per pixel or per line
|
||||
*
|
||||
* @param {WheelEvent} event
|
||||
* @param {Window} window
|
||||
* @return {Number} The scroll size in pixels
|
||||
*/
|
||||
function getScrollDelta(event, window) {
|
||||
if (event.deltaMode === LINE_SCROLL_MODE) {
|
||||
// Update by a fixed arbitrary value to normalize scroll types
|
||||
return event.deltaY * SCROLL_LINE_SIZE;
|
||||
}
|
||||
return event.deltaY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the dragging and zooming within the view by updating the values in the
|
||||
* `dragZoom` object.
|
||||
*
|
||||
* @param {HTMLDivElement} container
|
||||
* @param {Object} dragZoom
|
||||
*/
|
||||
function keepInView(container, dragZoom) {
|
||||
let { devicePixelRatio } = container.ownerDocument.defaultView;
|
||||
let overdrawX = (dragZoom.zoomedWidth - container.offsetWidth) / 2;
|
||||
let overdrawY = (dragZoom.height - container.offsetHeight) / 2;
|
||||
|
||||
dragZoom.translateX = Math.max(-overdrawX,
|
||||
Math.min(overdrawX, dragZoom.translateX));
|
||||
dragZoom.translateY = Math.max(-overdrawY,
|
||||
Math.min(overdrawY, dragZoom.translateY));
|
||||
|
||||
dragZoom.offsetX = devicePixelRatio * (
|
||||
(dragZoom.zoomedWidth - container.offsetWidth) / 2 - dragZoom.translateX
|
||||
);
|
||||
dragZoom.offsetY = devicePixelRatio * (
|
||||
(dragZoom.height - container.offsetHeight) / 2 - dragZoom.translateY
|
||||
);
|
||||
}
|
285
devtools/client/memory/components/tree-map/draw.js
Normal file
285
devtools/client/memory/components/tree-map/draw.js
Normal file
@ -0,0 +1,285 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
/**
|
||||
* Draw the treemap into the provided canvases using the 2d context. The treemap
|
||||
* layout is computed with d3. There are 2 canvases provided, each matching
|
||||
* the resolution of the window. The main canvas is a fully drawn version of
|
||||
* the treemap that is positioned and zoomed using css. It gets blurry the more
|
||||
* you zoom in as it doesn't get redrawn when zooming. The zoom canvas is
|
||||
* repositioned absolutely after every change in the dragZoom object, and then
|
||||
* redrawn to provide a full-resolution (non-blurry) view of zoomed in segment
|
||||
* of the treemap.
|
||||
*/
|
||||
|
||||
const colorCoarseType = require("./color-coarse-type");
|
||||
const {
|
||||
hslToStyle,
|
||||
formatAbbreviatedBytes,
|
||||
L10N
|
||||
} = require("devtools/client/memory/utils");
|
||||
|
||||
// A constant fully zoomed out dragZoom object for the main canvas
|
||||
const NO_SCROLL = {
|
||||
translateX: 0,
|
||||
translateY: 0,
|
||||
zoom: 0,
|
||||
offsetX: 0,
|
||||
offsetY: 0
|
||||
};
|
||||
|
||||
// Drawing constants
|
||||
const ELLIPSIS = "...";
|
||||
const TEXT_MARGIN = 2;
|
||||
const TEXT_COLOR = "#000000";
|
||||
const TEXT_LIGHT_COLOR = "rgba(0,0,0,0.5)";
|
||||
const LINE_WIDTH = 1;
|
||||
const FONT_SIZE = 10;
|
||||
const FONT_LINE_HEIGHT = 2;
|
||||
const PADDING = [5, 5, 5, 5];
|
||||
const COUNT_LABEL = L10N.getStr("tree-map.node-count");
|
||||
|
||||
/**
|
||||
* Setup and start drawing the treemap visualization
|
||||
*
|
||||
* @param {Object} report
|
||||
* @param {Object} canvases
|
||||
* A CanvasUtils object that contains references to the main and zoom
|
||||
* canvases and contexts
|
||||
* @param {Object} dragZoom
|
||||
* A DragZoom object representing the current state of the dragging
|
||||
* and zooming behavior
|
||||
*/
|
||||
exports.setupDraw = function(report, canvases, dragZoom) {
|
||||
let getTreemap = configureD3Treemap.bind(null, canvases.main.canvas);
|
||||
|
||||
let treemap, nodes;
|
||||
|
||||
function drawFullTreemap() {
|
||||
treemap = getTreemap();
|
||||
nodes = treemap(report);
|
||||
drawTreemap(canvases.main, nodes, NO_SCROLL);
|
||||
drawTreemap(canvases.zoom, nodes, dragZoom);
|
||||
}
|
||||
|
||||
function drawZoomedTreemap() {
|
||||
drawTreemap(canvases.zoom, nodes, dragZoom);
|
||||
positionZoomedCanvas(canvases.zoom.canvas, dragZoom);
|
||||
}
|
||||
|
||||
drawFullTreemap();
|
||||
canvases.on("resize", drawFullTreemap);
|
||||
dragZoom.on("change", drawZoomedTreemap);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a configured d3 treemap function
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
* @return {Function}
|
||||
*/
|
||||
const configureD3Treemap = exports.configureD3Treemap = function(canvas) {
|
||||
let window = canvas.ownerDocument.defaultView;
|
||||
let ratio = window.devicePixelRatio;
|
||||
let treemap = window.d3.layout.treemap()
|
||||
.size([canvas.width, canvas.height])
|
||||
.sticky(true)
|
||||
.padding([
|
||||
(PADDING[0] + FONT_SIZE) * ratio,
|
||||
PADDING[1] * ratio,
|
||||
PADDING[2] * ratio,
|
||||
PADDING[3] * ratio,
|
||||
])
|
||||
.value(d => d.bytes);
|
||||
|
||||
/**
|
||||
* Create treemap nodes from a census report that are sorted by depth
|
||||
*
|
||||
* @param {Object} report
|
||||
* @return {Array} An array of d3 treemap nodes
|
||||
* // https://github.com/mbostock/d3/wiki/Treemap-Layout
|
||||
* parent - the parent node, or null for the root.
|
||||
* children - the array of child nodes, or null for leaf nodes.
|
||||
* value - the node value, as returned by the value accessor.
|
||||
* depth - the depth of the node, starting at 0 for the root.
|
||||
* area - the computed pixel area of this node.
|
||||
* x - the minimum x-coordinate of the node position.
|
||||
* y - the minimum y-coordinate of the node position.
|
||||
* z - the orientation of this cell’s subdivision, if any.
|
||||
* dx - the x-extent of the node position.
|
||||
* dy - the y-extent of the node position.
|
||||
*/
|
||||
return function depthSortedNodes(report) {
|
||||
let nodes = treemap(report);
|
||||
nodes.sort((a, b) => a.depth - b.depth);
|
||||
return nodes;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw the text, cut it in half every time it doesn't fit until it fits or
|
||||
* it's smaller than the "..." text.
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {Number} x
|
||||
* the position of the text
|
||||
* @param {Number} y
|
||||
* the position of the text
|
||||
* @param {Number} innerWidth
|
||||
* the inner width of the containing treemap cell
|
||||
* @param {Text} name
|
||||
*/
|
||||
const drawTruncatedName = exports.drawTruncatedName = function(ctx, x, y,
|
||||
innerWidth,
|
||||
name) {
|
||||
let truncated = name.substr(0, Math.floor(name.length / 2));
|
||||
let formatted = truncated + ELLIPSIS;
|
||||
|
||||
if (ctx.measureText(formatted).width > innerWidth) {
|
||||
drawTruncatedName(ctx, x, y, innerWidth, truncated);
|
||||
} else {
|
||||
ctx.fillText(formatted, x, y);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fit and draw the text in a node with the following strategies to shrink
|
||||
* down the text size:
|
||||
*
|
||||
* Function 608KB 9083 count
|
||||
* Function
|
||||
* Func...
|
||||
* Fu...
|
||||
* ...
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {Object} node
|
||||
* @param {Number} borderWidth
|
||||
* @param {Number} ratio
|
||||
* @param {Object} dragZoom
|
||||
*/
|
||||
const drawText = exports.drawText = function(ctx, node, borderWidth, ratio,
|
||||
dragZoom) {
|
||||
let { dx, dy, name, totalBytes, totalCount } = node;
|
||||
let scale = dragZoom.zoom + 1;
|
||||
dx *= scale;
|
||||
dy *= scale;
|
||||
|
||||
// Start checking to see how much text we can fit in, optimizing for the
|
||||
// common case of lots of small leaf nodes
|
||||
if (FONT_SIZE * FONT_LINE_HEIGHT < dy) {
|
||||
let margin = borderWidth(node) * 1.5 + ratio * TEXT_MARGIN;
|
||||
let x = margin + node.x * scale - dragZoom.offsetX;
|
||||
let y = margin + node.y * scale - dragZoom.offsetY;
|
||||
let innerWidth = dx - margin * 2;
|
||||
let nameSize = ctx.measureText(name).width;
|
||||
|
||||
if (ctx.measureText(ELLIPSIS).width > innerWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.fillStyle = TEXT_COLOR;
|
||||
|
||||
if (nameSize > innerWidth) {
|
||||
// The name is too long - halve the name as an expediant way to shorten it
|
||||
drawTruncatedName(ctx, x, y, innerWidth, name);
|
||||
} else {
|
||||
let bytesFormatted = formatAbbreviatedBytes(totalBytes);
|
||||
let countFormatted = `${totalCount} ${COUNT_LABEL}`;
|
||||
let byteSize = ctx.measureText(bytesFormatted).width;
|
||||
let countSize = ctx.measureText(countFormatted).width;
|
||||
let spaceSize = ctx.measureText(" ").width;
|
||||
|
||||
if (nameSize + byteSize + countSize + spaceSize * 3 > innerWidth) {
|
||||
// The full name will fit
|
||||
ctx.fillText(`${name}`, x, y);
|
||||
} else {
|
||||
// The full name plus the byte information will fit
|
||||
ctx.fillText(name, x, y);
|
||||
ctx.fillStyle = TEXT_LIGHT_COLOR;
|
||||
ctx.fillText(`${bytesFormatted} ${countFormatted}`,
|
||||
x + nameSize + spaceSize, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw a box given a node
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {Object} node
|
||||
* @param {Number} borderWidth
|
||||
* @param {Number} ratio
|
||||
* @param {Object} dragZoom
|
||||
*/
|
||||
const drawBox = exports.drawBox = function(ctx, node, borderWidth, dragZoom) {
|
||||
let border = borderWidth(node);
|
||||
let fillHSL = colorCoarseType(node);
|
||||
let strokeHSL = [fillHSL[0], fillHSL[1], fillHSL[2] * 0.5];
|
||||
let scale = 1 + dragZoom.zoom;
|
||||
|
||||
// Offset the draw so that box strokes don't overlap
|
||||
let x = scale * node.x - dragZoom.offsetX + border / 2;
|
||||
let y = scale * node.y - dragZoom.offsetY + border / 2;
|
||||
let dx = scale * node.dx - border;
|
||||
let dy = scale * node.dy - border;
|
||||
|
||||
ctx.fillStyle = hslToStyle(...fillHSL);
|
||||
ctx.fillRect(x, y, dx, dy);
|
||||
|
||||
ctx.strokeStyle = hslToStyle(...strokeHSL);
|
||||
ctx.lineWidth = border;
|
||||
ctx.strokeRect(x, y, dx, dy);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw the overall treemap
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {Array} nodes
|
||||
* @param {Objbect} dragZoom
|
||||
*/
|
||||
const drawTreemap = exports.drawTreemap = function({canvas, ctx}, nodes,
|
||||
dragZoom) {
|
||||
let window = canvas.ownerDocument.defaultView;
|
||||
let ratio = window.devicePixelRatio;
|
||||
let canvasArea = canvas.width * canvas.height;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = `${FONT_SIZE * ratio}px sans-serif`;
|
||||
ctx.textBaseline = "top";
|
||||
|
||||
function borderWidth(node) {
|
||||
let areaRatio = Math.sqrt(node.area / canvasArea);
|
||||
return ratio * Math.max(1, LINE_WIDTH * areaRatio);
|
||||
}
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let node = nodes[i];
|
||||
if (node.parent === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawBox(ctx, node, borderWidth, dragZoom);
|
||||
drawText(ctx, node, borderWidth, ratio, dragZoom);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the position of the zoomed in canvas. It always take up 100% of the view
|
||||
* window, but is transformed relative to the zoomed in containing element,
|
||||
* essentially reversing the transform of the containing element.
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
* @param {Object} dragZoom
|
||||
*/
|
||||
const positionZoomedCanvas = function(canvas, dragZoom) {
|
||||
let scale = 1 / (1 + dragZoom.zoom);
|
||||
let x = -dragZoom.translateX;
|
||||
let y = -dragZoom.translateY;
|
||||
canvas.style.transform = `scale(${scale}) translate(${x}px, ${y}px)`;
|
||||
};
|
||||
|
||||
exports.positionZoomedCanvas = positionZoomedCanvas;
|
12
devtools/client/memory/components/tree-map/moz.build
Normal file
12
devtools/client/memory/components/tree-map/moz.build
Normal file
@ -0,0 +1,12 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'canvas-utils.js',
|
||||
'color-coarse-type.js',
|
||||
'drag-zoom.js',
|
||||
'draw.js',
|
||||
'start.js',
|
||||
)
|
32
devtools/client/memory/components/tree-map/start.js
Normal file
32
devtools/client/memory/components/tree-map/start.js
Normal file
@ -0,0 +1,32 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { setupDraw } = require("./draw");
|
||||
const DragZoom = require("./drag-zoom");
|
||||
const CanvasUtils = require("./canvas-utils");
|
||||
|
||||
/**
|
||||
* Start the tree map visualization
|
||||
*
|
||||
* @param {HTMLDivElement} container
|
||||
* @param {Object} report
|
||||
* the report from a census
|
||||
* @param {Number} debounceRate
|
||||
*/
|
||||
module.exports = function startVisualization(parentEl, report,
|
||||
debounceRate = 100) {
|
||||
let window = parentEl.ownerDocument.defaultView;
|
||||
let canvases = new CanvasUtils(parentEl, debounceRate);
|
||||
let dragZoom = new DragZoom(canvases.container, debounceRate,
|
||||
window.requestAnimationFrame);
|
||||
|
||||
setupDraw(report, canvases, dragZoom);
|
||||
|
||||
return function stopVisualization() {
|
||||
canvases.destroy();
|
||||
dragZoom.destroy();
|
||||
};
|
||||
};
|
@ -30,6 +30,12 @@ actions.READ_SNAPSHOT_END = "read-snapshot-end";
|
||||
// When a census is being performed on a heap snapshot
|
||||
actions.TAKE_CENSUS_START = "take-census-start";
|
||||
actions.TAKE_CENSUS_END = "take-census-end";
|
||||
actions.TAKE_CENSUS_ERROR = "take-census-error";
|
||||
|
||||
// When a tree map is being calculated on a heap snapshot
|
||||
actions.TAKE_TREE_MAP_START = "take-tree-map-start";
|
||||
actions.TAKE_TREE_MAP_END = "take-tree-map-end";
|
||||
actions.TAKE_TREE_MAP_ERROR = "take-tree-map-error";
|
||||
|
||||
// When requesting that the server start/stop recording allocation stacks.
|
||||
actions.TOGGLE_RECORD_ALLOCATION_STACKS_START = "toggle-record-allocation-stacks-start";
|
||||
@ -71,6 +77,9 @@ actions.SET_CENSUS_DISPLAY = "set-census-display";
|
||||
// Fired to change the display that controls the dominator tree labels.
|
||||
actions.SET_DOMINATOR_TREE_DISPLAY = "set-dominator-tree-display";
|
||||
|
||||
// Fired to set a tree map display
|
||||
actions.SET_TREEMAP_DISPLAY = "set-treemap-display";
|
||||
|
||||
// Fired when changing between census or dominators view.
|
||||
actions.CHANGE_VIEW = "change-view";
|
||||
|
||||
@ -109,6 +118,17 @@ const COUNT = Object.freeze({ by: "count", count: true, bytes: true });
|
||||
const INTERNAL_TYPE = Object.freeze({ by: "internalType", then: COUNT });
|
||||
const ALLOCATION_STACK = Object.freeze({ by: "allocationStack", then: COUNT, noStack: COUNT });
|
||||
const OBJECT_CLASS = Object.freeze({ by: "objectClass", then: COUNT, other: COUNT });
|
||||
const COARSE_TYPE = Object.freeze({
|
||||
by: "coarseType",
|
||||
objects: OBJECT_CLASS,
|
||||
strings: COUNT,
|
||||
scripts: {
|
||||
by: "filename",
|
||||
then: INTERNAL_TYPE,
|
||||
noFilename: INTERNAL_TYPE
|
||||
},
|
||||
other: INTERNAL_TYPE,
|
||||
});
|
||||
|
||||
exports.censusDisplays = Object.freeze({
|
||||
coarseType: Object.freeze({
|
||||
@ -120,17 +140,7 @@ exports.censusDisplays = Object.freeze({
|
||||
return L10N.getStr("censusDisplays.coarseType.tooltip");
|
||||
},
|
||||
inverted: true,
|
||||
breakdown: Object.freeze({
|
||||
by: "coarseType",
|
||||
objects: OBJECT_CLASS,
|
||||
strings: COUNT,
|
||||
scripts: {
|
||||
by: "filename",
|
||||
then: INTERNAL_TYPE,
|
||||
noFilename: INTERNAL_TYPE
|
||||
},
|
||||
other: INTERNAL_TYPE,
|
||||
})
|
||||
breakdown: COARSE_TYPE
|
||||
}),
|
||||
|
||||
allocationStack: Object.freeze({
|
||||
@ -193,6 +203,18 @@ exports.dominatorTreeDisplays = Object.freeze({
|
||||
}),
|
||||
});
|
||||
|
||||
exports.treeMapDisplays = Object.freeze({
|
||||
coarseType: Object.freeze({
|
||||
displayName: "Type",
|
||||
get tooltip() {
|
||||
const { L10N } = require("./utils");
|
||||
return L10N.getStr("treeMapDisplays.coarseType.tooltip");
|
||||
},
|
||||
breakdown: COARSE_TYPE,
|
||||
inverted: false,
|
||||
})
|
||||
});
|
||||
|
||||
/*** View States **************************************************************/
|
||||
|
||||
/**
|
||||
@ -202,6 +224,7 @@ const viewState = exports.viewState = Object.create(null);
|
||||
viewState.CENSUS = "view-state-census";
|
||||
viewState.DIFFING = "view-state-diffing";
|
||||
viewState.DOMINATOR_TREE = "view-state-dominator-tree";
|
||||
viewState.TREE_MAP = "view-state-tree-map";
|
||||
|
||||
/*** Snapshot States **********************************************************/
|
||||
|
||||
@ -211,9 +234,9 @@ const snapshotState = exports.snapshotState = Object.create(null);
|
||||
* Various states a snapshot can be in.
|
||||
* An FSM describing snapshot states:
|
||||
*
|
||||
* SAVING -> SAVED -> READING -> READ SAVED_CENSUS
|
||||
* ↗ ↘ ↑ ↓
|
||||
* IMPORTING SAVING_CENSUS
|
||||
* SAVING -> SAVED -> READING -> READ
|
||||
* ↗
|
||||
* IMPORTING
|
||||
*
|
||||
* Any of these states may go to the ERROR state, from which they can never
|
||||
* leave (mwah ha ha ha!)
|
||||
@ -224,8 +247,36 @@ snapshotState.SAVING = "snapshot-state-saving";
|
||||
snapshotState.SAVED = "snapshot-state-saved";
|
||||
snapshotState.READING = "snapshot-state-reading";
|
||||
snapshotState.READ = "snapshot-state-read";
|
||||
snapshotState.SAVING_CENSUS = "snapshot-state-saving-census";
|
||||
snapshotState.SAVED_CENSUS = "snapshot-state-saved-census";
|
||||
|
||||
/*
|
||||
* Various states the census model can be in.
|
||||
*
|
||||
* SAVING <-> SAVED
|
||||
* |
|
||||
* V
|
||||
* ERROR
|
||||
*/
|
||||
|
||||
const censusState = exports.censusState = Object.create(null);
|
||||
|
||||
censusState.SAVING = "census-state-saving";
|
||||
censusState.SAVED = "census-state-saved";
|
||||
censusState.ERROR = "census-state-error";
|
||||
|
||||
/*
|
||||
* Various states the tree map model can be in.
|
||||
*
|
||||
* SAVING <-> SAVED
|
||||
* |
|
||||
* V
|
||||
* ERROR
|
||||
*/
|
||||
|
||||
const treeMapState = exports.treeMapState = Object.create(null);
|
||||
|
||||
treeMapState.SAVING = "tree-map-state-saving";
|
||||
treeMapState.SAVED = "tree-map-state-saved";
|
||||
treeMapState.ERROR = "tree-map-state-error";
|
||||
|
||||
/*** Diffing States ***********************************************************/
|
||||
|
||||
|
@ -73,6 +73,50 @@ const dominatorTreeDisplayModel = exports.dominatorTreeDisplay = PropTypes.shape
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* The data describing the tree map's shape, and its associated metadata.
|
||||
*
|
||||
* @see `js/src/doc/Debugger/Debugger.Memory.md`
|
||||
*/
|
||||
const treeMapDisplayModel = exports.treeMapDisplay = PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
tooltip: PropTypes.string.isRequired,
|
||||
inverted: PropTypes.bool.isRequired,
|
||||
breakdown: PropTypes.shape({
|
||||
by: PropTypes.string.isRequired,
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Tree map model.
|
||||
*/
|
||||
const treeMapModel = exports.treeMapModel = PropTypes.shape({
|
||||
// The current census report data.
|
||||
report: PropTypes.object,
|
||||
// The display data used to generate the current census.
|
||||
display: treeMapDisplayModel,
|
||||
// The current treeMapState this is in
|
||||
state: catchAndIgnore(function (treeMap) {
|
||||
switch (treeMap.state) {
|
||||
case treeMapState.SAVING:
|
||||
assert(!treeMap.report, "Should not have a report");
|
||||
assert(!treeMap.error, "Should not have an error");
|
||||
break;
|
||||
case treeMapState.SAVED:
|
||||
assert(treeMap.report, "Should have a report");
|
||||
assert(!treeMap.error, "Should not have an error");
|
||||
break;
|
||||
|
||||
case treeMapState.ERROR:
|
||||
assert(treeMap.error, "Should have an error");
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false, `Unexpected treeMap state: ${treeMap.state}`);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let censusModel = exports.censusModel = PropTypes.shape({
|
||||
// The current census report data.
|
||||
report: PropTypes.object,
|
||||
@ -92,6 +136,32 @@ let censusModel = exports.censusModel = PropTypes.shape({
|
||||
}),
|
||||
// If a node is currently focused in the report tree, then this is it.
|
||||
focused: PropTypes.object,
|
||||
// The censusModelState that this census is currently in.
|
||||
state: catchAndIgnore(function (census) {
|
||||
switch (census.state) {
|
||||
case censusState.SAVING:
|
||||
assert(!census.report, "Should not have a report");
|
||||
assert(!census.parentMap, "Should not have a parent map");
|
||||
assert(census.expanded, "Should not have an expanded set");
|
||||
assert(!census.error, "Should not have an error");
|
||||
break;
|
||||
|
||||
case censusState.SAVED:
|
||||
assert(census.report, "Should have a report");
|
||||
assert(census.parentMap, "Should have a parent map");
|
||||
assert(census.expanded, "Should have an expanded set");
|
||||
assert(!census.error, "Should not have an error");
|
||||
break;
|
||||
|
||||
case censusState.ERROR:
|
||||
assert(!census.report, "Should not have a report");
|
||||
assert(census.error, "Should have an error");
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false, `Unexpected census state: ${census.state}`);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
@ -193,6 +263,8 @@ let snapshotModel = exports.snapshot = PropTypes.shape({
|
||||
census: censusModel,
|
||||
// Current dominator tree data for this snapshot.
|
||||
dominatorTree: dominatorTreeModel,
|
||||
// Current tree map data for this snapshot.
|
||||
treeMap: treeMapModel,
|
||||
// If an error was thrown while processing this snapshot, the `Error` instance
|
||||
// is attached here.
|
||||
error: PropTypes.object,
|
||||
@ -205,9 +277,8 @@ let snapshotModel = exports.snapshot = PropTypes.shape({
|
||||
// @see ./constants.js
|
||||
state: catchAndIgnore(function (snapshot, propName) {
|
||||
let current = snapshot.state;
|
||||
let shouldHavePath = [states.IMPORTING, states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
|
||||
let shouldHaveCreationTime = [states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
|
||||
let shouldHaveCensus = [states.SAVED_CENSUS];
|
||||
let shouldHavePath = [states.IMPORTING, states.SAVED, states.READ];
|
||||
let shouldHaveCreationTime = [states.READ];
|
||||
|
||||
if (!stateKeys.includes(current)) {
|
||||
throw new Error(`Snapshot state must be one of ${stateKeys}.`);
|
||||
@ -215,10 +286,6 @@ let snapshotModel = exports.snapshot = PropTypes.shape({
|
||||
if (shouldHavePath.includes(current) && !snapshot.path) {
|
||||
throw new Error(`Snapshots in state ${current} must have a snapshot path.`);
|
||||
}
|
||||
if (shouldHaveCensus.includes(current) &&
|
||||
(!snapshot.census || !snapshot.census.display || !snapshot.census.display.breakdown)) {
|
||||
throw new Error(`Snapshots in state ${current} must have a census and breakdown.`);
|
||||
}
|
||||
if (shouldHaveCreationTime.includes(current) && !snapshot.creationTime) {
|
||||
throw new Error(`Snapshots in state ${current} must have a creation time.`);
|
||||
}
|
||||
@ -295,6 +362,10 @@ let appModel = exports.app = {
|
||||
// computed.
|
||||
dominatorTreeDisplay: dominatorTreeDisplayModel.isRequired,
|
||||
|
||||
// The display data describing how we want the dominator tree labels to be
|
||||
// computed.
|
||||
treeMapDisplay: treeMapDisplayModel.isRequired,
|
||||
|
||||
// List of reference to all snapshots taken
|
||||
snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
|
||||
|
||||
@ -319,6 +390,10 @@ let appModel = exports.app = {
|
||||
assert(!app.diffing, "Should not be diffing");
|
||||
break;
|
||||
|
||||
case viewState.TREE_MAP:
|
||||
assert(!app.diffing, "Should not be diffing");
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false, `Unexpected type of view: ${app.view}`);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ exports.allocations = require("./reducers/allocations");
|
||||
exports.censusDisplay = require("./reducers/census-display");
|
||||
exports.diffing = require("./reducers/diffing");
|
||||
exports.dominatorTreeDisplay = require("./reducers/dominator-tree-display");
|
||||
exports.treeMapDisplay = require("./reducers/tree-map-display");
|
||||
exports.errors = require("./reducers/errors");
|
||||
exports.filter = require("./reducers/filter");
|
||||
exports.sizes = require("./reducers/sizes");
|
||||
|
@ -12,5 +12,6 @@ DevToolsModules(
|
||||
'filter.js',
|
||||
'sizes.js',
|
||||
'snapshots.js',
|
||||
'tree-map-display.js',
|
||||
'view.js',
|
||||
)
|
||||
|
@ -8,6 +8,8 @@ const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
|
||||
const {
|
||||
actions,
|
||||
snapshotState: states,
|
||||
censusState,
|
||||
treeMapState,
|
||||
dominatorTreeState,
|
||||
viewState,
|
||||
} = require("../constants");
|
||||
@ -58,11 +60,12 @@ handlers[actions.TAKE_CENSUS_START] = function (snapshots, { id, display, filter
|
||||
report: null,
|
||||
display,
|
||||
filter,
|
||||
state: censusState.SAVING
|
||||
};
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { state: states.SAVING_CENSUS, census })
|
||||
? immutableUpdate(snapshot, { census })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
@ -78,15 +81,79 @@ handlers[actions.TAKE_CENSUS_END] = function (snapshots, { id,
|
||||
expanded: Immutable.Set(),
|
||||
display,
|
||||
filter,
|
||||
state: censusState.SAVED
|
||||
};
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { state: states.SAVED_CENSUS, census })
|
||||
? immutableUpdate(snapshot, { census })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_CENSUS_ERROR] = function (snapshots, { id, error }) {
|
||||
assert(error, "actions with TAKE_CENSUS_ERROR should have an error");
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
const census = Object.freeze({
|
||||
state: censusState.ERROR,
|
||||
error,
|
||||
});
|
||||
|
||||
return immutableUpdate(snapshot, { census });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_TREE_MAP_START] = function (snapshots, { id, display }) {
|
||||
const treeMap = {
|
||||
report: null,
|
||||
display,
|
||||
state: treeMapState.SAVING
|
||||
};
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { treeMap })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_TREE_MAP_END] = function (snapshots, action) {
|
||||
const { id, report, display } = action;
|
||||
const treeMap = {
|
||||
report,
|
||||
display,
|
||||
state: treeMapState.SAVED
|
||||
};
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { treeMap })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_TREE_MAP_ERROR] = function (snapshots, { id, error }) {
|
||||
assert(error, "actions with TAKE_TREE_MAP_ERROR should have an error");
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
const treeMap = Object.freeze({
|
||||
state: treeMapState.ERROR,
|
||||
error,
|
||||
});
|
||||
|
||||
return immutableUpdate(snapshot, { treeMap });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.EXPAND_CENSUS_NODE] = function (snapshots, { id, node }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
|
19
devtools/client/memory/reducers/tree-map-display.js
Normal file
19
devtools/client/memory/reducers/tree-map-display.js
Normal file
@ -0,0 +1,19 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { actions, treeMapDisplays } = require("../constants");
|
||||
const DEFAULT_TREE_MAP_DISPLAY = treeMapDisplays.coarseType;
|
||||
|
||||
const handlers = Object.create(null);
|
||||
|
||||
handlers[actions.SET_TREE_MAP_DISPLAY] = function (_, { display }) {
|
||||
return display;
|
||||
};
|
||||
|
||||
module.exports = function (state = DEFAULT_TREE_MAP_DISPLAY, action) {
|
||||
const handler = handlers[action.type];
|
||||
return handler ? handler(state, action) : state;
|
||||
};
|
@ -11,7 +11,7 @@ handlers[actions.CHANGE_VIEW] = function (_, { view }) {
|
||||
return view;
|
||||
};
|
||||
|
||||
module.exports = function (view = viewState.CENSUS, action) {
|
||||
module.exports = function (view = viewState.TREE_MAP, action) {
|
||||
const handler = handlers[action.type];
|
||||
return handler ? handler(view, action) : view;
|
||||
};
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
const { telemetry } = require("Services");
|
||||
const { makeInfallible, immutableUpdate } = require("devtools/shared/DevToolsUtils");
|
||||
const { dominatorTreeDisplays, censusDisplays } = require("./constants");
|
||||
const { dominatorTreeDisplays, treeMapDisplays, censusDisplays } = require("./constants");
|
||||
|
||||
exports.countTakeSnapshot = makeInfallible(function () {
|
||||
const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_TAKE_SNAPSHOT_COUNT");
|
||||
|
@ -22,3 +22,5 @@ support-files =
|
||||
[browser_memory_percents_01.js]
|
||||
[browser_memory_simple_01.js]
|
||||
[browser_memory_transferHeapSnapshot_e10s_01.js]
|
||||
[browser_memory_tree_map-01.js]
|
||||
[browser_memory_tree_map-02.js]
|
||||
|
@ -9,6 +9,8 @@ const { waitForTime } = require("devtools/shared/DevToolsUtils");
|
||||
const { toggleRecordingAllocationStacks } = require("devtools/client/memory/actions/allocations");
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const censusDisplayActions = require("devtools/client/memory/actions/census-display");
|
||||
const { viewState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
@ -18,6 +20,8 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { getState, dispatch } = panel.panelWin.gStore;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
dispatch(censusDisplayActions.setCensusDisplay(censusDisplays.invertedAllocationStack));
|
||||
is(getState().censusDisplay.breakdown.by, "allocationStack");
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
* Tests taking and then clearing snapshots.
|
||||
*/
|
||||
|
||||
const { treeMapState } = require("devtools/client/memory/constants");
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
@ -20,8 +21,9 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
takeSnapshot(panel.panelWin);
|
||||
yield waitUntilState(gStore, state =>
|
||||
state.snapshots.length === 2 &&
|
||||
state.snapshots[0].state === states.SAVED_CENSUS &&
|
||||
state.snapshots[1].state === states.SAVED_CENSUS);
|
||||
state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
|
||||
state.snapshots[0].treeMap.state === treeMapState.SAVED &&
|
||||
state.snapshots[1].treeMap.state === treeMapState.SAVED);
|
||||
|
||||
snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
|
||||
is(snapshotEls.length, 2, "Two snapshots visible");
|
||||
|
@ -8,7 +8,8 @@
|
||||
const { waitForTime } = require("devtools/shared/DevToolsUtils");
|
||||
const {
|
||||
snapshotState,
|
||||
diffingState
|
||||
diffingState,
|
||||
treeMapState
|
||||
} = require("devtools/client/memory/constants");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
@ -40,8 +41,9 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length === 2 &&
|
||||
state.snapshots[0].state === snapshotState.SAVED_CENSUS &&
|
||||
state.snapshots[1].state === snapshotState.SAVED_CENSUS);
|
||||
state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
|
||||
state.snapshots[0].treeMap.state === treeMapState.SAVED &&
|
||||
state.snapshots[1].treeMap.state === treeMapState.SAVED);
|
||||
|
||||
const listItems = [...doc.querySelectorAll(".snapshot-list-item")];
|
||||
is(listItems.length, 2, "Should have two snapshot list items");
|
||||
|
@ -7,18 +7,24 @@
|
||||
*/
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
const { viewState, censusState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { gStore, document } = panel.panelWin;
|
||||
|
||||
const { dispatch } = panel.panelWin.gStore;
|
||||
|
||||
function $$(selector) {
|
||||
return [...document.querySelectorAll(selector)];
|
||||
}
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
yield takeSnapshot(panel.panelWin);
|
||||
|
||||
yield waitUntilState(gStore, state =>
|
||||
state.snapshots[0].state === states.SAVED_CENSUS);
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVED);
|
||||
|
||||
info("Check coarse type heap view");
|
||||
["Function", "js::Shape", "Object", "strings"].forEach(findNameCell);
|
||||
|
@ -9,8 +9,9 @@ const {
|
||||
dominatorTreeState,
|
||||
snapshotState,
|
||||
viewState,
|
||||
censusState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
const { changeViewAndRefresh } = require("devtools/client/memory/actions/view");
|
||||
const { changeViewAndRefresh, changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
@ -21,12 +22,15 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { getState, dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
const takeSnapshotButton = doc.getElementById("take-snapshot");
|
||||
EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length === 1 &&
|
||||
state.snapshots[0].state === snapshotState.SAVED_CENSUS);
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVING);
|
||||
|
||||
let filterInput = doc.getElementById("filter");
|
||||
EventUtils.synthesizeMouseAtCenter(filterInput, {}, panel.panelWin);
|
||||
@ -34,12 +38,14 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length === 1 &&
|
||||
state.snapshots[0].state === snapshotState.SAVING_CENSUS);
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVING);
|
||||
ok(true, "adding a filter string should trigger census recompute");
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length === 1 &&
|
||||
state.snapshots[0].state === snapshotState.SAVED_CENSUS);
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVED);
|
||||
|
||||
let nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
|
||||
ok(nameElem, "Should get a tree item row with a name");
|
||||
@ -61,7 +67,8 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length === 1 &&
|
||||
state.snapshots[0].state === snapshotState.SAVED_CENSUS);
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVED);
|
||||
|
||||
nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
|
||||
filterInput = doc.getElementById("filter");
|
||||
|
@ -6,11 +6,14 @@
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
snapshotState
|
||||
snapshotState,
|
||||
viewState,
|
||||
censusState
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
takeSnapshotAndCensus
|
||||
} = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
@ -25,6 +28,8 @@ this.test = makeMemoryTest(TEST_URL, function* ({ panel }) {
|
||||
const { dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
info("Take 3 snapshots");
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
@ -32,8 +37,8 @@ this.test = makeMemoryTest(TEST_URL, function* ({ panel }) {
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length == 3 &&
|
||||
state.snapshots.every(s => s.state === snapshotState.SAVED_CENSUS));
|
||||
ok(true, "All snapshots are in SAVED_CENSUS state");
|
||||
state.snapshots.every(s => s.census && s.census.state === censusState.SAVED));
|
||||
ok(true, "All snapshots censuses are in SAVED state");
|
||||
|
||||
yield waitUntilSnapshotSelected(store, 2);
|
||||
ok(true, "Third snapshot selected after creating all snapshots.");
|
||||
|
@ -7,19 +7,22 @@
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
snapshotState
|
||||
snapshotState,
|
||||
censusState,
|
||||
viewState
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
takeSnapshotAndCensus
|
||||
} = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
function waitUntilFocused(store, node) {
|
||||
return waitUntilState(store, state =>
|
||||
state.snapshots.length === 1 &&
|
||||
state.snapshots[0].state === snapshotState.SAVED_CENSUS &&
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVED &&
|
||||
state.snapshots[0].census.focused &&
|
||||
state.snapshots[0].census.focused === node
|
||||
);
|
||||
@ -39,7 +42,10 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { getState, dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
is(getState().censusDisplay.breakdown.by, "coarseType");
|
||||
|
||||
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
let census = getState().snapshots[0].census;
|
||||
let root1 = census.report.children[0];
|
||||
|
@ -7,6 +7,8 @@
|
||||
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const censusDisplayActions = require("devtools/client/memory/actions/census-display");
|
||||
const { viewState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
@ -16,6 +18,8 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { getState, dispatch } = panel.panelWin.gStore;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
ok(!getState().allocations.recording,
|
||||
"Should not be recording allocagtions");
|
||||
|
||||
|
@ -7,6 +7,8 @@
|
||||
"use strict";
|
||||
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const { viewState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
@ -16,6 +18,8 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { getState, dispatch } = panel.panelWin.gStore;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
|
||||
is(getState().allocations.recording, false);
|
||||
|
@ -6,6 +6,8 @@
|
||||
"use strict";
|
||||
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const { viewState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
@ -25,6 +27,8 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { getState, dispatch } = panel.panelWin.gStore;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
is(getState().censusDisplay.breakdown.by, "coarseType",
|
||||
"Should be using coarse type breakdown");
|
||||
|
@ -6,11 +6,15 @@
|
||||
*/
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
const { viewState, censusState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { gStore, document } = panel.panelWin;
|
||||
const { getState, dispatch } = gStore;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
let snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
|
||||
is(getState().snapshots.length, 0, "Starts with no snapshots in store");
|
||||
is(snapshotEls.length, 0, "No snapshots rendered");
|
||||
@ -28,9 +32,8 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
ok(!snapshotEls[0].classList.contains("selected"), "First snapshot no longer has `selected` class");
|
||||
ok(snapshotEls[1].classList.contains("selected"), "Second snapshot has `selected` class");
|
||||
|
||||
yield waitUntilState(gStore, state =>
|
||||
state.snapshots[0].state === states.SAVED_CENSUS &&
|
||||
state.snapshots[1].state === states.SAVED_CENSUS);
|
||||
yield waitUntilCensusState(gStore, s => s.census, [censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
|
||||
ok(document.querySelector(".heap-tree-item-name"),
|
||||
"Should have rendered some tree items");
|
||||
|
@ -0,0 +1,102 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Make sure the canvases are created correctly
|
||||
|
||||
"use strict";
|
||||
|
||||
const CanvasUtils = require("devtools/client/memory/components/tree-map/canvas-utils");
|
||||
const D3_SCRIPT = '<script type="application/javascript" ' +
|
||||
'src="chrome://devtools/content/shared/vendor/d3.js>';
|
||||
const TEST_URL = `data:text/html,<html><body>${D3_SCRIPT}</body></html>`;
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const document = panel.panelWin.document;
|
||||
const window = panel.panelWin;
|
||||
const div = document.createElement("div");
|
||||
|
||||
Object.assign(div.style, {
|
||||
width: "100px",
|
||||
height: "200px",
|
||||
position: "absolute"
|
||||
});
|
||||
|
||||
document.body.appendChild(div);
|
||||
|
||||
info("Create the canvases");
|
||||
|
||||
let canvases = new CanvasUtils(div, 0);
|
||||
|
||||
info("Test the shape of the returned object");
|
||||
|
||||
is(typeof canvases, "object", "Canvases create an object");
|
||||
is(typeof canvases.emit, "function", "Decorated with an EventEmitter");
|
||||
is(typeof canvases.on, "function", "Decorated with an EventEmitter");
|
||||
is(div.children[0], canvases.container, "Div has the container");
|
||||
ok(canvases.main.canvas instanceof window.HTMLCanvasElement,
|
||||
"Creates the main canvas");
|
||||
ok(canvases.zoom.canvas instanceof window.HTMLCanvasElement,
|
||||
"Creates the zoom canvas");
|
||||
ok(canvases.main.ctx instanceof window.CanvasRenderingContext2D,
|
||||
"Creates the main canvas context");
|
||||
ok(canvases.zoom.ctx instanceof window.CanvasRenderingContext2D,
|
||||
"Creates the zoom canvas context");
|
||||
|
||||
info("Test resizing");
|
||||
|
||||
let timesResizeCalled = 0;
|
||||
canvases.on('resize', function() {
|
||||
timesResizeCalled++;
|
||||
});
|
||||
|
||||
let main = canvases.main.canvas;
|
||||
let zoom = canvases.zoom.canvas;
|
||||
let ratio = window.devicePixelRatio;
|
||||
|
||||
is(main.width, 100 * ratio,
|
||||
"Main canvas width is the same as the parent div");
|
||||
is(main.height, 200 * ratio,
|
||||
"Main canvas height is the same as the parent div");
|
||||
is(zoom.width, 100 * ratio,
|
||||
"Zoom canvas width is the same as the parent div");
|
||||
is(zoom.height, 200 * ratio,
|
||||
"Zoom canvas height is the same as the parent div");
|
||||
is(timesResizeCalled, 0,
|
||||
"Resize was not emitted");
|
||||
|
||||
div.style.width = "500px";
|
||||
div.style.height = "700px";
|
||||
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
|
||||
is(main.width, 500 * ratio,
|
||||
"Main canvas width is resized to be the same as the parent div");
|
||||
is(main.height, 700 * ratio,
|
||||
"Main canvas height is resized to be the same as the parent div");
|
||||
is(zoom.width, 500 * ratio,
|
||||
"Zoom canvas width is resized to be the same as the parent div");
|
||||
is(zoom.height, 700 * ratio,
|
||||
"Zoom canvas height is resized to be the same as the parent div");
|
||||
is(timesResizeCalled, 1,
|
||||
"'resize' was emitted was emitted");
|
||||
|
||||
div.style.width = "1100px";
|
||||
div.style.height = "1300px";
|
||||
|
||||
canvases.destroy();
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
|
||||
is(main.width, 500 * ratio,
|
||||
"Main canvas width is not resized after destroy");
|
||||
is(main.height, 700 * ratio,
|
||||
"Main canvas height is not resized after destroy");
|
||||
is(zoom.width, 500 * ratio,
|
||||
"Zoom canvas width is not resized after destroy");
|
||||
is(zoom.height, 700 * ratio,
|
||||
"Zoom canvas height is not resized after destroy");
|
||||
is(timesResizeCalled, 1,
|
||||
"onResize was not called again");
|
||||
|
||||
document.body.removeChild(div);
|
||||
});
|
@ -0,0 +1,129 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Test the drag and zooming behavior
|
||||
|
||||
"use strict";
|
||||
|
||||
const CanvasUtils = require("devtools/client/memory/components/tree-map/canvas-utils");
|
||||
const DragZoom = require("devtools/client/memory/components/tree-map/drag-zoom");
|
||||
|
||||
const TEST_URL = `data:text/html,<html><body></body></html>`;
|
||||
const PIXEL_SCROLL_MODE = 0;
|
||||
const PIXEL_DELTA = 10;
|
||||
const MAX_RAF_LOOP = 1000;
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const panelWin = panel.panelWin;
|
||||
const panelDoc = panelWin.document;
|
||||
const div = panelDoc.createElement("div");
|
||||
|
||||
Object.assign(div.style, {
|
||||
width: "100px",
|
||||
height: "200px",
|
||||
position: "absolute",
|
||||
left:0,
|
||||
top:0
|
||||
});
|
||||
|
||||
let rafMock = createRAFMock();
|
||||
|
||||
panelDoc.body.appendChild(div);
|
||||
|
||||
let canvases = new CanvasUtils(div, 0);
|
||||
let dragZoom = new DragZoom(canvases.container, 0, rafMock.raf);
|
||||
|
||||
info("Check initial state of dragZoom");
|
||||
{
|
||||
is(dragZoom.zoom, 0, "Zooming starts at 0");
|
||||
is(dragZoom.smoothZoom, 0, "Smoothed zooming starts at 0");
|
||||
is(rafMock.timesCalled, 0, "No RAFs have been queued");
|
||||
|
||||
panelWin.dispatchEvent(new WheelEvent("wheel", {
|
||||
deltaY: -PIXEL_DELTA,
|
||||
deltaMode: PIXEL_SCROLL_MODE
|
||||
}));
|
||||
|
||||
is(dragZoom.zoom, PIXEL_DELTA * dragZoom.ZOOM_SPEED,
|
||||
"The zoom was increased");
|
||||
ok(dragZoom.smoothZoom < dragZoom.zoom && dragZoom.smoothZoom > 0,
|
||||
"The smooth zoom is between the initial value and the target");
|
||||
is(rafMock.timesCalled, 1, "A RAF has been queued");
|
||||
}
|
||||
|
||||
info("RAF will eventually stop once the smooth values approach the target");
|
||||
{
|
||||
let i;
|
||||
let lastCallCount;
|
||||
for (i = 0; i < MAX_RAF_LOOP; i++) {
|
||||
if (lastCallCount === rafMock.timesCalled) {
|
||||
break;
|
||||
}
|
||||
lastCallCount = rafMock.timesCalled;
|
||||
rafMock.nextFrame();
|
||||
}
|
||||
|
||||
is(dragZoom.zoom, dragZoom.smoothZoom,
|
||||
"The smooth and target zoom values match");
|
||||
isnot(MAX_RAF_LOOP, i,
|
||||
"The RAF loop correctly stopped");
|
||||
}
|
||||
|
||||
info("Dragging correctly translates the div");
|
||||
{
|
||||
let initialX = dragZoom.translateX;
|
||||
let initialY = dragZoom.translateY;
|
||||
div.dispatchEvent(new MouseEvent("mousemove", {
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
}));
|
||||
div.dispatchEvent(new MouseEvent("mousedown"));
|
||||
div.dispatchEvent(new MouseEvent("mousemove", {
|
||||
clientX: 20,
|
||||
clientY: 20,
|
||||
}));
|
||||
div.dispatchEvent(new MouseEvent("mouseup"));
|
||||
|
||||
ok(dragZoom.translateX - initialX > 0,
|
||||
"Translate X moved by some pixel amount");
|
||||
ok(dragZoom.translateY - initialY > 0,
|
||||
"Translate Y moved by some pixel amount");
|
||||
}
|
||||
|
||||
dragZoom.destroy();
|
||||
|
||||
info("Scroll isn't tracked after destruction");
|
||||
{
|
||||
let previousZoom = dragZoom.zoom;
|
||||
let previousSmoothZoom = dragZoom.smoothZoom;
|
||||
|
||||
panelWin.dispatchEvent(new WheelEvent("wheel", {
|
||||
deltaY: -PIXEL_DELTA,
|
||||
deltaMode: PIXEL_SCROLL_MODE
|
||||
}));
|
||||
|
||||
is(dragZoom.zoom, previousZoom,
|
||||
"The zoom stayed the same");
|
||||
is(dragZoom.smoothZoom, previousSmoothZoom,
|
||||
"The smooth zoom stayed the same");
|
||||
}
|
||||
|
||||
info("Translation isn't tracked after destruction");
|
||||
{
|
||||
let initialX = dragZoom.translateX;
|
||||
let initialY = dragZoom.translateY;
|
||||
|
||||
div.dispatchEvent(new MouseEvent("mousedown"));
|
||||
div.dispatchEvent(new MouseEvent("mousemove"), {
|
||||
clientX: 40,
|
||||
clientY: 40,
|
||||
});
|
||||
div.dispatchEvent(new MouseEvent("mouseup"));
|
||||
is(dragZoom.translateX, initialX,
|
||||
"The translationX didn't change");
|
||||
is(dragZoom.translateY, initialY,
|
||||
"The translationY didn't change");
|
||||
}
|
||||
panelDoc.body.removeChild(div);
|
||||
});
|
@ -112,7 +112,7 @@ function clearSnapshots (window) {
|
||||
let { gStore, document } = window;
|
||||
document.querySelector(".devtools-toolbar .clear-snapshots").click();
|
||||
return waitUntilState(gStore, () => gStore.getState().snapshots.every(
|
||||
(snapshot) => snapshot.state !== states.SAVED_CENSUS)
|
||||
(snapshot) => snapshot.state !== states.READ)
|
||||
);
|
||||
}
|
||||
|
||||
@ -131,7 +131,9 @@ function setCensusDisplay(window, display) {
|
||||
|
||||
return waitUntilState(window.gStore, () => {
|
||||
let selected = window.gStore.getState().snapshots.find(s => s.selected);
|
||||
return selected.state === states.SAVED_CENSUS &&
|
||||
return selected.state === states.READ &&
|
||||
selected.census &&
|
||||
selected.census.state === censusState.SAVED &&
|
||||
selected.census.display === display;
|
||||
});
|
||||
}
|
||||
@ -169,3 +171,57 @@ function waitUntilSnapshotSelected(store, snapshotIndex) {
|
||||
state.snapshots[snapshotIndex] &&
|
||||
state.snapshots[snapshotIndex].selected === true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wait until the state has censuses in a certain state.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
function waitUntilCensusState (store, getCensus, expected) {
|
||||
let predicate = () => {
|
||||
let snapshots = store.getState().snapshots;
|
||||
|
||||
info('Current census state:' +
|
||||
snapshots.map(x => getCensus(x) ? getCensus(x).state : null ));
|
||||
|
||||
return snapshots.length === expected.length &&
|
||||
expected.every((state, i) => {
|
||||
let census = getCensus(snapshots[i]);
|
||||
return (state === "*") ||
|
||||
(!census && !state) ||
|
||||
(census && census.state === state);
|
||||
});
|
||||
};
|
||||
info(`Waiting for snapshots' censuses to be of state: ${expected}`);
|
||||
return waitUntilState(store, predicate);
|
||||
}
|
||||
/**
|
||||
* Mock out the requestAnimationFrame.
|
||||
*
|
||||
* @return {Object}
|
||||
* @function nextFrame
|
||||
* Call the last queued function
|
||||
* @function raf
|
||||
* The mocked raf function
|
||||
* @function timesCalled
|
||||
* How many times the RAF has been called
|
||||
*/
|
||||
function createRAFMock() {
|
||||
let queuedFns = [];
|
||||
let mock = { timesCalled: 0 };
|
||||
|
||||
mock.nextFrame = function() {
|
||||
let thisQueue = queuedFns;
|
||||
queuedFns = [];
|
||||
for(var i = 0; i < thisQueue.length; i++) {
|
||||
thisQueue[i]();
|
||||
}
|
||||
};
|
||||
|
||||
mock.raf = function(fn) {
|
||||
mock.timesCalled++;
|
||||
queuedFns.push(fn);
|
||||
};
|
||||
return mock;
|
||||
}
|
||||
|
@ -15,3 +15,4 @@ support-files =
|
||||
[test_ShortestPaths_01.html]
|
||||
[test_ShortestPaths_02.html]
|
||||
[test_Toolbar_01.html]
|
||||
[test_TreeMap_01.html]
|
||||
|
@ -25,7 +25,8 @@ var {
|
||||
dominatorTreeDisplays,
|
||||
dominatorTreeState,
|
||||
snapshotState,
|
||||
viewState
|
||||
viewState,
|
||||
censusState
|
||||
} = constants;
|
||||
|
||||
const {
|
||||
@ -41,6 +42,7 @@ var CensusTreeItem = React.createFactory(require("devtools/client/memory/compone
|
||||
var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree"));
|
||||
var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item"));
|
||||
var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths"));
|
||||
var TreeMap = React.createFactory(require("devtools/client/memory/components/tree-map"));
|
||||
var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
|
||||
|
||||
// All tests are asynchronous.
|
||||
@ -186,16 +188,18 @@ var TEST_HEAP_PROPS = Object.freeze({
|
||||
other: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
}),
|
||||
}),
|
||||
state: censusState.SAVED,
|
||||
inverted: false,
|
||||
filter: null,
|
||||
expanded: new Set(),
|
||||
focused: null,
|
||||
parentMap: Object.freeze(Object.create(null))
|
||||
}),
|
||||
dominatorTree: TEST_DOMINATOR_TREE,
|
||||
error: null,
|
||||
imported: false,
|
||||
creationTime: 0,
|
||||
state: snapshotState.SAVED_CENSUS,
|
||||
state: snapshotState.READ,
|
||||
}),
|
||||
sizes: Object.freeze({ shortestPathsSize: .5 }),
|
||||
onShortestPathsResize: noop,
|
||||
@ -228,6 +232,47 @@ var TEST_TOOLBAR_PROPS = Object.freeze({
|
||||
snapshots: [],
|
||||
});
|
||||
|
||||
function makeTestCensusNode() {
|
||||
return {
|
||||
name: "Function",
|
||||
bytes: 100,
|
||||
totalBytes: 100,
|
||||
count: 100,
|
||||
totalCount: 100,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
|
||||
var TEST_TREE_MAP_PROPS = Object.freeze({
|
||||
treeMap: Object.freeze({
|
||||
report: {
|
||||
name: null,
|
||||
bytes: 0,
|
||||
totalBytes: 400,
|
||||
count: 0,
|
||||
totalCount: 400,
|
||||
children: [
|
||||
{
|
||||
name: "objects",
|
||||
bytes: 0,
|
||||
totalBytes: 200,
|
||||
count: 0,
|
||||
totalCount: 200,
|
||||
children: [ makeTestCensusNode(), makeTestCensusNode() ]
|
||||
},
|
||||
{
|
||||
name: "other",
|
||||
bytes: 0,
|
||||
totalBytes: 200,
|
||||
count: 0,
|
||||
totalCount: 200,
|
||||
children: [ makeTestCensusNode(), makeTestCensusNode() ],
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function onNextAnimationFrame(fn) {
|
||||
return () =>
|
||||
requestAnimationFrame(() =>
|
||||
|
@ -37,7 +37,7 @@ Test that the currently selected view is rendered.
|
||||
view: viewState.CENSUS,
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(`[data-state=${snapshotState.SAVED_CENSUS}]`),
|
||||
ok(container.querySelector(`[data-state=${censusState.SAVED}]`),
|
||||
"Should render the census.");
|
||||
|
||||
// Diffing view.
|
||||
|
44
devtools/client/memory/test/chrome/test_TreeMap_01.html
Normal file
44
devtools/client/memory/test/chrome/test_TreeMap_01.html
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that the Tree Map correctly renders onto 2 managed canvases.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://devtools/content/shared/vendor/d3.js">
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Give the container height so that the whole tree is rendered. -->
|
||||
<div id="container" style="height: 900px;"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function*() {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
yield renderComponent(TreeMap(TEST_TREE_MAP_PROPS), container);
|
||||
|
||||
let treeMapContainer = container.querySelector(".tree-map-container");
|
||||
ok(treeMapContainer, "Component creates a container");
|
||||
|
||||
let canvases = treeMapContainer.querySelectorAll("canvas");
|
||||
is(canvases.length, 2, "Creates 2 canvases");
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -73,6 +73,25 @@ function waitUntilSnapshotState (store, expected) {
|
||||
return waitUntilState(store, predicate);
|
||||
}
|
||||
|
||||
function waitUntilCensusState (store, getCensus, expected) {
|
||||
let predicate = () => {
|
||||
let snapshots = store.getState().snapshots;
|
||||
|
||||
do_print('Current census state:' +
|
||||
snapshots.map(x => getCensus(x) ? getCensus(x).state : null ));
|
||||
|
||||
return snapshots.length === expected.length &&
|
||||
expected.every((state, i) => {
|
||||
let census = getCensus(snapshots[i]);
|
||||
return (state === "*") ||
|
||||
(!census && !state) ||
|
||||
(census && census.state === state);
|
||||
});
|
||||
};
|
||||
do_print(`Waiting for snapshots' censuses to be of state: ${expected}`);
|
||||
return waitUntilState(store, predicate);
|
||||
}
|
||||
|
||||
function *createTempFile () {
|
||||
let file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]);
|
||||
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
|
@ -1,10 +1,11 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test clearSnapshots deletes snapshots with state SAVED_CENSUS
|
||||
// Test clearSnapshots deletes snapshots with READ censuses
|
||||
|
||||
let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
|
||||
let { snapshotState: states, actions } = require("devtools/client/memory/constants");
|
||||
const { treeMapState } = require("devtools/client/memory/constants");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -18,7 +19,7 @@ add_task(function *() {
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
|
||||
ok(true, "snapshot created");
|
||||
|
||||
ok(true, "dispatch clearSnapshots action");
|
||||
|
@ -1,10 +1,10 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test clearSnapshots preserves snapshots with state != SAVED_CENSUS or ERROR
|
||||
// Test clearSnapshots preserves snapshots with state != READ or ERROR
|
||||
|
||||
let { takeSnapshotAndCensus, clearSnapshots, takeSnapshot } = require("devtools/client/memory/actions/snapshot");
|
||||
let { snapshotState: states, actions } = require("devtools/client/memory/constants");
|
||||
let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -17,11 +17,13 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
ok(true, "create a snapshot in SAVED_CENSUS state");
|
||||
ok(true, "create a snapshot with a census in SAVED state");
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
ok(true, "create a snapshot in SAVED state");
|
||||
dispatch(takeSnapshot(front));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED]);
|
||||
yield waitUntilSnapshotState(store, [states.SAVED, states.SAVED]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
|
||||
[treeMapState.SAVED, null]);
|
||||
ok(true, "snapshots created with expected states");
|
||||
|
||||
ok(true, "dispatch clearSnapshots action");
|
||||
@ -35,8 +37,10 @@ add_task(function *() {
|
||||
|
||||
equal(getState().snapshots.length, 1, "one snapshot remaining");
|
||||
let remainingSnapshot = getState().snapshots[0];
|
||||
notEqual(remainingSnapshot.state, states.SAVED_CENSUS,
|
||||
"remaining snapshot doesn't have the SAVED_CENSUS state");
|
||||
equal(remainingSnapshot.treeMap, undefined,
|
||||
"remaining snapshot doesn't have a treeMap property");
|
||||
equal(remainingSnapshot.census, undefined,
|
||||
"remaining snapshot doesn't have a census property");
|
||||
|
||||
heapWorker.destroy();
|
||||
yield front.detach();
|
||||
|
@ -4,7 +4,7 @@
|
||||
// Test clearSnapshots deletes snapshots with state ERROR
|
||||
|
||||
let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
|
||||
let { snapshotState: states, actions } = require("devtools/client/memory/constants");
|
||||
let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -17,10 +17,13 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
ok(true, "create a snapshot with SAVED_CENSUS state");
|
||||
ok(true, "create a snapshot with a treeMap");
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
ok(true, "snapshot created with SAVED_CENSUS state");
|
||||
yield waitUntilSnapshotState(store, [states.SAVED]);
|
||||
ok(true, "snapshot created with a SAVED state");
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
|
||||
[treeMapState.SAVED]);
|
||||
ok(true, "treeMap created with a SAVED state");
|
||||
|
||||
ok(true, "set snapshot state to error");
|
||||
let id = getState().snapshots[0].id;
|
||||
|
@ -4,7 +4,7 @@
|
||||
// Test clearSnapshots deletes several snapshots
|
||||
|
||||
let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
|
||||
let { snapshotState: states, actions } = require("devtools/client/memory/constants");
|
||||
let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -17,19 +17,20 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
ok(true, "create 3 snapshots in SAVED_CENSUS state");
|
||||
ok(true, "create 3 snapshots with a saved census");
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
ok(true, "snapshots created in SAVED_CENSUS state");
|
||||
yield waitUntilSnapshotState(store,
|
||||
[states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
|
||||
[treeMapState.SAVED, treeMapState.SAVED,
|
||||
treeMapState.SAVED]);
|
||||
ok(true, "snapshots created with a saved census");
|
||||
|
||||
ok(true, "set first snapshot state to error");
|
||||
let id = getState().snapshots[0].id;
|
||||
dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") });
|
||||
yield waitUntilSnapshotState(store,
|
||||
[states.ERROR, states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
[states.ERROR, states.READ, states.READ]);
|
||||
ok(true, "first snapshot set to error state");
|
||||
|
||||
ok(true, "dispatch clearSnapshots action");
|
||||
|
@ -4,7 +4,7 @@
|
||||
// Test clearSnapshots deletes several snapshots
|
||||
|
||||
let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
|
||||
let { snapshotState: states, actions } = require("devtools/client/memory/constants");
|
||||
let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -17,11 +17,12 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
ok(true, "create 3 snapshots in SAVED_CENSUS state");
|
||||
ok(true, "create 2 snapshots with a saved census");
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
ok(true, "snapshots created in SAVED_CENSUS state");
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
ok(true, "snapshots created with a saved census");
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
|
||||
[treeMapState.SAVED, treeMapState.SAVED]);
|
||||
|
||||
let errorHeapWorker = {
|
||||
deleteHeapSnapshot: function() {
|
||||
|
@ -7,10 +7,13 @@
|
||||
|
||||
const {
|
||||
takeSnapshotAndCensus,
|
||||
clearSnapshots } = require("devtools/client/memory/actions/snapshot");
|
||||
clearSnapshots
|
||||
} = require("devtools/client/memory/actions/snapshot");
|
||||
const {
|
||||
snapshotState: states,
|
||||
actions } = require("devtools/client/memory/constants");
|
||||
actions,
|
||||
treeMapState
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
toggleDiffing,
|
||||
selectSnapshotForDiffingAndRefresh
|
||||
@ -27,12 +30,12 @@ add_task(function* () {
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
ok(true, "Create 2 snapshots in SAVED_CENSUS state");
|
||||
ok(true, "create 2 snapshots with a saved census");
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
ok(true, "Snapshots created in SAVED_CENSUS state");
|
||||
yield waitUntilSnapshotState(store,
|
||||
[states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
|
||||
[treeMapState.SAVED, treeMapState.SAVED]);
|
||||
ok(true, "snapshots created with a saved census");
|
||||
|
||||
dispatch(toggleDiffing());
|
||||
dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
let { exportSnapshot } = require("devtools/client/memory/actions/io");
|
||||
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
let { snapshotState: states, actions } = require("devtools/client/memory/constants");
|
||||
let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -20,7 +20,8 @@ add_task(function *() {
|
||||
|
||||
let destPath = yield createTempFile();
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
|
||||
[treeMapState.SAVED]);
|
||||
|
||||
let exportEvents = Promise.all([
|
||||
waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
|
||||
|
@ -4,9 +4,11 @@
|
||||
|
||||
// Test that changing filter state properly refreshes the selected census.
|
||||
|
||||
let { snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants");
|
||||
let { setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter");
|
||||
let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
|
||||
let { setCensusDisplay } = require("devtools/client/memory/actions/census-display");
|
||||
let { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -19,42 +21,49 @@ add_task(function*() {
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
equal(getState().filter, null, "no filter by default");
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
ok(true, "saved 3 snapshots and took a census of each of them");
|
||||
|
||||
dispatch(setFilterStringAndRefresh("str", heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS,
|
||||
states.SAVING_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVING]);
|
||||
ok(true, "setting filter string should recompute the selected snapshot's census");
|
||||
|
||||
equal(getState().filter, "str", "now inverted");
|
||||
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
|
||||
equal(getState().snapshots[0].census.filter, null);
|
||||
equal(getState().snapshots[1].census.filter, null);
|
||||
equal(getState().snapshots[2].census.filter, "str");
|
||||
|
||||
dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVING_CENSUS,
|
||||
states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED,
|
||||
censusState.SAVING,
|
||||
censusState.SAVED]);
|
||||
ok(true, "selecting non-inverted census should trigger a recompute");
|
||||
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
|
||||
equal(getState().snapshots[0].census.filter, null);
|
||||
equal(getState().snapshots[1].census.filter, "str");
|
||||
|
@ -4,9 +4,10 @@
|
||||
// Test that changing filter state in the middle of taking a snapshot results in
|
||||
// the properly fitered census.
|
||||
|
||||
let { snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
|
||||
let { setFilterString, setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter");
|
||||
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
let { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -19,24 +20,29 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVING]);
|
||||
|
||||
dispatch(setFilterString("str"));
|
||||
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED]);
|
||||
equal(getState().filter, "str",
|
||||
"should want filtered trees");
|
||||
equal(getState().snapshots[0].census.filter, "str",
|
||||
"snapshot-we-were-in-the-middle-of-saving's census should be filtered");
|
||||
|
||||
dispatch(setFilterStringAndRefresh("", heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVING]);
|
||||
ok(true, "changing filter string retriggers census");
|
||||
ok(!getState().filter, "no longer filtering");
|
||||
|
||||
dispatch(setFilterString("obj"));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED]);
|
||||
equal(getState().filter, "obj", "filtering for obj now");
|
||||
equal(getState().snapshots[0].census.filter, "obj",
|
||||
"census-we-were-in-the-middle-of-recomputing should be filtered again");
|
||||
|
@ -7,7 +7,7 @@
|
||||
* importing a snapshot, and its sub-actions.
|
||||
*/
|
||||
|
||||
let { actions, snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { actions, snapshotState: states, treeMapState } = require("devtools/client/memory/constants");
|
||||
let { exportSnapshot, importSnapshotAndCensus } = require("devtools/client/memory/actions/io");
|
||||
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
|
||||
@ -24,7 +24,7 @@ add_task(function *() {
|
||||
|
||||
let destPath = yield createTempFile();
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
|
||||
|
||||
let exportEvents = Promise.all([
|
||||
waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
|
||||
@ -34,40 +34,53 @@ add_task(function *() {
|
||||
yield exportEvents;
|
||||
|
||||
// Now import our freshly exported snapshot
|
||||
let i = 0;
|
||||
let expected = ["IMPORTING", "READING", "READ", "SAVING_CENSUS", "SAVED_CENSUS"];
|
||||
let snapshotI = 0;
|
||||
let censusI = 0;
|
||||
let snapshotStates = ["IMPORTING", "READING", "READ"];
|
||||
let censusStates = ["SAVING", "SAVED"];
|
||||
let expectStates = () => {
|
||||
let snapshot = getState().snapshots[1];
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
let isCorrectState = snapshot.state === states[expected[i]];
|
||||
if (isCorrectState) {
|
||||
ok(true, `Found expected state ${expected[i]}`);
|
||||
i++;
|
||||
if (snapshotI < snapshotStates.length) {
|
||||
let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]];
|
||||
if (isCorrectState) {
|
||||
ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
|
||||
snapshotI++;
|
||||
}
|
||||
}
|
||||
if (snapshot.treeMap && censusI < censusStates.length) {
|
||||
if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
|
||||
ok(true, `Found expected census state ${censusStates[censusI]}`);
|
||||
censusI++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let unsubscribe = subscribe(expectStates);
|
||||
dispatch(importSnapshotAndCensus(heapWorker, destPath));
|
||||
|
||||
yield waitUntilState(store, () => i === expected.length);
|
||||
yield waitUntilState(store, () => { return snapshotI === snapshotStates.length &&
|
||||
censusI === censusStates.length });
|
||||
unsubscribe();
|
||||
equal(i, expected.length, "importSnapshotAndCensus() produces the correct sequence of states in a snapshot");
|
||||
equal(getState().snapshots[1].state, states.SAVED_CENSUS, "imported snapshot is in SAVED_CENSUS state");
|
||||
equal(snapshotI, snapshotStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a snapshot");
|
||||
equal(getState().snapshots[1].state, states.READ, "imported snapshot is in READ state");
|
||||
equal(censusI, censusStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a census");
|
||||
equal(getState().snapshots[1].treeMap.state, treeMapState.SAVED, "imported snapshot is in READ state");
|
||||
ok(getState().snapshots[1].selected, "imported snapshot is selected");
|
||||
|
||||
// Check snapshot data
|
||||
let snapshot1 = getState().snapshots[0];
|
||||
let snapshot2 = getState().snapshots[1];
|
||||
|
||||
equal(snapshot1.census.display, snapshot2.census.display,
|
||||
equal(snapshot1.treeMap.display, snapshot2.treeMap.display,
|
||||
"imported snapshot has correct display");
|
||||
|
||||
// Clone the census data so we can destructively remove the ID/parents to compare
|
||||
// equal census data
|
||||
let census1 = stripUnique(JSON.parse(JSON.stringify(snapshot1.census.report)));
|
||||
let census2 = stripUnique(JSON.parse(JSON.stringify(snapshot2.census.report)));
|
||||
let census1 = stripUnique(JSON.parse(JSON.stringify(snapshot1.treeMap.report)));
|
||||
let census2 = stripUnique(JSON.parse(JSON.stringify(snapshot2.treeMap.report)));
|
||||
|
||||
equal(JSON.stringify(census1), JSON.stringify(census2), "Imported snapshot has correct census");
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
* should be computed.
|
||||
*/
|
||||
|
||||
let { snapshotState, dominatorTreeState, viewState, } =
|
||||
let { snapshotState, dominatorTreeState, viewState, treeMapState } =
|
||||
require("devtools/client/memory/constants");
|
||||
let { importSnapshotAndCensus } = require("devtools/client/memory/actions/io");
|
||||
let { changeViewAndRefresh } = require("devtools/client/memory/actions/view");
|
||||
@ -33,8 +33,8 @@ add_task(function* () {
|
||||
"IMPORTING",
|
||||
"READING",
|
||||
"READ",
|
||||
"SAVING_CENSUS",
|
||||
"SAVED_CENSUS",
|
||||
"treeMap:SAVING",
|
||||
"treeMap:SAVED",
|
||||
"dominatorTree:COMPUTING",
|
||||
"dominatorTree:FETCHING",
|
||||
"dominatorTree:LOADED",
|
||||
@ -73,6 +73,12 @@ function hasExpectedState(snapshot, expectedState) {
|
||||
return snapshot.dominatorTree && snapshot.dominatorTree.state === state;
|
||||
}
|
||||
|
||||
let isTreeMapState = expectedState.indexOf("treeMap:") === 0;
|
||||
if (isTreeMapState) {
|
||||
let state = treeMapState[expectedState.replace("treeMap:", "")];
|
||||
return snapshot.treeMap && snapshot.treeMap.state === state;
|
||||
}
|
||||
|
||||
let state = snapshotState[expectedState];
|
||||
return snapshot.state === state;
|
||||
}
|
||||
|
@ -9,9 +9,10 @@
|
||||
* `setCensusDisplayAndRefresh`.
|
||||
*/
|
||||
|
||||
let { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
|
||||
let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
|
||||
let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -24,6 +25,8 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
// Test default display with no snapshots
|
||||
equal(getState().censusDisplay.breakdown.by, "coarseType",
|
||||
"default coarseType display selected at start.");
|
||||
@ -43,21 +46,29 @@ add_task(function *() {
|
||||
|
||||
// Test new snapshots
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED]);
|
||||
|
||||
// Updates when changing display during `SAVING`
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED, censusState.SAVING]);
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED, censusState.SAVED]);
|
||||
|
||||
|
||||
// Updates when changing display during `SAVING_CENSUS`
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVING_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVING]);
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
|
||||
equal(getState().snapshots[2].census.display, censusDisplays.allocationStack,
|
||||
"Display can be changed while saving census, stores updated display in snapshot");
|
||||
@ -65,12 +76,12 @@ add_task(function *() {
|
||||
// Updates census on currently selected snapshot when changing display
|
||||
ok(getState().snapshots[2].selected, "Third snapshot currently selected");
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
|
||||
yield waitUntilState(store, state => state.snapshots[2].state === states.SAVED_CENSUS);
|
||||
yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED);
|
||||
equal(getState().snapshots[2].census.display, censusDisplays.coarseType,
|
||||
"Snapshot census updated when changing displays after already generating one census");
|
||||
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
|
||||
yield waitUntilState(store, state => state.snapshots[2].state === states.SAVED_CENSUS);
|
||||
yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED);
|
||||
equal(getState().snapshots[2].census.display, censusDisplays.allocationStack,
|
||||
"Snapshot census updated when changing displays after already generating one census");
|
||||
|
||||
@ -81,8 +92,14 @@ add_task(function *() {
|
||||
|
||||
// Updates to current display when switching to stale snapshot
|
||||
dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING_CENSUS, states.SAVED_CENSUS]);
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED,
|
||||
censusState.SAVING,
|
||||
censusState.SAVED]);
|
||||
yield waitUntilCensusState(store, snapshot => snapshot.census,
|
||||
[censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
|
||||
ok(getState().snapshots[1].selected, "Second snapshot selected currently");
|
||||
equal(getState().snapshots[1].census.display, censusDisplays.allocationStack,
|
||||
|
@ -7,9 +7,10 @@
|
||||
* displays.
|
||||
*/
|
||||
|
||||
let { snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
|
||||
let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
|
||||
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
let { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
let CUSTOM = {
|
||||
displayName: "Custom",
|
||||
@ -32,12 +33,13 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, CUSTOM));
|
||||
equal(getState().censusDisplay, CUSTOM,
|
||||
"CUSTOM display stored in display state.");
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
|
||||
|
||||
equal(getState().snapshots[0].census.display, CUSTOM,
|
||||
"New snapshot stored CUSTOM display when done taking census");
|
||||
|
@ -8,9 +8,10 @@
|
||||
* action for that.
|
||||
*/
|
||||
|
||||
let { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
|
||||
let { setCensusDisplay } = require("devtools/client/memory/actions/census-display");
|
||||
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -23,6 +24,8 @@ add_task(function*() {
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
// Test default display with no snapshots
|
||||
equal(getState().censusDisplay.breakdown.by, "coarseType",
|
||||
"default coarseType display selected at start.");
|
||||
@ -43,7 +46,7 @@ add_task(function*() {
|
||||
|
||||
// Test new snapshots
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
|
||||
equal(getState().snapshots[0].census.display, censusDisplays.allocationStack,
|
||||
"New snapshots use the current, non-default display");
|
||||
});
|
||||
|
@ -5,8 +5,10 @@
|
||||
* Tests the async reducer responding to the action `takeCensus(heapWorker, snapshot)`
|
||||
*/
|
||||
|
||||
var { snapshotState: states, censusDisplays } = require("devtools/client/memory/constants");
|
||||
var { snapshotState: states, censusDisplays, censusState, censusState, viewState } = require("devtools/client/memory/constants");
|
||||
var actions = require("devtools/client/memory/actions/snapshot");
|
||||
var { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -18,6 +20,8 @@ add_task(function *() {
|
||||
yield front.attach();
|
||||
let store = Store();
|
||||
|
||||
store.dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
store.dispatch(actions.takeSnapshot(front));
|
||||
yield waitUntilState(store, () => {
|
||||
let snapshots = store.getState().snapshots;
|
||||
@ -37,12 +41,13 @@ add_task(function *() {
|
||||
yield waitUntilState(store, () => store.getState().snapshots[0].state === states.READ);
|
||||
|
||||
store.dispatch(actions.takeCensus(heapWorker, snapshot.id));
|
||||
yield waitUntilState(store, () => store.getState().snapshots[0].state === states.SAVING_CENSUS);
|
||||
yield waitUntilState(store, () => store.getState().snapshots[0].state === states.SAVED_CENSUS);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
|
||||
|
||||
snapshot = store.getState().snapshots[0];
|
||||
ok(snapshot.census, "Snapshot has census after saved census");
|
||||
ok(snapshot.census.report.children.length, "Census is in tree node form");
|
||||
equal(snapshot.census.display, censusDisplays.coarseType,
|
||||
"Snapshot stored correct display used for the census");
|
||||
|
||||
});
|
||||
|
@ -6,7 +6,7 @@
|
||||
* taking a snapshot, and its sub-actions.
|
||||
*/
|
||||
|
||||
let { snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { snapshotState: states, treeMapState } = require("devtools/client/memory/constants");
|
||||
let actions = require("devtools/client/memory/actions/snapshot");
|
||||
|
||||
function run_test() {
|
||||
@ -19,28 +19,40 @@ add_task(function *() {
|
||||
yield front.attach();
|
||||
let store = Store();
|
||||
|
||||
let i = 0;
|
||||
let expected = ["SAVING", "SAVED", "READING", "READ", "SAVING_CENSUS", "SAVED_CENSUS"];
|
||||
let snapshotI = 0;
|
||||
let censusI = 0;
|
||||
let snapshotStates = ["SAVING", "SAVED", "READING", "READ"];
|
||||
let censusStates = ["SAVING", "SAVED"];
|
||||
let expectStates = () => {
|
||||
if (i >= expected.length) { return false; }
|
||||
|
||||
let snapshot = store.getState().snapshots[0] || {};
|
||||
let isCorrectState = snapshot.state === states[expected[i]];
|
||||
if (isCorrectState) {
|
||||
ok(true, `Found expected state ${expected[i]}`);
|
||||
i++;
|
||||
let snapshot = store.getState().snapshots[0];
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
if (snapshotI < snapshotStates.length) {
|
||||
let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]];
|
||||
if (isCorrectState) {
|
||||
ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
|
||||
snapshotI++;
|
||||
}
|
||||
}
|
||||
if (snapshot.treeMap && censusI < censusStates.length) {
|
||||
if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
|
||||
ok(true, `Found expected census state ${censusStates[censusI]}`);
|
||||
censusI++;
|
||||
}
|
||||
}
|
||||
return isCorrectState;
|
||||
};
|
||||
|
||||
|
||||
let unsubscribe = store.subscribe(expectStates);
|
||||
store.dispatch(actions.takeSnapshotAndCensus(front, heapWorker));
|
||||
|
||||
yield waitUntilState(store, () => i === 6);
|
||||
yield waitUntilState(store, () => { return snapshotI === snapshotStates.length &&
|
||||
censusI === censusStates.length });
|
||||
unsubscribe();
|
||||
|
||||
ok(true, "takeSnapshotAndCensus() produces the correct sequence of states in a snapshot");
|
||||
let snapshot = store.getState().snapshots[0];
|
||||
ok(snapshot.census, "snapshot has census data");
|
||||
ok(snapshot.treeMap, "snapshot has tree map census data");
|
||||
ok(snapshot.selected, "snapshot is selected");
|
||||
});
|
||||
|
@ -7,6 +7,8 @@
|
||||
const {
|
||||
censusDisplays,
|
||||
snapshotState: states,
|
||||
censusState,
|
||||
viewState
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
setCensusDisplayAndRefresh
|
||||
@ -15,6 +17,7 @@ const {
|
||||
takeSnapshotAndCensus,
|
||||
selectSnapshotAndRefresh,
|
||||
} = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -27,6 +30,8 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
// Select a non-inverted display.
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
|
||||
equal(getState().censusDisplay.inverted, false, "not inverted by default");
|
||||
@ -35,38 +40,38 @@ add_task(function *() {
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
ok(true, "saved 3 snapshots and took a census of each of them");
|
||||
|
||||
// Select an inverted display.
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
|
||||
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS,
|
||||
states.SAVING_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVING]);
|
||||
ok(true, "toggling inverted should recompute the selected snapshot's census");
|
||||
|
||||
equal(getState().censusDisplay.inverted, true, "now inverted");
|
||||
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
|
||||
equal(getState().snapshots[0].census.display.inverted, false);
|
||||
equal(getState().snapshots[1].census.display.inverted, false);
|
||||
equal(getState().snapshots[2].census.display.inverted, true);
|
||||
|
||||
dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVING_CENSUS,
|
||||
states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
|
||||
censusState.SAVING,
|
||||
censusState.SAVED]);
|
||||
ok(true, "selecting non-inverted census should trigger a recompute");
|
||||
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
|
||||
equal(getState().snapshots[0].census.display.inverted, false);
|
||||
equal(getState().snapshots[1].census.display.inverted, true);
|
||||
|
@ -5,9 +5,10 @@
|
||||
// Test that changing inverted state in the middle of taking a snapshot results
|
||||
// in an inverted census.
|
||||
|
||||
const { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
|
||||
const { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -20,6 +21,8 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
|
||||
equal(getState().censusDisplay.inverted, false);
|
||||
|
||||
@ -28,19 +31,20 @@ add_task(function *() {
|
||||
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
|
||||
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
|
||||
|
||||
ok(getState().censusDisplay.inverted,
|
||||
"should want inverted trees");
|
||||
ok(getState().snapshots[0].census.display.inverted,
|
||||
"snapshot-we-were-in-the-middle-of-saving's census should be inverted");
|
||||
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
|
||||
yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
|
||||
ok(true, "toggling inverted retriggers census");
|
||||
ok(!getState().censusDisplay.inverted, "no longer inverted");
|
||||
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
|
||||
ok(getState().censusDisplay.inverted, "inverted again");
|
||||
ok(getState().snapshots[0].census.display.inverted,
|
||||
"census-we-were-in-the-middle-of-recomputing should be inverted again");
|
||||
|
@ -3,9 +3,10 @@
|
||||
|
||||
// Test that toggling diffing unselects all snapshots.
|
||||
|
||||
const { snapshotState } = require("devtools/client/memory/constants");
|
||||
const { snapshotState, censusState, viewState } = require("devtools/client/memory/constants");
|
||||
const { toggleDiffing } = require("devtools/client/memory/actions/diffing");
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -18,14 +19,16 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
equal(getState().diffing, null, "not diffing by default");
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [snapshotState.SAVED_CENSUS,
|
||||
snapshotState.SAVED_CENSUS,
|
||||
snapshotState.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
|
||||
censusState.SAVED,
|
||||
censusState.SAVED]);
|
||||
|
||||
ok(getState().snapshots.some(s => s.selected),
|
||||
"One of the new snapshots is selected");
|
||||
|
@ -3,12 +3,13 @@
|
||||
|
||||
// Test selecting snapshots for diffing.
|
||||
|
||||
const { diffingState, snapshotState } = require("devtools/client/memory/constants");
|
||||
const { diffingState, snapshotState, viewState } = require("devtools/client/memory/constants");
|
||||
const {
|
||||
toggleDiffing,
|
||||
selectSnapshotForDiffing
|
||||
} = require("devtools/client/memory/actions/diffing");
|
||||
const { takeSnapshot } = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -21,11 +22,13 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
equal(getState().diffing, null, "not diffing by default");
|
||||
|
||||
dispatch(takeSnapshot(front, heapWorker));
|
||||
dispatch(takeSnapshot(front, heapWorker));
|
||||
dispatch(takeSnapshot(front, heapWorker));
|
||||
|
||||
yield waitUntilSnapshotState(store, [snapshotState.SAVED,
|
||||
snapshotState.SAVED,
|
||||
snapshotState.SAVED]);
|
||||
|
@ -5,7 +5,8 @@
|
||||
|
||||
const {
|
||||
diffingState,
|
||||
snapshotState
|
||||
snapshotState,
|
||||
viewState
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
toggleDiffing,
|
||||
@ -15,6 +16,7 @@ const {
|
||||
takeSnapshot,
|
||||
readSnapshot
|
||||
} = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -26,6 +28,7 @@ add_task(function *() {
|
||||
yield front.attach();
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
equal(getState().diffing, null, "not diffing by default");
|
||||
|
||||
|
@ -7,6 +7,7 @@ const {
|
||||
diffingState,
|
||||
snapshotState,
|
||||
censusDisplays,
|
||||
viewState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
setCensusDisplayAndRefresh,
|
||||
@ -22,6 +23,7 @@ const {
|
||||
takeSnapshot,
|
||||
readSnapshot,
|
||||
} = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -33,6 +35,7 @@ add_task(function *() {
|
||||
yield front.attach();
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
yield dispatch(setCensusDisplayAndRefresh(heapWorker,
|
||||
censusDisplays.allocationStack));
|
||||
|
@ -6,6 +6,7 @@
|
||||
let {
|
||||
snapshotState: states,
|
||||
dominatorTreeState,
|
||||
treeMapState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
let {
|
||||
takeSnapshotAndCensus,
|
||||
@ -24,7 +25,7 @@ add_task(function *() {
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
|
||||
ok(!getState().snapshots[0].dominatorTree,
|
||||
"There shouldn't be a dominator tree model yet since it is not computed " +
|
||||
"until we switch to the dominators view.");
|
||||
|
@ -8,6 +8,7 @@ const {
|
||||
snapshotState: states,
|
||||
dominatorTreeState,
|
||||
viewState,
|
||||
treeMapState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
takeSnapshotAndCensus,
|
||||
@ -28,7 +29,7 @@ add_task(function *() {
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
|
||||
ok(!getState().snapshots[0].dominatorTree,
|
||||
"There shouldn't be a dominator tree model yet since it is not computed " +
|
||||
"until we switch to the dominators view.");
|
||||
|
@ -27,7 +27,7 @@ add_task(function *() {
|
||||
|
||||
for (let intermediateSnapshotState of [states.SAVING,
|
||||
states.READING,
|
||||
states.SAVING_CENSUS]) {
|
||||
states.READ]) {
|
||||
dumpn(`Testing switching to the DOMINATOR_TREE view in the middle of the ${intermediateSnapshotState} snapshot state`);
|
||||
|
||||
let store = Store();
|
||||
|
@ -8,6 +8,7 @@ let {
|
||||
snapshotState: states,
|
||||
dominatorTreeState,
|
||||
viewState,
|
||||
treeMapState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
let {
|
||||
takeSnapshotAndCensus,
|
||||
@ -29,8 +30,8 @@ add_task(function *() {
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
|
||||
states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED,
|
||||
treeMapState.SAVED]);
|
||||
|
||||
ok(getState().snapshots[1].selected, "The second snapshot is selected");
|
||||
|
||||
|
@ -9,6 +9,7 @@ const {
|
||||
dominatorTreeState,
|
||||
viewState,
|
||||
dominatorTreeDisplays,
|
||||
treeMapState
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
setDominatorTreeDisplayAndRefresh
|
||||
@ -35,7 +36,7 @@ add_task(function *() {
|
||||
dispatch(changeView(viewState.DOMINATOR_TREE));
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
|
||||
ok(!getState().snapshots[0].dominatorTree,
|
||||
"There shouldn't be a dominator tree model yet since it is not computed " +
|
||||
"until we switch to the dominators view.");
|
||||
|
@ -9,6 +9,7 @@ const {
|
||||
dominatorTreeState,
|
||||
viewState,
|
||||
dominatorTreeDisplays,
|
||||
treeMapState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
setDominatorTreeDisplayAndRefresh
|
||||
@ -35,7 +36,7 @@ add_task(function *() {
|
||||
dispatch(changeView(viewState.DOMINATOR_TREE));
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
|
||||
ok(!getState().snapshots[0].dominatorTree,
|
||||
"There shouldn't be a dominator tree model yet since it is not computed " +
|
||||
"until we switch to the dominators view.");
|
||||
|
57
devtools/client/memory/test/unit/test_tree-map-01.js
Normal file
57
devtools/client/memory/test/unit/test_tree-map-01.js
Normal file
@ -0,0 +1,57 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const { drawBox } = require("devtools/client/memory/components/tree-map/draw");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function *() {
|
||||
let fillRectValues, strokeRectValues;
|
||||
let ctx = {
|
||||
fillRect: (...args) => fillRectValues = args,
|
||||
strokeRect: (...args) => strokeRectValues = args
|
||||
};
|
||||
let node = {
|
||||
x: 20,
|
||||
y: 30,
|
||||
dx: 50,
|
||||
dy: 70,
|
||||
type: "other",
|
||||
depth: 2
|
||||
};
|
||||
let borderWidth = () => 1;
|
||||
let dragZoom = {
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
zoom: 0
|
||||
};
|
||||
|
||||
drawBox(ctx, node, borderWidth, dragZoom);
|
||||
ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
|
||||
equal(ctx.fillStyle, "hsl(210,60%,70%)", "The fillStyle is set");
|
||||
equal(ctx.strokeStyle, "hsl(210,60%,35%)", "The strokeStyle is set");
|
||||
equal(ctx.lineWidth, 1, "The lineWidth is set");
|
||||
deepEqual(fillRectValues, [20.5,30.5,49,69], "Draws a filled rectangle");
|
||||
deepEqual(strokeRectValues, [20.5,30.5,49,69], "Draws a stroked rectangle");
|
||||
|
||||
|
||||
dragZoom.zoom = 0.5;
|
||||
|
||||
drawBox(ctx, node, borderWidth, dragZoom);
|
||||
ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
|
||||
deepEqual(fillRectValues, [30.5,45.5,74,104],
|
||||
"Draws a zoomed filled rectangle");
|
||||
deepEqual(strokeRectValues, [30.5,45.5,74,104],
|
||||
"Draws a zoomed stroked rectangle");
|
||||
|
||||
dragZoom.offsetX = 110;
|
||||
dragZoom.offsetY = 130;
|
||||
|
||||
drawBox(ctx, node, borderWidth, dragZoom);
|
||||
deepEqual(fillRectValues, [-79.5,-84.5,74,104],
|
||||
"Draws a zoomed and offset filled rectangle");
|
||||
deepEqual(strokeRectValues, [-79.5,-84.5,74,104],
|
||||
"Draws a zoomed and offset stroked rectangle");
|
||||
});
|
80
devtools/client/memory/test/unit/test_tree-map-02.js
Normal file
80
devtools/client/memory/test/unit/test_tree-map-02.js
Normal file
@ -0,0 +1,80 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { drawText } = require("devtools/client/memory/components/tree-map/draw");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function*() {
|
||||
// Mock out the Canvas2dContext
|
||||
let ctx = {
|
||||
fillText: (...args) => fillTextValues.push(args),
|
||||
measureText: (text) => {
|
||||
let width = text ? text.length * 10 : 0;
|
||||
return { width };
|
||||
}
|
||||
};
|
||||
let node = {
|
||||
x: 20,
|
||||
y: 30,
|
||||
dx: 500,
|
||||
dy: 70,
|
||||
name: "Example Node",
|
||||
totalBytes: 1200,
|
||||
totalCount: 100
|
||||
};
|
||||
let ratio = 0;
|
||||
let borderWidth = () => 1;
|
||||
let dragZoom = {
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
zoom: 0
|
||||
};
|
||||
let fillTextValues = [];
|
||||
|
||||
drawText(ctx, node, borderWidth, ratio, dragZoom);
|
||||
deepEqual(fillTextValues[0], ["Example Node", 21.5, 31.5],
|
||||
"Fills in the full node name");
|
||||
deepEqual(fillTextValues[1], ["1KiB 100 count", 151.5, 31.5],
|
||||
"Includes the full byte and count information");
|
||||
|
||||
fillTextValues = [];
|
||||
node.dx = 250;
|
||||
drawText(ctx, node, borderWidth, ratio, dragZoom);
|
||||
|
||||
deepEqual(fillTextValues[0], ["Example Node", 21.5, 31.5],
|
||||
"Fills in the full node name");
|
||||
deepEqual(fillTextValues[1], undefined,
|
||||
"Drops off the byte and count information if not enough room");
|
||||
|
||||
fillTextValues = [];
|
||||
node.dx = 100;
|
||||
drawText(ctx, node, borderWidth, ratio, dragZoom);
|
||||
|
||||
deepEqual(fillTextValues[0], ["Exampl...", 21.5, 31.5],
|
||||
"Cuts the name with ellipsis");
|
||||
deepEqual(fillTextValues[1], undefined,
|
||||
"Drops off the byte and count information if not enough room");
|
||||
|
||||
fillTextValues = [];
|
||||
node.dx = 40;
|
||||
drawText(ctx, node, borderWidth, ratio, dragZoom);
|
||||
|
||||
deepEqual(fillTextValues[0], ["...", 21.5, 31.5],
|
||||
"Shows only ellipsis when smaller");
|
||||
deepEqual(fillTextValues[1], undefined,
|
||||
"Drops off the byte and count information if not enough room");
|
||||
|
||||
fillTextValues = [];
|
||||
node.dx = 20;
|
||||
drawText(ctx, node, borderWidth, ratio, dragZoom);
|
||||
|
||||
deepEqual(fillTextValues[0], undefined,
|
||||
"Draw nothing when not enough room");
|
||||
deepEqual(fillTextValues[1], undefined,
|
||||
"Drops off the byte and count information if not enough room");
|
||||
});
|
@ -7,10 +7,11 @@
|
||||
* in `utils.getSnapshotTotals(snapshot)`
|
||||
*/
|
||||
|
||||
const { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
|
||||
const { censusDisplays, snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants");
|
||||
const { getSnapshotTotals } = require("devtools/client/memory/utils");
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -23,11 +24,13 @@ add_task(function *() {
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
yield dispatch(setCensusDisplayAndRefresh(heapWorker,
|
||||
censusDisplays.allocationStack));
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
|
||||
|
||||
ok(!getState().snapshots[0].census.display.inverted, "Snapshot is not inverted");
|
||||
|
||||
@ -45,8 +48,9 @@ add_task(function *() {
|
||||
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker,
|
||||
censusDisplays.invertedAllocationStack));
|
||||
yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
|
||||
yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
|
||||
ok(getState().snapshots[0].census.display.inverted, "Snapshot is inverted");
|
||||
|
||||
result = getSnapshotTotals(getState().snapshots[0].census);
|
||||
|
@ -51,4 +51,20 @@ add_task(function *() {
|
||||
equal(utils.formatPercent(12), "12%", "formatPercent returns 12% for 12");
|
||||
equal(utils.formatPercent(12345), "12 345%",
|
||||
"formatPercent returns 12 345% for 12345");
|
||||
|
||||
equal(utils.formatAbbreviatedBytes(12), "12B", "Formats bytes");
|
||||
equal(utils.formatAbbreviatedBytes(12345), "12KiB", "Formats kilobytes");
|
||||
equal(utils.formatAbbreviatedBytes(12345678), "11MiB", "Formats megabytes");
|
||||
equal(utils.formatAbbreviatedBytes(12345678912), "11GiB", "Formats gigabytes");
|
||||
|
||||
equal(utils.hslToStyle(0.5, 0.6, 0.7),
|
||||
"hsl(180,60%,70%)", "hslToStyle converts an array to a style string");
|
||||
equal(utils.hslToStyle(0, 0, 0),
|
||||
"hsl(0,0%,0%)", "hslToStyle converts an array to a style string");
|
||||
equal(utils.hslToStyle(1, 1, 1),
|
||||
"hsl(360,100%,100%)", "hslToStyle converts an array to a style string");
|
||||
|
||||
equal(utils.lerp(5, 7, 0), 5, "lerp return first number for 0");
|
||||
equal(utils.lerp(5, 7, 1), 7, "lerp return second number for 1");
|
||||
equal(utils.lerp(5, 7, 0.5), 6, "lerp interpolates the numbers for 0.5");
|
||||
});
|
||||
|
@ -42,5 +42,7 @@ skip-if = toolkit == 'android' || toolkit == 'gonk'
|
||||
[test_dominator_trees_07.js]
|
||||
[test_dominator_trees_08.js]
|
||||
[test_dominator_trees_09.js]
|
||||
[test_tree-map-01.js]
|
||||
[test_tree-map-02.js]
|
||||
[test_utils.js]
|
||||
[test_utils-get-snapshot-totals.js]
|
||||
|
@ -13,10 +13,16 @@ const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { Preferences } = require("resource://gre/modules/Preferences.jsm");
|
||||
const CUSTOM_CENSUS_DISPLAY_PREF = "devtools.memory.custom-census-displays";
|
||||
const CUSTOM_DOMINATOR_TREE_DISPLAY_PREF = "devtools.memory.custom-dominator-tree-displays";
|
||||
const CUSTOM_TREE_MAP_DISPLAY_PREF = "devtools.memory.custom-tree-map-displays";
|
||||
const BYTES = 1024;
|
||||
const KILOBYTES = Math.pow(BYTES, 2);
|
||||
const MEGABYTES = Math.pow(BYTES, 3);
|
||||
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
const {
|
||||
snapshotState: states,
|
||||
diffingState,
|
||||
censusState,
|
||||
treeMapState,
|
||||
censusDisplays,
|
||||
dominatorTreeDisplays,
|
||||
dominatorTreeState
|
||||
@ -79,6 +85,16 @@ exports.getCustomDominatorTreeDisplays = function () {
|
||||
return getCustomDisplaysHelper(CUSTOM_DOMINATOR_TREE_DISPLAY_PREF);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns custom displays defined in
|
||||
* `devtools.memory.custom-tree-map-displays` pref.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
exports.getCustomTreeMapDisplays = function () {
|
||||
return getCustomDisplaysHelper(CUSTOM_TREE_MAP_DISPLAY_PREF);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a string representing a readable form of the snapshot's state. More
|
||||
* concise than `getStatusTextFull`.
|
||||
@ -106,9 +122,12 @@ exports.getStatusText = function (state) {
|
||||
case states.READING:
|
||||
return L10N.getStr("snapshot.state.reading");
|
||||
|
||||
case states.SAVING_CENSUS:
|
||||
case censusState.SAVING:
|
||||
return L10N.getStr("snapshot.state.saving-census");
|
||||
|
||||
case treeMapState.SAVING:
|
||||
return L10N.getStr("snapshot.state.saving-tree-map");
|
||||
|
||||
case diffingState.TAKING_DIFF:
|
||||
return L10N.getStr("diffing.state.taking-diff");
|
||||
|
||||
@ -133,7 +152,8 @@ exports.getStatusText = function (state) {
|
||||
case dominatorTreeState.LOADED:
|
||||
case diffingState.TOOK_DIFF:
|
||||
case states.READ:
|
||||
case states.SAVED_CENSUS:
|
||||
case censusState.SAVED:
|
||||
case treeMapState.SAVED:
|
||||
return "";
|
||||
|
||||
default:
|
||||
@ -169,9 +189,12 @@ exports.getStatusTextFull = function (state) {
|
||||
case states.READING:
|
||||
return L10N.getStr("snapshot.state.reading.full");
|
||||
|
||||
case states.SAVING_CENSUS:
|
||||
case censusState.SAVING:
|
||||
return L10N.getStr("snapshot.state.saving-census.full");
|
||||
|
||||
case treeMapState.SAVING:
|
||||
return L10N.getStr("snapshot.state.saving-tree-map.full");
|
||||
|
||||
case diffingState.TAKING_DIFF:
|
||||
return L10N.getStr("diffing.state.taking-diff.full");
|
||||
|
||||
@ -196,7 +219,8 @@ exports.getStatusTextFull = function (state) {
|
||||
case dominatorTreeState.LOADED:
|
||||
case diffingState.TOOK_DIFF:
|
||||
case states.READ:
|
||||
case states.SAVED_CENSUS:
|
||||
case censusState.SAVED:
|
||||
case treeMapState.SAVED:
|
||||
return "";
|
||||
|
||||
default:
|
||||
@ -212,8 +236,8 @@ exports.getStatusTextFull = function (state) {
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
exports.snapshotIsDiffable = function snapshotIsDiffable(snapshot) {
|
||||
return snapshot.state === states.SAVED_CENSUS
|
||||
|| snapshot.state === states.SAVING_CENSUS
|
||||
return (snapshot.census && snapshot.census.state === censusState.SAVED)
|
||||
|| (snapshot.census && snapshot.census.state === censusState.SAVING)
|
||||
|| snapshot.state === states.SAVED
|
||||
|| snapshot.state === states.READ;
|
||||
};
|
||||
@ -256,6 +280,7 @@ exports.createSnapshot = function createSnapshot(state) {
|
||||
state: states.SAVING,
|
||||
dominatorTree,
|
||||
census: null,
|
||||
treeMap: null,
|
||||
path: null,
|
||||
imported: false,
|
||||
selected: false,
|
||||
@ -275,10 +300,25 @@ exports.createSnapshot = function createSnapshot(state) {
|
||||
*/
|
||||
exports.censusIsUpToDate = function (filter, display, census) {
|
||||
return census
|
||||
&& filter === census.filter
|
||||
// Filter could be null == undefined so use loose equality.
|
||||
&& filter == census.filter
|
||||
&& display === census.display;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check to see if the snapshot is in a state that it can take a census.
|
||||
*
|
||||
* @param {SnapshotModel} A snapshot to check.
|
||||
* @param {Boolean} Assert that the snapshot must be in a ready state.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
exports.canTakeCensus = function (snapshot) {
|
||||
return snapshot.state === states.READ &&
|
||||
(!snapshot.census || snapshot.census.state === censusState.SAVED) &&
|
||||
(!snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given snapshot's dominator tree has been computed, false
|
||||
* otherwise.
|
||||
@ -293,6 +333,23 @@ exports.dominatorTreeIsComputed = function (snapshot) {
|
||||
snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the first SAVED census, either from the tree map or the normal
|
||||
* census.
|
||||
*
|
||||
* @param {SnapshotModel} snapshot
|
||||
* @returns {Object|null} Either the census, or null if one hasn't completed
|
||||
*/
|
||||
exports.getSavedCensus = function (snapshot) {
|
||||
if (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) {
|
||||
return snapshot.treeMap;
|
||||
}
|
||||
if (snapshot.census && snapshot.census.state === censusState.SAVED) {
|
||||
return snapshot.census;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a snapshot and returns the total bytes and total count that this
|
||||
* snapshot represents.
|
||||
@ -394,3 +451,54 @@ exports.formatPercent = function(percent, showSign = false) {
|
||||
return exports.L10N.getFormatStr("tree-item.percent",
|
||||
exports.formatNumber(percent, showSign));
|
||||
};
|
||||
|
||||
/**
|
||||
* Change an HSL color array with values ranged 0-1 to a properly formatted
|
||||
* ctx.fillStyle string.
|
||||
*
|
||||
* @param {Number} h
|
||||
* hue values ranged between [0 - 1]
|
||||
* @param {Number} s
|
||||
* hue values ranged between [0 - 1]
|
||||
* @param {Number} l
|
||||
* hue values ranged between [0 - 1]
|
||||
* @return {type}
|
||||
*/
|
||||
exports.hslToStyle = function(h, s, l) {
|
||||
h = parseInt(h * 360, 10);
|
||||
s = parseInt(s * 100, 10);
|
||||
l = parseInt(l * 100, 10);
|
||||
|
||||
return `hsl(${h},${s}%,${l}%)`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Linearly interpolate between 2 numbers.
|
||||
*
|
||||
* @param {Number} a
|
||||
* @param {Number} b
|
||||
* @param {Number} t
|
||||
* A value of 0 returns a, and 1 returns b
|
||||
* @return {Number}
|
||||
*/
|
||||
exports.lerp = function(a, b, t) {
|
||||
return a * (1 - t) + b * t;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a number of bytes as human readable, e.g. 13434 => '13KiB'.
|
||||
*
|
||||
* @param {Number} n
|
||||
* Number of bytes
|
||||
* @return {String}
|
||||
*/
|
||||
exports.formatAbbreviatedBytes = function(n) {
|
||||
if (n < BYTES) {
|
||||
return n + "B";
|
||||
} else if (n < KILOBYTES) {
|
||||
return Math.floor(n / BYTES) + "KiB";
|
||||
} else if (n < MEGABYTES) {
|
||||
return Math.floor(n / KILOBYTES) + "MiB";
|
||||
}
|
||||
return Math.floor(n / MEGABYTES) + "GiB";
|
||||
};
|
||||
|
@ -109,6 +109,7 @@ pref("devtools.memory.enabled", false);
|
||||
|
||||
pref("devtools.memory.custom-census-displays", "{}");
|
||||
pref("devtools.memory.custom-dominator-tree-displays", "{}");
|
||||
pref("devtools.memory.custom-tree-map-displays", "{}");
|
||||
|
||||
// Enable the Performance tools
|
||||
pref("devtools.performance.enabled", true);
|
||||
|
@ -494,6 +494,19 @@ html, body, #app, #memory-tool {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tree map
|
||||
*/
|
||||
|
||||
.tree-map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-top: -1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heap tree errors.
|
||||
*/
|
||||
|
@ -204,11 +204,15 @@ state machine describing the snapshot states. Any of these states may go to the
|
||||
ERROR state, from which they can never leave.
|
||||
|
||||
```
|
||||
SAVING → SAVED → READING → READ SAVED_CENSUS
|
||||
↗ ↘ ↑ ↓
|
||||
IMPORTING SAVING_CENSUS
|
||||
SAVING → SAVED → READING → READ
|
||||
↗
|
||||
IMPORTING
|
||||
```
|
||||
|
||||
Each of the report types (census, diffing, tree maps, dominators) have their own states as well, and are documented at `devtools/client/memory/constants.js`.
|
||||
These report states are updated as the various filtering and selecting options
|
||||
are updated in the UI.
|
||||
|
||||
### Testing the Frontend
|
||||
|
||||
Unit tests for React components are in `devtools/client/memory/test/chrome/*`.
|
||||
|
Loading…
Reference in New Issue
Block a user