diff options
Diffstat (limited to 'toolkit/devtools/performance/performance-controller.js')
-rw-r--r-- | toolkit/devtools/performance/performance-controller.js | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/toolkit/devtools/performance/performance-controller.js b/toolkit/devtools/performance/performance-controller.js new file mode 100644 index 000000000..c02836cb3 --- /dev/null +++ b/toolkit/devtools/performance/performance-controller.js @@ -0,0 +1,385 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/devtools/Loader.jsm"); +Cu.import("resource://gre/modules/devtools/Console.jsm"); +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); + +devtools.lazyRequireGetter(this, "Services"); +devtools.lazyRequireGetter(this, "promise"); +devtools.lazyRequireGetter(this, "EventEmitter", + "devtools/toolkit/event-emitter"); +devtools.lazyRequireGetter(this, "DevToolsUtils", + "devtools/toolkit/DevToolsUtils"); + +devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT", + "devtools/shared/timeline/global", true); +devtools.lazyRequireGetter(this, "L10N", + "devtools/shared/profiler/global", true); +devtools.lazyRequireGetter(this, "RecordingUtils", + "devtools/performance/recording-utils", true); +devtools.lazyRequireGetter(this, "RecordingModel", + "devtools/performance/recording-model", true); +devtools.lazyRequireGetter(this, "MarkersOverview", + "devtools/shared/timeline/markers-overview", true); +devtools.lazyRequireGetter(this, "MemoryOverview", + "devtools/shared/timeline/memory-overview", true); +devtools.lazyRequireGetter(this, "Waterfall", + "devtools/shared/timeline/waterfall", true); +devtools.lazyRequireGetter(this, "MarkerDetails", + "devtools/shared/timeline/marker-details", true); +devtools.lazyRequireGetter(this, "CallView", + "devtools/shared/profiler/tree-view", true); +devtools.lazyRequireGetter(this, "ThreadNode", + "devtools/shared/profiler/tree-model", true); +devtools.lazyRequireGetter(this, "FrameNode", + "devtools/shared/profiler/tree-model", true); +devtools.lazyRequireGetter(this, "OptionsView", + "devtools/shared/options-view", true); + +devtools.lazyImporter(this, "CanvasGraphUtils", + "resource:///modules/devtools/Graphs.jsm"); +devtools.lazyImporter(this, "LineGraphWidget", + "resource:///modules/devtools/Graphs.jsm"); +devtools.lazyImporter(this, "FlameGraphUtils", + "resource:///modules/devtools/FlameGraph.jsm"); +devtools.lazyImporter(this, "FlameGraph", + "resource:///modules/devtools/FlameGraph.jsm"); +devtools.lazyImporter(this, "SideMenuWidget", + "resource:///modules/devtools/SideMenuWidget.jsm"); + +const BRANCH_NAME = "devtools.performance.ui."; + +// Events emitted by various objects in the panel. +const EVENTS = { + // Fired by the OptionsView when a preference changes. + PREF_CHANGED: "Performance:PrefChanged", + + // Emitted by the PerformanceView when the state (display mode) changes. + UI_STATE_CHANGED: "Performance:UI:StateChanged", + + // Emitted by the PerformanceView on clear button click + UI_CLEAR_RECORDINGS: "Performance:UI:ClearRecordings", + + // 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 button click + UI_IMPORT_RECORDING: "Performance:UI:ImportRecording", + // Emitted by the RecordingsView on export button click + UI_EXPORT_RECORDING: "Performance:UI:ExportRecording", + + // When a recording is started or stopped via the PerformanceController + RECORDING_STARTED: "Performance:RecordingStarted", + RECORDING_STOPPED: "Performance:RecordingStopped", + RECORDING_WILL_START: "Performance:RecordingWillStart", + RECORDING_WILL_STOP: "Performance:RecordingWillStop", + + // Emitted by the PerformanceController or RecordingView + // when a recording model is selected + RECORDING_SELECTED: "Performance:RecordingSelected", + + // When recordings have been cleared out + RECORDINGS_CLEARED: "Performance:RecordingsCleared", + + // When a recording is imported or exported via the PerformanceController + RECORDING_IMPORTED: "Performance:RecordingImported", + RECORDING_EXPORTED: "Performance:RecordingExported", + + // 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", + FRAMERATE_GRAPH_RENDERED: "Performance:UI:OverviewFramerateRendered", + MARKERS_GRAPH_RENDERED: "Performance:UI:OverviewMarkersRendered", + MEMORY_GRAPH_RENDERED: "Performance:UI:OverviewMemoryRendered", + + // Emitted by the OverviewView when a range has been selected in the graphs + OVERVIEW_RANGE_SELECTED: "Performance:UI:OverviewRangeSelected", + // Emitted by the OverviewView when a selection range has been removed + OVERVIEW_RANGE_CLEARED: "Performance:UI:OverviewRangeCleared", + + // Emitted by the DetailsView when a subview is selected + DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected", + + // Emitted by the WaterfallView when it has been rendered + WATERFALL_RENDERED: "Performance:UI:WaterfallRendered", + + // Emitted by the JsCallTreeView when a call tree has been rendered + JS_CALL_TREE_RENDERED: "Performance:UI:JsCallTreeRendered", + + // Emitted by the JsFlameGraphView when it has been rendered + JS_FLAMEGRAPH_RENDERED: "Performance:UI:JsFlameGraphRendered", + + // Emitted by the MemoryCallTreeView when a call tree has been rendered + MEMORY_CALL_TREE_RENDERED: "Performance:UI:MemoryCallTreeRendered", + + // Emitted by the MemoryFlameGraphView when it has been rendered + MEMORY_FLAMEGRAPH_RENDERED: "Performance:UI:MemoryFlameGraphRendered", + + // When a source is shown in the JavaScript Debugger at a specific location. + SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger", + SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger" +}; + +/** + * The current target and the profiler connection, set by this tool's host. + */ +let gToolbox, gTarget, gFront; + +/** + * Initializes the profiler controller and views. + */ +let startupPerformance = Task.async(function*() { + yield promise.all([ + PerformanceController.initialize(), + PerformanceView.initialize() + ]); +}); + +/** + * Destroys the profiler controller and views. + */ +let shutdownPerformance = Task.async(function*() { + yield promise.all([ + PerformanceController.destroy(), + PerformanceView.destroy() + ]); +}); + +/** + * Functions handling target-related lifetime events and + * UI interaction. + */ +let PerformanceController = { + _recordings: [], + _currentRecording: null, + + /** + * Listen for events emitted by the current tab target and + * main UI events. + */ + initialize: Task.async(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.clearRecordings = this.clearRecordings.bind(this); + this._onTimelineData = this._onTimelineData.bind(this); + this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this); + this._onPrefChanged = this._onPrefChanged.bind(this); + + ToolbarView.on(EVENTS.PREF_CHANGED, this._onPrefChanged); + PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording); + PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording); + PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording); + PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings); + RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording); + RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView); + + gFront.on("markers", this._onTimelineData); // timeline markers + gFront.on("frames", this._onTimelineData); // stack frames + gFront.on("memory", this._onTimelineData); // memory measurements + gFront.on("ticks", this._onTimelineData); // framerate + gFront.on("allocations", this._onTimelineData); // memory allocations + }), + + /** + * Remove events handled by the PerformanceController + */ + destroy: function() { + ToolbarView.off(EVENTS.PREF_CHANGED, this._onPrefChanged); + PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording); + PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording); + PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording); + PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings); + RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording); + RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView); + + gFront.off("markers", this._onTimelineData); + gFront.off("frames", this._onTimelineData); + gFront.off("memory", this._onTimelineData); + gFront.off("ticks", this._onTimelineData); + gFront.off("allocations", this._onTimelineData); + }, + + /** + * Get a preference setting from `prefName` via the underlying + * OptionsView in the ToolbarView. + */ + getPref: function (prefName) { + return ToolbarView.optionsView.getPref(prefName); + }, + + /** + * Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED` + * when the front has started to record. + */ + startRecording: Task.async(function *() { + let withMemory = this.getPref("enable-memory"); + let withTicks = this.getPref("enable-framerate"); + let withAllocations = this.getPref("enable-memory"); + + let recording = this._createRecording({ withMemory, withTicks, withAllocations }); + + this.emit(EVENTS.RECORDING_WILL_START, recording); + yield recording.startRecording({ withTicks, withMemory, withAllocations }); + this.emit(EVENTS.RECORDING_STARTED, recording); + + this.setCurrentRecording(recording); + }), + + /** + * Stops recording with the PerformanceFront. Emits `EVENTS.RECORDING_STOPPED` + * when the front has stopped recording. + */ + stopRecording: Task.async(function *() { + let recording = this._getLatestRecording(); + + this.emit(EVENTS.RECORDING_WILL_STOP, recording); + yield recording.stopRecording(); + this.emit(EVENTS.RECORDING_STOPPED, recording); + }), + + /** + * Saves the given recording to a file. Emits `EVENTS.RECORDING_EXPORTED` + * when the file was saved. + * + * @param RecordingModel recording + * The model that holds the recording data. + * @param nsILocalFile file + * The file to stream the data into. + */ + exportRecording: Task.async(function*(_, recording, file) { + yield recording.exportRecording(file); + this.emit(EVENTS.RECORDING_EXPORTED, recording); + }), + + /** + * Clears all recordings from the list as well as the current recording. + * Emits `EVENTS.RECORDINGS_CLEARED` when complete so other components can clean up. + */ + clearRecordings: Task.async(function* () { + let latest = this._getLatestRecording(); + + if (latest && latest.isRecording()) { + yield this.stopRecording(); + } + + this._recordings.length = 0; + this.setCurrentRecording(null); + this.emit(EVENTS.RECORDINGS_CLEARED); + }), + + /** + * Loads a recording from a file, adding it to the recordings list. Emits + * `EVENTS.RECORDING_IMPORTED` when the file was loaded. + * + * @param nsILocalFile file + * The file to import the data from. + */ + importRecording: Task.async(function*(_, file) { + let recording = this._createRecording(); + yield recording.importRecording(file); + + this.emit(EVENTS.RECORDING_IMPORTED, recording); + }), + + /** + * Creates a new RecordingModel, fires events and stores it + * internally in the controller. + * + * @param object options + * @see PerformanceFront.prototype.startRecording + * @return RecordingModel + * The newly created recording model. + */ + _createRecording: function (options={}) { + let { withMemory, withTicks, withAllocations } = options; + let front = gFront; + + let recording = new RecordingModel( + { front, performance, withMemory, withTicks, withAllocations }); + + this._recordings.push(recording); + return recording; + }, + + /** + * Sets the currently active RecordingModel. + * @param RecordingModel recording + */ + setCurrentRecording: function (recording) { + if (this._currentRecording !== recording) { + this._currentRecording = recording; + this.emit(EVENTS.RECORDING_SELECTED, recording); + } + }, + + /** + * Gets the currently active RecordingModel. + * @return RecordingModel + */ + getCurrentRecording: function () { + return this._currentRecording; + }, + + /** + * Get most recently added recording that was triggered manually (via UI). + * @return RecordingModel + */ + _getLatestRecording: function () { + for (let i = this._recordings.length - 1; i >= 0; i--) { + return this._recordings[i]; + } + return null; + }, + + /** + * Fired whenever the PerformanceFront emits markers, memory or ticks. + */ + _onTimelineData: function (...data) { + this._recordings.forEach(e => e.addTimelineData.apply(e, data)); + this.emit(EVENTS.TIMELINE_DATA, ...data); + }, + + /** + * Fired from RecordingsView, we listen on the PerformanceController so we can + * set it here and re-emit on the controller, where all views can listen. + */ + _onRecordingSelectFromView: function (_, recording) { + this.setCurrentRecording(recording); + }, + + /** + * Fired when the ToolbarView fires a PREF_CHANGED event. + * with the value. + */ + _onPrefChanged: function (_, prefName, value) { + this.emit(EVENTS.PREF_CHANGED, prefName, value); + }, + + toString: () => "[object PerformanceController]" +}; + +/** + * Convenient way of emitting events from the controller. + */ +EventEmitter.decorate(PerformanceController); + +/** + * DOM query helpers. + */ +function $(selector, target = document) { + return target.querySelector(selector); +} +function $$(selector, target = document) { + return target.querySelectorAll(selector); +} |