mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Bug 1215954 - Add feature to save a heap snapshot from memory tool to
disk. r=fitzgen,vp
This commit is contained in:
parent
aa32ce7f03
commit
cb4eb14f47
@ -24,6 +24,18 @@ memory.panelLabel=Memory Panel
|
||||
# displayed inside the developer tools window.
|
||||
memory.tooltip=Memory
|
||||
|
||||
# LOCALIZATION NOTE (snapshot.io.save): The label for the link that saves a snapshot
|
||||
# to disk.
|
||||
snapshot.io.save=Save
|
||||
|
||||
# LOCALIZATION NOTE (snapshot.io.save.window): The title for the window displayed when
|
||||
# saving a snapshot to disk.
|
||||
snapshot.io.save.window=Save Heap Snapshot
|
||||
|
||||
# LOCALIZATION NOTE (snapshot.io.filter): The title for the filter used to
|
||||
# filter file types (*.fxsnapshot)
|
||||
snapshot.io.filter=Firefox Heap Snapshots
|
||||
|
||||
# LOCALIZATION NOTE (aggregate.mb): The label annotating the number of bytes (in megabytes)
|
||||
# in a snapshot. %S represents the value, rounded to 2 decimal points.
|
||||
aggregate.mb=%S MB
|
||||
|
45
devtools/client/memory/actions/io.js
Normal file
45
devtools/client/memory/actions/io.js
Normal file
@ -0,0 +1,45 @@
|
||||
/* 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 { snapshotState: states, actions } = require("../constants");
|
||||
const { L10N, openFilePicker } = require("../utils");
|
||||
const { OS } = require("resource://gre/modules/osfile.jsm");
|
||||
const VALID_EXPORT_STATES = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
|
||||
|
||||
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"]]
|
||||
});
|
||||
|
||||
if (!outputFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield dispatch(exportSnapshot(snapshot, outputFile.path));
|
||||
};
|
||||
};
|
||||
|
||||
const exportSnapshot = exports.exportSnapshot = function (snapshot, dest) {
|
||||
return function* (dispatch, getState) {
|
||||
|
||||
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 });
|
||||
};
|
||||
};
|
@ -8,5 +8,6 @@ DevToolsModules(
|
||||
'breakdown.js',
|
||||
'filter.js',
|
||||
'inverted.js',
|
||||
'io.js',
|
||||
'snapshot.js',
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ const { toggleRecordingAllocationStacks } = require("./actions/allocations");
|
||||
const { setBreakdownAndRefresh } = require("./actions/breakdown");
|
||||
const { toggleInvertedAndRefresh } = require("./actions/inverted");
|
||||
const { setFilterStringAndRefresh } = require("./actions/filter");
|
||||
const { pickFileAndExportSnapshot } = require("./actions/io");
|
||||
const { selectSnapshotAndRefresh, takeSnapshotAndCensus } = require("./actions/snapshot");
|
||||
const { breakdownNameToSpec, getBreakdownDisplayData } = require("./utils");
|
||||
const Toolbar = createFactory(require("./components/toolbar"));
|
||||
@ -78,7 +79,8 @@ const App = createClass({
|
||||
List({
|
||||
itemComponent: SnapshotListItem,
|
||||
items: snapshots,
|
||||
onClick: snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot))
|
||||
onClick: snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot)),
|
||||
onSave: snapshot => dispatch(pickFileAndExportSnapshot(snapshot))
|
||||
}),
|
||||
|
||||
HeapView({
|
||||
|
@ -23,9 +23,9 @@ const List = module.exports = createClass({
|
||||
|
||||
return (
|
||||
dom.ul({ className: "list" }, ...items.map((item, index) => {
|
||||
return Item({
|
||||
return Item(Object.assign({}, this.props, {
|
||||
key: index, item, index, onClick: () => onClick(item),
|
||||
});
|
||||
}));
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
@ -11,13 +11,14 @@ const SnapshotListItem = module.exports = createClass({
|
||||
displayName: "snapshot-list-item",
|
||||
|
||||
propTypes: {
|
||||
onClick: PropTypes.func,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
item: snapshotModel.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
let { index, item: snapshot, onClick } = this.props;
|
||||
let { index, item: snapshot, onClick, onSave } = this.props;
|
||||
let className = `snapshot-list-item ${snapshot.selected ? " selected" : ""}`;
|
||||
let statusText = getSnapshotStatusText(snapshot);
|
||||
let title = getSnapshotTitle(snapshot);
|
||||
@ -34,12 +35,20 @@ const SnapshotListItem = module.exports = createClass({
|
||||
details = dom.span({ className: "snapshot-state" }, statusText);
|
||||
}
|
||||
|
||||
let saveLink = !snapshot.path ? void 0 : dom.a({
|
||||
onClick: () => onSave(snapshot),
|
||||
className: "save",
|
||||
}, L10N.getFormatStr("snapshot.io.save"));
|
||||
|
||||
return (
|
||||
dom.li({ className, onClick },
|
||||
dom.span({
|
||||
className: `snapshot-title ${statusText ? " devtools-throbber" : ""}`
|
||||
}, title),
|
||||
details
|
||||
dom.div({ className: "snapshot-info" },
|
||||
details,
|
||||
saveLink
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -23,6 +23,12 @@ actions.TAKE_CENSUS_END = "take-census-end";
|
||||
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";
|
||||
|
||||
// Fired by UI to select a snapshot to view.
|
||||
actions.SELECT_SNAPSHOT = "select-snapshot";
|
||||
|
||||
|
@ -8,6 +8,20 @@ const reducers = require("./reducers");
|
||||
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
|
||||
module.exports = function () {
|
||||
let shouldLog = DevToolsUtils.testing;
|
||||
return createStore({ log: shouldLog })(combineReducers(reducers), {});
|
||||
let shouldLog = false;
|
||||
let history;
|
||||
|
||||
// If testing, store the action history in an array
|
||||
// we'll later attach to the store
|
||||
if (DevToolsUtils.testing) {
|
||||
history = [];
|
||||
}
|
||||
|
||||
let store = createStore({ log: shouldLog, history })(combineReducers(reducers), {});
|
||||
|
||||
if (history) {
|
||||
store.history = history;
|
||||
}
|
||||
|
||||
return store;
|
||||
};
|
||||
|
@ -8,6 +8,8 @@ var { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
|
||||
var { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
|
||||
var { console } = Cu.import("resource://gre/modules/Console.jsm", {});
|
||||
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
var { OS } = require("resource://gre/modules/osfile.jsm");
|
||||
var { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
|
||||
var { TargetFactory } = require("devtools/client/framework/target");
|
||||
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
var promise = require("promise");
|
||||
@ -70,6 +72,25 @@ function waitUntilState (store, predicate) {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function waitUntilAction (store, actionType) {
|
||||
let deferred = promise.defer();
|
||||
let unsubscribe = store.subscribe(check);
|
||||
let history = store.history;
|
||||
let index = history.length;
|
||||
|
||||
do_print(`Waiting for action "${actionType}"`);
|
||||
function check () {
|
||||
let action = history[index++];
|
||||
if (action && action.type === actionType) {
|
||||
do_print(`Found action "${actionType}"`);
|
||||
unsubscribe();
|
||||
deferred.resolve(store.getState());
|
||||
}
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function waitUntilSnapshotState (store, expected) {
|
||||
let predicate = () => {
|
||||
let snapshots = store.getState().snapshots;
|
||||
|
@ -0,0 +1,43 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test exporting a snapshot to a user specified location on disk.
|
||||
|
||||
let { exportSnapshot } = require("devtools/client/memory/actions/io");
|
||||
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
let { snapshotState: states, actions } = require("devtools/client/memory/constants");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function *() {
|
||||
let front = new StubbedMemoryFront();
|
||||
let heapWorker = new HeapAnalysesClient();
|
||||
yield front.attach();
|
||||
let store = Store();
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
let file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]);
|
||||
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
let destPath = file.path;
|
||||
let stat = yield OS.File.stat(destPath);
|
||||
ok(stat.size === 0, "new file is 0 bytes at start");
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
|
||||
let exportEvents = Promise.all([
|
||||
waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
|
||||
waitUntilAction(store, actions.EXPORT_SNAPSHOT_END)
|
||||
]);
|
||||
dispatch(exportSnapshot(getState().snapshots[0], destPath));
|
||||
yield exportEvents;
|
||||
|
||||
stat = yield OS.File.stat(destPath);
|
||||
do_print(stat.size);
|
||||
ok(stat.size > 0, "destination file is more than 0 bytes");
|
||||
|
||||
heapWorker.destroy();
|
||||
yield front.detach();
|
||||
});
|
@ -35,7 +35,7 @@ add_task(function *() {
|
||||
|
||||
let s1 = utils.createSnapshot();
|
||||
let s2 = utils.createSnapshot();
|
||||
ok(s1.state, states.SAVING, "utils.createSnapshot() creates snapshot in saving state");
|
||||
equal(s1.state, states.SAVING, "utils.createSnapshot() creates snapshot in saving state");
|
||||
ok(s1.id !== s2.id, "utils.createSnapshot() creates snapshot with unique ids");
|
||||
|
||||
ok(utils.breakdownEquals(utils.breakdownNameToSpec("coarseType"), breakdowns.coarseType.breakdown),
|
||||
|
@ -5,6 +5,7 @@ tail =
|
||||
firefox-appdir = browser
|
||||
skip-if = toolkit == 'android' || toolkit == 'gonk'
|
||||
|
||||
[test_action-export-snapshot.js]
|
||||
[test_action-filter-01.js]
|
||||
[test_action-filter-02.js]
|
||||
[test_action-filter-03.js]
|
||||
|
@ -2,7 +2,7 @@
|
||||
* 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 { Cu } = require("chrome");
|
||||
const { Cu, Cc, Ci } = require("chrome");
|
||||
|
||||
Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
|
||||
const STRINGS_URI = "chrome://devtools/locale/memory.properties"
|
||||
@ -314,3 +314,40 @@ exports.parseSource = function (source) {
|
||||
|
||||
return { short, long, host };
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes some configurations and opens up a file picker and returns
|
||||
* a promise to the chosen file if successful.
|
||||
*
|
||||
* @param {String} .title
|
||||
* The title displayed in the file picker window.
|
||||
* @param {Array<Array<String>>} .filters
|
||||
* An array of filters to display in the file picker. Each filter in the array
|
||||
* is a duple of two strings, one a name for the filter, and one the filter itself
|
||||
* (like "*.json").
|
||||
* @param {String} .defaultName
|
||||
* The default name chosen by the file picker window.
|
||||
* @return {Promise<?nsILocalFile>}
|
||||
* The file selected by the user, or null, if cancelled.
|
||||
*/
|
||||
exports.openFilePicker = function({ title, filters, defaultName }) {
|
||||
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
||||
fp.init(window, title, Ci.nsIFilePicker.modeSave);
|
||||
|
||||
for (let filter of (filters || [])) {
|
||||
fp.appendFilter(filter[0], filter[1]);
|
||||
}
|
||||
fp.defaultString = defaultName;
|
||||
|
||||
return new Promise(resolve => {
|
||||
fp.open({
|
||||
done: result => {
|
||||
if (result === Ci.nsIFilePicker.returnCancel) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve(fp.file);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -9,14 +9,17 @@ const { waitUntilService } = require("./middleware/wait-service");
|
||||
const { task } = require("./middleware/task");
|
||||
const { log } = require("./middleware/log");
|
||||
const { promise } = require("./middleware/promise");
|
||||
const { history } = require("./middleware/history");
|
||||
|
||||
/**
|
||||
* This creates a dispatcher with all the standard middleware in place
|
||||
* that all code requires. It can also be optionally configured in
|
||||
* various ways, such as logging and recording.
|
||||
*
|
||||
* @param {object} opts - boolean configuration flags
|
||||
* @param {object} opts:
|
||||
* - log: log all dispatched actions to console
|
||||
* - history: an array to store every action in. Should only be
|
||||
* used in tests.
|
||||
* - middleware: array of middleware to be included in the redux store
|
||||
*/
|
||||
module.exports = (opts={}) => {
|
||||
@ -27,13 +30,17 @@ module.exports = (opts={}) => {
|
||||
promise,
|
||||
];
|
||||
|
||||
if (opts.log) {
|
||||
middleware.push(log);
|
||||
if (opts.history) {
|
||||
middleware.push(history(opts.history));
|
||||
}
|
||||
|
||||
if (opts.middleware) {
|
||||
opts.middleware.forEach(fn => middleware.push(fn));
|
||||
}
|
||||
|
||||
if (opts.log) {
|
||||
middleware.push(log);
|
||||
}
|
||||
|
||||
return applyMiddleware(...middleware)(createStore);
|
||||
}
|
||||
|
23
devtools/client/shared/redux/middleware/history.js
Normal file
23
devtools/client/shared/redux/middleware/history.js
Normal file
@ -0,0 +1,23 @@
|
||||
/* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
|
||||
/**
|
||||
* A middleware that stores every action coming through the store in the passed
|
||||
* in logging object. Should only be used for tests, as it collects all
|
||||
* action information, which will cause memory bloat.
|
||||
*/
|
||||
exports.history = (log=[]) => ({ dispatch, getState }) => {
|
||||
if (!DevToolsUtils.testing) {
|
||||
console.warn(`Using history middleware stores all actions in state for testing\
|
||||
and devtools is not currently running in test mode. Be sure this is\
|
||||
intentional.`);
|
||||
}
|
||||
return next => action => {
|
||||
log.push(action);
|
||||
next(action);
|
||||
};
|
||||
};
|
@ -5,6 +5,7 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'history.js',
|
||||
'log.js',
|
||||
'promise.js',
|
||||
'task.js',
|
||||
|
@ -153,7 +153,6 @@ html, body, #app, #memory-tool {
|
||||
color: var(--theme-body-color);
|
||||
border-bottom: 1px solid rgba(128,128,128,0.15);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.snapshot-list-item.selected {
|
||||
@ -161,6 +160,17 @@ html, body, #app, #memory-tool {
|
||||
color: var(--theme-selection-color);
|
||||
}
|
||||
|
||||
.snapshot-list-item .snapshot-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.snapshot-list-item .save {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.snapshot-list-item > .snapshot-title {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user