mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 15:58:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
20
devtools/client/memory/actions/allocations.js
Normal file
20
devtools/client/memory/actions/allocations.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* 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, ALLOCATION_RECORDING_OPTIONS } = require("../constants");
|
||||
|
||||
exports.toggleRecordingAllocationStacks = function (front) {
|
||||
return function* (dispatch, getState) {
|
||||
dispatch({ type: actions.TOGGLE_RECORD_ALLOCATION_STACKS_START });
|
||||
|
||||
if (getState().recordingAllocationStacks) {
|
||||
yield front.stopRecordingAllocations();
|
||||
} else {
|
||||
yield front.startRecordingAllocations(ALLOCATION_RECORDING_OPTIONS);
|
||||
}
|
||||
|
||||
dispatch({ type: actions.TOGGLE_RECORD_ALLOCATION_STACKS_END });
|
||||
};
|
||||
};
|
||||
34
devtools/client/memory/actions/census-display.js
Normal file
34
devtools/client/memory/actions/census-display.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/* 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");
|
||||
|
||||
exports.setCensusDisplayAndRefresh = function (heapWorker, display) {
|
||||
return function* (dispatch, getState) {
|
||||
dispatch(setCensusDisplay(display));
|
||||
yield dispatch(refresh(heapWorker));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears out all cached census data in the snapshots and sets new display data
|
||||
* for censuses.
|
||||
*
|
||||
* @param {censusDisplayModel} display
|
||||
*/
|
||||
const setCensusDisplay = exports.setCensusDisplay = 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_CENSUS_DISPLAY,
|
||||
display,
|
||||
};
|
||||
};
|
||||
201
devtools/client/memory/actions/diffing.js
Normal file
201
devtools/client/memory/actions/diffing.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { assert, reportException } = require("devtools/shared/DevToolsUtils");
|
||||
const { actions, diffingState, viewState } = require("../constants");
|
||||
const telemetry = require("../telemetry");
|
||||
const {
|
||||
getSnapshot,
|
||||
censusIsUpToDate,
|
||||
snapshotIsDiffable,
|
||||
findSelectedSnapshot,
|
||||
} = require("../utils");
|
||||
// This is a circular dependency, so do not destructure the needed properties.
|
||||
const snapshotActions = require("./snapshot");
|
||||
|
||||
/**
|
||||
* Toggle diffing mode on or off.
|
||||
*/
|
||||
const toggleDiffing = exports.toggleDiffing = function () {
|
||||
return function (dispatch, getState) {
|
||||
dispatch({
|
||||
type: actions.CHANGE_VIEW,
|
||||
newViewState: getState().diffing ? viewState.CENSUS : viewState.DIFFING,
|
||||
oldDiffing: getState().diffing,
|
||||
oldSelected: findSelectedSnapshot(getState()),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Select the given snapshot for diffing.
|
||||
*
|
||||
* @param {snapshotModel} snapshot
|
||||
*/
|
||||
const selectSnapshotForDiffing = exports.selectSnapshotForDiffing = function (snapshot) {
|
||||
assert(snapshotIsDiffable(snapshot),
|
||||
"To select a snapshot for diffing, it must be diffable");
|
||||
return { type: actions.SELECT_SNAPSHOT_FOR_DIFFING, snapshot };
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the difference between the first and second snapshots.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {snapshotModel} first
|
||||
* @param {snapshotModel} second
|
||||
*/
|
||||
const takeCensusDiff = exports.takeCensusDiff = function (heapWorker, first, second) {
|
||||
return function* (dispatch, getState) {
|
||||
assert(snapshotIsDiffable(first),
|
||||
`First snapshot must be in a diffable state, found ${first.state}`);
|
||||
assert(snapshotIsDiffable(second),
|
||||
`Second snapshot must be in a diffable state, found ${second.state}`);
|
||||
|
||||
let report, parentMap;
|
||||
let display = getState().censusDisplay;
|
||||
let filter = getState().filter;
|
||||
|
||||
if (censusIsUpToDate(filter, display, getState().diffing.census)) {
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
if (!getState().diffing
|
||||
|| getState().diffing.firstSnapshotId !== first.id
|
||||
|| getState().diffing.secondSnapshotId !== second.id) {
|
||||
// If we stopped diffing or stopped and then started diffing a different
|
||||
// pair of snapshots, then just give up with diffing this pair. In the
|
||||
// latter case, a newly spawned task will handle the diffing for the new
|
||||
// pair.
|
||||
return;
|
||||
}
|
||||
|
||||
display = getState().censusDisplay;
|
||||
filter = getState().filter;
|
||||
|
||||
dispatch({
|
||||
type: actions.TAKE_CENSUS_DIFF_START,
|
||||
first,
|
||||
second,
|
||||
filter,
|
||||
display,
|
||||
});
|
||||
|
||||
let opts = display.inverted
|
||||
? { asInvertedTreeNode: true }
|
||||
: { asTreeNode: true };
|
||||
opts.filter = filter || null;
|
||||
|
||||
try {
|
||||
({ delta: report, parentMap } = yield heapWorker.takeCensusDiff(
|
||||
first.path,
|
||||
second.path,
|
||||
{ breakdown: display.breakdown },
|
||||
opts));
|
||||
} catch (error) {
|
||||
reportException("actions/diffing/takeCensusDiff", error);
|
||||
dispatch({ type: actions.DIFFING_ERROR, error });
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (filter !== getState().filter
|
||||
|| display !== getState().censusDisplay);
|
||||
|
||||
dispatch({
|
||||
type: actions.TAKE_CENSUS_DIFF_END,
|
||||
first,
|
||||
second,
|
||||
report,
|
||||
parentMap,
|
||||
filter,
|
||||
display,
|
||||
});
|
||||
|
||||
telemetry.countDiff({ filter, display });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure that the current diffing data is up to date with the currently
|
||||
* selected display, filter, etc. If the state is not up-to-date, then a
|
||||
* recompute is triggered.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
*/
|
||||
const refreshDiffing = exports.refreshDiffing = function (heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
if (getState().diffing.secondSnapshotId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(getState().diffing.firstSnapshotId,
|
||||
"Should have first snapshot id");
|
||||
|
||||
if (getState().diffing.state === diffingState.TAKING_DIFF) {
|
||||
// There is an existing task that will ensure that the diffing data is
|
||||
// up-to-date.
|
||||
return;
|
||||
}
|
||||
|
||||
const { firstSnapshotId, secondSnapshotId } = getState().diffing;
|
||||
|
||||
const first = getSnapshot(getState(), firstSnapshotId);
|
||||
const second = getSnapshot(getState(), secondSnapshotId);
|
||||
dispatch(takeCensusDiff(heapWorker, first, second));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Select the given snapshot for diffing and refresh the diffing data if
|
||||
* necessary (for example, if two snapshots are now selected for diffing).
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {snapshotModel} snapshot
|
||||
*/
|
||||
const selectSnapshotForDiffingAndRefresh = exports.selectSnapshotForDiffingAndRefresh = function (heapWorker, snapshot) {
|
||||
return function* (dispatch, getState) {
|
||||
assert(getState().diffing,
|
||||
"If we are selecting for diffing, we must be in diffing mode");
|
||||
dispatch(selectSnapshotForDiffing(snapshot));
|
||||
yield dispatch(refreshDiffing(heapWorker));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand the given node in the diffing's census's delta-report.
|
||||
*
|
||||
* @param {CensusTreeNode} node
|
||||
*/
|
||||
const expandDiffingCensusNode = exports.expandDiffingCensusNode = function (node) {
|
||||
return {
|
||||
type: actions.EXPAND_DIFFING_CENSUS_NODE,
|
||||
node,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapse the given node in the diffing's census's delta-report.
|
||||
*
|
||||
* @param {CensusTreeNode} node
|
||||
*/
|
||||
const collapseDiffingCensusNode = exports.collapseDiffingCensusNode = function (node) {
|
||||
return {
|
||||
type: actions.COLLAPSE_DIFFING_CENSUS_NODE,
|
||||
node,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the given node in the snapshot's census's report.
|
||||
*
|
||||
* @param {DominatorTreeNode} node
|
||||
*/
|
||||
const focusDiffingCensusNode = exports.focusDiffingCensusNode = function (node) {
|
||||
return {
|
||||
type: actions.FOCUS_DIFFING_CENSUS_NODE,
|
||||
node,
|
||||
};
|
||||
};
|
||||
35
devtools/client/memory/actions/filter.js
Normal file
35
devtools/client/memory/actions/filter.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { actions } = require("../constants");
|
||||
const { refresh } = require("./refresh");
|
||||
|
||||
const setFilterString = exports.setFilterString = function (filterString) {
|
||||
return {
|
||||
type: actions.SET_FILTER_STRING,
|
||||
filter: filterString
|
||||
};
|
||||
};
|
||||
|
||||
// The number of milliseconds we should wait before kicking off a new census
|
||||
// when the filter string is updated. This helps us avoid doing any work while
|
||||
// the user is still typing.
|
||||
const FILTER_INPUT_DEBOUNCE_MS = 250;
|
||||
|
||||
// The timer id for the debounced census refresh.
|
||||
let timerId = null;
|
||||
|
||||
exports.setFilterStringAndRefresh = function (filterString, heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
dispatch(setFilterString(filterString));
|
||||
|
||||
if (timerId !== null) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
|
||||
timerId = setTimeout(() => dispatch(refresh(heapWorker)),
|
||||
FILTER_INPUT_DEBOUNCE_MS);
|
||||
};
|
||||
};
|
||||
97
devtools/client/memory/actions/io.js
Normal file
97
devtools/client/memory/actions/io.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { immutableUpdate, reportException, assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { snapshotState: states, actions, viewState } = require("../constants");
|
||||
const { L10N, openFilePicker, createSnapshot } = require("../utils");
|
||||
const telemetry = require("../telemetry");
|
||||
const { OS } = require("resource://gre/modules/osfile.jsm");
|
||||
const {
|
||||
selectSnapshot,
|
||||
computeSnapshotData,
|
||||
readSnapshot,
|
||||
takeCensus,
|
||||
takeTreeMap
|
||||
} = require("./snapshot");
|
||||
const VALID_EXPORT_STATES = [states.SAVED, states.READ];
|
||||
|
||||
exports.pickFileAndExportSnapshot = function (snapshot) {
|
||||
return function* (dispatch, getState) {
|
||||
let outputFile = yield openFilePicker({
|
||||
title: L10N.getFormatStr("snapshot.io.save.window"),
|
||||
defaultName: OS.Path.basename(snapshot.path),
|
||||
filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]],
|
||||
mode: "save",
|
||||
});
|
||||
|
||||
if (!outputFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield dispatch(exportSnapshot(snapshot, outputFile.path));
|
||||
};
|
||||
};
|
||||
|
||||
const exportSnapshot = exports.exportSnapshot = function (snapshot, dest) {
|
||||
return function* (dispatch, getState) {
|
||||
telemetry.countExportSnapshot();
|
||||
|
||||
dispatch({ type: actions.EXPORT_SNAPSHOT_START, snapshot });
|
||||
|
||||
assert(VALID_EXPORT_STATES.includes(snapshot.state),
|
||||
`Snapshot is in invalid state for exporting: ${snapshot.state}`);
|
||||
|
||||
try {
|
||||
yield OS.File.copy(snapshot.path, dest);
|
||||
} catch (error) {
|
||||
reportException("exportSnapshot", error);
|
||||
dispatch({ type: actions.EXPORT_SNAPSHOT_ERROR, snapshot, error });
|
||||
}
|
||||
|
||||
dispatch({ type: actions.EXPORT_SNAPSHOT_END, snapshot });
|
||||
};
|
||||
};
|
||||
|
||||
const pickFileAndImportSnapshotAndCensus = exports.pickFileAndImportSnapshotAndCensus = function (heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
let input = yield openFilePicker({
|
||||
title: L10N.getFormatStr("snapshot.io.import.window"),
|
||||
filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]],
|
||||
mode: "open",
|
||||
});
|
||||
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield dispatch(importSnapshotAndCensus(heapWorker, input.path));
|
||||
};
|
||||
};
|
||||
|
||||
const importSnapshotAndCensus = exports.importSnapshotAndCensus = function (heapWorker, path) {
|
||||
return function* (dispatch, getState) {
|
||||
telemetry.countImportSnapshot();
|
||||
|
||||
const snapshot = immutableUpdate(createSnapshot(getState()), {
|
||||
path,
|
||||
state: states.IMPORTING,
|
||||
imported: true,
|
||||
});
|
||||
const id = snapshot.id;
|
||||
|
||||
dispatch({ type: actions.IMPORT_SNAPSHOT_START, snapshot });
|
||||
dispatch(selectSnapshot(snapshot.id));
|
||||
|
||||
try {
|
||||
yield dispatch(readSnapshot(heapWorker, id));
|
||||
yield dispatch(computeSnapshotData(heapWorker, id));
|
||||
} catch (error) {
|
||||
reportException("importSnapshot", error);
|
||||
dispatch({ type: actions.IMPORT_SNAPSHOT_ERROR, error, id });
|
||||
}
|
||||
|
||||
dispatch({ type: actions.IMPORT_SNAPSHOT_END, id });
|
||||
};
|
||||
};
|
||||
38
devtools/client/memory/actions/label-display.js
Normal file
38
devtools/client/memory/actions/label-display.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/* 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");
|
||||
|
||||
/**
|
||||
* Change the display we use for labeling individual nodes and refresh the
|
||||
* current data.
|
||||
*/
|
||||
exports.setLabelDisplayAndRefresh = function (heapWorker, display) {
|
||||
return function* (dispatch, getState) {
|
||||
// Clears out all stored census data and sets the display.
|
||||
dispatch(setLabelDisplay(display));
|
||||
yield dispatch(refresh(heapWorker));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the display we use for labeling individual nodes.
|
||||
*
|
||||
* @param {labelDisplayModel} display
|
||||
*/
|
||||
const setLabelDisplay = exports.setLabelDisplay = 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_LABEL_DISPLAY,
|
||||
display,
|
||||
};
|
||||
};
|
||||
19
devtools/client/memory/actions/moz.build
Normal file
19
devtools/client/memory/actions/moz.build
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# 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(
|
||||
'allocations.js',
|
||||
'census-display.js',
|
||||
'diffing.js',
|
||||
'filter.js',
|
||||
'io.js',
|
||||
'label-display.js',
|
||||
'refresh.js',
|
||||
'sizes.js',
|
||||
'snapshot.js',
|
||||
'task-cache.js',
|
||||
'tree-map-display.js',
|
||||
'view.js',
|
||||
)
|
||||
44
devtools/client/memory/actions/refresh.js
Normal file
44
devtools/client/memory/actions/refresh.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { viewState } = require("../constants");
|
||||
const { refreshDiffing } = require("./diffing");
|
||||
const snapshot = require("./snapshot");
|
||||
|
||||
/**
|
||||
* Refresh the main thread's data from the heap analyses worker, if needed.
|
||||
*
|
||||
* @param {HeapAnalysesWorker} heapWorker
|
||||
*/
|
||||
exports.refresh = function (heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
switch (getState().view.state) {
|
||||
case viewState.DIFFING:
|
||||
assert(getState().diffing, "Should have diffing state if in diffing view");
|
||||
yield dispatch(refreshDiffing(heapWorker));
|
||||
return;
|
||||
|
||||
case viewState.CENSUS:
|
||||
yield dispatch(snapshot.refreshSelectedCensus(heapWorker));
|
||||
return;
|
||||
|
||||
case viewState.DOMINATOR_TREE:
|
||||
yield dispatch(snapshot.refreshSelectedDominatorTree(heapWorker));
|
||||
return;
|
||||
|
||||
case viewState.TREE_MAP:
|
||||
yield dispatch(snapshot.refreshSelectedTreeMap(heapWorker));
|
||||
return;
|
||||
|
||||
case viewState.INDIVIDUALS:
|
||||
yield dispatch(snapshot.refreshIndividuals(heapWorker));
|
||||
return;
|
||||
|
||||
default:
|
||||
assert(false, `Unexpected view state: ${getState().view.state}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
13
devtools/client/memory/actions/sizes.js
Normal file
13
devtools/client/memory/actions/sizes.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { actions } = require("../constants");
|
||||
|
||||
exports.resizeShortestPaths = function (newSize) {
|
||||
return {
|
||||
type: actions.RESIZE_SHORTEST_PATHS,
|
||||
size: newSize,
|
||||
};
|
||||
};
|
||||
865
devtools/client/memory/actions/snapshot.js
Normal file
865
devtools/client/memory/actions/snapshot.js
Normal file
|
|
@ -0,0 +1,865 @@
|
|||
/* 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 { Preferences } = require("resource://gre/modules/Preferences.jsm");
|
||||
const { assert, reportException, isSet } = require("devtools/shared/DevToolsUtils");
|
||||
const {
|
||||
censusIsUpToDate,
|
||||
getSnapshot,
|
||||
createSnapshot,
|
||||
dominatorTreeIsComputed,
|
||||
} = require("../utils");
|
||||
const {
|
||||
actions,
|
||||
snapshotState: states,
|
||||
viewState,
|
||||
censusState,
|
||||
treeMapState,
|
||||
dominatorTreeState,
|
||||
individualsState,
|
||||
} = require("../constants");
|
||||
const telemetry = require("../telemetry");
|
||||
const view = require("./view");
|
||||
const refresh = require("./refresh");
|
||||
const diffing = require("./diffing");
|
||||
const TaskCache = require("./task-cache");
|
||||
|
||||
/**
|
||||
* A series of actions are fired from this task to save, read and generate the
|
||||
* initial census from a snapshot.
|
||||
*
|
||||
* @param {MemoryFront}
|
||||
* @param {HeapAnalysesClient}
|
||||
* @param {Object}
|
||||
*/
|
||||
const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function (front, heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
const id = yield dispatch(takeSnapshot(front));
|
||||
if (id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield dispatch(readSnapshot(heapWorker, id));
|
||||
if (getSnapshot(getState(), id).state !== states.READ) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield dispatch(computeSnapshotData(heapWorker, id));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the census for the snapshot with the provided snapshot id. If the
|
||||
* current view is the DOMINATOR_TREE view, create the dominator tree for this
|
||||
* snapshot as well.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {snapshotId} id
|
||||
*/
|
||||
const computeSnapshotData = exports.computeSnapshotData = function (heapWorker, id) {
|
||||
return function* (dispatch, getState) {
|
||||
if (getSnapshot(getState(), id).state !== states.READ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Decide which type of census to take.
|
||||
const censusTaker = getCurrentCensusTaker(getState().view.state);
|
||||
yield dispatch(censusTaker(heapWorker, id));
|
||||
|
||||
if (getState().view.state === viewState.DOMINATOR_TREE &&
|
||||
!getSnapshot(getState(), id).dominatorTree) {
|
||||
yield dispatch(computeAndFetchDominatorTree(heapWorker, id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects a snapshot and if the snapshot's census is using a different
|
||||
* display, take a new census.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {snapshotId} id
|
||||
*/
|
||||
const selectSnapshotAndRefresh = exports.selectSnapshotAndRefresh = function (heapWorker, id) {
|
||||
return function* (dispatch, getState) {
|
||||
if (getState().diffing || getState().individuals) {
|
||||
dispatch(view.changeView(viewState.CENSUS));
|
||||
}
|
||||
|
||||
dispatch(selectSnapshot(id));
|
||||
yield dispatch(refresh.refresh(heapWorker));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Take a snapshot and return its id on success, or null on failure.
|
||||
*
|
||||
* @param {MemoryFront} front
|
||||
* @returns {Number|null}
|
||||
*/
|
||||
const takeSnapshot = exports.takeSnapshot = function (front) {
|
||||
return function* (dispatch, getState) {
|
||||
telemetry.countTakeSnapshot();
|
||||
|
||||
if (getState().diffing || getState().individuals) {
|
||||
dispatch(view.changeView(viewState.CENSUS));
|
||||
}
|
||||
|
||||
const snapshot = createSnapshot(getState());
|
||||
const id = snapshot.id;
|
||||
dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot });
|
||||
dispatch(selectSnapshot(id));
|
||||
|
||||
let path;
|
||||
try {
|
||||
path = yield front.saveHeapSnapshot();
|
||||
} catch (error) {
|
||||
reportException("takeSnapshot", error);
|
||||
dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
|
||||
return null;
|
||||
}
|
||||
|
||||
dispatch({ type: actions.TAKE_SNAPSHOT_END, id, path });
|
||||
return snapshot.id;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a snapshot into memory; necessary to do before taking
|
||||
* a census on the snapshot. May only be called once per snapshot.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {snapshotId} id
|
||||
*/
|
||||
const readSnapshot = exports.readSnapshot =
|
||||
TaskCache.declareCacheableTask({
|
||||
getCacheKey(_, id) {
|
||||
return id;
|
||||
},
|
||||
|
||||
task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
|
||||
const snapshot = getSnapshot(getState(), id);
|
||||
assert([states.SAVED, states.IMPORTING].includes(snapshot.state),
|
||||
`Should only read a snapshot once. Found snapshot in state ${snapshot.state}`);
|
||||
|
||||
let creationTime;
|
||||
|
||||
dispatch({ type: actions.READ_SNAPSHOT_START, id });
|
||||
try {
|
||||
yield heapWorker.readHeapSnapshot(snapshot.path);
|
||||
creationTime = yield heapWorker.getCreationTime(snapshot.path);
|
||||
} catch (error) {
|
||||
removeFromCache();
|
||||
reportException("readSnapshot", error);
|
||||
dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
|
||||
return;
|
||||
}
|
||||
|
||||
removeFromCache();
|
||||
dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime });
|
||||
}
|
||||
});
|
||||
|
||||
let takeCensusTaskCounter = 0;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
|
||||
endAction, errorAction, canTakeCensus }) {
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
let thisTakeCensusTaskId = ++takeCensusTaskCounter;
|
||||
return TaskCache.declareCacheableTask({
|
||||
getCacheKey(_, id) {
|
||||
return `take-census-task-${thisTakeCensusTaskId}-${id}`;
|
||||
},
|
||||
|
||||
task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
|
||||
const snapshot = getSnapshot(getState(), id);
|
||||
if (!snapshot) {
|
||||
removeFromCache();
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert that snapshot is in a valid state
|
||||
assert(canTakeCensus(snapshot),
|
||||
`Attempting to take a census when the snapshot is not in a ready state. snapshot.state = ${snapshot.state}, census.state = ${(getCensus(snapshot) || { state: null }).state}`);
|
||||
|
||||
let report, parentMap;
|
||||
let display = getDisplay(getState());
|
||||
let filter = getFilter(getState());
|
||||
|
||||
// If display, filter and inversion haven't changed, don't do anything.
|
||||
if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
|
||||
removeFromCache();
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep taking a census if the display changes while our request is in
|
||||
// flight. Recheck that the display used for the census is the same as the
|
||||
// state's display.
|
||||
do {
|
||||
display = getDisplay(getState());
|
||||
filter = getState().filter;
|
||||
|
||||
dispatch({
|
||||
type: beginAction,
|
||||
id,
|
||||
filter,
|
||||
display
|
||||
});
|
||||
|
||||
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) {
|
||||
removeFromCache();
|
||||
reportException("takeCensus", error);
|
||||
dispatch({ type: errorAction, id, error });
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (filter !== getState().filter ||
|
||||
display !== getDisplay(getState()));
|
||||
|
||||
removeFromCache();
|
||||
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,
|
||||
canTakeCensus: snapshot =>
|
||||
snapshot.state === states.READ &&
|
||||
(!snapshot.census || snapshot.census.state === censusState.SAVED),
|
||||
});
|
||||
|
||||
/**
|
||||
* 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,
|
||||
canTakeCensus: snapshot =>
|
||||
snapshot.state === states.READ &&
|
||||
(!snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED),
|
||||
});
|
||||
|
||||
/**
|
||||
* 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;
|
||||
case viewState.CENSUS:
|
||||
return takeCensus;
|
||||
default:
|
||||
return defaultCensusTaker;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the given node in the individuals view.
|
||||
*
|
||||
* @param {DominatorTreeNode} node.
|
||||
*/
|
||||
const focusIndividual = exports.focusIndividual = function (node) {
|
||||
return {
|
||||
type: actions.FOCUS_INDIVIDUAL,
|
||||
node,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the individual `DominatorTreeNodes` for the census group specified by
|
||||
* `censusBreakdown` and `reportLeafIndex`.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {SnapshotId} id
|
||||
* @param {Object} censusBreakdown
|
||||
* @param {Set<Number> | Number} reportLeafIndex
|
||||
*/
|
||||
const fetchIndividuals = exports.fetchIndividuals =
|
||||
function (heapWorker, id, censusBreakdown, reportLeafIndex) {
|
||||
return function* (dispatch, getState) {
|
||||
if (getState().view.state !== viewState.INDIVIDUALS) {
|
||||
dispatch(view.changeView(viewState.INDIVIDUALS));
|
||||
}
|
||||
|
||||
const snapshot = getSnapshot(getState(), id);
|
||||
assert(snapshot && snapshot.state === states.READ,
|
||||
"The snapshot should already be read into memory");
|
||||
|
||||
if (!dominatorTreeIsComputed(snapshot)) {
|
||||
yield dispatch(computeAndFetchDominatorTree(heapWorker, id));
|
||||
}
|
||||
|
||||
const snapshot_ = getSnapshot(getState(), id);
|
||||
assert(snapshot_.dominatorTree && snapshot_.dominatorTree.root,
|
||||
"Should have a dominator tree with a root.");
|
||||
|
||||
const dominatorTreeId = snapshot_.dominatorTree.dominatorTreeId;
|
||||
|
||||
const indices = isSet(reportLeafIndex)
|
||||
? reportLeafIndex
|
||||
: new Set([reportLeafIndex]);
|
||||
|
||||
let labelDisplay;
|
||||
let nodes;
|
||||
do {
|
||||
labelDisplay = getState().labelDisplay;
|
||||
assert(labelDisplay && labelDisplay.breakdown && labelDisplay.breakdown.by,
|
||||
`Should have a breakdown to label nodes with, got: ${uneval(labelDisplay)}`);
|
||||
|
||||
if (getState().view.state !== viewState.INDIVIDUALS) {
|
||||
// We switched views while in the process of fetching individuals -- any
|
||||
// further work is useless.
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: actions.FETCH_INDIVIDUALS_START });
|
||||
|
||||
try {
|
||||
({ nodes } = yield heapWorker.getCensusIndividuals({
|
||||
dominatorTreeId,
|
||||
indices,
|
||||
censusBreakdown,
|
||||
labelBreakdown: labelDisplay.breakdown,
|
||||
maxRetainingPaths: Preferences.get("devtools.memory.max-retaining-paths"),
|
||||
maxIndividuals: Preferences.get("devtools.memory.max-individuals"),
|
||||
}));
|
||||
} catch (error) {
|
||||
reportException("actions/snapshot/fetchIndividuals", error);
|
||||
dispatch({ type: actions.INDIVIDUALS_ERROR, error });
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (labelDisplay !== getState().labelDisplay);
|
||||
|
||||
dispatch({
|
||||
type: actions.FETCH_INDIVIDUALS_END,
|
||||
id,
|
||||
censusBreakdown,
|
||||
indices,
|
||||
labelDisplay,
|
||||
nodes,
|
||||
dominatorTree: snapshot_.dominatorTree,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh the current individuals view.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
*/
|
||||
const refreshIndividuals = exports.refreshIndividuals = function (heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
assert(getState().view.state === viewState.INDIVIDUALS,
|
||||
"Should be in INDIVIDUALS view.");
|
||||
|
||||
const { individuals } = getState();
|
||||
|
||||
switch (individuals.state) {
|
||||
case individualsState.COMPUTING_DOMINATOR_TREE:
|
||||
case individualsState.FETCHING:
|
||||
// Nothing to do here.
|
||||
return;
|
||||
|
||||
case individualsState.FETCHED:
|
||||
if (getState().individuals.labelDisplay === getState().labelDisplay) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case individualsState.ERROR:
|
||||
// Doesn't hurt to retry: maybe we won't get an error this time around?
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false, `Unexpected individuals state: ${individuals.state}`);
|
||||
return;
|
||||
}
|
||||
|
||||
yield dispatch(fetchIndividuals(heapWorker,
|
||||
individuals.id,
|
||||
individuals.censusBreakdown,
|
||||
individuals.indices));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh the selected snapshot's census data, if need be (for example,
|
||||
* display configuration changed).
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
*/
|
||||
const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
let snapshot = getState().snapshots.find(s => s.selected);
|
||||
if (!snapshot || snapshot.state !== states.READ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.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);
|
||||
if (!snapshot || snapshot.state !== states.READ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
|
||||
!snapshot.treeMap) {
|
||||
yield dispatch(takeTreeMap(heapWorker, snapshot.id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request that the `HeapAnalysesWorker` compute the dominator tree for the
|
||||
* snapshot with the given `id`.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {SnapshotId} id
|
||||
*
|
||||
* @returns {Promise<DominatorTreeId>}
|
||||
*/
|
||||
const computeDominatorTree = exports.computeDominatorTree =
|
||||
TaskCache.declareCacheableTask({
|
||||
getCacheKey(_, id) {
|
||||
return id;
|
||||
},
|
||||
|
||||
task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
|
||||
const snapshot = getSnapshot(getState(), id);
|
||||
assert(!(snapshot.dominatorTree && snapshot.dominatorTree.dominatorTreeId),
|
||||
"Should not re-compute dominator trees");
|
||||
|
||||
dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_START, id });
|
||||
|
||||
let dominatorTreeId;
|
||||
try {
|
||||
dominatorTreeId = yield heapWorker.computeDominatorTree(snapshot.path);
|
||||
} catch (error) {
|
||||
removeFromCache();
|
||||
reportException("actions/snapshot/computeDominatorTree", error);
|
||||
dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
|
||||
return null;
|
||||
}
|
||||
|
||||
removeFromCache();
|
||||
dispatch({ type: actions.COMPUTE_DOMINATOR_TREE_END, id, dominatorTreeId });
|
||||
return dominatorTreeId;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the partial subtree, starting from the root, of the
|
||||
* snapshot-with-the-given-id's dominator tree.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {SnapshotId} id
|
||||
*
|
||||
* @returns {Promise<DominatorTreeNode>}
|
||||
*/
|
||||
const fetchDominatorTree = exports.fetchDominatorTree =
|
||||
TaskCache.declareCacheableTask({
|
||||
getCacheKey(_, id) {
|
||||
return id;
|
||||
},
|
||||
|
||||
task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
|
||||
const snapshot = getSnapshot(getState(), id);
|
||||
assert(dominatorTreeIsComputed(snapshot),
|
||||
"Should have dominator tree model and it should be computed");
|
||||
|
||||
let display;
|
||||
let root;
|
||||
do {
|
||||
display = getState().labelDisplay;
|
||||
assert(display && display.breakdown,
|
||||
`Should have a breakdown to describe nodes with, got: ${uneval(display)}`);
|
||||
|
||||
dispatch({ type: actions.FETCH_DOMINATOR_TREE_START, id, display });
|
||||
|
||||
try {
|
||||
root = yield heapWorker.getDominatorTree({
|
||||
dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
|
||||
breakdown: display.breakdown,
|
||||
maxRetainingPaths: Preferences.get("devtools.memory.max-retaining-paths"),
|
||||
});
|
||||
} catch (error) {
|
||||
removeFromCache();
|
||||
reportException("actions/snapshot/fetchDominatorTree", error);
|
||||
dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
while (display !== getState().labelDisplay);
|
||||
|
||||
removeFromCache();
|
||||
dispatch({ type: actions.FETCH_DOMINATOR_TREE_END, id, root });
|
||||
telemetry.countDominatorTree({ display });
|
||||
return root;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch the immediately dominated children represented by the placeholder
|
||||
* `lazyChildren` from snapshot-with-the-given-id's dominator tree.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {SnapshotId} id
|
||||
* @param {DominatorTreeLazyChildren} lazyChildren
|
||||
*/
|
||||
const fetchImmediatelyDominated = exports.fetchImmediatelyDominated =
|
||||
TaskCache.declareCacheableTask({
|
||||
getCacheKey(_, id, lazyChildren) {
|
||||
return `${id}-${lazyChildren.key()}`;
|
||||
},
|
||||
|
||||
task: function* (heapWorker, id, lazyChildren, removeFromCache, dispatch, getState) {
|
||||
const snapshot = getSnapshot(getState(), id);
|
||||
assert(snapshot.dominatorTree, "Should have dominator tree model");
|
||||
assert(snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
|
||||
snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
|
||||
"Cannot fetch immediately dominated nodes in a dominator tree unless " +
|
||||
" the dominator tree has already been computed");
|
||||
|
||||
let display;
|
||||
let response;
|
||||
do {
|
||||
display = getState().labelDisplay;
|
||||
assert(display, "Should have a display to describe nodes with.");
|
||||
|
||||
dispatch({ type: actions.FETCH_IMMEDIATELY_DOMINATED_START, id });
|
||||
|
||||
try {
|
||||
response = yield heapWorker.getImmediatelyDominated({
|
||||
dominatorTreeId: snapshot.dominatorTree.dominatorTreeId,
|
||||
breakdown: display.breakdown,
|
||||
nodeId: lazyChildren.parentNodeId(),
|
||||
startIndex: lazyChildren.siblingIndex(),
|
||||
maxRetainingPaths: Preferences.get("devtools.memory.max-retaining-paths"),
|
||||
});
|
||||
} catch (error) {
|
||||
removeFromCache();
|
||||
reportException("actions/snapshot/fetchImmediatelyDominated", error);
|
||||
dispatch({ type: actions.DOMINATOR_TREE_ERROR, id, error });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
while (display !== getState().labelDisplay);
|
||||
|
||||
removeFromCache();
|
||||
dispatch({
|
||||
type: actions.FETCH_IMMEDIATELY_DOMINATED_END,
|
||||
id,
|
||||
path: response.path,
|
||||
nodes: response.nodes,
|
||||
moreChildrenAvailable: response.moreChildrenAvailable,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Compute and then fetch the dominator tree of the snapshot with the given
|
||||
* `id`.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {SnapshotId} id
|
||||
*
|
||||
* @returns {Promise<DominatorTreeNode>}
|
||||
*/
|
||||
const computeAndFetchDominatorTree = exports.computeAndFetchDominatorTree =
|
||||
TaskCache.declareCacheableTask({
|
||||
getCacheKey(_, id) {
|
||||
return id;
|
||||
},
|
||||
|
||||
task: function* (heapWorker, id, removeFromCache, dispatch, getState) {
|
||||
const dominatorTreeId = yield dispatch(computeDominatorTree(heapWorker, id));
|
||||
if (dominatorTreeId === null) {
|
||||
removeFromCache();
|
||||
return null;
|
||||
}
|
||||
|
||||
const root = yield dispatch(fetchDominatorTree(heapWorker, id));
|
||||
removeFromCache();
|
||||
|
||||
if (!root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update the currently selected snapshot's dominator tree.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
*/
|
||||
const refreshSelectedDominatorTree = exports.refreshSelectedDominatorTree = function (heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
let snapshot = getState().snapshots.find(s => s.selected);
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapshot.dominatorTree &&
|
||||
!(snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
|
||||
snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
|
||||
snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapshot.state === states.READ) {
|
||||
if (snapshot.dominatorTree) {
|
||||
yield dispatch(fetchDominatorTree(heapWorker, snapshot.id));
|
||||
} else {
|
||||
yield dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
|
||||
}
|
||||
} else {
|
||||
// If there was an error, we can't continue. If we are still saving or
|
||||
// reading the snapshot, then takeSnapshotAndCensus will finish the job
|
||||
// for us.
|
||||
return;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Select the snapshot with the given id.
|
||||
*
|
||||
* @param {snapshotId} id
|
||||
* @see {Snapshot} model defined in devtools/client/memory/models.js
|
||||
*/
|
||||
const selectSnapshot = exports.selectSnapshot = function (id) {
|
||||
return {
|
||||
type: actions.SELECT_SNAPSHOT,
|
||||
id
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 => {
|
||||
let snapshotReady = s.state === states.READ || s.state === states.ERROR;
|
||||
let censusReady = (s.treeMap && s.treeMap.state === treeMapState.SAVED) ||
|
||||
(s.census && s.census.state === censusState.SAVED);
|
||||
|
||||
return snapshotReady && censusReady;
|
||||
});
|
||||
|
||||
let ids = snapshots.map(s => s.id);
|
||||
|
||||
dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids });
|
||||
|
||||
if (getState().diffing) {
|
||||
dispatch(diffing.toggleDiffing());
|
||||
}
|
||||
if (getState().individuals) {
|
||||
dispatch(view.popView());
|
||||
}
|
||||
|
||||
yield Promise.all(snapshots.map(snapshot => {
|
||||
return heapWorker.deleteHeapSnapshot(snapshot.path).catch(error => {
|
||||
reportException("clearSnapshots", error);
|
||||
dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
|
||||
});
|
||||
}));
|
||||
|
||||
dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a snapshot
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {snapshotModel} snapshot
|
||||
*/
|
||||
const deleteSnapshot = exports.deleteSnapshot = function (heapWorker, snapshot) {
|
||||
return function* (dispatch, getState) {
|
||||
dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids: [snapshot.id] });
|
||||
|
||||
try {
|
||||
yield heapWorker.deleteHeapSnapshot(snapshot.path);
|
||||
} catch (error) {
|
||||
reportException("deleteSnapshot", error);
|
||||
dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
|
||||
}
|
||||
|
||||
dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids: [snapshot.id] });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand the given node in the snapshot's census report.
|
||||
*
|
||||
* @param {CensusTreeNode} node
|
||||
*/
|
||||
const expandCensusNode = exports.expandCensusNode = function (id, node) {
|
||||
return {
|
||||
type: actions.EXPAND_CENSUS_NODE,
|
||||
id,
|
||||
node,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapse the given node in the snapshot's census report.
|
||||
*
|
||||
* @param {CensusTreeNode} node
|
||||
*/
|
||||
const collapseCensusNode = exports.collapseCensusNode = function (id, node) {
|
||||
return {
|
||||
type: actions.COLLAPSE_CENSUS_NODE,
|
||||
id,
|
||||
node,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the given node in the snapshot's census's report.
|
||||
*
|
||||
* @param {SnapshotId} id
|
||||
* @param {DominatorTreeNode} node
|
||||
*/
|
||||
const focusCensusNode = exports.focusCensusNode = function (id, node) {
|
||||
return {
|
||||
type: actions.FOCUS_CENSUS_NODE,
|
||||
id,
|
||||
node,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand the given node in the snapshot's dominator tree.
|
||||
*
|
||||
* @param {DominatorTreeTreeNode} node
|
||||
*/
|
||||
const expandDominatorTreeNode = exports.expandDominatorTreeNode = function (id, node) {
|
||||
return {
|
||||
type: actions.EXPAND_DOMINATOR_TREE_NODE,
|
||||
id,
|
||||
node,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapse the given node in the snapshot's dominator tree.
|
||||
*
|
||||
* @param {DominatorTreeTreeNode} node
|
||||
*/
|
||||
const collapseDominatorTreeNode = exports.collapseDominatorTreeNode = function (id, node) {
|
||||
return {
|
||||
type: actions.COLLAPSE_DOMINATOR_TREE_NODE,
|
||||
id,
|
||||
node,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the given node in the snapshot's dominator tree.
|
||||
*
|
||||
* @param {SnapshotId} id
|
||||
* @param {DominatorTreeNode} node
|
||||
*/
|
||||
const focusDominatorTreeNode = exports.focusDominatorTreeNode = function (id, node) {
|
||||
return {
|
||||
type: actions.FOCUS_DOMINATOR_TREE_NODE,
|
||||
id,
|
||||
node,
|
||||
};
|
||||
};
|
||||
99
devtools/client/memory/actions/task-cache.js
Normal file
99
devtools/client/memory/actions/task-cache.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
|
||||
/**
|
||||
* The `TaskCache` allows for re-using active tasks when spawning a second task
|
||||
* would simply duplicate work and is unnecessary. It maps from a task's unique
|
||||
* key to the promise of its result.
|
||||
*/
|
||||
const TaskCache = module.exports = class TaskCache {
|
||||
constructor() {
|
||||
this._cache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the promise keyed by the given unique `key`, if one exists.
|
||||
*
|
||||
* @param {Any} key
|
||||
* @returns {Promise<Any> | undefined}
|
||||
*/
|
||||
get(key) {
|
||||
return this._cache.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the task result promise in the cache and associate it with the given
|
||||
* `key` which must not already have an entry in the cache.
|
||||
*
|
||||
* @param {Any} key
|
||||
* @param {Promise<Any>} promise
|
||||
*/
|
||||
put(key, promise) {
|
||||
assert(!this._cache.has(key),
|
||||
"We should not override extant entries");
|
||||
|
||||
this._cache.set(key, promise);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the cache entry with the given key.
|
||||
*
|
||||
* @param {Any} key
|
||||
*/
|
||||
remove(key) {
|
||||
assert(this._cache.has(key),
|
||||
`Should have an extant entry for key = ${key}`);
|
||||
|
||||
this._cache.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new action-orchestrating task that is automatically cached. The
|
||||
* tasks themselves are responsible from removing themselves from the cache.
|
||||
*
|
||||
* @param {Function(...args) -> Any} getCacheKey
|
||||
* @param {Generator(...args) -> Any} task
|
||||
*
|
||||
* @returns Cacheable, Action-Creating Task
|
||||
*/
|
||||
TaskCache.declareCacheableTask = function ({ getCacheKey, task }) {
|
||||
const cache = new TaskCache();
|
||||
|
||||
return function (...args) {
|
||||
return function* (dispatch, getState) {
|
||||
const key = getCacheKey(...args);
|
||||
|
||||
const extantResult = cache.get(key);
|
||||
if (extantResult) {
|
||||
return extantResult;
|
||||
}
|
||||
|
||||
// Ensure that we have our new entry in the cache *before* dispatching the
|
||||
// task!
|
||||
let resolve;
|
||||
cache.put(key, new Promise(r => {
|
||||
resolve = r;
|
||||
}));
|
||||
|
||||
resolve(dispatch(function* () {
|
||||
try {
|
||||
args.push(() => cache.remove(key), dispatch, getState);
|
||||
return yield* task(...args);
|
||||
} catch (error) {
|
||||
// Don't perma-cache errors.
|
||||
if (cache.get(key)) {
|
||||
cache.remove(key);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}));
|
||||
|
||||
return yield cache.get(key);
|
||||
};
|
||||
};
|
||||
};
|
||||
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,
|
||||
};
|
||||
};
|
||||
67
devtools/client/memory/actions/view.js
Normal file
67
devtools/client/memory/actions/view.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* 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 { findSelectedSnapshot } = require("../utils");
|
||||
const refresh = require("./refresh");
|
||||
|
||||
/**
|
||||
* Change the currently selected view.
|
||||
*
|
||||
* @param {viewState} view
|
||||
*/
|
||||
const changeView = exports.changeView = function (view) {
|
||||
return function (dispatch, getState) {
|
||||
dispatch({
|
||||
type: actions.CHANGE_VIEW,
|
||||
newViewState: view,
|
||||
oldDiffing: getState().diffing,
|
||||
oldSelected: findSelectedSnapshot(getState()),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Given that we are in the INDIVIDUALS view state, go back to the state we were
|
||||
* in before.
|
||||
*/
|
||||
const popView = exports.popView = function () {
|
||||
return function (dispatch, getState) {
|
||||
const { previous } = getState().view;
|
||||
assert(previous);
|
||||
dispatch({
|
||||
type: actions.POP_VIEW,
|
||||
previousView: previous,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the currently selected view and ensure all our data is up to date from
|
||||
* the heap worker.
|
||||
*
|
||||
* @param {viewState} view
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
*/
|
||||
exports.changeViewAndRefresh = function (view, heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
dispatch(changeView(view));
|
||||
yield dispatch(refresh.refresh(heapWorker));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Given that we are in the INDIVIDUALS view state, go back to the state we were
|
||||
* previously in and refresh our data.
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
*/
|
||||
exports.popViewAndRefresh = function (heapWorker) {
|
||||
return function* (dispatch, getState) {
|
||||
dispatch(popView());
|
||||
yield dispatch(refresh.refresh(heapWorker));
|
||||
};
|
||||
};
|
||||
322
devtools/client/memory/app.js
Normal file
322
devtools/client/memory/app.js
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { appinfo } = require("Services");
|
||||
const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
||||
const { censusDisplays, labelDisplays, treeMapDisplays, diffingState, viewState } = require("./constants");
|
||||
const { toggleRecordingAllocationStacks } = require("./actions/allocations");
|
||||
const { setCensusDisplayAndRefresh } = require("./actions/census-display");
|
||||
const { setLabelDisplayAndRefresh } = require("./actions/label-display");
|
||||
const { setTreeMapDisplayAndRefresh } = require("./actions/tree-map-display");
|
||||
|
||||
const {
|
||||
getCustomCensusDisplays,
|
||||
getCustomLabelDisplays,
|
||||
getCustomTreeMapDisplays,
|
||||
} = require("devtools/client/memory/utils");
|
||||
const {
|
||||
selectSnapshotForDiffingAndRefresh,
|
||||
toggleDiffing,
|
||||
expandDiffingCensusNode,
|
||||
collapseDiffingCensusNode,
|
||||
focusDiffingCensusNode,
|
||||
} = require("./actions/diffing");
|
||||
const { setFilterStringAndRefresh } = require("./actions/filter");
|
||||
const { pickFileAndExportSnapshot, pickFileAndImportSnapshotAndCensus } = require("./actions/io");
|
||||
const {
|
||||
selectSnapshotAndRefresh,
|
||||
takeSnapshotAndCensus,
|
||||
clearSnapshots,
|
||||
deleteSnapshot,
|
||||
fetchImmediatelyDominated,
|
||||
expandCensusNode,
|
||||
collapseCensusNode,
|
||||
focusCensusNode,
|
||||
expandDominatorTreeNode,
|
||||
collapseDominatorTreeNode,
|
||||
focusDominatorTreeNode,
|
||||
fetchIndividuals,
|
||||
focusIndividual,
|
||||
} = require("./actions/snapshot");
|
||||
const { changeViewAndRefresh, popViewAndRefresh } = require("./actions/view");
|
||||
const { resizeShortestPaths } = require("./actions/sizes");
|
||||
const Toolbar = createFactory(require("./components/toolbar"));
|
||||
const List = createFactory(require("./components/list"));
|
||||
const SnapshotListItem = createFactory(require("./components/snapshot-list-item"));
|
||||
const Heap = createFactory(require("./components/heap"));
|
||||
const { app: appModel } = require("./models");
|
||||
|
||||
const MemoryApp = createClass({
|
||||
displayName: "MemoryApp",
|
||||
|
||||
propTypes: appModel,
|
||||
|
||||
getDefaultProps() {
|
||||
return {};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
// Attach the keydown listener directly to the window. When an element that
|
||||
// has the focus (such as a tree node) is removed from the DOM, the focus
|
||||
// falls back to the body.
|
||||
window.addEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
front: PropTypes.any,
|
||||
heapWorker: PropTypes.any,
|
||||
toolbox: PropTypes.any,
|
||||
},
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
front: this.props.front,
|
||||
heapWorker: this.props.heapWorker,
|
||||
toolbox: this.props.toolbox,
|
||||
};
|
||||
},
|
||||
|
||||
onKeyDown(e) {
|
||||
let { snapshots, dispatch, heapWorker } = this.props;
|
||||
const selectedSnapshot = snapshots.find(s => s.selected);
|
||||
const selectedIndex = snapshots.indexOf(selectedSnapshot);
|
||||
|
||||
let isOSX = appinfo.OS == "Darwin";
|
||||
let isAccelKey = (isOSX && e.metaKey) || (!isOSX && e.ctrlKey);
|
||||
|
||||
// On ACCEL+UP, select previous snapshot.
|
||||
if (isAccelKey && e.key === "ArrowUp") {
|
||||
let previousIndex = Math.max(0, selectedIndex - 1);
|
||||
let previousSnapshotId = snapshots[previousIndex].id;
|
||||
dispatch(selectSnapshotAndRefresh(heapWorker, previousSnapshotId));
|
||||
}
|
||||
|
||||
// On ACCEL+DOWN, select next snapshot.
|
||||
if (isAccelKey && e.key === "ArrowDown") {
|
||||
let nextIndex = Math.min(snapshots.length - 1, selectedIndex + 1);
|
||||
let nextSnapshotId = snapshots[nextIndex].id;
|
||||
dispatch(selectSnapshotAndRefresh(heapWorker, nextSnapshotId));
|
||||
}
|
||||
},
|
||||
|
||||
_getCensusDisplays() {
|
||||
const customDisplays = getCustomCensusDisplays();
|
||||
const custom = Object.keys(customDisplays).reduce((arr, key) => {
|
||||
arr.push(customDisplays[key]);
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
return [
|
||||
censusDisplays.coarseType,
|
||||
censusDisplays.allocationStack,
|
||||
censusDisplays.invertedAllocationStack,
|
||||
].concat(custom);
|
||||
},
|
||||
|
||||
_getLabelDisplays() {
|
||||
const customDisplays = getCustomLabelDisplays();
|
||||
const custom = Object.keys(customDisplays).reduce((arr, key) => {
|
||||
arr.push(customDisplays[key]);
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
return [
|
||||
labelDisplays.coarseType,
|
||||
labelDisplays.allocationStack,
|
||||
].concat(custom);
|
||||
},
|
||||
|
||||
_getTreeMapDisplays() {
|
||||
const customDisplays = getCustomTreeMapDisplays();
|
||||
const custom = Object.keys(customDisplays).reduce((arr, key) => {
|
||||
arr.push(customDisplays[key]);
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
return [
|
||||
treeMapDisplays.coarseType
|
||||
].concat(custom);
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
dispatch,
|
||||
snapshots,
|
||||
front,
|
||||
heapWorker,
|
||||
allocations,
|
||||
toolbox,
|
||||
filter,
|
||||
diffing,
|
||||
view,
|
||||
sizes,
|
||||
censusDisplay,
|
||||
labelDisplay,
|
||||
individuals,
|
||||
} = this.props;
|
||||
|
||||
const selectedSnapshot = snapshots.find(s => s.selected);
|
||||
|
||||
const onClickSnapshotListItem = diffing && diffing.state === diffingState.SELECTING
|
||||
? snapshot => dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, snapshot))
|
||||
: snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot.id));
|
||||
|
||||
return (
|
||||
dom.div(
|
||||
{
|
||||
id: "memory-tool"
|
||||
},
|
||||
|
||||
Toolbar({
|
||||
snapshots,
|
||||
censusDisplays: this._getCensusDisplays(),
|
||||
censusDisplay,
|
||||
onCensusDisplayChange: newDisplay =>
|
||||
dispatch(setCensusDisplayAndRefresh(heapWorker, newDisplay)),
|
||||
onImportClick: () => dispatch(pickFileAndImportSnapshotAndCensus(heapWorker)),
|
||||
onClearSnapshotsClick: () => dispatch(clearSnapshots(heapWorker)),
|
||||
onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)),
|
||||
onToggleRecordAllocationStacks: () =>
|
||||
dispatch(toggleRecordingAllocationStacks(front)),
|
||||
allocations,
|
||||
filterString: filter,
|
||||
setFilterString: filterString =>
|
||||
dispatch(setFilterStringAndRefresh(filterString, heapWorker)),
|
||||
diffing,
|
||||
onToggleDiffing: () => dispatch(toggleDiffing()),
|
||||
view,
|
||||
labelDisplays: this._getLabelDisplays(),
|
||||
labelDisplay,
|
||||
onLabelDisplayChange: newDisplay =>
|
||||
dispatch(setLabelDisplayAndRefresh(heapWorker, newDisplay)),
|
||||
treeMapDisplays: this._getTreeMapDisplays(),
|
||||
onTreeMapDisplayChange: newDisplay =>
|
||||
dispatch(setTreeMapDisplayAndRefresh(heapWorker, newDisplay)),
|
||||
onViewChange: v => dispatch(changeViewAndRefresh(v, heapWorker)),
|
||||
}),
|
||||
|
||||
dom.div(
|
||||
{
|
||||
id: "memory-tool-container"
|
||||
},
|
||||
|
||||
List({
|
||||
itemComponent: SnapshotListItem,
|
||||
items: snapshots,
|
||||
onSave: snapshot => dispatch(pickFileAndExportSnapshot(snapshot)),
|
||||
onDelete: snapshot => dispatch(deleteSnapshot(heapWorker, snapshot)),
|
||||
onClick: onClickSnapshotListItem,
|
||||
diffing,
|
||||
}),
|
||||
|
||||
Heap({
|
||||
snapshot: selectedSnapshot,
|
||||
diffing,
|
||||
onViewSourceInDebugger: frame => toolbox.viewSourceInDebugger(frame.source, frame.line),
|
||||
onSnapshotClick: () =>
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker)),
|
||||
onLoadMoreSiblings: lazyChildren =>
|
||||
dispatch(fetchImmediatelyDominated(heapWorker,
|
||||
selectedSnapshot.id,
|
||||
lazyChildren)),
|
||||
onPopView: () => dispatch(popViewAndRefresh(heapWorker)),
|
||||
individuals,
|
||||
onViewIndividuals: node => {
|
||||
const snapshotId = diffing
|
||||
? diffing.secondSnapshotId
|
||||
: selectedSnapshot.id;
|
||||
dispatch(fetchIndividuals(heapWorker,
|
||||
snapshotId,
|
||||
censusDisplay.breakdown,
|
||||
node.reportLeafIndex));
|
||||
},
|
||||
onFocusIndividual: node => {
|
||||
assert(view.state === viewState.INDIVIDUALS,
|
||||
"Should be in the individuals view");
|
||||
dispatch(focusIndividual(node));
|
||||
},
|
||||
onCensusExpand: (census, node) => {
|
||||
if (diffing) {
|
||||
assert(diffing.census === census,
|
||||
"Should only expand active census");
|
||||
dispatch(expandDiffingCensusNode(node));
|
||||
} else {
|
||||
assert(selectedSnapshot && selectedSnapshot.census === census,
|
||||
"If not diffing, should be expanding on selected snapshot's census");
|
||||
dispatch(expandCensusNode(selectedSnapshot.id, node));
|
||||
}
|
||||
},
|
||||
onCensusCollapse: (census, node) => {
|
||||
if (diffing) {
|
||||
assert(diffing.census === census,
|
||||
"Should only collapse active census");
|
||||
dispatch(collapseDiffingCensusNode(node));
|
||||
} else {
|
||||
assert(selectedSnapshot && selectedSnapshot.census === census,
|
||||
"If not diffing, should be collapsing on selected snapshot's census");
|
||||
dispatch(collapseCensusNode(selectedSnapshot.id, node));
|
||||
}
|
||||
},
|
||||
onCensusFocus: (census, node) => {
|
||||
if (diffing) {
|
||||
assert(diffing.census === census,
|
||||
"Should only focus nodes in active census");
|
||||
dispatch(focusDiffingCensusNode(node));
|
||||
} else {
|
||||
assert(selectedSnapshot && selectedSnapshot.census === census,
|
||||
"If not diffing, should be focusing on nodes in selected snapshot's census");
|
||||
dispatch(focusCensusNode(selectedSnapshot.id, node));
|
||||
}
|
||||
},
|
||||
onDominatorTreeExpand: node => {
|
||||
assert(view.state === viewState.DOMINATOR_TREE,
|
||||
"If expanding dominator tree nodes, should be in dominator tree view");
|
||||
assert(selectedSnapshot, "...and we should have a selected snapshot");
|
||||
assert(selectedSnapshot.dominatorTree,
|
||||
"...and that snapshot should have a dominator tree");
|
||||
dispatch(expandDominatorTreeNode(selectedSnapshot.id, node));
|
||||
},
|
||||
onDominatorTreeCollapse: node => {
|
||||
assert(view.state === viewState.DOMINATOR_TREE,
|
||||
"If collapsing dominator tree nodes, should be in dominator tree view");
|
||||
assert(selectedSnapshot, "...and we should have a selected snapshot");
|
||||
assert(selectedSnapshot.dominatorTree,
|
||||
"...and that snapshot should have a dominator tree");
|
||||
dispatch(collapseDominatorTreeNode(selectedSnapshot.id, node));
|
||||
},
|
||||
onDominatorTreeFocus: node => {
|
||||
assert(view.state === viewState.DOMINATOR_TREE,
|
||||
"If focusing dominator tree nodes, should be in dominator tree view");
|
||||
assert(selectedSnapshot, "...and we should have a selected snapshot");
|
||||
assert(selectedSnapshot.dominatorTree,
|
||||
"...and that snapshot should have a dominator tree");
|
||||
dispatch(focusDominatorTreeNode(selectedSnapshot.id, node));
|
||||
},
|
||||
onShortestPathsResize: newSize => {
|
||||
dispatch(resizeShortestPaths(newSize));
|
||||
},
|
||||
sizes,
|
||||
view,
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Passed into react-redux's `connect` method that is called on store change
|
||||
* and passed to components.
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return state;
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(MemoryApp);
|
||||
72
devtools/client/memory/components/census-header.js
Normal file
72
devtools/client/memory/components/census-header.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N } = require("../utils");
|
||||
const models = require("../models");
|
||||
|
||||
const CensusHeader = module.exports = createClass({
|
||||
displayName: "CensusHeader",
|
||||
|
||||
propTypes: {
|
||||
diffing: models.diffingModel,
|
||||
},
|
||||
|
||||
render() {
|
||||
let individualsCell;
|
||||
if (!this.props.diffing) {
|
||||
individualsCell = dom.span({
|
||||
className: "heap-tree-item-field heap-tree-item-individuals"
|
||||
});
|
||||
}
|
||||
|
||||
return dom.div(
|
||||
{
|
||||
className: "header"
|
||||
},
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-bytes",
|
||||
title: L10N.getStr("heapview.field.bytes.tooltip"),
|
||||
},
|
||||
L10N.getStr("heapview.field.bytes")
|
||||
),
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-count",
|
||||
title: L10N.getStr("heapview.field.count.tooltip"),
|
||||
},
|
||||
L10N.getStr("heapview.field.count")
|
||||
),
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-total-bytes",
|
||||
title: L10N.getStr("heapview.field.totalbytes.tooltip"),
|
||||
},
|
||||
L10N.getStr("heapview.field.totalbytes")
|
||||
),
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-total-count",
|
||||
title: L10N.getStr("heapview.field.totalcount.tooltip"),
|
||||
},
|
||||
L10N.getStr("heapview.field.totalcount")
|
||||
),
|
||||
|
||||
individualsCell,
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-name",
|
||||
title: L10N.getStr("heapview.field.name.tooltip"),
|
||||
},
|
||||
L10N.getStr("heapview.field.name")
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
134
devtools/client/memory/components/census-tree-item.js
Normal file
134
devtools/client/memory/components/census-tree-item.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/. */
|
||||
"use strict";
|
||||
|
||||
const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
|
||||
const { DOM: dom, createClass, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N, formatNumber, formatPercent } = require("../utils");
|
||||
const Frame = createFactory(require("devtools/client/shared/components/frame"));
|
||||
const { TREE_ROW_HEIGHT } = require("../constants");
|
||||
|
||||
const CensusTreeItem = module.exports = createClass({
|
||||
displayName: "CensusTreeItem",
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.item != nextProps.item
|
||||
|| this.props.depth != nextProps.depth
|
||||
|| this.props.expanded != nextProps.expanded
|
||||
|| this.props.focused != nextProps.focused
|
||||
|| this.props.diffing != nextProps.diffing;
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
item,
|
||||
depth,
|
||||
arrow,
|
||||
focused,
|
||||
getPercentBytes,
|
||||
getPercentCount,
|
||||
diffing,
|
||||
onViewSourceInDebugger,
|
||||
onViewIndividuals,
|
||||
inverted,
|
||||
} = this.props;
|
||||
|
||||
const bytes = formatNumber(item.bytes, !!diffing);
|
||||
const percentBytes = formatPercent(getPercentBytes(item.bytes), !!diffing);
|
||||
|
||||
const count = formatNumber(item.count, !!diffing);
|
||||
const percentCount = formatPercent(getPercentCount(item.count), !!diffing);
|
||||
|
||||
const totalBytes = formatNumber(item.totalBytes, !!diffing);
|
||||
const percentTotalBytes = formatPercent(getPercentBytes(item.totalBytes), !!diffing);
|
||||
|
||||
const totalCount = formatNumber(item.totalCount, !!diffing);
|
||||
const percentTotalCount = formatPercent(getPercentCount(item.totalCount), !!diffing);
|
||||
|
||||
let pointer;
|
||||
if (inverted && depth > 0) {
|
||||
pointer = dom.span({ className: "children-pointer" }, "↖");
|
||||
} else if (!inverted && item.children && item.children.length) {
|
||||
pointer = dom.span({ className: "children-pointer" }, "↘");
|
||||
}
|
||||
|
||||
let individualsCell;
|
||||
if (!diffing) {
|
||||
let individualsButton;
|
||||
if (item.reportLeafIndex !== undefined) {
|
||||
individualsButton = dom.button(
|
||||
{
|
||||
key: `individuals-button-${item.id}`,
|
||||
title: L10N.getStr("tree-item.view-individuals.tooltip"),
|
||||
className: "devtools-button individuals-button",
|
||||
onClick: e => {
|
||||
// Don't let the event bubble up to cause this item to focus after
|
||||
// we have switched views, which would lead to assertion failures.
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
onViewIndividuals(item);
|
||||
},
|
||||
},
|
||||
"⁂"
|
||||
);
|
||||
}
|
||||
individualsCell = dom.span(
|
||||
{ className: "heap-tree-item-field heap-tree-item-individuals" },
|
||||
individualsButton
|
||||
);
|
||||
}
|
||||
|
||||
return dom.div(
|
||||
{ className: `heap-tree-item ${focused ? "focused" : ""}` },
|
||||
dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" },
|
||||
dom.span({ className: "heap-tree-number" }, bytes),
|
||||
dom.span({ className: "heap-tree-percent" }, percentBytes)),
|
||||
dom.span({ className: "heap-tree-item-field heap-tree-item-count" },
|
||||
dom.span({ className: "heap-tree-number" }, count),
|
||||
dom.span({ className: "heap-tree-percent" }, percentCount)),
|
||||
dom.span({ className: "heap-tree-item-field heap-tree-item-total-bytes" },
|
||||
dom.span({ className: "heap-tree-number" }, totalBytes),
|
||||
dom.span({ className: "heap-tree-percent" }, percentTotalBytes)),
|
||||
dom.span({ className: "heap-tree-item-field heap-tree-item-total-count" },
|
||||
dom.span({ className: "heap-tree-number" }, totalCount),
|
||||
dom.span({ className: "heap-tree-percent" }, percentTotalCount)),
|
||||
individualsCell,
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-field heap-tree-item-name",
|
||||
style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
|
||||
},
|
||||
arrow,
|
||||
pointer,
|
||||
this.toLabel(item.name, onViewSourceInDebugger)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
toLabel(name, linkToDebugger) {
|
||||
if (isSavedFrame(name)) {
|
||||
return Frame({
|
||||
frame: name,
|
||||
onClick: () => linkToDebugger(name),
|
||||
showFunctionName: true,
|
||||
showHost: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (name === null) {
|
||||
return L10N.getStr("tree-item.root");
|
||||
}
|
||||
|
||||
if (name === "noStack") {
|
||||
return L10N.getStr("tree-item.nostack");
|
||||
}
|
||||
|
||||
if (name === "noFilename") {
|
||||
return L10N.getStr("tree-item.nofilename");
|
||||
}
|
||||
|
||||
return String(name);
|
||||
},
|
||||
});
|
||||
79
devtools/client/memory/components/census.js
Normal file
79
devtools/client/memory/components/census.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const Tree = createFactory(require("devtools/client/shared/components/tree"));
|
||||
const CensusTreeItem = createFactory(require("./census-tree-item"));
|
||||
const { createParentMap } = require("../utils");
|
||||
const { TREE_ROW_HEIGHT } = require("../constants");
|
||||
const { censusModel, diffingModel } = require("../models");
|
||||
|
||||
const Census = module.exports = createClass({
|
||||
displayName: "Census",
|
||||
|
||||
propTypes: {
|
||||
census: censusModel,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onViewIndividuals: PropTypes.func.isRequired,
|
||||
diffing: diffingModel,
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
census,
|
||||
onExpand,
|
||||
onCollapse,
|
||||
onFocus,
|
||||
diffing,
|
||||
onViewSourceInDebugger,
|
||||
onViewIndividuals,
|
||||
} = this.props;
|
||||
|
||||
const report = census.report;
|
||||
let parentMap = census.parentMap;
|
||||
const { totalBytes, totalCount } = report;
|
||||
|
||||
const getPercentBytes = totalBytes === 0
|
||||
? _ => 0
|
||||
: bytes => (bytes / totalBytes) * 100;
|
||||
|
||||
const getPercentCount = totalCount === 0
|
||||
? _ => 0
|
||||
: count => (count / totalCount) * 100;
|
||||
|
||||
return Tree({
|
||||
autoExpandDepth: 0,
|
||||
focused: census.focused,
|
||||
getParent: node => {
|
||||
const parent = parentMap[node.id];
|
||||
return parent === report ? null : parent;
|
||||
},
|
||||
getChildren: node => node.children || [],
|
||||
isExpanded: node => census.expanded.has(node.id),
|
||||
onExpand,
|
||||
onCollapse,
|
||||
onFocus,
|
||||
renderItem: (item, depth, focused, arrow, expanded) =>
|
||||
new CensusTreeItem({
|
||||
onViewSourceInDebugger,
|
||||
item,
|
||||
depth,
|
||||
focused,
|
||||
arrow,
|
||||
expanded,
|
||||
getPercentBytes,
|
||||
getPercentCount,
|
||||
diffing,
|
||||
inverted: census.display.inverted,
|
||||
onViewIndividuals,
|
||||
}),
|
||||
getRoots: () => report.children || [],
|
||||
getKey: node => node.id,
|
||||
itemHeight: TREE_ROW_HEIGHT,
|
||||
});
|
||||
}
|
||||
});
|
||||
44
devtools/client/memory/components/dominator-tree-header.js
Normal file
44
devtools/client/memory/components/dominator-tree-header.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N } = require("../utils");
|
||||
|
||||
const DominatorTreeHeader = module.exports = createClass({
|
||||
displayName: "DominatorTreeHeader",
|
||||
|
||||
propTypes: { },
|
||||
|
||||
render() {
|
||||
return dom.div(
|
||||
{
|
||||
className: "header"
|
||||
},
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-bytes",
|
||||
title: L10N.getStr("heapview.field.retainedSize.tooltip"),
|
||||
},
|
||||
L10N.getStr("heapview.field.retainedSize")
|
||||
),
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-bytes",
|
||||
title: L10N.getStr("heapview.field.shallowSize.tooltip"),
|
||||
},
|
||||
L10N.getStr("heapview.field.shallowSize")
|
||||
),
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-name",
|
||||
title: L10N.getStr("dominatortree.field.label.tooltip"),
|
||||
},
|
||||
L10N.getStr("dominatortree.field.label")
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
142
devtools/client/memory/components/dominator-tree-item.js
Normal file
142
devtools/client/memory/components/dominator-tree-item.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { assert, isSavedFrame } = require("devtools/shared/DevToolsUtils");
|
||||
const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N, formatNumber, formatPercent } = require("../utils");
|
||||
const Frame = createFactory(require("devtools/client/shared/components/frame"));
|
||||
const { TREE_ROW_HEIGHT } = require("../constants");
|
||||
|
||||
const Separator = createFactory(createClass({
|
||||
displayName: "Separator",
|
||||
|
||||
render() {
|
||||
return dom.span({ className: "separator" }, "›");
|
||||
}
|
||||
}));
|
||||
|
||||
const DominatorTreeItem = module.exports = createClass({
|
||||
displayName: "DominatorTreeItem",
|
||||
|
||||
propTypes: {
|
||||
item: PropTypes.object.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
arrow: PropTypes.object,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
getPercentSize: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.item != nextProps.item
|
||||
|| this.props.depth != nextProps.depth
|
||||
|| this.props.expanded != nextProps.expanded
|
||||
|| this.props.focused != nextProps.focused;
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
item,
|
||||
depth,
|
||||
arrow,
|
||||
focused,
|
||||
getPercentSize,
|
||||
onViewSourceInDebugger,
|
||||
} = this.props;
|
||||
|
||||
const retainedSize = formatNumber(item.retainedSize);
|
||||
const percentRetainedSize = formatPercent(getPercentSize(item.retainedSize));
|
||||
|
||||
const shallowSize = formatNumber(item.shallowSize);
|
||||
const percentShallowSize = formatPercent(getPercentSize(item.shallowSize));
|
||||
|
||||
// Build up our label UI as an array of each label piece, which is either a
|
||||
// string or a frame, and separators in between them.
|
||||
|
||||
assert(item.label.length > 0,
|
||||
"Our label should not be empty");
|
||||
const label = Array(item.label.length * 2 - 1);
|
||||
label.fill(undefined);
|
||||
|
||||
for (let i = 0, length = item.label.length; i < length; i++) {
|
||||
const piece = item.label[i];
|
||||
const key = `${item.nodeId}-label-${i}`;
|
||||
|
||||
// `i` is the index of the label piece we are rendering, `label[i*2]` is
|
||||
// where the rendered label piece belngs, and `label[i*2+1]` (if it isn't
|
||||
// out of bounds) is where the separator belongs.
|
||||
|
||||
if (isSavedFrame(piece)) {
|
||||
label[i * 2] = Frame({
|
||||
key,
|
||||
onClick: () => onViewSourceInDebugger(piece),
|
||||
frame: piece,
|
||||
showFunctionName: true
|
||||
});
|
||||
} else if (piece === "noStack") {
|
||||
label[i * 2] = dom.span({ key, className: "not-available" },
|
||||
L10N.getStr("tree-item.nostack"));
|
||||
} else if (piece === "noFilename") {
|
||||
label[i * 2] = dom.span({ key, className: "not-available" },
|
||||
L10N.getStr("tree-item.nofilename"));
|
||||
} else if (piece === "JS::ubi::RootList") {
|
||||
// Don't use the usual labeling machinery for root lists: replace it
|
||||
// with the "GC Roots" string.
|
||||
label.splice(0, label.length);
|
||||
label.push(L10N.getStr("tree-item.rootlist"));
|
||||
break;
|
||||
} else {
|
||||
label[i * 2] = piece;
|
||||
}
|
||||
|
||||
// If this is not the last piece of the label, add a separator.
|
||||
if (i < length - 1) {
|
||||
label[i * 2 + 1] = Separator({ key: `${item.nodeId}-separator-${i}` });
|
||||
}
|
||||
}
|
||||
|
||||
return dom.div(
|
||||
{
|
||||
className: `heap-tree-item ${focused ? "focused" : ""} node-${item.nodeId}`
|
||||
},
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-field heap-tree-item-bytes"
|
||||
},
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-number"
|
||||
},
|
||||
retainedSize
|
||||
),
|
||||
dom.span({ className: "heap-tree-percent" }, percentRetainedSize)
|
||||
),
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-field heap-tree-item-bytes"
|
||||
},
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-number"
|
||||
},
|
||||
shallowSize
|
||||
),
|
||||
dom.span({ className: "heap-tree-percent" }, percentShallowSize)
|
||||
),
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-field heap-tree-item-name",
|
||||
style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
|
||||
},
|
||||
arrow,
|
||||
label,
|
||||
dom.span({ className: "heap-tree-item-address" },
|
||||
`@ 0x${item.nodeId.toString(16)}`)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
216
devtools/client/memory/components/dominator-tree.js
Normal file
216
devtools/client/memory/components/dominator-tree.js
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils");
|
||||
const { createParentMap } = require("devtools/shared/heapsnapshot/CensusUtils");
|
||||
const Tree = createFactory(require("devtools/client/shared/components/tree"));
|
||||
const DominatorTreeItem = createFactory(require("./dominator-tree-item"));
|
||||
const { L10N } = require("../utils");
|
||||
const { TREE_ROW_HEIGHT, dominatorTreeState } = require("../constants");
|
||||
const { dominatorTreeModel } = require("../models");
|
||||
const DominatorTreeLazyChildren = require("../dominator-tree-lazy-children");
|
||||
|
||||
const DOMINATOR_TREE_AUTO_EXPAND_DEPTH = 3;
|
||||
|
||||
/**
|
||||
* A throbber that represents a subtree in the dominator tree that is actively
|
||||
* being incrementally loaded and fetched from the `HeapAnalysesWorker`.
|
||||
*/
|
||||
const DominatorTreeSubtreeFetching = createFactory(createClass({
|
||||
displayName: "DominatorTreeSubtreeFetching",
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.depth !== nextProps.depth
|
||||
|| this.props.focused !== nextProps.focused;
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
depth: PropTypes.number.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
depth,
|
||||
focused,
|
||||
} = this.props;
|
||||
|
||||
return dom.div(
|
||||
{
|
||||
className: `heap-tree-item subtree-fetching ${focused ? "focused" : ""}`
|
||||
},
|
||||
dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
|
||||
dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
|
||||
dom.span({
|
||||
className: "heap-tree-item-field heap-tree-item-name devtools-throbber",
|
||||
style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
|
||||
})
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* A link to fetch and load more siblings in the dominator tree, when there are
|
||||
* already many loaded above.
|
||||
*/
|
||||
const DominatorTreeSiblingLink = createFactory(createClass({
|
||||
displayName: "DominatorTreeSiblingLink",
|
||||
|
||||
propTypes: {
|
||||
depth: PropTypes.number.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
item: PropTypes.instanceOf(DominatorTreeLazyChildren).isRequired,
|
||||
onLoadMoreSiblings: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.depth !== nextProps.depth
|
||||
|| this.props.focused !== nextProps.focused;
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
depth,
|
||||
focused,
|
||||
item,
|
||||
onLoadMoreSiblings,
|
||||
} = this.props;
|
||||
|
||||
return dom.div(
|
||||
{
|
||||
className: `heap-tree-item more-children ${focused ? "focused" : ""}`
|
||||
},
|
||||
dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
|
||||
dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-field heap-tree-item-name",
|
||||
style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
|
||||
},
|
||||
dom.a(
|
||||
{
|
||||
onClick: () => onLoadMoreSiblings(item)
|
||||
},
|
||||
L10N.getStr("tree-item.load-more")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* The actual dominator tree rendered as an expandable and collapsible tree.
|
||||
*/
|
||||
const DominatorTree = module.exports = createClass({
|
||||
displayName: "DominatorTree",
|
||||
|
||||
propTypes: {
|
||||
dominatorTree: dominatorTreeModel.isRequired,
|
||||
onLoadMoreSiblings: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
// Safe to use referential equality here because all of our mutations on
|
||||
// dominator tree models use immutableUpdate in a persistent manner. The
|
||||
// exception to the rule are mutations of the expanded set, however we take
|
||||
// care that the dominatorTree model itself is still re-allocated when
|
||||
// mutations to the expanded set occur. Because of the re-allocations, we
|
||||
// can continue using referential equality here.
|
||||
return this.props.dominatorTree !== nextProps.dominatorTree;
|
||||
},
|
||||
|
||||
render() {
|
||||
const { dominatorTree, onViewSourceInDebugger, onLoadMoreSiblings } = this.props;
|
||||
|
||||
const parentMap = createParentMap(dominatorTree.root, node => node.nodeId);
|
||||
|
||||
return Tree({
|
||||
key: "dominator-tree-tree",
|
||||
autoExpandDepth: DOMINATOR_TREE_AUTO_EXPAND_DEPTH,
|
||||
focused: dominatorTree.focused,
|
||||
getParent: node =>
|
||||
node instanceof DominatorTreeLazyChildren
|
||||
? parentMap[node.parentNodeId()]
|
||||
: parentMap[node.nodeId],
|
||||
getChildren: node => {
|
||||
const children = node.children ? node.children.slice() : [];
|
||||
if (node.moreChildrenAvailable) {
|
||||
children.push(new DominatorTreeLazyChildren(node.nodeId, children.length));
|
||||
}
|
||||
return children;
|
||||
},
|
||||
isExpanded: node => {
|
||||
return node instanceof DominatorTreeLazyChildren
|
||||
? false
|
||||
: dominatorTree.expanded.has(node.nodeId);
|
||||
},
|
||||
onExpand: item => {
|
||||
if (item instanceof DominatorTreeLazyChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.moreChildrenAvailable && (!item.children || !item.children.length)) {
|
||||
const startIndex = item.children ? item.children.length : 0;
|
||||
onLoadMoreSiblings(new DominatorTreeLazyChildren(item.nodeId, startIndex));
|
||||
}
|
||||
|
||||
this.props.onExpand(item);
|
||||
},
|
||||
onCollapse: item => {
|
||||
if (item instanceof DominatorTreeLazyChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onCollapse(item);
|
||||
},
|
||||
onFocus: item => {
|
||||
if (item instanceof DominatorTreeLazyChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onFocus(item);
|
||||
},
|
||||
renderItem: (item, depth, focused, arrow, expanded) => {
|
||||
if (item instanceof DominatorTreeLazyChildren) {
|
||||
if (item.isFirstChild()) {
|
||||
assert(dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
|
||||
"If we are displaying a throbber for loading a subtree, " +
|
||||
"then we should be INCREMENTAL_FETCHING those children right now");
|
||||
return DominatorTreeSubtreeFetching({
|
||||
key: item.key(),
|
||||
depth,
|
||||
focused,
|
||||
});
|
||||
}
|
||||
|
||||
return DominatorTreeSiblingLink({
|
||||
key: item.key(),
|
||||
item,
|
||||
depth,
|
||||
focused,
|
||||
onLoadMoreSiblings,
|
||||
});
|
||||
}
|
||||
|
||||
return DominatorTreeItem({
|
||||
item,
|
||||
depth,
|
||||
focused,
|
||||
arrow,
|
||||
expanded,
|
||||
getPercentSize: size => (size / dominatorTree.root.retainedSize) * 100,
|
||||
onViewSourceInDebugger,
|
||||
});
|
||||
},
|
||||
getRoots: () => [dominatorTree.root],
|
||||
getKey: node =>
|
||||
node instanceof DominatorTreeLazyChildren ? node.key() : node.nodeId,
|
||||
itemHeight: TREE_ROW_HEIGHT,
|
||||
});
|
||||
}
|
||||
});
|
||||
455
devtools/client/memory/components/heap.js
Normal file
455
devtools/client/memory/components/heap.js
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils");
|
||||
const 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 Individuals = createFactory(require("./individuals"));
|
||||
const IndividualsHeader = createFactory(require("./individuals-header"));
|
||||
const ShortestPaths = createFactory(require("./shortest-paths"));
|
||||
const { getStatusTextFull, L10N } = require("../utils");
|
||||
const {
|
||||
snapshotState: states,
|
||||
diffingState,
|
||||
viewState,
|
||||
censusState,
|
||||
treeMapState,
|
||||
dominatorTreeState,
|
||||
individualsState,
|
||||
} = require("../constants");
|
||||
const models = require("../models");
|
||||
const { snapshot: snapshotModel, diffingModel } = models;
|
||||
|
||||
/**
|
||||
* Get the app state's current state atom.
|
||||
*
|
||||
* @see the relevant state string constants in `../constants.js`.
|
||||
*
|
||||
* @param {models.view} view
|
||||
* @param {snapshotModel} snapshot
|
||||
* @param {diffingModel} diffing
|
||||
* @param {individualsModel} individuals
|
||||
*
|
||||
* @return {snapshotState|diffingState|dominatorTreeState}
|
||||
*/
|
||||
function getState(view, snapshot, diffing, individuals) {
|
||||
switch (view.state) {
|
||||
case viewState.CENSUS:
|
||||
return snapshot.census
|
||||
? snapshot.census.state
|
||||
: snapshot.state;
|
||||
|
||||
case viewState.DIFFING:
|
||||
return diffing.state;
|
||||
|
||||
case viewState.TREE_MAP:
|
||||
return snapshot.treeMap
|
||||
? snapshot.treeMap.state
|
||||
: snapshot.state;
|
||||
|
||||
case viewState.DOMINATOR_TREE:
|
||||
return snapshot.dominatorTree
|
||||
? snapshot.dominatorTree.state
|
||||
: snapshot.state;
|
||||
|
||||
case viewState.INDIVIDUALS:
|
||||
return individuals.state;
|
||||
}
|
||||
|
||||
assert(false, `Unexpected view state: ${view.state}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if we should display a status message when we are in the given
|
||||
* state. Return false otherwise.
|
||||
*
|
||||
* @param {snapshotState|diffingState|dominatorTreeState} state
|
||||
* @param {models.view} view
|
||||
* @param {snapshotModel} snapshot
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function shouldDisplayStatus(state, view, snapshot) {
|
||||
switch (state) {
|
||||
case states.IMPORTING:
|
||||
case states.SAVING:
|
||||
case states.SAVED:
|
||||
case states.READING:
|
||||
case censusState.SAVING:
|
||||
case treeMapState.SAVING:
|
||||
case diffingState.SELECTING:
|
||||
case diffingState.TAKING_DIFF:
|
||||
case dominatorTreeState.COMPUTING:
|
||||
case dominatorTreeState.COMPUTED:
|
||||
case dominatorTreeState.FETCHING:
|
||||
case individualsState.COMPUTING_DOMINATOR_TREE:
|
||||
case individualsState.FETCHING:
|
||||
return true;
|
||||
}
|
||||
return view.state === viewState.DOMINATOR_TREE && !snapshot.dominatorTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status text to display for the given state.
|
||||
*
|
||||
* @param {snapshotState|diffingState|dominatorTreeState} state
|
||||
* @param {diffingModel} diffing
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
function getStateStatusText(state, diffing) {
|
||||
if (state === diffingState.SELECTING) {
|
||||
return L10N.getStr(diffing.firstSnapshotId === null
|
||||
? "diffing.prompt.selectBaseline"
|
||||
: "diffing.prompt.selectComparison");
|
||||
}
|
||||
|
||||
return getStatusTextFull(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given that we should display a status message, return true if we should also
|
||||
* display a throbber along with the status message. Return false otherwise.
|
||||
*
|
||||
* @param {diffingModel} diffing
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function shouldDisplayThrobber(diffing) {
|
||||
return !diffing || diffing.state !== diffingState.SELECTING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state's error, or return null if there is none.
|
||||
*
|
||||
* @param {snapshotModel} snapshot
|
||||
* @param {diffingModel} diffing
|
||||
* @param {individualsModel} individuals
|
||||
*
|
||||
* @returns {Error|null}
|
||||
*/
|
||||
function getError(snapshot, diffing, individuals) {
|
||||
if (diffing) {
|
||||
if (diffing.state === diffingState.ERROR) {
|
||||
return diffing.error;
|
||||
}
|
||||
if (diffing.census === censusState.ERROR) {
|
||||
return diffing.census.error;
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot) {
|
||||
if (snapshot.state === states.ERROR) {
|
||||
return snapshot.error;
|
||||
}
|
||||
|
||||
if (snapshot.census === censusState.ERROR) {
|
||||
return snapshot.census.error;
|
||||
}
|
||||
|
||||
if (snapshot.treeMap === treeMapState.ERROR) {
|
||||
return snapshot.treeMap.error;
|
||||
}
|
||||
|
||||
if (snapshot.dominatorTree &&
|
||||
snapshot.dominatorTree.state === dominatorTreeState.ERROR) {
|
||||
return snapshot.dominatorTree.error;
|
||||
}
|
||||
}
|
||||
|
||||
if (individuals && individuals.state === individualsState.ERROR) {
|
||||
return individuals.error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main view for the memory tool.
|
||||
*
|
||||
* The Heap component contains several panels for different states; an initial
|
||||
* state of only a button to take a snapshot, loading states, the census view
|
||||
* tree, the dominator tree, etc.
|
||||
*/
|
||||
const Heap = module.exports = createClass({
|
||||
displayName: "Heap",
|
||||
|
||||
propTypes: {
|
||||
onSnapshotClick: PropTypes.func.isRequired,
|
||||
onLoadMoreSiblings: PropTypes.func.isRequired,
|
||||
onCensusExpand: PropTypes.func.isRequired,
|
||||
onCensusCollapse: PropTypes.func.isRequired,
|
||||
onDominatorTreeExpand: PropTypes.func.isRequired,
|
||||
onDominatorTreeCollapse: PropTypes.func.isRequired,
|
||||
onCensusFocus: PropTypes.func.isRequired,
|
||||
onDominatorTreeFocus: PropTypes.func.isRequired,
|
||||
onShortestPathsResize: PropTypes.func.isRequired,
|
||||
snapshot: snapshotModel,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onPopView: PropTypes.func.isRequired,
|
||||
individuals: models.individuals,
|
||||
onViewIndividuals: PropTypes.func.isRequired,
|
||||
onFocusIndividual: PropTypes.func.isRequired,
|
||||
diffing: diffingModel,
|
||||
view: models.view.isRequired,
|
||||
sizes: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
snapshot,
|
||||
diffing,
|
||||
onSnapshotClick,
|
||||
onLoadMoreSiblings,
|
||||
onViewSourceInDebugger,
|
||||
onViewIndividuals,
|
||||
individuals,
|
||||
view,
|
||||
} = this.props;
|
||||
|
||||
|
||||
if (!diffing && !snapshot && !individuals) {
|
||||
return this._renderInitial(onSnapshotClick);
|
||||
}
|
||||
|
||||
const state = getState(view, snapshot, diffing, individuals);
|
||||
const statusText = getStateStatusText(state, diffing);
|
||||
|
||||
if (shouldDisplayStatus(state, view, snapshot)) {
|
||||
return this._renderStatus(state, statusText, diffing);
|
||||
}
|
||||
|
||||
const error = getError(snapshot, diffing, individuals);
|
||||
if (error) {
|
||||
return this._renderError(state, statusText, error);
|
||||
}
|
||||
|
||||
if (view.state === viewState.CENSUS || view.state === viewState.DIFFING) {
|
||||
const census = view.state === viewState.CENSUS
|
||||
? snapshot.census
|
||||
: diffing.census;
|
||||
if (!census) {
|
||||
return this._renderStatus(state, statusText, diffing);
|
||||
}
|
||||
return this._renderCensus(state, census, diffing, onViewSourceInDebugger,
|
||||
onViewIndividuals);
|
||||
}
|
||||
|
||||
if (view.state === viewState.TREE_MAP) {
|
||||
return this._renderTreeMap(state, snapshot.treeMap);
|
||||
}
|
||||
|
||||
if (view.state === viewState.INDIVIDUALS) {
|
||||
assert(individuals.state === individualsState.FETCHED,
|
||||
"Should have fetched the individuals -- other states are rendered as statuses");
|
||||
return this._renderIndividuals(state, individuals,
|
||||
individuals.dominatorTree,
|
||||
onViewSourceInDebugger);
|
||||
}
|
||||
|
||||
assert(view.state === viewState.DOMINATOR_TREE,
|
||||
"If we aren't in progress, looking at a census, or diffing, then we " +
|
||||
"must be looking at a dominator tree");
|
||||
assert(!diffing, "Should not have diffing");
|
||||
assert(snapshot.dominatorTree, "Should have a dominator tree");
|
||||
|
||||
return this._renderDominatorTree(state, onViewSourceInDebugger, snapshot.dominatorTree,
|
||||
onLoadMoreSiblings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the heap view's container panel with the given contents inside of
|
||||
* it.
|
||||
*
|
||||
* @param {snapshotState|diffingState|dominatorTreeState} state
|
||||
* @param {...Any} contents
|
||||
*/
|
||||
_renderHeapView(state, ...contents) {
|
||||
return dom.div(
|
||||
{
|
||||
id: "heap-view",
|
||||
"data-state": state
|
||||
},
|
||||
dom.div(
|
||||
{
|
||||
className: "heap-view-panel",
|
||||
"data-state": state,
|
||||
},
|
||||
...contents
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
_renderInitial(onSnapshotClick) {
|
||||
return this._renderHeapView("initial", dom.button(
|
||||
{
|
||||
className: "devtools-toolbarbutton take-snapshot",
|
||||
onClick: onSnapshotClick,
|
||||
"data-standalone": true,
|
||||
"data-text-only": true,
|
||||
},
|
||||
L10N.getStr("take-snapshot")
|
||||
));
|
||||
},
|
||||
|
||||
_renderStatus(state, statusText, diffing) {
|
||||
let throbber = "";
|
||||
if (shouldDisplayThrobber(diffing)) {
|
||||
throbber = "devtools-throbber";
|
||||
}
|
||||
|
||||
return this._renderHeapView(state, dom.span(
|
||||
{
|
||||
className: `snapshot-status ${throbber}`
|
||||
},
|
||||
statusText
|
||||
));
|
||||
},
|
||||
|
||||
_renderError(state, statusText, error) {
|
||||
return this._renderHeapView(
|
||||
state,
|
||||
dom.span({ className: "snapshot-status error" }, statusText),
|
||||
dom.pre({}, safeErrorString(error))
|
||||
);
|
||||
},
|
||||
|
||||
_renderCensus(state, census, diffing, onViewSourceInDebugger, onViewIndividuals) {
|
||||
assert(census.report, "Should not render census that does not have a report");
|
||||
|
||||
if (!census.report.children) {
|
||||
const msg = diffing ? L10N.getStr("heapview.no-difference")
|
||||
: census.filter ? L10N.getStr("heapview.none-match")
|
||||
: L10N.getStr("heapview.empty");
|
||||
return this._renderHeapView(state, dom.div({ className: "empty" }, msg));
|
||||
}
|
||||
|
||||
const contents = [];
|
||||
|
||||
if (census.display.breakdown.by === "allocationStack"
|
||||
&& census.report.children
|
||||
&& census.report.children.length === 1
|
||||
&& census.report.children[0].name === "noStack") {
|
||||
contents.push(dom.div({ className: "error no-allocation-stacks" },
|
||||
L10N.getStr("heapview.noAllocationStacks")));
|
||||
}
|
||||
|
||||
contents.push(CensusHeader({ diffing }));
|
||||
contents.push(Census({
|
||||
onViewSourceInDebugger,
|
||||
onViewIndividuals,
|
||||
diffing,
|
||||
census,
|
||||
onExpand: node => this.props.onCensusExpand(census, node),
|
||||
onCollapse: node => this.props.onCensusCollapse(census, node),
|
||||
onFocus: node => this.props.onCensusFocus(census, node),
|
||||
}));
|
||||
|
||||
return this._renderHeapView(state, ...contents);
|
||||
},
|
||||
|
||||
_renderTreeMap(state, treeMap) {
|
||||
return this._renderHeapView(
|
||||
state,
|
||||
TreeMap({ treeMap })
|
||||
);
|
||||
},
|
||||
|
||||
_renderIndividuals(state, individuals, dominatorTree, onViewSourceInDebugger) {
|
||||
assert(individuals.state === individualsState.FETCHED,
|
||||
"Should have fetched individuals");
|
||||
assert(dominatorTree && dominatorTree.root,
|
||||
"Should have a dominator tree and its root");
|
||||
|
||||
const tree = dom.div(
|
||||
{
|
||||
className: "vbox",
|
||||
style: {
|
||||
overflowY: "auto"
|
||||
}
|
||||
},
|
||||
IndividualsHeader(),
|
||||
Individuals({
|
||||
individuals,
|
||||
dominatorTree,
|
||||
onViewSourceInDebugger,
|
||||
onFocus: this.props.onFocusIndividual
|
||||
})
|
||||
);
|
||||
|
||||
const shortestPaths = ShortestPaths({
|
||||
graph: individuals.focused
|
||||
? individuals.focused.shortestPaths
|
||||
: null
|
||||
});
|
||||
|
||||
return this._renderHeapView(
|
||||
state,
|
||||
dom.div(
|
||||
{ className: "hbox devtools-toolbar" },
|
||||
dom.label(
|
||||
{ id: "pop-view-button-label" },
|
||||
dom.button(
|
||||
{
|
||||
id: "pop-view-button",
|
||||
className: "devtools-button",
|
||||
onClick: this.props.onPopView,
|
||||
},
|
||||
L10N.getStr("toolbar.pop-view")
|
||||
),
|
||||
L10N.getStr("toolbar.pop-view.label")
|
||||
),
|
||||
L10N.getStr("toolbar.viewing-individuals")
|
||||
),
|
||||
HSplitBox({
|
||||
start: tree,
|
||||
end: shortestPaths,
|
||||
startWidth: this.props.sizes.shortestPathsSize,
|
||||
onResize: this.props.onShortestPathsResize,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
|
||||
const tree = dom.div(
|
||||
{
|
||||
className: "vbox",
|
||||
style: {
|
||||
overflowY: "auto"
|
||||
}
|
||||
},
|
||||
DominatorTreeHeader(),
|
||||
DominatorTree({
|
||||
onViewSourceInDebugger,
|
||||
dominatorTree,
|
||||
onLoadMoreSiblings,
|
||||
onExpand: this.props.onDominatorTreeExpand,
|
||||
onCollapse: this.props.onDominatorTreeCollapse,
|
||||
onFocus: this.props.onDominatorTreeFocus,
|
||||
})
|
||||
);
|
||||
|
||||
const shortestPaths = ShortestPaths({
|
||||
graph: dominatorTree.focused
|
||||
? dominatorTree.focused.shortestPaths
|
||||
: null
|
||||
});
|
||||
|
||||
return this._renderHeapView(
|
||||
state,
|
||||
HSplitBox({
|
||||
start: tree,
|
||||
end: shortestPaths,
|
||||
startWidth: this.props.sizes.shortestPathsSize,
|
||||
onResize: this.props.onShortestPathsResize,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
44
devtools/client/memory/components/individuals-header.js
Normal file
44
devtools/client/memory/components/individuals-header.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N } = require("../utils");
|
||||
|
||||
const IndividualsHeader = module.exports = createClass({
|
||||
displayName: "IndividualsHeader",
|
||||
|
||||
propTypes: { },
|
||||
|
||||
render() {
|
||||
return dom.div(
|
||||
{
|
||||
className: "header"
|
||||
},
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-bytes",
|
||||
title: L10N.getStr("heapview.field.retainedSize.tooltip"),
|
||||
},
|
||||
L10N.getStr("heapview.field.retainedSize")
|
||||
),
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-bytes",
|
||||
title: L10N.getStr("heapview.field.shallowSize.tooltip"),
|
||||
},
|
||||
L10N.getStr("heapview.field.shallowSize")
|
||||
),
|
||||
|
||||
dom.span(
|
||||
{
|
||||
className: "heap-tree-item-name",
|
||||
title: L10N.getStr("individuals.field.node.tooltip"),
|
||||
},
|
||||
L10N.getStr("individuals.field.node")
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
61
devtools/client/memory/components/individuals.js
Normal file
61
devtools/client/memory/components/individuals.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { createParentMap } = require("devtools/shared/heapsnapshot/CensusUtils");
|
||||
const Tree = createFactory(require("devtools/client/shared/components/tree"));
|
||||
const DominatorTreeItem = createFactory(require("./dominator-tree-item"));
|
||||
const { L10N } = require("../utils");
|
||||
const { TREE_ROW_HEIGHT } = require("../constants");
|
||||
const models = require("../models");
|
||||
|
||||
/**
|
||||
* The list of individuals in a census group.
|
||||
*/
|
||||
const Individuals = module.exports = createClass({
|
||||
displayName: "Individuals",
|
||||
|
||||
propTypes: {
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
individuals: models.individuals,
|
||||
dominatorTree: models.dominatorTreeModel,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
individuals,
|
||||
dominatorTree,
|
||||
onViewSourceInDebugger,
|
||||
onFocus,
|
||||
} = this.props;
|
||||
|
||||
return Tree({
|
||||
key: "individuals-tree",
|
||||
autoExpandDepth: 0,
|
||||
focused: individuals.focused,
|
||||
getParent: node => null,
|
||||
getChildren: node => [],
|
||||
isExpanded: node => false,
|
||||
onExpand: () => {},
|
||||
onCollapse: () => {},
|
||||
onFocus,
|
||||
renderItem: (item, depth, focused, _, expanded) => {
|
||||
return DominatorTreeItem({
|
||||
item,
|
||||
depth,
|
||||
focused,
|
||||
arrow: undefined,
|
||||
expanded,
|
||||
getPercentSize: size => (size / dominatorTree.root.retainedSize) * 100,
|
||||
onViewSourceInDebugger,
|
||||
});
|
||||
},
|
||||
getRoots: () => individuals.nodes,
|
||||
getKey: node => node.nodeId,
|
||||
itemHeight: TREE_ROW_HEIGHT,
|
||||
});
|
||||
}
|
||||
});
|
||||
35
devtools/client/memory/components/list.js
Normal file
35
devtools/client/memory/components/list.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
|
||||
/**
|
||||
* Generic list component that takes another react component to represent
|
||||
* the children nodes as `itemComponent`, and a list of items to render
|
||||
* as that component with a click handler.
|
||||
*/
|
||||
const List = module.exports = createClass({
|
||||
displayName: "List",
|
||||
|
||||
propTypes: {
|
||||
itemComponent: PropTypes.any.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
items: PropTypes.array.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
let { items, onClick, itemComponent: Item } = this.props;
|
||||
|
||||
return (
|
||||
dom.ul({ className: "list" }, ...items.map((item, index) => {
|
||||
return Item(Object.assign({}, this.props, {
|
||||
key: index,
|
||||
item,
|
||||
index,
|
||||
onClick: () => onClick(item),
|
||||
}));
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
25
devtools/client/memory/components/moz.build
Normal file
25
devtools/client/memory/components/moz.build
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += [
|
||||
'tree-map',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'census-header.js',
|
||||
'census-tree-item.js',
|
||||
'census.js',
|
||||
'dominator-tree-header.js',
|
||||
'dominator-tree-item.js',
|
||||
'dominator-tree.js',
|
||||
'heap.js',
|
||||
'individuals-header.js',
|
||||
'individuals.js',
|
||||
'list.js',
|
||||
'shortest-paths.js',
|
||||
'snapshot-list-item.js',
|
||||
'toolbar.js',
|
||||
'tree-map.js',
|
||||
)
|
||||
184
devtools/client/memory/components/shortest-paths.js
Normal file
184
devtools/client/memory/components/shortest-paths.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/* 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,
|
||||
PropTypes,
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
|
||||
const { getSourceNames } = require("devtools/client/shared/source-utils");
|
||||
const { L10N } = require("../utils");
|
||||
|
||||
const GRAPH_DEFAULTS = {
|
||||
translate: [20, 20],
|
||||
scale: 1
|
||||
};
|
||||
|
||||
const NO_STACK = "noStack";
|
||||
const NO_FILENAME = "noFilename";
|
||||
const ROOT_LIST = "JS::ubi::RootList";
|
||||
|
||||
function stringifyLabel(label, id) {
|
||||
const sanitized = [];
|
||||
|
||||
for (let i = 0, length = label.length; i < length; i++) {
|
||||
const piece = label[i];
|
||||
|
||||
if (isSavedFrame(piece)) {
|
||||
const { short } = getSourceNames(piece.source);
|
||||
sanitized[i] = `${piece.functionDisplayName} @ ${short}:${piece.line}:${piece.column}`;
|
||||
} else if (piece === NO_STACK) {
|
||||
sanitized[i] = L10N.getStr("tree-item.nostack");
|
||||
} else if (piece === NO_FILENAME) {
|
||||
sanitized[i] = L10N.getStr("tree-item.nofilename");
|
||||
} else if (piece === ROOT_LIST) {
|
||||
// Don't use the usual labeling machinery for root lists: replace it
|
||||
// with the "GC Roots" string.
|
||||
sanitized.splice(0, label.length);
|
||||
sanitized.push(L10N.getStr("tree-item.rootlist"));
|
||||
break;
|
||||
} else {
|
||||
sanitized[i] = "" + piece;
|
||||
}
|
||||
}
|
||||
|
||||
return `${sanitized.join(" › ")} @ 0x${id.toString(16)}`;
|
||||
}
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "ShortestPaths",
|
||||
|
||||
propTypes: {
|
||||
graph: PropTypes.shape({
|
||||
nodes: PropTypes.arrayOf(PropTypes.object),
|
||||
edges: PropTypes.arrayOf(PropTypes.object),
|
||||
}),
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return { zoom: null };
|
||||
},
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.graph != nextProps.graph;
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.graph) {
|
||||
this._renderGraph(this.refs.container, this.props.graph);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.graph) {
|
||||
this._renderGraph(this.refs.container, this.props.graph);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.zoom) {
|
||||
this.state.zoom.on("zoom", null);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
let contents;
|
||||
if (this.props.graph) {
|
||||
// Let the componentDidMount or componentDidUpdate method draw the graph
|
||||
// with DagreD3. We just provide the container for the graph here.
|
||||
contents = dom.div({
|
||||
ref: "container",
|
||||
style: {
|
||||
flex: 1,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}
|
||||
});
|
||||
} else {
|
||||
contents = dom.div(
|
||||
{
|
||||
id: "shortest-paths-select-node-msg"
|
||||
},
|
||||
L10N.getStr("shortest-paths.select-node")
|
||||
);
|
||||
}
|
||||
|
||||
return dom.div(
|
||||
{
|
||||
id: "shortest-paths",
|
||||
className: "vbox",
|
||||
},
|
||||
dom.label(
|
||||
{
|
||||
id: "shortest-paths-header",
|
||||
className: "header",
|
||||
},
|
||||
L10N.getStr("shortest-paths.header")
|
||||
),
|
||||
contents
|
||||
);
|
||||
},
|
||||
|
||||
_renderGraph(container, { nodes, edges }) {
|
||||
if (!container.firstChild) {
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("id", "graph-svg");
|
||||
svg.setAttribute("xlink", "http://www.w3.org/1999/xlink");
|
||||
svg.style.width = "100%";
|
||||
svg.style.height = "100%";
|
||||
|
||||
const target = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
target.setAttribute("id", "graph-target");
|
||||
target.style.width = "100%";
|
||||
target.style.height = "100%";
|
||||
|
||||
svg.appendChild(target);
|
||||
container.appendChild(svg);
|
||||
}
|
||||
|
||||
const graph = new dagreD3.Digraph();
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
graph.addNode(nodes[i].id, {
|
||||
id: nodes[i].id,
|
||||
label: stringifyLabel(nodes[i].label, nodes[i].id),
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < edges.length; i++) {
|
||||
graph.addEdge(null, edges[i].from, edges[i].to, {
|
||||
label: edges[i].name
|
||||
});
|
||||
}
|
||||
|
||||
const renderer = new dagreD3.Renderer();
|
||||
renderer.drawNodes();
|
||||
renderer.drawEdgePaths();
|
||||
|
||||
const svg = d3.select("#graph-svg");
|
||||
const target = d3.select("#graph-target");
|
||||
|
||||
let zoom = this.state.zoom;
|
||||
if (!zoom) {
|
||||
zoom = d3.behavior.zoom().on("zoom", function () {
|
||||
target.attr(
|
||||
"transform",
|
||||
`translate(${d3.event.translate}) scale(${d3.event.scale})`
|
||||
);
|
||||
});
|
||||
svg.call(zoom);
|
||||
this.setState({ zoom });
|
||||
}
|
||||
|
||||
const { translate, scale } = GRAPH_DEFAULTS;
|
||||
zoom.scale(scale);
|
||||
zoom.translate(translate);
|
||||
target.attr("transform", `translate(${translate}) scale(${scale})`);
|
||||
|
||||
const layout = dagreD3.layout();
|
||||
renderer.layout(layout).run(graph, target);
|
||||
},
|
||||
});
|
||||
114
devtools/client/memory/components/snapshot-list-item.js
Normal file
114
devtools/client/memory/components/snapshot-list-item.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const {
|
||||
L10N,
|
||||
getSnapshotTitle,
|
||||
getSnapshotTotals,
|
||||
getStatusText,
|
||||
snapshotIsDiffable,
|
||||
getSavedCensus
|
||||
} = require("../utils");
|
||||
const {
|
||||
snapshotState: states,
|
||||
diffingState,
|
||||
censusState,
|
||||
treeMapState
|
||||
} = require("../constants");
|
||||
const { snapshot: snapshotModel } = require("../models");
|
||||
|
||||
const SnapshotListItem = module.exports = createClass({
|
||||
displayName: "SnapshotListItem",
|
||||
|
||||
propTypes: {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
item: snapshotModel.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
let { index, item: snapshot, onClick, onSave, onDelete, diffing } = this.props;
|
||||
let className = `snapshot-list-item ${snapshot.selected ? " selected" : ""}`;
|
||||
let statusText = getStatusText(snapshot.state);
|
||||
let wantThrobber = !!statusText;
|
||||
let title = getSnapshotTitle(snapshot);
|
||||
|
||||
const selectedForDiffing = diffing
|
||||
&& (diffing.firstSnapshotId === snapshot.id
|
||||
|| diffing.secondSnapshotId === snapshot.id);
|
||||
|
||||
let checkbox;
|
||||
if (diffing && snapshotIsDiffable(snapshot)) {
|
||||
if (diffing.state === diffingState.SELECTING) {
|
||||
wantThrobber = false;
|
||||
}
|
||||
|
||||
const checkboxAttrs = {
|
||||
type: "checkbox",
|
||||
checked: false,
|
||||
};
|
||||
|
||||
if (selectedForDiffing) {
|
||||
checkboxAttrs.checked = true;
|
||||
checkboxAttrs.disabled = true;
|
||||
className += " selected";
|
||||
statusText = L10N.getStr(diffing.firstSnapshotId === snapshot.id
|
||||
? "diffing.baseline"
|
||||
: "diffing.comparison");
|
||||
}
|
||||
|
||||
if (selectedForDiffing || diffing.state == diffingState.SELECTING) {
|
||||
checkbox = dom.input(checkboxAttrs);
|
||||
}
|
||||
}
|
||||
|
||||
let details;
|
||||
if (!selectedForDiffing) {
|
||||
// See if a tree map or census is in the read state.
|
||||
let census = getSavedCensus(snapshot);
|
||||
|
||||
// If there is census data, fill in the total bytes.
|
||||
if (census) {
|
||||
let { bytes } = getSnapshotTotals(census);
|
||||
let formatBytes = L10N.getFormatStr("aggregate.mb", L10N.numberWithDecimals(bytes / 1000000, 2));
|
||||
|
||||
details = dom.span({ className: "snapshot-totals" },
|
||||
dom.span({ className: "total-bytes" }, formatBytes)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!details) {
|
||||
details = dom.span({ className: "snapshot-state" }, statusText);
|
||||
}
|
||||
|
||||
let saveLink = !snapshot.path ? void 0 : dom.a({
|
||||
onClick: () => onSave(snapshot),
|
||||
className: "save",
|
||||
}, L10N.getStr("snapshot.io.save"));
|
||||
|
||||
let deleteButton = !snapshot.path ? void 0 : dom.button({
|
||||
onClick: () => onDelete(snapshot),
|
||||
className: "devtools-button delete",
|
||||
title: L10N.getStr("snapshot.io.delete")
|
||||
});
|
||||
|
||||
return (
|
||||
dom.li({ className, onClick },
|
||||
dom.span({ className: `snapshot-title ${wantThrobber ? " devtools-throbber" : ""}` },
|
||||
checkbox,
|
||||
title,
|
||||
deleteButton
|
||||
),
|
||||
dom.span({ className: "snapshot-info" },
|
||||
details,
|
||||
saveLink
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
300
devtools/client/memory/components/toolbar.js
Normal file
300
devtools/client/memory/components/toolbar.js
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
/* 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 { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N } = require("../utils");
|
||||
const models = require("../models");
|
||||
const { viewState } = require("../constants");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "Toolbar",
|
||||
|
||||
propTypes: {
|
||||
censusDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
censusDisplay: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onTakeSnapshotClick: PropTypes.func.isRequired,
|
||||
onImportClick: PropTypes.func.isRequired,
|
||||
onClearSnapshotsClick: PropTypes.func.isRequired,
|
||||
onCensusDisplayChange: PropTypes.func.isRequired,
|
||||
onToggleRecordAllocationStacks: PropTypes.func.isRequired,
|
||||
allocations: models.allocations,
|
||||
filterString: PropTypes.string,
|
||||
setFilterString: PropTypes.func.isRequired,
|
||||
diffing: models.diffingModel,
|
||||
onToggleDiffing: PropTypes.func.isRequired,
|
||||
view: models.view.isRequired,
|
||||
onViewChange: PropTypes.func.isRequired,
|
||||
labelDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
labelDisplay: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onLabelDisplayChange: PropTypes.func.isRequired,
|
||||
treeMapDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
onTreeMapDisplayChange: PropTypes.func.isRequired,
|
||||
snapshots: PropTypes.arrayOf(models.snapshot).isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
onTakeSnapshotClick,
|
||||
onImportClick,
|
||||
onClearSnapshotsClick,
|
||||
onCensusDisplayChange,
|
||||
censusDisplays,
|
||||
censusDisplay,
|
||||
labelDisplays,
|
||||
labelDisplay,
|
||||
onLabelDisplayChange,
|
||||
treeMapDisplays,
|
||||
onTreeMapDisplayChange,
|
||||
onToggleRecordAllocationStacks,
|
||||
allocations,
|
||||
filterString,
|
||||
setFilterString,
|
||||
snapshots,
|
||||
diffing,
|
||||
onToggleDiffing,
|
||||
view,
|
||||
onViewChange,
|
||||
} = this.props;
|
||||
|
||||
let viewToolbarOptions;
|
||||
if (view.state == viewState.CENSUS || view.state === viewState.DIFFING) {
|
||||
viewToolbarOptions = dom.div(
|
||||
{
|
||||
className: "toolbar-group"
|
||||
},
|
||||
|
||||
dom.label(
|
||||
{
|
||||
className: "display-by",
|
||||
title: L10N.getStr("toolbar.displayBy.tooltip"),
|
||||
},
|
||||
L10N.getStr("toolbar.displayBy"),
|
||||
dom.select(
|
||||
{
|
||||
id: "select-display",
|
||||
className: "select-display",
|
||||
onChange: e => {
|
||||
const newDisplay =
|
||||
censusDisplays.find(b => b.displayName === e.target.value);
|
||||
onCensusDisplayChange(newDisplay);
|
||||
},
|
||||
value: censusDisplay.displayName,
|
||||
},
|
||||
censusDisplays.map(({ tooltip, displayName }) => dom.option(
|
||||
{
|
||||
key: `display-${displayName}`,
|
||||
value: displayName,
|
||||
title: tooltip,
|
||||
},
|
||||
displayName
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
dom.div({ id: "toolbar-spacer", className: "spacer" }),
|
||||
|
||||
dom.input({
|
||||
id: "filter",
|
||||
type: "search",
|
||||
className: "devtools-filterinput",
|
||||
placeholder: L10N.getStr("filter.placeholder"),
|
||||
title: L10N.getStr("filter.tooltip"),
|
||||
onChange: event => setFilterString(event.target.value),
|
||||
value: filterString || undefined,
|
||||
})
|
||||
);
|
||||
} else if (view.state == 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.state === viewState.DOMINATOR_TREE ||
|
||||
view.state === viewState.INDIVIDUALS);
|
||||
|
||||
viewToolbarOptions = dom.div(
|
||||
{
|
||||
className: "toolbar-group"
|
||||
},
|
||||
|
||||
dom.label(
|
||||
{
|
||||
className: "label-by",
|
||||
title: L10N.getStr("toolbar.labelBy.tooltip"),
|
||||
},
|
||||
L10N.getStr("toolbar.labelBy"),
|
||||
dom.select(
|
||||
{
|
||||
id: "select-label-display",
|
||||
onChange: e => {
|
||||
const newDisplay =
|
||||
labelDisplays.find(b => b.displayName === e.target.value);
|
||||
onLabelDisplayChange(newDisplay);
|
||||
},
|
||||
value: labelDisplay.displayName,
|
||||
},
|
||||
labelDisplays.map(({ tooltip, displayName }) => dom.option(
|
||||
{
|
||||
key: `label-display-${displayName}`,
|
||||
value: displayName,
|
||||
title: tooltip,
|
||||
},
|
||||
displayName
|
||||
))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let viewSelect;
|
||||
if (view.state !== viewState.DIFFING && view.state !== viewState.INDIVIDUALS) {
|
||||
viewSelect = dom.label(
|
||||
{
|
||||
title: L10N.getStr("toolbar.view.tooltip"),
|
||||
},
|
||||
L10N.getStr("toolbar.view"),
|
||||
dom.select(
|
||||
{
|
||||
id: "select-view",
|
||||
onChange: e => onViewChange(e.target.value),
|
||||
defaultValue: view,
|
||||
value: view.state,
|
||||
},
|
||||
dom.option(
|
||||
{
|
||||
value: viewState.TREE_MAP,
|
||||
title: L10N.getStr("toolbar.view.treemap.tooltip"),
|
||||
},
|
||||
L10N.getStr("toolbar.view.treemap")
|
||||
),
|
||||
dom.option(
|
||||
{
|
||||
value: viewState.CENSUS,
|
||||
title: L10N.getStr("toolbar.view.census.tooltip"),
|
||||
},
|
||||
L10N.getStr("toolbar.view.census")
|
||||
),
|
||||
dom.option(
|
||||
{
|
||||
value: viewState.DOMINATOR_TREE,
|
||||
title: L10N.getStr("toolbar.view.dominators.tooltip"),
|
||||
},
|
||||
L10N.getStr("toolbar.view.dominators")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
dom.div(
|
||||
{
|
||||
className: "devtools-toolbar"
|
||||
},
|
||||
|
||||
dom.div(
|
||||
{
|
||||
className: "toolbar-group"
|
||||
},
|
||||
|
||||
dom.button({
|
||||
id: "clear-snapshots",
|
||||
className: "clear-snapshots devtools-button",
|
||||
disabled: !snapshots.length,
|
||||
onClick: onClearSnapshotsClick,
|
||||
title: L10N.getStr("clear-snapshots.tooltip")
|
||||
}),
|
||||
|
||||
dom.button({
|
||||
id: "take-snapshot",
|
||||
className: "take-snapshot devtools-button",
|
||||
onClick: onTakeSnapshotClick,
|
||||
title: L10N.getStr("take-snapshot")
|
||||
}),
|
||||
|
||||
dom.button(
|
||||
{
|
||||
id: "diff-snapshots",
|
||||
className: "devtools-button devtools-monospace" + (!!diffing ? " checked" : ""),
|
||||
disabled: snapshots.length < 2,
|
||||
onClick: onToggleDiffing,
|
||||
title: L10N.getStr("diff-snapshots.tooltip"),
|
||||
}
|
||||
),
|
||||
|
||||
dom.button(
|
||||
{
|
||||
id: "import-snapshot",
|
||||
className: "devtools-toolbarbutton import-snapshot devtools-button",
|
||||
onClick: onImportClick,
|
||||
title: L10N.getStr("import-snapshot"),
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
dom.label(
|
||||
{
|
||||
id: "record-allocation-stacks-label",
|
||||
title: L10N.getStr("checkbox.recordAllocationStacks.tooltip"),
|
||||
},
|
||||
dom.input({
|
||||
id: "record-allocation-stacks-checkbox",
|
||||
type: "checkbox",
|
||||
checked: allocations.recording,
|
||||
disabled: allocations.togglingInProgress,
|
||||
onChange: onToggleRecordAllocationStacks,
|
||||
}),
|
||||
L10N.getStr("checkbox.recordAllocationStacks")
|
||||
),
|
||||
|
||||
viewSelect,
|
||||
viewToolbarOptions
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
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];
|
||||
};
|
||||
316
devtools/client/memory/components/tree-map/drag-zoom.js
Normal file
316
devtools/client/memory/components/tree-map/drag-zoom.js
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
/* 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 prevZoomedWidth = container.offsetWidth * (1 + prevZoom);
|
||||
let prevZoomedHeight = container.offsetHeight * (1 + prevZoom);
|
||||
dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
|
||||
dragZoom.zoomedHeight = container.offsetHeight * (1 + dragZoom.zoom);
|
||||
let deltaWidth = dragZoom.zoomedWidth - prevZoomedWidth;
|
||||
let deltaHeight = dragZoom.zoomedHeight - prevZoomedHeight;
|
||||
|
||||
let mouseOffsetX = dragZoom.mouseX - container.offsetWidth / 2;
|
||||
let mouseOffsetY = dragZoom.mouseY - container.offsetHeight / 2;
|
||||
|
||||
// The ratio of where the center of the mouse is in regards to the total
|
||||
// zoomed width/height
|
||||
let ratioZoomX = (prevZoomedWidth / 2 + mouseOffsetX - dragZoom.translateX)
|
||||
/ prevZoomedWidth;
|
||||
let ratioZoomY = (prevZoomedHeight / 2 + mouseOffsetY - dragZoom.translateY)
|
||||
/ prevZoomedHeight;
|
||||
|
||||
// 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);
|
||||
|
||||
// Keep the canvas in range of the container
|
||||
keepInView(container, dragZoom);
|
||||
emitChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
container.addEventListener("wheel", handleWheel, false);
|
||||
|
||||
return function removeListener() {
|
||||
container.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.zoomedHeight - 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.zoomedHeight - container.offsetHeight) / 2 - dragZoom.translateY
|
||||
);
|
||||
}
|
||||
295
devtools/client/memory/components/tree-map/draw.js
Normal file
295
devtools/client/memory/components/tree-map/draw.js
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
/* 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 + FONT_SIZE, 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([
|
||||
// The d3 layout includes the padding around everything, add some
|
||||
// extra padding to the size to compensate for thi
|
||||
canvas.width + (PADDING[1] + PADDING[3]) * ratio,
|
||||
canvas.height + (PADDING[0] + PADDING[2]) * ratio
|
||||
])
|
||||
.sticky(true)
|
||||
.padding([
|
||||
PADDING[0] * 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 {Object} dragZoom
|
||||
* @param {Array} padding
|
||||
*/
|
||||
const drawText = exports.drawText = function (ctx, node, borderWidth, ratio,
|
||||
dragZoom, padding) {
|
||||
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 - padding[0]) * scale - dragZoom.offsetX;
|
||||
let y = margin + (node.y - padding[1]) * 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
|
||||
* @param {Array} padding
|
||||
*/
|
||||
const drawBox = exports.drawBox = function (ctx, node, borderWidth, dragZoom,
|
||||
padding) {
|
||||
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 - padding[0]) - dragZoom.offsetX + border / 2;
|
||||
let y = scale * (node.y - padding[1]) - 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;
|
||||
// Subtract the outer padding from the tree map layout.
|
||||
let padding = [PADDING[3] * ratio, PADDING[0] * ratio];
|
||||
|
||||
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, padding);
|
||||
drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = 60) {
|
||||
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();
|
||||
};
|
||||
};
|
||||
342
devtools/client/memory/constants.js
Normal file
342
devtools/client/memory/constants.js
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
/* 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";
|
||||
|
||||
// Options passed to MemoryFront's startRecordingAllocations never change.
|
||||
exports.ALLOCATION_RECORDING_OPTIONS = {
|
||||
probability: 1,
|
||||
maxLogLength: 1
|
||||
};
|
||||
|
||||
// If TREE_ROW_HEIGHT changes, be sure to change `var(--heap-tree-row-height)`
|
||||
// in `devtools/client/themes/memory.css`
|
||||
exports.TREE_ROW_HEIGHT = 18;
|
||||
|
||||
/** * Actions ******************************************************************/
|
||||
|
||||
const actions = exports.actions = {};
|
||||
|
||||
// Fired by UI to request a snapshot from the actor.
|
||||
actions.TAKE_SNAPSHOT_START = "take-snapshot-start";
|
||||
actions.TAKE_SNAPSHOT_END = "take-snapshot-end";
|
||||
|
||||
// When a heap snapshot is read into memory -- only fired
|
||||
// once per snapshot.
|
||||
actions.READ_SNAPSHOT_START = "read-snapshot-start";
|
||||
actions.READ_SNAPSHOT_END = "read-snapshot-end";
|
||||
|
||||
// When a census is being performed on a heap snapshot
|
||||
actions.TAKE_CENSUS_START = "take-census-start";
|
||||
actions.TAKE_CENSUS_END = "take-census-end";
|
||||
actions.TAKE_CENSUS_ERROR = "take-census-error";
|
||||
|
||||
// When a tree map is being calculated on a heap snapshot
|
||||
actions.TAKE_TREE_MAP_START = "take-tree-map-start";
|
||||
actions.TAKE_TREE_MAP_END = "take-tree-map-end";
|
||||
actions.TAKE_TREE_MAP_ERROR = "take-tree-map-error";
|
||||
|
||||
// When requesting that the server start/stop recording allocation stacks.
|
||||
actions.TOGGLE_RECORD_ALLOCATION_STACKS_START = "toggle-record-allocation-stacks-start";
|
||||
actions.TOGGLE_RECORD_ALLOCATION_STACKS_END = "toggle-record-allocation-stacks-end";
|
||||
|
||||
// When a heap snapshot is being saved to a user-specified
|
||||
// location on disk.
|
||||
actions.EXPORT_SNAPSHOT_START = "export-snapshot-start";
|
||||
actions.EXPORT_SNAPSHOT_END = "export-snapshot-end";
|
||||
actions.EXPORT_SNAPSHOT_ERROR = "export-snapshot-error";
|
||||
|
||||
// When a heap snapshot is being read from a user selected file,
|
||||
// and represents the entire state until the census is available.
|
||||
actions.IMPORT_SNAPSHOT_START = "import-snapshot-start";
|
||||
actions.IMPORT_SNAPSHOT_END = "import-snapshot-end";
|
||||
actions.IMPORT_SNAPSHOT_ERROR = "import-snapshot-error";
|
||||
|
||||
// Fired by UI to select a snapshot to view.
|
||||
actions.SELECT_SNAPSHOT = "select-snapshot";
|
||||
|
||||
// Fired to delete a provided list of snapshots
|
||||
actions.DELETE_SNAPSHOTS_START = "delete-snapshots-start";
|
||||
actions.DELETE_SNAPSHOTS_END = "delete-snapshots-end";
|
||||
|
||||
// Fired to toggle tree inversion on or off.
|
||||
actions.TOGGLE_INVERTED = "toggle-inverted";
|
||||
|
||||
// Fired when a snapshot is selected for diffing.
|
||||
actions.SELECT_SNAPSHOT_FOR_DIFFING = "select-snapshot-for-diffing";
|
||||
|
||||
// Fired when taking a census diff.
|
||||
actions.TAKE_CENSUS_DIFF_START = "take-census-diff-start";
|
||||
actions.TAKE_CENSUS_DIFF_END = "take-census-diff-end";
|
||||
actions.DIFFING_ERROR = "diffing-error";
|
||||
|
||||
// Fired to set a new census display.
|
||||
actions.SET_CENSUS_DISPLAY = "set-census-display";
|
||||
|
||||
// Fired to change the display that controls the dominator tree labels.
|
||||
actions.SET_LABEL_DISPLAY = "set-label-display";
|
||||
|
||||
// Fired to set a tree map display
|
||||
actions.SET_TREE_MAP_DISPLAY = "set-tree-map-display";
|
||||
|
||||
// Fired when changing between census or dominators view.
|
||||
actions.CHANGE_VIEW = "change-view";
|
||||
actions.POP_VIEW = "pop-view";
|
||||
|
||||
// Fired when there is an error processing a snapshot or taking a census.
|
||||
actions.SNAPSHOT_ERROR = "snapshot-error";
|
||||
|
||||
// Fired when there is a new filter string set.
|
||||
actions.SET_FILTER_STRING = "set-filter-string";
|
||||
|
||||
// Fired to expand or collapse nodes in census reports.
|
||||
actions.EXPAND_CENSUS_NODE = "expand-census-node";
|
||||
actions.EXPAND_DIFFING_CENSUS_NODE = "expand-diffing-census-node";
|
||||
actions.COLLAPSE_CENSUS_NODE = "collapse-census-node";
|
||||
actions.COLLAPSE_DIFFING_CENSUS_NODE = "collapse-diffing-census-node";
|
||||
|
||||
// Fired when nodes in various trees are focused.
|
||||
actions.FOCUS_CENSUS_NODE = "focus-census-node";
|
||||
actions.FOCUS_DIFFING_CENSUS_NODE = "focus-diffing-census-node";
|
||||
actions.FOCUS_DOMINATOR_TREE_NODE = "focus-dominator-tree-node";
|
||||
|
||||
actions.FOCUS_INDIVIDUAL = "focus-individual";
|
||||
actions.FETCH_INDIVIDUALS_START = "fetch-individuals-start";
|
||||
actions.FETCH_INDIVIDUALS_END = "fetch-individuals-end";
|
||||
actions.INDIVIDUALS_ERROR = "individuals-error";
|
||||
|
||||
actions.COMPUTE_DOMINATOR_TREE_START = "compute-dominator-tree-start";
|
||||
actions.COMPUTE_DOMINATOR_TREE_END = "compute-dominator-tree-end";
|
||||
actions.FETCH_DOMINATOR_TREE_START = "fetch-dominator-tree-start";
|
||||
actions.FETCH_DOMINATOR_TREE_END = "fetch-dominator-tree-end";
|
||||
actions.DOMINATOR_TREE_ERROR = "dominator-tree-error";
|
||||
actions.FETCH_IMMEDIATELY_DOMINATED_START = "fetch-immediately-dominated-start";
|
||||
actions.FETCH_IMMEDIATELY_DOMINATED_END = "fetch-immediately-dominated-end";
|
||||
actions.EXPAND_DOMINATOR_TREE_NODE = "expand-dominator-tree-node";
|
||||
actions.COLLAPSE_DOMINATOR_TREE_NODE = "collapse-dominator-tree-node";
|
||||
|
||||
actions.RESIZE_SHORTEST_PATHS = "resize-shortest-paths";
|
||||
|
||||
/** * Census Displays ***************************************************************/
|
||||
|
||||
const COUNT = Object.freeze({ by: "count", count: true, bytes: true });
|
||||
const INTERNAL_TYPE = Object.freeze({ by: "internalType", then: COUNT });
|
||||
const ALLOCATION_STACK = Object.freeze({ by: "allocationStack", then: COUNT, noStack: COUNT });
|
||||
const OBJECT_CLASS = Object.freeze({ by: "objectClass", then: COUNT, other: COUNT });
|
||||
const COARSE_TYPE = Object.freeze({
|
||||
by: "coarseType",
|
||||
objects: OBJECT_CLASS,
|
||||
strings: COUNT,
|
||||
scripts: {
|
||||
by: "filename",
|
||||
then: INTERNAL_TYPE,
|
||||
noFilename: INTERNAL_TYPE
|
||||
},
|
||||
other: INTERNAL_TYPE,
|
||||
});
|
||||
|
||||
exports.censusDisplays = Object.freeze({
|
||||
coarseType: Object.freeze({
|
||||
displayName: "Type",
|
||||
get tooltip() {
|
||||
// Importing down here is necessary because of the circular dependency
|
||||
// this introduces with `./utils.js`.
|
||||
const { L10N } = require("./utils");
|
||||
return L10N.getStr("censusDisplays.coarseType.tooltip");
|
||||
},
|
||||
inverted: true,
|
||||
breakdown: COARSE_TYPE
|
||||
}),
|
||||
|
||||
allocationStack: Object.freeze({
|
||||
displayName: "Call Stack",
|
||||
get tooltip() {
|
||||
const { L10N } = require("./utils");
|
||||
return L10N.getStr("censusDisplays.allocationStack.tooltip");
|
||||
},
|
||||
inverted: false,
|
||||
breakdown: ALLOCATION_STACK,
|
||||
}),
|
||||
|
||||
invertedAllocationStack: Object.freeze({
|
||||
displayName: "Inverted Call Stack",
|
||||
get tooltip() {
|
||||
const { L10N } = require("./utils");
|
||||
return L10N.getStr("censusDisplays.invertedAllocationStack.tooltip");
|
||||
},
|
||||
inverted: true,
|
||||
breakdown: ALLOCATION_STACK,
|
||||
}),
|
||||
});
|
||||
|
||||
const DOMINATOR_TREE_LABEL_COARSE_TYPE = Object.freeze({
|
||||
by: "coarseType",
|
||||
objects: OBJECT_CLASS,
|
||||
scripts: Object.freeze({
|
||||
by: "internalType",
|
||||
then: Object.freeze({
|
||||
by: "filename",
|
||||
then: COUNT,
|
||||
noFilename: COUNT,
|
||||
}),
|
||||
}),
|
||||
strings: INTERNAL_TYPE,
|
||||
other: INTERNAL_TYPE,
|
||||
});
|
||||
|
||||
exports.labelDisplays = Object.freeze({
|
||||
coarseType: Object.freeze({
|
||||
displayName: "Type",
|
||||
get tooltip() {
|
||||
const { L10N } = require("./utils");
|
||||
return L10N.getStr("dominatorTreeDisplays.coarseType.tooltip");
|
||||
},
|
||||
breakdown: DOMINATOR_TREE_LABEL_COARSE_TYPE
|
||||
}),
|
||||
|
||||
allocationStack: Object.freeze({
|
||||
displayName: "Call Stack",
|
||||
get tooltip() {
|
||||
const { L10N } = require("./utils");
|
||||
return L10N.getStr("dominatorTreeDisplays.allocationStack.tooltip");
|
||||
},
|
||||
breakdown: Object.freeze({
|
||||
by: "allocationStack",
|
||||
then: DOMINATOR_TREE_LABEL_COARSE_TYPE,
|
||||
noStack: DOMINATOR_TREE_LABEL_COARSE_TYPE,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
exports.treeMapDisplays = Object.freeze({
|
||||
coarseType: Object.freeze({
|
||||
displayName: "Type",
|
||||
get tooltip() {
|
||||
const { L10N } = require("./utils");
|
||||
return L10N.getStr("treeMapDisplays.coarseType.tooltip");
|
||||
},
|
||||
breakdown: COARSE_TYPE,
|
||||
inverted: false,
|
||||
})
|
||||
});
|
||||
|
||||
/** * View States **************************************************************/
|
||||
|
||||
/**
|
||||
* The various main views that the tool can be in.
|
||||
*/
|
||||
const viewState = exports.viewState = Object.create(null);
|
||||
viewState.CENSUS = "view-state-census";
|
||||
viewState.DIFFING = "view-state-diffing";
|
||||
viewState.DOMINATOR_TREE = "view-state-dominator-tree";
|
||||
viewState.TREE_MAP = "view-state-tree-map";
|
||||
viewState.INDIVIDUALS = "view-state-individuals";
|
||||
|
||||
/** * Snapshot States **********************************************************/
|
||||
|
||||
const snapshotState = exports.snapshotState = Object.create(null);
|
||||
|
||||
/**
|
||||
* Various states a snapshot can be in.
|
||||
* An FSM describing snapshot states:
|
||||
*
|
||||
* SAVING -> SAVED -> READING -> READ
|
||||
* ↗
|
||||
* IMPORTING
|
||||
*
|
||||
* Any of these states may go to the ERROR state, from which they can never
|
||||
* leave (mwah ha ha ha!)
|
||||
*/
|
||||
snapshotState.ERROR = "snapshot-state-error";
|
||||
snapshotState.IMPORTING = "snapshot-state-importing";
|
||||
snapshotState.SAVING = "snapshot-state-saving";
|
||||
snapshotState.SAVED = "snapshot-state-saved";
|
||||
snapshotState.READING = "snapshot-state-reading";
|
||||
snapshotState.READ = "snapshot-state-read";
|
||||
|
||||
/*
|
||||
* Various states the census model can be in.
|
||||
*
|
||||
* SAVING <-> SAVED
|
||||
* |
|
||||
* V
|
||||
* ERROR
|
||||
*/
|
||||
|
||||
const censusState = exports.censusState = Object.create(null);
|
||||
|
||||
censusState.SAVING = "census-state-saving";
|
||||
censusState.SAVED = "census-state-saved";
|
||||
censusState.ERROR = "census-state-error";
|
||||
|
||||
/*
|
||||
* Various states the tree map model can be in.
|
||||
*
|
||||
* SAVING <-> SAVED
|
||||
* |
|
||||
* V
|
||||
* ERROR
|
||||
*/
|
||||
|
||||
const treeMapState = exports.treeMapState = Object.create(null);
|
||||
|
||||
treeMapState.SAVING = "tree-map-state-saving";
|
||||
treeMapState.SAVED = "tree-map-state-saved";
|
||||
treeMapState.ERROR = "tree-map-state-error";
|
||||
|
||||
/** * Diffing States ***********************************************************/
|
||||
|
||||
/*
|
||||
* Various states the diffing model can be in.
|
||||
*
|
||||
* SELECTING --> TAKING_DIFF <---> TOOK_DIFF
|
||||
* |
|
||||
* V
|
||||
* ERROR
|
||||
*/
|
||||
const diffingState = exports.diffingState = Object.create(null);
|
||||
|
||||
// Selecting the two snapshots to diff.
|
||||
diffingState.SELECTING = "diffing-state-selecting";
|
||||
|
||||
// Currently computing the diff between the two selected snapshots.
|
||||
diffingState.TAKING_DIFF = "diffing-state-taking-diff";
|
||||
|
||||
// Have the diff between the two selected snapshots.
|
||||
diffingState.TOOK_DIFF = "diffing-state-took-diff";
|
||||
|
||||
// An error occurred while computing the diff.
|
||||
diffingState.ERROR = "diffing-state-error";
|
||||
|
||||
/** * Dominator Tree States ****************************************************/
|
||||
|
||||
/*
|
||||
* Various states the dominator tree model can be in.
|
||||
*
|
||||
* COMPUTING -> COMPUTED -> FETCHING -> LOADED <--> INCREMENTAL_FETCHING
|
||||
*
|
||||
* Any state may lead to the ERROR state, from which it can never leave.
|
||||
*/
|
||||
const dominatorTreeState = exports.dominatorTreeState = Object.create(null);
|
||||
dominatorTreeState.COMPUTING = "dominator-tree-state-computing";
|
||||
dominatorTreeState.COMPUTED = "dominator-tree-state-computed";
|
||||
dominatorTreeState.FETCHING = "dominator-tree-state-fetching";
|
||||
dominatorTreeState.LOADED = "dominator-tree-state-loaded";
|
||||
dominatorTreeState.INCREMENTAL_FETCHING = "dominator-tree-state-incremental-fetching";
|
||||
dominatorTreeState.ERROR = "dominator-tree-state-error";
|
||||
|
||||
/** * States for Individuals Model *********************************************/
|
||||
|
||||
/*
|
||||
* Various states the individuals model can be in.
|
||||
*
|
||||
* COMPUTING_DOMINATOR_TREE -> FETCHING -> FETCHED
|
||||
*
|
||||
* Any state may lead to the ERROR state, from which it can never leave.
|
||||
*/
|
||||
const individualsState = exports.individualsState = Object.create(null);
|
||||
individualsState.COMPUTING_DOMINATOR_TREE = "individuals-state-computing-dominator-tree";
|
||||
individualsState.FETCHING = "individuals-state-fetching";
|
||||
individualsState.FETCHED = "individuals-state-fetched";
|
||||
individualsState.ERROR = "individuals-state-error";
|
||||
58
devtools/client/memory/dominator-tree-lazy-children.js
Normal file
58
devtools/client/memory/dominator-tree-lazy-children.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* The `DominatorTreeLazyChildren` is a placeholder that represents a future
|
||||
* subtree in an existing `DominatorTreeNode` tree that is currently being
|
||||
* incrementally fetched from the `HeapAnalysesWorker`.
|
||||
*
|
||||
* @param {NodeId} parentNodeId
|
||||
* @param {Number} siblingIndex
|
||||
*/
|
||||
function DominatorTreeLazyChildren(parentNodeId, siblingIndex) {
|
||||
this._parentNodeId = parentNodeId;
|
||||
this._siblingIndex = siblingIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique key for this `DominatorTreeLazyChildren` instance. This can
|
||||
* be used as the key in a hash table or as the `key` property for a React
|
||||
* component, for example.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
DominatorTreeLazyChildren.prototype.key = function () {
|
||||
return `dominator-tree-lazy-children-${this._parentNodeId}-${this._siblingIndex}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if this is a placeholder for the first child of its
|
||||
* parent. Return false if it is a placeholder for loading more of its parent's
|
||||
* children.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
DominatorTreeLazyChildren.prototype.isFirstChild = function () {
|
||||
return this._siblingIndex === 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get this subtree's parent node's identifier.
|
||||
*
|
||||
* @returns {NodeId}
|
||||
*/
|
||||
DominatorTreeLazyChildren.prototype.parentNodeId = function () {
|
||||
return this._parentNodeId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get this subtree's index in its parent's children array.
|
||||
*
|
||||
* @returns {Number}
|
||||
*/
|
||||
DominatorTreeLazyChildren.prototype.siblingIndex = function () {
|
||||
return this._siblingIndex;
|
||||
};
|
||||
|
||||
module.exports = DominatorTreeLazyChildren;
|
||||
67
devtools/client/memory/initializer.js
Normal file
67
devtools/client/memory/initializer.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
const BrowserLoaderModule = {};
|
||||
Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
|
||||
const { require } = BrowserLoaderModule.BrowserLoader({
|
||||
baseURI: "resource://devtools/client/memory/",
|
||||
window
|
||||
});
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const { createFactory, createElement } = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const { Provider } = require("devtools/client/shared/vendor/react-redux");
|
||||
const App = createFactory(require("devtools/client/memory/app"));
|
||||
const Store = require("devtools/client/memory/store");
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
|
||||
/**
|
||||
* The current target, toolbox, MemoryFront, and HeapAnalysesClient, set by this tool's host.
|
||||
*/
|
||||
var gToolbox, gTarget, gFront, gHeapAnalysesClient;
|
||||
|
||||
/**
|
||||
* Variables set by `initialize()`
|
||||
*/
|
||||
var gStore, gRoot, gApp, gProvider, unsubscribe, isHighlighted, telemetry;
|
||||
|
||||
var initialize = Task.async(function* () {
|
||||
gRoot = document.querySelector("#app");
|
||||
gStore = Store();
|
||||
gApp = createElement(App, { toolbox: gToolbox, front: gFront, heapWorker: gHeapAnalysesClient });
|
||||
gProvider = createElement(Provider, { store: gStore }, gApp);
|
||||
ReactDOM.render(gProvider, gRoot);
|
||||
unsubscribe = gStore.subscribe(onStateChange);
|
||||
});
|
||||
|
||||
var destroy = Task.async(function* () {
|
||||
const ok = ReactDOM.unmountComponentAtNode(gRoot);
|
||||
assert(ok, "Should successfully unmount the memory tool's top level React component");
|
||||
|
||||
unsubscribe();
|
||||
|
||||
gStore, gRoot, gApp, gProvider, unsubscribe, isHighlighted;
|
||||
});
|
||||
|
||||
/**
|
||||
* Fired on any state change, currently only handles toggling
|
||||
* the highlighting of the tool when recording allocations.
|
||||
*/
|
||||
function onStateChange() {
|
||||
let isRecording = gStore.getState().allocations.recording;
|
||||
if (isRecording === isHighlighted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRecording) {
|
||||
gToolbox.highlightTool("memory");
|
||||
} else {
|
||||
gToolbox.unhighlightTool("memory");
|
||||
}
|
||||
|
||||
isHighlighted = isRecording;
|
||||
}
|
||||
42
devtools/client/memory/memory.xhtml
Normal file
42
devtools/client/memory/memory.xhtml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % htmlDTD
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"DTD/xhtml1-strict.dtd">
|
||||
%htmlDTD;
|
||||
]>
|
||||
|
||||
<!-- 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/. -->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" dir="">
|
||||
<head>
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/memory.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/components-frame.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
|
||||
</head>
|
||||
<body class="theme-body">
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="application/javascript;version=1.8"
|
||||
src="chrome://devtools/content/shared/theme-switching.js"
|
||||
defer="true">
|
||||
</script>
|
||||
|
||||
<script type="application/javascript;version=1.8"
|
||||
src="initializer.js"
|
||||
defer="true">
|
||||
</script>
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://devtools/content/shared/vendor/d3.js"
|
||||
defer="true">
|
||||
</script>
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://devtools/content/shared/vendor/dagre-d3.js"
|
||||
defer="true">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
519
devtools/client/memory/models.js
Normal file
519
devtools/client/memory/models.js
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
/* 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/shared/fronts/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);
|
||||
},
|
||||
};
|
||||
29
devtools/client/memory/moz.build
Normal file
29
devtools/client/memory/moz.build
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# 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/.
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'Developer Tools: Memory')
|
||||
|
||||
DIRS += [
|
||||
'actions',
|
||||
'components',
|
||||
'reducers',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'app.js',
|
||||
'constants.js',
|
||||
'dominator-tree-lazy-children.js',
|
||||
'models.js',
|
||||
'panel.js',
|
||||
'reducers.js',
|
||||
'store.js',
|
||||
'telemetry.js',
|
||||
'utils.js',
|
||||
)
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
|
||||
MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
|
||||
75
devtools/client/memory/panel.js
Normal file
75
devtools/client/memory/panel.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Cc, Ci, Cu, Cr } = require("chrome");
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const { MemoryFront } = require("devtools/shared/fronts/memory");
|
||||
const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
|
||||
const promise = require("promise");
|
||||
|
||||
function MemoryPanel(iframeWindow, toolbox) {
|
||||
this.panelWin = iframeWindow;
|
||||
this._toolbox = toolbox;
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
MemoryPanel.prototype = {
|
||||
open: Task.async(function* () {
|
||||
if (this._opening) {
|
||||
return this._opening;
|
||||
}
|
||||
|
||||
this.panelWin.gToolbox = this._toolbox;
|
||||
this.panelWin.gTarget = this.target;
|
||||
|
||||
const rootForm = yield this.target.root;
|
||||
this.panelWin.gFront = new MemoryFront(this.target.client,
|
||||
this.target.form,
|
||||
rootForm);
|
||||
this.panelWin.gHeapAnalysesClient = new HeapAnalysesClient();
|
||||
|
||||
yield this.panelWin.gFront.attach();
|
||||
|
||||
this._opening = this.panelWin.initialize().then(() => {
|
||||
this.isReady = true;
|
||||
this.emit("ready");
|
||||
return this;
|
||||
});
|
||||
|
||||
return this._opening;
|
||||
}),
|
||||
|
||||
// DevToolPanel API
|
||||
|
||||
get target() {
|
||||
return this._toolbox.target;
|
||||
},
|
||||
|
||||
destroy: Task.async(function* () {
|
||||
// Make sure this panel is not already destroyed.
|
||||
if (this._destroyer) {
|
||||
return this._destroyer;
|
||||
}
|
||||
|
||||
yield this.panelWin.gFront.detach();
|
||||
|
||||
this._destroyer = this.panelWin.destroy().then(() => {
|
||||
// Destroy front to ensure packet handler is removed from client
|
||||
this.panelWin.gFront.destroy();
|
||||
this.panelWin.gHeapAnalysesClient.destroy();
|
||||
this.panelWin = null;
|
||||
this._opening = null;
|
||||
this.isReady = false;
|
||||
this.emit("destroyed");
|
||||
});
|
||||
|
||||
return this._destroyer;
|
||||
})
|
||||
};
|
||||
|
||||
exports.MemoryPanel = MemoryPanel;
|
||||
16
devtools/client/memory/reducers.js
Normal file
16
devtools/client/memory/reducers.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
exports.allocations = require("./reducers/allocations");
|
||||
exports.censusDisplay = require("./reducers/census-display");
|
||||
exports.diffing = require("./reducers/diffing");
|
||||
exports.individuals = require("./reducers/individuals");
|
||||
exports.labelDisplay = require("./reducers/label-display");
|
||||
exports.treeMapDisplay = require("./reducers/tree-map-display");
|
||||
exports.errors = require("./reducers/errors");
|
||||
exports.filter = require("./reducers/filter");
|
||||
exports.sizes = require("./reducers/sizes");
|
||||
exports.snapshots = require("./reducers/snapshots");
|
||||
exports.view = require("./reducers/view");
|
||||
42
devtools/client/memory/reducers/allocations.js
Normal file
42
devtools/client/memory/reducers/allocations.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* 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 { actions } = require("../constants");
|
||||
|
||||
let handlers = Object.create(null);
|
||||
|
||||
handlers[actions.TOGGLE_RECORD_ALLOCATION_STACKS_START] = function (state, action) {
|
||||
assert(!state.togglingInProgress,
|
||||
"Changing recording state must not be reentrant.");
|
||||
|
||||
return {
|
||||
recording: !state.recording,
|
||||
togglingInProgress: true,
|
||||
};
|
||||
};
|
||||
|
||||
handlers[actions.TOGGLE_RECORD_ALLOCATION_STACKS_END] = function (state, action) {
|
||||
assert(state.togglingInProgress,
|
||||
"Should not complete changing recording state if we weren't changing "
|
||||
+ "recording state already.");
|
||||
|
||||
return {
|
||||
recording: state.recording,
|
||||
togglingInProgress: false,
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_ALLOCATIONS_STATE = {
|
||||
recording: false,
|
||||
togglingInProgress: false
|
||||
};
|
||||
|
||||
module.exports = function (state = DEFAULT_ALLOCATIONS_STATE, action) {
|
||||
let handle = handlers[action.type];
|
||||
if (handle) {
|
||||
return handle(state, action);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
21
devtools/client/memory/reducers/census-display.js
Normal file
21
devtools/client/memory/reducers/census-display.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { actions, censusDisplays } = require("../constants");
|
||||
const DEFAULT_CENSUS_DISPLAY = censusDisplays.coarseType;
|
||||
|
||||
let handlers = Object.create(null);
|
||||
|
||||
handlers[actions.SET_CENSUS_DISPLAY] = function (_, { display }) {
|
||||
return display;
|
||||
};
|
||||
|
||||
module.exports = function (state = DEFAULT_CENSUS_DISPLAY, action) {
|
||||
let handle = handlers[action.type];
|
||||
if (handle) {
|
||||
return handle(state, action);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
146
devtools/client/memory/reducers/diffing.js
Normal file
146
devtools/client/memory/reducers/diffing.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/* 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 Immutable = require("devtools/client/shared/vendor/immutable");
|
||||
const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { actions, diffingState, viewState } = require("../constants");
|
||||
const { snapshotIsDiffable } = require("../utils");
|
||||
|
||||
const handlers = Object.create(null);
|
||||
|
||||
handlers[actions.POP_VIEW] = function (diffing, { previousView }) {
|
||||
if (previousView.state === viewState.DIFFING) {
|
||||
assert(previousView.diffing, "Should have previousView.diffing");
|
||||
return previousView.diffing;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
handlers[actions.CHANGE_VIEW] = function (diffing, { newViewState }) {
|
||||
if (newViewState === viewState.DIFFING) {
|
||||
assert(!diffing, "Should not switch to diffing view when already diffing");
|
||||
return Object.freeze({
|
||||
firstSnapshotId: null,
|
||||
secondSnapshotId: null,
|
||||
census: null,
|
||||
state: diffingState.SELECTING,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
handlers[actions.SELECT_SNAPSHOT_FOR_DIFFING] = function (diffing, { snapshot }) {
|
||||
assert(diffing,
|
||||
"Should never select a snapshot for diffing when we aren't diffing " +
|
||||
"anything");
|
||||
assert(diffing.state === diffingState.SELECTING,
|
||||
"Can't select when not in SELECTING state");
|
||||
assert(snapshotIsDiffable(snapshot),
|
||||
"snapshot must be in a diffable state");
|
||||
|
||||
if (!diffing.firstSnapshotId) {
|
||||
return immutableUpdate(diffing, {
|
||||
firstSnapshotId: snapshot.id
|
||||
});
|
||||
}
|
||||
|
||||
assert(!diffing.secondSnapshotId,
|
||||
"If we aren't selecting the first, then we must be selecting the " +
|
||||
"second");
|
||||
|
||||
if (snapshot.id === diffing.firstSnapshotId) {
|
||||
// Ignore requests to select the same snapshot.
|
||||
return diffing;
|
||||
}
|
||||
|
||||
return immutableUpdate(diffing, {
|
||||
secondSnapshotId: snapshot.id
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_CENSUS_DIFF_START] = function (diffing, action) {
|
||||
assert(diffing, "Should be diffing when starting a census diff");
|
||||
assert(action.first.id === diffing.firstSnapshotId,
|
||||
"First snapshot's id should match");
|
||||
assert(action.second.id === diffing.secondSnapshotId,
|
||||
"Second snapshot's id should match");
|
||||
|
||||
return immutableUpdate(diffing, {
|
||||
state: diffingState.TAKING_DIFF,
|
||||
census: {
|
||||
report: null,
|
||||
inverted: action.inverted,
|
||||
filter: action.filter,
|
||||
display: action.display,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_CENSUS_DIFF_END] = function (diffing, action) {
|
||||
assert(diffing, "Should be diffing when ending a census diff");
|
||||
assert(action.first.id === diffing.firstSnapshotId,
|
||||
"First snapshot's id should match");
|
||||
assert(action.second.id === diffing.secondSnapshotId,
|
||||
"Second snapshot's id should match");
|
||||
|
||||
return immutableUpdate(diffing, {
|
||||
state: diffingState.TOOK_DIFF,
|
||||
census: {
|
||||
report: action.report,
|
||||
parentMap: action.parentMap,
|
||||
expanded: Immutable.Set(),
|
||||
inverted: action.inverted,
|
||||
filter: action.filter,
|
||||
display: action.display,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.DIFFING_ERROR] = function (diffing, action) {
|
||||
return {
|
||||
state: diffingState.ERROR,
|
||||
error: action.error
|
||||
};
|
||||
};
|
||||
|
||||
handlers[actions.EXPAND_DIFFING_CENSUS_NODE] = function (diffing, { node }) {
|
||||
assert(diffing, "Should be diffing if expanding diffing's census nodes");
|
||||
assert(diffing.state === diffingState.TOOK_DIFF,
|
||||
"Should have taken the census diff if expanding nodes");
|
||||
assert(diffing.census, "Should have a census");
|
||||
assert(diffing.census.report, "Should have a census report");
|
||||
assert(diffing.census.expanded, "Should have a census's expanded set");
|
||||
|
||||
const expanded = diffing.census.expanded.add(node.id);
|
||||
const census = immutableUpdate(diffing.census, { expanded });
|
||||
return immutableUpdate(diffing, { census });
|
||||
};
|
||||
|
||||
handlers[actions.COLLAPSE_DIFFING_CENSUS_NODE] = function (diffing, { node }) {
|
||||
assert(diffing, "Should be diffing if expanding diffing's census nodes");
|
||||
assert(diffing.state === diffingState.TOOK_DIFF,
|
||||
"Should have taken the census diff if expanding nodes");
|
||||
assert(diffing.census, "Should have a census");
|
||||
assert(diffing.census.report, "Should have a census report");
|
||||
assert(diffing.census.expanded, "Should have a census's expanded set");
|
||||
|
||||
const expanded = diffing.census.expanded.delete(node.id);
|
||||
const census = immutableUpdate(diffing.census, { expanded });
|
||||
return immutableUpdate(diffing, { census });
|
||||
};
|
||||
|
||||
handlers[actions.FOCUS_DIFFING_CENSUS_NODE] = function (diffing, { node }) {
|
||||
assert(diffing, "Should be diffing.");
|
||||
assert(diffing.census, "Should have a census");
|
||||
const census = immutableUpdate(diffing.census, { focused: node });
|
||||
return immutableUpdate(diffing, { census });
|
||||
};
|
||||
|
||||
module.exports = function (diffing = null, action) {
|
||||
const handler = handlers[action.type];
|
||||
return handler ? handler(diffing, action) : diffing;
|
||||
};
|
||||
17
devtools/client/memory/reducers/errors.js
Normal file
17
devtools/client/memory/reducers/errors.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { ERROR_TYPE: TASK_ERROR_TYPE } = require("devtools/client/shared/redux/middleware/task");
|
||||
|
||||
/**
|
||||
* Handle errors dispatched from task middleware and
|
||||
* store them so we can check in tests or dump them out.
|
||||
*/
|
||||
module.exports = function (state = [], action) {
|
||||
switch (action.type) {
|
||||
case TASK_ERROR_TYPE:
|
||||
return [...state, action.error];
|
||||
}
|
||||
return state;
|
||||
};
|
||||
14
devtools/client/memory/reducers/filter.js
Normal file
14
devtools/client/memory/reducers/filter.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { actions } = require("../constants");
|
||||
|
||||
module.exports = function (filterString = null, action) {
|
||||
if (action.type === actions.SET_FILTER_STRING) {
|
||||
return action.filter || null;
|
||||
} else {
|
||||
return filterString;
|
||||
}
|
||||
};
|
||||
73
devtools/client/memory/reducers/individuals.js
Normal file
73
devtools/client/memory/reducers/individuals.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { assert, immutableUpdate } = require("devtools/shared/DevToolsUtils");
|
||||
const { actions, individualsState, viewState } = require("../constants");
|
||||
|
||||
const handlers = Object.create(null);
|
||||
|
||||
handlers[actions.POP_VIEW] = function (_state, _action) {
|
||||
return null;
|
||||
};
|
||||
|
||||
handlers[actions.CHANGE_VIEW] = function (individuals, { newViewState }) {
|
||||
if (newViewState === viewState.INDIVIDUALS) {
|
||||
assert(!individuals,
|
||||
"Should not switch to individuals view when already in individuals view");
|
||||
return Object.freeze({
|
||||
state: individualsState.COMPUTING_DOMINATOR_TREE,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
handlers[actions.FOCUS_INDIVIDUAL] = function (individuals, { node }) {
|
||||
assert(individuals, "Should have individuals");
|
||||
return immutableUpdate(individuals, { focused: node });
|
||||
};
|
||||
|
||||
handlers[actions.FETCH_INDIVIDUALS_START] = function (individuals, action) {
|
||||
assert(individuals, "Should have individuals");
|
||||
return Object.freeze({
|
||||
state: individualsState.FETCHING,
|
||||
focused: individuals.focused,
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.FETCH_INDIVIDUALS_END] = function (individuals, action) {
|
||||
assert(individuals, "Should have individuals");
|
||||
assert(!individuals.nodes, "Should not have nodes");
|
||||
assert(individuals.state === individualsState.FETCHING,
|
||||
"Should only end fetching individuals after starting.");
|
||||
|
||||
const focused = individuals.focused
|
||||
? action.nodes.find(n => n.nodeId === individuals.focused.nodeId)
|
||||
: null;
|
||||
|
||||
return Object.freeze({
|
||||
state: individualsState.FETCHED,
|
||||
nodes: action.nodes,
|
||||
id: action.id,
|
||||
censusBreakdown: action.censusBreakdown,
|
||||
indices: action.indices,
|
||||
labelDisplay: action.labelDisplay,
|
||||
focused,
|
||||
dominatorTree: action.dominatorTree,
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.INDIVIDUALS_ERROR] = function (_, { error }) {
|
||||
return Object.freeze({
|
||||
error,
|
||||
nodes: null,
|
||||
state: individualsState.ERROR,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = function (individuals = null, action) {
|
||||
const handler = handlers[action.type];
|
||||
return handler ? handler(individuals, action) : individuals;
|
||||
};
|
||||
19
devtools/client/memory/reducers/label-display.js
Normal file
19
devtools/client/memory/reducers/label-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, labelDisplays } = require("../constants");
|
||||
const DEFAULT_LABEL_DISPLAY = labelDisplays.coarseType;
|
||||
|
||||
const handlers = Object.create(null);
|
||||
|
||||
handlers[actions.SET_LABEL_DISPLAY] = function (_, { display }) {
|
||||
return display;
|
||||
};
|
||||
|
||||
module.exports = function (state = DEFAULT_LABEL_DISPLAY, action) {
|
||||
const handler = handlers[action.type];
|
||||
return handler ? handler(state, action) : state;
|
||||
};
|
||||
18
devtools/client/memory/reducers/moz.build
Normal file
18
devtools/client/memory/reducers/moz.build
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# 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(
|
||||
'allocations.js',
|
||||
'census-display.js',
|
||||
'diffing.js',
|
||||
'errors.js',
|
||||
'filter.js',
|
||||
'individuals.js',
|
||||
'label-display.js',
|
||||
'sizes.js',
|
||||
'snapshots.js',
|
||||
'tree-map-display.js',
|
||||
'view.js',
|
||||
)
|
||||
18
devtools/client/memory/reducers/sizes.js
Normal file
18
devtools/client/memory/reducers/sizes.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { actions } = require("../constants");
|
||||
const { immutableUpdate } = require("devtools/shared/DevToolsUtils");
|
||||
|
||||
const handlers = Object.create(null);
|
||||
|
||||
handlers[actions.RESIZE_SHORTEST_PATHS] = function (sizes, { size }) {
|
||||
return immutableUpdate(sizes, { shortestPathsSize: size });
|
||||
};
|
||||
|
||||
module.exports = function (sizes = { shortestPathsSize: .5 }, action) {
|
||||
const handler = handlers[action.type];
|
||||
return handler ? handler(sizes, action) : sizes;
|
||||
};
|
||||
459
devtools/client/memory/reducers/snapshots.js
Normal file
459
devtools/client/memory/reducers/snapshots.js
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
/* 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 Immutable = require("devtools/client/shared/vendor/immutable");
|
||||
const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
|
||||
const {
|
||||
actions,
|
||||
snapshotState: states,
|
||||
censusState,
|
||||
treeMapState,
|
||||
dominatorTreeState,
|
||||
viewState,
|
||||
} = require("../constants");
|
||||
const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode");
|
||||
|
||||
const handlers = Object.create(null);
|
||||
|
||||
handlers[actions.SNAPSHOT_ERROR] = function (snapshots, { id, error }) {
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { state: states.ERROR, error })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_SNAPSHOT_START] = function (snapshots, { snapshot }) {
|
||||
return [...snapshots, snapshot];
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_SNAPSHOT_END] = function (snapshots, { id, path }) {
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { state: states.SAVED, path })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.IMPORT_SNAPSHOT_START] = handlers[actions.TAKE_SNAPSHOT_START];
|
||||
|
||||
handlers[actions.READ_SNAPSHOT_START] = function (snapshots, { id }) {
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { state: states.READING })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.READ_SNAPSHOT_END] = function (snapshots, { id, creationTime }) {
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { state: states.READ, creationTime })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_CENSUS_START] = function (snapshots, { id, display, filter }) {
|
||||
const census = {
|
||||
report: null,
|
||||
display,
|
||||
filter,
|
||||
state: censusState.SAVING
|
||||
};
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { census })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_CENSUS_END] = function (snapshots, { id,
|
||||
report,
|
||||
parentMap,
|
||||
display,
|
||||
filter }) {
|
||||
const census = {
|
||||
report,
|
||||
parentMap,
|
||||
expanded: Immutable.Set(),
|
||||
display,
|
||||
filter,
|
||||
state: censusState.SAVED
|
||||
};
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { census })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_CENSUS_ERROR] = function (snapshots, { id, error }) {
|
||||
assert(error, "actions with TAKE_CENSUS_ERROR should have an error");
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
const census = Object.freeze({
|
||||
state: censusState.ERROR,
|
||||
error,
|
||||
});
|
||||
|
||||
return immutableUpdate(snapshot, { census });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_TREE_MAP_START] = function (snapshots, { id, display }) {
|
||||
const treeMap = {
|
||||
report: null,
|
||||
display,
|
||||
state: treeMapState.SAVING
|
||||
};
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { treeMap })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_TREE_MAP_END] = function (snapshots, action) {
|
||||
const { id, report, display } = action;
|
||||
const treeMap = {
|
||||
report,
|
||||
display,
|
||||
state: treeMapState.SAVED
|
||||
};
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
return snapshot.id === id
|
||||
? immutableUpdate(snapshot, { treeMap })
|
||||
: snapshot;
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.TAKE_TREE_MAP_ERROR] = function (snapshots, { id, error }) {
|
||||
assert(error, "actions with TAKE_TREE_MAP_ERROR should have an error");
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
const treeMap = Object.freeze({
|
||||
state: treeMapState.ERROR,
|
||||
error,
|
||||
});
|
||||
|
||||
return immutableUpdate(snapshot, { treeMap });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.EXPAND_CENSUS_NODE] = function (snapshots, { id, node }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.census, "Should have a census");
|
||||
assert(snapshot.census.report, "Should have a census report");
|
||||
assert(snapshot.census.expanded, "Should have a census's expanded set");
|
||||
|
||||
const expanded = snapshot.census.expanded.add(node.id);
|
||||
const census = immutableUpdate(snapshot.census, { expanded });
|
||||
return immutableUpdate(snapshot, { census });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.COLLAPSE_CENSUS_NODE] = function (snapshots, { id, node }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.census, "Should have a census");
|
||||
assert(snapshot.census.report, "Should have a census report");
|
||||
assert(snapshot.census.expanded, "Should have a census's expanded set");
|
||||
|
||||
const expanded = snapshot.census.expanded.delete(node.id);
|
||||
const census = immutableUpdate(snapshot.census, { expanded });
|
||||
return immutableUpdate(snapshot, { census });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.FOCUS_CENSUS_NODE] = function (snapshots, { id, node }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.census, "Should have a census");
|
||||
const census = immutableUpdate(snapshot.census, { focused: node });
|
||||
return immutableUpdate(snapshot, { census });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.SELECT_SNAPSHOT] = function (snapshots, { id }) {
|
||||
return snapshots.map(s => immutableUpdate(s, { selected: s.id === id }));
|
||||
};
|
||||
|
||||
handlers[actions.DELETE_SNAPSHOTS_START] = function (snapshots, { ids }) {
|
||||
return snapshots.filter(s => ids.indexOf(s.id) === -1);
|
||||
};
|
||||
|
||||
handlers[actions.DELETE_SNAPSHOTS_END] = function (snapshots) {
|
||||
return snapshots;
|
||||
};
|
||||
|
||||
handlers[actions.CHANGE_VIEW] = function (snapshots, { newViewState }) {
|
||||
return newViewState === viewState.DIFFING
|
||||
? snapshots.map(s => immutableUpdate(s, { selected: false }))
|
||||
: snapshots;
|
||||
};
|
||||
|
||||
handlers[actions.POP_VIEW] = function (snapshots, { previousView }) {
|
||||
return snapshots.map(s => immutableUpdate(s, {
|
||||
selected: s.id === previousView.selected
|
||||
}));
|
||||
};
|
||||
|
||||
handlers[actions.COMPUTE_DOMINATOR_TREE_START] = function (snapshots, { id }) {
|
||||
const dominatorTree = Object.freeze({
|
||||
state: dominatorTreeState.COMPUTING,
|
||||
dominatorTreeId: undefined,
|
||||
root: undefined,
|
||||
});
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(!snapshot.dominatorTree,
|
||||
"Should not have a dominator tree model");
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.COMPUTE_DOMINATOR_TREE_END] = function (snapshots, { id, dominatorTreeId }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.dominatorTree, "Should have a dominator tree model");
|
||||
assert(snapshot.dominatorTree.state == dominatorTreeState.COMPUTING,
|
||||
"Should be in the COMPUTING state");
|
||||
|
||||
const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
|
||||
state: dominatorTreeState.COMPUTED,
|
||||
dominatorTreeId,
|
||||
});
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.FETCH_DOMINATOR_TREE_START] = function (snapshots, { id, display }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.dominatorTree, "Should have a dominator tree model");
|
||||
assert(snapshot.dominatorTree.state !== dominatorTreeState.COMPUTING &&
|
||||
snapshot.dominatorTree.state !== dominatorTreeState.ERROR,
|
||||
`Should have already computed the dominator tree, found state = ${snapshot.dominatorTree.state}`);
|
||||
|
||||
const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
|
||||
state: dominatorTreeState.FETCHING,
|
||||
root: undefined,
|
||||
display,
|
||||
});
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.FETCH_DOMINATOR_TREE_END] = function (snapshots, { id, root }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.dominatorTree, "Should have a dominator tree model");
|
||||
assert(snapshot.dominatorTree.state == dominatorTreeState.FETCHING,
|
||||
"Should be in the FETCHING state");
|
||||
|
||||
let focused;
|
||||
if (snapshot.dominatorTree.focused) {
|
||||
focused = (function findFocused(node) {
|
||||
if (node.nodeId === snapshot.dominatorTree.focused.nodeId) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
const length = node.children.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const result = findFocused(node.children[i]);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}(root));
|
||||
}
|
||||
|
||||
const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
|
||||
state: dominatorTreeState.LOADED,
|
||||
root,
|
||||
expanded: Immutable.Set(),
|
||||
focused,
|
||||
});
|
||||
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.EXPAND_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.dominatorTree, "Should have a dominator tree");
|
||||
assert(snapshot.dominatorTree.expanded,
|
||||
"Should have the dominator tree's expanded set");
|
||||
|
||||
const expanded = snapshot.dominatorTree.expanded.add(node.nodeId);
|
||||
const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.COLLAPSE_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.dominatorTree, "Should have a dominator tree");
|
||||
assert(snapshot.dominatorTree.expanded,
|
||||
"Should have the dominator tree's expanded set");
|
||||
|
||||
const expanded = snapshot.dominatorTree.expanded.delete(node.nodeId);
|
||||
const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.FOCUS_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.dominatorTree, "Should have a dominator tree");
|
||||
const dominatorTree = immutableUpdate(snapshot.dominatorTree, { focused: node });
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.FETCH_IMMEDIATELY_DOMINATED_START] = function (snapshots, { id }) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.dominatorTree, "Should have a dominator tree model");
|
||||
assert(snapshot.dominatorTree.state == dominatorTreeState.INCREMENTAL_FETCHING ||
|
||||
snapshot.dominatorTree.state == dominatorTreeState.LOADED,
|
||||
"The dominator tree should be loaded if we are going to " +
|
||||
"incrementally fetch children.");
|
||||
|
||||
const activeFetchRequestCount = snapshot.dominatorTree.activeFetchRequestCount
|
||||
? snapshot.dominatorTree.activeFetchRequestCount + 1
|
||||
: 1;
|
||||
|
||||
const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
|
||||
state: dominatorTreeState.INCREMENTAL_FETCHING,
|
||||
activeFetchRequestCount,
|
||||
});
|
||||
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.FETCH_IMMEDIATELY_DOMINATED_END] =
|
||||
function (snapshots, { id, path, nodes, moreChildrenAvailable}) {
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
assert(snapshot.dominatorTree, "Should have a dominator tree model");
|
||||
assert(snapshot.dominatorTree.root, "Should have a dominator tree model root");
|
||||
assert(snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
|
||||
"The dominator tree state should be INCREMENTAL_FETCHING");
|
||||
|
||||
const root = DominatorTreeNode.insert(snapshot.dominatorTree.root,
|
||||
path,
|
||||
nodes,
|
||||
moreChildrenAvailable);
|
||||
|
||||
const focused = snapshot.dominatorTree.focused
|
||||
? DominatorTreeNode.getNodeByIdAlongPath(snapshot.dominatorTree.focused.nodeId,
|
||||
root,
|
||||
path)
|
||||
: undefined;
|
||||
|
||||
const activeFetchRequestCount = snapshot.dominatorTree.activeFetchRequestCount === 1
|
||||
? undefined
|
||||
: snapshot.dominatorTree.activeFetchRequestCount - 1;
|
||||
|
||||
// If there are still outstanding requests, we need to stay in the
|
||||
// INCREMENTAL_FETCHING state until they complete.
|
||||
const state = activeFetchRequestCount
|
||||
? dominatorTreeState.INCREMENTAL_FETCHING
|
||||
: dominatorTreeState.LOADED;
|
||||
|
||||
const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
|
||||
state,
|
||||
root,
|
||||
focused,
|
||||
activeFetchRequestCount,
|
||||
});
|
||||
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.DOMINATOR_TREE_ERROR] = function (snapshots, { id, error }) {
|
||||
assert(error, "actions with DOMINATOR_TREE_ERROR should have an error");
|
||||
|
||||
return snapshots.map(snapshot => {
|
||||
if (snapshot.id !== id) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
const dominatorTree = Object.freeze({
|
||||
state: dominatorTreeState.ERROR,
|
||||
error,
|
||||
});
|
||||
|
||||
return immutableUpdate(snapshot, { dominatorTree });
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = function (snapshots = [], action) {
|
||||
const handler = handlers[action.type];
|
||||
if (handler) {
|
||||
return handler(snapshots, action);
|
||||
}
|
||||
return snapshots;
|
||||
};
|
||||
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;
|
||||
};
|
||||
49
devtools/client/memory/reducers/view.js
Normal file
49
devtools/client/memory/reducers/view.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/* 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, viewState } = require("../constants");
|
||||
|
||||
const handlers = Object.create(null);
|
||||
|
||||
handlers[actions.POP_VIEW] = function (view, _) {
|
||||
assert(view.previous, "Had better have a previous view state when POP_VIEW");
|
||||
return Object.freeze({
|
||||
state: view.previous.state,
|
||||
previous: null,
|
||||
});
|
||||
};
|
||||
|
||||
handlers[actions.CHANGE_VIEW] = function (view, action) {
|
||||
const { newViewState, oldDiffing, oldSelected } = action;
|
||||
assert(newViewState);
|
||||
|
||||
if (newViewState === viewState.INDIVIDUALS) {
|
||||
assert(oldDiffing || oldSelected);
|
||||
return Object.freeze({
|
||||
state: newViewState,
|
||||
previous: Object.freeze({
|
||||
state: view.state,
|
||||
selected: oldSelected,
|
||||
diffing: oldDiffing,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
state: newViewState,
|
||||
previous: null,
|
||||
});
|
||||
};
|
||||
|
||||
const DEFAULT_VIEW = {
|
||||
state: viewState.TREE_MAP,
|
||||
previous: null,
|
||||
};
|
||||
|
||||
module.exports = function (view = DEFAULT_VIEW, action) {
|
||||
const handler = handlers[action.type];
|
||||
return handler ? handler(view, action) : view;
|
||||
};
|
||||
33
devtools/client/memory/store.js
Normal file
33
devtools/client/memory/store.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { combineReducers } = require("../shared/vendor/redux");
|
||||
const createStore = require("../shared/redux/create-store");
|
||||
const reducers = require("./reducers");
|
||||
const { viewState } = require("./constants");
|
||||
const flags = require("devtools/shared/flags");
|
||||
|
||||
module.exports = function () {
|
||||
let shouldLog = false;
|
||||
let history;
|
||||
|
||||
// If testing, store the action history in an array
|
||||
// we'll later attach to the store
|
||||
if (flags.testing) {
|
||||
history = [];
|
||||
// Uncomment this for TONS of logging in tests.
|
||||
// shouldLog = true;
|
||||
}
|
||||
|
||||
let store = createStore({
|
||||
log: shouldLog,
|
||||
history
|
||||
})(combineReducers(reducers), {});
|
||||
|
||||
if (history) {
|
||||
store.history = history;
|
||||
}
|
||||
|
||||
return store;
|
||||
};
|
||||
91
devtools/client/memory/telemetry.js
Normal file
91
devtools/client/memory/telemetry.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/* 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/. */
|
||||
|
||||
// This module exports methods to record telemetry data for memory tool usage.
|
||||
//
|
||||
// NB: Ensure that *every* exported function is wrapped in `makeInfallible` so
|
||||
// that our probes don't accidentally break code that actually does productive
|
||||
// work for the user!
|
||||
|
||||
const { telemetry } = require("Services");
|
||||
const { makeInfallible, immutableUpdate } = require("devtools/shared/DevToolsUtils");
|
||||
const { labelDisplays, treeMapDisplays, censusDisplays } = require("./constants");
|
||||
|
||||
exports.countTakeSnapshot = makeInfallible(function () {
|
||||
const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_TAKE_SNAPSHOT_COUNT");
|
||||
histogram.add(1);
|
||||
}, "devtools/client/memory/telemetry#countTakeSnapshot");
|
||||
|
||||
exports.countImportSnapshot = makeInfallible(function () {
|
||||
const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_IMPORT_SNAPSHOT_COUNT");
|
||||
histogram.add(1);
|
||||
}, "devtools/client/memory/telemetry#countImportSnapshot");
|
||||
|
||||
exports.countExportSnapshot = makeInfallible(function () {
|
||||
const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_EXPORT_SNAPSHOT_COUNT");
|
||||
histogram.add(1);
|
||||
}, "devtools/client/memory/telemetry#countExportSnapshot");
|
||||
|
||||
const COARSE_TYPE = "Coarse Type";
|
||||
const ALLOCATION_STACK = "Allocation Stack";
|
||||
const INVERTED_ALLOCATION_STACK = "Inverted Allocation Stack";
|
||||
const CUSTOM = "Custom";
|
||||
|
||||
/**
|
||||
* @param {String|null} filter
|
||||
* The filter string used, if any.
|
||||
*
|
||||
* @param {Boolean} diffing
|
||||
* True if the census was a diffing census, false otherwise.
|
||||
*
|
||||
* @param {censusDisplayModel} display
|
||||
* The display used with the census.
|
||||
*/
|
||||
exports.countCensus = makeInfallible(function ({ filter, diffing, display }) {
|
||||
let histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_INVERTED_CENSUS");
|
||||
histogram.add(!!display.inverted);
|
||||
|
||||
histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_FILTER_CENSUS");
|
||||
histogram.add(!!filter);
|
||||
|
||||
histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_DIFF_CENSUS");
|
||||
histogram.add(!!diffing);
|
||||
|
||||
histogram = telemetry.getKeyedHistogramById("DEVTOOLS_MEMORY_BREAKDOWN_CENSUS_COUNT");
|
||||
if (display === censusDisplays.coarseType) {
|
||||
histogram.add(COARSE_TYPE);
|
||||
} else if (display === censusDisplays.allocationStack) {
|
||||
histogram.add(ALLOCATION_STACK);
|
||||
} else if (display === censusDisplays.invertedAllocationStack) {
|
||||
histogram.add(INVERTED_ALLOCATION_STACK);
|
||||
} else {
|
||||
histogram.add(CUSTOM);
|
||||
}
|
||||
}, "devtools/client/memory/telemetry#countCensus");
|
||||
|
||||
/**
|
||||
* @param {Object} opts
|
||||
* The same parameters specified for countCensus.
|
||||
*/
|
||||
exports.countDiff = makeInfallible(function (opts) {
|
||||
exports.countCensus(immutableUpdate(opts, { diffing: true }));
|
||||
}, "devtools/client/memory/telemetry#countDiff");
|
||||
|
||||
/**
|
||||
* @param {Object} display
|
||||
* The display used to label nodes in the dominator tree.
|
||||
*/
|
||||
exports.countDominatorTree = makeInfallible(function ({ display }) {
|
||||
let histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_DOMINATOR_TREE_COUNT");
|
||||
histogram.add(1);
|
||||
|
||||
histogram = telemetry.getKeyedHistogramById("DEVTOOLS_MEMORY_BREAKDOWN_DOMINATOR_TREE_COUNT");
|
||||
if (display === labelDisplays.coarseType) {
|
||||
histogram.add(COARSE_TYPE);
|
||||
} else if (display === labelDisplays.allocationStack) {
|
||||
histogram.add(ALLOCATION_STACK);
|
||||
} else {
|
||||
histogram.add(CUSTOM);
|
||||
}
|
||||
}, "devtools/client/memory/telemetry#countDominatorTree");
|
||||
6
devtools/client/memory/test/browser/.eslintrc.js
Normal file
6
devtools/client/memory/test/browser/.eslintrc.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for mochitests.
|
||||
"extends": "../../../../.eslintrc.mochitests.js"
|
||||
};
|
||||
31
devtools/client/memory/test/browser/browser.ini
Normal file
31
devtools/client/memory/test/browser/browser.ini
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
[DEFAULT]
|
||||
tags = devtools devtools-memory
|
||||
subsuite = devtools
|
||||
support-files =
|
||||
head.js
|
||||
doc_big_tree.html
|
||||
doc_empty.html
|
||||
doc_steady_allocation.html
|
||||
!/devtools/client/framework/test/shared-head.js
|
||||
!/devtools/client/framework/test/shared-redux-head.js
|
||||
|
||||
[browser_memory_allocationStackDisplay_01.js]
|
||||
skip-if = debug # bug 1219554
|
||||
[browser_memory_displays_01.js]
|
||||
[browser_memory_clear_snapshots.js]
|
||||
[browser_memory_diff_01.js]
|
||||
[browser_memory_dominator_trees_01.js]
|
||||
[browser_memory_dominator_trees_02.js]
|
||||
[browser_memory_filter_01.js]
|
||||
[browser_memory_individuals_01.js]
|
||||
[browser_memory_keyboard.js]
|
||||
[browser_memory_keyboard-snapshot-list.js]
|
||||
[browser_memory_no_allocation_stacks.js]
|
||||
[browser_memory_no_auto_expand.js]
|
||||
skip-if = debug # bug 1219554
|
||||
[browser_memory_percents_01.js]
|
||||
[browser_memory_refresh_does_not_leak.js]
|
||||
[browser_memory_simple_01.js]
|
||||
[browser_memory_transferHeapSnapshot_e10s_01.js]
|
||||
[browser_memory_tree_map-01.js]
|
||||
[browser_memory_tree_map-02.js]
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Sanity test that we can show allocation stack displays in the tree.
|
||||
|
||||
"use strict";
|
||||
|
||||
const { toggleRecordingAllocationStacks } = require("devtools/client/memory/actions/allocations");
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const censusDisplayActions = require("devtools/client/memory/actions/census-display");
|
||||
const { viewState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const { getState, dispatch } = panel.panelWin.gStore;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
dispatch(censusDisplayActions.setCensusDisplay(censusDisplays.invertedAllocationStack));
|
||||
is(getState().censusDisplay.breakdown.by, "allocationStack");
|
||||
|
||||
yield dispatch(toggleRecordingAllocationStacks(front));
|
||||
ok(getState().allocations.recording);
|
||||
|
||||
// Let some allocations build up.
|
||||
yield waitForTime(500);
|
||||
|
||||
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
|
||||
const names = [...doc.querySelectorAll(".frame-link-function-display-name")];
|
||||
ok(names.length, "Should have rendered some allocation stack tree items");
|
||||
ok(names.some(e => !!e.textContent.trim()),
|
||||
"And at least some of them should have functionDisplayNames");
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests taking and then clearing snapshots.
|
||||
*/
|
||||
|
||||
const { treeMapState } = require("devtools/client/memory/constants");
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { gStore, document } = panel.panelWin;
|
||||
const { getState, dispatch } = gStore;
|
||||
|
||||
let snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
|
||||
is(getState().snapshots.length, 0, "Starts with no snapshots in store");
|
||||
is(snapshotEls.length, 0, "No snapshots visible");
|
||||
|
||||
info("Take two snapshots");
|
||||
takeSnapshot(panel.panelWin);
|
||||
takeSnapshot(panel.panelWin);
|
||||
yield waitUntilState(gStore, state =>
|
||||
state.snapshots.length === 2 &&
|
||||
state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
|
||||
state.snapshots[0].treeMap.state === treeMapState.SAVED &&
|
||||
state.snapshots[1].treeMap.state === treeMapState.SAVED);
|
||||
|
||||
snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
|
||||
is(snapshotEls.length, 2, "Two snapshots visible");
|
||||
|
||||
info("Click on Clear Snapshots");
|
||||
yield clearSnapshots(panel.panelWin);
|
||||
is(getState().snapshots.length, 0, "No snapshots in store");
|
||||
snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
|
||||
is(snapshotEls.length, 0, "No snapshot visible");
|
||||
});
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test diffing.
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
snapshotState,
|
||||
diffingState,
|
||||
treeMapState
|
||||
} = require("devtools/client/memory/constants");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const store = panel.panelWin.gStore;
|
||||
const { getState, dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
ok(!getState().diffing, "Not diffing by default.");
|
||||
|
||||
// Take two snapshots.
|
||||
const takeSnapshotButton = doc.getElementById("take-snapshot");
|
||||
EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
|
||||
yield waitForTime(1000);
|
||||
EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
|
||||
|
||||
// Enable diffing mode.
|
||||
const diffButton = doc.getElementById("diff-snapshots");
|
||||
EventUtils.synthesizeMouseAtCenter(diffButton, {}, panel.panelWin);
|
||||
yield waitUntilState(store,
|
||||
state =>
|
||||
!!state.diffing &&
|
||||
state.diffing.state === diffingState.SELECTING);
|
||||
ok(true, "Clicking the diffing button put us into the diffing state.");
|
||||
is(getDisplayedSnapshotStatus(doc), "Select the baseline snapshot");
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length === 2 &&
|
||||
state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
|
||||
state.snapshots[0].treeMap.state === treeMapState.SAVED &&
|
||||
state.snapshots[1].treeMap.state === treeMapState.SAVED);
|
||||
|
||||
const listItems = [...doc.querySelectorAll(".snapshot-list-item")];
|
||||
is(listItems.length, 2, "Should have two snapshot list items");
|
||||
|
||||
// Select the first snapshot.
|
||||
EventUtils.synthesizeMouseAtCenter(listItems[0], {}, panel.panelWin);
|
||||
yield waitUntilState(store,
|
||||
state =>
|
||||
state.diffing.state === diffingState.SELECTING &&
|
||||
state.diffing.firstSnapshotId);
|
||||
is(getDisplayedSnapshotStatus(doc),
|
||||
"Select the snapshot to compare to the baseline");
|
||||
|
||||
// Select the second snapshot.
|
||||
EventUtils.synthesizeMouseAtCenter(listItems[1], {}, panel.panelWin);
|
||||
yield waitUntilState(store,
|
||||
state =>
|
||||
state.diffing.state === diffingState.TAKING_DIFF);
|
||||
ok(true, "Selecting two snapshots for diffing triggers computing the diff");
|
||||
|
||||
// .startsWith because the ellipsis is lost in translation.
|
||||
ok(getDisplayedSnapshotStatus(doc).startsWith("Computing difference"));
|
||||
|
||||
yield waitUntilState(store,
|
||||
state => state.diffing.state === diffingState.TOOK_DIFF);
|
||||
ok(true, "And that diff is computed successfully");
|
||||
is(getDisplayedSnapshotStatus(doc), null, "No status text anymore");
|
||||
ok(doc.querySelector(".heap-tree-item"), "And instead we should be showing the tree");
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Tests that the heap tree renders rows based on the display
|
||||
*/
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
const { viewState, censusState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { gStore, document } = panel.panelWin;
|
||||
|
||||
const { dispatch } = panel.panelWin.gStore;
|
||||
|
||||
function $$(selector) {
|
||||
return [...document.querySelectorAll(selector)];
|
||||
}
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
yield takeSnapshot(panel.panelWin);
|
||||
|
||||
yield waitUntilState(gStore, state =>
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVED);
|
||||
|
||||
info("Check coarse type heap view");
|
||||
["Function", "js::Shape", "Object", "strings"].forEach(findNameCell);
|
||||
|
||||
yield setCensusDisplay(panel.panelWin, censusDisplays.allocationStack);
|
||||
info("Check allocation stack heap view");
|
||||
[L10N.getStr("tree-item.nostack")].forEach(findNameCell);
|
||||
|
||||
function findNameCell(name) {
|
||||
const el = $$(".tree .heap-tree-item-name span")
|
||||
.find(e => e.textContent === name);
|
||||
ok(el, `Found heap tree item cell for ${name}.`);
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Sanity test for dominator trees, their focused nodes, and keyboard navigating
|
||||
// through nodes across incrementally fetching subtrees.
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
dominatorTreeState,
|
||||
viewState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
expandDominatorTreeNode,
|
||||
} = 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_big_tree.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
// Taking snapshots and computing dominator trees is slow :-/
|
||||
requestLongerTimeout(4);
|
||||
|
||||
const store = panel.panelWin.gStore;
|
||||
const { getState, dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.DOMINATOR_TREE));
|
||||
|
||||
// Take a snapshot.
|
||||
|
||||
const takeSnapshotButton = doc.getElementById("take-snapshot");
|
||||
EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
|
||||
|
||||
// Wait for the dominator tree to be computed and fetched.
|
||||
|
||||
yield waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]);
|
||||
ok(true, "Computed and fetched the dominator tree.");
|
||||
|
||||
// Expand all the dominator tree nodes that are eagerly fetched, except for
|
||||
// the leaves which will trigger fetching their lazily loaded subtrees.
|
||||
|
||||
const id = getState().snapshots[0].id;
|
||||
const root = getState().snapshots[0].dominatorTree.root;
|
||||
(function expandAllEagerlyFetched(node = root) {
|
||||
if (!node.moreChildrenAvailable || node.children) {
|
||||
dispatch(expandDominatorTreeNode(id, node));
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
for (let child of node.children) {
|
||||
expandAllEagerlyFetched(child);
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
// Find the deepest eagerly loaded node: one which has more children but none
|
||||
// of them are loaded.
|
||||
|
||||
const deepest = (function findDeepest(node = root) {
|
||||
if (node.moreChildrenAvailable && !node.children) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
for (let child of node.children) {
|
||||
const found = findDeepest(child);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}());
|
||||
|
||||
ok(deepest, "Found the deepest node");
|
||||
ok(!getState().snapshots[0].dominatorTree.expanded.has(deepest.nodeId),
|
||||
"The deepest node should not be expanded");
|
||||
|
||||
// Select the deepest node.
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(doc.querySelector(`.node-${deepest.nodeId}`),
|
||||
{},
|
||||
panel.panelWin);
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots[0].dominatorTree.focused.nodeId === deepest.nodeId);
|
||||
ok(doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
|
||||
"The deepest node should be focused now");
|
||||
|
||||
// Expand the deepest node, which triggers an incremental fetch of its lazily
|
||||
// loaded subtree.
|
||||
|
||||
EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots[0].dominatorTree.expanded.has(deepest.nodeId));
|
||||
is(getState().snapshots[0].dominatorTree.state,
|
||||
dominatorTreeState.INCREMENTAL_FETCHING,
|
||||
"Expanding the deepest node should start an incremental fetch of its subtree");
|
||||
ok(doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
|
||||
"The deepest node should still be focused after expansion");
|
||||
|
||||
// Wait for the incremental fetch to complete.
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
|
||||
ok(true, "And the incremental fetch completes.");
|
||||
ok(doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"),
|
||||
"The deepest node should still be focused after we have loaded its children");
|
||||
|
||||
// Find the most up-to-date version of the node whose children we just
|
||||
// incrementally fetched.
|
||||
|
||||
const newDeepest = (function findNewDeepest(node = getState().snapshots[0].dominatorTree.root) {
|
||||
if (node.nodeId === deepest.nodeId) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
for (let child of node.children) {
|
||||
const found = findNewDeepest(child);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}());
|
||||
|
||||
ok(newDeepest, "We found the up-to-date version of deepest");
|
||||
ok(newDeepest.children, "And its children are loaded");
|
||||
ok(newDeepest.children.length, "And there are more than 0 children");
|
||||
|
||||
const firstChild = newDeepest.children[0];
|
||||
ok(firstChild, "deepest should have a first child");
|
||||
ok(doc.querySelector(`.node-${firstChild.nodeId}`),
|
||||
"and the first child should exist in the dom");
|
||||
|
||||
// Select the newly loaded first child by pressing the right arrow once more.
|
||||
|
||||
EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots[0].dominatorTree.focused === firstChild);
|
||||
ok(doc.querySelector(`.node-${firstChild.nodeId}`).classList.contains("focused"),
|
||||
"The first child should now be focused");
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Integration test for mouse interaction in the dominator tree
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
dominatorTreeState,
|
||||
viewState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
function clickOnNodeArrow(node, panel) {
|
||||
EventUtils.synthesizeMouseAtCenter(node.querySelector(".arrow"),
|
||||
{}, panel.panelWin);
|
||||
}
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ panel }) {
|
||||
// Taking snapshots and computing dominator trees is slow :-/
|
||||
requestLongerTimeout(4);
|
||||
|
||||
const store = panel.panelWin.gStore;
|
||||
const { getState, dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.DOMINATOR_TREE));
|
||||
|
||||
// Take a snapshot.
|
||||
const takeSnapshotButton = doc.getElementById("take-snapshot");
|
||||
EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
|
||||
|
||||
// Wait for the dominator tree to be computed and fetched.
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots[0] &&
|
||||
state.snapshots[0].dominatorTree &&
|
||||
state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
|
||||
ok(true, "Computed and fetched the dominator tree.");
|
||||
|
||||
const root = getState().snapshots[0].dominatorTree.root;
|
||||
ok(getState().snapshots[0].dominatorTree.expanded.has(root.nodeId),
|
||||
"Root node is expanded by default");
|
||||
|
||||
// Click on root arrow to collapse the root element
|
||||
const rootNode = doc.querySelector(`.node-${root.nodeId}`);
|
||||
clickOnNodeArrow(rootNode, panel);
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots[0] &&
|
||||
state.snapshots[0].dominatorTree &&
|
||||
!state.snapshots[0].dominatorTree.expanded.has(root.nodeId));
|
||||
ok(true, "Root node collapsed");
|
||||
|
||||
// Click on root arrow to expand it again
|
||||
clickOnNodeArrow(rootNode, panel);
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots[0] &&
|
||||
state.snapshots[0].dominatorTree &&
|
||||
state.snapshots[0].dominatorTree.expanded.has(root.nodeId));
|
||||
ok(true, "Root node is expanded again");
|
||||
});
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Sanity test that we can show allocation stack displays in the tree.
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
dominatorTreeState,
|
||||
snapshotState,
|
||||
viewState,
|
||||
censusState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
const { changeViewAndRefresh, changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const store = panel.panelWin.gStore;
|
||||
const { getState, dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
const takeSnapshotButton = doc.getElementById("take-snapshot");
|
||||
EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length === 1 &&
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVING);
|
||||
|
||||
let filterInput = doc.getElementById("filter");
|
||||
EventUtils.synthesizeMouseAtCenter(filterInput, {}, panel.panelWin);
|
||||
EventUtils.sendString("js::Shape", panel.panelWin);
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length === 1 &&
|
||||
state.snapshots[0].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].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVED);
|
||||
|
||||
let nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
|
||||
ok(nameElem, "Should get a tree item row with a name");
|
||||
is(nameElem.textContent.trim(), "js::Shape", "the tree item should be the one we filtered for");
|
||||
is(filterInput.value, "js::Shape",
|
||||
"and filter input contains the user value");
|
||||
|
||||
// Now switch the dominator view, then switch back to census view
|
||||
// and check that the filter word is still correctly applied
|
||||
dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
|
||||
ok(true, "change view to dominator tree");
|
||||
|
||||
// Wait for the dominator tree to be computed and fetched.
|
||||
yield waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]);
|
||||
ok(true, "computed and fetched the dominator tree.");
|
||||
|
||||
dispatch(changeViewAndRefresh(viewState.CENSUS, heapWorker));
|
||||
ok(true, "change view back to census");
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length === 1 &&
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVED);
|
||||
|
||||
nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
|
||||
filterInput = doc.getElementById("filter");
|
||||
|
||||
ok(nameElem, "Should still get a tree item row with a name");
|
||||
is(nameElem.textContent.trim(), "js::Shape",
|
||||
"the tree item should still be the one we filtered for");
|
||||
is(filterInput.value, "js::Shape",
|
||||
"and filter input still contains the user value");
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Sanity test that we can show census group individuals, and then go back to
|
||||
// the previous view.
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
individualsState,
|
||||
viewState,
|
||||
censusState,
|
||||
} = require("devtools/client/memory/constants");
|
||||
const { changeViewAndRefresh, changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const store = panel.panelWin.gStore;
|
||||
const { getState, dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
// Take a snapshot and wait for the census to finish.
|
||||
|
||||
const takeSnapshotButton = doc.getElementById("take-snapshot");
|
||||
EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
|
||||
|
||||
yield waitUntilState(store, state => {
|
||||
return state.snapshots.length === 1 &&
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVED;
|
||||
});
|
||||
|
||||
// Click on the first individuals button found, and wait for the individuals
|
||||
// to be fetched.
|
||||
|
||||
const individualsButton = doc.querySelector(".individuals-button");
|
||||
EventUtils.synthesizeMouseAtCenter(individualsButton, {}, panel.panelWin);
|
||||
|
||||
yield waitUntilState(store, state => {
|
||||
return state.view.state === viewState.INDIVIDUALS &&
|
||||
state.individuals &&
|
||||
state.individuals.state === individualsState.FETCHED;
|
||||
});
|
||||
|
||||
ok(doc.getElementById("shortest-paths"),
|
||||
"Should be showing the shortest paths component");
|
||||
ok(doc.querySelector(".heap-tree-item"),
|
||||
"Should be showing the individuals");
|
||||
|
||||
// Go back to the previous view.
|
||||
|
||||
const popViewButton = doc.getElementById("pop-view-button");
|
||||
ok(popViewButton, "Should be showing the #pop-view-button");
|
||||
EventUtils.synthesizeMouseAtCenter(popViewButton, {}, panel.panelWin);
|
||||
|
||||
yield waitUntilState(store, state => {
|
||||
return state.view.state === viewState.CENSUS;
|
||||
});
|
||||
|
||||
ok(!doc.getElementById("shortest-paths"),
|
||||
"Should not be showing the shortest paths component anymore");
|
||||
});
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that using ACCEL+UP/DOWN, the user can navigate between snapshots.
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
snapshotState,
|
||||
viewState,
|
||||
censusState
|
||||
} = require("devtools/client/memory/constants");
|
||||
const {
|
||||
takeSnapshotAndCensus
|
||||
} = require("devtools/client/memory/actions/snapshot");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ panel }) {
|
||||
// Creating snapshots already takes ~25 seconds on linux 32 debug machines
|
||||
// which makes the test very likely to go over the allowed timeout
|
||||
requestLongerTimeout(2);
|
||||
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const store = panel.panelWin.gStore;
|
||||
const { dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
info("Take 3 snapshots");
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
|
||||
yield waitUntilState(store, state =>
|
||||
state.snapshots.length == 3 &&
|
||||
state.snapshots.every(s => s.census && s.census.state === censusState.SAVED));
|
||||
ok(true, "All snapshots censuses are in SAVED state");
|
||||
|
||||
yield waitUntilSnapshotSelected(store, 2);
|
||||
ok(true, "Third snapshot selected after creating all snapshots.");
|
||||
|
||||
info("Press ACCEL+UP key, expect second snapshot selected.");
|
||||
EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
|
||||
yield waitUntilSnapshotSelected(store, 1);
|
||||
ok(true, "Second snapshot selected after alt+UP.");
|
||||
|
||||
info("Press ACCEL+UP key, expect first snapshot selected.");
|
||||
EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
|
||||
yield waitUntilSnapshotSelected(store, 0);
|
||||
ok(true, "First snapshot is selected after ACCEL+UP");
|
||||
|
||||
info("Check ACCEL+UP is a noop when the first snapshot is selected.");
|
||||
EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
|
||||
// We assume the snapshot selection should be synchronous here.
|
||||
is(getSelectedSnapshotIndex(store), 0, "First snapshot is still selected");
|
||||
|
||||
info("Press ACCEL+DOWN key, expect second snapshot selected.");
|
||||
EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin);
|
||||
yield waitUntilSnapshotSelected(store, 1);
|
||||
ok(true, "Second snapshot is selected after ACCEL+DOWN");
|
||||
|
||||
info("Click on first node.");
|
||||
let firstNode = doc.querySelector(".tree .heap-tree-item-name");
|
||||
EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin);
|
||||
yield waitUntilState(store, state => state.snapshots[1].census.focused ===
|
||||
state.snapshots[1].census.report.children[0]
|
||||
);
|
||||
ok(true, "First root is selected after click.");
|
||||
|
||||
info("Press DOWN key, expect second root focused.");
|
||||
EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
|
||||
yield waitUntilState(store, state => state.snapshots[1].census.focused ===
|
||||
state.snapshots[1].census.report.children[1]
|
||||
);
|
||||
ok(true, "Second root is selected after pressing DOWN.");
|
||||
is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected");
|
||||
|
||||
info("Press UP key, expect second root focused.");
|
||||
EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
|
||||
yield waitUntilState(store, state => state.snapshots[1].census.focused ===
|
||||
state.snapshots[1].census.report.children[0]
|
||||
);
|
||||
ok(true, "First root is selected after pressing UP.");
|
||||
is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected");
|
||||
|
||||
info("Press ACCEL+DOWN key, expect third snapshot selected.");
|
||||
EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin);
|
||||
yield waitUntilSnapshotSelected(store, 2);
|
||||
ok(true, "Thirdˆ snapshot is selected after ACCEL+DOWN");
|
||||
|
||||
info("Check ACCEL+DOWN is a noop when the last snapshot is selected.");
|
||||
EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin);
|
||||
// We assume the snapshot selection should be synchronous here.
|
||||
is(getSelectedSnapshotIndex(store), 2, "Third snapshot is still selected");
|
||||
});
|
||||
107
devtools/client/memory/test/browser/browser_memory_keyboard.js
Normal file
107
devtools/client/memory/test/browser/browser_memory_keyboard.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Bug 1246570 - Check that when pressing on LEFT arrow, the parent tree node
|
||||
// gets focused.
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
snapshotState,
|
||||
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].census &&
|
||||
state.snapshots[0].census.state === censusState.SAVED &&
|
||||
state.snapshots[0].census.focused &&
|
||||
state.snapshots[0].census.focused === node
|
||||
);
|
||||
}
|
||||
|
||||
function waitUntilExpanded(store, node) {
|
||||
return waitUntilState(store, state =>
|
||||
state.snapshots[0] &&
|
||||
state.snapshots[0].census &&
|
||||
state.snapshots[0].census.expanded.has(node.id));
|
||||
}
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const store = panel.panelWin.gStore;
|
||||
const { getState, dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
is(getState().censusDisplay.breakdown.by, "coarseType");
|
||||
|
||||
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
let census = getState().snapshots[0].census;
|
||||
let root1 = census.report.children[0];
|
||||
let root2 = census.report.children[0];
|
||||
let root3 = census.report.children[0];
|
||||
let root4 = census.report.children[0];
|
||||
let child1 = root1.children[0];
|
||||
|
||||
info("Click on first node.");
|
||||
let firstNode = doc.querySelector(".tree .heap-tree-item-name");
|
||||
EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin);
|
||||
yield waitUntilFocused(store, root1);
|
||||
ok(true, "First root is selected after click.");
|
||||
|
||||
info("Press DOWN key, expect second root focused.");
|
||||
EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
|
||||
yield waitUntilFocused(store, root2);
|
||||
ok(true, "Second root is selected after pressing DOWN arrow.");
|
||||
|
||||
info("Press DOWN key, expect third root focused.");
|
||||
EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
|
||||
yield waitUntilFocused(store, root3);
|
||||
ok(true, "Third root is selected after pressing DOWN arrow.");
|
||||
|
||||
info("Press DOWN key, expect fourth root focused.");
|
||||
EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin);
|
||||
yield waitUntilFocused(store, root4);
|
||||
ok(true, "Fourth root is selected after pressing DOWN arrow.");
|
||||
|
||||
info("Press UP key, expect third root focused.");
|
||||
EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
|
||||
yield waitUntilFocused(store, root3);
|
||||
ok(true, "Third root is selected after pressing UP arrow.");
|
||||
|
||||
info("Press UP key, expect second root focused.");
|
||||
EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
|
||||
yield waitUntilFocused(store, root2);
|
||||
ok(true, "Second root is selected after pressing UP arrow.");
|
||||
|
||||
info("Press UP key, expect first root focused.");
|
||||
EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin);
|
||||
yield waitUntilFocused(store, root1);
|
||||
ok(true, "First root is selected after pressing UP arrow.");
|
||||
|
||||
info("Press RIGHT key");
|
||||
EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
|
||||
yield waitUntilExpanded(store, root1);
|
||||
ok(true, "Root node is expanded.");
|
||||
|
||||
info("Press RIGHT key, expect first child focused.");
|
||||
EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin);
|
||||
yield waitUntilFocused(store, child1);
|
||||
ok(true, "First child is selected after pressing RIGHT arrow.");
|
||||
|
||||
info("Press LEFT key, expect first root focused.");
|
||||
EventUtils.synthesizeKey("VK_LEFT", {}, panel.panelWin);
|
||||
yield waitUntilFocused(store, root1);
|
||||
ok(true, "First root is selected after pressing LEFT arrow.");
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Sanity test that we can show allocation stack displays in the tree.
|
||||
|
||||
"use strict";
|
||||
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const censusDisplayActions = require("devtools/client/memory/actions/census-display");
|
||||
const { viewState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const { getState, dispatch } = panel.panelWin.gStore;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
ok(!getState().allocations.recording,
|
||||
"Should not be recording allocagtions");
|
||||
|
||||
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield dispatch(censusDisplayActions.setCensusDisplayAndRefresh(
|
||||
heapWorker,
|
||||
censusDisplays.allocationStack));
|
||||
|
||||
is(getState().censusDisplay.breakdown.by, "allocationStack",
|
||||
"Should be using allocation stack breakdown");
|
||||
|
||||
ok(!getState().allocations.recording,
|
||||
"Should still not be recording allocagtions");
|
||||
|
||||
ok(doc.querySelector(".no-allocation-stacks"),
|
||||
"Because we did not record allocations, the no-allocation-stack warning should be visible");
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Bug 1221150 - Ensure that census trees do not accidentally auto expand
|
||||
// when clicking on the allocation stacks checkbox.
|
||||
|
||||
"use strict";
|
||||
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const { viewState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const { getState, dispatch } = panel.panelWin.gStore;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
|
||||
is(getState().allocations.recording, false);
|
||||
const recordingCheckbox = doc.getElementById("record-allocation-stacks-checkbox");
|
||||
EventUtils.synthesizeMouseAtCenter(recordingCheckbox, {}, panel.panelWin);
|
||||
is(getState().allocations.recording, true);
|
||||
|
||||
const nameElems = [...doc.querySelectorAll(".heap-tree-item-field.heap-tree-item-name")];
|
||||
|
||||
for (let el of nameElems) {
|
||||
dumpn(`Found ${el.textContent.trim()}`);
|
||||
is(el.style.marginInlineStart, "0px",
|
||||
"None of the elements should be an indented/expanded child");
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Sanity test that we calculate percentages in the tree.
|
||||
|
||||
"use strict";
|
||||
|
||||
const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
const { viewState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
|
||||
function checkCells(cells) {
|
||||
ok(cells.length > 1, "Should have found some");
|
||||
// Ignore the first header cell.
|
||||
for (let cell of cells.slice(1)) {
|
||||
const percent = cell.querySelector(".heap-tree-percent");
|
||||
ok(percent, "should have a percent cell");
|
||||
ok(percent.textContent.match(/^\d?\d%$/), "should be of the form nn% or n%");
|
||||
}
|
||||
}
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const { getState, dispatch } = panel.panelWin.gStore;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
yield dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
is(getState().censusDisplay.breakdown.by, "coarseType",
|
||||
"Should be using coarse type breakdown");
|
||||
|
||||
const bytesCells = [...doc.querySelectorAll(".heap-tree-item-bytes")];
|
||||
checkCells(bytesCells);
|
||||
|
||||
const totalBytesCells = [...doc.querySelectorAll(".heap-tree-item-total-bytes")];
|
||||
checkCells(totalBytesCells);
|
||||
|
||||
const countCells = [...doc.querySelectorAll(".heap-tree-item-count")];
|
||||
checkCells(countCells);
|
||||
|
||||
const totalCountCells = [...doc.querySelectorAll(".heap-tree-item-total-count")];
|
||||
checkCells(totalCountCells);
|
||||
});
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that refreshing the page with devtools open does not leak the old
|
||||
// windows from previous navigations.
|
||||
//
|
||||
// IF THIS TEST STARTS FAILING, YOU ARE LEAKING EVERY WINDOW EVER NAVIGATED TO
|
||||
// WHILE DEVTOOLS ARE OPEN! THIS IS NOT SPECIFIC TO THE MEMORY TOOL ONLY!
|
||||
|
||||
"use strict";
|
||||
|
||||
const HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
|
||||
const { getLabelAndShallowSize } = require("devtools/shared/heapsnapshot/DominatorTreeNode");
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_empty.html";
|
||||
|
||||
function* getWindowsInSnapshot(front) {
|
||||
dumpn("Taking snapshot.");
|
||||
const path = yield front.saveHeapSnapshot();
|
||||
dumpn("Took snapshot with path = " + path);
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(path);
|
||||
dumpn("Read snapshot into memory, taking census.");
|
||||
const report = snapshot.takeCensus({
|
||||
breakdown: {
|
||||
by: "objectClass",
|
||||
then: { by: "bucket" },
|
||||
other: { by: "count", count: true, bytes: false },
|
||||
}
|
||||
});
|
||||
dumpn("Took census, window count = " + report.Window.count);
|
||||
return report.Window;
|
||||
}
|
||||
|
||||
const DESCRIPTION = {
|
||||
by: "coarseType",
|
||||
objects: {
|
||||
by: "objectClass",
|
||||
then: { by: "count", count: true, bytes: false },
|
||||
other: { by: "count", count: true, bytes: false },
|
||||
},
|
||||
strings: { by: "count", count: true, bytes: false },
|
||||
scripts: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: false },
|
||||
},
|
||||
other: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: false },
|
||||
}
|
||||
};
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const heapWorker = panel.panelWin.gHeapAnalysesClient;
|
||||
const front = panel.panelWin.gFront;
|
||||
const store = panel.panelWin.gStore;
|
||||
const { getState, dispatch } = store;
|
||||
const doc = panel.panelWin.document;
|
||||
|
||||
const startWindows = yield getWindowsInSnapshot(front);
|
||||
dumpn("Initial windows found = " + startWindows.map(w => "0x" + w.toString(16)).join(", "));
|
||||
is(startWindows.length, 1);
|
||||
|
||||
yield refreshTab(tab);
|
||||
|
||||
const endWindows = yield getWindowsInSnapshot(front);
|
||||
is(endWindows.length, 1);
|
||||
|
||||
if (endWindows.length === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
dumpn("Test failed, diagnosing leaking windows.");
|
||||
dumpn("(This may fail if a moving GC has relocated the initial Window objects.)");
|
||||
|
||||
dumpn("Taking full runtime snapshot.");
|
||||
const path = yield front.saveHeapSnapshot({ boundaries: { runtime: true } });
|
||||
dumpn("Full runtime's snapshot path = " + path);
|
||||
|
||||
dumpn("Reading full runtime heap snapshot.");
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(path);
|
||||
dumpn("Done reading full runtime heap snapshot.");
|
||||
|
||||
const dominatorTree = snapshot.computeDominatorTree();
|
||||
const paths = snapshot.computeShortestPaths(dominatorTree.root, startWindows, 50);
|
||||
|
||||
for (let i = 0; i < startWindows.length; i++) {
|
||||
dumpn("Shortest retaining paths for leaking Window 0x" + startWindows[i].toString(16) + " =========================");
|
||||
let j = 0;
|
||||
for (let retainingPath of paths.get(startWindows[i])) {
|
||||
if (retainingPath.find(part => part.predecessor === startWindows[i])) {
|
||||
// Skip paths that loop out from the target window and back to it again.
|
||||
continue;
|
||||
}
|
||||
|
||||
dumpn(" Path #" + (++j) + ": --------------------------------------------------------------------");
|
||||
for (let part of retainingPath) {
|
||||
const { label } = getLabelAndShallowSize(part.predecessor, snapshot, DESCRIPTION);
|
||||
dumpn(" 0x" + part.predecessor.toString(16) +
|
||||
" (" + label.join(" > ") + ")");
|
||||
dumpn(" |");
|
||||
dumpn(" " + part.edge);
|
||||
dumpn(" |");
|
||||
dumpn(" V");
|
||||
}
|
||||
dumpn(" 0x" + startWindows[i].toString(16) + " (objects > Window)");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests taking snapshots and default states.
|
||||
*/
|
||||
|
||||
const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
|
||||
const { viewState, censusState } = require("devtools/client/memory/constants");
|
||||
const { changeView } = require("devtools/client/memory/actions/view");
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const { gStore, document } = panel.panelWin;
|
||||
const { getState, dispatch } = gStore;
|
||||
|
||||
dispatch(changeView(viewState.CENSUS));
|
||||
|
||||
let snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
|
||||
is(getState().snapshots.length, 0, "Starts with no snapshots in store");
|
||||
is(snapshotEls.length, 0, "No snapshots rendered");
|
||||
|
||||
yield takeSnapshot(panel.panelWin);
|
||||
snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
|
||||
is(getState().snapshots.length, 1, "One snapshot was created in store");
|
||||
is(snapshotEls.length, 1, "One snapshot was rendered");
|
||||
ok(snapshotEls[0].classList.contains("selected"), "Only snapshot has `selected` class");
|
||||
|
||||
yield takeSnapshot(panel.panelWin);
|
||||
snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
|
||||
is(getState().snapshots.length, 2, "Two snapshots created in store");
|
||||
is(snapshotEls.length, 2, "Two snapshots rendered");
|
||||
ok(!snapshotEls[0].classList.contains("selected"), "First snapshot no longer has `selected` class");
|
||||
ok(snapshotEls[1].classList.contains("selected"), "Second snapshot has `selected` class");
|
||||
|
||||
yield 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,28 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can save a heap snapshot and transfer it over the RDP in e10s
|
||||
// where the child process is sandboxed and so we have to use
|
||||
// HeapSnapshotFileActor to get the heap snapshot file.
|
||||
|
||||
"use strict";
|
||||
|
||||
const TEST_URL = "data:text/html,<html><body></body></html>";
|
||||
|
||||
this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
const memoryFront = panel.panelWin.gFront;
|
||||
ok(memoryFront, "Should get the MemoryFront");
|
||||
|
||||
const snapshotFilePath = yield memoryFront.saveHeapSnapshot({
|
||||
// Force a copy so that we go through the HeapSnapshotFileActor's
|
||||
// transferHeapSnapshot request and exercise this code path on e10s.
|
||||
forceCopy: true
|
||||
});
|
||||
|
||||
ok(!!(yield OS.File.stat(snapshotFilePath)),
|
||||
"Should have the heap snapshot file");
|
||||
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath);
|
||||
ok(snapshot instanceof HeapSnapshot,
|
||||
"And we should be able to read a HeapSnapshot instance from the file");
|
||||
});
|
||||
|
|
@ -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,161 @@
|
|||
/* 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);
|
||||
let style = canvases.container.style;
|
||||
|
||||
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");
|
||||
is(style.transform, "translate(0px, 0px) scale(1)",
|
||||
"No transforms have been done.");
|
||||
|
||||
canvases.container.dispatchEvent(new WheelEvent("wheel", {
|
||||
deltaY: -PIXEL_DELTA,
|
||||
deltaMode: PIXEL_SCROLL_MODE
|
||||
}));
|
||||
|
||||
is(style.transform, "translate(0px, 0px) scale(1.05)",
|
||||
"The div has been slightly scaled.");
|
||||
is(dragZoom.zoom, PIXEL_DELTA * dragZoom.ZOOM_SPEED,
|
||||
"The zoom was increased");
|
||||
ok(floatEquality(dragZoom.smoothZoom, 0.05),
|
||||
"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(style.transform, "translate(0px, 0px) scale(1.1)",
|
||||
"The scale has been fully applied");
|
||||
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"));
|
||||
|
||||
is(style.transform, "translate(2.5px, 5px) scale(1.1)",
|
||||
"The style is correctly translated");
|
||||
ok(floatEquality(dragZoom.translateX, 5),
|
||||
"Translate X moved by some pixel amount");
|
||||
ok(floatEquality(dragZoom.translateY, 10),
|
||||
"Translate Y moved by some pixel amount");
|
||||
}
|
||||
|
||||
info("Zooming centers around the mouse");
|
||||
{
|
||||
canvases.container.dispatchEvent(new WheelEvent("wheel", {
|
||||
deltaY: -PIXEL_DELTA,
|
||||
deltaMode: PIXEL_SCROLL_MODE
|
||||
}));
|
||||
// Run through the RAF loop to zoom in towards that value.
|
||||
let lastCallCount;
|
||||
for (let i = 0; i < MAX_RAF_LOOP; i++) {
|
||||
if (lastCallCount === rafMock.timesCalled) {
|
||||
break;
|
||||
}
|
||||
lastCallCount = rafMock.timesCalled;
|
||||
rafMock.nextFrame();
|
||||
}
|
||||
is(style.transform, "translate(8.18182px, 18.1818px) scale(1.2)",
|
||||
"Zooming affects the translation to keep the mouse centered");
|
||||
ok(floatEquality(dragZoom.translateX, 8.181818181818185),
|
||||
"Translate X was affected by the mouse position");
|
||||
ok(floatEquality(dragZoom.translateY, 18.18181818181817),
|
||||
"Translate Y was affected by the mouse position");
|
||||
is(dragZoom.zoom, 0.2, "Zooming starts at 0");
|
||||
}
|
||||
|
||||
dragZoom.destroy();
|
||||
|
||||
info("Scroll isn't tracked after destruction");
|
||||
{
|
||||
let previousZoom = dragZoom.zoom;
|
||||
let previousSmoothZoom = dragZoom.smoothZoom;
|
||||
|
||||
canvases.container.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);
|
||||
});
|
||||
15
devtools/client/memory/test/browser/doc_big_tree.html
Normal file
15
devtools/client/memory/test/browser/doc_big_tree.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
window.big = (function makeBig(depth = 0) {
|
||||
var big = Array(5);
|
||||
big.fill(undefined);
|
||||
if (depth < 5) {
|
||||
big = big.map(_ => makeBig(depth + 1));
|
||||
}
|
||||
return big;
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
9
devtools/client/memory/test/browser/doc_empty.html
Normal file
9
devtools/client/memory/test/browser/doc_empty.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
</head>
|
||||
<body>
|
||||
This is an empty window.
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
var objects = window.objects = [];
|
||||
|
||||
var allocate = this.allocate = function allocate() {
|
||||
for (var i = 0; i < 100; i++)
|
||||
objects.push({});
|
||||
setTimeout(allocate, 10);
|
||||
}
|
||||
|
||||
allocate();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
248
devtools/client/memory/test/browser/head.js
Normal file
248
devtools/client/memory/test/browser/head.js
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Load the shared test helpers into this compartment.
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
|
||||
this);
|
||||
|
||||
// Load the shared Redux helpers into this compartment.
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
|
||||
this);
|
||||
|
||||
var { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
|
||||
var { L10N } = require("devtools/client/memory/utils");
|
||||
|
||||
Services.prefs.setBoolPref("devtools.memory.enabled", true);
|
||||
|
||||
/**
|
||||
* Open the memory panel for the given tab.
|
||||
*/
|
||||
this.openMemoryPanel = Task.async(function* (tab) {
|
||||
info("Opening memory panel.");
|
||||
const target = TargetFactory.forTab(tab);
|
||||
const toolbox = yield gDevTools.showToolbox(target, "memory");
|
||||
info("Memory panel shown successfully.");
|
||||
let panel = toolbox.getCurrentPanel();
|
||||
return { tab, panel };
|
||||
});
|
||||
|
||||
/**
|
||||
* Close the memory panel for the given tab.
|
||||
*/
|
||||
this.closeMemoryPanel = Task.async(function* (tab) {
|
||||
info("Closing memory panel.");
|
||||
const target = TargetFactory.forTab(tab);
|
||||
const toolbox = gDevTools.getToolbox(target);
|
||||
yield toolbox.destroy();
|
||||
info("Closed memory panel successfully.");
|
||||
});
|
||||
|
||||
/**
|
||||
* Return a test function that adds a tab with the given url, opens the memory
|
||||
* panel, runs the given generator, closes the memory panel, removes the tab,
|
||||
* and finishes.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
|
||||
* // Your tests go here...
|
||||
* });
|
||||
*/
|
||||
function makeMemoryTest(url, generator) {
|
||||
return Task.async(function* () {
|
||||
waitForExplicitFinish();
|
||||
|
||||
// It can take a long time to save a snapshot to disk, read the snapshots
|
||||
// back from disk, and finally perform analyses on them.
|
||||
requestLongerTimeout(2);
|
||||
|
||||
const tab = yield addTab(url);
|
||||
const results = yield openMemoryPanel(tab);
|
||||
|
||||
try {
|
||||
yield* generator(results);
|
||||
} catch (err) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
|
||||
}
|
||||
|
||||
yield closeMemoryPanel(tab);
|
||||
yield removeTab(tab);
|
||||
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
function dumpn(msg) {
|
||||
dump(`MEMORY-TEST: ${msg}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will resolve when the provided store matches
|
||||
* the expected array. expectedStates is an array of dominatorTree states.
|
||||
* Expectations :
|
||||
* - store.getState().snapshots.length == expected.length
|
||||
* - snapshots[i].dominatorTree.state == expected[i]
|
||||
*
|
||||
* @param {Store} store
|
||||
* @param {Array<string>} expectedStates [description]
|
||||
* @return {Promise}
|
||||
*/
|
||||
function waitUntilDominatorTreeState(store, expected) {
|
||||
let predicate = () => {
|
||||
let snapshots = store.getState().snapshots;
|
||||
return snapshots.length === expected.length &&
|
||||
expected.every((state, i) => {
|
||||
return snapshots[i].dominatorTree &&
|
||||
snapshots[i].dominatorTree.state === state;
|
||||
});
|
||||
};
|
||||
info(`Waiting for dominator trees to be of state: ${expected}`);
|
||||
return waitUntilState(store, predicate);
|
||||
}
|
||||
|
||||
function takeSnapshot(window) {
|
||||
let { gStore, document } = window;
|
||||
let snapshotCount = gStore.getState().snapshots.length;
|
||||
info("Taking snapshot...");
|
||||
document.querySelector(".devtools-toolbar .take-snapshot").click();
|
||||
return waitUntilState(gStore, () => gStore.getState().snapshots.length === snapshotCount + 1);
|
||||
}
|
||||
|
||||
function clearSnapshots(window) {
|
||||
let { gStore, document } = window;
|
||||
document.querySelector(".devtools-toolbar .clear-snapshots").click();
|
||||
return waitUntilState(gStore, () => gStore.getState().snapshots.every(
|
||||
(snapshot) => snapshot.state !== states.READ)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current requested display and waits for the selected snapshot to use
|
||||
* it and complete the new census that entails.
|
||||
*/
|
||||
function setCensusDisplay(window, display) {
|
||||
info(`Setting census display to ${display}...`);
|
||||
let { gStore, gHeapAnalysesClient } = window;
|
||||
// XXX: Should handle this via clicking the DOM, but React doesn't
|
||||
// fire the onChange event, so just change it in the store.
|
||||
// window.document.querySelector(`.select-display`).value = type;
|
||||
gStore.dispatch(require("devtools/client/memory/actions/census-display")
|
||||
.setCensusDisplayAndRefresh(gHeapAnalysesClient, display));
|
||||
|
||||
return waitUntilState(window.gStore, () => {
|
||||
let selected = window.gStore.getState().snapshots.find(s => s.selected);
|
||||
return selected.state === states.READ &&
|
||||
selected.census &&
|
||||
selected.census.state === censusState.SAVED &&
|
||||
selected.census.display === display;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the snapshot tatus text currently displayed, or null if none is
|
||||
* displayed.
|
||||
*
|
||||
* @param {Document} document
|
||||
*/
|
||||
function getDisplayedSnapshotStatus(document) {
|
||||
const status = document.querySelector(".snapshot-status");
|
||||
return status ? status.textContent.trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the currently selected snapshot.
|
||||
*
|
||||
* @return {Number}
|
||||
*/
|
||||
function getSelectedSnapshotIndex(store) {
|
||||
let snapshots = store.getState().snapshots;
|
||||
let selectedSnapshot = snapshots.find(s => s.selected);
|
||||
return snapshots.indexOf(selectedSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will resolve when the snapshot with provided index
|
||||
* becomes selected.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
function waitUntilSnapshotSelected(store, snapshotIndex) {
|
||||
return waitUntilState(store, state =>
|
||||
state.snapshots[snapshotIndex] &&
|
||||
state.snapshots[snapshotIndex].selected === true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wait until the state has censuses in a certain state.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
function waitUntilCensusState(store, getCensus, expected) {
|
||||
let predicate = () => {
|
||||
let snapshots = store.getState().snapshots;
|
||||
|
||||
info("Current census state:" +
|
||||
snapshots.map(x => getCensus(x) ? getCensus(x).state : null));
|
||||
|
||||
return snapshots.length === expected.length &&
|
||||
expected.every((state, i) => {
|
||||
let census = getCensus(snapshots[i]);
|
||||
return (state === "*") ||
|
||||
(!census && !state) ||
|
||||
(census && census.state === state);
|
||||
});
|
||||
};
|
||||
info(`Waiting for snapshot 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if two floats are equivalent.
|
||||
*
|
||||
* @param {Float} a
|
||||
* @param {Float} b
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function floatEquality(a, b) {
|
||||
const EPSILON = 0.00000000001;
|
||||
const equals = Math.abs(a - b) < EPSILON;
|
||||
if (!equals) {
|
||||
info(`${a} not equal to ${b}`);
|
||||
}
|
||||
return equals;
|
||||
}
|
||||
20
devtools/client/memory/test/chrome/chrome.ini
Normal file
20
devtools/client/memory/test/chrome/chrome.ini
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[DEFAULT]
|
||||
support-files =
|
||||
head.js
|
||||
|
||||
[test_CensusTreeItem_01.html]
|
||||
[test_DominatorTree_01.html]
|
||||
[test_DominatorTree_02.html]
|
||||
[test_DominatorTree_03.html]
|
||||
[test_DominatorTreeItem_01.html]
|
||||
[test_Heap_01.html]
|
||||
[test_Heap_02.html]
|
||||
[test_Heap_03.html]
|
||||
[test_Heap_04.html]
|
||||
[test_Heap_05.html]
|
||||
[test_List_01.html]
|
||||
[test_ShortestPaths_01.html]
|
||||
[test_ShortestPaths_02.html]
|
||||
[test_SnapshotListItem_01.html]
|
||||
[test_Toolbar_01.html]
|
||||
[test_TreeMap_01.html]
|
||||
335
devtools/client/memory/test/chrome/head.js
Normal file
335
devtools/client/memory/test/chrome/head.js
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
|
||||
var { require } = BrowserLoader({
|
||||
baseURI: "resource://devtools/client/memory/",
|
||||
window
|
||||
});
|
||||
var { Assert } = require("resource://testing-common/Assert.jsm");
|
||||
var Services = require("Services");
|
||||
var { Task } = require("devtools/shared/task");
|
||||
|
||||
var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
|
||||
|
||||
SimpleTest.registerCleanupFunction(function () {
|
||||
if (DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
|
||||
ok(false, "Should have had the expected number of DevToolsUtils.assert() failures. Expected " +
|
||||
EXPECTED_DTU_ASSERT_FAILURE_COUNT + ", got " + DevToolsUtils.assertionFailureCount);
|
||||
}
|
||||
});
|
||||
|
||||
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
var { immutableUpdate } = DevToolsUtils;
|
||||
var flags = require("devtools/shared/flags");
|
||||
flags.testing = true;
|
||||
|
||||
var constants = require("devtools/client/memory/constants");
|
||||
var {
|
||||
censusDisplays,
|
||||
diffingState,
|
||||
labelDisplays,
|
||||
dominatorTreeState,
|
||||
snapshotState,
|
||||
viewState,
|
||||
censusState
|
||||
} = constants;
|
||||
|
||||
const {
|
||||
L10N,
|
||||
} = require("devtools/client/memory/utils");
|
||||
|
||||
var models = require("devtools/client/memory/models");
|
||||
|
||||
var Immutable = require("devtools/client/shared/vendor/immutable");
|
||||
var React = require("devtools/client/shared/vendor/react");
|
||||
var ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
var Heap = React.createFactory(require("devtools/client/memory/components/heap"));
|
||||
var CensusTreeItem = React.createFactory(require("devtools/client/memory/components/census-tree-item"));
|
||||
var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree"));
|
||||
var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item"));
|
||||
var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths"));
|
||||
var TreeMap = React.createFactory(require("devtools/client/memory/components/tree-map"));
|
||||
var SnapshotListItem = React.createFactory(require("devtools/client/memory/components/snapshot-list-item"));
|
||||
var List = React.createFactory(require("devtools/client/memory/components/list"));
|
||||
var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
|
||||
|
||||
// All tests are asynchronous.
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var noop = () => {};
|
||||
|
||||
var TEST_CENSUS_TREE_ITEM_PROPS = Object.freeze({
|
||||
item: Object.freeze({
|
||||
bytes: 10,
|
||||
count: 1,
|
||||
totalBytes: 10,
|
||||
totalCount: 1,
|
||||
name: "foo",
|
||||
children: [
|
||||
Object.freeze({
|
||||
bytes: 10,
|
||||
count: 1,
|
||||
totalBytes: 10,
|
||||
totalCount: 1,
|
||||
name: "bar",
|
||||
})
|
||||
]
|
||||
}),
|
||||
depth: 0,
|
||||
arrow: ">",
|
||||
focused: true,
|
||||
getPercentBytes: () => 50,
|
||||
getPercentCount: () => 50,
|
||||
showSign: false,
|
||||
onViewSourceInDebugger: noop,
|
||||
inverted: false,
|
||||
});
|
||||
|
||||
// Counter for mock DominatorTreeNode ids.
|
||||
var TEST_NODE_ID_COUNTER = 0;
|
||||
|
||||
/**
|
||||
* Create a mock DominatorTreeNode for testing, with sane defaults. Override any
|
||||
* property by providing it on `opts`. Optionally pass child nodes as well.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {Array<DominatorTreeNode>?} children
|
||||
*
|
||||
* @returns {DominatorTreeNode}
|
||||
*/
|
||||
function makeTestDominatorTreeNode(opts, children) {
|
||||
const nodeId = TEST_NODE_ID_COUNTER++;
|
||||
|
||||
const node = Object.assign({
|
||||
nodeId,
|
||||
label: ["other", "SomeType"],
|
||||
shallowSize: 1,
|
||||
retainedSize: (children || []).reduce((size, c) => size + c.retainedSize, 1),
|
||||
parentId: undefined,
|
||||
children,
|
||||
moreChildrenAvailable: true,
|
||||
}, opts);
|
||||
|
||||
if (children && children.length) {
|
||||
children.map(c => c.parentId = node.nodeId);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
var TEST_DOMINATOR_TREE = Object.freeze({
|
||||
dominatorTreeId: 666,
|
||||
root: (function makeTree(depth = 0) {
|
||||
let children;
|
||||
if (depth <= 3) {
|
||||
children = [
|
||||
makeTree(depth + 1),
|
||||
makeTree(depth + 1),
|
||||
makeTree(depth + 1),
|
||||
];
|
||||
}
|
||||
return makeTestDominatorTreeNode({}, children);
|
||||
}()),
|
||||
expanded: new Set(),
|
||||
focused: null,
|
||||
error: null,
|
||||
display: labelDisplays.coarseType,
|
||||
activeFetchRequestCount: null,
|
||||
state: dominatorTreeState.LOADED,
|
||||
});
|
||||
|
||||
var TEST_DOMINATOR_TREE_PROPS = Object.freeze({
|
||||
dominatorTree: TEST_DOMINATOR_TREE,
|
||||
onLoadMoreSiblings: noop,
|
||||
onViewSourceInDebugger: noop,
|
||||
onExpand: noop,
|
||||
onCollapse: noop,
|
||||
});
|
||||
|
||||
var TEST_SHORTEST_PATHS_PROPS = Object.freeze({
|
||||
graph: Object.freeze({
|
||||
nodes: [
|
||||
{ id: 1, label: ["other", "SomeType"] },
|
||||
{ id: 2, label: ["other", "SomeType"] },
|
||||
{ id: 3, label: ["other", "SomeType"] },
|
||||
],
|
||||
edges: [
|
||||
{ from: 1, to: 2, name: "1->2" },
|
||||
{ from: 1, to: 3, name: "1->3" },
|
||||
{ from: 2, to: 3, name: "2->3" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
var TEST_SNAPSHOT = Object.freeze({
|
||||
id: 1337,
|
||||
selected: true,
|
||||
path: "/fake/path/to/snapshot",
|
||||
census: Object.freeze({
|
||||
report: Object.freeze({
|
||||
objects: Object.freeze({ count: 4, bytes: 400 }),
|
||||
scripts: Object.freeze({ count: 3, bytes: 300 }),
|
||||
strings: Object.freeze({ count: 2, bytes: 200 }),
|
||||
other: Object.freeze({ count: 1, bytes: 100 }),
|
||||
}),
|
||||
display: Object.freeze({
|
||||
displayName: "Test Display",
|
||||
tooltip: "Test display tooltup",
|
||||
inverted: false,
|
||||
breakdown: Object.freeze({
|
||||
by: "coarseType",
|
||||
objects: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
scripts: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
strings: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
other: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
}),
|
||||
}),
|
||||
state: censusState.SAVED,
|
||||
inverted: false,
|
||||
filter: null,
|
||||
expanded: new Set(),
|
||||
focused: null,
|
||||
parentMap: Object.freeze(Object.create(null))
|
||||
}),
|
||||
dominatorTree: TEST_DOMINATOR_TREE,
|
||||
error: null,
|
||||
imported: false,
|
||||
creationTime: 0,
|
||||
state: snapshotState.READ,
|
||||
});
|
||||
|
||||
var TEST_HEAP_PROPS = Object.freeze({
|
||||
onSnapshotClick: noop,
|
||||
onLoadMoreSiblings: noop,
|
||||
onCensusExpand: noop,
|
||||
onCensusCollapse: noop,
|
||||
onDominatorTreeExpand: noop,
|
||||
onDominatorTreeCollapse: noop,
|
||||
onCensusFocus: noop,
|
||||
onDominatorTreeFocus: noop,
|
||||
onViewSourceInDebugger: noop,
|
||||
diffing: null,
|
||||
view: { state: viewState.CENSUS, },
|
||||
snapshot: TEST_SNAPSHOT,
|
||||
sizes: Object.freeze({ shortestPathsSize: .5 }),
|
||||
onShortestPathsResize: noop,
|
||||
});
|
||||
|
||||
var TEST_TOOLBAR_PROPS = Object.freeze({
|
||||
censusDisplays: [
|
||||
censusDisplays.coarseType,
|
||||
censusDisplays.allocationStack,
|
||||
censusDisplays.invertedAllocationStack,
|
||||
],
|
||||
censusDisplay: censusDisplays.coarseType,
|
||||
onTakeSnapshotClick: noop,
|
||||
onImportClick: noop,
|
||||
onCensusDisplayChange: noop,
|
||||
onToggleRecordAllocationStacks: noop,
|
||||
allocations: models.allocations,
|
||||
onToggleInverted: noop,
|
||||
inverted: false,
|
||||
filterString: null,
|
||||
setFilterString: noop,
|
||||
diffing: null,
|
||||
onToggleDiffing: noop,
|
||||
view: { state: viewState.CENSUS, },
|
||||
onViewChange: noop,
|
||||
labelDisplays: [
|
||||
labelDisplays.coarseType,
|
||||
labelDisplays.allocationStack,
|
||||
],
|
||||
labelDisplay: labelDisplays.coarseType,
|
||||
onLabelDisplayChange: noop,
|
||||
snapshots: [],
|
||||
});
|
||||
|
||||
function makeTestCensusNode() {
|
||||
return {
|
||||
name: "Function",
|
||||
bytes: 100,
|
||||
totalBytes: 100,
|
||||
count: 100,
|
||||
totalCount: 100,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
|
||||
var TEST_TREE_MAP_PROPS = Object.freeze({
|
||||
treeMap: Object.freeze({
|
||||
report: {
|
||||
name: null,
|
||||
bytes: 0,
|
||||
totalBytes: 400,
|
||||
count: 0,
|
||||
totalCount: 400,
|
||||
children: [
|
||||
{
|
||||
name: "objects",
|
||||
bytes: 0,
|
||||
totalBytes: 200,
|
||||
count: 0,
|
||||
totalCount: 200,
|
||||
children: [ makeTestCensusNode(), makeTestCensusNode() ]
|
||||
},
|
||||
{
|
||||
name: "other",
|
||||
bytes: 0,
|
||||
totalBytes: 200,
|
||||
count: 0,
|
||||
totalCount: 200,
|
||||
children: [ makeTestCensusNode(), makeTestCensusNode() ],
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
var TEST_SNAPSHOT_LIST_ITEM_PROPS = Object.freeze({
|
||||
onClick: noop,
|
||||
onSave: noop,
|
||||
onDelete: noop,
|
||||
item: TEST_SNAPSHOT,
|
||||
index: 1234,
|
||||
});
|
||||
|
||||
function onNextAnimationFrame(fn) {
|
||||
return () =>
|
||||
requestAnimationFrame(() =>
|
||||
requestAnimationFrame(fn));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the provided ReactElement in the provided HTML container.
|
||||
* Returns a Promise that will resolve the rendered element as a React
|
||||
* component.
|
||||
*/
|
||||
function renderComponent(element, container) {
|
||||
return new Promise(resolve => {
|
||||
let component = ReactDOM.render(element, container,
|
||||
onNextAnimationFrame(() => {
|
||||
dumpn("Rendered = " + container.innerHTML);
|
||||
resolve(component);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function setState(component, newState) {
|
||||
return new Promise(resolve => {
|
||||
component.setState(newState, onNextAnimationFrame(resolve));
|
||||
});
|
||||
}
|
||||
|
||||
function setProps(component, newProps) {
|
||||
return new Promise(resolve => {
|
||||
component.setProps(newProps, onNextAnimationFrame(resolve));
|
||||
});
|
||||
}
|
||||
|
||||
function dumpn(msg) {
|
||||
dump(`MEMORY-TEST: ${msg}\n`);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that children pointers show up at the correct times.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 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(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
|
||||
inverted: true,
|
||||
depth: 0,
|
||||
})), container);
|
||||
|
||||
ok(!container.querySelector(".children-pointer"),
|
||||
"Don't show children pointer for roots when we are inverted");
|
||||
|
||||
yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
|
||||
inverted: true,
|
||||
depth: 1,
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(".children-pointer"),
|
||||
"Do show children pointer for non-roots when we are inverted");
|
||||
|
||||
yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
|
||||
inverted: false,
|
||||
item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: undefined }),
|
||||
})), container);
|
||||
|
||||
ok(!container.querySelector(".children-pointer"),
|
||||
"Don't show children pointer when non-inverted and no children");
|
||||
|
||||
yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
|
||||
inverted: false,
|
||||
depth: 0,
|
||||
item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: [{}] }),
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(".children-pointer"),
|
||||
"Do show children pointer when non-inverted and have children");
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that we don't display `JS::ubi::RootList` for the root, and instead show "GC Roots".
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Give the container height so that the whole tree is rendered. -->
|
||||
<div id="container" style="height: 900px;"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
yield renderComponent(DominatorTreeItem({
|
||||
item: makeTestDominatorTreeNode({ label: ["other", "JS::ubi::RootList"] }),
|
||||
depth: 0,
|
||||
arrow: React.DOM.div(),
|
||||
focused: true,
|
||||
getPercentSize: _ => 50,
|
||||
onViewSourceInDebugger: _ => { },
|
||||
}), container);
|
||||
|
||||
ok(container.textContent.indexOf("JS::ubi::RootList") == -1,
|
||||
"Should not display `JS::ubi::RootList`");
|
||||
ok(container.textContent.indexOf("GC Roots") >= 0,
|
||||
"Should display `GC Roots` instead");
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that we show a place holder for a subtree we are lazily fetching.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Give the container height so that the whole tree is rendered. -->
|
||||
<div id="container" style="height: 900px;"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true});
|
||||
ok(!root.children);
|
||||
|
||||
const expanded = new Set();
|
||||
expanded.add(root.nodeId);
|
||||
|
||||
yield renderComponent(DominatorTreeComponent(immutableUpdate(TEST_DOMINATOR_TREE_PROPS, {
|
||||
dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, {
|
||||
expanded,
|
||||
root,
|
||||
state: dominatorTreeState.INCREMENTAL_FETCHING,
|
||||
activeFetchRequestCount: 1,
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(".subtree-fetching"),
|
||||
"Expanded nodes with more children available, but no children " +
|
||||
"loaded, should get a placeholder");
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that we show a link to load more children when some (but not all) are loaded.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Give the container height so that the whole tree is rendered. -->
|
||||
<div id="container" style="height: 900px;"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true }, [
|
||||
makeTestDominatorTreeNode({}),
|
||||
]);
|
||||
ok(root.children);
|
||||
ok(root.moreChildrenAvailable);
|
||||
|
||||
const expanded = new Set();
|
||||
expanded.add(root.nodeId);
|
||||
|
||||
yield renderComponent(DominatorTreeComponent(immutableUpdate(TEST_DOMINATOR_TREE_PROPS, {
|
||||
dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, {
|
||||
expanded,
|
||||
root,
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(".more-children"),
|
||||
"Should get a link to load more children");
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that expanded DominatorTreeItems are correctly rendered and updated
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Give the container height so that the whole tree is rendered. -->
|
||||
<div id="container" style="height: 900px;"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
// simple tree with one root and one child
|
||||
const root = makeTestDominatorTreeNode(
|
||||
{ moreChildrenAvailable: false },
|
||||
[
|
||||
makeTestDominatorTreeNode({ moreChildrenAvailable: false }),
|
||||
]);
|
||||
ok(root.children);
|
||||
|
||||
// root node is expanded
|
||||
const expanded = new Set();
|
||||
expanded.add(root.nodeId);
|
||||
|
||||
let component = yield renderComponent(
|
||||
DominatorTreeComponent(immutableUpdate(
|
||||
TEST_DOMINATOR_TREE_PROPS,
|
||||
{
|
||||
dominatorTree: immutableUpdate(
|
||||
TEST_DOMINATOR_TREE_PROPS.dominatorTree,
|
||||
{ expanded, root }
|
||||
),
|
||||
})), container);
|
||||
ok(true, "Dominator tree rendered");
|
||||
|
||||
is(container.querySelectorAll(".tree-node").length, 2,
|
||||
"Should display two rows");
|
||||
is(container.querySelectorAll(".arrow.open").length, 1,
|
||||
"Should display one expanded arrow");
|
||||
|
||||
yield setProps(component, immutableUpdate(
|
||||
TEST_DOMINATOR_TREE_PROPS,
|
||||
{
|
||||
dominatorTree: immutableUpdate(
|
||||
TEST_DOMINATOR_TREE_PROPS.dominatorTree,
|
||||
{ expanded: new Set(), root }
|
||||
)
|
||||
}));
|
||||
ok(true, "Dominator tree props updated to collapse all nodes");
|
||||
|
||||
is(container.querySelectorAll(".tree-node").length, 1,
|
||||
"Should display only one row");
|
||||
is(container.querySelectorAll(".arrow.open").length, 0,
|
||||
"Should display no expanded arrow");
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
50
devtools/client/memory/test/chrome/test_Heap_01.html
Normal file
50
devtools/client/memory/test/chrome/test_Heap_01.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that rendering a dominator tree error is handled correctly.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
ok(React, "Should get React");
|
||||
ok(Heap, "Should get Heap");
|
||||
|
||||
const errorMessage = "Something went wrong!";
|
||||
const container = document.getElementById("container");
|
||||
|
||||
const props = immutableUpdate(TEST_HEAP_PROPS, {
|
||||
view: { state: viewState.DOMINATOR_TREE, },
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
dominatorTree: {
|
||||
error: new Error(errorMessage),
|
||||
state: dominatorTreeState.ERROR,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
yield renderComponent(Heap(props), container);
|
||||
|
||||
ok(container.querySelector(".error"), "Should render an error view");
|
||||
ok(container.textContent.indexOf(errorMessage) !== -1,
|
||||
"Should see our error message");
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
78
devtools/client/memory/test/chrome/test_Heap_02.html
Normal file
78
devtools/client/memory/test/chrome/test_Heap_02.html
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that the currently selected view is rendered.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
ok(React, "Should get React");
|
||||
ok(Heap, "Should get Heap");
|
||||
|
||||
const errorMessage = "Something went wrong!";
|
||||
const container = document.getElementById("container");
|
||||
|
||||
// Dominator tree view.
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
view: { state: viewState.DOMINATOR_TREE, },
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(`[data-state=${dominatorTreeState.LOADED}]`),
|
||||
"Should render the dominator tree.");
|
||||
|
||||
// Census view.
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
view: { state: viewState.CENSUS, },
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(`[data-state=${censusState.SAVED}]`),
|
||||
"Should render the census.");
|
||||
|
||||
// Diffing view.
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
view: { state: viewState.DIFFING, },
|
||||
snapshot: null,
|
||||
diffing: {
|
||||
firstSnapshotId: null,
|
||||
secondSnapshotId: null,
|
||||
census: null,
|
||||
error: null,
|
||||
state: diffingState.SELECTING,
|
||||
},
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(`[data-state=${diffingState.SELECTING}]`),
|
||||
"Should render the diffing.");
|
||||
|
||||
// Initial view.
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
snapshot: null,
|
||||
diffing: null,
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector("[data-state=initial]"),
|
||||
"With no snapshot, nor a diffing, should render initial prompt.");
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
74
devtools/client/memory/test/chrome/test_Heap_03.html
Normal file
74
devtools/client/memory/test/chrome/test_Heap_03.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that we show a throbber while computing and fetching dominator trees,
|
||||
but not in other dominator tree states.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
for (let state of [dominatorTreeState.COMPUTING, dominatorTreeState.FETCHING]) {
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
view: { state: viewState.DOMINATOR_TREE, },
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
|
||||
state,
|
||||
root: null,
|
||||
dominatorTreeId: state === dominatorTreeState.FETCHING ? 1 : null,
|
||||
}),
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(".devtools-throbber"),
|
||||
`Should show a throbber for state = ${state}`);
|
||||
}
|
||||
|
||||
for (let state of [dominatorTreeState.LOADED, dominatorTreeState.INCREMENTAL_FETCHING]) {
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
view: { state: viewState.DOMINATOR_TREE, },
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, {
|
||||
state,
|
||||
activeFetchRequestCount: state === dominatorTreeState.INCREMENTAL_FETCHING ? 1 : undefined,
|
||||
}),
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(!container.querySelector(".devtools-throbber"),
|
||||
`Should not show a throbber for state = ${state}`);
|
||||
}
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
view: { state: viewState.DOMINATOR_TREE, },
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
dominatorTree: {
|
||||
state: dominatorTreeState.ERROR,
|
||||
error: new Error("example error for testing"),
|
||||
},
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(!container.querySelector(".devtools-throbber"),
|
||||
`Should not show a throbber for ERROR state`);
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
121
devtools/client/memory/test/chrome/test_Heap_04.html
Normal file
121
devtools/client/memory/test/chrome/test_Heap_04.html
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that we show the "hey you're not recording allocation stacks" message at the appropriate times.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
|
||||
report: {
|
||||
bytes: 1,
|
||||
totalBytes: 1,
|
||||
count: 1,
|
||||
totalCount: 1,
|
||||
id: 1,
|
||||
parent: undefined,
|
||||
children: [
|
||||
{
|
||||
name: "noStack",
|
||||
bytes: 1,
|
||||
totalBytes: 1,
|
||||
count: 1,
|
||||
totalCount: 1,
|
||||
children: undefined,
|
||||
id: 3,
|
||||
parent: 1,
|
||||
}
|
||||
]
|
||||
},
|
||||
display: censusDisplays.allocationStack,
|
||||
}),
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(".no-allocation-stacks"),
|
||||
"When there are no allocation stacks, we should show the message");
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
|
||||
report: {
|
||||
bytes: 1,
|
||||
totalBytes: 1,
|
||||
count: 1,
|
||||
totalCount: 1,
|
||||
id: 1,
|
||||
parent: undefined,
|
||||
children: [
|
||||
{
|
||||
name: Cu.getJSTestingFunctions().saveStack(),
|
||||
bytes: 1,
|
||||
totalBytes: 1,
|
||||
count: 1,
|
||||
totalCount: 1,
|
||||
children: undefined,
|
||||
id: 2,
|
||||
parent: 1,
|
||||
},
|
||||
{
|
||||
name: "noStack",
|
||||
bytes: 1,
|
||||
totalBytes: 1,
|
||||
count: 1,
|
||||
totalCount: 1,
|
||||
children: undefined,
|
||||
id: 3,
|
||||
parent: 1,
|
||||
}
|
||||
]
|
||||
},
|
||||
display: censusDisplays.allocationStack,
|
||||
}),
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(!container.querySelector(".no-allocation-stacks"),
|
||||
"When there are allocation stacks, we should not show the message");
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
|
||||
report: {
|
||||
bytes: 1,
|
||||
totalBytes: 1,
|
||||
count: 1,
|
||||
totalCount: 1,
|
||||
id: 1,
|
||||
parent: undefined,
|
||||
children: undefined
|
||||
},
|
||||
display: censusDisplays.allocationStack,
|
||||
}),
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(!container.querySelector(".no-allocation-stacks"),
|
||||
"When there isn't census data, we should not show the message");
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
132
devtools/client/memory/test/chrome/test_Heap_05.html
Normal file
132
devtools/client/memory/test/chrome/test_Heap_05.html
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that we show a message when the census results are empty.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, {
|
||||
report: {
|
||||
bytes: 1,
|
||||
totalBytes: 1,
|
||||
count: 1,
|
||||
totalCount: 1,
|
||||
id: 1,
|
||||
parent: undefined,
|
||||
children: [
|
||||
{
|
||||
name: Cu.getJSTestingFunctions().saveStack(),
|
||||
bytes: 1,
|
||||
totalBytes: 1,
|
||||
count: 1,
|
||||
totalCount: 1,
|
||||
children: undefined,
|
||||
id: 2,
|
||||
parent: 1,
|
||||
},
|
||||
{
|
||||
name: "noStack",
|
||||
bytes: 1,
|
||||
totalBytes: 1,
|
||||
count: 1,
|
||||
totalCount: 1,
|
||||
children: undefined,
|
||||
id: 3,
|
||||
parent: 1,
|
||||
}
|
||||
]
|
||||
},
|
||||
display: censusDisplays.allocationStack,
|
||||
}),
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(!container.querySelector(".empty"),
|
||||
"When the report is not empty, we should not show the empty message");
|
||||
|
||||
// Empty Census Report
|
||||
|
||||
const emptyCensus = {
|
||||
report: {
|
||||
bytes: 0,
|
||||
totalBytes: 0,
|
||||
count: 0,
|
||||
totalCount: 0,
|
||||
id: 1,
|
||||
parent: undefined,
|
||||
children: undefined,
|
||||
},
|
||||
parentMap: Object.create(null),
|
||||
display: censusDisplays.allocationStack,
|
||||
filter: null,
|
||||
expanded: new Immutable.Set(),
|
||||
focused: null,
|
||||
state: censusState.SAVED,
|
||||
};
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, emptyCensus),
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(".empty"),
|
||||
"When the report is empty in census view, we show the empty message");
|
||||
ok(container.textContent.indexOf(L10N.getStr("heapview.empty")) >= 0);
|
||||
|
||||
// Empty Diffing Report
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
view: { state: viewState.DIFFING, },
|
||||
diffing: {
|
||||
firstSnapshotId: 1,
|
||||
secondSnapshotId: 2,
|
||||
census: emptyCensus,
|
||||
state: diffingState.TOOK_DIFF,
|
||||
},
|
||||
snapshot: null,
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(".empty"),
|
||||
"When the report is empty in diffing view, the empty message is shown");
|
||||
ok(container.textContent.indexOf(L10N.getStr("heapview.no-difference")) >= 0);
|
||||
|
||||
// Empty Filtered Census
|
||||
|
||||
yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
|
||||
snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, {
|
||||
census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, immutableUpdate(emptyCensus, {
|
||||
filter: "zzzz"
|
||||
})),
|
||||
}),
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector(".empty"),
|
||||
"When the report is empty in census view w/ filter, we show the empty message");
|
||||
ok(container.textContent.indexOf(L10N.getStr("heapview.none-match")) >= 0);
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
74
devtools/client/memory/test/chrome/test_List_01.html
Normal file
74
devtools/client/memory/test/chrome/test_List_01.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test to verify the delete button calls the onDelete handler for an item
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
let deletedSnapshots = [];
|
||||
|
||||
let snapshots = [ TEST_SNAPSHOT, TEST_SNAPSHOT, TEST_SNAPSHOT ]
|
||||
.map((snapshot, index) => immutableUpdate(snapshot, {
|
||||
index: snapshot.index + index
|
||||
}));
|
||||
|
||||
yield renderComponent(
|
||||
List({
|
||||
itemComponent: SnapshotListItem,
|
||||
onClick: noop,
|
||||
onDelete: (item) => deletedSnapshots.push(item),
|
||||
items: snapshots
|
||||
}),
|
||||
container
|
||||
);
|
||||
|
||||
let deleteButtons = container.querySelectorAll('.delete');
|
||||
|
||||
is(container.querySelectorAll('.snapshot-list-item').length, 3,
|
||||
"There are 3 list items\n");
|
||||
is(deletedSnapshots.length, 0,
|
||||
"Not snapshots have been deleted\n");
|
||||
|
||||
deleteButtons[1].click();
|
||||
|
||||
is(deletedSnapshots.length, 1, "One snapshot was deleted\n");
|
||||
is(deletedSnapshots[0], snapshots[1],
|
||||
"Deleted snapshot was added to the deleted list\n");
|
||||
|
||||
deleteButtons[0].click();
|
||||
|
||||
is(deletedSnapshots.length, 2, "Two snapshots were deleted\n");
|
||||
is(deletedSnapshots[1], snapshots[0],
|
||||
"Deleted snapshot was added to the deleted list\n");
|
||||
|
||||
deleteButtons[2].click();
|
||||
|
||||
is(deletedSnapshots.length, 3, "Three snapshots were deleted\n");
|
||||
is(deletedSnapshots[2], snapshots[2],
|
||||
"Deleted snapshot was added to the deleted list\n");
|
||||
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
112
devtools/client/memory/test/chrome/test_ShortestPaths_01.html
Normal file
112
devtools/client/memory/test/chrome/test_ShortestPaths_01.html
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that the ShortestPaths component properly renders a graph of the merged shortest paths.
|
||||
-->
|
||||
<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>
|
||||
<script type="application/javascript"
|
||||
src="chrome://devtools/content/shared/vendor/dagre-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(ShortestPaths(TEST_SHORTEST_PATHS_PROPS), container);
|
||||
|
||||
let found1 = false;
|
||||
let found2 = false;
|
||||
let found3 = false;
|
||||
|
||||
let found1to2 = false;
|
||||
let found1to3 = false;
|
||||
let found2to3 = false;
|
||||
|
||||
const tspans = [...container.querySelectorAll("tspan")];
|
||||
for (let el of tspans) {
|
||||
const text = el.textContent.trim();
|
||||
dumpn("tspan's text = " + text);
|
||||
|
||||
switch (text) {
|
||||
// Nodes
|
||||
|
||||
case "other › SomeType @ 0x1": {
|
||||
ok(!found1, "Should only find node 1 once");
|
||||
found1 = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case "other › SomeType @ 0x2": {
|
||||
ok(!found2, "Should only find node 2 once");
|
||||
found2 = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case "other › SomeType @ 0x3": {
|
||||
ok(!found3, "Should only find node 3 once");
|
||||
found3 = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Edges
|
||||
|
||||
case "1->2": {
|
||||
ok(!found1to2, "Should only find edge 1->2 once");
|
||||
found1to2 = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case "1->3": {
|
||||
ok(!found1to3, "Should only find edge 1->3 once");
|
||||
found1to3 = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case "2->3": {
|
||||
ok(!found2to3, "Should only find edge 2->3 once");
|
||||
found2to3 = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Unexpected
|
||||
|
||||
default: {
|
||||
ok(false, `Unexpected tspan: ${text}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ok(found1, "Should have rendered node 1");
|
||||
ok(found2, "Should have rendered node 2");
|
||||
ok(found3, "Should have rendered node 3");
|
||||
|
||||
ok(found1to2, "Should have rendered edge 1->2");
|
||||
ok(found1to3, "Should have rendered edge 1->3");
|
||||
ok(found2to3, "Should have rendered edge 2->3");
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that the ShortestPaths component renders a suggestion to select a node when there is no graph.
|
||||
-->
|
||||
<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>
|
||||
<script type="application/javascript"
|
||||
src="chrome://devtools/content/shared/vendor/dagre-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(ShortestPaths(immutableUpdate(TEST_SHORTEST_PATHS_PROPS,
|
||||
{ graph: null })),
|
||||
container);
|
||||
|
||||
ok(container.textContent.indexOf(L10N.getStr("shortest-paths.select-node")) !== -1,
|
||||
"The node selection prompt is displayed");
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test to verify that the delete button only shows up for a snapshot when it has a
|
||||
path.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
yield renderComponent(
|
||||
SnapshotListItem(TEST_SNAPSHOT_LIST_ITEM_PROPS),
|
||||
container
|
||||
);
|
||||
|
||||
ok(container.querySelector('.delete'),
|
||||
"Should have delete button when there is a path");
|
||||
|
||||
const pathlessProps = immutableUpdate(
|
||||
TEST_SNAPSHOT_LIST_ITEM_PROPS,
|
||||
{item: immutableUpdate(TEST_SNAPSHOT, {path: null})}
|
||||
);
|
||||
|
||||
yield renderComponent(
|
||||
SnapshotListItem(pathlessProps),
|
||||
container
|
||||
);
|
||||
|
||||
ok(!container.querySelector('.delete'),
|
||||
"No delete button should be found if there is no path\n");
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
47
devtools/client/memory/test/chrome/test_Toolbar_01.html
Normal file
47
devtools/client/memory/test/chrome/test_Toolbar_01.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that the Toolbar component shows the view switcher only at the appropriate times.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
// Census and dominator tree views.
|
||||
|
||||
for (let view of [viewState.CENSUS, viewState.DOMINATOR_TREE]) {
|
||||
yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
|
||||
view: { state: view },
|
||||
})), container);
|
||||
|
||||
ok(container.querySelector("#select-view"),
|
||||
`The view selector is shown in view = ${view}`);
|
||||
}
|
||||
|
||||
yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, {
|
||||
view: { state: viewState.DIFFING, },
|
||||
})), container);
|
||||
|
||||
ok(!container.querySelector("#select-view"),
|
||||
"The view selector is NOT shown in the DIFFING view");
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
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>
|
||||
6
devtools/client/memory/test/unit/.eslintrc.js
Normal file
6
devtools/client/memory/test/unit/.eslintrc.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for mochitests.
|
||||
"extends": "../../../../.eslintrc.xpcshell.js"
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue