Bug 1077454 - Handle import/export in new performance tool, r=jsantell

This commit is contained in:
Victor Porof 2014-12-23 11:50:50 -05:00
parent 36f390802c
commit ca34cd6763
17 changed files with 486 additions and 85 deletions

View File

@ -4,8 +4,8 @@
"use strict";
const { Cc, Ci, Cu, Cr } = require("chrome");
const { extend } = require("sdk/util/object");
const { Task } = require("resource://gre/modules/Task.jsm");
const { extend } = require("sdk/util/object");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "promise");
@ -20,7 +20,8 @@ loader.lazyImporter(this, "gDevTools",
"resource:///modules/devtools/gDevTools.jsm");
/**
* A cache of all PerformanceActorsConnection instances. The keys are Target objects.
* A cache of all PerformanceActorsConnection instances.
* The keys are Target objects.
*/
let SharedPerformanceActors = new WeakMap();
@ -42,7 +43,7 @@ SharedPerformanceActors.forTarget = function(target) {
};
/**
* A connection to underlying actors (profiler, memory, framerate, etc)
* A connection to underlying actors (profiler, memory, framerate, etc.)
* shared by all tools in a target.
*
* Use `SharedPerformanceActors.forTarget` to make sure you get the same
@ -62,7 +63,6 @@ function PerformanceActorsConnection(target) {
}
PerformanceActorsConnection.prototype = {
/**
* Initializes a connection to the profiler and other miscellaneous actors.
* If already open, nothing happens.
@ -224,10 +224,9 @@ PerformanceFront.prototype = {
// for all targets and interacts with the whole platform, so we don't want
// to affect other clients by stopping (or restarting) it.
if (!isActive) {
// Extend the options so that protocol.js doesn't modify
// the source object.
let options = extend({}, this._customPerformanceOptions);
yield this._request("profiler", "startProfiler", options);
// Extend the profiler options so that protocol.js doesn't modify the original.
let profilerOptions = extend({}, this._customProfilerOptions);
yield this._request("profiler", "startProfiler", profilerOptions);
this._profilingStartTime = 0;
this.emit("profiler-activated");
} else {
@ -237,9 +236,9 @@ PerformanceFront.prototype = {
// The timeline actor is target-dependent, so just make sure
// it's recording.
// Return start time from timeline actor
let startTime = yield this._request("timeline", "start", options);
// Return only the start time from the timeline actor.
return { startTime };
}),
@ -273,7 +272,7 @@ PerformanceFront.prototype = {
*
* Used in tests and for older backend implementations.
*/
_customPerformanceOptions: {
_customProfilerOptions: {
entries: 1000000,
interval: 1,
features: ["js"]

View File

@ -0,0 +1,102 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci, Cu, Cr } = require("chrome");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "promise");
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_VERSION = 1;
/**
* 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_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(file);
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 (recordingData.version != PERF_TOOL_SERIALIZER_VERSION) {
deferred.reject(new Error("Unsupported recording data file version."));
return;
}
deferred.resolve(recordingData);
});
return deferred.promise;
}
};
exports.PerformanceIO = PerformanceIO;

View File

@ -5,6 +5,7 @@
EXTRA_JS_MODULES.devtools.performance += [
'modules/front.js',
'modules/io.js',
'panel.js'
]

View File

@ -16,9 +16,11 @@ devtools.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
devtools.lazyRequireGetter(this, "DevToolsUtils",
"devtools/toolkit/DevToolsUtils");
devtools.lazyRequireGetter(this, "L10N",
"devtools/profiler/global", true);
devtools.lazyRequireGetter(this, "PerformanceIO",
"devtools/performance/io", true);
devtools.lazyRequireGetter(this, "MarkersOverview",
"devtools/timeline/markers-overview", true);
devtools.lazyRequireGetter(this, "MemoryOverview",
@ -39,16 +41,24 @@ devtools.lazyImporter(this, "LineGraphWidget",
// Events emitted by various objects in the panel.
const EVENTS = {
// Emitted by the PerformanceView on record button click
UI_START_RECORDING: "Performance:UI:StartRecording",
UI_STOP_RECORDING: "Performance:UI:StopRecording",
// Emitted by the PerformanceView on import or export button click
UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
// When a recording is started or stopped via the PerformanceController
RECORDING_STARTED: "Performance:RecordingStarted",
RECORDING_STOPPED: "Performance:RecordingStopped",
// When the PerformanceController has new recording data.
TIMELINE_DATA: "Performance:TimelineData",
// When a recording is imported or exported via the PerformanceController
RECORDING_IMPORTED: "Performance:RecordingImported",
RECORDING_EXPORTED: "Performance:RecordingExported",
// Emitted by the PerformanceView on record button click
UI_START_RECORDING: "Performance:UI:StartRecording",
UI_STOP_RECORDING: "Performance:UI:StopRecording",
// When the PerformanceController has new recording data
TIMELINE_DATA: "Performance:TimelineData",
// Emitted by the OverviewView when more data has been rendered
OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
@ -75,6 +85,11 @@ const EVENTS = {
WATERFALL_RENDERED: "Performance:UI:WaterfallRendered"
};
// Constant defining the end time for a recording that hasn't finished
// or is not yet available.
const RECORDING_IN_PROGRESS = -1;
const RECORDING_UNAVAILABLE = null;
/**
* The current target and the profiler connection, set by this tool's host.
*/
@ -128,11 +143,13 @@ let PerformanceController = {
* Permanent storage for the markers and the memory measurements streamed by
* the backend, along with the start and end timestamps.
*/
_startTime: 0,
_endTime: 0,
_localStartTime: RECORDING_UNAVAILABLE,
_startTime: RECORDING_UNAVAILABLE,
_endTime: RECORDING_UNAVAILABLE,
_markers: [],
_memory: [],
_ticks: [],
_profilerData: {},
/**
* Listen for events emitted by the current tab target and
@ -141,10 +158,15 @@ let PerformanceController = {
initialize: function() {
this.startRecording = this.startRecording.bind(this);
this.stopRecording = this.stopRecording.bind(this);
this.importRecording = this.importRecording.bind(this);
this.exportRecording = this.exportRecording.bind(this);
this._onTimelineData = this._onTimelineData.bind(this);
PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
PerformanceView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
gFront.on("ticks", this._onTimelineData); // framerate
gFront.on("markers", this._onTimelineData); // timeline markers
gFront.on("memory", this._onTimelineData); // timeline memory
@ -156,6 +178,9 @@ let PerformanceController = {
destroy: function() {
PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
PerformanceView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
gFront.off("ticks", this._onTimelineData);
gFront.off("markers", this._onTimelineData);
gFront.off("memory", this._onTimelineData);
@ -178,12 +203,12 @@ let PerformanceController = {
});
this._startTime = startTime;
this._endTime = startTime;
this._endTime = RECORDING_IN_PROGRESS;
this._markers = [];
this._memory = [];
this._ticks = [];
this.emit(EVENTS.RECORDING_STARTED, this._startTime);
this.emit(EVENTS.RECORDING_STARTED);
}),
/**
@ -195,24 +220,76 @@ let PerformanceController = {
// If `endTime` is not yielded from timeline actor (< Fx36), fake it.
if (!results.endTime) {
results.endTime = this._startTime + this.getInterval().localElapsedTime;
results.endTime = this._startTime + this.getLocalElapsedTime();
}
this._endTime = results.endTime;
this._profilerData = results.profilerData;
this._markers = this._markers.sort((a,b) => (a.start > b.start));
this.emit(EVENTS.RECORDING_STOPPED, results);
this.emit(EVENTS.RECORDING_STOPPED);
}),
/**
* Saves the current recording to a file.
*
* @param nsILocalFile file
* The file to stream the data into.
*/
exportRecording: Task.async(function*(_, file) {
let recordingData = this.getAllData();
yield PerformanceIO.saveRecordingToFile(recordingData, file);
this.emit(EVENTS.RECORDING_EXPORTED, recordingData);
}),
/**
* Loads a recording from a file, replacing the current one.
* XXX: Handle multiple recordings, bug 1111004.
*
* @param nsILocalFile file
* The file to import the data from.
*/
importRecording: Task.async(function*(_, file) {
let recordingData = yield PerformanceIO.loadRecordingFromFile(file);
this._startTime = recordingData.interval.startTime;
this._endTime = recordingData.interval.endTime;
this._markers = recordingData.markers;
this._memory = recordingData.memory;
this._ticks = recordingData.ticks;
this._profilerData = recordingData.profilerData;
this.emit(EVENTS.RECORDING_IMPORTED, recordingData);
// Flush the current recording.
this.emit(EVENTS.RECORDING_STARTED);
this.emit(EVENTS.RECORDING_STOPPED);
}),
/**
* Gets the amount of time elapsed locally after starting a recording.
*/
getLocalElapsedTime: function() {
return performance.now() - this._localStartTime;
},
/**
* Gets the time interval for the current recording.
* @return object
*/
getInterval: function() {
let localElapsedTime = performance.now() - this._localStartTime;
let startTime = this._startTime;
let endTime = this._endTime;
return { localElapsedTime, startTime, endTime };
// Compute an approximate ending time for the current recording. This is
// needed to ensure that the view updates even when new data is
// not being generated.
if (endTime == RECORDING_IN_PROGRESS) {
endTime = startTime + this.getLocalElapsedTime();
}
return { startTime, endTime };
},
/**
@ -239,6 +316,26 @@ let PerformanceController = {
return this._ticks;
},
/**
* Gets the profiler data in this recording.
* @return array
*/
getProfilerData: function() {
return this._profilerData;
},
/**
* Gets all the data in this recording.
*/
getAllData: function() {
let interval = this.getInterval();
let markers = this.getMarkers();
let memory = this.getMemory();
let ticks = this.getTicks();
let profilerData = this.getProfilerData();
return { interval, markers, memory, ticks, profilerData };
},
/**
* Fired whenever the PerformanceFront emits markers, memory or ticks.
*/

View File

@ -12,12 +12,18 @@ let PerformanceView = {
*/
initialize: function () {
this._recordButton = $("#record-button");
this._importButton = $("#import-button");
this._exportButton = $("#export-button");
this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
this._onImportButtonClick = this._onImportButtonClick.bind(this);
this._onExportButtonClick = this._onExportButtonClick.bind(this);
this._lockRecordButton = this._lockRecordButton.bind(this);
this._unlockRecordButton = this._unlockRecordButton.bind(this);
this._recordButton.addEventListener("click", this._onRecordButtonClick);
this._importButton.addEventListener("click", this._onImportButtonClick);
this._exportButton.addEventListener("click", this._onExportButtonClick);
// Bind to controller events to unlock the record button
PerformanceController.on(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
@ -34,6 +40,9 @@ let PerformanceView = {
*/
destroy: function () {
this._recordButton.removeEventListener("click", this._onRecordButtonClick);
this._importButton.removeEventListener("click", this._onImportButtonClick);
this._exportButton.removeEventListener("click", this._onExportButtonClick);
PerformanceController.off(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
@ -71,6 +80,37 @@ let PerformanceView = {
this._lockRecordButton();
this.emit(EVENTS.UI_START_RECORDING);
}
},
/**
* Handler for clicking the import button.
*/
_onImportButtonClick: function(e) {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen);
fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
if (fp.show() == Ci.nsIFilePicker.returnOK) {
this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file);
}
},
/**
* Handler for clicking the export button.
*/
_onExportButtonClick: function(e) {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave);
fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
fp.defaultString = "profile.json";
fp.open({ done: result => {
if (result != Ci.nsIFilePicker.returnCancel) {
this.emit(EVENTS.UI_EXPORT_RECORDING, fp.file);
}
}});
}
};

View File

@ -36,6 +36,9 @@
<toolbarbutton id="import-button"
class="devtools-toolbarbutton"
label="&profilerUI.importButton;"/>
<toolbarbutton id="export-button"
class="devtools-toolbarbutton"
label="&profilerUI.exportButton;"/>
</hbox>
</toolbar>

View File

@ -9,36 +9,32 @@ support-files =
# that need to be moved over to performance tool
[browser_perf-aaa-run-first-leaktest.js]
[browser_perf-front.js]
[browser_perf-front-basic-timeline-01.js]
[browser_perf-data-massaging-01.js]
[browser_perf-data-samples.js]
[browser_perf-details-calltree-render-01.js]
[browser_perf-details-calltree-render-02.js]
[browser_perf-details-waterfall-render-01.js]
[browser_perf-details.js]
[browser_perf-front-basic-profiler-01.js]
# bug 1077464
#[browser_perf-front-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]
# bug 1077464
#[browser_perf-front-profiler-05.js]
# bug 1077464
#[browser_perf-front-profiler-05.js] bug 1077464
#[browser_perf-front-profiler-06.js]
# needs shared connection with profiler's shared connection
#[browser_perf-shared-connection-01.js]
[browser_perf-shared-connection-02.js]
[browser_perf-shared-connection-03.js]
# bug 1077464
#[browser_perf-shared-connection-04.js]
[browser_perf-data-samples.js]
[browser_perf-data-massaging-01.js]
[browser_perf-ui-recording.js]
[browser_perf-front.js]
[browser_perf-jump-to-debugger-01.js]
[browser_perf-jump-to-debugger-02.js]
[browser_perf-overview-render-01.js]
[browser_perf-overview-render-02.js]
[browser_perf-overview-selection-01.js]
[browser_perf-overview-selection-02.js]
[browser_perf-overview-selection-03.js]
[browser_perf-details.js]
[browser_perf-jump-to-debugger-01.js]
[browser_perf-jump-to-debugger-02.js]
[browser_perf-details-calltree-render-01.js]
[browser_perf-details-calltree-render-02.js]
[browser_perf-details-waterfall-render-01.js]
[browser_perf-shared-connection-02.js]
[browser_perf-shared-connection-03.js]
# [browser_perf-shared-connection-04.js] bug 1077464
[browser_perf-ui-recording.js]
[browser_perf_recordings-io-01.js]
[browser_perf_recordings-io-02.js]
[browser_perf_recordings-io-03.js]

View File

@ -0,0 +1,63 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the performance tool is able to save and load recordings.
*/
let test = Task.async(function*() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
let { EVENTS, PerformanceController } = panel.panelWin;
yield startRecording(panel);
yield stopRecording(panel);
// Verify original recording.
let originalData = PerformanceController.getAllData();
ok(originalData, "The original recording is not empty.");
// Save recording.
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("", file);
yield exported;
ok(true, "The recording data appears to have been successfully saved.");
// Import recording.
let rerendered = waitForWidgetsRendered(panel);
let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
yield PerformanceController.importRecording("", file);
yield imported;
ok(true, "The recording data appears to have been successfully imported.");
yield rerendered;
ok(true, "The imported data was re-rendered.");
// Verify imported recording.
let importedData = PerformanceController.getAllData();
is(importedData.startTime, originalData.startTime,
"The impored data is identical to the original data (1).");
is(importedData.endTime, originalData.endTime,
"The impored data is identical to the original data (2).");
is(importedData.markers.toSource(), originalData.markers.toSource(),
"The impored data is identical to the original data (3).");
is(importedData.memory.toSource(), originalData.memory.toSource(),
"The impored data is identical to the original data (4).");
is(importedData.ticks.toSource(), originalData.ticks.toSource(),
"The impored data is identical to the original data (5).");
is(importedData.profilerData.toSource(), originalData.profilerData.toSource(),
"The impored data is identical to the original data (6).");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,25 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the performance tool gracefully handles loading bogus files.
*/
let test = Task.async(function*() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
let { EVENTS, PerformanceController } = panel.panelWin;
let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
try {
yield PerformanceController.importRecording("", file);
ok(false, "The recording succeeded unexpectedly.");
} catch (e) {
is(e.message, "Could not read recording data file.");
ok(true, "The recording was cancelled.");
}
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,54 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the performance tool gracefully handles loading files that are JSON,
* but don't contain the appropriate recording data.
*/
let { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
let { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
let test = Task.async(function*() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
let { EVENTS, PerformanceController } = panel.panelWin;
let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
yield asyncCopy({ bogus: "data" }, file);
try {
yield PerformanceController.importRecording("", file);
ok(false, "The recording succeeded unexpectedly.");
} catch (e) {
is(e.message, "Unrecognized recording data file.");
ok(true, "The recording was cancelled.");
}
yield teardown(panel);
finish();
});
function getUnicodeConverter() {
let className = "@mozilla.org/intl/scriptableunicodeconverter";
let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
return converter;
}
function asyncCopy(data, file) {
let deferred = Promise.defer();
let string = JSON.stringify(data);
let inputStream = getUnicodeConverter().convertToInputStream(string);
let outputStream = FileUtils.openSafeFileOutputStream(file);
NetUtil.asyncCopy(inputStream, outputStream, status => {
if (!Components.isSuccessCode(status)) {
deferred.reject(new Error("Could not save data to file."));
}
deferred.resolve();
});
return deferred.promise;
}

View File

@ -279,6 +279,19 @@ function* stopRecording(panel) {
"The record button should not be locked.");
}
function waitForWidgetsRendered(panel) {
let { EVENTS, OverviewView, CallTreeView, WaterfallView } = panel.panelWin;
return Promise.all([
once(OverviewView, EVENTS.FRAMERATE_GRAPH_RENDERED),
once(OverviewView, EVENTS.MARKERS_GRAPH_RENDERED),
once(OverviewView, EVENTS.MEMORY_GRAPH_RENDERED),
once(OverviewView, EVENTS.OVERVIEW_RENDERED),
once(CallTreeView, EVENTS.CALL_TREE_RENDERED),
once(WaterfallView, EVENTS.WATERFALL_RENDERED)
]);
}
/**
* Waits until a predicate returns true.
*

View File

@ -12,28 +12,32 @@ let CallTreeView = {
*/
initialize: function () {
this._callTree = $(".call-tree-cells-container");
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onRangeChange = this._onRangeChange.bind(this);
this._onLink = this._onLink.bind(this);
this._stop = this._stop.bind(this);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._stop);
},
/**
* Unbinds events.
*/
destroy: function () {
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._stop);
},
/**
* Method for handling all the set up for rendering a new call tree.
*/
render: function (profilerData, beginAt, endAt, options={}) {
// Empty recordings might yield no profiler data.
if (profilerData.profile == null) {
return;
}
let threadNode = this._prepareCallTree(profilerData, beginAt, endAt, options);
this._populateCallTree(threadNode, options);
this.emit(EVENTS.CALL_TREE_RENDERED);
@ -42,8 +46,8 @@ let CallTreeView = {
/**
* Called when recording is stopped.
*/
_stop: function (_, { profilerData }) {
this._profilerData = profilerData;
_onRecordingStopped: function () {
let profilerData = PerformanceController.getProfilerData();
this.render(profilerData);
},
@ -53,8 +57,9 @@ let CallTreeView = {
_onRangeChange: function (_, params) {
// When a range is cleared, we'll have no beginAt/endAt data,
// so the rebuild will just render all the data again.
let profilerData = PerformanceController.getProfilerData();
let { beginAt, endAt } = params || {};
this.render(this._profilerData, beginAt, endAt);
this.render(profilerData, beginAt, endAt);
},
/**
@ -122,18 +127,19 @@ let viewSourceInDebugger = Task.async(function *(url, line) {
// source immediately. Otherwise, initialize it and wait for the sources
// to be added first.
let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger");
let { panelWin: dbg } = yield gToolbox.selectTool("jsdebugger");
if (!debuggerAlreadyOpen) {
yield new Promise((resolve) => dbg.once(dbg.EVENTS.SOURCES_ADDED, () => resolve(dbg)));
yield dbg.once(dbg.EVENTS.SOURCES_ADDED);
}
let { DebuggerView } = dbg;
let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === url);
let { Sources } = DebuggerView;
let item = Sources.getItemForAttachment(a => a.source.url === url);
if (item) {
return DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true });
}
return Promise.reject();
return Promise.reject("Couldn't find the specified source in the debugger.");
});

View File

@ -11,8 +11,8 @@ let WaterfallView = {
* Sets up the view with event binding.
*/
initialize: Task.async(function *() {
this._start = this._start.bind(this);
this._stop = this._stop.bind(this);
this._onRecordingStarted = this._onRecordingStarted.bind(this);
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onMarkerSelected = this._onMarkerSelected.bind(this);
this._onResize = this._onResize.bind(this);
@ -23,8 +23,8 @@ let WaterfallView = {
this.graph.on("unselected", this._onMarkerSelected);
this.markerDetails.on("resize", this._onResize);
PerformanceController.on(EVENTS.RECORDING_STARTED, this._start);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._stop);
PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
this.graph.recalculateBounds();
}),
@ -37,8 +37,8 @@ let WaterfallView = {
this.graph.off("unselected", this._onMarkerSelected);
this.markerDetails.off("resize", this._onResize);
PerformanceController.off(EVENTS.RECORDING_STARTED, this._start);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._stop);
PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
},
/**
@ -55,14 +55,14 @@ let WaterfallView = {
/**
* Called when recording starts.
*/
_start: function (_, { startTime }) {
_onRecordingStarted: function () {
this.graph.clearView();
},
/**
* Called when recording stops.
*/
_stop: function (_, { endTime }) {
_onRecordingStopped: function () {
this.render();
},
@ -88,7 +88,6 @@ let WaterfallView = {
}
};
/**
* Convenient way of emitting events from the view.
*/

View File

@ -32,6 +32,7 @@ let DetailsView = {
yield CallTreeView.initialize();
yield WaterfallView.initialize();
this.selectView(DEFAULT_DETAILS_SUBVIEW);
}),

View File

@ -21,15 +21,15 @@ const GRAPH_SCROLL_EVENTS_DRAIN = 50; // ms
/**
* View handler for the overview panel's time view, displaying
* framerate over time.
* framerate, markers and memory over time.
*/
let OverviewView = {
/**
* Sets up the view with event binding.
*/
initialize: Task.async(function *() {
this._start = this._start.bind(this);
this._stop = this._stop.bind(this);
this._onRecordingStarted = this._onRecordingStarted.bind(this);
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onRecordingTick = this._onRecordingTick.bind(this);
this._onGraphMouseUp = this._onGraphMouseUp.bind(this);
this._onGraphScroll = this._onGraphScroll.bind(this);
@ -45,8 +45,8 @@ let OverviewView = {
this.memoryOverview.on("mouseup", this._onGraphMouseUp);
this.memoryOverview.on("scroll", this._onGraphScroll);
PerformanceController.on(EVENTS.RECORDING_STARTED, this._start);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._stop);
PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
}),
/**
@ -61,8 +61,8 @@ let OverviewView = {
this.memoryOverview.off("scroll", this._onGraphScroll);
clearNamedTimeout("graph-scroll");
PerformanceController.off(EVENTS.RECORDING_STARTED, this._start);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._stop);
PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
},
/**
@ -112,12 +112,6 @@ let OverviewView = {
let memory = PerformanceController.getMemory();
let timestamps = PerformanceController.getTicks();
// Compute an approximate ending time for the view. This is
// needed to ensure that the view updates even when new data is
// not being generated.
let fakeTime = interval.startTime + interval.localElapsedTime;
interval.endTime = fakeTime;
this.markersOverview.setData({ interval, markers });
this.emit(EVENTS.MARKERS_GRAPH_RENDERED);
@ -133,7 +127,7 @@ let OverviewView = {
/**
* Called at most every OVERVIEW_UPDATE_INTERVAL milliseconds
* and uses data fetched from `_onTimelineData` to render
* and uses data fetched from the controller to render
* data into all the corresponding overview graphs.
*/
_onRecordingTick: Task.async(function *() {
@ -167,7 +161,7 @@ let OverviewView = {
/**
* Listener handling the "scroll" event for the framerate graph.
* Fires an event to be handled elsewhere.
* Fires a debounced event to be handled elsewhere.
*/
_onGraphScroll: function () {
setNamedTimeout("graph-scroll", GRAPH_SCROLL_EVENTS_DRAIN, () => {
@ -189,7 +183,7 @@ let OverviewView = {
/**
* Called when recording starts.
*/
_start: function () {
_onRecordingStarted: function () {
this._timeoutId = setTimeout(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
this.framerateGraph.dropSelection();
@ -201,7 +195,7 @@ let OverviewView = {
/**
* Called when recording stops.
*/
_stop: function () {
_onRecordingStopped: function () {
clearTimeout(this._timeoutId);
this._timeoutId = null;

View File

@ -2101,7 +2101,11 @@ this.CanvasGraphUtils = {
* @return number
*/
function map(value, istart, istop, ostart, ostop) {
return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
let ratio = istop - istart;
if (ratio == 0) {
return value;
}
return ostart + (ostop - ostart) * ((value - istart) / ratio);
}
/**

View File

@ -33,6 +33,10 @@
- on a button that opens a dialog to import a saved profile data file. -->
<!ENTITY profilerUI.importButton "Import…">
<!-- LOCALIZATION NOTE (profilerUI.exportButton): This string is displayed
- on a button that opens a dialog to export a saved profile data file. -->
<!ENTITY profilerUI.exportButton "Save">
<!-- LOCALIZATION NOTE (profilerUI.clearButton): This string is displayed
- on a button that remvoes all the recordings. -->
<!ENTITY profilerUI.clearButton "Clear">