Bug 1172180 - Create a PerformanceActor, and migrate existing pseudo PerformanceFront to a legacy front. r=vp

This commit is contained in:
Jordan Santell 2015-08-06 14:38:45 -07:00
parent 286196be2d
commit 0a12d64761
84 changed files with 2629 additions and 1625 deletions

View File

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* globals gDevTools, DOMHelpers, toolboxStrings, InspectorFront, Selection,
CommandUtils, DevToolsUtils, Hosts, osString, showDoorhanger,
getHighlighterUtils, getPerformanceFront */
getHighlighterUtils, createPerformanceFront */
"use strict";
@ -59,8 +59,8 @@ loader.lazyRequireGetter(this, "DevToolsUtils",
"devtools/toolkit/DevToolsUtils");
loader.lazyRequireGetter(this, "showDoorhanger",
"devtools/shared/doorhanger", true);
loader.lazyRequireGetter(this, "getPerformanceFront",
"devtools/performance/front", true);
loader.lazyRequireGetter(this, "createPerformanceFront",
"devtools/server/actors/performance", true);
loader.lazyRequireGetter(this, "system",
"devtools/toolkit/shared/system");
loader.lazyGetter(this, "osString", () => {
@ -135,6 +135,7 @@ function Toolbox(target, selectedTool, hostType, hostOptions) {
this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
this._toggleMinimizeMode = this._toggleMinimizeMode.bind(this);
@ -1987,12 +1988,15 @@ Toolbox.prototype = {
}
if (this.performance) {
yield this.performance.open();
yield this.performance.connect();
return this.performance;
}
this._performance = getPerformanceFront(this.target);
yield this.performance.open();
this._performance = createPerformanceFront(this._target);
this.performance.on("*", this._onPerformanceFrontEvent);
yield this.performance.connect();
// Emit an event when connected, but don't wait on startup for this.
this.emit("profiler-connected");
@ -2008,10 +2012,46 @@ Toolbox.prototype = {
if (!this.performance) {
return;
}
this.performance.off("*", this._onPerformanceFrontEvent);
yield this.performance.destroy();
this._performance = null;
}),
/**
* Called when any event comes from the PerformanceFront. If the performance tool is already
* loaded when the first event comes in, immediately unbind this handler, as this is
* only used to queue up observed recordings before the performance tool can handle them,
* which will only occur when `console.profile()` recordings are started before the tool loads.
*/
_onPerformanceFrontEvent: Task.async(function*(eventName, recording) {
if (this.getPanel("performance")) {
this.performance.off("*", this._onPerformanceFrontEvent);
return;
}
let recordings = this._performanceQueuedRecordings = this._performanceQueuedRecordings || [];
// Before any console recordings, we'll get a `console-profile-start` event
// warning us that a recording will come later (via `recording-started`), so
// start to boot up the tool and populate the tool with any other recordings
// observed during that time.
if (eventName === "console-profile-start" && !this._performanceToolOpenedViaConsole) {
this._performanceToolOpenedViaConsole = this.loadTool("performance");
let panel = yield this._performanceToolOpenedViaConsole;
yield panel.open();
panel.panelWin.PerformanceController.populateWithRecordings(recordings);
this.performance.off("*", this._onPerformanceFrontEvent);
}
// Otherwise, if it's a recording-started event, we've already started loading
// the tool, so just store this recording in our array to be later populated
// once the tool loads.
if (eventName === "recording-started") {
recordings.push(recording);
}
}),
/**
* Returns gViewSourceUtils for viewing source.
*/

View File

@ -1,163 +0,0 @@
/* 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");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "RecordingUtils",
"devtools/performance/recording-utils");
loader.lazyImporter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
loader.lazyImporter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
// This identifier string is used to tentatively ascertain whether or not
// a JSON loaded from disk is actually something generated by this tool.
// It isn't, of course, a definitive verification, but a Good Enough™
// approximation before continuing the import. Don't localize this.
const PERF_TOOL_SERIALIZER_IDENTIFIER = "Recorded Performance Data";
const PERF_TOOL_SERIALIZER_LEGACY_VERSION = 1;
const PERF_TOOL_SERIALIZER_CURRENT_VERSION = 2;
/**
* Helpers for importing/exporting JSON.
*/
let PerformanceIO = {
/**
* Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
* @return object
*/
getUnicodeConverter: function() {
let className = "@mozilla.org/intl/scriptableunicodeconverter";
let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
return converter;
},
/**
* Saves a recording as JSON to a file. The provided data is assumed to be
* acyclical, so that it can be properly serialized.
*
* @param object recordingData
* The recording data to stream as JSON.
* @param nsILocalFile file
* The file to stream the data into.
* @return object
* A promise that is resolved once streaming finishes, or rejected
* if there was an error.
*/
saveRecordingToFile: function(recordingData, file) {
let deferred = promise.defer();
recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER;
recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION;
let string = JSON.stringify(recordingData);
let inputStream = this.getUnicodeConverter().convertToInputStream(string);
let outputStream = FileUtils.openSafeFileOutputStream(file);
NetUtil.asyncCopy(inputStream, outputStream, deferred.resolve);
return deferred.promise;
},
/**
* Loads a recording stored as JSON from a file.
*
* @param nsILocalFile file
* The file to import the data from.
* @return object
* A promise that is resolved once importing finishes, or rejected
* if there was an error.
*/
loadRecordingFromFile: function(file) {
let deferred = promise.defer();
let channel = NetUtil.newChannel({
uri: NetUtil.newURI(file),
loadUsingSystemPrincipal: true});
channel.contentType = "text/plain";
NetUtil.asyncFetch(channel, (inputStream, status) => {
try {
let string = NetUtil.readInputStreamToString(inputStream, inputStream.available());
var recordingData = JSON.parse(string);
} catch (e) {
deferred.reject(new Error("Could not read recording data file."));
return;
}
if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
deferred.reject(new Error("Unrecognized recording data file."));
return;
}
if (!isValidSerializerVersion(recordingData.version)) {
deferred.reject(new Error("Unsupported recording data file version."));
return;
}
if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
recordingData = convertLegacyData(recordingData);
}
if (recordingData.profile.meta.version === 2) {
RecordingUtils.deflateProfile(recordingData.profile);
}
deferred.resolve(recordingData);
});
return deferred.promise;
}
};
/**
* Returns a boolean indicating whether or not the passed in `version`
* is supported by this serializer.
*
* @param number version
* @return boolean
*/
function isValidSerializerVersion (version) {
return !!~[
PERF_TOOL_SERIALIZER_LEGACY_VERSION,
PERF_TOOL_SERIALIZER_CURRENT_VERSION
].indexOf(version);
}
/**
* Takes recording data (with version `1`, from the original profiler tool), and
* massages the data to be line with the current performance tool's property names
* and values.
*
* @param object legacyData
* @return object
*/
function convertLegacyData (legacyData) {
let { profilerData, ticksData, recordingDuration } = legacyData;
// The `profilerData` and `ticksData` stay, but the previously unrecorded
// fields just are empty arrays or objects.
let data = {
label: profilerData.profilerLabel,
duration: recordingDuration,
markers: [],
frames: [],
memory: [],
ticks: ticksData,
allocations: { sites: [], timestamps: [], frames: [] },
profile: profilerData.profile,
// Fake a configuration object here if there's tick data,
// so that it can be rendered
configuration: {
withTicks: !!ticksData.length,
withMarkers: false,
withMemory: false,
withAllocations: false
}
};
return data;
}
exports.PerformanceIO = PerformanceIO;

View File

@ -5,15 +5,9 @@
EXTRA_JS_MODULES.devtools.performance += [
'modules/global.js',
'modules/logic/actors.js',
'modules/logic/compatibility.js',
'modules/logic/frame-utils.js',
'modules/logic/front.js',
'modules/logic/io.js',
'modules/logic/jit.js',
'modules/logic/marker-utils.js',
'modules/logic/recording-model.js',
'modules/logic/recording-utils.js',
'modules/logic/tree-model.js',
'modules/logic/waterfall-utils.js',
'modules/markers.js',

View File

@ -11,8 +11,6 @@ const { Task } = require("resource://gre/modules/Task.jsm");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
loader.lazyRequireGetter(this, "PerformanceFront",
"devtools/performance/front", true);
function PerformancePanel(iframeWindow, toolbox) {
this.panelWin = iframeWindow;
@ -32,9 +30,15 @@ PerformancePanel.prototype = {
* completes opening.
*/
open: Task.async(function*() {
if (this._opening) {
return this._opening;
}
let deferred = promise.defer();
this._opening = deferred.promise;
this.panelWin.gToolbox = this._toolbox;
this.panelWin.gTarget = this.target;
this._onRecordingStartOrStop = this._onRecordingStartOrStop.bind(this);
this._checkRecordingStatus = this._checkRecordingStatus.bind(this);
// Actor is already created in the toolbox; reuse
// the same front, and the toolbox will also initialize the front,
@ -50,14 +54,21 @@ PerformancePanel.prototype = {
}
this.panelWin.gFront = front;
this.panelWin.gFront.on("recording-started", this._onRecordingStartOrStop);
this.panelWin.gFront.on("recording-stopped", this._onRecordingStartOrStop);
let { PerformanceController, EVENTS } = this.panelWin;
PerformanceController.on(EVENTS.NEW_RECORDING, this._checkRecordingStatus);
PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._checkRecordingStatus);
yield this.panelWin.startupPerformance();
// Fire this once incase we have an in-progress recording (console profile)
// that caused this start up, and no state change yet, so we can highlight the
// tab if we need.
this._checkRecordingStatus();
this.isReady = true;
this.emit("ready");
return this;
deferred.resolve(this);
return this._opening;
}),
// DevToolPanel API
@ -72,16 +83,16 @@ PerformancePanel.prototype = {
return;
}
this.panelWin.gFront.off("recording-started", this._onRecordingStartOrStop);
this.panelWin.gFront.off("recording-stopped", this._onRecordingStartOrStop);
let { PerformanceController, EVENTS } = this.panelWin;
PerformanceController.off(EVENTS.NEW_RECORDING, this._checkRecordingStatus);
PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, this._checkRecordingStatus);
yield this.panelWin.shutdownPerformance();
this.emit("destroyed");
this._destroyed = true;
}),
_onRecordingStartOrStop: function () {
let front = this.panelWin.gFront;
if (front.isRecording()) {
_checkRecordingStatus: function () {
if (this.panelWin.PerformanceController.isRecording()) {
this._toolbox.highlightTool("performance");
} else {
this._toolbox.unhighlightTool("performance");

View File

@ -25,9 +25,7 @@ loader.lazyRequireGetter(this, "L10N",
loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
"devtools/performance/markers", true);
loader.lazyRequireGetter(this, "RecordingUtils",
"devtools/performance/recording-utils");
loader.lazyRequireGetter(this, "RecordingModel",
"devtools/performance/recording-model", true);
"devtools/toolkit/performance/utils");
loader.lazyRequireGetter(this, "GraphsController",
"devtools/performance/graphs", true);
loader.lazyRequireGetter(this, "WaterfallHeader",
@ -92,15 +90,15 @@ const EVENTS = {
UI_STOP_RECORDING: "Performance:UI:StopRecording",
// Emitted by the PerformanceView on import button click
UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
UI_RECORDING_IMPORTED: "Performance:UI:ImportRecording",
// Emitted by the RecordingsView on export button click
UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
// When a recording is started or stopped via the PerformanceController
RECORDING_STARTED: "Performance:RecordingStarted",
RECORDING_STOPPED: "Performance:RecordingStopped",
RECORDING_WILL_START: "Performance:RecordingWillStart",
RECORDING_WILL_STOP: "Performance:RecordingWillStop",
// When a new recording is being tracked in the panel.
NEW_RECORDING: "Performance:NewRecording",
// When a recording is started or stopped or stopping via the PerformanceController
RECORDING_STATE_CHANGE: "Performance:RecordingStateChange",
// Emitted by the PerformanceController or RecordingView
// when a recording model is selected
@ -109,8 +107,7 @@ const EVENTS = {
// When recordings have been cleared out
RECORDINGS_CLEARED: "Performance:RecordingsCleared",
// When a recording is imported or exported via the PerformanceController
RECORDING_IMPORTED: "Performance:RecordingImported",
// When a recording is exported via the PerformanceController
RECORDING_EXPORTED: "Performance:RecordingExported",
// When the front has updated information on the profiler's circular buffer
@ -153,7 +150,25 @@ const EVENTS = {
// When a source is shown in the JavaScript Debugger at a specific location.
SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger"
SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
// These are short hands for the RECORDING_STATE_CHANGE event to make refactoring
// tests easier. UI components should use RECORDING_STATE_CHANGE, and these are
// deprecated for test usage only.
RECORDING_STARTED: "Performance:RecordingStarted",
RECORDING_WILL_STOP: "Performance:RecordingWillStop",
RECORDING_STOPPED: "Performance:RecordingStopped",
// Fired by the PerformanceController when `populateWithRecordings` is finished.
RECORDINGS_SEEDED: "Performance:RecordingsSeeded",
// Emitted by the PerformanceController when `PerformanceController.stopRecording()`
// is completed; used in tests, to know when a manual UI click is finished.
CONTROLLER_STOPPED_RECORDING: "Performance:Controller:StoppedRecording",
// Emitted by the PerformanceController when a recording is imported. Used
// only in tests. Should use the normal RECORDING_STATE_CHANGE in the UI.
RECORDING_IMPORTED: "Performance:ImportedRecording",
};
/**
@ -165,20 +180,16 @@ let gToolbox, gTarget, gFront;
* Initializes the profiler controller and views.
*/
let startupPerformance = Task.async(function*() {
yield promise.all([
PerformanceController.initialize(),
PerformanceView.initialize()
]);
yield PerformanceController.initialize();
yield PerformanceView.initialize();
});
/**
* Destroys the profiler controller and views.
*/
let shutdownPerformance = Task.async(function*() {
yield promise.all([
PerformanceController.destroy(),
PerformanceView.destroy()
]);
yield PerformanceController.destroy();
yield PerformanceView.destroy();
});
/**
@ -202,8 +213,7 @@ let PerformanceController = {
this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
this._onPrefChanged = this._onPrefChanged.bind(this);
this._onThemeChanged = this._onThemeChanged.bind(this);
this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
this._onProfilerStatusUpdated = this._onProfilerStatusUpdated.bind(this);
this._onFrontEvent = this._onFrontEvent.bind(this);
// Store data regarding if e10s is enabled.
this._e10s = Services.appinfo.browserTabsRemoteAutostart;
@ -212,15 +222,11 @@ let PerformanceController = {
this._prefs = require("devtools/performance/global").PREFS;
this._prefs.on("pref-changed", this._onPrefChanged);
gFront.on("recording-starting", this._onRecordingStateChange);
gFront.on("recording-started", this._onRecordingStateChange);
gFront.on("recording-stopping", this._onRecordingStateChange);
gFront.on("recording-stopped", this._onRecordingStateChange);
gFront.on("profiler-status", this._onProfilerStatusUpdated);
gFront.on("*", this._onFrontEvent);
ToolbarView.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
PerformanceView.on(EVENTS.UI_RECORDING_IMPORTED, this.importRecording);
PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
@ -234,15 +240,11 @@ let PerformanceController = {
destroy: function() {
this._prefs.off("pref-changed", this._onPrefChanged);
gFront.off("recording-starting", this._onRecordingStateChange);
gFront.off("recording-started", this._onRecordingStateChange);
gFront.off("recording-stopping", this._onRecordingStateChange);
gFront.off("recording-stopped", this._onRecordingStateChange);
gFront.off("profiler-status", this._onProfilerStatusUpdated);
gFront.off("*", this._onFrontEvent);
ToolbarView.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
PerformanceView.off(EVENTS.UI_RECORDING_IMPORTED, this.importRecording);
PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
@ -292,8 +294,7 @@ let PerformanceController = {
},
/**
* Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED`
* when the front has started to record.
* Starts recording with the PerformanceFront.
*/
startRecording: Task.async(function *() {
let options = {
@ -312,19 +313,25 @@ let PerformanceController = {
}),
/**
* Stops recording with the PerformanceFront. Emits `EVENTS.RECORDING_STOPPED`
* when the front has stopped recording.
* Stops recording with the PerformanceFront.
*/
stopRecording: Task.async(function *() {
let recording = this.getLatestManualRecording();
yield gFront.stopRecording(recording);
// Emit another stop event here, as a lot of tests use
// the RECORDING_STOPPED event, but in the case of a UI click on a button,
// the RECORDING_STOPPED event happens from the server, where this request may
// not have yet finished, so listen to this in tests that fail because the `stopRecording`
// request is not yet completed. Should only be used in that scenario.
this.emit(EVENTS.CONTROLLER_STOPPED_RECORDING);
}),
/**
* Saves the given recording to a file. Emits `EVENTS.RECORDING_EXPORTED`
* when the file was saved.
*
* @param RecordingModel recording
* @param PerformanceRecording recording
* The model that holds the recording data.
* @param nsILocalFile file
* The file to stream the data into.
@ -346,7 +353,7 @@ let PerformanceController = {
// If last recording is not recording, but finalizing itself,
// wait for that to finish
if (latest && !latest.isCompleted()) {
yield this.once(EVENTS.RECORDING_STOPPED);
yield this.waitForStateChangeOnRecording(latest, "recording-stopped");
}
this._recordings.length = 0;
@ -362,18 +369,22 @@ let PerformanceController = {
* The file to import the data from.
*/
importRecording: Task.async(function*(_, file) {
let recording = new RecordingModel();
this._recordings.push(recording);
yield recording.importRecording(file);
let recording = yield gFront.importRecording(file);
this._addNewRecording(recording);
this.emit(EVENTS.RECORDING_IMPORTED, recording);
// Only emit in tests for legacy purposes for shorthand --
// other things in UI should handle the generic NEW_RECORDING
// event to handle lazy recordings.
if (DevToolsUtils.testing) {
this.emit(EVENTS.RECORDING_IMPORTED, recording);
}
}),
/**
* Sets the currently active RecordingModel. Should rarely be called directly,
* Sets the currently active PerformanceRecording. Should rarely be called directly,
* as RecordingsView handles this when manually selected a recording item. Exceptions
* are when clearing the view.
* @param RecordingModel recording
* @param PerformanceRecording recording
*/
setCurrentRecording: function (recording) {
if (this._currentRecording !== recording) {
@ -383,8 +394,8 @@ let PerformanceController = {
},
/**
* Gets the currently active RecordingModel.
* @return RecordingModel
* Gets the currently active PerformanceRecording.
* @return PerformanceRecording
*/
getCurrentRecording: function () {
return this._currentRecording;
@ -392,7 +403,7 @@ let PerformanceController = {
/**
* Get most recently added recording that was triggered manually (via UI).
* @return RecordingModel
* @return PerformanceRecording
*/
getLatestManualRecording: function () {
for (let i = this._recordings.length - 1; i >= 0; i--) {
@ -434,48 +445,85 @@ let PerformanceController = {
},
/**
* Emitted when the front updates RecordingModel's buffer status.
* Fired from the front on any event. Propagates to other handlers from here.
*/
_onProfilerStatusUpdated: function (_, data) {
_onFrontEvent: function (eventName, ...data) {
if (eventName === "profiler-status") {
this._onProfilerStatusUpdated(...data);
return;
}
if (["recording-started", "recording-stopped", "recording-stopping"].indexOf(eventName) !== -1) {
this._onRecordingStateChange(eventName, ...data);
}
},
/**
* Emitted when the front updates PerformanceRecording's buffer status.
*/
_onProfilerStatusUpdated: function (data) {
this.emit(EVENTS.PROFILER_STATUS_UPDATED, data);
},
/**
* Stores a recording internally.
*
* @param {PerformanceRecordingFront} recording
*/
_addNewRecording: function (recording) {
if (this._recordings.indexOf(recording) === -1) {
this._recordings.push(recording);
this.emit(EVENTS.NEW_RECORDING, recording);
}
},
/**
* Fired when a recording model changes state.
*
* @param {string} state
* @param {RecordingModel} model
* Can be "recording-started", "recording-stopped" or "recording-stopping".
* @param {PerformanceRecording} model
*/
_onRecordingStateChange: function (state, model) {
// If we get a state change for a recording that isn't being tracked in the front,
// just ignore it. This can occur when stopping a profile via console that was cleared.
if (state !== "recording-starting" && this.getRecordings().indexOf(model) === -1) {
return;
}
this._addNewRecording(model);
switch (state) {
// Fired when a RecordingModel was just created from the front
case "recording-starting":
// When a recording is just starting, store it internally
this._recordings.push(model);
this.emit(EVENTS.RECORDING_WILL_START, model);
break;
// Fired when a RecordingModel has started recording
case "recording-started":
this.emit(EVENTS.RECORDING_STARTED, model);
break;
// Fired when a RecordingModel is no longer recording, and
// starting to fetch all the profiler data
case "recording-stopping":
this.emit(EVENTS.RECORDING_WILL_STOP, model);
break;
// Fired when a RecordingModel is finished fetching all of its data
case "recording-stopped":
this.emit(EVENTS.RECORDING_STOPPED, model);
break;
this.emit(EVENTS.RECORDING_STATE_CHANGE, state, model);
// Emit the state specific events for tests that I'm too
// lazy and frusterated to change right now. These events
// should only be used in tests, as the rest of the UI should
// react to general RECORDING_STATE_CHANGE events and NEW_RECORDING
// events to handle lazy recordings.
if (DevToolsUtils.testing) {
switch (state) {
case "recording-started":
this.emit(EVENTS.RECORDING_STARTED, model);
break;
case "recording-stopping":
this.emit(EVENTS.RECORDING_WILL_STOP, model);
break;
case "recording-stopped":
this.emit(EVENTS.RECORDING_STOPPED, model);
break;
}
}
},
/**
* Takes a recording and returns a value between 0 and 1 indicating how much
* of the buffer is used.
*/
getBufferUsageForRecording: function (recording) {
return gFront.getBufferUsageForRecording(recording);
},
/**
* Returns a boolean indicating if any recordings are currently in progress or not.
*/
isRecording: function () {
return this._recordings.some(r => r.isRecording());
},
/**
* Returns the internal store of recording models.
*/
@ -483,6 +531,13 @@ let PerformanceController = {
return this._recordings;
},
/**
* Returns traits from the front.
*/
getTraits: function () {
return gFront.traits;
},
/**
* Utility method taking a string or an array of strings of feature names (like
* "withAllocations" or "withMarkers"), and returns whether or not the current
@ -508,6 +563,21 @@ let PerformanceController = {
return [].concat(features).every(f => config[f]);
},
/**
* Takes an array of PerformanceRecordingFronts and adds them to the internal
* store of the UI. Used by the toolbox to lazily seed recordings that
* were observed before the panel was loaded in the scenario where `console.profile()`
* is used before the tool is loaded.
*
* @param {Array<PerformanceRecordingFront>} recordings
*/
populateWithRecordings: function (recordings=[]) {
for (let recording of recordings) {
PerformanceController._addNewRecording(recording);
}
this.emit(EVENTS.RECORDINGS_SEEDED);
},
/**
* Returns an object with `supported` and `enabled` properties indicating
* whether or not the platform is capable of turning on e10s and whether or not
@ -530,6 +600,25 @@ let PerformanceController = {
return { supported, enabled };
},
/**
* Takes a PerformanceRecording and a state, and waits for
* the event to be emitted from the front for that recording.
*
* @param {PerformanceRecordingFront} recording
* @param {string} expectedState
* @return {Promise}
*/
waitForStateChangeOnRecording: Task.async(function *(recording, expectedState) {
let deferred = promise.defer();
this.on(EVENTS.RECORDING_STATE_CHANGE, function handler (state, model) {
if (state === expectedState && model === recording) {
this.off(EVENTS.RECORDING_STATE_CHANGE, handler);
deferred.resolve();
}
});
yield deferred.promise;
}),
/**
* Called on init, sets an `e10s` attribute on the main view container with
* "disabled" if e10s is possible on the platform and just not on, or "unsupported"

View File

@ -51,10 +51,7 @@ let PerformanceView = {
this._onClearButtonClick = this._onClearButtonClick.bind(this);
this._onRecordingSelected = this._onRecordingSelected.bind(this);
this._onProfilerStatusUpdated = this._onProfilerStatusUpdated.bind(this);
this._onRecordingWillStart = this._onRecordingWillStart.bind(this);
this._onRecordingStarted = this._onRecordingStarted.bind(this);
this._onRecordingWillStop = this._onRecordingWillStop.bind(this);
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
for (let button of $$(".record-button")) {
button.addEventListener("click", this._onRecordButtonClick);
@ -65,10 +62,8 @@ let PerformanceView = {
// Bind to controller events to unlock the record button
PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
PerformanceController.on(EVENTS.PROFILER_STATUS_UPDATED, this._onProfilerStatusUpdated);
PerformanceController.on(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.on(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
PerformanceController.on(EVENTS.NEW_RECORDING, this._onRecordingStateChange);
this.setState("empty");
@ -92,10 +87,8 @@ let PerformanceView = {
PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
PerformanceController.off(EVENTS.PROFILER_STATUS_UPDATED, this._onProfilerStatusUpdated);
PerformanceController.off(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.off(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
PerformanceController.off(EVENTS.NEW_RECORDING, this._onRecordingStateChange);
yield ToolbarView.destroy();
yield RecordingsView.destroy();
@ -157,7 +150,7 @@ let PerformanceView = {
return;
}
let bufferUsage = recording.getBufferUsage();
let bufferUsage = PerformanceController.getBufferUsageForRecording(recording) || 0;
// Normalize to a percentage value
let percent = Math.floor(bufferUsage * 100);
@ -174,7 +167,7 @@ let PerformanceView = {
}
$bufferLabel.value = L10N.getFormatStr("profiler.bufferFull", percent);
this.emit(EVENTS.UI_BUFFER_UPDATED, percent);
this.emit(EVENTS.UI_BUFFER_STATUS_UPDATED, percent);
},
/**
@ -209,61 +202,25 @@ let PerformanceView = {
}
},
/**
* Fired when a recording is just starting, but actors may not have
* yet started actually recording.
*/
_onRecordingWillStart: function (_, recording) {
if (!recording.isConsole()) {
this._lockRecordButtons(true);
this._activateRecordButtons(true);
}
},
/**
* When a recording has started.
*/
_onRecordingStarted: function (_, recording) {
// A stopped recording can be from `console.profileEnd` -- only unlock
// the button if it's the main recording that was started via UI.
if (!recording.isConsole()) {
this._lockRecordButtons(false);
}
if (recording.isRecording()) {
this.updateBufferStatus();
}
},
_onRecordingStateChange: function () {
let currentRecording = PerformanceController.getCurrentRecording();
let recordings = PerformanceController.getRecordings();
/**
* Fired when a recording is stopping, but not yet completed
*/
_onRecordingWillStop: function (_, recording) {
if (!recording.isConsole()) {
this._lockRecordButtons(true);
this._activateRecordButtons(false);
}
// Lock the details view while the recording is being loaded in the UI.
// Only do this if this is the current recording.
if (recording === PerformanceController.getCurrentRecording()) {
this._activateRecordButtons(recordings.find(r => !r.isConsole() && r.isRecording()));
this._lockRecordButtons(recordings.find(r => !r.isConsole() && r.isFinalizing()));
if (currentRecording && currentRecording.isFinalizing()) {
this.setState("loading");
}
},
/**
* When a recording is complete.
*/
_onRecordingStopped: function (_, recording) {
// A stopped recording can be from `console.profileEnd` -- only unlock
// the button if it's the main recording that was started via UI.
if (!recording.isConsole()) {
this._lockRecordButtons(false);
}
// If the currently selected recording is the one that just stopped,
// switch state to "recorded".
if (recording === PerformanceController.getCurrentRecording()) {
if (currentRecording && currentRecording.isCompleted()) {
this.setState("recorded");
}
if (currentRecording && currentRecording.isRecording()) {
this.updateBufferStatus();
}
},
/**
@ -280,6 +237,8 @@ let PerformanceView = {
if (this._recordButton.hasAttribute("checked")) {
this.emit(EVENTS.UI_STOP_RECORDING);
} else {
this._lockRecordButtons(true);
this._activateRecordButtons(true);
this.emit(EVENTS.UI_START_RECORDING);
}
},

View File

@ -21,14 +21,6 @@ support-files =
[browser_markers-timestamp.js]
[browser_perf-allocations-to-samples.js]
[browser_perf-categories-js-calltree.js]
[browser_perf-compatibility-01.js]
[browser_perf-compatibility-02.js]
[browser_perf-compatibility-03.js]
[browser_perf-compatibility-04.js]
[browser_perf-compatibility-05.js]
[browser_perf-compatibility-06.js]
[browser_perf-compatibility-07.js]
[browser_perf-compatibility-08.js]
[browser_perf-clear-01.js]
[browser_perf-clear-02.js]
[browser_perf-columns-js-calltree.js]
@ -42,8 +34,6 @@ support-files =
[browser_perf-console-record-07.js]
[browser_perf-console-record-08.js]
[browser_perf-console-record-09.js]
[browser_perf-data-massaging-01.js]
[browser_perf-data-samples.js]
[browser_perf-details-calltree-render.js]
[browser_perf-details-flamegraph-render.js]
[browser_perf-details-memory-calltree-render.js]
@ -57,19 +47,15 @@ support-files =
[browser_perf-details-06.js]
[browser_perf-details-07.js]
[browser_perf-events-calltree.js]
[browser_perf-front-basic-profiler-01.js]
[browser_perf-front-basic-timeline-01.js]
#[browser_perf-front-profiler-01.js] bug 1077464
[browser_perf-front-profiler-02.js]
[browser_perf-front-profiler-03.js]
[browser_perf-front-profiler-04.js]
#[browser_perf-front-profiler-05.js] bug 1077464
#[browser_perf-front-profiler-06.js]
[browser_perf-front-01.js]
[browser_perf-front-02.js]
[browser_perf-highlighted.js]
#[browser_perf-jit-view-01.js] bug 1176056
#[browser_perf-jit-view-02.js] bug 1176056
[browser_perf-legacy-front-01.js]
[browser_perf-legacy-front-02.js]
[browser_perf-legacy-front-03.js]
[browser_perf-legacy-front-04.js]
[browser_perf-legacy-front-05.js]
[browser_perf-legacy-front-06.js]
[browser_perf-loading-01.js]
[browser_perf-loading-02.js]
[browser_perf-marker-details-01.js]
@ -101,13 +87,9 @@ skip-if = os == 'linux' # Bug 1172120
[browser_perf-overview-selection-02.js]
[browser_perf-overview-selection-03.js]
[browser_perf-overview-time-interval.js]
[browser_perf-shared-connection-02.js]
[browser_perf-shared-connection-03.js]
[browser_perf-states.js]
[browser_perf-refresh.js]
[browser_perf-ui-recording.js]
[browser_perf-recording-model-01.js]
[browser_perf-recording-model-02.js]
[browser_perf-recording-notices-01.js]
[browser_perf-recording-notices-02.js]
[browser_perf-recording-notices-03.js]

View File

@ -12,13 +12,13 @@ function waitForMarkerType(front, type) {
info("Waiting for marker of type = " + type);
const { promise, resolve } = Promise.defer();
const handler = (_, name, markers) => {
const handler = (name, data) => {
if (name !== "markers") {
return;
}
let markers = data.markers;
info("Got markers: " + JSON.stringify(markers, null, 2));
if (markers.some(m => m.name === type)) {
ok(true, "Found marker of type = " + type);
front.off("timeline-data", handler);
@ -36,7 +36,7 @@ function* spawnTest () {
let { target, front } = yield initBackend(TEST_URL);
yield front.startRecording({ withMarkers: true, withTicks: true });
let rec = yield front.startRecording({ withMarkers: true, withTicks: true });
yield Promise.all([
waitForMarkerType(front, "nsCycleCollector::Collect"),
@ -44,7 +44,7 @@ function* spawnTest () {
]);
ok(true, "Got expected cycle collection events");
yield front.stopRecording();
yield front.stopRecording(rec);
// Destroy the front before removing tab to ensure no
// lingering requests

View File

@ -24,13 +24,11 @@ function* spawnTest () {
info(`Got ${markers.length} markers.`);
let maxMarkerTime = model._timelineStartTime + model.getDuration() + TIME_CLOSE_TO;
ok(markers.every(({name}) => name === "GarbageCollection"), "All markers found are GC markers");
ok(markers.length > 0, "found atleast one GC marker");
ok(markers.every(({start}) => typeof start === "number" && start > 0 && start < maxMarkerTime),
ok(markers.every(({start, end}) => typeof start === "number" && start > 0 && start < end),
"All markers have a start time between the valid range.");
ok(markers.every(({end}) => typeof end === "number" && end > 0 && end < maxMarkerTime),
ok(markers.every(({end}) => typeof end === "number"),
"All markers have an end time between the valid range.");
ok(markers.every(({causeName}) => typeof causeName === "string"),
"All markers have a causeName.");
@ -41,7 +39,8 @@ function* spawnTest () {
yield removeTab(target.tab);
finish();
function handler (_, name, m) {
function handler (name, m) {
m = m.markers;
if (name === "markers" && m[0].name === "GarbageCollection") {
markers = m;
}

View File

@ -31,12 +31,12 @@ function* spawnTest () {
yield removeTab(target.tab);
finish();
function handler (_, name, _markers) {
function handler (name, data) {
if (name !== "markers") {
return;
}
_markers.forEach(marker => {
data.markers.forEach(marker => {
info(marker.name);
if (marker.name === "Parse HTML") {
markers.push(marker);

View File

@ -33,9 +33,9 @@ function* spawnTest () {
yield removeTab(target.tab);
finish();
function handler (_, name, m) {
function handler (name, data) {
if (name === "markers") {
markers = markers.concat(m.filter(marker => marker.name === "Styles"));
markers = markers.concat(data.markers.filter(marker => marker.name === "Styles"));
}
}
}

View File

@ -29,10 +29,8 @@ function* spawnTest () {
ok(markers.every(({stack}) => typeof stack === "number"), "All markers have stack references.");
ok(markers.every(({name}) => name === "TimeStamp"), "All markers found are TimeStamp markers");
ok(markers.length === 2, "found 2 TimeStamp markers");
ok(markers.every(({start}) => typeof start === "number" && start > 0 && start < maxMarkerTime),
"All markers have a start time between the valid range.");
ok(markers.every(({end}) => typeof end === "number" && end > 0 && end < maxMarkerTime),
"All markers have an end time between the valid range.");
ok(markers.every(({start, end}) => typeof start === "number" && start === end),
"All markers have equal start and end times");
is(markers[0].causeName, void 0, "Unlabeled timestamps have an empty causeName");
is(markers[1].causeName, "myLabel", "Labeled timestamps have correct causeName");
@ -42,9 +40,9 @@ function* spawnTest () {
yield removeTab(target.tab);
finish();
function handler (_, name, m) {
function handler (name, data) {
if (name === "markers") {
markers = markers.concat(m.filter(marker => marker.name === "TimeStamp"));
markers = markers.concat(data.markers.filter(marker => marker.name === "TimeStamp"));
}
}
}

View File

@ -8,8 +8,6 @@
*/
function test() {
let RecordingUtils = require("devtools/performance/recording-utils");
let output = RecordingUtils.getProfileThreadFromAllocations(TEST_DATA);
is(output.toSource(), EXPECTED_OUTPUT.toSource(), "The output is correct.");

View File

@ -1,53 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test basic functionality of PerformanceFront with mock memory and timeline actors.
*/
let WAIT_TIME = 100;
function* spawnTest() {
let { target, front } = yield initBackend(SIMPLE_URL, {
TEST_MOCK_MEMORY_ACTOR: true,
TEST_MOCK_TIMELINE_ACTOR: true
});
Services.prefs.setBoolPref(MEMORY_PREF, true);
Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
let { memory, timeline } = front.getActorSupport();
ok(!memory, "memory should be mocked.");
ok(!timeline, "timeline should be mocked.");
let recording = yield front.startRecording({
withTicks: true,
withMarkers: true,
withMemory: true,
withAllocations: true,
allocationsSampleProbability: +Services.prefs.getCharPref(MEMORY_SAMPLE_PROB_PREF),
allocationsMaxLogLength: Services.prefs.getIntPref(MEMORY_MAX_LOG_LEN_PREF)
});
ok(typeof recording._profilerStartTime === "number",
"The front.startRecording() returns a recording with a profiler start time");
ok(typeof recording._timelineStartTime === "number",
"The front.startRecording() returns a recording with a timeline start time");
ok(typeof recording._memoryStartTime === "number",
"The front.startRecording() returns a recording with a memory start time");
yield busyWait(WAIT_TIME);
yield front.stopRecording(recording);
ok(typeof recording.getDuration() === "number",
"The front.stopRecording() allows recording to get a duration.");
ok(recording.getDuration() >= 0,
"The profilerEndTime is after profilerStartTime.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -1,52 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test basic functionality of PerformanceFront with only mock memory.
*/
let WAIT_TIME = 100;
function* spawnTest() {
let { target, front } = yield initBackend(SIMPLE_URL, {
TEST_MOCK_MEMORY_ACTOR: true
});
Services.prefs.setBoolPref(MEMORY_PREF, true);
Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
let { memory, timeline } = front.getActorSupport();
ok(!memory, "memory should be mocked.");
ok(timeline, "timeline should not be mocked.");
let recording = yield front.startRecording({
withTicks: true,
withMarkers: true,
withMemory: true,
withAllocations: true,
allocationsSampleProbability: +Services.prefs.getCharPref(MEMORY_SAMPLE_PROB_PREF),
allocationsMaxLogLength: Services.prefs.getIntPref(MEMORY_MAX_LOG_LEN_PREF)
});
ok(typeof recording._profilerStartTime === "number",
"The front.startRecording() returns a recording with a profiler start time");
ok(typeof recording._timelineStartTime === "number",
"The front.startRecording() returns a recording with a timeline start time");
ok(typeof recording._memoryStartTime === "number",
"The front.startRecording() returns a recording with a memory start time");
yield busyWait(WAIT_TIME);
yield front.stopRecording(recording);
ok(typeof recording.getDuration() === "number",
"The front.stopRecording() allows recording to get a duration.");
ok(recording.getDuration() >= 0,
"The profilerEndTime is after profilerStartTime.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -1,35 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that when using an older server (< Fx40) where the profiler actor does not
* have the `getBufferInfo` method that nothing breaks and RecordingModels have null
* `getBufferUsage()` values.
*/
function* spawnTest() {
let { target, front } = yield initBackend(SIMPLE_URL, {
TEST_MOCK_PROFILER_CHECK_TIMER: 10,
TEST_PROFILER_FILTER_STATUS: ["position", "totalSize", "generation"]
});
let model = yield front.startRecording();
let count = 0;
while (count < 5) {
let [_, stats] = yield onceSpread(front._profiler, "profiler-status");
is(stats.generation, void 0, "profiler-status has void `generation`");
is(stats.totalSize, void 0, "profiler-status has void `totalSize`");
is(stats.position, void 0, "profiler-status has void `position`");
count++;
}
is(model.getBufferUsage(), null, "model should have `null` for its buffer usage");
yield front.stopRecording(model);
is(model.getBufferUsage(), null, "after recording, model should still have `null` for its buffer usage");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -6,14 +6,11 @@
* before it was opened.
*/
let { getPerformanceFront } = require("devtools/performance/front");
let WAIT_TIME = 10;
function* spawnTest() {
let profilerConnected = waitForProfilerConnection();
let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
yield profilerConnected;
let front = getPerformanceFront(target);
let front = toolbox.performance;
let profileStart = once(front, "recording-started");
console.profile("rust");
@ -29,6 +26,7 @@ function* spawnTest() {
let { panelWin: { PerformanceController, RecordingsView }} = panel;
let recordings = PerformanceController.getRecordings();
yield waitUntil(() => PerformanceController.getRecordings().length === 1);
is(recordings.length, 1, "one recording found in the performance panel.");
is(recordings[0].isConsole(), true, "recording came from console.profile.");
is(recordings[0].getLabel(), "rust", "correct label in the recording model.");

View File

@ -6,14 +6,11 @@
* when it is opened.
*/
let { getPerformanceFront } = require("devtools/performance/front");
let WAIT_TIME = 10;
function* spawnTest() {
let profilerConnected = waitForProfilerConnection();
let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
yield profilerConnected;
let front = getPerformanceFront(target);
let front = toolbox.performance;
let profileStart = once(front, "recording-started");
console.profile("rust");
@ -26,6 +23,7 @@ function* spawnTest() {
let panel = toolbox.getCurrentPanel();
let { panelWin: { PerformanceController, RecordingsView }} = panel;
yield waitUntil(() => PerformanceController.getRecordings().length === 2);
let recordings = PerformanceController.getRecordings();
is(recordings.length, 2, "two recordings found in the performance panel.");
is(recordings[0].isConsole(), true, "recording came from console.profile (1).");

View File

@ -6,14 +6,11 @@
* also console recordings that have finished before it was opened.
*/
let { getPerformanceFront } = require("devtools/performance/front");
let WAIT_TIME = 10;
function* spawnTest() {
let profilerConnected = waitForProfilerConnection();
let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
yield profilerConnected;
let front = getPerformanceFront(target);
let front = toolbox.performance;
let profileStart = once(front, "recording-started");
console.profile("rust");
@ -31,6 +28,7 @@ function* spawnTest() {
let panel = toolbox.getCurrentPanel();
let { panelWin: { PerformanceController, RecordingsView }} = panel;
yield waitUntil(() => PerformanceController.getRecordings().length === 2);
let recordings = PerformanceController.getRecordings();
is(recordings.length, 2, "two recordings found in the performance panel.");
is(recordings[0].isConsole(), true, "recording came from console.profile (1).");

View File

@ -1,41 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the retrieved profiler data samples always have a (root) node.
* If this ever changes, the |ThreadNode.prototype.insert| function in
* browser/devtools/performance/modules/logic/tree-model.js will have to be changed.
*/
const WAIT_TIME = 1000; // ms
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let front = panel.panelWin.gFront;
let rec = yield front.startRecording();
busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
yield front.stopRecording(rec);
let profile = rec.getProfile();
let sampleCount = 0;
for (let thread of profile.threads) {
info("Checking thread: " + thread.name);
for (let sample of thread.samples.data) {
sampleCount++;
let stack = getInflatedStackLocations(thread, sample);
if (stack[0] != "(root)") {
ok(false, "The sample " + stack.toSource() + " doesn't have a root node.");
}
}
}
ok(sampleCount > 0,
"At least some samples have been iterated over, checking for root nodes.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,40 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that the waterfall view renders content after recording.
*/
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let { EVENTS, PerformanceController, DetailsView, WaterfallView } = panel.panelWin;
Services.prefs.setBoolPref(ALLOCATIONS_PREF, false);
yield startRecording(panel);
yield waitUntil(hasGCMarkers(PerformanceController));
let rendered = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
yield stopRecording(panel);
ok(DetailsView.isViewSelected(WaterfallView),
"The waterfall view is selected by default in the details view.");
yield rendered;
Services.prefs.setBoolPref(ALLOCATIONS_PREF, false);
yield startRecording(panel);
yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length);
rendered = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
yield stopRecording(panel);
yield rendered;
ok(true, "WaterfallView rendered again after recording completed a second time.");
yield teardown(panel);
finish();
}
function hasGCMarkers (controller) {
return function () {
controller.getCurrentRecording().getMarkers().filter(m => m.name === "GarbageCollection").length >== 2;
};
}

View File

@ -5,7 +5,6 @@
* Tests that the call tree up/down events work for js calltree and memory calltree.
*/
const { ThreadNode } = require("devtools/performance/tree-model");
const RecordingUtils = require("devtools/performance/recording-utils")
function* spawnTest() {
let focus = 0;

View File

@ -1,55 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test basic functionality of PerformanceFront, emitting start and endtime values
*/
let WAIT_TIME = 1000;
function* spawnTest() {
let { target, front } = yield initBackend(SIMPLE_URL);
let recording = yield front.startRecording({
withAllocations: true,
allocationsSampleProbability: +Services.prefs.getCharPref(MEMORY_SAMPLE_PROB_PREF),
allocationsMaxLogLength: Services.prefs.getIntPref(MEMORY_MAX_LOG_LEN_PREF)
});
let allocationsCount = 0;
let allocationsCounter = (_, type) => type === "allocations" && allocationsCount++;
// Record allocation events to ensure it's called more than once
// so we know it's polling
front.on("timeline-data", allocationsCounter);
ok(typeof recording._profilerStartTime === "number",
"The front.startRecording() returns a recording model with a profiler start time.");
ok(typeof recording._timelineStartTime === "number",
"The front.startRecording() returns a recording model with a timeline start time.");
ok(typeof recording._memoryStartTime === "number",
"The front.startRecording() returns a recording model with a memory start time.");
yield Promise.all([
busyWait(WAIT_TIME),
waitUntil(() => allocationsCount > 1)
]);
yield front.stopRecording(recording);
front.off("timeline-data", allocationsCounter);
ok(typeof recording.getDuration() === "number",
"The front.stopRecording() gives the recording model a stop time and duration.");
ok(recording.getDuration() > 0,
"The front.stopRecording() gives a positive duration amount.");
is((yield front._request("memory", "getState")), "detached",
"Memory actor is detached when stopping recording with allocations.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -1,38 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that timeline and memory actors can be started multiple times to get
* different start times for different recording sessions.
*/
let WAIT_TIME = 1000;
function* spawnTest() {
let { target, front } = yield initBackend(SIMPLE_URL);
let config = { withMarkers: true, withAllocations: true, withTicks: true };
yield front._request("memory", "attach");
let timelineStart1 = yield front._request("timeline", "start", config);
let memoryStart1 = yield front._request("memory", "startRecordingAllocations");
let timelineStart2 = yield front._request("timeline", "start", config);
let memoryStart2 = yield front._request("memory", "startRecordingAllocations");
let timelineStop = yield front._request("timeline", "stop");
let memoryStop = yield front._request("memory", "stopRecordingAllocations");
info(timelineStart1);
info(timelineStart2);
ok(typeof timelineStart1 === "number", "first timeline start returned a number");
ok(typeof timelineStart2 === "number", "second timeline start returned a number");
ok(typeof memoryStart1 === "number", "first memory start returned a number");
ok(typeof memoryStart2 === "number", "second memory start returned a number");
ok(timelineStart1 < timelineStart2, "can start timeline actor twice and get different start times");
ok(memoryStart1 < memoryStart2, "can start memory actor twice and get different start times");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -1,35 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test basic functionality of PerformanceFront
*/
let WAIT_TIME = 1000;
function* spawnTest() {
let { target, front } = yield initBackend(SIMPLE_URL);
let startModel = yield front.startRecording();
let { profilerStartTime, timelineStartTime, memoryStartTime } = startModel;
ok(startModel._profilerStartTime !== undefined,
"A `_profilerStartTime` property exists in the recording model.");
ok(startModel._timelineStartTime !== undefined,
"A `_timelineStartTime` property exists in the recording model.");
is(startModel._memoryStartTime, 0,
"A `_memoryStartTime` property exists in the recording model, but it's 0.");
yield busyWait(WAIT_TIME);
let stopModel = yield front.stopRecording(startModel);
ok(stopModel.getProfile(), "recording model has a profile after stopping.");
ok(stopModel.getDuration(), "recording model has a duration after stopping.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -1,40 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the built-in profiler module doesn't deactivate when the toolbox
* is destroyed if there are other consumers using it.
*/
let test = Task.async(function*() {
let { panel: firstPanel } = yield initPerformance(SIMPLE_URL);
let firstFront = firstPanel.panelWin.gFront;
let activated = firstFront.once("profiler-activated");
yield firstFront.startRecording();
yield activated;
let { panel: secondPanel } = yield initPerformance(SIMPLE_URL);
let secondFront = secondPanel.panelWin.gFront;
loadFrameScripts();
let alreadyActive = secondFront.once("profiler-already-active");
yield secondFront.startRecording();
yield alreadyActive;
// Manually teardown the tabs so we can check profiler status
let tab1 = firstPanel.target.tab;
let tab2 = secondPanel.target.tab;
yield firstPanel._toolbox.destroy();
yield removeTab(tab1);
ok((yield PMM_isProfilerActive()),
"The built-in profiler module should still be active.");
yield secondPanel._toolbox.destroy();
ok(!(yield PMM_isProfilerActive()),
"The built-in profiler module should no longer be active.");
yield removeTab(tab2);
tab1 = tab2 = null;
finish();
});

View File

@ -1,44 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the built-in profiler module is not reactivated if no other
* consumer was using it over the remote debugger protocol, and ensures
* that the actor will work properly even in such cases (e.g. the Gecko Profiler
* addon was installed and automatically activated the profiler module).
*/
let test = Task.async(function*() {
// Ensure the profiler is already running when the test starts.
loadFrameScripts();
let ENTRIES = 1000000;
let INTERVAL = 1;
let FEATURES = ["js"];
yield sendProfilerCommand("StartProfiler", [ENTRIES, INTERVAL, FEATURES, FEATURES.length]);
let { panel: firstPanel } = yield initPerformance(SIMPLE_URL);
let firstFront = firstPanel.panelWin.gFront;
let firstAlreadyActive = firstFront.once("profiler-already-active");
let recording = yield firstFront.startRecording();
yield firstAlreadyActive;
ok(recording._profilerStartTime > 0, "The profiler was not restarted.");
let { panel: secondPanel } = yield initPerformance(SIMPLE_URL);
let secondFront = secondPanel.panelWin.gFront;
let secondAlreadyActive = secondFront.once("profiler-already-active");
let secondRecording = yield secondFront.startRecording();
yield secondAlreadyActive;
ok(secondRecording._profilerStartTime > 0, "The profiler was not restarted.");
yield teardown(firstPanel);
ok((yield PMM_isProfilerActive()),
"The built-in profiler module should still be active.");
yield teardown(secondPanel);
ok(!(yield PMM_isProfilerActive()),
"The built-in profiler module should have been automatically stoped.");
finish();
});

View File

@ -6,19 +6,17 @@
* whether already loaded, or via console.profile with an unloaded performance tools.
*/
let { getPerformanceFront } = require("devtools/performance/front");
function* spawnTest() {
let profilerConnected = waitForProfilerConnection();
let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
yield profilerConnected;
let front = getPerformanceFront(target);
let front = toolbox.performance;
let tab = toolbox.doc.getElementById("toolbox-tab-performance");
let profileStart = once(front, "recording-started");
console.profile("rust");
yield profileStart;
yield waitUntil(() => tab.hasAttribute("highlighted"));
ok(tab.hasAttribute("highlighted"),
"performance tab is highlighted during recording from console.profile when unloaded");

View File

@ -6,8 +6,6 @@
* if on, and displays selected frames on focus.
*/
const RecordingUtils = require("devtools/performance/recording-utils");
Services.prefs.setBoolPref(INVERT_PREF, false);
function* spawnTest() {

View File

@ -6,9 +6,6 @@
* for meta nodes when viewing "content only".
*/
const { CATEGORY_MASK } = require("devtools/performance/global");
const RecordingUtils = require("devtools/performance/recording-utils");
Services.prefs.setBoolPref(INVERT_PREF, false);
Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);

View File

@ -18,19 +18,20 @@ let test = Task.async(function*() {
// Test mock memory
function *testMockMemory () {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL, "performance", {
TEST_MOCK_MEMORY_ACTOR: true,
TEST_PERFORMANCE_LEGACY_FRONT: true,
});
Services.prefs.setBoolPref(MEMORY_PREF, true);
Services.prefs.setBoolPref(FRAMERATE_PREF, true);
Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
let { EVENTS, $, gFront, PerformanceController, PerformanceView, DetailsView, WaterfallView } = panel.panelWin;
let { memory: memorySupport, timeline: timelineSupport } = gFront.getActorSupport();
yield startRecording(panel, { waitForOverview: false });
yield waitUntil(() => PerformanceController.getCurrentRecording().getTicks().length);
yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length);
yield stopRecording(panel, { waitForOverview: false });
ok(gFront.LEGACY_FRONT, "using legacy front");
let config = PerformanceController.getCurrentRecording().getConfiguration();
let {
markers, allocations, memory, ticks
@ -75,7 +76,7 @@ function *testMockMemory () {
// Test mock memory and timeline actor
function *testMockMemoryAndTimeline() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL, "performance", {
TEST_MOCK_MEMORY_ACTOR: true,
TEST_PERFORMANCE_LEGACY_FRONT: true,
TEST_MOCK_TIMELINE_ACTOR: true,
});
Services.prefs.setBoolPref(MEMORY_PREF, true);
@ -83,7 +84,6 @@ function *testMockMemoryAndTimeline() {
Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
let { EVENTS, $, gFront, PerformanceController, PerformanceView, DetailsView, WaterfallView } = panel.panelWin;
let { memory: memorySupport, timeline: timelineSupport } = gFront.getActorSupport();
yield startRecording(panel, { waitForOverview: false });
yield busyWait(WAIT_TIME);
yield stopRecording(panel, { waitForOverview: false });

View File

@ -10,15 +10,17 @@ const WAIT_TIME = 1000;
let test = Task.async(function*() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL, "performance", {
TEST_MOCK_MEMORY_ACTOR: true,
TEST_MOCK_TIMELINE_ACTOR: true
TEST_MOCK_TIMELINE_ACTOR: true,
TEST_PERFORMANCE_LEGACY_FRONT: true,
});
Services.prefs.setBoolPref(MEMORY_PREF, true);
let { EVENTS, $, gFront, PerformanceController, PerformanceView, DetailsView, JsCallTreeView } = panel.panelWin;
let { EVENTS, $, gFront: front, PerformanceController, PerformanceView, DetailsView, JsCallTreeView } = panel.panelWin;
let { memory: memorySupport, timeline: timelineSupport } = gFront.getActorSupport();
ok(!memorySupport, "memory should be mocked.");
ok(!timelineSupport, "timeline should be mocked.");
ok(front.LEGACY_FRONT, true, "Using legacy front");
is(front.traits.features.withMarkers, false, "traits.features.withMarkers is false.");
is(front.traits.features.withTicks, false, "traits.features.withTicks is false.");
is(front.traits.features.withMemory, false, "traits.features.withMemory is false.");
is(front.traits.features.withAllocations, false, "traits.features.withAllocations is false.");
yield startRecording(panel, { waitForOverview: false });
busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity

View File

@ -7,18 +7,21 @@
*/
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL, void 0, {
TEST_MOCK_PROFILER_CHECK_TIMER: 10,
TEST_PERFORMANCE_LEGACY_FRONT: true,
TEST_PROFILER_FILTER_STATUS: ["position", "totalSize", "generation"]
});
let { gFront: front, EVENTS, $, PerformanceController, PerformanceView, RecordingsView } = panel.panelWin;
let { gFront: front, EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
front.setProfilerStatusInterval(10);
yield startRecording(panel);
yield once(front._profiler, "profiler-status");
front.on("profiler-status", () => ok(false, "profiler-status should not be emitted when not supported"));
yield busyWait(100);
ok(!$("#details-pane-container").getAttribute("buffer-status"),
"container does not have [buffer-status] attribute when not supported");
yield once(front._profiler, "profiler-status");
yield busyWait(100);
ok(!$("#details-pane-container").getAttribute("buffer-status"),
"container does not have [buffer-status] attribute when not supported");

View File

@ -10,15 +10,16 @@ const WAIT_TIME = 1000;
let test = Task.async(function*() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL, "performance", {
TEST_MOCK_MEMORY_ACTOR: true
TEST_PERFORMANCE_LEGACY_FRONT: true
});
Services.prefs.setBoolPref(MEMORY_PREF, true);
let { EVENTS, $, gFront, PerformanceController, PerformanceView, DetailsView, WaterfallView } = panel.panelWin;
let { EVENTS, $, gFront: front, PerformanceController, PerformanceView, DetailsView, WaterfallView } = panel.panelWin;
let { memory: memorySupport, timeline: timelineSupport } = gFront.getActorSupport();
ok(!memorySupport, "memory should be mocked.");
ok(timelineSupport, "timeline should not be mocked.");
ok(front.LEGACY_FRONT, true, "Using legacy front");
is(front.traits.features.withMarkers, true, "traits.features.withMarkers is true.");
is(front.traits.features.withTicks, true, "traits.features.withTicks is true.");
is(front.traits.features.withMemory, false, "traits.features.withMemory is false.");
is(front.traits.features.withAllocations, false, "traits.features.withAllocations is false.");
yield startRecording(panel);
yield busyWait(100);

View File

@ -10,7 +10,9 @@
const WAIT_TIME = 1000; // ms
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let { panel } = yield initPerformance(SIMPLE_URL, void 0, {
TEST_PERFORMANCE_LEGACY_FRONT: true,
});
let { gFront: front, gTarget: target } = panel.panelWin;
// Explicitly override the profiler's trait `filterable`

View File

@ -0,0 +1,48 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the profiler is populated by in-progress console recordings
* when it is opened with the legacy front.
*/
let WAIT_TIME = 10;
function* spawnTest() {
let { target, toolbox, console } = yield initConsole(SIMPLE_URL, { TEST_PERFORMANCE_LEGACY_FRONT: true });
let front = toolbox.performance;
let profileStart = once(front, "recording-started");
console.profile("rust");
yield profileStart;
profileStart = once(front, "recording-started");
console.profile("rust2");
yield profileStart;
yield gDevTools.showToolbox(target, "performance");
let panel = toolbox.getCurrentPanel();
let { panelWin: { PerformanceController, RecordingsView }} = panel;
yield waitUntil(() => PerformanceController.getRecordings().length === 2);
let recordings = PerformanceController.getRecordings();
is(recordings.length, 2, "two recordings found in the performance panel.");
is(recordings[0].isConsole(), true, "recording came from console.profile (1).");
is(recordings[0].getLabel(), "rust", "correct label in the recording model (1).");
is(recordings[0].isRecording(), true, "recording is still recording (1).");
is(recordings[1].isConsole(), true, "recording came from console.profile (2).");
is(recordings[1].getLabel(), "rust2", "correct label in the recording model (2).");
is(recordings[1].isRecording(), true, "recording is still recording (2).");
is(RecordingsView.selectedItem.attachment, recordings[0],
"The first console recording should be selected.");
let profileEnd = once(front, "recording-stopped");
console.profileEnd("rust");
yield profileEnd;
profileEnd = once(front, "recording-stopped");
console.profileEnd("rust2");
yield profileEnd;
yield teardown(panel);
finish();
}

View File

@ -21,21 +21,21 @@ let test = Task.async(function*() {
"The duration node should show the 'recording' message while recording");
info("Stop the recording and wait for the WILL_STOP and STOPPED events");
let clicked = PerformanceView.once(EVENTS.UI_STOP_RECORDING);
let willStop = PerformanceController.once(EVENTS.RECORDING_WILL_STOP);
let hasStopped = PerformanceController.once(EVENTS.RECORDING_STOPPED);
let stoppingRecording = PerformanceController.stopRecording();
click(panel.panelWin, $("#main-record-button"));
yield clicked;
yield willStop;
is(durationNode.getAttribute("value"),
L10N.getStr("recordingsList.loadingLabel"),
"The duration node should show the 'loading' message while stopping");
let stateChanged = once(PerformanceView, EVENTS.UI_STATE_CHANGED);
yield hasStopped;
yield stateChanged;
yield stoppingRecording;
ok(PerformanceController.getCurrentRecording().isCompleted(), "recording should be completed");
let duration = RecordingsView.selectedItem.attachment.getDuration().toFixed(0);
is(durationNode.getAttribute("value"),
@ -43,6 +43,7 @@ let test = Task.async(function*() {
"The duration node should show the duration after the record has stopped");
yield PerformanceController.clearRecordings();
yield teardown(panel);
finish();
});

View File

@ -25,20 +25,17 @@ let test = Task.async(function*() {
"The recording-notice is shown while recording");
info("Stop the recording and wait for the WILL_STOP and STOPPED events");
let clicked = PerformanceView.once(EVENTS.UI_STOP_RECORDING);
let willStop = PerformanceController.once(EVENTS.RECORDING_WILL_STOP);
let hasStopped = PerformanceController.once(EVENTS.RECORDING_STOPPED);
let stoppingRecording = PerformanceController.stopRecording();
click(panel.panelWin, $("#main-record-button"));
yield clicked;
yield willStop;
is(detailsContainer.selectedPanel, loadingNotice,
"The loading-notice is shown while the record is stopping");
let stateChanged = once(PerformanceView, EVENTS.UI_STATE_CHANGED);
yield hasStopped;
yield stateChanged;
yield stoppingRecording;
is(detailsContainer.selectedPanel, detailsPane,
"The details panel is shown after the record has stopped");
@ -52,21 +49,18 @@ let test = Task.async(function*() {
yield select;
info("Stop the 2nd recording and wait for the WILL_STOP and STOPPED events");
clicked = PerformanceView.once(EVENTS.UI_STOP_RECORDING);
willStop = PerformanceController.once(EVENTS.RECORDING_WILL_STOP);
hasStopped = PerformanceController.once(EVENTS.RECORDING_STOPPED);
stoppingRecording = PerformanceController.stopRecording();
click(panel.panelWin, $("#main-record-button"));
yield clicked;
yield willStop;
is(detailsContainer.selectedPanel, detailsPane,
"The details panel is still shown while the 2nd record is being stopped");
is(RecordingsView.selectedIndex, 0, "The first record is still selected");
stateChanged = once(PerformanceView, EVENTS.UI_STATE_CHANGED);
yield hasStopped;
yield stateChanged;
yield stoppingRecording;
is(detailsContainer.selectedPanel, detailsPane,
"The details panel is still shown after the 2nd record has stopped");

View File

@ -18,7 +18,7 @@ function* spawnTest() {
yield startRecording(panel);
let { probability, maxLogLength } = yield gFront._request("memory", "getAllocationsSettings");
let { probability, maxLogLength } = yield gFront.getConfiguration();
yield stopRecording(panel);

View File

@ -14,7 +14,7 @@ function* spawnTest() {
yield startRecording(panel);
let { entries, interval } = yield gFront._request("profiler", "getStartOptions");
let { entries, interval } = yield gFront.getConfiguration();
yield stopRecording(panel);

View File

@ -10,17 +10,20 @@ function* spawnTest() {
loadFrameScripts();
// Keep it large, but still get to 1% relatively quick
Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000000);
let { panel } = yield initPerformance(SIMPLE_URL, void 0, { TEST_MOCK_PROFILER_CHECK_TIMER: 10 });
let { EVENTS, $, PerformanceController, PerformanceView, RecordingsView } = panel.panelWin;
let { panel } = yield initPerformance(SIMPLE_URL);
let { gFront, EVENTS, $, PerformanceController, PerformanceView, RecordingsView } = panel.panelWin;
// Set a fast profiler-status update interval
yield gFront.setProfilerStatusInterval(10);
yield startRecording(panel);
let percent = 0;
while (percent === 0) {
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_UPDATED);
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_STATUS_UPDATED);
}
let bufferUsage = PerformanceController.getCurrentRecording().getBufferUsage();
let bufferUsage = PerformanceController.getBufferUsageForRecording(PerformanceController.getCurrentRecording());
is($("#details-pane-container").getAttribute("buffer-status"), "in-progress",
"container has [buffer-status=in-progress]");
ok($("#recording-notice .buffer-status-message").value.indexOf(percent + "%") !== -1,
@ -31,11 +34,11 @@ function* spawnTest() {
percent = 0;
while (percent <= (Math.floor(bufferUsage * 100))) {
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_UPDATED);
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_STATUS_UPDATED);
}
ok(percent > Math.floor(bufferUsage * 100), "buffer percentage increased in display");
bufferUsage = PerformanceController.getCurrentRecording().getBufferUsage();
bufferUsage = PerformanceController.getBufferUsageForRecording(PerformanceController.getCurrentRecording());
is($("#details-pane-container").getAttribute("buffer-status"), "in-progress",
"container has [buffer-status=in-progress]");
@ -45,7 +48,7 @@ function* spawnTest() {
RecordingsView.selectedIndex = 1;
percent = 0;
while (percent === 0) {
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_UPDATED);
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_STATUS_UPDATED);
}
ok(percent < Math.floor(bufferUsage * 100), "percentage updated for newly selected recording");
@ -59,7 +62,7 @@ function* spawnTest() {
percent = 0;
while (percent <= (Math.floor(bufferUsage * 100))) {
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_UPDATED);
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_STATUS_UPDATED);
}
ok(percent > Math.floor(bufferUsage * 100), "percentage increased for original recording");
is($("#details-pane-container").getAttribute("buffer-status"), "in-progress",

View File

@ -6,22 +6,24 @@
* a class is assigned to the recording notices.
*/
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL, void 0, { TEST_MOCK_PROFILER_CHECK_TIMER: 10 });
let { EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
let { panel } = yield initPerformance(SIMPLE_URL);
let { gFront, EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
// Make sure the profiler module is stopped so we can set a new buffer limit
loadFrameScripts();
PMM_loadProfilerScripts(gBrowser);
yield PMM_stopProfiler();
Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000);
// Set a fast profiler-status update interval
yield gFront.setProfilerStatusInterval(10);
yield startRecording(panel);
let percent = 0;
while (percent < 100) {
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_UPDATED);
[,percent] = yield onceSpread(PerformanceView, EVENTS.UI_BUFFER_STATUS_UPDATED);
}
let bufferUsage = PerformanceController.getCurrentRecording().getBufferUsage();
let bufferUsage = PerformanceController.getBufferUsageForRecording(PerformanceController.getCurrentRecording());
ok(bufferUsage, 1, "Buffer is full for this recording");
ok($("#details-pane-container").getAttribute("buffer-status"), "full",
"container has [buffer-status=full]");

View File

@ -6,8 +6,11 @@
* a class is assigned to the recording notices.
*/
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL, void 0, { TEST_MOCK_PROFILER_CHECK_TIMER: 10 });
let { EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
let { panel } = yield initPerformance(SIMPLE_URL);
let { gFront, EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
// Set a fast profiler-status update interval
yield gFront.setProfilerStatusInterval(10);
let supported = false;
let enabled = false;

View File

@ -1,38 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the shared PerformanceFront is only opened once.
*/
let gProfilerConnectionsOpened = 0;
Services.obs.addObserver(profilerConnectionObserver, "performance-tools-connection-opened", false);
function* spawnTest() {
let { target, panel } = yield initPerformance(SIMPLE_URL);
is(gProfilerConnectionsOpened, 1,
"Only one profiler connection was opened.");
let front = getPerformanceFront(target);
ok(front, "A front for the current toolbox was retrieved.");
is(panel.panelWin.gFront, front, "The same front is used by the panel's front");
yield front.open();
is(gProfilerConnectionsOpened, 1,
"No additional profiler connections were opened.");
yield teardown(panel);
finish();
}
function profilerConnectionObserver(subject, topic, data) {
is(topic, "performance-tools-connection-opened", "The correct topic was observed.");
gProfilerConnectionsOpened++;
}
registerCleanupFunction(() => {
Services.obs.removeObserver(profilerConnectionObserver, "performance-tools-connection-opened");
});

View File

@ -1,31 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the shared PerformanceFront can properly send requests.
*/
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let front = panel.panelWin.gFront;
loadFrameScripts();
ok(!(yield PMM_isProfilerActive()),
"The built-in profiler module should not have been automatically started.");
let result = yield front._request("profiler", "startProfiler");
is(result.started, true,
"The request finished successfully and the profiler should've been started.");
ok((yield PMM_isProfilerActive()),
"The built-in profiler module should now be active.");
result = yield front._request("profiler", "stopProfiler");
is(result.started, false,
"The request finished successfully and the profiler should've been stopped.");
ok(!(yield PMM_isProfilerActive()),
"The built-in profiler module should now be inactive.");
yield teardown(panel);
finish();
}

View File

@ -11,7 +11,7 @@ function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let { EVENTS, PerformanceController } = panel.panelWin;
let front = panel.panelWin.gFront;
loadFrameScripts();
PMM_loadProfilerScripts(gBrowser);
ok(!(yield PMM_isProfilerActive()),
"The built-in profiler module should not have been automatically started.");

View File

@ -5,8 +5,6 @@
* Tests if the performance tool can import profiler data from the
* original profiler tool (Performance Recording v1, and Profiler data v2) and the correct views and graphs are loaded.
*/
let RecordingUtils = require("devtools/performance/recording-utils");
let TICKS_DATA = (function () {
let ticks = [];
for (let i = 0; i < 100; i++) {

View File

@ -6,8 +6,6 @@
* and requires deflating, and has an extra thread that's a string. Not sure
* what causes this.
*/
let RecordingUtils = require("devtools/performance/recording-utils");
let STRINGED_THREAD = (function () {
let thread = {};

View File

@ -6,8 +6,7 @@
* icon is next to the frame with optimizations
*/
const RecordingUtils = require("devtools/performance/recording-utils");
const { CATEGORY_MASK } = require("devtools/performance/global");
let { CATEGORY_MASK } = require("devtools/performance/global");
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);

View File

@ -15,8 +15,11 @@ const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
let { DebuggerServer } = require("devtools/server/main");
let { console } = require("resource://gre/modules/devtools/Console.jsm");
let { merge } = require("sdk/util/object");
let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
let { getPerformanceFront, PerformanceFront } = require("devtools/performance/front");
let { createPerformanceFront } = require("devtools/server/actors/performance");
let RecordingUtils = require("devtools/toolkit/performance/utils");
let {
PMM_loadProfilerScripts, PMM_isProfilerActive, PMM_stopProfiler, sendProfilerCommand
} = require("devtools/toolkit/shared/profiler");
let mm = null;
@ -188,17 +191,16 @@ function initBackend(aUrl, targetOps={}) {
yield target.makeRemote();
// Attach addition options to `target`. This is used to force mock fronts
// Attach addition options to `client`. This is used to force mock fronts
// to smokescreen test different servers where memory or timeline actors
// may not exist. Possible options that will actually work:
// TEST_MOCK_MEMORY_ACTOR = true
// TEST_PERFORMANCE_LEGACY_FRONT = true
// TEST_MOCK_TIMELINE_ACTOR = true
// TEST_MOCK_PROFILER_CHECK_TIMER = number
// TEST_PROFILER_FILTER_STATUS = array
merge(target, targetOps);
let front = getPerformanceFront(target);
yield front.open();
let front = createPerformanceFront(target);
yield front.connect();
return { target, front };
});
}
@ -212,12 +214,11 @@ function initPerformance(aUrl, tool="performance", targetOps={}) {
yield target.makeRemote();
// Attach addition options to `target`. This is used to force mock fronts
// Attach addition options to `client`. This is used to force mock fronts
// to smokescreen test different servers where memory or timeline actors
// may not exist. Possible options that will actually work:
// TEST_MOCK_MEMORY_ACTOR = true
// TEST_PERFORMANCE_LEGACY_FRONT = true
// TEST_MOCK_TIMELINE_ACTOR = true
// TEST_MOCK_PROFILER_CHECK_TIMER = number
// TEST_PROFILER_FILTER_STATUS = array
merge(target, targetOps);
@ -231,9 +232,9 @@ function initPerformance(aUrl, tool="performance", targetOps={}) {
* Initializes a webconsole panel. Returns a target, panel and toolbox reference.
* Also returns a console property that allows calls to `profile` and `profileEnd`.
*/
function initConsole(aUrl) {
function initConsole(aUrl, options) {
return Task.spawn(function*() {
let { target, toolbox, panel } = yield initPerformance(aUrl, "webconsole");
let { target, toolbox, panel } = yield initPerformance(aUrl, "webconsole", options);
let { hud } = panel;
return {
target, toolbox, panel, console: {
@ -265,13 +266,6 @@ function consoleExecute (console, method, val) {
return promise;
}
function waitForProfilerConnection() {
let { promise, resolve } = Promise.defer();
Services.obs.addObserver(resolve, "performance-tools-connection-opened", false);
return promise.then(() =>
Services.obs.removeObserver(resolve, "performance-tools-connection-opened"));
}
function* teardown(panel) {
info("Destroying the performance tool.");
@ -337,9 +331,12 @@ function* startRecording(panel, options = {
}) {
let win = panel.panelWin;
let clicked = panel.panelWin.PerformanceView.once(win.EVENTS.UI_START_RECORDING);
let willStart = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_WILL_START);
let hasStarted = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_STARTED);
let button = win.$("#main-record-button");
let stateChanged = options.waitForStateChanged
? once(win.PerformanceView, win.EVENTS.UI_STATE_CHANGED)
: Promise.resolve();
ok(!button.hasAttribute("checked"),
"The record button should not be checked yet.");
@ -349,25 +346,17 @@ function* startRecording(panel, options = {
click(win, button);
yield clicked;
yield willStart;
ok(button.hasAttribute("checked"),
"The record button should now be checked.");
ok(button.hasAttribute("locked"),
"The record button should be locked.");
let stateChanged = options.waitForStateChanged
? once(win.PerformanceView, win.EVENTS.UI_STATE_CHANGED)
: Promise.resolve();
yield hasStarted;
let overviewRendered = options.waitForOverview
yield options.waitForOverview
? once(win.OverviewView, win.EVENTS.OVERVIEW_RENDERED)
: Promise.resolve();
yield stateChanged;
yield overviewRendered;
is(win.PerformanceView.getState(), "recording",
"The current state is 'recording'.");
@ -386,6 +375,7 @@ function* stopRecording(panel, options = {
let clicked = panel.panelWin.PerformanceView.once(win.EVENTS.UI_STOP_RECORDING);
let willStop = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_WILL_STOP);
let hasStopped = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_STOPPED);
let controllerStopped = panel.panelWin.PerformanceController.once(win.EVENTS.CONTROLLER_STOPPED_RECORDING);
let button = win.$("#main-record-button");
let overviewRendered = null;
@ -427,6 +417,8 @@ function* stopRecording(panel, options = {
"The record button should not be checked.");
ok(!button.hasAttribute("locked"),
"The record button should not be locked.");
yield controllerStopped;
}
function waitForWidgetsRendered(panel) {
@ -546,8 +538,6 @@ function getInflatedStackLocations(thread, sample) {
* Synthesize a profile for testing.
*/
function synthesizeProfileForTest(samples) {
const RecordingUtils = require("devtools/performance/recording-utils");
samples.unshift({
time: 0,
frames: [
@ -561,39 +551,3 @@ function synthesizeProfileForTest(samples) {
markers: []
}, uniqueStacks);
}
function PMM_isProfilerActive () {
return sendProfilerCommand("IsActive");
}
function PMM_stopProfiler () {
return Task.spawn(function*() {
let isActive = (yield sendProfilerCommand("IsActive")).isActive;
if (isActive) {
return sendProfilerCommand("StopProfiler");
}
});
}
function sendProfilerCommand (method, args=[]) {
let deferred = Promise.defer();
if (!mm) {
throw new Error("`loadFrameScripts()` must be called when using MessageManager.");
}
let id = generateUUID().toString();
mm.addMessageListener("devtools:test:profiler:response", handler);
mm.sendAsyncMessage("devtools:test:profiler", { method, args, id });
function handler ({ data }) {
if (id !== data.id) {
return;
}
mm.removeMessageListener("devtools:test:profiler:response", handler);
deferred.resolve(data.data);
}
return deferred.promise;
}

View File

@ -7,7 +7,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
let { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
let { console } = require("resource://gre/modules/devtools/Console.jsm");
const RecordingUtils = require("devtools/performance/recording-utils");
const RecordingUtils = require("devtools/toolkit/performance/utils");
const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";

View File

@ -16,7 +16,7 @@ let DetailsSubview = {
this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
this._onPrefChanged = this._onPrefChanged.bind(this);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStoppedOrSelected);
PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
@ -29,7 +29,7 @@ let DetailsSubview = {
destroy: function () {
clearNamedTimeout("range-change-debounce");
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStoppedOrSelected);
PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
@ -85,7 +85,14 @@ let DetailsSubview = {
/**
* Called when recording stops or is selected.
*/
_onRecordingStoppedOrSelected: function(_, recording) {
_onRecordingStoppedOrSelected: function(_, state, recording) {
if (typeof state !== "string") {
recording = state;
}
if (arguments.length === 3 && state !== "recording-stopped") {
return;
}
if (!recording || !recording.isCompleted()) {
return;
}

View File

@ -60,7 +60,7 @@ let DetailsView = {
yield this.setAvailableViews();
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStoppedOrSelected);
PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
PerformanceController.on(EVENTS.PREF_CHANGED, this.setAvailableViews);
}),
@ -77,7 +77,7 @@ let DetailsView = {
component.initialized && (yield component.view.destroy());
}
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStoppedOrSelected);
PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
PerformanceController.off(EVENTS.PREF_CHANGED, this.setAvailableViews);
}),
@ -248,7 +248,10 @@ let DetailsView = {
/**
* Called when recording stops or is selected.
*/
_onRecordingStoppedOrSelected: function(_, recording) {
_onRecordingStoppedOrSelected: function(_, state, recording) {
if (typeof state === "string" && state !== "recording-stopped") {
return;
}
this.setAvailableViews();
},

View File

@ -44,7 +44,7 @@ let OverviewView = {
});
// If no timeline support, shut it all down.
if (!gFront.getActorSupport().timeline) {
if (!PerformanceController.getTraits().features.withMarkers) {
this.disable();
return;
}
@ -52,10 +52,7 @@ let OverviewView = {
// Store info on multiprocess support.
this._multiprocessData = PerformanceController.getMultiprocessStatus();
this._onRecordingWillStart = this._onRecordingWillStart.bind(this);
this._onRecordingStarted = this._onRecordingStarted.bind(this);
this._onRecordingWillStop = this._onRecordingWillStop.bind(this);
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
this._onRecordingSelected = this._onRecordingSelected.bind(this);
this._onRecordingTick = this._onRecordingTick.bind(this);
this._onGraphSelecting = this._onGraphSelecting.bind(this);
@ -67,10 +64,7 @@ let OverviewView = {
// based off of prefs.
PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
PerformanceController.on(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.on(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
this.graphs.on("selecting", this._onGraphSelecting);
this.graphs.on("rendered", this._onGraphRendered);
@ -82,10 +76,7 @@ let OverviewView = {
destroy: Task.async(function*() {
PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
PerformanceController.off(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.off(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
this.graphs.off("selecting", this._onGraphSelecting);
this.graphs.off("rendered", this._onGraphRendered);
@ -211,29 +202,12 @@ let OverviewView = {
},
/**
* Called when recording will start. No recording because it does not
* exist yet, but can just disable from here. This will only trigger for
* manual recordings.
* Called when recording state changes.
*/
_onRecordingWillStart: OverviewViewOnStateChange(Task.async(function* () {
yield this._checkSelection();
this.graphs.dropSelection();
})),
/**
* Called when recording actually starts.
*/
_onRecordingStarted: OverviewViewOnStateChange(),
/**
* Called when recording will stop.
*/
_onRecordingWillStop: OverviewViewOnStateChange(),
/**
* Called when recording actually stops.
*/
_onRecordingStopped: OverviewViewOnStateChange(Task.async(function* (_, recording) {
_onRecordingStateChange: OverviewViewOnStateChange(Task.async(function* (_, state, recording) {
if (state !== "recording-stopped") {
return;
}
// Check to see if the recording that just stopped is the current recording.
// If it is, render the high-res graphs. For manual recordings, it will also
// be the current recording, but profiles generated by `console.profile` can stop
@ -394,6 +368,12 @@ let OverviewView = {
*/
function OverviewViewOnStateChange (fn) {
return function _onRecordingStateChange (eventName, recording) {
// Normalize arguments for the RECORDING_STATE_CHANGE event,
// as it also has a `state` argument.
if (typeof recording === "string") {
recording = arguments[2];
}
let currentRecording = PerformanceController.getCurrentRecording();
// All these methods require a recording to exist selected and
@ -426,6 +406,7 @@ function OverviewViewOnStateChange (fn) {
} else if (currentRecording.isRecording() && !this.isRendering()) {
this._startPolling();
}
if (fn) {
fn.apply(this, arguments);
}

View File

@ -14,19 +14,15 @@ let RecordingsView = Heritage.extend(WidgetMethods, {
this.widget = new SideMenuWidget($("#recordings-list"));
this._onSelect = this._onSelect.bind(this);
this._onRecordingStarted = this._onRecordingStarted.bind(this);
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onRecordingWillStop = this._onRecordingWillStop.bind(this);
this._onRecordingImported = this._onRecordingImported.bind(this);
this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
this._onNewRecording = this._onNewRecording.bind(this);
this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
this._onRecordingsCleared = this._onRecordingsCleared.bind(this);
this.emptyText = L10N.getStr("noRecordingsText");
PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.on(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
PerformanceController.on(EVENTS.RECORDING_IMPORTED, this._onRecordingImported);
PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
PerformanceController.on(EVENTS.NEW_RECORDING, this._onNewRecording);
PerformanceController.on(EVENTS.RECORDINGS_CLEARED, this._onRecordingsCleared);
this.widget.addEventListener("select", this._onSelect, false);
},
@ -35,10 +31,8 @@ let RecordingsView = Heritage.extend(WidgetMethods, {
* Destruction function, called when the tool is closed.
*/
destroy: function() {
PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.off(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
PerformanceController.off(EVENTS.RECORDING_IMPORTED, this._onRecordingImported);
PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
PerformanceController.off(EVENTS.NEW_RECORDING, this._onNewRecording);
PerformanceController.off(EVENTS.RECORDINGS_CLEARED, this._onRecordingsCleared);
this.widget.removeEventListener("select", this._onSelect, false);
},
@ -92,81 +86,59 @@ let RecordingsView = Heritage.extend(WidgetMethods, {
},
/**
* Signals that a recording session has started.
* Called when a new recording is stored in the UI. This handles
* when recordings are lazily loaded (like a console.profile occurring
* before the tool is loaded) or imported. In normal manual recording cases,
* this will also be fired.
*/
_onNewRecording: function (_, recording) {
this._onRecordingStateChange(_, null, recording);
},
/**
* Signals that a recording has changed state.
*
* @param string state
* Can be "recording-started", "recording-stopped", "recording-stopping"
* @param RecordingModel recording
* Model of the recording that was started.
*/
_onRecordingStarted: function (_, recording) {
// TODO bug 1144388
// If a label is identical to an existing recording item,
// logically group them here.
// For now, insert a "dummy" recording item, to hint that recording has now started.
let recordingItem = this.addEmptyRecording(recording);
_onRecordingStateChange: function (_, state, recording) {
let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
if (!recordingItem) {
recordingItem = this.addEmptyRecording(recording);
// Mark the corresponding item as being a "record in progress".
recordingItem.isRecording = true;
// If this is a manual recording, immediately select it, or
// select a console profile if its the only one
if (!recording.isConsole() || this.selectedIndex === -1) {
this.selectedItem = recordingItem;
}
}
// If this is a manual recording, immediately select it, or
// select a console profile if its the only one
if (!recording.isConsole() || this.selectedIndex === -1) {
recordingItem.isRecording = recording.isRecording();
// This recording is in the process of stopping.
if (!recording.isRecording() && !recording.isCompleted()) {
// Mark the corresponding item as loading.
let durationNode = $(".recording-item-duration", recordingItem.target);
durationNode.setAttribute("value", L10N.getStr("recordingsList.loadingLabel"));
}
// Render the recording item with finalized information (timing, etc)
if (recording.isCompleted() && !recordingItem.finalized) {
this.finalizeRecording(recordingItem);
// Select the recording if it was a manual recording only
if (!recording.isConsole()) {
this.forceSelect(recordingItem);
}
}
// Auto select imported items.
if (recording.isImported()) {
this.selectedItem = recordingItem;
}
},
/**
* Signals that a recording session has ended.
*
* @param RecordingModel recording
* The model of the recording that just stopped.
*/
_onRecordingStopped: function (_, recording) {
let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
// Mark the corresponding item as being a "finished recording".
recordingItem.isRecording = false;
// Render the recording item with finalized information (timing, etc)
this.finalizeRecording(recordingItem);
// Select the recording if it was a manual recording only
if (!recording.isConsole()) {
this.forceSelect(recordingItem);
}
},
/**
* Signals that a recording session is ending, and hasn't finished being
* processed yet.
*
* @param RecordingModel recording
* The model of the recording that is being stopped.
*/
_onRecordingWillStop: function(_, recording) {
let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
// Mark the corresponding item as loading.
let durationNode = $(".recording-item-duration", recordingItem.target);
durationNode.setAttribute("value", L10N.getStr("recordingsList.loadingLabel"));
},
/**
* Signals that a recording has been imported.
*
* @param RecordingModel model
* The recording model containing data on the recording session.
*/
_onRecordingImported: function (_, model) {
let recordingItem = this.addEmptyRecording(model);
recordingItem.isRecording = false;
// Immediately select the imported recording
this.selectedItem = recordingItem;
// Render the recording item with finalized information (timing, etc)
this.finalizeRecording(recordingItem);
},
/**
* Clears out all recordings.
*/
@ -182,6 +154,7 @@ let RecordingsView = Heritage.extend(WidgetMethods, {
*/
finalizeRecording: function (recordingItem) {
let model = recordingItem.attachment;
recordingItem.finalized = true;
let saveNode = $(".recording-item-save", recordingItem.target);
saveNode.setAttribute("value",

View File

@ -251,7 +251,7 @@ function* openAndCloseToolbox(nbOfTimes, usageTime, toolId) {
* Synthesize a profile for testing.
*/
function synthesizeProfileForTest(samples) {
const RecordingUtils = require("devtools/performance/recording-utils");
const RecordingUtils = require("devtools/toolkit/performance/utils");
samples.unshift({
time: 0,

View File

@ -11,6 +11,7 @@ DIRS += [
'discovery',
'gcli',
'jsbeautify',
'performance',
'pretty-fast',
'qrcode',
'security',

View File

@ -0,0 +1,164 @@
/* 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");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "RecordingUtils",
"devtools/toolkit/performance/utils");
loader.lazyImporter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
loader.lazyImporter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
// This identifier string is used to tentatively ascertain whether or not
// a JSON loaded from disk is actually something generated by this tool.
// It isn't, of course, a definitive verification, but a Good Enough™
// approximation before continuing the import. Don't localize this.
const PERF_TOOL_SERIALIZER_IDENTIFIER = "Recorded Performance Data";
const PERF_TOOL_SERIALIZER_LEGACY_VERSION = 1;
const PERF_TOOL_SERIALIZER_CURRENT_VERSION = 2;
/**
* Helpers for importing/exporting JSON.
*/
/**
* Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
* @return object
*/
function getUnicodeConverter () {
let className = "@mozilla.org/intl/scriptableunicodeconverter";
let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
return converter;
}
/**
* Saves a recording as JSON to a file. The provided data is assumed to be
* acyclical, so that it can be properly serialized.
*
* @param object recordingData
* The recording data to stream as JSON.
* @param nsILocalFile file
* The file to stream the data into.
* @return object
* A promise that is resolved once streaming finishes, or rejected
* if there was an error.
*/
function saveRecordingToFile (recordingData, file) {
let deferred = promise.defer();
recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER;
recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION;
let string = JSON.stringify(recordingData);
let inputStream = this.getUnicodeConverter().convertToInputStream(string);
let outputStream = FileUtils.openSafeFileOutputStream(file);
NetUtil.asyncCopy(inputStream, outputStream, deferred.resolve);
return deferred.promise;
}
/**
* Loads a recording stored as JSON from a file.
*
* @param nsILocalFile file
* The file to import the data from.
* @return object
* A promise that is resolved once importing finishes, or rejected
* if there was an error.
*/
function loadRecordingFromFile (file) {
let deferred = promise.defer();
let channel = NetUtil.newChannel({
uri: NetUtil.newURI(file),
loadUsingSystemPrincipal: true});
channel.contentType = "text/plain";
NetUtil.asyncFetch(channel, (inputStream, status) => {
try {
let string = NetUtil.readInputStreamToString(inputStream, inputStream.available());
var recordingData = JSON.parse(string);
} catch (e) {
deferred.reject(new Error("Could not read recording data file."));
return;
}
if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
deferred.reject(new Error("Unrecognized recording data file."));
return;
}
if (!isValidSerializerVersion(recordingData.version)) {
deferred.reject(new Error("Unsupported recording data file version."));
return;
}
if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
recordingData = convertLegacyData(recordingData);
}
if (recordingData.profile.meta.version === 2) {
RecordingUtils.deflateProfile(recordingData.profile);
}
deferred.resolve(recordingData);
});
return deferred.promise;
}
/**
* Returns a boolean indicating whether or not the passed in `version`
* is supported by this serializer.
*
* @param number version
* @return boolean
*/
function isValidSerializerVersion (version) {
return !!~[
PERF_TOOL_SERIALIZER_LEGACY_VERSION,
PERF_TOOL_SERIALIZER_CURRENT_VERSION
].indexOf(version);
}
/**
* Takes recording data (with version `1`, from the original profiler tool), and
* massages the data to be line with the current performance tool's property names
* and values.
*
* @param object legacyData
* @return object
*/
function convertLegacyData (legacyData) {
let { profilerData, ticksData, recordingDuration } = legacyData;
// The `profilerData` and `ticksData` stay, but the previously unrecorded
// fields just are empty arrays or objects.
let data = {
label: profilerData.profilerLabel,
duration: recordingDuration,
markers: [],
frames: [],
memory: [],
ticks: ticksData,
allocations: { sites: [], timestamps: [], frames: [] },
profile: profilerData.profile,
// Fake a configuration object here if there's tick data,
// so that it can be rendered
configuration: {
withTicks: !!ticksData.length,
withMarkers: false,
withMemory: false,
withAllocations: false
}
};
return data;
}
exports.getUnicodeConverter = getUnicodeConverter;
exports.saveRecordingToFile = saveRecordingToFile;
exports.loadRecordingFromFile = loadRecordingFromFile;

View File

@ -12,27 +12,17 @@ loader.lazyRequireGetter(this, "Poller",
"devtools/shared/poller", true);
loader.lazyRequireGetter(this, "CompatUtils",
"devtools/performance/compatibility");
"devtools/toolkit/performance/legacy/compatibility");
loader.lazyRequireGetter(this, "RecordingUtils",
"devtools/performance/recording-utils");
"devtools/toolkit/performance/utils");
loader.lazyRequireGetter(this, "TimelineFront",
"devtools/server/actors/timeline", true);
loader.lazyRequireGetter(this, "MemoryFront",
"devtools/server/actors/memory", true);
loader.lazyRequireGetter(this, "ProfilerFront",
"devtools/server/actors/profiler", true);
// how often do we pull allocation sites from the memory actor
const ALLOCATION_SITE_POLL_TIMER = 200; // ms
// how often do we check the status of the profiler's circular buffer
const PROFILER_CHECK_TIMER = 5000; // ms
const MEMORY_ACTOR_METHODS = [
"attach", "detach", "getState", "getAllocationsSettings",
"getAllocations", "startRecordingAllocations", "stopRecordingAllocations"
];
const TIMELINE_ACTOR_METHODS = [
"start", "stop",
];
@ -45,7 +35,7 @@ const PROFILER_ACTOR_METHODS = [
/**
* Constructor for a facade around an underlying ProfilerFront.
*/
function ProfilerFrontFacade (target) {
function LegacyProfilerFront (target) {
this._target = target;
this._onProfilerEvent = this._onProfilerEvent.bind(this);
this._checkProfilerStatus = this._checkProfilerStatus.bind(this);
@ -54,7 +44,7 @@ function ProfilerFrontFacade (target) {
EventEmitter.decorate(this);
}
ProfilerFrontFacade.prototype = {
LegacyProfilerFront.prototype = {
EVENTS: ["console-api-profiler", "profiler-stopped"],
// Connects to the targets underlying real ProfilerFront.
@ -70,7 +60,7 @@ ProfilerFrontFacade.prototype = {
// Directly register to event notifications when connected
// to hook into `console.profile|profileEnd` calls.
yield this.registerEventNotifications({ events: this.EVENTS });
this.EVENTS.forEach(e => this._front.on(e, this._onProfilerEvent));
target.client.addListener("eventNotification", this._onProfilerEvent);
}),
/**
@ -80,10 +70,8 @@ ProfilerFrontFacade.prototype = {
if (this._poller) {
yield this._poller.destroy();
}
this.EVENTS.forEach(e => this._front.off(e, this._onProfilerEvent));
yield this.unregisterEventNotifications({ events: this.EVENTS });
yield this._front.destroy();
this._target.client.removeListener("eventNotification", this._onProfilerEvent);
}),
/**
@ -107,17 +95,9 @@ ProfilerFrontFacade.prototype = {
// nsIPerformance module will be kept recording, because it's the same instance
// for all targets and interacts with the whole platform, so we don't want
// to affect other clients by stopping (or restarting) it.
let status = yield this.getStatus();
// This should only occur during teardown
if (!status) {
return;
}
let { isActive, currentTime, position, generation, totalSize } = status;
let { isActive, currentTime, position, generation, totalSize } = yield this.getStatus();
if (isActive) {
this.emit("profiler-already-active");
return { startTime: currentTime, position, generation, totalSize };
}
@ -130,11 +110,10 @@ ProfilerFrontFacade.prototype = {
let startInfo = yield this.startProfiler(profilerOptions);
let startTime = 0;
if ("currentTime" in startInfo) {
if ('currentTime' in startInfo) {
startTime = startInfo.currentTime;
}
this.emit("profiler-activated");
return { startTime, position, generation, totalSize };
}),
@ -151,7 +130,7 @@ ProfilerFrontFacade.prototype = {
* Wrapper around `profiler.isActive()` to take profiler status data and emit.
*/
getStatus: Task.async(function *() {
let data = yield CompatUtils.callFrontMethod("isActive").call(this);
let data = yield (CompatUtils.callFrontMethod("isActive").call(this));
// If no data, the last poll for `isActive()` was wrapping up, and the target.client
// is now null, so we no longer have data, so just abort here.
if (!data) {
@ -178,7 +157,7 @@ ProfilerFrontFacade.prototype = {
* Returns profile data from now since `startTime`.
*/
getProfile: Task.async(function *(options) {
let profilerData = yield CompatUtils.callFrontMethod("getProfile").call(this, options);
let profilerData = yield (CompatUtils.callFrontMethod("getProfile").call(this, options));
// If the backend is not deduped, dedupe it ourselves, as rest of the code
// expects a deduped profile.
if (profilerData.profile.meta.version === 2) {
@ -200,14 +179,12 @@ ProfilerFrontFacade.prototype = {
* @param object response
* The data received from the backend.
*/
_onProfilerEvent: function (data) {
let { subject, topic, details } = data;
_onProfilerEvent: function (_, { topic, subject, details }) {
if (topic === "console-api-profiler") {
if (subject.action === "profile") {
this.emit("console-profile-start", details);
} else if (subject.action === "profileEnd") {
this.emit("console-profile-end", details);
this.emit("console-profile-stop", details);
}
} else if (topic === "profiler-stopped") {
this.emit("profiler-stopped");
@ -219,19 +196,19 @@ ProfilerFrontFacade.prototype = {
yield this.getStatus();
}),
toString: () => "[object ProfilerFrontFacade]"
toString: () => "[object LegacyProfilerFront]"
};
/**
* Constructor for a facade around an underlying TimelineFront.
*/
function TimelineFrontFacade (target) {
function LegacyTimelineFront (target) {
this._target = target;
EventEmitter.decorate(this);
}
TimelineFrontFacade.prototype = {
EVENTS: ["markers", "frames", "memory", "ticks"],
LegacyTimelineFront.prototype = {
EVENTS: ["markers", "frames", "ticks"],
connect: Task.async(function*() {
let supported = yield CompatUtils.timelineActorSupported(this._target);
@ -259,131 +236,19 @@ TimelineFrontFacade.prototype = {
}),
/**
* An aggregate of all events (markers, frames, memory, ticks) and exposes
* to PerformanceFront as a single event.
* An aggregate of all events (markers, frames, ticks) and exposes
* to PerformanceActorsConnection as a single event.
*/
_onTimelineData: function (type, ...data) {
this.emit("timeline-data", type, ...data);
},
toString: () => "[object TimelineFrontFacade]"
};
/**
* Constructor for a facade around an underlying ProfilerFront.
*/
function MemoryFrontFacade (target) {
this._target = target;
this._pullAllocationSites = this._pullAllocationSites.bind(this);
EventEmitter.decorate(this);
}
MemoryFrontFacade.prototype = {
connect: Task.async(function*() {
let supported = yield CompatUtils.memoryActorSupported(this._target);
this._front = supported ?
new MemoryFront(this._target.client, this._target.form) :
new CompatUtils.MockMemoryFront();
this.IS_MOCK = !supported;
}),
/**
* Disables polling and destroys actor.
*/
destroy: Task.async(function *() {
if (this._poller) {
yield this._poller.destroy();
}
yield this._front.destroy();
}),
/**
* Starts polling for allocation information.
*/
start: Task.async(function *(options) {
if (!options.withAllocations) {
return 0;
}
yield this.attach();
// Reconstruct our options because the server actor fails when being passed
// undefined values in objects.
let allocationOptions = {};
if (options.allocationsSampleProbability !== void 0) {
allocationOptions.probability = options.allocationsSampleProbability;
}
if (options.allocationsMaxLogLength !== void 0) {
allocationOptions.maxLogLength = options.allocationsMaxLogLength;
}
let startTime = yield this.startRecordingAllocations(allocationOptions);
if (!this._poller) {
this._poller = new Poller(this._pullAllocationSites, ALLOCATION_SITE_POLL_TIMER, false);
}
if (!this._poller.isPolling()) {
this._poller.on();
}
return startTime;
}),
/**
* Stops polling for allocation information.
*/
stop: Task.async(function *(options) {
if (!options.withAllocations) {
return 0;
}
// Since `_pullAllocationSites` is usually running inside a timeout, and
// it's performing asynchronous requests to the server, a recording may
// be stopped before that method finishes executing. Therefore, we need to
// wait for the last request to `getAllocations` to finish before actually
// stopping recording allocations.
yield this._poller.off();
yield this._lastPullAllocationSitesFinished;
let endTime = yield this.stopRecordingAllocations();
yield this.detach();
return endTime;
}),
/**
* At regular intervals, pull allocations from the memory actor, and
* forward them on this Front facade as "timeline-data" events. This
* gives the illusion that the MemoryActor supports an EventEmitter-style
* event stream.
*/
_pullAllocationSites: Task.async(function *() {
let deferred = promise.defer();
this._lastPullAllocationSitesFinished = deferred.promise;
if ((yield this.getState()) !== "attached") {
deferred.resolve();
return;
}
let memoryData = yield this.getAllocations();
// Match the signature of the TimelineFront events, with "timeline-data"
// being the event name, and the second argument describing the type.
this.emit("timeline-data", "allocations", memoryData);
deferred.resolve();
}),
toString: () => "[object MemoryFrontFacade]"
toString: () => "[object LegacyTimelineFront]"
};
// Bind all the methods that directly proxy to the actor
PROFILER_ACTOR_METHODS.forEach(m => ProfilerFrontFacade.prototype[m] = CompatUtils.callFrontMethod(m));
TIMELINE_ACTOR_METHODS.forEach(m => TimelineFrontFacade.prototype[m] = CompatUtils.callFrontMethod(m));
MEMORY_ACTOR_METHODS.forEach(m => MemoryFrontFacade.prototype[m] = CompatUtils.callFrontMethod(m));
PROFILER_ACTOR_METHODS.forEach(m => LegacyProfilerFront.prototype[m] = CompatUtils.callFrontMethod(m));
TIMELINE_ACTOR_METHODS.forEach(m => LegacyTimelineFront.prototype[m] = CompatUtils.callFrontMethod(m));
exports.ProfilerFront = ProfilerFrontFacade;
exports.TimelineFront = TimelineFrontFacade;
exports.MemoryFront = MemoryFrontFacade;
exports.LegacyProfilerFront = LegacyProfilerFront;
exports.LegacyTimelineFront = LegacyTimelineFront;

View File

@ -21,20 +21,6 @@ function MockFront (blueprint) {
}
}
function MockMemoryFront () {
MockFront.call(this, [
["start", 0], // for facade
["stop", 0], // for facade
["destroy"],
["attach"],
["detach"],
["getState", "detached"],
["startRecordingAllocations", 0],
["stopRecordingAllocations", 0],
["getAllocations", createMockAllocations],
]);
}
function MockTimelineFront () {
MockFront.call(this, [
["destroy"],
@ -43,46 +29,6 @@ function MockTimelineFront () {
]);
}
/**
* Create a fake allocations object, to be used with the MockMemoryFront
* so we create a fresh object each time.
*
* @return {Object}
*/
function createMockAllocations () {
return {
allocations: [],
allocationsTimestamps: [],
frames: [],
counts: []
};
}
/**
* Takes a TabTarget, and checks through all methods that are needed
* on the server's memory actor to determine if a mock or real MemoryActor
* should be used. The last of the methods added to MemoryActor
* landed in Gecko 35, so previous versions will fail this. Setting the `target`'s
* TEST_MOCK_MEMORY_ACTOR property to true will cause this function to indicate that
* the memory actor is not supported.
*
* @param {TabTarget} target
* @return {Boolean}
*/
function memoryActorSupported (target) {
// This `target` property is used only in tests to test
// instances where the memory actor is not available.
if (target.TEST_MOCK_MEMORY_ACTOR) {
return false;
}
// We need to test that both the root actor has `memoryActorAllocations`,
// which is in Gecko 38+, and also that the target has a memory actor. There
// are scenarios, like addon debugging, where memoryActorAllocations is available,
// but no memory actor (like addon debugging in Gecko 38+)
return !!target.getTrait("memoryActorAllocations") && target.hasActor("memory");
}
/**
* Takes a TabTarget, and checks existence of a TimelineActor on
* the server, or if TEST_MOCK_TIMELINE_ACTOR is to be used.
@ -117,8 +63,6 @@ function callFrontMethod (method) {
};
}
exports.MockMemoryFront = MockMemoryFront;
exports.MockTimelineFront = MockTimelineFront;
exports.memoryActorSupported = memoryActorSupported;
exports.timelineActorSupported = timelineActorSupported;
exports.callFrontMethod = callFrontMethod;

View File

@ -3,87 +3,66 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci, Cu, Cr } = require("chrome");
const { Cu } = require("chrome");
const { Task } = require("resource://gre/modules/Task.jsm");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
loader.lazyRequireGetter(this, "extend",
"sdk/util/object", true);
loader.lazyRequireGetter(this, "Actors",
"devtools/performance/actors");
loader.lazyRequireGetter(this, "RecordingModel",
"devtools/performance/recording-model", true);
"devtools/toolkit/performance/legacy/actors");
loader.lazyRequireGetter(this, "LegacyPerformanceRecording",
"devtools/toolkit/performance/legacy/recording", true);
loader.lazyRequireGetter(this, "importRecording",
"devtools/toolkit/performance/legacy/recording", true);
loader.lazyRequireGetter(this, "normalizePerformanceFeatures",
"devtools/performance/recording-utils", true);
"devtools/toolkit/performance/utils", true);
loader.lazyRequireGetter(this, "DevToolsUtils",
"devtools/toolkit/DevToolsUtils");
loader.lazyImporter(this, "gDevTools",
"resource:///modules/devtools/gDevTools.jsm");
loader.lazyRequireGetter(this, "events",
"sdk/event/core");
loader.lazyRequireGetter(this, "EventTarget",
"sdk/event/target", true);
loader.lazyRequireGetter(this, "Class",
"sdk/core/heritage", true);
/**
* A cache of all PerformanceFront instances.
* The keys are Target objects.
*/
let PerformanceFronts = new WeakMap();
/**
* Instantiates a shared PerformanceFront for the specified target.
* Consumers must yield on `open` to make sure the connection is established.
*
* @param Target target
* The target owning this connection.
* @return PerformanceFront
* The pseudofront for all the underlying actors.
*/
PerformanceFronts.forTarget = function(target) {
if (this.has(target)) {
return this.get(target);
}
let instance = new PerformanceFront(target);
this.set(target, instance);
return instance;
};
/**
* A connection to underlying actors (profiler, memory, framerate, etc.)
* A connection to underlying actors (profiler, framerate, etc.)
* shared by all tools in a target.
*
* Use `PerformanceFronts.forTarget` to make sure you get the same
* instance every time, and the `PerformanceFront` to start/stop recordings.
*
* @param Target target
* The target owning this connection.
*/
function PerformanceFront (target) {
EventEmitter.decorate(this);
const LegacyPerformanceFront = Class({
extends: EventTarget,
this._target = target;
this._client = this._target.client;
this._pendingConsoleRecordings = [];
this._sitesPullTimeout = 0;
this._recordings = [];
LEGACY_FRONT: true,
this._pipeToFront = this._pipeToFront.bind(this);
this._onTimelineData = this._onTimelineData.bind(this);
this._onConsoleProfileStart = this._onConsoleProfileStart.bind(this);
this._onConsoleProfileEnd = this._onConsoleProfileEnd.bind(this);
this._onProfilerStatus = this._onProfilerStatus.bind(this);
this._onProfilerUnexpectedlyStopped = this._onProfilerUnexpectedlyStopped.bind(this);
traits: {
features: {
withMarkers: true,
withTicks: true,
withMemory: false,
withAllocations: false,
withJITOptimizations: false,
},
},
Services.obs.notifyObservers(null, "performance-tools-connection-created", null);
}
initialize: function (target) {
let { form, client } = target;
this._target = target;
this._form = form;
this._client = client;
this._pendingConsoleRecordings = [];
this._sitesPullTimeout = 0;
this._recordings = [];
PerformanceFront.prototype = {
// Properties set based off of server actor support
_memorySupported: true,
_timelineSupported: true,
this._pipeToFront = this._pipeToFront.bind(this);
this._onTimelineData = this._onTimelineData.bind(this);
this._onConsoleProfileStart = this._onConsoleProfileStart.bind(this);
this._onConsoleProfileStop = this._onConsoleProfileStop.bind(this);
this._onProfilerStatus = this._onProfilerStatus.bind(this);
this._onProfilerUnexpectedlyStopped = this._onProfilerUnexpectedlyStopped.bind(this);
},
/**
* Initializes a connection to the profiler and other miscellaneous actors.
@ -92,7 +71,7 @@ PerformanceFront.prototype = {
* @return object
* A promise that is resolved once the connection is established.
*/
open: Task.async(function*() {
connect: Task.async(function*() {
if (this._connecting) {
return this._connecting.promise;
}
@ -101,18 +80,14 @@ PerformanceFront.prototype = {
// other attempts to open the connection use the same resolution promise
this._connecting = promise.defer();
// Local debugging needs to make the target remote.
yield this._target.makeRemote();
// Sets `this._profiler`, `this._timeline` and `this._memory`.
// Only initialize the timeline and memory fronts if the respective actors
// Sets `this._profiler`, `this._timeline`.
// Only initialize the timeline fronts if the respective actors
// are available. Older Gecko versions don't have existing implementations,
// in which case all the methods we need can be easily mocked.
yield this._connectActors();
yield this._registerListeners();
this._connecting.resolve();
Services.obs.notifyObservers(null, "performance-tools-connection-opened", null);
}),
/**
@ -131,9 +106,9 @@ PerformanceFront.prototype = {
this._connecting = null;
this._profiler = null;
this._timeline = null;
this._memory = null;
this._target = null;
this._client = null;
this._form = null;
this._target = this._target;
}),
/**
@ -141,20 +116,17 @@ PerformanceFront.prototype = {
* found in ./actors.js.
*/
_connectActors: Task.async(function*() {
this._profiler = new Actors.ProfilerFront(this._target);
this._memory = new Actors.MemoryFront(this._target);
this._timeline = new Actors.TimelineFront(this._target);
this._profiler = new Actors.LegacyProfilerFront(this._target);
this._timeline = new Actors.LegacyTimelineFront(this._target);
yield promise.all([
this._profiler.connect(),
this._memory.connect(),
this._timeline.connect()
]);
// Expose server support status of underlying actors
// after connecting.
this._memorySupported = !this._memory.IS_MOCK;
this._timelineSupported = !this._timeline.IS_MOCK;
// If mocked timeline, update the traits
this.traits.features.withMarkers = !this._timeline.IS_MOCK;
this.traits.features.withTicks = !this._timeline.IS_MOCK;
}),
/**
@ -163,12 +135,9 @@ PerformanceFront.prototype = {
*/
_registerListeners: function () {
this._timeline.on("timeline-data", this._onTimelineData);
this._memory.on("timeline-data", this._onTimelineData);
this._profiler.on("console-profile-start", this._onConsoleProfileStart);
this._profiler.on("console-profile-end", this._onConsoleProfileEnd);
this._profiler.on("console-profile-stop", this._onConsoleProfileStop);
this._profiler.on("profiler-stopped", this._onProfilerUnexpectedlyStopped);
this._profiler.on("profiler-already-active", this._pipeToFront);
this._profiler.on("profiler-activated", this._pipeToFront);
this._profiler.on("profiler-status", this._onProfilerStatus);
},
@ -177,12 +146,9 @@ PerformanceFront.prototype = {
*/
_unregisterListeners: function () {
this._timeline.off("timeline-data", this._onTimelineData);
this._memory.off("timeline-data", this._onTimelineData);
this._profiler.off("console-profile-start", this._onConsoleProfileStart);
this._profiler.off("console-profile-end", this._onConsoleProfileEnd);
this._profiler.off("console-profile-stop", this._onConsoleProfileStop);
this._profiler.off("profiler-stopped", this._onProfilerUnexpectedlyStopped);
this._profiler.off("profiler-already-active", this._pipeToFront);
this._profiler.off("profiler-activated", this._pipeToFront);
this._profiler.off("profiler-status", this._onProfilerStatus);
},
@ -193,7 +159,6 @@ PerformanceFront.prototype = {
yield promise.all([
this._profiler.destroy(),
this._timeline.destroy(),
this._memory.destroy()
]);
}),
@ -214,13 +179,9 @@ PerformanceFront.prototype = {
return;
}
// Ensure the performance front is set up and ready.
// Slight performance overhead for this, should research some more.
// This is to ensure that there is a front to receive the events for
// the console profiles.
yield gDevTools.getToolbox(this._target).loadTool("performance");
events.emit(this, "console-profile-start");
let model = yield this.startRecording(extend(getRecordingModelPrefs(), {
yield this.startRecording(extend({}, getLegacyPerformanceRecordingPrefs(), {
console: true,
label: profileLabel
}));
@ -235,7 +196,7 @@ PerformanceFront.prototype = {
* The time (in milliseconds) when the call was made, relative to when
* the nsIProfiler module was started.
*/
_onConsoleProfileEnd: Task.async(function *(_, data) {
_onConsoleProfileStop: Task.async(function *(_, data) {
// If no data, abort; can occur if profiler isn't running and we get a surprise
// call to console.profileEnd()
if (!data) {
@ -280,15 +241,13 @@ PerformanceFront.prototype = {
* Called whenever there is timeline data of any of the following types:
* - markers
* - frames
* - memory
* - ticks
* - allocations
*
* Populate our internal store of recordings for all currently recording sessions.
*/
_onTimelineData: function (_, ...data) {
this._recordings.forEach(e => e._addTimelineData.apply(e, data));
this.emit("timeline-data", ...data);
events.emit(this, "timeline-data", ...data);
},
/**
@ -297,15 +256,12 @@ PerformanceFront.prototype = {
_onProfilerStatus: function (_, data) {
// If no data emitted (whether from an older actor being destroyed
// from a previous test, or the server does not support it), just ignore.
if (!data) {
if (!data || data.position === void 0) {
return;
}
// Check for a value of buffer status (`position`) to see if the server
// supports buffer status -- apply to the recording models if so.
if (data.position !== void 0) {
this._recordings.forEach(e => e._addBufferStatusData.call(e, data));
}
this.emit("profiler-status", data);
this._currentBufferStatus = data;
events.emit(this, "profiler-status", data);
},
/**
@ -318,24 +274,20 @@ PerformanceFront.prototype = {
* A promise that is resolved once recording has started.
*/
startRecording: Task.async(function*(options = {}) {
let model = new RecordingModel(normalizePerformanceFeatures(options, this.getActorSupport()));
this.emit("recording-starting", model);
let model = new LegacyPerformanceRecording(normalizePerformanceFeatures(options, this.traits.features));
// All actors are started asynchronously over the remote debugging protocol.
// Get the corresponding start times from each one of them.
// The timeline and memory actors are target-dependent, so start those as well,
// The timeline actors are target-dependent, so start those as well,
// even though these are mocked in older Geckos (FF < 35)
let profilerStart = this._profiler.start(options);
let timelineStart = this._timeline.start(options);
let memoryStart = this._memory.start(options);
let { startTime, position, generation, totalSize } = yield profilerStart;
let timelineStartTime = yield timelineStart;
let memoryStartTime = yield memoryStart;
let data = {
profilerStartTime: startTime, timelineStartTime, memoryStartTime,
profilerStartTime: startTime, timelineStartTime,
generation, position, totalSize
};
@ -344,20 +296,20 @@ PerformanceFront.prototype = {
model._populate(data);
this._recordings.push(model);
this.emit("recording-started", model);
events.emit(this, "recording-started", model);
return model;
}),
/**
* Manually ends the recording session for the corresponding RecordingModel.
* Manually ends the recording session for the corresponding LegacyPerformanceRecording.
*
* @param RecordingModel model
* The corresponding RecordingModel that belongs to the recording session wished to stop.
* @return RecordingModel
* @param LegacyPerformanceRecording model
* The corresponding LegacyPerformanceRecording that belongs to the recording session wished to stop.
* @return LegacyPerformanceRecording
* Returns the same model, populated with the profiling data.
*/
stopRecording: Task.async(function*(model) {
// If model isn't in the PerformanceFront internal store,
// If model isn't in the LegacyPerformanceFront internal store,
// then do nothing.
if (this._recordings.indexOf(model) === -1) {
return;
@ -368,12 +320,12 @@ PerformanceFront.prototype = {
// the recording can be considered "completed".
let endTime = Date.now();
model._onStoppingRecording(endTime);
this.emit("recording-stopping", model);
events.emit(this, "recording-stopping", model);
// Currently there are two ways profiles stop recording. Either manually in the
// performance tool, or via console.profileEnd. Once a recording is done,
// we want to deliver the model to the performance tool (either as a return
// from the PerformanceFront or via `console-profile-end` event) and then
// from the LegacyPerformanceFront or via `console-profile-stop` event) and then
// remove it from the internal store.
//
// In the case where a console.profile is generated via the console (so the tools are
@ -383,46 +335,42 @@ PerformanceFront.prototype = {
let config = model.getConfiguration();
let startTime = model.getProfilerStartTime();
let profilerData = yield this._profiler.getProfile({ startTime });
let memoryEndTime = Date.now();
let timelineEndTime = Date.now();
// Only if there are no more sessions recording do we stop
// the underlying memory and timeline actors. If we're still recording,
// juse use Date.now() for the memory and timeline end times, as those
// the underlying timeline actors. If we're still recording,
// juse use Date.now() for the timeline end times, as those
// are only used in tests.
if (!this.isRecording()) {
// This doesn't stop the profiler, just turns off polling for
// events, and also turns off events on memory/timeline actors.
// events, and also turns off events on timeline actors.
yield this._profiler.stop();
memoryEndTime = yield this._memory.stop(config);
timelineEndTime = yield this._timeline.stop(config);
}
// Set the results on the RecordingModel itself.
// Set the results on the LegacyPerformanceRecording itself.
model._onStopRecording({
// Data available only at the end of a recording.
profile: profilerData.profile,
// End times for all the actors.
profilerEndTime: profilerData.currentTime,
timelineEndTime: timelineEndTime,
memoryEndTime: memoryEndTime
timelineEndTime: timelineEndTime
});
this.emit("recording-stopped", model);
events.emit(this, "recording-stopped", model);
return model;
}),
/**
* Returns an object indicating what server actors are available and
* initialized. A falsy value indicates that the server does not support
* that feature, or that mock actors were explicitly requested (tests).
* Creates a recording object when given a nsILocalFile.
*
* @param {nsILocalFile} file
* The file to import the data from.
* @return {Promise<LegacyPerformanceRecording>}
*/
getActorSupport: function () {
return {
memory: this._memorySupported,
timeline: this._timelineSupported
};
importRecording: function (file) {
return importRecording(file);
},
/**
@ -435,12 +383,43 @@ PerformanceFront.prototype = {
return this._recordings.some(recording => recording.isRecording());
},
/**
* Pass in a PerformanceRecording and get a normalized value from 0 to 1 of how much
* of this recording's lifetime remains without being overwritten.
*
* @param {PerformanceRecording} recording
* @return {number?}
*/
getBufferUsageForRecording: function (recording) {
if (!recording.isRecording() || !this._currentBufferStatus) {
return null;
}
let { position: currentPosition, totalSize, generation: currentGeneration } = this._currentBufferStatus;
let { position: origPosition, generation: origGeneration } = recording.getStartingBufferStatus();
let normalizedCurrent = (totalSize * (currentGeneration - origGeneration)) + currentPosition;
let percent = (normalizedCurrent - origPosition) / totalSize;
return percent > 1 ? 1 : percent;
},
/**
* Returns the configurations set on underlying components, used in tests.
* Returns an object with `probability`, `maxLogLength` for allocations, and
* `entries` and `interval` for profiler.
*
* @return {object}
*/
getConfiguration: Task.async(function *() {
let profilerConfig = yield this._request("profiler", "getStartOptions");
return profilerConfig;
}),
/**
* An event from an underlying actor that we just want
* to pipe to the front itself.
*/
_pipeToFront: function (eventName, ...args) {
this.emit(eventName, ...args);
events.emit(this, eventName, ...args);
},
/**
@ -449,19 +428,30 @@ PerformanceFront.prototype = {
*/
_request: function (actorName, method, ...args) {
if (!DevToolsUtils.testing) {
throw new Error("PerformanceFront._request may only be used in tests.");
throw new Error("LegacyPerformanceFront._request may only be used in tests.");
}
let actor = this[`_${actorName}`];
return actor[method].apply(actor, args);
},
toString: () => "[object PerformanceFront]"
};
/**
* Sets how often the "profiler-status" event should be emitted.
* Used in tests.
*/
setProfilerStatusInterval: function (n) {
if (this._profiler._poller) {
this._profiler._poller._wait = n;
}
this._profiler._PROFILER_CHECK_TIMER = n;
},
toString: () => "[object LegacyPerformanceFront]"
});
/**
* Creates an object of configurations based off of preferences for a RecordingModel.
* Creates an object of configurations based off of preferences for a LegacyPerformanceRecording.
*/
function getRecordingModelPrefs () {
function getLegacyPerformanceRecordingPrefs () {
return {
withMarkers: true,
withMemory: Services.prefs.getBoolPref("devtools.performance.ui.enable-memory"),
@ -473,5 +463,4 @@ function getRecordingModelPrefs () {
};
}
exports.getPerformanceFront = t => PerformanceFronts.forTarget(t);
exports.PerformanceFront = PerformanceFront;
exports.LegacyPerformanceFront = LegacyPerformanceFront;

View File

@ -7,16 +7,16 @@ const { Cc, Ci, Cu, Cr } = require("chrome");
const { Task } = require("resource://gre/modules/Task.jsm");
loader.lazyRequireGetter(this, "PerformanceIO",
"devtools/performance/io", true);
"devtools/toolkit/performance/io");
loader.lazyRequireGetter(this, "RecordingUtils",
"devtools/performance/recording-utils");
"devtools/toolkit/performance/utils");
/**
* Model for a wholistic profile, containing the duration, profiling data,
* frames data, timeline (marker, tick, memory) data, and methods to mark
* a recording as 'in progress' or 'finished'.
*/
const RecordingModel = function (options={}) {
const LegacyPerformanceRecording = function (options={}) {
this._label = options.label || "";
this._console = options.console || false;
@ -33,7 +33,7 @@ const RecordingModel = function (options={}) {
};
};
RecordingModel.prototype = {
LegacyPerformanceRecording.prototype = {
// Private fields, only needed when a recording is started or stopped.
_console: false,
_imported: false,
@ -43,7 +43,7 @@ RecordingModel.prototype = {
_timelineStartTime: 0,
_memoryStartTime: 0,
_configuration: {},
_originalBufferStatus: null,
_startingBufferStatus: null,
_bufferPercent: null,
// Serializable fields, necessary and sufficient for import and export.
@ -102,7 +102,7 @@ RecordingModel.prototype = {
this._profilerStartTime = info.profilerStartTime;
this._timelineStartTime = info.timelineStartTime;
this._memoryStartTime = info.memoryStartTime;
this._originalBufferStatus = {
this._startingBufferStatus = {
position: info.position,
totalSize: info.totalSize,
generation: info.generation
@ -293,29 +293,19 @@ RecordingModel.prototype = {
},
/**
* Returns the percent (value between 0 and 1) of buffer used in this
* recording. Returns `null` for recordings that are no longer recording.
* Returns a boolean indicating if this recording is no longer recording, but
* not yet completed.
*/
getBufferUsage: function () {
return this.isRecording() ? this._bufferPercent : null;
isFinalizing: function () {
return !this.isRecording() && !this.isCompleted();
},
/**
* Fired whenever the PerformanceFront has new buffer data.
* Returns the position, generation and totalSize of the profiler
* when this recording was started.
*/
_addBufferStatusData: function (bufferStatus) {
// If this model isn't currently recording, or if the server does not
// support buffer status (or if this fires after actors are being destroyed),
// ignore this information.
if (!bufferStatus || !this.isRecording()) {
return;
}
let { position: currentPosition, totalSize, generation: currentGeneration } = bufferStatus;
let { position: origPosition, generation: origGeneration } = this._originalBufferStatus;
let normalizedCurrent = (totalSize * (currentGeneration - origGeneration)) + currentPosition;
let percent = (normalizedCurrent - origPosition) / totalSize;
this._bufferPercent = percent > 1 ? 1 : percent;
getStartingBufferStatus: function () {
return this._startingBufferStatus;
},
/**
@ -337,25 +327,14 @@ RecordingModel.prototype = {
if (!config.withMarkers) { break; }
let [markers] = data;
RecordingUtils.offsetMarkerTimes(markers, this._timelineStartTime);
pushAll(this._markers, markers);
RecordingUtils.pushAll(this._markers, markers);
break;
}
// Accumulate stack frames into an array.
case "frames": {
if (!config.withMarkers) { break; }
let [, frames] = data;
pushAll(this._frames, frames);
break;
}
// Accumulate memory measurements into an array. Furthermore, the timestamp
// does not have a zero epoch, so offset it by the actor's start time.
case "memory": {
if (!config.withMemory) { break; }
let [currentTime, measurement] = data;
this._memory.push({
delta: currentTime - this._timelineStartTime,
value: measurement.total / 1024 / 1024
});
RecordingUtils.pushAll(this._frames, frames);
break;
}
// Save the accumulated refresh driver ticks.
@ -365,47 +344,10 @@ RecordingModel.prototype = {
this._ticks = timestamps;
break;
}
// Accumulate allocation sites into an array. Furthermore, the timestamps
// do not have a zero epoch, and are microseconds instead of milliseconds,
// so offset all of them by the start time, also converting from µs to ms.
case "allocations": {
if (!config.withAllocations) { break; }
let [{
allocations: sites,
allocationsTimestamps: timestamps,
frames,
}] = data;
let timeOffset = this._memoryStartTime;
RecordingUtils.offsetAndScaleTimestamps(timestamps, timeOffset);
pushAll(this._allocations.sites, sites);
pushAll(this._allocations.timestamps, timestamps);
pushAll(this._allocations.frames, frames);
break;
}
}
},
toString: () => "[object RecordingModel]"
toString: () => "[object LegacyPerformanceRecording]"
};
/**
* Push all elements of src array into dest array. Marker data will come in small chunks
* and add up over time, whereas allocation arrays can be > 500000 elements (and
* Function.prototype.apply throws if applying more than 500000 elements, which
* is what spawned this separate function), so iterate one element at a time.
* @see bug 1166823
* @see http://jsperf.com/concat-large-arrays
* @see http://jsperf.com/concat-large-arrays/2
*
* @param {Array} dest
* @param {Array} src
*/
function pushAll (dest, src) {
let length = src.length;
for (let i = 0; i < length; i++) {
dest.push(src[i]);
}
}
exports.RecordingModel = RecordingModel;
exports.LegacyPerformanceRecording = LegacyPerformanceRecording;

View File

@ -0,0 +1,18 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# 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/.
EXTRA_JS_MODULES.devtools.performance += [
'io.js',
'recorder.js',
'utils.js',
]
EXTRA_JS_MODULES.devtools.performance.legacy += [
'legacy/actors.js',
'legacy/compatibility.js',
'legacy/front.js',
'legacy/recording.js',
]

View File

@ -0,0 +1,462 @@
/* 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("resource://gre/modules/Task.jsm");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "extend",
"sdk/util/object", true);
loader.lazyRequireGetter(this, "Class",
"sdk/core/heritage", true);
loader.lazyRequireGetter(this, "EventTarget",
"sdk/event/target", true);
loader.lazyRequireGetter(this, "events",
"sdk/event/core");
loader.lazyRequireGetter(this, "Memory",
"devtools/toolkit/shared/memory", true);
loader.lazyRequireGetter(this, "Timeline",
"devtools/toolkit/shared/timeline", true);
loader.lazyRequireGetter(this, "Profiler",
"devtools/toolkit/shared/profiler", true);
loader.lazyRequireGetter(this, "PerformanceRecordingActor",
"devtools/server/actors/performance-recording", true);
loader.lazyRequireGetter(this, "PerformanceRecordingFront",
"devtools/server/actors/performance-recording", true);
loader.lazyRequireGetter(this, "mapRecordingOptions",
"devtools/toolkit/performance/utils", true);
loader.lazyRequireGetter(this, "DevToolsUtils",
"devtools/toolkit/DevToolsUtils");
const PROFILER_EVENTS = [
"console-api-profiler",
"profiler-started",
"profiler-stopped",
"profiler-status"
];
// Max time in milliseconds for the allocations event to occur, which will
// occur on every GC, or at least as often as DRAIN_ALLOCATIONS_TIMEOUT.
const DRAIN_ALLOCATIONS_TIMEOUT = 2000;
/**
* A connection to underlying actors (profiler, memory, framerate, etc.)
* shared by all tools in a target.
*
* @param Target target
* The target owning this connection.
*/
const PerformanceRecorder = exports.PerformanceRecorder = Class({
extends: EventTarget,
initialize: function (conn, tabActor) {
this.conn = conn;
this.tabActor = tabActor;
this._pendingConsoleRecordings = [];
this._recordings = [];
this._onTimelineData = this._onTimelineData.bind(this);
this._onProfilerEvent = this._onProfilerEvent.bind(this);
},
/**
* Initializes a connection to the profiler and other miscellaneous actors.
* If in the process of opening, or already open, nothing happens.
*
* @return object
* A promise that is resolved once the connection is established.
*/
connect: function () {
if (this._connected) {
return;
}
// Sets `this._profiler`, `this._timeline` and `this._memory`.
// Only initialize the timeline and memory fronts if the respective actors
// are available. Older Gecko versions don't have existing implementations,
// in which case all the methods we need can be easily mocked.
this._connectComponents();
this._registerListeners();
this._connected = true;
},
/**
* Destroys this connection.
*/
destroy: function () {
this._unregisterListeners();
this._disconnectComponents();
this._connected = null;
this._profiler = null;
this._timeline = null;
this._memory = null;
this._target = null;
this._client = null;
},
/**
* Initializes fronts and connects to the underlying actors using the facades
* found in ./actors.js.
*/
_connectComponents: function () {
this._profiler = new Profiler(this.tabActor);
this._memory = new Memory(this.tabActor);
this._timeline = new Timeline(this.tabActor);
this._profiler.registerEventNotifications({ events: PROFILER_EVENTS });
},
/**
* Registers listeners on events from the underlying
* actors, so the connection can handle them.
*/
_registerListeners: function () {
this._timeline.on("*", this._onTimelineData);
this._memory.on("*", this._onTimelineData);
this._profiler.on("*", this._onProfilerEvent);
},
/**
* Unregisters listeners on events on the underlying actors.
*/
_unregisterListeners: function () {
this._timeline.off("*", this._onTimelineData);
this._memory.off("*", this._onTimelineData);
this._profiler.off("*", this._onProfilerEvent);
},
/**
* Closes the connections to non-profiler actors.
*/
_disconnectComponents: function () {
this._profiler.unregisterEventNotifications({ events: PROFILER_EVENTS });
this._profiler.destroy();
this._timeline.destroy();
this._memory.destroy();
},
_onProfilerEvent: function (topic, data) {
if (topic === "console-api-profiler") {
if (data.subject.action === "profile") {
this._onConsoleProfileStart(data.details);
} else if (data.subject.action === "profileEnd") {
this._onConsoleProfileEnd(data.details);
}
} else if (topic === "profiler-stopped") {
this._onProfilerUnexpectedlyStopped();
} else if (topic === "profiler-status") {
events.emit(this, "profiler-status", data);
}
},
/**
* Invoked whenever `console.profile` is called.
*
* @param string profileLabel
* The provided string argument if available; undefined otherwise.
* @param number currentTime
* The time (in milliseconds) when the call was made, relative to when
* the nsIProfiler module was started.
*/
_onConsoleProfileStart: Task.async(function *({ profileLabel, currentTime: startTime }) {
let recordings = this._recordings;
// Abort if a profile with this label already exists.
if (recordings.find(e => e.getLabel() === profileLabel)) {
return;
}
// Immediately emit this so the client can start setting things up,
// expecting a recording very soon.
events.emit(this, "console-profile-start");
let model = yield this.startRecording(extend({}, getPerformanceRecordingPrefs(), {
console: true,
label: profileLabel
}));
}),
/**
* Invoked whenever `console.profileEnd` is called.
*
* @param string profileLabel
* The provided string argument if available; undefined otherwise.
* @param number currentTime
* The time (in milliseconds) when the call was made, relative to when
* the nsIProfiler module was started.
*/
_onConsoleProfileEnd: Task.async(function *(data) {
// If no data, abort; can occur if profiler isn't running and we get a surprise
// call to console.profileEnd()
if (!data) {
return;
}
let { profileLabel, currentTime: endTime } = data;
let pending = this._recordings.filter(r => r.isConsole() && r.isRecording());
if (pending.length === 0) {
return;
}
let model;
// Try to find the corresponding `console.profile` call if
// a label was used in profileEnd(). If no matches, abort.
if (profileLabel) {
model = pending.find(e => e.getLabel() === profileLabel);
}
// If no label supplied, pop off the most recent pending console recording
else {
model = pending[pending.length - 1];
}
// If `profileEnd()` was called with a label, and there are no matching
// sessions, abort.
if (!model) {
Cu.reportError("console.profileEnd() called with label that does not match a recording.");
return;
}
yield this.stopRecording(model);
}),
/**
* TODO handle bug 1144438
*/
_onProfilerUnexpectedlyStopped: function () {
Cu.reportError("Profiler unexpectedly stopped.", arguments);
},
/**
* Called whenever there is timeline data of any of the following types:
* - markers
* - frames
* - memory
* - ticks
* - allocations
*/
_onTimelineData: function (eventName, ...data) {
let eventData = Object.create(null);
switch (eventName) {
case "markers": {
eventData = { markers: data[0], endTime: data[1] };
break;
}
case "ticks": {
eventData = { delta: data[0], timestamps: data[1] };
break;
}
case "memory": {
eventData = { delta: data[0], measurement: data[1] };
break;
}
case "frames": {
eventData = { delta: data[0], frames: data[1] };
break;
}
case "allocations": {
eventData = data[0];
break;
}
}
// Filter by only recordings that are currently recording;
// TODO should filter by recordings that have realtimeMarkers enabled.
let activeRecordings = this._recordings.filter(r => r.isRecording());
if (activeRecordings.length) {
events.emit(this, "timeline-data", eventName, eventData, activeRecordings);
}
},
/**
* Begins a recording session
*
* @param boolean options.withMarkers
* @param boolean options.withJITOptimizations
* @param boolean options.withTicks
* @param boolean options.withMemory
* @param boolean options.withAllocations
* @param boolean options.allocationsSampleProbability
* @param boolean options.allocationsMaxLogLength
* @param boolean options.bufferSize
* @param boolean options.sampleFrequency
* @param boolean options.console
* @param string options.label
* @param boolean options.realtimeMarkers
* @return object
* A promise that is resolved once recording has started.
*/
startRecording: Task.async(function*(options) {
let profilerStart, timelineStart, memoryStart;
profilerStart = Task.spawn(function *() {
let data = yield this._profiler.isActive();
if (data.isActive) {
return data;
}
let startData = yield this._profiler.start(mapRecordingOptions("profiler", options));
// If no current time is exposed from starting, set it to 0 -- this is an
// older Gecko that does not return its starting time, and uses an epoch based
// on the profiler's start time.
if (startData.currentTime == null) {
startData.currentTime = 0;
}
return startData;
}.bind(this));
// Timeline will almost always be on if using the DevTools, but using component
// independently could result in no timeline.
if (options.withMarkers || options.withTicks || options.withMemory) {
timelineStart = this._timeline.start(mapRecordingOptions("timeline", options));
}
if (options.withAllocations) {
if (this._memory.getState() === "detached") {
this._memory.attach();
}
memoryStart = this._memory.startRecordingAllocations(extend(mapRecordingOptions("memory", options), {
drainAllocationsTimeout: DRAIN_ALLOCATIONS_TIMEOUT
}));
}
let [profilerStartData, timelineStartData, memoryStartData] = yield promise.all([
profilerStart, timelineStart, memoryStart
]);
let data = Object.create(null);
// Filter out start times that are not actually used (0 or undefined), and
// find the earliest time since all sources use same epoch.
let startTimes = [profilerStartData.currentTime, memoryStartData, timelineStartData].filter(Boolean);
data.startTime = Math.min(...startTimes);
data.position = profilerStartData.position;
data.generation = profilerStartData.generation;
data.totalSize = profilerStartData.totalSize;
let model = new PerformanceRecordingActor(this.conn, options, data);
this._recordings.push(model);
events.emit(this, "recording-started", model);
return model;
}),
/**
* Manually ends the recording session for the corresponding PerformanceRecording.
*
* @param PerformanceRecording model
* The corresponding PerformanceRecording that belongs to the recording session wished to stop.
* @return PerformanceRecording
* Returns the same model, populated with the profiling data.
*/
stopRecording: Task.async(function *(model) {
// If model isn't in the Recorder's internal store,
// then do nothing, like if this was a console.profileEnd
// from a different target.
if (this._recordings.indexOf(model) === -1) {
return model;
}
// Flag the recording as no longer recording, so that `model.isRecording()`
// is false. Do this before we fetch all the data, and then subsequently
// the recording can be considered "completed".
let endTime = Date.now();
events.emit(this, "recording-stopping", model);
// Currently there are two ways profiles stop recording. Either manually in the
// performance tool, or via console.profileEnd. Once a recording is done,
// we want to deliver the model to the performance tool (either as a return
// from the PerformanceFront or via `console-profile-stop` event) and then
// remove it from the internal store.
//
// In the case where a console.profile is generated via the console (so the tools are
// open), we initialize the Performance tool so it can listen to those events.
this._recordings.splice(this._recordings.indexOf(model), 1);
let startTime = model._startTime;
let profilerData = this._profiler.getProfile({ startTime });
// Only if there are no more sessions recording do we stop
// the underlying memory and timeline actors. If we're still recording,
// juse use Date.now() for the memory and timeline end times, as those
// are only used in tests.
if (!this.isRecording()) {
// Check to see if memory is recording, so we only stop recording
// if necessary (otherwise if the memory component is not attached, this will fail)
if (this._memory.isRecordingAllocations()) {
this._memory.stopRecordingAllocations();
}
this._timeline.stop();
}
let recordingData = {
// Data available only at the end of a recording.
profile: profilerData.profile,
// End times for all the actors.
duration: profilerData.currentTime - startTime,
};
events.emit(this, "recording-stopped", model, recordingData);
return model;
}),
/**
* Checks all currently stored recording handles and returns a boolean
* if there is a session currently being recorded.
*
* @return Boolean
*/
isRecording: function () {
return this._recordings.some(h => h.isRecording());
},
/**
* Returns all current recordings.
*/
getRecordings: function () {
return this._recordings;
},
/**
* Sets how often the "profiler-status" event should be emitted.
* Used in tests.
*/
setProfilerStatusInterval: function (n) {
this._profiler.setProfilerStatusInterval(n);
},
/**
* Returns the configurations set on underlying components, used in tests.
* Returns an object with `probability`, `maxLogLength` for allocations, and
* `features`, `threadFilters`, `entries` and `interval` for profiler.
*
* @return {object}
*/
getConfiguration: function () {
return extend({}, this._memory.getAllocationsSettings(), this._profiler.getStartOptions());
},
toString: () => "[object PerformanceRecorder]"
});
/**
* Creates an object of configurations based off of preferences for a PerformanceRecording.
*/
function getPerformanceRecordingPrefs () {
return {
withMarkers: true,
withMemory: Services.prefs.getBoolPref("devtools.performance.ui.enable-memory"),
withTicks: Services.prefs.getBoolPref("devtools.performance.ui.enable-framerate"),
withAllocations: Services.prefs.getBoolPref("devtools.performance.ui.enable-allocations"),
withJITOptimizations: Services.prefs.getBoolPref("devtools.performance.ui.enable-jit-optimizations"),
allocationsSampleProbability: +Services.prefs.getCharPref("devtools.performance.memory.sample-probability"),
allocationsMaxLogLength: Services.prefs.getIntPref("devtools.performance.memory.max-log-length")
};
}

View File

@ -12,6 +12,31 @@ loader.lazyRequireGetter(this, "extend",
* such as filtering profile samples or offsetting timestamps.
*/
function mapRecordingOptions (type, options) {
if (type === "profiler") {
return {
entries: options.bufferSize,
interval: options.sampleFrequency ? (1000 / (options.sampleFrequency * 1000)) : void 0
};
}
if (type === "memory") {
return {
probability: options.allocationsSampleProbability,
maxLogLength: options.allocationsMaxLogLength
};
}
if (type === "timeline") {
return {
withMemory: options.withMemory,
withTicks: options.withTicks,
};
}
return options;
}
/**
* Takes an options object for `startRecording`, and normalizes
* it based off of server support. For example, if the user
@ -21,26 +46,15 @@ loader.lazyRequireGetter(this, "extend",
* what the user initially requested.
*
* @param {object} options
* @param {boolean} support.timeline
* @param {boolean} support.memory
* @param {boolean}
*/
function normalizePerformanceFeatures (options, support) {
let supportOptions = Object.create(null);
// TODO bug 1172180 disable `withAllocations` and `withJITOptimizations` when using the
// pseudo front, as we only want to support it directly from the real actor
// in Fx42+
if (!support.memory) {
supportOptions.withMemory = false;
supportOptions.withAllocations = false;
}
if (!support.timeline) {
supportOptions.withMarkers = false;
supportOptions.withTicks = false;
}
return extend(options, supportOptions);
function normalizePerformanceFeatures (options, supportedFeatures) {
return Object.keys(options).reduce((modifiedOptions, feature) => {
if (supportedFeatures[feature] !== false) {
modifiedOptions[feature] = options[feature];
}
return modifiedOptions;
}, Object.create(null));
}
/**
@ -112,6 +126,25 @@ function offsetAndScaleTimestamps(timestamps, timeOffset, timeScale) {
}
}
/**
* Push all elements of src array into dest array. Marker data will come in small chunks
* and add up over time, whereas allocation arrays can be > 500000 elements (and
* Function.prototype.apply throws if applying more than 500000 elements, which
* is what spawned this separate function), so iterate one element at a time.
* @see bug 1166823
* @see http://jsperf.com/concat-large-arrays
* @see http://jsperf.com/concat-large-arrays/2
*
* @param {Array} dest
* @param {Array} src
*/
function pushAll (dest, src) {
let length = src.length;
for (let i = 0; i < length; i++) {
dest.push(src[i]);
}
}
/**
* Cache used in `RecordingUtils.getProfileThreadFromAllocations`.
*/
@ -576,6 +609,8 @@ UniqueStacks.prototype.getOrAddStringIndex = function(s) {
return this._uniqueStrings.getOrAddStringIndex(s);
};
exports.pushAll = pushAll;
exports.mapRecordingOptions = mapRecordingOptions;
exports.normalizePerformanceFeatures = normalizePerformanceFeatures;
exports.filterSamples = filterSamples;
exports.offsetSampleTimes = offsetSampleTimes;

View File

@ -0,0 +1,352 @@
/* 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 { Cu } = require("chrome");
const protocol = require("devtools/server/protocol");
const { custom, method, RetVal, Arg, Option, types, preEvent } = protocol;
const { actorBridge } = require("devtools/server/actors/common");
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "merge", "sdk/util/object", true);
loader.lazyRequireGetter(this, "PerformanceIO",
"devtools/toolkit/performance/io");
loader.lazyRequireGetter(this, "RecordingUtils",
"devtools/toolkit/performance/utils");
/**
* A set of functions used by both the front and actor to access
* internal properties.
*/
const PerformanceRecordingCommon = {
// Private fields, only needed when a recording is started or stopped.
_console: false,
_imported: false,
_recording: false,
_completed: false,
_configuration: {},
_startingBufferStatus: null,
_localStartTime: null,
// Serializable fields, necessary and sufficient for import and export.
_label: "",
_duration: 0,
_markers: null,
_frames: null,
_memory: null,
_ticks: null,
_allocations: null,
_profile: null,
/**
* Helper methods for returning the status of the recording.
* These methods should be consistent on both the front and actor.
*/
isRecording: function () { return this._recording; },
isCompleted: function () { return this._completed || this.isImported(); },
isFinalizing: function () { return !this.isRecording() && !this.isCompleted(); },
isConsole: function () { return this._console; },
isImported: function () { return this._imported; },
/**
* Helper methods for returning configuration for the recording.
* These methods should be consistent on both the front and actor.
*/
getConfiguration: function () { return this._configuration; },
getLabel: function () { return this._label; },
/**
* Helper methods for returning recording data.
* These methods should be consistent on both the front and actor.
*/
getMarkers: function() { return this._markers; },
getFrames: function() { return this._frames; },
getMemory: function() { return this._memory; },
getTicks: function() { return this._ticks; },
getAllocations: function() { return this._allocations; },
getProfile: function() { return this._profile; },
getAllData: function () {
let label = this.getLabel();
let duration = this.getDuration();
let markers = this.getMarkers();
let frames = this.getFrames();
let memory = this.getMemory();
let ticks = this.getTicks();
let allocations = this.getAllocations();
let profile = this.getProfile();
let configuration = this.getConfiguration();
return { label, duration, markers, frames, memory, ticks, allocations, profile, configuration };
},
};
/**
* This actor wraps the Performance module at toolkit/devtools/shared/performance.js
* and provides RDP definitions.
*
* @see toolkit/devtools/shared/performance.js for documentation.
*/
let PerformanceRecordingActor = exports.PerformanceRecordingActor = protocol.ActorClass(merge({
typeName: "performance-recording",
form: function(detail) {
if (detail === "actorid") {
return this.actorID;
}
let form = {
actor: this.actorID, // actorID is set when this is added to a pool
configuration: this._configuration,
startingBufferStatus: this._startingBufferStatus,
console: this._console,
label: this._label,
startTime: this._startTime,
localStartTime: this._localStartTime,
recording: this._recording,
completed: this._completed,
duration: this._duration,
};
// Only send profiler data once it exists and it has
// not yet been sent
if (this._profile && !this._sentProfilerData) {
form.profile = this._profile;
this._sentProfilerData = true;
}
return form;
},
/**
* @param {object} conn
* @param {object} options
* A hash of features that this recording is utilizing.
* @param {object} meta
* A hash of temporary metadata for a recording that is recording
* (as opposed to an imported recording).
*/
initialize: function (conn, options, meta) {
protocol.Actor.prototype.initialize.call(this, conn);
this._configuration = {
withMarkers: options.withMarkers || false,
withTicks: options.withTicks || false,
withMemory: options.withMemory || false,
withAllocations: options.withAllocations || false,
withJITOptimizations: options.withJITOptimizations || false,
allocationsSampleProbability: options.allocationsSampleProbability || 0,
allocationsMaxLogLength: options.allocationsMaxLogLength || 0,
bufferSize: options.bufferSize || 0,
sampleFrequency: options.sampleFrequency || 1
};
this._console = !!options.console;
this._label = options.label || "";
if (meta) {
// Store the start time roughly with Date.now() so when we
// are checking the duration during a recording, we can get close
// to the approximate duration to render elements without
// making a real request
this._localStartTime = Date.now();
this._startTime = meta.startTime;
this._startingBufferStatus = {
position: meta.position,
totalSize: meta.totalSize,
generation: meta.generation
};
this._recording = true;
this._markers = [];
this._frames = [];
this._memory = [];
this._ticks = [];
this._allocations = { sites: [], timestamps: [], frames: [], counts: [] };
}
},
destroy: function() {
protocol.Actor.prototype.destroy.call(this);
},
/**
* Internal utility called by the PerformanceActor and PerformanceFront on state changes
* to update the internal state of the PerformanceRecording.
*
* @param {string} state
* @param {object} extraData
*/
_setState: function (state, extraData) {
switch (state) {
case "recording-started": {
this._recording = true;
break;
}
case "recording-stopping": {
this._recording = false;
break;
}
case "recording-stopped": {
this._profile = extraData.profile;
this._duration = extraData.duration;
// We filter out all samples that fall out of current profile's range
// since the profiler is continuously running. Because of this, sample
// times are not guaranteed to have a zero epoch, so offset the
// timestamps.
RecordingUtils.offsetSampleTimes(this._profile, this._startTime);
// Markers need to be sorted ascending by time, to be properly displayed
// in a waterfall view.
this._markers = this._markers.sort((a, b) => (a.start > b.start));
this._completed = true;
break;
}
};
},
}, PerformanceRecordingCommon));
/**
* This can be used on older Profiler implementations, but the methods cannot
* be changed -- you must introduce a new method, and detect the server.
*/
let PerformanceRecordingFront = exports.PerformanceRecordingFront = protocol.FrontClass(PerformanceRecordingActor, merge({
form: function(form, detail) {
if (detail === "actorid") {
this.actorID = form;
return;
}
this.actorID = form.actor;
this._form = form;
this._configuration = form.configuration;
this._startingBufferStatus = form.startingBufferStatus;
this._console = form.console;
this._label = form.label;
this._startTime = form.startTime;
this._localStartTime = form.localStartTime;
this._recording = form.recording;
this._completed = form.completed;
this._duration = form.duration;
if (form.profile) {
this._profile = form.profile;
}
},
initialize: function (client, form, config) {
protocol.Front.prototype.initialize.call(this, client, form);
this._markers = [];
this._frames = [];
this._memory = [];
this._ticks = [];
this._allocations = { sites: [], timestamps: [], frames: [], counts: [] };
},
destroy: function () {
protocol.Front.prototype.destroy.call(this);
},
/**
* Saves the current recording to a file.
*
* @param nsILocalFile file
* The file to stream the data into.
*/
exportRecording: function (file) {
let recordingData = this.getAllData();
return PerformanceIO.saveRecordingToFile(recordingData, file);
},
/**
* Returns the position, generation, and totalSize of the profiler
* when this recording was started.
*
* @return {object}
*/
getStartingBufferStatus: function () {
return this._form.startingBufferStatus;
},
/**
* Gets duration of this recording, in milliseconds.
* @return number
*/
getDuration: function () {
// Compute an approximate ending time for the current recording if it is
// still in progress. This is needed to ensure that the view updates even
// when new data is not being generated. If recording is completed, use
// the duration from the profiler; if between recording and being finalized,
// use the last estimated duration.
if (this.isRecording()) {
return this._estimatedDuration = Date.now() - this._localStartTime;
} else {
return this._duration || this._estimatedDuration || 0;
}
},
/**
* Fired whenever the PerformanceFront emits markers, memory or ticks.
*/
_addTimelineData: function (eventName, data) {
let config = this.getConfiguration();
switch (eventName) {
// Accumulate timeline markers into an array. Furthermore, the timestamps
// do not have a zero epoch, so offset all of them by the start time.
case "markers": {
if (!config.withMarkers) { break; }
let { markers } = data;
RecordingUtils.offsetMarkerTimes(markers, this._startTime);
RecordingUtils.pushAll(this._markers, markers);
break;
}
// Accumulate stack frames into an array.
case "frames": {
if (!config.withMarkers) { break; }
let { frames } = data;
RecordingUtils.pushAll(this._frames, frames);
break;
}
// Accumulate memory measurements into an array. Furthermore, the timestamp
// does not have a zero epoch, so offset it by the actor's start time.
case "memory": {
if (!config.withMemory) { break; }
let { delta, measurement } = data;
this._memory.push({
delta: delta - this._startTime,
value: measurement.total / 1024 / 1024
});
break;
}
// Save the accumulated refresh driver ticks.
case "ticks": {
if (!config.withTicks) { break; }
let { timestamps } = data;
this._ticks = timestamps;
break;
}
// Accumulate allocation sites into an array.
case "allocations": {
if (!config.withAllocations) { break; }
let {
allocations: sites,
allocationsTimestamps: timestamps,
frames,
} = data;
RecordingUtils.offsetAndScaleTimestamps(timestamps, this._startTime);
RecordingUtils.pushAll(this._allocations.sites, sites);
RecordingUtils.pushAll(this._allocations.timestamps, timestamps);
RecordingUtils.pushAll(this._allocations.frames, frames);
break;
}
}
},
toString: () => "[object PerformanceRecordingFront]"
}, PerformanceRecordingCommon));

View File

@ -0,0 +1,259 @@
/* 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 { Cu } = require("chrome");
const protocol = require("devtools/server/protocol");
const { Task } = require("resource://gre/modules/Task.jsm");
const { Actor, custom, method, RetVal, Arg, Option, types, preEvent } = protocol;
const { actorBridge } = require("devtools/server/actors/common");
const { PerformanceRecordingActor, PerformanceRecordingFront } = require("devtools/server/actors/performance-recording");
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "extend", "sdk/util/object", true);
loader.lazyRequireGetter(this, "PerformanceRecorder",
"devtools/toolkit/performance/recorder", true);
loader.lazyRequireGetter(this, "PerformanceIO",
"devtools/toolkit/performance/io");
loader.lazyRequireGetter(this, "normalizePerformanceFeatures",
"devtools/toolkit/performance/utils", true);
loader.lazyRequireGetter(this, "LegacyPerformanceFront",
"devtools/toolkit/performance/legacy/front", true);
const PIPE_TO_FRONT_EVENTS = new Set([
"recording-started", "recording-stopping", "recording-stopped",
"profiler-status", "timeline-data", "console-profile-start"
]);
const RECORDING_STATE_CHANGE_EVENTS = new Set([
"recording-started", "recording-stopping", "recording-stopped"
]);
/**
* This actor wraps the Performance module at toolkit/devtools/shared/performance.js
* and provides RDP definitions.
*
* @see toolkit/devtools/shared/performance.js for documentation.
*/
let PerformanceActor = exports.PerformanceActor = protocol.ActorClass({
typeName: "performance",
traits: {
features: {
withMarkers: true,
withMemory: true,
withTicks: true,
withAllocations: true,
withJITOptimizations: true,
},
},
/**
* The set of events the PerformanceActor emits over RDP.
*/
events: {
"recording-started": {
recording: Arg(0, "performance-recording"),
},
"recording-stopping": {
recording: Arg(0, "performance-recording"),
},
"recording-stopped": {
recording: Arg(0, "performance-recording"),
data: Arg(1, "json"),
},
"profiler-status": {
data: Arg(0, "json"),
},
"console-profile-start": {},
"timeline-data": {
name: Arg(0, "string"),
data: Arg(1, "json"),
recordings: Arg(2, "array:performance-recording"),
},
},
initialize: function (conn, tabActor) {
Actor.prototype.initialize.call(this, conn);
this._onRecorderEvent = this._onRecorderEvent.bind(this);
this.bridge = new PerformanceRecorder(conn, tabActor);
events.on(this.bridge, "*", this._onRecorderEvent);
},
/**
* `disconnect` method required to call destroy, since this
* actor is not managed by a parent actor.
*/
disconnect: function() {
this.destroy();
},
destroy: function () {
events.off(this.bridge, "*", this._onRecorderEvent);
this.bridge.destroy();
protocol.Actor.prototype.destroy.call(this);
},
connect: method(function () {
this.bridge.connect();
return this.traits;
}, { response: RetVal("json") }),
startRecording: method(Task.async(function *(options={}) {
let normalizedOptions = normalizePerformanceFeatures(options, this.traits.features);
let recording = yield this.bridge.startRecording(normalizedOptions);
this.manage(recording);
return recording;
}), {
request: {
options: Arg(0, "nullable:json"),
},
response: RetVal("performance-recording"),
}),
stopRecording: actorBridge("stopRecording", {
request: {
options: Arg(0, "performance-recording"),
},
response: RetVal("performance-recording"),
}),
isRecording: actorBridge("isRecording", {
response: { isRecording: RetVal("boolean") }
}),
getRecordings: actorBridge("getRecordings", {
response: { recordings: RetVal("array:performance-recording") }
}),
getConfiguration: actorBridge("getConfiguration", {
response: { config: RetVal("json") }
}),
setProfilerStatusInterval: actorBridge("setProfilerStatusInterval", {
request: { interval: Arg(0, "number") },
response: { oneway: true }
}),
/**
* Filter which events get piped to the front.
*/
_onRecorderEvent: function (eventName, ...data) {
// If this is a recording state change, call
// a method on the related PerformanceRecordingActor so it can
// update its internal state.
if (RECORDING_STATE_CHANGE_EVENTS.has(eventName)) {
let recording = data[0];
let extraData = data[1];
recording._setState(eventName, extraData);
}
if (PIPE_TO_FRONT_EVENTS.has(eventName)) {
events.emit(this, eventName, ...data);
}
},
});
exports.createPerformanceFront = function createPerformanceFront (target) {
// If we force legacy mode, or the server does not have a performance actor (< Fx42),
// use our LegacyPerformanceFront which will handle
// the communication over RDP to other underlying actors.
if (target.TEST_PERFORMANCE_LEGACY_FRONT || !target.form.performanceActor) {
return new LegacyPerformanceFront(target);
}
// If our server has a PerformanceActor implementation, set this
// up like a normal front.
return new PerformanceFront(target.client, target.form);
};
const PerformanceFront = exports.PerformanceFront = protocol.FrontClass(PerformanceActor, {
initialize: function (client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
this.actorID = form.performanceActor;
this.manage(this);
},
destroy: function () {
protocol.Front.prototype.destroy.call(this);
},
connect: custom(function () {
return this._connect().then(traits => this._traits = traits);
}, {
impl: "_connect"
}),
get traits() {
if (!this._traits) {
Cu.reportError("Cannot access traits of PerformanceFront before calling `connect()`.");
}
return this._traits;
},
/**
* Pass in a PerformanceRecording and get a normalized value from 0 to 1 of how much
* of this recording's lifetime remains without being overwritten.
*
* @param {PerformanceRecording} recording
* @return {number?}
*/
getBufferUsageForRecording: function (recording) {
if (!recording.isRecording()) {
return void 0;
}
let { position: currentPosition, totalSize, generation: currentGeneration } = this._currentBufferStatus;
let { position: origPosition, generation: origGeneration } = recording.getStartingBufferStatus();
let normalizedCurrent = (totalSize * (currentGeneration - origGeneration)) + currentPosition;
let percent = (normalizedCurrent - origPosition) / totalSize;
return percent > 1 ? 1 : percent;
},
/**
* Loads a recording from a file.
*
* @param {nsILocalFile} file
* The file to import the data from.
* @return {Promise<PerformanceRecordingFront>}
*/
importRecording: function (file) {
return PerformanceIO.loadRecordingFromFile(file).then(recordingData => {
let model = new PerformanceRecordingFront();
model._imported = true;
model._label = recordingData.label || "";
model._duration = recordingData.duration;
model._markers = recordingData.markers;
model._frames = recordingData.frames;
model._memory = recordingData.memory;
model._ticks = recordingData.ticks;
model._allocations = recordingData.allocations;
model._profile = recordingData.profile;
model._configuration = recordingData.configuration || {};
return model;
});
},
/**
* Store profiler status when the position has been update so we can
* calculate recording's buffer percentage usage after emitting the event.
*/
_onProfilerStatus: preEvent("profiler-status", function (data) {
this._currentBufferStatus = data;
}),
/**
* For all PerformanceRecordings that are recording, and needing realtime markers,
* apply the timeline data to the front PerformanceRecording (so we only have one event
* for each timeline data chunk as they could be shared amongst several recordings).
*/
_onTimelineEvent: preEvent("timeline-data", function (type, data, recordings) {
for (let recording of recordings) {
recording._addTimelineData(type, data);
}
}),
});

View File

@ -534,6 +534,11 @@ var DebuggerServer = {
constructor: "ProfilerActor",
type: { tab: true }
});
this.registerModule("devtools/server/actors/performance", {
prefix: "performance",
constructor: "PerformanceActor",
type: { tab: true }
});
}
this.registerModule("devtools/server/actors/animation", {
prefix: "animations",

View File

@ -76,6 +76,8 @@ EXTRA_JS_MODULES.devtools.server.actors += [
'actors/monitor.js',
'actors/object.js',
'actors/performance-entries.js',
'actors/performance-recording.js',
'actors/performance.js',
'actors/preference.js',
'actors/pretty-print-worker.js',
'actors/profiler.js',

View File

@ -5,6 +5,7 @@ subsuite = devtools
support-files =
head.js
animation.html
doc_perf.html
navigate-first.html
navigate-second.html
storage-dynamic-windows.html
@ -40,6 +41,20 @@ support-files =
[browser_canvasframe_helper_05.js]
[browser_canvasframe_helper_06.js]
[browser_navigateEvents.js]
[browser_perf-legacy-front-01.js]
[browser_perf-legacy-front-02.js]
[browser_perf-legacy-front-03.js]
[browser_perf-profiler-01.js]
[browser_perf-profiler-02.js]
[browser_perf-profiler-03.js]
[browser_perf-realtime-markers.js]
[browser_perf-recording-actor-01.js]
[browser_perf-recording-actor-02.js]
[browser_perf-samples-01.js]
[browser_perf-samples-02.js]
#[browser_perf-front-profiler-01.js] bug 1077464
#[browser_perf-front-profiler-05.js] bug 1077464
#[browser_perf-front-profiler-06.js]
[browser_storage_dynamic_windows.js]
[browser_storage_listings.js]
[browser_storage_updates.js]

View File

@ -0,0 +1,79 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test basic functionality of PerformanceFront with mock memory and timeline actors.
*/
let WAIT_TIME = 100;
const { TargetFactory } = require("devtools/framework/target");
const { LegacyPerformanceFront } = require("devtools/toolkit/performance/legacy/front");
const { defer } = require("sdk/core/promise");
const { merge } = require("sdk/util/object");
add_task(function*() {
let tab = yield getTab(MAIN_DOMAIN + "doc_perf.html");
let target = TargetFactory.forTab(tab);
yield target.makeRemote();
merge(target, {
TEST_MOCK_TIMELINE_ACTOR: true,
TEST_PERFORMANCE_LEGACY_FRONT: true,
});
let front = new LegacyPerformanceFront(target);
yield front.connect();
ok(front.LEGACY_FRONT, true, "Using legacy front");
front.on("timeline-data", () => ok(false, "There should not be any timeline-data events when mocked"));
let recording = yield front.startRecording({
withTicks: true,
withMarkers: true,
withMemory: true,
withAllocations: true,
});
is(recording.getConfiguration().withMarkers, false, "overrides withMarkers based off of actor support");
is(recording.getConfiguration().withTicks, false, "overrides withTicks based off of actor support");
is(recording.getConfiguration().withMemory, false, "overrides withMemory based off of actor support");
is(recording.getConfiguration().withAllocations, false, "overrides withAllocations based off of actor support");
yield busyWait(WAIT_TIME);
yield front.stopRecording(recording);
ok(typeof recording.getDuration() === "number",
"The front.stopRecording() allows recording to get a duration.");
ok(recording.getDuration() >= 0, "duration is a positive number");
isEmptyArray(recording.getMarkers(), "markers");
isEmptyArray(recording.getTicks(), "ticks");
isEmptyArray(recording.getMemory(), "memory");
isEmptyArray(recording.getAllocations().sites, "allocations.sites");
isEmptyArray(recording.getAllocations().timestamps, "allocations.timestamps");
isEmptyArray(recording.getAllocations().frames, "allocations.frames");
ok(recording.getProfile().threads[0].samples.data.length, "profile data has some samples");
yield front.destroy();
yield closeDebuggerClient(target.client);
gBrowser.removeCurrentTab();
});
function isEmptyArray (array, name) {
ok(Array.isArray(array), `${name} is an array`);
ok(array.length === 0, `${name} is empty`);
}
function getTab (url) {
let tab = gBrowser.selectedTab = gBrowser.addTab();
let loaded = once(gBrowser.selectedBrowser, "load", true);
content.location = url;
return loaded.then(() => {
return new Promise(resolve => {
let isBlank = url == "about:blank";
waitForFocus(() => resolve(tab), content, isBlank);
});
});
}

View File

@ -0,0 +1,79 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test basic functionality of PerformanceFront without a mock Timeline actor.
*/
let WAIT_TIME = 100;
const { TargetFactory } = require("devtools/framework/target");
const { LegacyPerformanceFront } = require("devtools/toolkit/performance/legacy/front");
const { defer } = require("sdk/core/promise");
const { merge } = require("sdk/util/object");
add_task(function*() {
let tab = yield getTab(MAIN_DOMAIN + "doc_perf.html");
let target = TargetFactory.forTab(tab);
yield target.makeRemote();
merge(target, {
TEST_PERFORMANCE_LEGACY_FRONT: true,
});
let front = new LegacyPerformanceFront(target);
yield front.connect();
ok(front.LEGACY_FRONT, true, "Using legacy front");
let recording = yield front.startRecording({
withTicks: true,
withMarkers: true,
withMemory: true,
withAllocations: true,
});
is(recording.getConfiguration().withMarkers, true, "allows withMarkers based off of actor support");
is(recording.getConfiguration().withTicks, true, "allows withTicks based off of actor support");
is(recording.getConfiguration().withMemory, false, "overrides withMemory based off of actor support");
is(recording.getConfiguration().withAllocations, false, "overrides withAllocations based off of actor support");
yield waitUntil(() => recording.getMarkers().length);
yield waitUntil(() => recording.getTicks().length);
yield front.stopRecording(recording);
ok(recording.getMarkers().length, "we have several markers");
ok(recording.getTicks().length, "we have several ticks");
ok(typeof recording.getDuration() === "number",
"The front.stopRecording() allows recording to get a duration.");
ok(recording.getDuration() >= 0, "duration is a positive number");
isEmptyArray(recording.getMemory(), "memory");
isEmptyArray(recording.getAllocations().sites, "allocations.sites");
isEmptyArray(recording.getAllocations().timestamps, "allocations.timestamps");
isEmptyArray(recording.getAllocations().frames, "allocations.frames");
ok(recording.getProfile().threads[0].samples.data.length, "profile data has some samples");
yield front.destroy();
yield closeDebuggerClient(target.client);
gBrowser.removeCurrentTab();
});
function isEmptyArray (array, name) {
ok(Array.isArray(array), `${name} is an array`);
ok(array.length === 0, `${name} is empty`);
}
function getTab (url) {
let tab = gBrowser.selectedTab = gBrowser.addTab();
let loaded = once(gBrowser.selectedBrowser, "load", true);
content.location = url;
return loaded.then(() => {
return new Promise(resolve => {
let isBlank = url == "about:blank";
waitForFocus(() => resolve(tab), content, isBlank);
});
});
}

View File

@ -0,0 +1,51 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that when using an older server (< Fx40) where the profiler actor does not
* have the `getBufferInfo` method that nothing breaks and RecordingModels have null
* `getBufferUsage()` values.
*/
const { TargetFactory } = require("devtools/framework/target");
const { LegacyPerformanceFront } = require("devtools/toolkit/performance/legacy/front");
const { merge } = require("sdk/util/object");
add_task(function*() {
let tab = yield getTab(MAIN_DOMAIN + "doc_perf.html");
let target = TargetFactory.forTab(tab);
yield target.makeRemote();
merge(target, {
TEST_PROFILER_FILTER_STATUS: ["position", "totalSize", "generation"],
TEST_PERFORMANCE_LEGACY_FRONT: true,
});
let front = new LegacyPerformanceFront(target);
yield front.connect();
front.setProfilerStatusInterval(10);
front.on("profiler-status", () => ok(false, "profiler-status should not be called when not supported"));
let model = yield front.startRecording();
yield busyWait(100);
is(front.getBufferUsageForRecording(model), null, "buffer usage for recording should be null");
yield front.stopRecording(model);
yield front.destroy();
yield closeDebuggerClient(target.client);
gBrowser.removeCurrentTab();
});
function getTab (url) {
let tab = gBrowser.selectedTab = gBrowser.addTab();
let loaded = once(gBrowser.selectedBrowser, "load", true);
content.location = url;
return loaded.then(() => {
return new Promise(resolve => {
let isBlank = url == "about:blank";
waitForFocus(() => resolve(tab), content, isBlank);
});
});
}

View File

@ -7,35 +7,37 @@
* a recording is stopped.
*/
let test = Task.async(function*() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
let front = panel.panelWin.gFront;
loadFrameScripts();
const { PerformanceFront } = require("devtools/server/actors/performance");
const { PMM_isProfilerActive, PMM_stopProfiler, PMM_loadProfilerScripts } = require("devtools/toolkit/shared/profiler");
add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "doc_perf.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = PerformanceFront(client, form);
yield front.connect();
PMM_loadProfilerScripts(gBrowser);
ok(!(yield PMM_isProfilerActive()),
"The built-in profiler module should not have been automatically started.");
let activated = front.once("profiler-activated");
let rec = yield front.startRecording();
yield activated;
yield front.stopRecording(rec);
ok((yield PMM_isProfilerActive()),
"The built-in profiler module should still be active (1).");
let alreadyActive = front.once("profiler-already-active");
rec = yield front.startRecording();
yield alreadyActive;
yield front.stopRecording(rec);
ok((yield PMM_isProfilerActive()),
"The built-in profiler module should still be active (2).");
// Manually tear down so we can check profiler status
let tab = panel.target.tab;
yield panel._toolbox.destroy();
yield closeDebuggerClient(client);
ok(!(yield PMM_isProfilerActive()),
"The built-in profiler module should no longer be active.");
yield removeTab(tab);
tab = null;
finish();
gBrowser.removeCurrentTab();
});

View File

@ -0,0 +1,44 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the built-in profiler module doesn't deactivate when the toolbox
* is destroyed if there are other consumers using it.
*/
const { PerformanceFront } = require("devtools/server/actors/performance");
const { PMM_isProfilerActive, PMM_stopProfiler, PMM_loadProfilerScripts } = require("devtools/toolkit/shared/profiler");
add_task(function*() {
yield addTab(MAIN_DOMAIN + "doc_perf.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let firstFront = PerformanceFront(client, form);
yield firstFront.connect();
PMM_loadProfilerScripts(gBrowser);
yield firstFront.startRecording();
yield addTab(MAIN_DOMAIN + "doc_perf.html");
let client2 = new DebuggerClient(DebuggerServer.connectPipe());
let form2 = yield connectDebuggerClient(client2);
let secondFront = PerformanceFront(client2, form2);
yield secondFront.connect();
PMM_loadProfilerScripts(gBrowser);
yield secondFront.startRecording();
// Manually teardown the tabs so we can check profiler status
yield closeDebuggerClient(client2);
ok((yield PMM_isProfilerActive()),
"The built-in profiler module should still be active.");
yield closeDebuggerClient(client);
ok(!(yield PMM_isProfilerActive()),
"The built-in profiler module should no longer be active.");
gBrowser.removeCurrentTab();
gBrowser.removeCurrentTab();
});

View File

@ -0,0 +1,50 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the built-in profiler module is not reactivated if no other
* consumer was using it over the remote debugger protocol, and ensures
* that the actor will work properly even in such cases (e.g. the Gecko Profiler
* addon was installed and automatically activated the profiler module).
*/
const { PerformanceFront } = require("devtools/server/actors/performance");
const { sendProfilerCommand, PMM_isProfilerActive, PMM_stopProfiler, PMM_loadProfilerScripts } = require("devtools/toolkit/shared/profiler");
add_task(function*() {
// Ensure the profiler is already running when the test starts.
PMM_loadProfilerScripts(gBrowser);
let ENTRIES = 1000000;
let INTERVAL = 1;
let FEATURES = ["js"];
yield sendProfilerCommand("StartProfiler", [ENTRIES, INTERVAL, FEATURES, FEATURES.length]);
ok((yield PMM_isProfilerActive()),
"The built-in profiler module should still be active.");
yield addTab(MAIN_DOMAIN + "doc_perf.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let firstFront = PerformanceFront(client, form);
yield firstFront.connect();
let recording = yield firstFront.startRecording();
yield addTab(MAIN_DOMAIN + "doc_perf.html");
let client2 = new DebuggerClient(DebuggerServer.connectPipe());
let form2 = yield connectDebuggerClient(client2);
let secondFront = PerformanceFront(client2, form2);
yield secondFront.connect();
yield closeDebuggerClient(client2);
ok((yield PMM_isProfilerActive()),
"The built-in profiler module should still be active.");
yield closeDebuggerClient(client);
ok(!(yield PMM_isProfilerActive()),
"The built-in profiler module should have been automatically stopped.");
gBrowser.removeCurrentTab();
gBrowser.removeCurrentTab();
});

View File

@ -2,11 +2,20 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test basic functionality of PerformanceFront, retrieving timeline data.
* Test functionality of real time markers.
*/
function* spawnTest() {
let { target, front } = yield initBackend(SIMPLE_URL);
const { PerformanceFront } = require("devtools/server/actors/performance");
const { defer } = require("sdk/core/promise");
add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "doc_perf.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = PerformanceFront(client, form);
yield front.connect();
let lastMemoryDelta = 0;
let lastTickDelta = 0;
@ -18,41 +27,37 @@ function* spawnTest() {
};
let deferreds = {
markers: Promise.defer(),
memory: Promise.defer(),
ticks: Promise.defer()
markers: defer(),
memory: defer(),
ticks: defer()
};
front.on("timeline-data", handler);
yield front.startRecording({ withMarkers: true, withMemory: true, withTicks: true });
let rec = yield front.startRecording({ withMarkers: true, withMemory: true, withTicks: true });
yield Promise.all(Object.keys(deferreds).map(type => deferreds[type].promise));
yield front.stopRecording();
yield front.stopRecording(rec);
front.off("timeline-data", handler);
is(counters.markers.length, 1, "one marker event fired.");
is(counters.memory.length, 3, "three memory events fired.");
is(counters.ticks.length, 3, "three ticks events fired.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
yield closeDebuggerClient(client);
gBrowser.removeCurrentTab();
function handler (_, name, ...args) {
function handler (name, data) {
if (name === "markers") {
if (counters.markers.length >= 1) { return; }
let [markers] = args;
ok(markers[0].start, "received atleast one marker with `start`");
ok(markers[0].end, "received atleast one marker with `end`");
ok(markers[0].name, "received atleast one marker with `name`");
ok(data.markers[0].start, "received atleast one marker with `start`");
ok(data.markers[0].end, "received atleast one marker with `end`");
ok(data.markers[0].name, "received atleast one marker with `name`");
counters.markers.push(markers);
counters.markers.push(data.markers);
}
else if (name === "memory") {
if (counters.memory.length >= 3) { return; }
let [delta, measurement] = args;
let { delta, measurement } = data;
is(typeof delta, "number", "received `delta` in memory event");
ok(delta > lastMemoryDelta, "received `delta` in memory event");
ok(measurement.total, "received `total` in memory event");
@ -62,7 +67,7 @@ function* spawnTest() {
}
else if (name === "ticks") {
if (counters.ticks.length >= 3) { return; }
let [delta, timestamps] = args;
let { delta, timestamps } = data;
ok(delta > lastTickDelta, "received `delta` in ticks event");
// Timestamps aren't guaranteed to always contain tick events, since
@ -84,4 +89,4 @@ function* spawnTest() {
deferreds[name].resolve();
}
};
}
});

View File

@ -6,9 +6,16 @@
* completed, and rec data.
*/
function* spawnTest() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
let { EVENTS, gFront: front, PerformanceController } = panel.panelWin;
const { PerformanceFront } = require("devtools/server/actors/performance");
add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "doc_perf.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = PerformanceFront(client, form);
yield front.connect();
let rec = yield front.startRecording({ withMarkers: true, withTicks: true, withMemory: true });
ok(rec.isRecording(), "RecordingModel is recording when created");
@ -35,6 +42,7 @@ function* spawnTest() {
ok(rec.isCompleted(), "recording is completed once it has profile data");
} else {
ok(!rec.isCompleted(), "recording is not yet completed on 'recording-stopping'");
ok(rec.isFinalizing(), "recording is considered finalizing between 'recording-stopping' and 'recording-stopped'");
}
yield stopped;
@ -44,19 +52,14 @@ function* spawnTest() {
// Export and import a rec, and ensure it has the correct state.
let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
yield PerformanceController.exportRecording("", rec, file);
yield exported;
yield rec.exportRecording(file);
let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
yield PerformanceController.importRecording("", file);
yield imported;
let importedModel = PerformanceController.getCurrentRecording();
let importedModel = yield front.importRecording(file);
ok(importedModel.isCompleted(), "All imported recordings should be completed");
ok(!importedModel.isRecording(), "All imported recordings should not be recording");
ok(importedModel.isImported(), "All imported recordings should be considerd imported");
yield teardown(panel);
finish();
}
yield closeDebuggerClient(client);
gBrowser.removeCurrentTab();
});

View File

@ -6,13 +6,22 @@
*/
let BUFFER_SIZE = 20000;
let config = { bufferSize: BUFFER_SIZE };
function* spawnTest() {
let { target, front } = yield initBackend(SIMPLE_URL, { TEST_MOCK_PROFILER_CHECK_TIMER: 10 });
let config = { bufferSize: BUFFER_SIZE };
const { PerformanceFront } = require("devtools/server/actors/performance");
add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "doc_perf.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = PerformanceFront(client, form);
yield front.connect();
yield front.setProfilerStatusInterval(10);
let model = yield front.startRecording(config);
let [_, stats] = yield onceSpread(front, "profiler-status");
let stats = yield once(front, "profiler-status");
is(stats.totalSize, BUFFER_SIZE, `profiler-status event has totalSize: ${stats.totalSize}`);
ok(stats.position < BUFFER_SIZE, `profiler-status event has position: ${stats.position}`);
ok(stats.generation >= 0, `profiler-status event has generation: ${stats.generation}`);
@ -25,7 +34,7 @@ function* spawnTest() {
let lastBufferStatus = 0;
let checkCount = 0;
while (lastBufferStatus < 1) {
let currentBufferStatus = model.getBufferUsage();
let currentBufferStatus = front.getBufferUsageForRecording(model);
ok(currentBufferStatus > lastBufferStatus, `buffer is more filled than before: ${currentBufferStatus} > ${lastBufferStatus}`);
lastBufferStatus = currentBufferStatus;
checkCount++;
@ -36,11 +45,8 @@ function* spawnTest() {
is(lastBufferStatus, 1, "buffer usage cannot surpass 100%");
yield front.stopRecording(model);
is(model.getBufferUsage(), null, "getBufferUsage() should be null when no longer recording.");
is(front.getBufferUsageForRecording(model), null, "buffer usage should be null when no longer recording.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}
yield closeDebuggerClient(client);
gBrowser.removeCurrentTab();
});

View File

@ -8,14 +8,21 @@
const WAIT_TIME = 1000; // ms
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let front = panel.panelWin.gFront;
const { PerformanceFront } = require("devtools/server/actors/performance");
add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "doc_perf.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = PerformanceFront(client, form);
yield front.connect();
// Perform the first recording...
let firstRecording = yield front.startRecording();
let firstRecordingStartTime = firstRecording._profilerStartTime;
let firstRecordingStartTime = firstRecording._startTime;
info("Started profiling at: " + firstRecordingStartTime);
busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
@ -28,7 +35,7 @@ function* spawnTest() {
// Perform the second recording...
let secondRecording = yield front.startRecording();
let secondRecordingStartTime = secondRecording._profilerStartTime;
let secondRecordingStartTime = secondRecording._startTime;
info("Started profiling at: " + secondRecordingStartTime);
busyWait(WAIT_TIME); // allow the profiler module to sample more cpu activity
@ -49,6 +56,6 @@ function* spawnTest() {
"There should be no samples from the first recording in the second one, " +
"even though the total number of frames did not overflow.");
yield teardown(panel);
finish();
}
yield closeDebuggerClient(client);
gBrowser.removeCurrentTab();
});

View File

@ -0,0 +1,75 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the retrieved profiler data samples always have a (root) node.
* If this ever changes, the |ThreadNode.prototype.insert| function in
* browser/devtools/performance/modules/logic/tree-model.js will have to be changed.
*/
const WAIT_TIME = 1000; // ms
const { PerformanceFront } = require("devtools/server/actors/performance");
add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "doc_perf.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = PerformanceFront(client, form);
yield front.connect();
let rec = yield front.startRecording();
busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
yield front.stopRecording(rec);
let profile = rec.getProfile();
let sampleCount = 0;
for (let thread of profile.threads) {
info("Checking thread: " + thread.name);
for (let sample of thread.samples.data) {
sampleCount++;
let stack = getInflatedStackLocations(thread, sample);
if (stack[0] != "(root)") {
ok(false, "The sample " + stack.toSource() + " doesn't have a root node.");
}
}
}
ok(sampleCount > 0,
"At least some samples have been iterated over, checking for root nodes.");
yield closeDebuggerClient(client);
gBrowser.removeCurrentTab();
});
/**
* Inflate a particular sample's stack and return an array of strings.
*/
function getInflatedStackLocations(thread, sample) {
let stackTable = thread.stackTable;
let frameTable = thread.frameTable;
let stringTable = thread.stringTable;
let SAMPLE_STACK_SLOT = thread.samples.schema.stack;
let STACK_PREFIX_SLOT = stackTable.schema.prefix;
let STACK_FRAME_SLOT = stackTable.schema.frame;
let FRAME_LOCATION_SLOT = frameTable.schema.location;
// Build the stack from the raw data and accumulate the locations in
// an array.
let stackIndex = sample[SAMPLE_STACK_SLOT];
let locations = [];
while (stackIndex !== null) {
let stackEntry = stackTable.data[stackIndex];
let frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]];
locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]);
stackIndex = stackEntry[STACK_PREFIX_SLOT];
}
// The profiler tree is inverted, so reverse the array.
return locations.reverse();
}

View File

@ -0,0 +1,25 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Performance test page</title>
</head>
<body>
<script type="text/javascript">
var x = 1;
function test() {
document.body.style.borderTop = x + "px solid red";
x = 1^x;
document.body.innerHeight; // flush pending reflows
}
// Prevent this script from being garbage collected.
window.setInterval(test, 1);
</script>
</body>
</html>

View File

@ -11,6 +11,7 @@ const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {DebuggerClient} = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
const {DebuggerServer} = require("devtools/server/main");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const PATH = "browser/toolkit/devtools/server/tests/browser/";
const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
@ -137,3 +138,34 @@ registerCleanupFunction(function tearDown() {
gBrowser.removeCurrentTab();
}
});
function idleWait(time) {
return DevToolsUtils.waitForTime(time);
}
function busyWait(time) {
let start = Date.now();
let stack;
while (Date.now() - start < time) { stack = Components.stack; }
}
/**
* Waits until a predicate returns true.
*
* @param function predicate
* Invoked once in a while until it returns true.
* @param number interval [optional]
* How often the predicate is invoked, in milliseconds.
*/
function waitUntil(predicate, interval = 10) {
if (predicate()) {
return Promise.resolve(true);
}
return new Promise(resolve => {
setTimeout(function() {
waitUntil(predicate).then(() => resolve(true));
}, interval);
});
}
// EventUtils just doesn't work!

View File

@ -6,10 +6,13 @@
const { Cc, Ci, Cu } = require("chrome");
const Services = require("Services");
const { Class } = require("sdk/core/heritage");
const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-utils.js";
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);
loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/toolkit/DevToolsUtils.js");
loader.lazyRequireGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm", true);
loader.lazyRequireGetter(this, "Task", "resource://gre/modules/Task.jsm", true);
loader.lazyRequireGetter(this, "uuid", "sdk/util/uuid", true);
// Events piped from system observers to Profiler instances.
const PROFILER_SYSTEM_EVENTS = [
@ -287,6 +290,7 @@ const ProfilerManager = (function () {
* - "console-api-profiler"
* - "profiler-started"
* - "profiler-stopped"
* - "profiler-status"
*
* The ProfilerManager listens to all events, and individual
* consumers filter which events they are interested in.
@ -513,3 +517,49 @@ function sanitizeHandler (handler, identifier) {
return handler.call(this, subject, topic, data);
}, identifier);
}
/**
* The following functions are used in testing to control and inspect
* the nsIProfiler in child process content. These should be called from
* the parent process.
*/
let mm = null;
exports.PMM_isProfilerActive = function () {
return sendProfilerCommand("IsActive");
}
exports.PMM_stopProfiler = function () {
return Task.spawn(function*() {
let isActive = (yield sendProfilerCommand("IsActive")).isActive;
if (isActive) {
return sendProfilerCommand("StopProfiler");
}
});
}
exports.PMM_loadProfilerScripts = function (gBrowser) {
mm = gBrowser.selectedBrowser.messageManager;
mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
};
function sendProfilerCommand (method, args=[]) {
if (!mm) {
throw new Error("`loadFrameScripts()` must be called when using MessageManager.");
}
let id = uuid().toString();
return new Promise(resolve => {
mm.addMessageListener("devtools:test:profiler:response", handler);
mm.sendAsyncMessage("devtools:test:profiler", { method, args, id });
function handler ({ data }) {
if (id !== data.id) {
return;
}
mm.removeMessageListener("devtools:test:profiler:response", handler);
resolve(data.data);
}
});
}
exports.sendProfilerCommand = sendProfilerCommand;