diff options
Diffstat (limited to 'devtools/client/performance/views/details.js')
-rw-r--r-- | devtools/client/performance/views/details.js | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/devtools/client/performance/views/details.js b/devtools/client/performance/views/details.js new file mode 100644 index 000000000..95557bc36 --- /dev/null +++ b/devtools/client/performance/views/details.js @@ -0,0 +1,263 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* globals WaterfallView, JsCallTreeView, JsFlameGraphView, MemoryCallTreeView, + MemoryFlameGraphView */ +"use strict"; + +/** + * Details view containing call trees, flamegraphs and markers waterfall. + * Manages subviews and toggles visibility between them. + */ +var DetailsView = { + /** + * Name to (node id, view object, actor requirements, pref killswitch) + * mapping of subviews. + */ + components: { + "waterfall": { + id: "waterfall-view", + view: WaterfallView, + features: ["withMarkers"] + }, + "js-calltree": { + id: "js-profile-view", + view: JsCallTreeView + }, + "js-flamegraph": { + id: "js-flamegraph-view", + view: JsFlameGraphView, + }, + "memory-calltree": { + id: "memory-calltree-view", + view: MemoryCallTreeView, + features: ["withAllocations"] + }, + "memory-flamegraph": { + id: "memory-flamegraph-view", + view: MemoryFlameGraphView, + features: ["withAllocations"], + prefs: ["enable-memory-flame"], + }, + }, + + /** + * Sets up the view with event binding, initializes subviews. + */ + initialize: Task.async(function* () { + this.el = $("#details-pane"); + this.toolbar = $("#performance-toolbar-controls-detail-views"); + + this._onViewToggle = this._onViewToggle.bind(this); + this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this); + this.setAvailableViews = this.setAvailableViews.bind(this); + + for (let button of $$("toolbarbutton[data-view]", this.toolbar)) { + button.addEventListener("command", this._onViewToggle); + } + + yield this.setAvailableViews(); + + PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStoppedOrSelected); + PerformanceController.on(EVENTS.RECORDING_SELECTED, + this._onRecordingStoppedOrSelected); + PerformanceController.on(EVENTS.PREF_CHANGED, this.setAvailableViews); + }), + + /** + * Unbinds events, destroys subviews. + */ + destroy: Task.async(function* () { + for (let button of $$("toolbarbutton[data-view]", this.toolbar)) { + button.removeEventListener("command", this._onViewToggle); + } + + for (let component of Object.values(this.components)) { + component.initialized && (yield component.view.destroy()); + } + + PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStoppedOrSelected); + PerformanceController.off(EVENTS.RECORDING_SELECTED, + this._onRecordingStoppedOrSelected); + PerformanceController.off(EVENTS.PREF_CHANGED, this.setAvailableViews); + }), + + /** + * Sets the possible views based off of recording features and server actor support + * by hiding/showing the buttons that select them and going to default view + * if currently selected. Called when a preference changes in + * `devtools.performance.ui.`. + */ + setAvailableViews: Task.async(function* () { + let recording = PerformanceController.getCurrentRecording(); + let isCompleted = recording && recording.isCompleted(); + let invalidCurrentView = false; + + for (let [name, { view }] of Object.entries(this.components)) { + let isSupported = this._isViewSupported(name); + + $(`toolbarbutton[data-view=${name}]`).hidden = !isSupported; + + // If the view is currently selected and not supported, go back to the + // default view. + if (!isSupported && this.isViewSelected(view)) { + invalidCurrentView = true; + } + } + + // Two scenarios in which we select the default view. + // + // 1: If we currently have selected a view that is no longer valid due + // to feature support, and this isn't the first view, and the current recording + // is completed. + // + // 2. If we have a finished recording and no panel was selected yet, + // use a default now that we have the recording configurations + if ((this._initialized && isCompleted && invalidCurrentView) || + (!this._initialized && isCompleted && recording)) { + yield this.selectDefaultView(); + } + }), + + /** + * Takes a view name and determines if the current recording + * can support the view. + * + * @param {string} viewName + * @return {boolean} + */ + _isViewSupported: function (viewName) { + let { features, prefs } = this.components[viewName]; + let recording = PerformanceController.getCurrentRecording(); + + if (!recording || !recording.isCompleted()) { + return false; + } + + let prefSupported = (prefs && prefs.length) ? + prefs.every(p => PerformanceController.getPref(p)) : + true; + return PerformanceController.isFeatureSupported(features) && prefSupported; + }, + + /** + * Select one of the DetailView's subviews to be rendered, + * hiding the others. + * + * @param String viewName + * Name of the view to be shown. + */ + selectView: Task.async(function* (viewName) { + let component = this.components[viewName]; + this.el.selectedPanel = $("#" + component.id); + + yield this._whenViewInitialized(component); + + for (let button of $$("toolbarbutton[data-view]", this.toolbar)) { + if (button.getAttribute("data-view") === viewName) { + button.setAttribute("checked", true); + } else { + button.removeAttribute("checked"); + } + } + + // Set a flag indicating that a view was explicitly set based on a + // recording's features. + this._initialized = true; + + this.emit(EVENTS.UI_DETAILS_VIEW_SELECTED, viewName); + }), + + /** + * Selects a default view based off of protocol support + * and preferences enabled. + */ + selectDefaultView: function () { + // We want the waterfall to be default view in almost all cases, except when + // timeline actor isn't supported, or we have markers disabled (which should only + // occur temporarily via bug 1156499 + if (this._isViewSupported("waterfall")) { + return this.selectView("waterfall"); + } + // The JS CallTree should always be supported since the profiler + // actor is as old as the world. + return this.selectView("js-calltree"); + }, + + /** + * Checks if the provided view is currently selected. + * + * @param object viewObject + * @return boolean + */ + isViewSelected: function (viewObject) { + // If not initialized, and we have no recordings, + // no views are selected (even though there's a selected panel) + if (!this._initialized) { + return false; + } + + let selectedPanel = this.el.selectedPanel; + let selectedId = selectedPanel.id; + + for (let { id, view } of Object.values(this.components)) { + if (id == selectedId && view == viewObject) { + return true; + } + } + + return false; + }, + + /** + * Initializes a subview if it wasn't already set up, and makes sure + * it's populated with recording data if there is some available. + * + * @param object component + * A component descriptor from DetailsView.components + */ + _whenViewInitialized: Task.async(function* (component) { + if (component.initialized) { + return; + } + component.initialized = true; + yield component.view.initialize(); + + // If this view is initialized *after* a recording is shown, it won't display + // any data. Make sure it's populated by setting `shouldUpdateWhenShown`. + // All detail views require a recording to be complete, so do not + // attempt to render if recording is in progress or does not exist. + let recording = PerformanceController.getCurrentRecording(); + if (recording && recording.isCompleted()) { + component.view.shouldUpdateWhenShown = true; + } + }), + + /** + * Called when recording stops or is selected. + */ + _onRecordingStoppedOrSelected: function (_, state, recording) { + if (typeof state === "string" && state !== "recording-stopped") { + return; + } + this.setAvailableViews(); + }, + + /** + * Called when a view button is clicked. + */ + _onViewToggle: function (e) { + this.selectView(e.target.getAttribute("data-view")); + }, + + toString: () => "[object DetailsView]" +}; + +/** + * Convenient way of emitting events from the view. + */ +EventEmitter.decorate(DetailsView); |