Bug 1201949 - Initial redux-style controllers for front end memory tool heap snapshot. r=jlongster

This commit is contained in:
Jordan Santell 2015-09-25 20:09:58 -07:00
parent e5c975bf46
commit 72dea1ba99
23 changed files with 339 additions and 43 deletions

View File

@ -111,7 +111,7 @@ devtools.jar:
content/performance/views/optimizations-list.js (performance/views/optimizations-list.js)
content/performance/views/recordings.js (performance/views/recordings.js)
content/memory/memory.xhtml (memory/memory.xhtml)
content/memory/controller.js (memory/controller.js)
content/memory/initializer.js (memory/initializer.js)
content/promisedebugger/promise-controller.js (promisedebugger/promise-controller.js)
content/promisedebugger/promise-panel.js (promisedebugger/promise-panel.js)
content/promisedebugger/promise-debugger.xhtml (promisedebugger/promise-debugger.xhtml)

View File

@ -0,0 +1,8 @@
# 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(
'snapshot.js',
)

View 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 { PROMISE } = require("devtools/client/shared/redux/middleware/promise");
const { actions } = require("../constants");
const takeSnapshot = exports.takeSnapshot = function takeSnapshot (front) {
return {
type: actions.TAKE_SNAPSHOT,
[PROMISE]: front.saveHeapSnapshot()
};
};

View File

@ -0,0 +1,9 @@
/* 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 = exports.actions = {};
// Fired by UI to request a snapshot from the actor.
actions.TAKE_SNAPSHOT = "take-snapshot";

View File

@ -3,26 +3,26 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
const { loader, require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
const { Task } = require("resource://gre/modules/Task.jsm");
const { Heritage, ViewHelpers, WidgetMethods } = require("resource:///modules/devtools/client/shared/widgets/ViewHelpers.jsm");
const Store = require("./store");
/**
* The current target, toolbox and MemoryFront, set by this tool's host.
*/
var gToolbox, gTarget, gFront;
/**
* Initializes the profiler controller and views.
*/
const MemoryController = {
initialize: Task.async(function *() {
yield gFront.attach();
}),
const REDUX_METHODS_TO_PIPE = ["dispatch", "subscribe", "getState"];
destroy: Task.async(function *() {
yield gFront.detach();
})
const MemoryController = exports.MemoryController = function ({ toolbox, target, front }) {
this.store = Store();
this.toolbox = toolbox;
this.target = target;
this.front = front;
};
REDUX_METHODS_TO_PIPE.map(m =>
MemoryController.prototype[m] = function (...args) { return this.store[m](...args); });
MemoryController.prototype.destroy = function () {
this.store = this.toolbox = this.target = this.front = null;
};

View File

@ -0,0 +1,30 @@
/* 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 { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { Task } = require("resource://gre/modules/Task.jsm");
const { MemoryController } = require("devtools/client/memory/controller");
/**
* The current target, toolbox and MemoryFront, set by this tool's host.
*/
let gToolbox, gTarget, gFront;
/**
* Initializes the profiler controller and views.
*/
var controller = null;
function initialize () {
return Task.spawn(function *() {
controller = new MemoryController({ toolbox: gToolbox, target: gTarget, front: gFront });
});
}
function destroy () {
return Task.spawn(function *() {
controller.destroy();
});
}

View File

@ -12,23 +12,21 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<link rel="stylesheet" href="chrome://browser/skin/" type="text/css"/>
<link rel="stylesheet" href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"/>
<link rel="stylesheet" href="chrome://devtools/skin/themes/common.css" type="text/css"/>
<link rel="stylesheet" href="chrome://devtools/skin/themes/widgets.css" type="text/css"/>
<link rel="stylesheet" href="chrome://devtools/skin/themes/memory.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/content/devtools/widgets.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/devtools/widgets.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/devtools/memory.css" type="text/css"/>
<script type="application/javascript;version=1.8"
src="chrome://devtools/content/shared/theme-switching.js"></script>
src="chrome://devtools/content/shared/theme-switching.js"/>
<script type="application/javascript;version=1.8"
src="controller.js"></script>
src="initializer.js"></script>
</head>
<body class="theme-body">
<toolbar class="devtools-toolbar">
<toolbarbutton id="snapshot-button" class="devtools-toolbarbutton"
tabindex="0"/>
<spacer flex="1"></spacer>
</toolbar>
<splitter class="devtools-horizontal-splitter"/>
<div class="devtools-toolbar">
<div id="snapshot-button" class="devtools-toolbarbutton" />
</div>
<div class="devtools-horizontal-splitter"></div>
<div id="memory-content"
class="devtools-responsive-container"
flex="1">

View File

@ -9,7 +9,6 @@
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
const { L10N } = require("devtools/client/performance/modules/global");
const { Heritage } = require("resource:///modules/devtools/client/shared/widgets/ViewHelpers.jsm");
const { AbstractTreeItem } = require("resource:///modules/devtools/client/shared/widgets/AbstractTreeItem.jsm");

View File

@ -4,12 +4,19 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
'actions',
'modules',
'reducers',
]
DevToolsModules(
'constants.js',
'controller.js',
'initializer.js',
'panel.js',
'reducers.js',
'store.js',
)
MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']

View File

@ -32,8 +32,8 @@ MemoryPanel.prototype = {
this.target.form,
rootForm);
console.log(this.panelWin, this.panelWin.MemoryController);
this._opening = this.panelWin.MemoryController.initialize().then(() => {
yield this.panelWin.gFront.attach();
return this._opening = this.panelWin.initialize().then(() => {
this.isReady = true;
this.emit("ready");
return this;
@ -47,21 +47,21 @@ MemoryPanel.prototype = {
return this._toolbox.target;
},
destroy: function () {
destroy: Task.async(function *() {
// Make sure this panel is not already destroyed.
if (this._destroyer) {
return this._destroyer;
}
this._destroyer = this.panelWin.MemoryController.destroy().then(() => {
yield this.panelWin.gFront.detach();
return this._destroyer = this.panelWin.destroy().then(() => {
// Destroy front to ensure packet handler is removed from client
this.panelWin.gFront.destroy();
this.panelWin = null;
this.emit("destroyed");
return this;
});
return this._destroyer;
}
})
};
exports.MemoryPanel = MemoryPanel;

View File

@ -0,0 +1 @@
exports.snapshots = require("./reducers/snapshot");

View File

@ -0,0 +1,8 @@
# 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(
'snapshot.js',
)

View File

@ -0,0 +1,37 @@
const { actions } = require("../constants");
const { PROMISE } = require("devtools/client/shared/redux/middleware/promise");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
function handleTakeSnapshot (state, action) {
switch (action.status) {
case "start":
return [...state, {
id: action.seqId,
status: action.status
}];
case "done":
let snapshot = state.find(s => s.id === action.seqId);
if (!snapshot) {
DevToolsUtils.reportException(`No snapshot with id "${action.seqId}" for TAKE_SNAPSHOT`);
break;
}
snapshot.status = "done";
snapshot.snapshotId = action.value;
return [...state];
case "error":
DevToolsUtils.reportException(`No async state found for ${action.type}`);
}
return [...state];
}
module.exports = function (state=[], action) {
switch (action.type) {
case actions.TAKE_SNAPSHOT:
return handleTakeSnapshot(state, action);
}
return state;
};

View File

@ -0,0 +1,8 @@
const { combineReducers } = require("../shared/vendor/redux");
const createStore = require("../shared/redux/create-store");
const reducers = require("./reducers");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
module.exports = function () {
return createStore({ log: DevToolsUtils.testing })(combineReducers(reducers), {});
};

View File

@ -9,4 +9,4 @@ const Cu = Components.utils;
const Cr = Components.results;
const CC = Components.Constructor;
const { require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
const { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});

View File

@ -7,11 +7,11 @@ Bug 1067491 - Test taking a census over the RDP.
<meta charset="utf-8">
<title>Census Tree 01</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css" />
<link href="chrome://devtools/skin/themes/light-theme.css" type="text/css" />
<link href="chrome://devtools/skin/themes/common.css" type="text/css" />
<link href="chrome://devtools/skin/themes/widgets.css" type="text/css" />
<link href="chrome://devtools/skin/themes/memory.css" type="text/css" />
<link href="chrome://browser/content/devtools/widgets.css" type="text/css" />
<link href="chrome://browser/skin/devtools/light-theme.css" type="text/css" />
<link href="chrome://browser/skin/devtools/common.css" type="text/css" />
<link href="chrome://browser/skin/devtools/widgets.css" type="text/css" />
<link href="chrome://browser/skin/devtools/memory.css" type="text/css" />
</head>
<body>
<ul id="container" style="width:100%;height:300px;"></ul>

View File

@ -0,0 +1,56 @@
/* 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 { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
var { gDevTools } = Cu.import("resource:///modules/devtools/client/framework/gDevTools.jsm", {});
var { console } = Cu.import("resource://gre/modules/devtools/shared/Console.jsm", {});
var { require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
var { TargetFactory } = require("devtools/client/framework/target");
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
var promise = require("promise");
var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
var { MemoryController } = require("devtools/client/memory/controller");
var { expectState } = require("devtools/server/actors/common");
var HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
var { addDebuggerToGlobal } = require("resource://gre/modules/jsdebugger.jsm");
var SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
var { setTimeout } = require("sdk/timers");
DevToolsUtils.testing = true;
function initDebugger () {
let global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
addDebuggerToGlobal(global);
return new global.Debugger();
}
function StubbedMemoryFront () {
this.dbg = initDebugger();
}
StubbedMemoryFront.prototype.attach = Task.async(function *() {
this.state = "attached";
});
StubbedMemoryFront.prototype.detach = Task.async(function *() {
this.state = "detached";
});
StubbedMemoryFront.prototype.saveHeapSnapshot = expectState("attached", Task.async(function *() {
let path = ThreadSafeChromeUtils.saveHeapSnapshot({ debugger: this.dbg });
return HeapSnapshotFileUtils.getSnapshotIdFromPath(path);
}), "saveHeapSnapshot");
function waitUntilState (store, predicate) {
let deferred = promise.defer();
let unsubscribe = store.subscribe(() => {
if (predicate(store.getState())) {
unsubscribe();
deferred.resolve()
}
});
return deferred.promise;
}

View File

@ -0,0 +1,45 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the async action creator `takeSnapshot(front)`
*/
let actions = require("devtools/client/memory/actions/snapshot");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
yield front.attach();
let controller = new MemoryController({ toolbox: {}, target: {}, front });
let unsubscribe = controller.subscribe(checkState);
let foundPendingState = false;
let foundDoneState = false;
function checkState () {
let state = controller.getState();
if (state.snapshots.length === 1 && state.snapshots[0].status === "start") {
foundPendingState = true;
ok(foundPendingState, "Got state change for pending heap snapshot request");
ok(!(state.snapshots[0].snapshotId), "Snapshot does not yet have a snapshotId");
}
if (state.snapshots.length === 1 && state.snapshots[0].status === "done") {
foundDoneState = true;
ok(foundDoneState, "Got state change for completed heap snapshot request");
ok(state.snapshots[0].snapshotId, "Snapshot fetched with a snapshotId");
}
if (state.snapshots.lenght === 1 && state.snapshots[0].status === "error") {
ok(false, "takeSnapshot's promise returned with an error");
}
}
controller.dispatch(actions.takeSnapshot(front));
yield waitUntilState(controller, () => foundPendingState && foundDoneState);
unsubscribe();
});

View File

@ -0,0 +1,8 @@
[DEFAULT]
tags = devtools
head = head.js
tail =
firefox-appdir = browser
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_action-take-snapshot.js]

View File

@ -7,6 +7,7 @@ const { createStore, applyMiddleware } = require("devtools/client/shared/vendor/
const { thunk } = require("./middleware/thunk");
const { waitUntilService } = require("./middleware/wait-service");
const { log } = require("./middleware/log");
const { promise } = require("./middleware/promise");
/**
* This creates a dispatcher with all the standard middleware in place
@ -20,7 +21,8 @@ const { log } = require("./middleware/log");
module.exports = (opts={}) => {
const middleware = [
thunk,
waitUntilService
waitUntilService,
promise,
];
if (opts.log) {

View File

@ -6,6 +6,7 @@
DevToolsModules(
'log.js',
'promise.js',
'thunk.js',
'wait-service.js',
)

View File

@ -0,0 +1,52 @@
/* 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 uuidgen = require("sdk/util/uuid").uuid;
const {
entries, toObject, reportException, executeSoon
} = require("devtools/shared/DevToolsUtils");
const PROMISE = exports.PROMISE = "@@dispatch/promise";
function promiseMiddleware ({ dispatch, getState }) {
return next => action => {
if (!(PROMISE in action)) {
return next(action);
}
const promise = action[PROMISE];
const seqId = uuidgen().toString();
// Create a new action that doesn't have the promise field and has
// the `seqId` field that represents the sequence id
action = Object.assign(
toObject(entries(action).filter(pair => pair[0] !== PROMISE)), { seqId }
);
dispatch(Object.assign({}, action, { status: "start" }));
promise.then(value => {
executeSoon(() => {
dispatch(Object.assign({}, action, {
status: "done",
value: value
}));
});
}).catch(error => {
executeSoon(() => {
dispatch(Object.assign({}, action, {
status: "error",
error
}));
});
reportException(`@@redux/middleware/promise#${action.type}`, error);
});
// Return the promise so action creators can still compose if they
// want to.
return promise;
};
}
exports.promise = promiseMiddleware;

View File

@ -12,6 +12,7 @@ var promise = require("promise");
loader.lazyRequireGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm", true);
loader.lazyRequireGetter(this, "setTimeout", "Timer", true);
/**
* Turn the error |aError| into a string, without fail.
@ -132,6 +133,18 @@ exports.entries = function entries(obj) {
return Object.keys(obj).map(k => [k, obj[k]]);
}
/**
* Takes an array of 2-element arrays as key/values pairs and
* constructs an object using them.
*/
exports.toObject = function(arr) {
const obj = {};
for(let pair of arr) {
obj[pair[0]] = pair[1];
}
return obj;
}
/**
* Composes the given functions into a single function, which will
* apply the results of each function right-to-left, starting with
@ -186,7 +199,7 @@ exports.waitForTick = function waitForTick() {
*/
exports.waitForTime = function waitForTime(aDelay) {
let deferred = promise.defer();
require("Timer").setTimeout(deferred.resolve, aDelay);
setTimeout(deferred.resolve, aDelay);
return deferred.promise;
};