summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/views/details.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/views/details.js')
-rw-r--r--devtools/client/performance/views/details.js263
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);