mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-02 15:15:23 +00:00
cd2c23a435
This adds the INDIVIDUALS view state to the memory panel, renames "dominator tree display" to "label display", and adds a view for listing the individual nodes in a census group and inspecting each nodes' retaining paths. --HG-- rename : devtools/client/memory/actions/dominator-tree-display.js => devtools/client/memory/actions/label-display.js rename : devtools/client/memory/reducers/dominator-tree-display.js => devtools/client/memory/reducers/label-display.js
520 lines
17 KiB
JavaScript
520 lines
17 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const { assert } = require("devtools/shared/DevToolsUtils");
|
|
const { MemoryFront } = require("devtools/server/actors/memory");
|
|
const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
|
|
const { PropTypes } = require("devtools/client/shared/vendor/react");
|
|
const {
|
|
snapshotState: states,
|
|
diffingState,
|
|
dominatorTreeState,
|
|
viewState,
|
|
individualsState,
|
|
} = require("./constants");
|
|
|
|
/**
|
|
* ONLY USE THIS FOR MODEL VALIDATORS IN CONJUCTION WITH assert()!
|
|
*
|
|
* React checks that the returned values from validator functions are instances
|
|
* of Error, but because React is loaded in its own global, that check is always
|
|
* false and always results in a warning.
|
|
*
|
|
* To work around this and still get model validation, just call assert() inside
|
|
* a function passed to catchAndIgnore. The assert() function will still report
|
|
* assertion failures, but this funciton will swallow the errors so that React
|
|
* doesn't go crazy and drown out the real error in irrelevant and incorrect
|
|
* warnings.
|
|
*
|
|
* Example usage:
|
|
*
|
|
* const MyModel = PropTypes.shape({
|
|
* someProperty: catchAndIgnore(function (model) {
|
|
* assert(someInvariant(model.someProperty), "Should blah blah");
|
|
* })
|
|
* });
|
|
*/
|
|
function catchAndIgnore(fn) {
|
|
return function (...args) {
|
|
try {
|
|
fn(...args);
|
|
} catch (err) { }
|
|
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The data describing the census report's shape, and its associated metadata.
|
|
*
|
|
* @see `js/src/doc/Debugger/Debugger.Memory.md`
|
|
*/
|
|
const censusDisplayModel = exports.censusDisplay = PropTypes.shape({
|
|
displayName: PropTypes.string.isRequired,
|
|
tooltip: PropTypes.string.isRequired,
|
|
inverted: PropTypes.bool.isRequired,
|
|
breakdown: PropTypes.shape({
|
|
by: PropTypes.string.isRequired,
|
|
})
|
|
});
|
|
|
|
/**
|
|
* How we want to label nodes in the dominator tree, and associated
|
|
* metadata. The notable difference from `censusDisplayModel` is the lack of
|
|
* an `inverted` property.
|
|
*
|
|
* @see `js/src/doc/Debugger/Debugger.Memory.md`
|
|
*/
|
|
const labelDisplayModel = exports.labelDisplay = PropTypes.shape({
|
|
displayName: PropTypes.string.isRequired,
|
|
tooltip: PropTypes.string.isRequired,
|
|
breakdown: PropTypes.shape({
|
|
by: PropTypes.string.isRequired,
|
|
})
|
|
});
|
|
|
|
/**
|
|
* The data describing the tree map's shape, and its associated metadata.
|
|
*
|
|
* @see `js/src/doc/Debugger/Debugger.Memory.md`
|
|
*/
|
|
const treeMapDisplayModel = exports.treeMapDisplay = PropTypes.shape({
|
|
displayName: PropTypes.string.isRequired,
|
|
tooltip: PropTypes.string.isRequired,
|
|
inverted: PropTypes.bool.isRequired,
|
|
breakdown: PropTypes.shape({
|
|
by: PropTypes.string.isRequired,
|
|
})
|
|
});
|
|
|
|
/**
|
|
* Tree map model.
|
|
*/
|
|
const treeMapModel = exports.treeMapModel = PropTypes.shape({
|
|
// The current census report data.
|
|
report: PropTypes.object,
|
|
// The display data used to generate the current census.
|
|
display: treeMapDisplayModel,
|
|
// The current treeMapState this is in
|
|
state: catchAndIgnore(function (treeMap) {
|
|
switch (treeMap.state) {
|
|
case treeMapState.SAVING:
|
|
assert(!treeMap.report, "Should not have a report");
|
|
assert(!treeMap.error, "Should not have an error");
|
|
break;
|
|
case treeMapState.SAVED:
|
|
assert(treeMap.report, "Should have a report");
|
|
assert(!treeMap.error, "Should not have an error");
|
|
break;
|
|
|
|
case treeMapState.ERROR:
|
|
assert(treeMap.error, "Should have an error");
|
|
break;
|
|
|
|
default:
|
|
assert(false, `Unexpected treeMap state: ${treeMap.state}`);
|
|
}
|
|
})
|
|
});
|
|
|
|
let censusModel = exports.censusModel = PropTypes.shape({
|
|
// The current census report data.
|
|
report: PropTypes.object,
|
|
// The parent map for the report.
|
|
parentMap: PropTypes.object,
|
|
// The display data used to generate the current census.
|
|
display: censusDisplayModel,
|
|
// If present, the currently cached report's filter string used for pruning
|
|
// the tree items.
|
|
filter: PropTypes.string,
|
|
// The Immutable.Set<CensusTreeNode.id> of expanded node ids in the report
|
|
// tree.
|
|
expanded: catchAndIgnore(function (census) {
|
|
if (census.report) {
|
|
assert(census.expanded,
|
|
"If we have a report, we should also have the set of expanded nodes");
|
|
}
|
|
}),
|
|
// If a node is currently focused in the report tree, then this is it.
|
|
focused: PropTypes.object,
|
|
// The censusModelState that this census is currently in.
|
|
state: catchAndIgnore(function (census) {
|
|
switch (census.state) {
|
|
case censusState.SAVING:
|
|
assert(!census.report, "Should not have a report");
|
|
assert(!census.parentMap, "Should not have a parent map");
|
|
assert(census.expanded, "Should not have an expanded set");
|
|
assert(!census.error, "Should not have an error");
|
|
break;
|
|
|
|
case censusState.SAVED:
|
|
assert(census.report, "Should have a report");
|
|
assert(census.parentMap, "Should have a parent map");
|
|
assert(census.expanded, "Should have an expanded set");
|
|
assert(!census.error, "Should not have an error");
|
|
break;
|
|
|
|
case censusState.ERROR:
|
|
assert(!census.report, "Should not have a report");
|
|
assert(census.error, "Should have an error");
|
|
break;
|
|
|
|
default:
|
|
assert(false, `Unexpected census state: ${census.state}`);
|
|
}
|
|
})
|
|
});
|
|
|
|
/**
|
|
* Dominator tree model.
|
|
*/
|
|
let dominatorTreeModel = exports.dominatorTreeModel = PropTypes.shape({
|
|
// The id of this dominator tree.
|
|
dominatorTreeId: PropTypes.number,
|
|
|
|
// The root DominatorTreeNode of this dominator tree.
|
|
root: PropTypes.object,
|
|
|
|
// The Set<NodeId> of expanded nodes in this dominator tree.
|
|
expanded: PropTypes.object,
|
|
|
|
// If a node is currently focused in the dominator tree, then this is it.
|
|
focused: PropTypes.object,
|
|
|
|
// If an error was thrown while getting this dominator tree, the `Error`
|
|
// instance (or an error string message) is attached here.
|
|
error: PropTypes.oneOfType([
|
|
PropTypes.string,
|
|
PropTypes.object,
|
|
]),
|
|
|
|
// The display used to generate descriptive labels of nodes in this dominator
|
|
// tree.
|
|
display: labelDisplayModel,
|
|
|
|
// The number of active requests to incrementally fetch subtrees. This should
|
|
// only be non-zero when the state is INCREMENTAL_FETCHING.
|
|
activeFetchRequestCount: PropTypes.number,
|
|
|
|
// The dominatorTreeState that this domintor tree is currently in.
|
|
state: catchAndIgnore(function (dominatorTree) {
|
|
switch (dominatorTree.state) {
|
|
case dominatorTreeState.COMPUTING:
|
|
assert(dominatorTree.dominatorTreeId == null,
|
|
"Should not have a dominator tree id yet");
|
|
assert(!dominatorTree.root,
|
|
"Should not have the root of the tree yet");
|
|
assert(!dominatorTree.error,
|
|
"Should not have an error");
|
|
break;
|
|
|
|
case dominatorTreeState.COMPUTED:
|
|
case dominatorTreeState.FETCHING:
|
|
assert(dominatorTree.dominatorTreeId != null,
|
|
"Should have a dominator tree id");
|
|
assert(!dominatorTree.root,
|
|
"Should not have the root of the tree yet");
|
|
assert(!dominatorTree.error,
|
|
"Should not have an error");
|
|
break;
|
|
|
|
case dominatorTreeState.INCREMENTAL_FETCHING:
|
|
assert(typeof dominatorTree.activeFetchRequestCount === "number",
|
|
"The active fetch request count is a number when we are in the " +
|
|
"INCREMENTAL_FETCHING state");
|
|
assert(dominatorTree.activeFetchRequestCount > 0,
|
|
"We are keeping track of how many active requests are in flight.");
|
|
// Fall through...
|
|
case dominatorTreeState.LOADED:
|
|
assert(dominatorTree.dominatorTreeId != null,
|
|
"Should have a dominator tree id");
|
|
assert(dominatorTree.root,
|
|
"Should have the root of the tree");
|
|
assert(dominatorTree.expanded,
|
|
"Should have an expanded set");
|
|
assert(!dominatorTree.error,
|
|
"Should not have an error");
|
|
break;
|
|
|
|
case dominatorTreeState.ERROR:
|
|
assert(dominatorTree.error, "Should have an error");
|
|
break;
|
|
|
|
default:
|
|
assert(false,
|
|
`Unexpected dominator tree state: ${dominatorTree.state}`);
|
|
}
|
|
}),
|
|
});
|
|
|
|
/**
|
|
* Snapshot model.
|
|
*/
|
|
let stateKeys = Object.keys(states).map(state => states[state]);
|
|
const snapshotId = PropTypes.number;
|
|
let snapshotModel = exports.snapshot = PropTypes.shape({
|
|
// Unique ID for a snapshot
|
|
id: snapshotId.isRequired,
|
|
// Whether or not this snapshot is currently selected.
|
|
selected: PropTypes.bool.isRequired,
|
|
// Filesystem path to where the snapshot is stored; used to identify the
|
|
// snapshot for HeapAnalysesClient.
|
|
path: PropTypes.string,
|
|
// Current census data for this snapshot.
|
|
census: censusModel,
|
|
// Current dominator tree data for this snapshot.
|
|
dominatorTree: dominatorTreeModel,
|
|
// Current tree map data for this snapshot.
|
|
treeMap: treeMapModel,
|
|
// If an error was thrown while processing this snapshot, the `Error` instance
|
|
// is attached here.
|
|
error: PropTypes.object,
|
|
// Boolean indicating whether or not this snapshot was imported.
|
|
imported: PropTypes.bool.isRequired,
|
|
// The creation time of the snapshot; required after the snapshot has been
|
|
// read.
|
|
creationTime: PropTypes.number,
|
|
// The current state the snapshot is in.
|
|
// @see ./constants.js
|
|
state: catchAndIgnore(function (snapshot, propName) {
|
|
let current = snapshot.state;
|
|
let shouldHavePath = [states.IMPORTING, states.SAVED, states.READ];
|
|
let shouldHaveCreationTime = [states.READ];
|
|
|
|
if (!stateKeys.includes(current)) {
|
|
throw new Error(`Snapshot state must be one of ${stateKeys}.`);
|
|
}
|
|
if (shouldHavePath.includes(current) && !snapshot.path) {
|
|
throw new Error(`Snapshots in state ${current} must have a snapshot path.`);
|
|
}
|
|
if (shouldHaveCreationTime.includes(current) && !snapshot.creationTime) {
|
|
throw new Error(`Snapshots in state ${current} must have a creation time.`);
|
|
}
|
|
}),
|
|
});
|
|
|
|
let allocationsModel = exports.allocations = PropTypes.shape({
|
|
// True iff we are recording allocation stacks right now.
|
|
recording: PropTypes.bool.isRequired,
|
|
// True iff we are in the process of toggling the recording of allocation
|
|
// stacks on or off right now.
|
|
togglingInProgress: PropTypes.bool.isRequired,
|
|
});
|
|
|
|
let diffingModel = exports.diffingModel = PropTypes.shape({
|
|
// The id of the first snapshot to diff.
|
|
firstSnapshotId: snapshotId,
|
|
|
|
// The id of the second snapshot to diff.
|
|
secondSnapshotId: catchAndIgnore(function (diffing, propName) {
|
|
if (diffing.secondSnapshotId && !diffing.firstSnapshotId) {
|
|
throw new Error("Cannot have second snapshot without already having " +
|
|
"first snapshot");
|
|
}
|
|
return snapshotId(diffing, propName);
|
|
}),
|
|
|
|
// The current census data for the diffing.
|
|
census: censusModel,
|
|
|
|
// If an error was thrown while diffing, the `Error` instance is attached
|
|
// here.
|
|
error: PropTypes.object,
|
|
|
|
// The current state the diffing is in.
|
|
// @see ./constants.js
|
|
state: catchAndIgnore(function (diffing) {
|
|
switch (diffing.state) {
|
|
case diffingState.TOOK_DIFF:
|
|
assert(diffing.census, "If we took a diff, we should have a census");
|
|
// Fall through...
|
|
case diffingState.TAKING_DIFF:
|
|
assert(diffing.firstSnapshotId, "Should have first snapshot");
|
|
assert(diffing.secondSnapshotId, "Should have second snapshot");
|
|
break;
|
|
|
|
case diffingState.SELECTING:
|
|
break;
|
|
|
|
case diffingState.ERROR:
|
|
assert(diffing.error, "Should have error");
|
|
break;
|
|
|
|
default:
|
|
assert(false, `Bad diffing state: ${diffing.state}`);
|
|
}
|
|
}),
|
|
});
|
|
|
|
let previousViewModel = exports.previousView = PropTypes.shape({
|
|
state: catchAndIgnore(function (previous) {
|
|
switch (previous.state) {
|
|
case viewState.DIFFING:
|
|
assert(previous.diffing, "Should have previous diffing state.");
|
|
assert(!previous.selected, "Should not have a previously selected snapshot.")
|
|
break;
|
|
|
|
case viewState.CENSUS:
|
|
case viewState.DOMINATOR_TREE:
|
|
case viewState.TREE_MAP:
|
|
assert(previous.selected, "Should have a previously selected snapshot.");
|
|
break;
|
|
|
|
case viewState.INDIVIDUALS:
|
|
default:
|
|
assert(false, `Unexpected previous view state: ${previous.state}.`);
|
|
}
|
|
}),
|
|
|
|
// The previous diffing state, if any.
|
|
diffing: diffingModel,
|
|
|
|
// The previously selected snapshot, if any.
|
|
selected: snapshotId,
|
|
});
|
|
|
|
let viewModel = exports.view = PropTypes.shape({
|
|
// The current view state.
|
|
state: catchAndIgnore(function (view) {
|
|
switch (view.state) {
|
|
case viewState.DIFFING:
|
|
case viewState.CENSUS:
|
|
case viewState.DOMINATOR_TREE:
|
|
case viewState.INDIVIDUALS:
|
|
case viewState.TREE_MAP:
|
|
break;
|
|
|
|
default:
|
|
assert(false, `Unexpected type of view: ${view.state}`);
|
|
}
|
|
}),
|
|
|
|
// The previous view state.
|
|
previous: previousViewModel,
|
|
});
|
|
|
|
const individualsModel = exports.individuals = PropTypes.shape({
|
|
error: PropTypes.object,
|
|
|
|
nodes: PropTypes.arrayOf(PropTypes.object),
|
|
|
|
dominatorTree: dominatorTreeModel,
|
|
|
|
id: snapshotId,
|
|
|
|
censusBreakdown: PropTypes.object,
|
|
|
|
indices: PropTypes.object,
|
|
|
|
labelDisplay: labelDisplayModel,
|
|
|
|
focused: PropTypes.object,
|
|
|
|
state: catchAndIgnore(function(individuals) {
|
|
switch (individuals.state) {
|
|
case individualsState.COMPUTING_DOMINATOR_TREE:
|
|
case individualsState.FETCHING:
|
|
assert(!individuals.nodes, "Should not have individual nodes");
|
|
assert(!individuals.dominatorTree, "Should not have dominator tree");
|
|
assert(!individuals.id, "Should not have an id");
|
|
assert(!individuals.censusBreakdown, "Should not have a censusBreakdown");
|
|
assert(!individuals.indices, "Should not have indices");
|
|
assert(!individuals.labelDisplay, "Should not have a labelDisplay");
|
|
break;
|
|
|
|
case individualsState.FETCHED:
|
|
assert(individuals.nodes, "Should have individual nodes");
|
|
assert(individuals.dominatorTree, "Should have dominator tree");
|
|
assert(individuals.id, "Should have an id");
|
|
assert(individuals.censusBreakdown, "Should have a censusBreakdown");
|
|
assert(individuals.indices, "Should have indices");
|
|
assert(individuals.labelDisplay, "Should have a labelDisplay");
|
|
break;
|
|
|
|
case individualsState.ERROR:
|
|
assert(individuals.error, "Should have an error object");
|
|
break;
|
|
|
|
default:
|
|
assert(false, `Unexpected individuals state: ${individuals.state}`);
|
|
break;
|
|
}
|
|
}),
|
|
});
|
|
|
|
let appModel = exports.app = {
|
|
// {MemoryFront} Used to communicate with platform
|
|
front: PropTypes.instanceOf(MemoryFront),
|
|
|
|
// Allocations recording related data.
|
|
allocations: allocationsModel.isRequired,
|
|
|
|
// {HeapAnalysesClient} Used to interface with snapshots
|
|
heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
|
|
|
|
// The display data describing how we want the census data to be.
|
|
censusDisplay: censusDisplayModel.isRequired,
|
|
|
|
// The display data describing how we want the dominator tree labels to be
|
|
// computed.
|
|
labelDisplay: labelDisplayModel.isRequired,
|
|
|
|
// The display data describing how we want the dominator tree labels to be
|
|
// computed.
|
|
treeMapDisplay: treeMapDisplayModel.isRequired,
|
|
|
|
// List of reference to all snapshots taken
|
|
snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
|
|
|
|
// If present, a filter string for pruning the tree items.
|
|
filter: PropTypes.string,
|
|
|
|
// If present, the current diffing state.
|
|
diffing: diffingModel,
|
|
|
|
// If present, the current individuals state.
|
|
individuals: individualsModel,
|
|
|
|
// The current type of view.
|
|
view: function(app) {
|
|
viewModel.isRequired(app, "view");
|
|
|
|
catchAndIgnore(function(app) {
|
|
switch (app.view.state) {
|
|
case viewState.DIFFING:
|
|
assert(app.diffing, "Should be diffing");
|
|
break;
|
|
|
|
case viewState.INDIVIDUALS:
|
|
case viewState.CENSUS:
|
|
case viewState.DOMINATOR_TREE:
|
|
case viewState.TREE_MAP:
|
|
assert(!app.diffing, "Should not be diffing");
|
|
break;
|
|
|
|
default:
|
|
assert(false, `Unexpected type of view: ${view.state}`);
|
|
}
|
|
})(app);
|
|
|
|
catchAndIgnore(function(app) {
|
|
switch (app.view.state) {
|
|
case viewState.INDIVIDUALS:
|
|
assert(app.individuals, "Should have individuals state");
|
|
break;
|
|
|
|
case viewState.DIFFING:
|
|
case viewState.CENSUS:
|
|
case viewState.DOMINATOR_TREE:
|
|
case viewState.TREE_MAP:
|
|
assert(!app.individuals, "Should not have individuals state");
|
|
break;
|
|
|
|
default:
|
|
assert(false, `Unexpected type of view: ${view.state}`);
|
|
}
|
|
})(app);
|
|
},
|
|
};
|