summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/views
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/views')
-rw-r--r--devtools/client/performance/views/details-abstract-subview.js194
-rw-r--r--devtools/client/performance/views/details-js-call-tree.js193
-rw-r--r--devtools/client/performance/views/details-js-flamegraph.js125
-rw-r--r--devtools/client/performance/views/details-memory-call-tree.js130
-rw-r--r--devtools/client/performance/views/details-memory-flamegraph.js121
-rw-r--r--devtools/client/performance/views/details-waterfall.js252
-rw-r--r--devtools/client/performance/views/details.js263
-rw-r--r--devtools/client/performance/views/overview.js423
-rw-r--r--devtools/client/performance/views/recordings.js202
-rw-r--r--devtools/client/performance/views/toolbar.js160
10 files changed, 2063 insertions, 0 deletions
diff --git a/devtools/client/performance/views/details-abstract-subview.js b/devtools/client/performance/views/details-abstract-subview.js
new file mode 100644
index 000000000..86ea45366
--- /dev/null
+++ b/devtools/client/performance/views/details-abstract-subview.js
@@ -0,0 +1,194 @@
+/* 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 */
+/* exported DetailsSubview */
+"use strict";
+
+/**
+ * A base class from which all detail views inherit.
+ */
+var DetailsSubview = {
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this);
+ this._onOverviewRangeChange = this._onOverviewRangeChange.bind(this);
+ this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.on(EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ OverviewView.on(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
+ DetailsView.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
+
+ let self = this;
+ let originalRenderFn = this.render;
+ let afterRenderFn = () => {
+ this._wasRendered = true;
+ };
+
+ this.render = Task.async(function* (...args) {
+ let maybeRetval = yield originalRenderFn.apply(self, args);
+ afterRenderFn();
+ return maybeRetval;
+ });
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function () {
+ clearNamedTimeout("range-change-debounce");
+
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.off(EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected);
+ PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ OverviewView.off(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
+ DetailsView.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
+ },
+
+ /**
+ * Returns true if this view was rendered at least once.
+ */
+ get wasRenderedAtLeastOnce() {
+ return !!this._wasRendered;
+ },
+
+ /**
+ * Amount of time (in milliseconds) to wait until this view gets updated,
+ * when the range is changed in the overview.
+ */
+ rangeChangeDebounceTime: 0,
+
+ /**
+ * When the overview range changes, all details views will require a
+ * rerendering at a later point, determined by `shouldUpdateWhenShown` and
+ * `canUpdateWhileHidden` and whether or not its the current view.
+ * Set `requiresUpdateOnRangeChange` to false to not invalidate the view
+ * when the range changes.
+ */
+ requiresUpdateOnRangeChange: true,
+
+ /**
+ * Flag specifying if this view should be updated when selected. This will
+ * be set to true, for example, when the range changes in the overview and
+ * this view is not currently visible.
+ */
+ shouldUpdateWhenShown: false,
+
+ /**
+ * Flag specifying if this view may get updated even when it's not selected.
+ * Should only be used in tests.
+ */
+ canUpdateWhileHidden: false,
+
+ /**
+ * An array of preferences under `devtools.performance.ui.` that the view should
+ * rerender and callback `this._onRerenderPrefChanged` upon change.
+ */
+ rerenderPrefs: [],
+
+ /**
+ * An array of preferences under `devtools.performance.` that the view should
+ * observe and callback `this._onObservedPrefChange` upon change.
+ */
+ observedPrefs: [],
+
+ /**
+ * Flag specifying if this view should update while the overview selection
+ * area is actively being dragged by the mouse.
+ */
+ shouldUpdateWhileMouseIsActive: false,
+
+ /**
+ * Called when recording stops or is selected.
+ */
+ _onRecordingStoppedOrSelected: function (_, state, recording) {
+ if (typeof state !== "string") {
+ recording = state;
+ }
+ if (arguments.length === 3 && state !== "recording-stopped") {
+ return;
+ }
+
+ if (!recording || !recording.isCompleted()) {
+ return;
+ }
+ if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
+ this.render(OverviewView.getTimeInterval());
+ } else {
+ this.shouldUpdateWhenShown = true;
+ }
+ },
+
+ /**
+ * Fired when a range is selected or cleared in the OverviewView.
+ */
+ _onOverviewRangeChange: function (_, interval) {
+ if (!this.requiresUpdateOnRangeChange) {
+ return;
+ }
+ if (DetailsView.isViewSelected(this)) {
+ let debounced = () => {
+ if (!this.shouldUpdateWhileMouseIsActive && OverviewView.isMouseActive) {
+ // Don't render yet, while the selection is still being dragged.
+ setNamedTimeout("range-change-debounce", this.rangeChangeDebounceTime,
+ debounced);
+ } else {
+ this.render(interval);
+ }
+ };
+ setNamedTimeout("range-change-debounce", this.rangeChangeDebounceTime, debounced);
+ } else {
+ this.shouldUpdateWhenShown = true;
+ }
+ },
+
+ /**
+ * Fired when a view is selected in the DetailsView.
+ */
+ _onDetailsViewSelected: function () {
+ if (DetailsView.isViewSelected(this) && this.shouldUpdateWhenShown) {
+ this.render(OverviewView.getTimeInterval());
+ this.shouldUpdateWhenShown = false;
+ }
+ },
+
+ /**
+ * Fired when a preference in `devtools.performance.ui.` is changed.
+ */
+ _onPrefChanged: function (_, prefName) {
+ if (~this.observedPrefs.indexOf(prefName) && this._onObservedPrefChange) {
+ this._onObservedPrefChange(_, prefName);
+ }
+
+ // 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()) {
+ return;
+ }
+
+ if (!~this.rerenderPrefs.indexOf(prefName)) {
+ return;
+ }
+
+ if (this._onRerenderPrefChanged) {
+ this._onRerenderPrefChanged(_, prefName);
+ }
+
+ if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
+ this.render(OverviewView.getTimeInterval());
+ } else {
+ this.shouldUpdateWhenShown = true;
+ }
+ }
+};
diff --git a/devtools/client/performance/views/details-js-call-tree.js b/devtools/client/performance/views/details-js-call-tree.js
new file mode 100644
index 000000000..6c4e808af
--- /dev/null
+++ b/devtools/client/performance/views/details-js-call-tree.js
@@ -0,0 +1,193 @@
+/* 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 DetailsSubview */
+"use strict";
+
+/**
+ * CallTree view containing profiler call tree, controlled by DetailsView.
+ */
+var JsCallTreeView = Heritage.extend(DetailsSubview, {
+
+ rerenderPrefs: [
+ "invert-call-tree",
+ "show-platform-data",
+ "flatten-tree-recursion",
+ "show-jit-optimizations",
+ ],
+
+ // Units are in milliseconds.
+ rangeChangeDebounceTime: 75,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ DetailsSubview.initialize.call(this);
+
+ this._onLink = this._onLink.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+
+ this.container = $("#js-calltree-view .call-tree-cells-container");
+
+ this.optimizationsElement = $("#jit-optimizations-view");
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function () {
+ ReactDOM.unmountComponentAtNode(this.optimizationsElement);
+ this.optimizationsElement = null;
+ this.container = null;
+ this.threadNode = null;
+ DetailsSubview.destroy.call(this);
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new call tree.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ let profile = recording.getProfile();
+ let showOptimizations = PerformanceController.getOption("show-jit-optimizations");
+
+ let options = {
+ contentOnly: !PerformanceController.getOption("show-platform-data"),
+ invertTree: PerformanceController.getOption("invert-call-tree"),
+ flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
+ showOptimizationHint: showOptimizations
+ };
+ let threadNode = this.threadNode = this._prepareCallTree(profile, interval, options);
+ this._populateCallTree(threadNode, options);
+
+ // For better or worse, re-rendering loses frame selection,
+ // so we should always hide opts on rerender
+ this.hideOptimizations();
+
+ this.emit(EVENTS.UI_JS_CALL_TREE_RENDERED);
+ },
+
+ showOptimizations: function () {
+ this.optimizationsElement.classList.remove("hidden");
+ },
+
+ hideOptimizations: function () {
+ this.optimizationsElement.classList.add("hidden");
+ },
+
+ _onFocus: function (_, treeItem) {
+ let showOptimizations = PerformanceController.getOption("show-jit-optimizations");
+ let frameNode = treeItem.frame;
+ let optimizationSites = frameNode && frameNode.hasOptimizations()
+ ? frameNode.getOptimizations().optimizationSites
+ : [];
+
+ if (!showOptimizations || !frameNode || optimizationSites.length === 0) {
+ this.hideOptimizations();
+ this.emit("focus", treeItem);
+ return;
+ }
+
+ this.showOptimizations();
+
+ let frameData = frameNode.getInfo();
+ let optimizations = JITOptimizationsView({
+ frameData,
+ optimizationSites,
+ onViewSourceInDebugger: (url, line) => {
+ gToolbox.viewSourceInDebugger(url, line).then(success => {
+ if (success) {
+ this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ });
+ }
+ });
+
+ ReactDOM.render(optimizations, this.optimizationsElement);
+
+ this.emit("focus", treeItem);
+ },
+
+ /**
+ * Fired on the "link" event for the call tree in this container.
+ */
+ _onLink: function (_, treeItem) {
+ let { url, line } = treeItem.frame.getInfo();
+ gToolbox.viewSourceInDebugger(url, line).then(success => {
+ if (success) {
+ this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ });
+ },
+
+ /**
+ * Called when the recording is stopped and prepares data to
+ * populate the call tree.
+ */
+ _prepareCallTree: function (profile, { startTime, endTime }, options) {
+ let thread = profile.threads[0];
+ let { contentOnly, invertTree, flattenRecursion } = options;
+ let threadNode = new ThreadNode(thread, { startTime, endTime, contentOnly, invertTree,
+ flattenRecursion });
+
+ // Real profiles from nsProfiler (i.e. not synthesized from allocation
+ // logs) always have a (root) node. Go down one level in the uninverted
+ // view to avoid displaying both the synthesized root node and the (root)
+ // node from the profiler.
+ if (!invertTree) {
+ threadNode.calls = threadNode.calls[0].calls;
+ }
+
+ return threadNode;
+ },
+
+ /**
+ * Renders the call tree.
+ */
+ _populateCallTree: function (frameNode, options = {}) {
+ // If we have an empty profile (no samples), then don't invert the tree, as
+ // it would hide the root node and a completely blank call tree space can be
+ // mis-interpreted as an error.
+ let inverted = options.invertTree && frameNode.samples > 0;
+
+ let root = new CallView({
+ frame: frameNode,
+ inverted: inverted,
+ // The synthesized root node is hidden in inverted call trees.
+ hidden: inverted,
+ // Call trees should only auto-expand when not inverted. Passing undefined
+ // will default to the CALL_TREE_AUTO_EXPAND depth.
+ autoExpandDepth: inverted ? 0 : undefined,
+ showOptimizationHint: options.showOptimizationHint
+ });
+
+ // Bind events.
+ root.on("link", this._onLink);
+ root.on("focus", this._onFocus);
+
+ // Clear out other call trees.
+ this.container.innerHTML = "";
+ root.attachTo(this.container);
+
+ // When platform data isn't shown, hide the cateogry labels, since they're
+ // only available for C++ frames. Pass *false* to make them invisible.
+ root.toggleCategories(!options.contentOnly);
+
+ // Return the CallView for tests
+ return root;
+ },
+
+ toString: () => "[object JsCallTreeView]"
+});
+
+EventEmitter.decorate(JsCallTreeView);
diff --git a/devtools/client/performance/views/details-js-flamegraph.js b/devtools/client/performance/views/details-js-flamegraph.js
new file mode 100644
index 000000000..0aca21252
--- /dev/null
+++ b/devtools/client/performance/views/details-js-flamegraph.js
@@ -0,0 +1,125 @@
+/* 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 DetailsSubview */
+"use strict";
+
+/**
+ * FlameGraph view containing a pyramid-like visualization of a profile,
+ * controlled by DetailsView.
+ */
+var JsFlameGraphView = Heritage.extend(DetailsSubview, {
+
+ shouldUpdateWhileMouseIsActive: true,
+
+ rerenderPrefs: [
+ "invert-flame-graph",
+ "flatten-tree-recursion",
+ "show-platform-data",
+ "show-idle-blocks"
+ ],
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: Task.async(function* () {
+ DetailsSubview.initialize.call(this);
+
+ this.graph = new FlameGraph($("#js-flamegraph-view"));
+ this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+ this.graph.setTheme(PerformanceController.getTheme());
+ yield this.graph.ready();
+
+ this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.on("selecting", this._onRangeChangeInGraph);
+ }),
+
+ /**
+ * Unbinds events.
+ */
+ destroy: Task.async(function* () {
+ DetailsSubview.destroy.call(this);
+
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.off("selecting", this._onRangeChangeInGraph);
+
+ yield this.graph.destroy();
+ }),
+
+ /**
+ * Method for handling all the set up for rendering a new flamegraph.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ let duration = recording.getDuration();
+ let profile = recording.getProfile();
+ let thread = profile.threads[0];
+
+ let data = FlameGraphUtils.createFlameGraphDataFromThread(thread, {
+ invertTree: PerformanceController.getOption("invert-flame-graph"),
+ flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
+ contentOnly: !PerformanceController.getOption("show-platform-data"),
+ showIdleBlocks: PerformanceController.getOption("show-idle-blocks")
+ && L10N.getStr("table.idle")
+ });
+
+ this.graph.setData({ data,
+ bounds: {
+ startTime: 0,
+ endTime: duration
+ },
+ visible: {
+ startTime: interval.startTime || 0,
+ endTime: interval.endTime || duration
+ }
+ });
+
+ this.graph.focus();
+
+ this.emit(EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ },
+
+ /**
+ * Fired when a range is selected or cleared in the FlameGraph.
+ */
+ _onRangeChangeInGraph: function () {
+ let interval = this.graph.getViewRange();
+
+ // Squelch rerendering this view when we update the range here
+ // to avoid recursion, as our FlameGraph handles rerendering itself
+ // when originating from within the graph.
+ this.requiresUpdateOnRangeChange = false;
+ OverviewView.setTimeInterval(interval);
+ this.requiresUpdateOnRangeChange = true;
+ },
+
+ /**
+ * Called whenever a pref is changed and this view needs to be rerendered.
+ */
+ _onRerenderPrefChanged: function () {
+ let recording = PerformanceController.getCurrentRecording();
+ let profile = recording.getProfile();
+ let thread = profile.threads[0];
+ FlameGraphUtils.removeFromCache(thread);
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function (_, theme) {
+ this.graph.setTheme(theme);
+ this.graph.refresh({ force: true });
+ },
+
+ toString: () => "[object JsFlameGraphView]"
+});
+
+EventEmitter.decorate(JsFlameGraphView);
diff --git a/devtools/client/performance/views/details-memory-call-tree.js b/devtools/client/performance/views/details-memory-call-tree.js
new file mode 100644
index 000000000..883d92e63
--- /dev/null
+++ b/devtools/client/performance/views/details-memory-call-tree.js
@@ -0,0 +1,130 @@
+/* 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 DetailsSubview */
+"use strict";
+
+/**
+ * CallTree view containing memory allocation sites, controlled by DetailsView.
+ */
+var MemoryCallTreeView = Heritage.extend(DetailsSubview, {
+
+ rerenderPrefs: [
+ "invert-call-tree"
+ ],
+
+ // Units are in milliseconds.
+ rangeChangeDebounceTime: 100,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ DetailsSubview.initialize.call(this);
+
+ this._onLink = this._onLink.bind(this);
+
+ this.container = $("#memory-calltree-view > .call-tree-cells-container");
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function () {
+ DetailsSubview.destroy.call(this);
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new call tree.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let options = {
+ invertTree: PerformanceController.getOption("invert-call-tree")
+ };
+ let recording = PerformanceController.getCurrentRecording();
+ let allocations = recording.getAllocations();
+ let threadNode = this._prepareCallTree(allocations, interval, options);
+ this._populateCallTree(threadNode, options);
+ this.emit(EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ },
+
+ /**
+ * Fired on the "link" event for the call tree in this container.
+ */
+ _onLink: function (_, treeItem) {
+ let { url, line } = treeItem.frame.getInfo();
+ gToolbox.viewSourceInDebugger(url, line).then(success => {
+ if (success) {
+ this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ });
+ },
+
+ /**
+ * Called when the recording is stopped and prepares data to
+ * populate the call tree.
+ */
+ _prepareCallTree: function (allocations, { startTime, endTime }, options) {
+ let thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+ let { invertTree } = options;
+
+ return new ThreadNode(thread, { startTime, endTime, invertTree });
+ },
+
+ /**
+ * Renders the call tree.
+ */
+ _populateCallTree: function (frameNode, options = {}) {
+ // If we have an empty profile (no samples), then don't invert the tree, as
+ // it would hide the root node and a completely blank call tree space can be
+ // mis-interpreted as an error.
+ let inverted = options.invertTree && frameNode.samples > 0;
+
+ let root = new CallView({
+ frame: frameNode,
+ inverted: inverted,
+ // Root nodes are hidden in inverted call trees.
+ hidden: inverted,
+ // Call trees should only auto-expand when not inverted. Passing undefined
+ // will default to the CALL_TREE_AUTO_EXPAND depth.
+ autoExpandDepth: inverted ? 0 : undefined,
+ // Some cells like the time duration and cost percentage don't make sense
+ // for a memory allocations call tree.
+ visibleCells: {
+ selfCount: true,
+ count: true,
+ selfSize: true,
+ size: true,
+ selfCountPercentage: true,
+ countPercentage: true,
+ selfSizePercentage: true,
+ sizePercentage: true,
+ function: true
+ }
+ });
+
+ // Bind events.
+ root.on("link", this._onLink);
+
+ // Pipe "focus" events to the view, mostly for tests
+ root.on("focus", () => this.emit("focus"));
+
+ // Clear out other call trees.
+ this.container.innerHTML = "";
+ root.attachTo(this.container);
+
+ // Memory allocation samples don't contain cateogry labels.
+ root.toggleCategories(false);
+ },
+
+ toString: () => "[object MemoryCallTreeView]"
+});
+
+EventEmitter.decorate(MemoryCallTreeView);
diff --git a/devtools/client/performance/views/details-memory-flamegraph.js b/devtools/client/performance/views/details-memory-flamegraph.js
new file mode 100644
index 000000000..70eaa3c7a
--- /dev/null
+++ b/devtools/client/performance/views/details-memory-flamegraph.js
@@ -0,0 +1,121 @@
+/* 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 DetailsSubview */
+"use strict";
+
+/**
+ * FlameGraph view containing a pyramid-like visualization of memory allocation
+ * sites, controlled by DetailsView.
+ */
+var MemoryFlameGraphView = Heritage.extend(DetailsSubview, {
+
+ shouldUpdateWhileMouseIsActive: true,
+
+ rerenderPrefs: [
+ "invert-flame-graph",
+ "flatten-tree-recursion",
+ "show-idle-blocks"
+ ],
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: Task.async(function* () {
+ DetailsSubview.initialize.call(this);
+
+ this.graph = new FlameGraph($("#memory-flamegraph-view"));
+ this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+ this.graph.setTheme(PerformanceController.getTheme());
+ yield this.graph.ready();
+
+ this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.on("selecting", this._onRangeChangeInGraph);
+ }),
+
+ /**
+ * Unbinds events.
+ */
+ destroy: Task.async(function* () {
+ DetailsSubview.destroy.call(this);
+
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.off("selecting", this._onRangeChangeInGraph);
+
+ yield this.graph.destroy();
+ }),
+
+ /**
+ * Method for handling all the set up for rendering a new flamegraph.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ let duration = recording.getDuration();
+ let allocations = recording.getAllocations();
+
+ let thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+ let data = FlameGraphUtils.createFlameGraphDataFromThread(thread, {
+ invertStack: PerformanceController.getOption("invert-flame-graph"),
+ flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
+ showIdleBlocks: PerformanceController.getOption("show-idle-blocks")
+ && L10N.getStr("table.idle")
+ });
+
+ this.graph.setData({ data,
+ bounds: {
+ startTime: 0,
+ endTime: duration
+ },
+ visible: {
+ startTime: interval.startTime || 0,
+ endTime: interval.endTime || duration
+ }
+ });
+
+ this.emit(EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ },
+
+ /**
+ * Fired when a range is selected or cleared in the FlameGraph.
+ */
+ _onRangeChangeInGraph: function () {
+ let interval = this.graph.getViewRange();
+
+ // Squelch rerendering this view when we update the range here
+ // to avoid recursion, as our FlameGraph handles rerendering itself
+ // when originating from within the graph.
+ this.requiresUpdateOnRangeChange = false;
+ OverviewView.setTimeInterval(interval);
+ this.requiresUpdateOnRangeChange = true;
+ },
+
+ /**
+ * Called whenever a pref is changed and this view needs to be rerendered.
+ */
+ _onRerenderPrefChanged: function () {
+ let recording = PerformanceController.getCurrentRecording();
+ let allocations = recording.getAllocations();
+ let thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+ FlameGraphUtils.removeFromCache(thread);
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function (_, theme) {
+ this.graph.setTheme(theme);
+ this.graph.refresh({ force: true });
+ },
+
+ toString: () => "[object MemoryFlameGraphView]"
+});
+
+EventEmitter.decorate(MemoryFlameGraphView);
diff --git a/devtools/client/performance/views/details-waterfall.js b/devtools/client/performance/views/details-waterfall.js
new file mode 100644
index 000000000..db8def053
--- /dev/null
+++ b/devtools/client/performance/views/details-waterfall.js
@@ -0,0 +1,252 @@
+/* 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 window, DetailsSubview */
+"use strict";
+
+const MARKER_DETAILS_WIDTH = 200;
+// Units are in milliseconds.
+const WATERFALL_RESIZE_EVENTS_DRAIN = 100;
+
+const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
+
+/**
+ * Waterfall view containing the timeline markers, controlled by DetailsView.
+ */
+var WaterfallView = Heritage.extend(DetailsSubview, {
+
+ // Smallest unit of time between two markers. Larger by 10x^3 than Number.EPSILON.
+ MARKER_EPSILON: 0.000000000001,
+ // px
+ WATERFALL_MARKER_SIDEBAR_WIDTH: 175,
+ // px
+ WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS: 20,
+
+ observedPrefs: [
+ "hidden-markers"
+ ],
+
+ rerenderPrefs: [
+ "hidden-markers"
+ ],
+
+ // Units are in milliseconds.
+ rangeChangeDebounceTime: 75,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ DetailsSubview.initialize.call(this);
+
+ this._cache = new WeakMap();
+
+ this._onMarkerSelected = this._onMarkerSelected.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._onViewSource = this._onViewSource.bind(this);
+ this._onShowAllocations = this._onShowAllocations.bind(this);
+ this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+ this.treeContainer = $("#waterfall-tree");
+ this.detailsContainer = $("#waterfall-details");
+ this.detailsSplitter = $("#waterfall-view > splitter");
+
+ this.details = new MarkerDetails($("#waterfall-details"),
+ $("#waterfall-view > splitter"));
+ this.details.hidden = true;
+
+ this.details.on("resize", this._onResize);
+ this.details.on("view-source", this._onViewSource);
+ this.details.on("show-allocations", this._onShowAllocations);
+ window.addEventListener("resize", this._onResize);
+
+ // TODO bug 1167093 save the previously set width, and ensure minimum width
+ this.details.width = MARKER_DETAILS_WIDTH;
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function () {
+ DetailsSubview.destroy.call(this);
+
+ clearNamedTimeout("waterfall-resize");
+
+ this._cache = null;
+
+ this.details.off("resize", this._onResize);
+ this.details.off("view-source", this._onViewSource);
+ this.details.off("show-allocations", this._onShowAllocations);
+ window.removeEventListener("resize", this._onResize);
+
+ ReactDOM.unmountComponentAtNode(this.treeContainer);
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new waterfall.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function (interval = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ if (recording.isRecording()) {
+ return;
+ }
+ let startTime = interval.startTime || 0;
+ let endTime = interval.endTime || recording.getDuration();
+ let markers = recording.getMarkers();
+ let rootMarkerNode = this._prepareWaterfallTree(markers);
+
+ this._populateWaterfallTree(rootMarkerNode, { startTime, endTime });
+ this.emit(EVENTS.UI_WATERFALL_RENDERED);
+ },
+
+ /**
+ * Called when a marker is selected in the waterfall view,
+ * updating the markers detail view.
+ */
+ _onMarkerSelected: function (event, marker) {
+ let recording = PerformanceController.getCurrentRecording();
+ let frames = recording.getFrames();
+ let allocations = recording.getConfiguration().withAllocations;
+
+ if (event === "selected") {
+ this.details.render({ marker, frames, allocations });
+ this.details.hidden = false;
+ }
+ if (event === "unselected") {
+ this.details.empty();
+ }
+ },
+
+ /**
+ * Called when the marker details view is resized.
+ */
+ _onResize: function () {
+ setNamedTimeout("waterfall-resize", WATERFALL_RESIZE_EVENTS_DRAIN, () => {
+ this.render(OverviewView.getTimeInterval());
+ });
+ },
+
+ /**
+ * Called whenever an observed pref is changed.
+ */
+ _onObservedPrefChange: function (_, prefName) {
+ this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+ // Clear the cache as we'll need to recompute the collapsed
+ // marker model
+ this._cache = new WeakMap();
+ },
+
+ /**
+ * Called when MarkerDetails view emits an event to view source.
+ */
+ _onViewSource: function (_, data) {
+ gToolbox.viewSourceInDebugger(data.url, data.line);
+ },
+
+ /**
+ * Called when MarkerDetails view emits an event to snap to allocations.
+ */
+ _onShowAllocations: function (_, data) {
+ let { endTime } = data;
+ let startTime = 0;
+ let recording = PerformanceController.getCurrentRecording();
+ let markers = recording.getMarkers();
+
+ let lastGCMarkerFromPreviousCycle = null;
+ let lastGCMarker = null;
+ // Iterate over markers looking for the most recent GC marker
+ // from the cycle before the marker's whose allocations we're interested in.
+ for (let marker of markers) {
+ // We found the marker whose allocations we're tracking; abort
+ if (marker.start === endTime) {
+ break;
+ }
+
+ if (marker.name === "GarbageCollection") {
+ if (lastGCMarker && lastGCMarker.cycle !== marker.cycle) {
+ lastGCMarkerFromPreviousCycle = lastGCMarker;
+ }
+ lastGCMarker = marker;
+ }
+ }
+
+ if (lastGCMarkerFromPreviousCycle) {
+ startTime = lastGCMarkerFromPreviousCycle.end;
+ }
+
+ // Adjust times so we don't include the range of these markers themselves.
+ endTime -= this.MARKER_EPSILON;
+ startTime += startTime !== 0 ? this.MARKER_EPSILON : 0;
+
+ OverviewView.setTimeInterval({ startTime, endTime });
+ DetailsView.selectView("memory-calltree");
+ },
+
+ /**
+ * Called when the recording is stopped and prepares data to
+ * populate the waterfall tree.
+ */
+ _prepareWaterfallTree: function (markers) {
+ let cached = this._cache.get(markers);
+ if (cached) {
+ return cached;
+ }
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: markers,
+ filter: this._hiddenMarkers
+ });
+
+ this._cache.set(markers, rootMarkerNode);
+ return rootMarkerNode;
+ },
+
+ /**
+ * Calculates the available width for the waterfall.
+ * This should be invoked every time the container node is resized.
+ */
+ _recalculateBounds: function () {
+ this.waterfallWidth = this.treeContainer.clientWidth
+ - this.WATERFALL_MARKER_SIDEBAR_WIDTH
+ - this.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS;
+ },
+
+ /**
+ * Renders the waterfall tree.
+ */
+ _populateWaterfallTree: function (rootMarkerNode, interval) {
+ this._recalculateBounds();
+
+ let doc = this.treeContainer.ownerDocument;
+ let startTime = interval.startTime | 0;
+ let endTime = interval.endTime | 0;
+ let dataScale = this.waterfallWidth / (endTime - startTime);
+
+ this.canvas = TickUtils.drawWaterfallBackground(doc, dataScale, this.waterfallWidth);
+
+ let treeView = Waterfall({
+ marker: rootMarkerNode,
+ startTime,
+ endTime,
+ dataScale,
+ sidebarWidth: this.WATERFALL_MARKER_SIDEBAR_WIDTH,
+ waterfallWidth: this.waterfallWidth,
+ onFocus: node => this._onMarkerSelected("selected", node)
+ });
+
+ ReactDOM.render(treeView, this.treeContainer);
+ },
+
+ toString: () => "[object WaterfallView]"
+});
+
+EventEmitter.decorate(WaterfallView);
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);
diff --git a/devtools/client/performance/views/overview.js b/devtools/client/performance/views/overview.js
new file mode 100644
index 000000000..f45a6d844
--- /dev/null
+++ b/devtools/client/performance/views/overview.js
@@ -0,0 +1,423 @@
+/* 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 */
+"use strict";
+
+// No sense updating the overview more often than receiving data from the
+// backend. Make sure this isn't lower than DEFAULT_TIMELINE_DATA_PULL_TIMEOUT
+// in devtools/server/actors/timeline.js
+
+// The following units are in milliseconds.
+const OVERVIEW_UPDATE_INTERVAL = 200;
+const FRAMERATE_GRAPH_LOW_RES_INTERVAL = 100;
+const FRAMERATE_GRAPH_HIGH_RES_INTERVAL = 16;
+const GRAPH_REQUIREMENTS = {
+ timeline: {
+ features: ["withMarkers"]
+ },
+ framerate: {
+ features: ["withTicks"]
+ },
+ memory: {
+ features: ["withMemory"]
+ },
+};
+
+/**
+ * View handler for the overview panel's time view, displaying
+ * framerate, timeline and memory over time.
+ */
+var OverviewView = {
+
+ /**
+ * How frequently we attempt to render the graphs. Overridden
+ * in tests.
+ */
+ OVERVIEW_UPDATE_INTERVAL: OVERVIEW_UPDATE_INTERVAL,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function () {
+ this.graphs = new GraphsController({
+ root: $("#overview-pane"),
+ getFilter: () => PerformanceController.getPref("hidden-markers"),
+ getTheme: () => PerformanceController.getTheme(),
+ });
+
+ // If no timeline support, shut it all down.
+ if (!PerformanceController.getTraits().features.withMarkers) {
+ this.disable();
+ return;
+ }
+
+ // Store info on multiprocess support.
+ this._multiprocessData = PerformanceController.getMultiprocessStatus();
+
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onRecordingSelected = this._onRecordingSelected.bind(this);
+ this._onRecordingTick = this._onRecordingTick.bind(this);
+ this._onGraphSelecting = this._onGraphSelecting.bind(this);
+ this._onGraphRendered = this._onGraphRendered.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ // Toggle the initial visibility of memory and framerate graph containers
+ // based off of prefs.
+ PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
+ PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+ this.graphs.on("selecting", this._onGraphSelecting);
+ this.graphs.on("rendered", this._onGraphRendered);
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: Task.async(function* () {
+ PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange);
+ PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+ this.graphs.off("selecting", this._onGraphSelecting);
+ this.graphs.off("rendered", this._onGraphRendered);
+ yield this.graphs.destroy();
+ }),
+
+ /**
+ * Returns true if any of the overview graphs have mouse dragging active,
+ * false otherwise.
+ */
+ get isMouseActive() {
+ // Fetch all graphs currently stored in the GraphsController.
+ // These graphs are not necessarily active, but will not have
+ // an active mouse, in that case.
+ return !!this.graphs.getWidgets().some(e => e.isMouseActive);
+ },
+
+ /**
+ * Disabled in the event we're using a Timeline mock, so we'll have no
+ * timeline, ticks or memory data to show, so just block rendering and hide
+ * the panel.
+ */
+ disable: function () {
+ this._disabled = true;
+ this.graphs.disableAll();
+ },
+
+ /**
+ * Returns the disabled status.
+ *
+ * @return boolean
+ */
+ isDisabled: function () {
+ return this._disabled;
+ },
+
+ /**
+ * Sets the time interval selection for all graphs in this overview.
+ *
+ * @param object interval
+ * The { startTime, endTime }, in milliseconds.
+ */
+ setTimeInterval: function (interval, options = {}) {
+ let recording = PerformanceController.getCurrentRecording();
+ if (recording == null) {
+ throw new Error("A recording should be available in order to set the selection.");
+ }
+ if (this.isDisabled()) {
+ return;
+ }
+ let mapStart = () => 0;
+ let mapEnd = () => recording.getDuration();
+ let selection = { start: interval.startTime, end: interval.endTime };
+ this._stopSelectionChangeEventPropagation = options.stopPropagation;
+ this.graphs.setMappedSelection(selection, { mapStart, mapEnd });
+ this._stopSelectionChangeEventPropagation = false;
+ },
+
+ /**
+ * Gets the time interval selection for all graphs in this overview.
+ *
+ * @return object
+ * The { startTime, endTime }, in milliseconds.
+ */
+ getTimeInterval: function () {
+ let recording = PerformanceController.getCurrentRecording();
+ if (recording == null) {
+ throw new Error("A recording should be available in order to get the selection.");
+ }
+ if (this.isDisabled()) {
+ return { startTime: 0, endTime: recording.getDuration() };
+ }
+ let mapStart = () => 0;
+ let mapEnd = () => recording.getDuration();
+ let selection = this.graphs.getMappedSelection({ mapStart, mapEnd });
+ // If no selection returned, this means the overview graphs have not been rendered
+ // yet, so act as if we have no selection (the full recording). Also
+ // if the selection range distance is tiny, assume the range was cleared or just
+ // clicked, and we do not have a range.
+ if (!selection || (selection.max - selection.min) < 1) {
+ return { startTime: 0, endTime: recording.getDuration() };
+ }
+ return { startTime: selection.min, endTime: selection.max };
+ },
+
+ /**
+ * Method for handling all the set up for rendering the overview graphs.
+ *
+ * @param number resolution
+ * The fps graph resolution. @see Graphs.js
+ */
+ render: Task.async(function* (resolution) {
+ if (this.isDisabled()) {
+ return;
+ }
+
+ let recording = PerformanceController.getCurrentRecording();
+ yield this.graphs.render(recording.getAllData(), resolution);
+
+ // Finished rendering all graphs in this overview.
+ this.emit(EVENTS.UI_OVERVIEW_RENDERED, resolution);
+ }),
+
+ /**
+ * Called at most every OVERVIEW_UPDATE_INTERVAL milliseconds
+ * and uses data fetched from the controller to render
+ * data into all the corresponding overview graphs.
+ */
+ _onRecordingTick: Task.async(function* () {
+ yield this.render(FRAMERATE_GRAPH_LOW_RES_INTERVAL);
+ this._prepareNextTick();
+ }),
+
+ /**
+ * Called to refresh the timer to keep firing _onRecordingTick.
+ */
+ _prepareNextTick: function () {
+ // Check here to see if there's still a _timeoutId, incase
+ // `stop` was called before the _prepareNextTick call was executed.
+ if (this.isRendering()) {
+ this._timeoutId = setTimeout(this._onRecordingTick, this.OVERVIEW_UPDATE_INTERVAL);
+ }
+ },
+
+ /**
+ * Called when recording state changes.
+ */
+ _onRecordingStateChange: OverviewViewOnStateChange(Task.async(
+ function* (_, state, recording) {
+ if (state !== "recording-stopped") {
+ return;
+ }
+ // Check to see if the recording that just stopped is the current recording.
+ // If it is, render the high-res graphs. For manual recordings, it will also
+ // be the current recording, but profiles generated by `console.profile` can stop
+ // while having another profile selected -- in this case, OverviewView should keep
+ // rendering the current recording.
+ if (recording !== PerformanceController.getCurrentRecording()) {
+ return;
+ }
+ this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+ yield this._checkSelection(recording);
+ })),
+
+ /**
+ * Called when a new recording is selected.
+ */
+ _onRecordingSelected: OverviewViewOnStateChange(Task.async(function* (_, recording) {
+ this._setGraphVisibilityFromRecordingFeatures(recording);
+
+ // If this recording is complete, render the high res graph
+ if (recording.isCompleted()) {
+ yield this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+ }
+ yield this._checkSelection(recording);
+ this.graphs.dropSelection();
+ })),
+
+ /**
+ * Start the polling for rendering the overview graph.
+ */
+ _startPolling: function () {
+ this._timeoutId = setTimeout(this._onRecordingTick, this.OVERVIEW_UPDATE_INTERVAL);
+ },
+
+ /**
+ * Stop the polling for rendering the overview graph.
+ */
+ _stopPolling: function () {
+ clearTimeout(this._timeoutId);
+ this._timeoutId = null;
+ },
+
+ /**
+ * Whether or not the overview view is in a state of polling rendering.
+ */
+ isRendering: function () {
+ return !!this._timeoutId;
+ },
+
+ /**
+ * Makes sure the selection is enabled or disabled in all the graphs,
+ * based on whether a recording currently exists and is not in progress.
+ */
+ _checkSelection: Task.async(function* (recording) {
+ let isEnabled = recording ? recording.isCompleted() : false;
+ yield this.graphs.selectionEnabled(isEnabled);
+ }),
+
+ /**
+ * Fired when the graph selection has changed. Called by
+ * mouseup and scroll events.
+ */
+ _onGraphSelecting: function () {
+ if (this._stopSelectionChangeEventPropagation) {
+ return;
+ }
+
+ this.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this.getTimeInterval());
+ },
+
+ _onGraphRendered: function (_, graphName) {
+ switch (graphName) {
+ case "timeline":
+ this.emit(EVENTS.UI_MARKERS_GRAPH_RENDERED);
+ break;
+ case "memory":
+ this.emit(EVENTS.UI_MEMORY_GRAPH_RENDERED);
+ break;
+ case "framerate":
+ this.emit(EVENTS.UI_FRAMERATE_GRAPH_RENDERED);
+ break;
+ }
+ },
+
+ /**
+ * Called whenever a preference in `devtools.performance.ui.` changes.
+ * Does not care about the enabling of memory/framerate graphs,
+ * because those will set values on a recording model, and
+ * the graphs will render based on the existence.
+ */
+ _onPrefChanged: Task.async(function* (_, prefName, prefValue) {
+ switch (prefName) {
+ case "hidden-markers": {
+ let graph = yield this.graphs.isAvailable("timeline");
+ if (graph) {
+ let filter = PerformanceController.getPref("hidden-markers");
+ graph.setFilter(filter);
+ graph.refresh({ force: true });
+ }
+ break;
+ }
+ }
+ }),
+
+ _setGraphVisibilityFromRecordingFeatures: function (recording) {
+ for (let [graphName, requirements] of Object.entries(GRAPH_REQUIREMENTS)) {
+ this.graphs.enable(graphName,
+ PerformanceController.isFeatureSupported(requirements.features));
+ }
+ },
+
+ /**
+ * Fetch the multiprocess status and if e10s is not currently on, disable
+ * realtime rendering.
+ *
+ * @return {boolean}
+ */
+ isRealtimeRenderingEnabled: function () {
+ return this._multiprocessData.enabled;
+ },
+
+ /**
+ * Show the graphs overview panel when a recording is finished
+ * when non-realtime graphs are enabled. Also set the graph visibility
+ * so the performance graphs know which graphs to render.
+ *
+ * @param {RecordingModel} recording
+ */
+ _showGraphsPanel: function (recording) {
+ this._setGraphVisibilityFromRecordingFeatures(recording);
+ $("#overview-pane").classList.remove("hidden");
+ },
+
+ /**
+ * Hide the graphs container completely.
+ */
+ _hideGraphsPanel: function () {
+ $("#overview-pane").classList.add("hidden");
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function (_, theme) {
+ this.graphs.setTheme({ theme, redraw: true });
+ },
+
+ toString: () => "[object OverviewView]"
+};
+
+/**
+ * Utility that can wrap a method of OverviewView that
+ * handles a recording state change like when a recording is starting,
+ * stopping, or about to start/stop, and determines whether or not
+ * the polling for rendering the overview graphs needs to start or stop.
+ * Must be called with the OverviewView context.
+ *
+ * @param {function?} fn
+ * @return {function}
+ */
+function OverviewViewOnStateChange(fn) {
+ return function _onRecordingStateChange(eventName, recording) {
+ // Normalize arguments for the RECORDING_STATE_CHANGE event,
+ // as it also has a `state` argument.
+ if (typeof recording === "string") {
+ recording = arguments[2];
+ }
+
+ let currentRecording = PerformanceController.getCurrentRecording();
+
+ // All these methods require a recording to exist selected and
+ // from the event name, since there is a delay between starting
+ // a recording and changing the selection.
+ if (!currentRecording || !recording) {
+ // If no recording (this can occur when having a console.profile recording, and
+ // we do not stop it from the backend), and we are still rendering updates,
+ // stop that.
+ if (this.isRendering()) {
+ this._stopPolling();
+ }
+ return;
+ }
+
+ // If realtime rendering is not enabed (e10s not on), then
+ // show the disabled message, or the full graphs if the recording is completed
+ if (!this.isRealtimeRenderingEnabled()) {
+ if (recording.isRecording()) {
+ this._hideGraphsPanel();
+ // Abort, as we do not want to change polling status.
+ return;
+ }
+ this._showGraphsPanel(recording);
+ }
+
+ if (this.isRendering() && !currentRecording.isRecording()) {
+ this._stopPolling();
+ } else if (currentRecording.isRecording() && !this.isRendering()) {
+ this._startPolling();
+ }
+
+ if (fn) {
+ fn.apply(this, arguments);
+ }
+ };
+}
+
+// Decorates the OverviewView as an EventEmitter
+EventEmitter.decorate(OverviewView);
diff --git a/devtools/client/performance/views/recordings.js b/devtools/client/performance/views/recordings.js
new file mode 100644
index 000000000..487ea4f03
--- /dev/null
+++ b/devtools/client/performance/views/recordings.js
@@ -0,0 +1,202 @@
+/* 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 document, window */
+"use strict";
+
+/**
+ * Functions handling the recordings UI.
+ */
+var RecordingsView = {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function () {
+ this._onSelect = this._onSelect.bind(this);
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onNewRecording = this._onNewRecording.bind(this);
+ this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
+ this._onRecordingDeleted = this._onRecordingDeleted.bind(this);
+ this._onRecordingExported = this._onRecordingExported.bind(this);
+
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
+ PerformanceController.on(EVENTS.RECORDING_ADDED, this._onNewRecording);
+ PerformanceController.on(EVENTS.RECORDING_DELETED, this._onRecordingDeleted);
+ PerformanceController.on(EVENTS.RECORDING_EXPORTED, this._onRecordingExported);
+
+ // DE-XUL: Begin migrating the recording sidebar to React. Temporarily hold state
+ // here.
+ this._listState = {
+ recordings: [],
+ labels: new WeakMap(),
+ selected: null,
+ };
+ this._listMount = PerformanceUtils.createHtmlMount($("#recording-list-mount"));
+ this._renderList();
+ },
+
+ /**
+ * Get the index of the currently selected recording. Only used by tests.
+ * @return {integer} index
+ */
+ getSelectedIndex() {
+ const { recordings, selected } = this._listState;
+ return recordings.indexOf(selected);
+ },
+
+ /**
+ * Set the currently selected recording via its index. Only used by tests.
+ * @param {integer} index
+ */
+ setSelectedByIndex(index) {
+ this._onSelect(this._listState.recordings[index]);
+ this._renderList();
+ },
+
+ /**
+ * DE-XUL: During the migration, this getter will access the selected recording from
+ * the private _listState object so that tests will continue to pass.
+ */
+ get selected() {
+ return this._listState.selected;
+ },
+
+ /**
+ * DE-XUL: During the migration, this getter will access the number of recordings.
+ */
+ get itemCount() {
+ return this._listState.recordings.length;
+ },
+
+ /**
+ * DE-XUL: Render the recording list using React.
+ */
+ _renderList: function () {
+ const {recordings, labels, selected} = this._listState;
+
+ const recordingList = RecordingList({
+ itemComponent: RecordingListItem,
+ items: recordings.map(recording => ({
+ onSelect: () => this._onSelect(recording),
+ onSave: () => this._onSaveButtonClick(recording),
+ isLoading: !recording.isRecording() && !recording.isCompleted(),
+ isRecording: recording.isRecording(),
+ isSelected: recording === selected,
+ duration: recording.getDuration().toFixed(0),
+ label: labels.get(recording),
+ }))
+ });
+
+ ReactDOM.render(recordingList, this._listMount);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function () {
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange);
+ PerformanceController.off(EVENTS.RECORDING_ADDED, this._onNewRecording);
+ PerformanceController.off(EVENTS.RECORDING_DELETED, this._onRecordingDeleted);
+ PerformanceController.off(EVENTS.RECORDING_EXPORTED, this._onRecordingExported);
+ },
+
+ /**
+ * Called when a new recording is stored in the UI. This handles
+ * when recordings are lazily loaded (like a console.profile occurring
+ * before the tool is loaded) or imported. In normal manual recording cases,
+ * this will also be fired.
+ */
+ _onNewRecording: function (_, recording) {
+ this._onRecordingStateChange(_, null, recording);
+ },
+
+ /**
+ * Signals that a recording has changed state.
+ *
+ * @param string state
+ * Can be "recording-started", "recording-stopped", "recording-stopping"
+ * @param RecordingModel recording
+ * Model of the recording that was started.
+ */
+ _onRecordingStateChange: function (_, state, recording) {
+ const { recordings, labels } = this._listState;
+
+ if (!recordings.includes(recording)) {
+ recordings.push(recording);
+ labels.set(recording, recording.getLabel() ||
+ L10N.getFormatStr("recordingsList.itemLabel", recordings.length));
+
+ // If this is a manual recording, immediately select it, or
+ // select a console profile if its the only one
+ if (!recording.isConsole() || !this._listState.selected) {
+ this._onSelect(recording);
+ }
+ }
+
+ // Determine if the recording needs to be selected.
+ const isCompletedManualRecording = !recording.isConsole() && recording.isCompleted();
+ if (recording.isImported() || isCompletedManualRecording) {
+ this._onSelect(recording);
+ }
+
+ this._renderList();
+ },
+
+ /**
+ * Clears out all non-console recordings.
+ */
+ _onRecordingDeleted: function (_, recording) {
+ const { recordings } = this._listState;
+ const index = recordings.indexOf(recording);
+ if (index === -1) {
+ throw new Error("Attempting to remove a recording that doesn't exist.");
+ }
+ recordings.splice(index, 1);
+ this._renderList();
+ },
+
+ /**
+ * The select listener for this container.
+ */
+ _onSelect: Task.async(function* (recording) {
+ this._listState.selected = recording;
+ this.emit(EVENTS.UI_RECORDING_SELECTED, recording);
+ this._renderList();
+ }),
+
+ /**
+ * The click listener for the "save" button of each item in this container.
+ */
+ _onSaveButtonClick: function (recording) {
+ 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) {
+ return;
+ }
+ this.emit(EVENTS.UI_EXPORT_RECORDING, recording, fp.file);
+ }});
+ },
+
+ _onRecordingExported: function (_, recording, file) {
+ if (recording.isConsole()) {
+ return;
+ }
+ const name = file.leafName.replace(/\..+$/, "");
+ this._listState.labels.set(recording, name);
+ this._renderList();
+ }
+};
+
+/**
+ * Convenient way of emitting events from the RecordingsView.
+ */
+EventEmitter.decorate(RecordingsView);
diff --git a/devtools/client/performance/views/toolbar.js b/devtools/client/performance/views/toolbar.js
new file mode 100644
index 000000000..bcab09a86
--- /dev/null
+++ b/devtools/client/performance/views/toolbar.js
@@ -0,0 +1,160 @@
+/* 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 document */
+"use strict";
+
+/**
+ * View handler for toolbar events (mostly option toggling and triggering)
+ */
+var ToolbarView = {
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: Task.async(function* () {
+ this._onFilterPopupShowing = this._onFilterPopupShowing.bind(this);
+ this._onFilterPopupHiding = this._onFilterPopupHiding.bind(this);
+ this._onHiddenMarkersChanged = this._onHiddenMarkersChanged.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._popup = $("#performance-options-menupopup");
+
+ this.optionsView = new OptionsView({
+ branchName: BRANCH_NAME,
+ menupopup: this._popup
+ });
+
+ // Set the visibility of experimental UI options on load
+ // based off of `devtools.performance.ui.experimental` preference
+ let experimentalEnabled = PerformanceController.getOption("experimental");
+ this._toggleExperimentalUI(experimentalEnabled);
+
+ yield this.optionsView.initialize();
+ this.optionsView.on("pref-changed", this._onPrefChanged);
+
+ this._buildMarkersFilterPopup();
+ this._updateHiddenMarkersPopup();
+ $("#performance-filter-menupopup").addEventListener("popupshowing",
+ this._onFilterPopupShowing);
+ $("#performance-filter-menupopup").addEventListener("popuphiding",
+ this._onFilterPopupHiding);
+ }),
+
+ /**
+ * Unbinds events and cleans up view.
+ */
+ destroy: function () {
+ $("#performance-filter-menupopup").removeEventListener("popupshowing",
+ this._onFilterPopupShowing);
+ $("#performance-filter-menupopup").removeEventListener("popuphiding",
+ this._onFilterPopupHiding);
+ this._popup = null;
+
+ this.optionsView.off("pref-changed", this._onPrefChanged);
+ this.optionsView.destroy();
+ },
+
+ /**
+ * Creates the timeline markers filter popup.
+ */
+ _buildMarkersFilterPopup: function () {
+ for (let [markerName, markerDetails] of Object.entries(TIMELINE_BLUEPRINT)) {
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("closemenu", "none");
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("align", "center");
+ menuitem.setAttribute("flex", "1");
+ menuitem.setAttribute("label",
+ MarkerBlueprintUtils.getMarkerGenericName(markerName));
+ menuitem.setAttribute("marker-type", markerName);
+ menuitem.className = `marker-color-${markerDetails.colorName}`;
+
+ menuitem.addEventListener("command", this._onHiddenMarkersChanged);
+
+ $("#performance-filter-menupopup").appendChild(menuitem);
+ }
+ },
+
+ /**
+ * Updates the menu items checked state in the timeline markers filter popup.
+ */
+ _updateHiddenMarkersPopup: function () {
+ let menuItems = $$("#performance-filter-menupopup menuitem[marker-type]");
+ let hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+ for (let menuitem of menuItems) {
+ if (~hiddenMarkers.indexOf(menuitem.getAttribute("marker-type"))) {
+ menuitem.removeAttribute("checked");
+ } else {
+ menuitem.setAttribute("checked", "true");
+ }
+ }
+ },
+
+ /**
+ * Fired when `devtools.performance.ui.experimental` is changed, or
+ * during init. Toggles the visibility of experimental performance tool options
+ * in the UI options.
+ *
+ * Sets or removes "experimental-enabled" on the menu and main elements,
+ * hiding or showing all elements with class "experimental-option".
+ *
+ * TODO re-enable "#option-enable-memory" permanently once stable in bug 1163350
+ * TODO re-enable "#option-show-jit-optimizations" permanently once stable in
+ * bug 1163351
+ *
+ * @param {boolean} isEnabled
+ */
+ _toggleExperimentalUI: function (isEnabled) {
+ if (isEnabled) {
+ $(".theme-body").classList.add("experimental-enabled");
+ this._popup.classList.add("experimental-enabled");
+ } else {
+ $(".theme-body").classList.remove("experimental-enabled");
+ this._popup.classList.remove("experimental-enabled");
+ }
+ },
+
+ /**
+ * Fired when the markers filter popup starts to show.
+ */
+ _onFilterPopupShowing: function () {
+ $("#filter-button").setAttribute("open", "true");
+ },
+
+ /**
+ * Fired when the markers filter popup starts to hide.
+ */
+ _onFilterPopupHiding: function () {
+ $("#filter-button").removeAttribute("open");
+ },
+
+ /**
+ * Fired when a menu item in the markers filter popup is checked or unchecked.
+ */
+ _onHiddenMarkersChanged: function () {
+ let checkedMenuItems =
+ $$("#performance-filter-menupopup menuitem[marker-type]:not([checked])");
+ let hiddenMarkers = Array.map(checkedMenuItems, e => e.getAttribute("marker-type"));
+ PerformanceController.setPref("hidden-markers", hiddenMarkers);
+ },
+
+ /**
+ * Fired when a preference changes in the underlying OptionsView.
+ * Propogated by the PerformanceController.
+ */
+ _onPrefChanged: function (_, prefName) {
+ let value = PerformanceController.getOption(prefName);
+
+ if (prefName === "experimental") {
+ this._toggleExperimentalUI(value);
+ }
+
+ this.emit(EVENTS.UI_PREF_CHANGED, prefName, value);
+ },
+
+ toString: () => "[object ToolbarView]"
+};
+
+EventEmitter.decorate(ToolbarView);