Mypal/devtools/client/performance/views/details.js
2019-03-11 13:26:37 +03:00

264 lines
8.3 KiB
JavaScript

/* 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);