From 4bd97bc067233fabb7a23583ee419f4753c3489e Mon Sep 17 00:00:00 2001 From: Victor Porof Date: Tue, 23 Dec 2014 11:50:50 -0500 Subject: [PATCH] Bug 1077454 - Handle import/export in new performance tool, r=jsantell --- browser/devtools/performance/modules/front.js | 21 ++- browser/devtools/performance/modules/io.js | 102 ++++++++++++++ browser/devtools/performance/moz.build | 1 + .../performance/performance-controller.js | 125 ++++++++++++++++-- .../devtools/performance/performance-view.js | 40 ++++++ browser/devtools/performance/performance.xul | 3 + browser/devtools/performance/test/browser.ini | 42 +++--- .../test/browser_perf_recordings-io-01.js | 63 +++++++++ .../test/browser_perf_recordings-io-02.js | 25 ++++ .../test/browser_perf_recordings-io-03.js | 54 ++++++++ browser/devtools/performance/test/head.js | 13 ++ .../performance/views/details-call-tree.js | 26 ++-- .../performance/views/details-waterfall.js | 17 ++- browser/devtools/performance/views/details.js | 1 + .../devtools/performance/views/overview.js | 28 ++-- browser/devtools/shared/widgets/Graphs.jsm | 6 +- .../chrome/browser/devtools/profiler.dtd | 4 + 17 files changed, 486 insertions(+), 85 deletions(-) create mode 100644 browser/devtools/performance/modules/io.js create mode 100644 browser/devtools/performance/test/browser_perf_recordings-io-01.js create mode 100644 browser/devtools/performance/test/browser_perf_recordings-io-02.js create mode 100644 browser/devtools/performance/test/browser_perf_recordings-io-03.js 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. --> + + +