diff --git a/browser/devtools/performance/modules/front.js b/browser/devtools/performance/modules/front.js
index dd7057ae7637..85c2e2c74d78 100644
--- a/browser/devtools/performance/modules/front.js
+++ b/browser/devtools/performance/modules/front.js
@@ -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"]
diff --git a/browser/devtools/performance/modules/io.js b/browser/devtools/performance/modules/io.js
new file mode 100644
index 000000000000..7fe968b98282
--- /dev/null
+++ b/browser/devtools/performance/modules/io.js
@@ -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;
diff --git a/browser/devtools/performance/moz.build b/browser/devtools/performance/moz.build
index 816eb2b0b676..e133978e6384 100644
--- a/browser/devtools/performance/moz.build
+++ b/browser/devtools/performance/moz.build
@@ -5,6 +5,7 @@
EXTRA_JS_MODULES.devtools.performance += [
'modules/front.js',
+ 'modules/io.js',
'panel.js'
]
diff --git a/browser/devtools/performance/performance-controller.js b/browser/devtools/performance/performance-controller.js
index 587c81809945..a58ad17715fd 100644
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -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.
*/
diff --git a/browser/devtools/performance/performance-view.js b/browser/devtools/performance/performance-view.js
index d51a4df0ebbb..7a1d5cc8fcf4 100644
--- a/browser/devtools/performance/performance-view.js
+++ b/browser/devtools/performance/performance-view.js
@@ -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);
+ }
+ }});
}
};
diff --git a/browser/devtools/performance/performance.xul b/browser/devtools/performance/performance.xul
index f06caa7a5f3c..586cadd34448 100644
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -36,6 +36,9 @@
+
diff --git a/browser/devtools/performance/test/browser.ini b/browser/devtools/performance/test/browser.ini
index 7e7c9d588d2f..2dced93ac7ba 100644
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -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]
diff --git a/browser/devtools/performance/test/browser_perf_recordings-io-01.js b/browser/devtools/performance/test/browser_perf_recordings-io-01.js
new file mode 100644
index 000000000000..c9ccff0b144a
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf_recordings-io-01.js
@@ -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();
+});
diff --git a/browser/devtools/performance/test/browser_perf_recordings-io-02.js b/browser/devtools/performance/test/browser_perf_recordings-io-02.js
new file mode 100644
index 000000000000..d1f9b06fcb8e
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf_recordings-io-02.js
@@ -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();
+});
diff --git a/browser/devtools/performance/test/browser_perf_recordings-io-03.js b/browser/devtools/performance/test/browser_perf_recordings-io-03.js
new file mode 100644
index 000000000000..742d5c411ea9
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf_recordings-io-03.js
@@ -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;
+}
diff --git a/browser/devtools/performance/test/head.js b/browser/devtools/performance/test/head.js
index f8b8a35fccdf..95507c038a03 100644
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -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.
*
diff --git a/browser/devtools/performance/views/details-call-tree.js b/browser/devtools/performance/views/details-call-tree.js
index 8997e899dd33..0b75f163e568 100644
--- a/browser/devtools/performance/views/details-call-tree.js
+++ b/browser/devtools/performance/views/details-call-tree.js
@@ -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.");
});
diff --git a/browser/devtools/performance/views/details-waterfall.js b/browser/devtools/performance/views/details-waterfall.js
index d807cb042837..4d9ab9d427ee 100644
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -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.
*/
diff --git a/browser/devtools/performance/views/details.js b/browser/devtools/performance/views/details.js
index 9dbed62e1ea5..bc7c21f84aea 100644
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -32,6 +32,7 @@ let DetailsView = {
yield CallTreeView.initialize();
yield WaterfallView.initialize();
+
this.selectView(DEFAULT_DETAILS_SUBVIEW);
}),
diff --git a/browser/devtools/performance/views/overview.js b/browser/devtools/performance/views/overview.js
index 81bdd2b911bd..1b361e5636fe 100644
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -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;
diff --git a/browser/devtools/shared/widgets/Graphs.jsm b/browser/devtools/shared/widgets/Graphs.jsm
index ecbfe0ce275a..ec80948677c5 100644
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -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);
}
/**
diff --git a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
index 4f5c097cdd90..4cab3121ab27 100644
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
@@ -33,6 +33,10 @@
- on a button that opens a dialog to import a saved profile data file. -->
+
+
+