summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/views/details-waterfall.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/views/details-waterfall.js')
-rw-r--r--devtools/client/performance/views/details-waterfall.js282
1 files changed, 282 insertions, 0 deletions
diff --git a/devtools/client/performance/views/details-waterfall.js b/devtools/client/performance/views/details-waterfall.js
new file mode 100644
index 0000000000..975aa43874
--- /dev/null
+++ b/devtools/client/performance/views/details-waterfall.js
@@ -0,0 +1,282 @@
+/* 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/. */
+/* globals $, PerformanceController, OverviewView, DetailsView */
+"use strict";
+
+const { extend } = require("devtools/shared/extend");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {
+ setNamedTimeout,
+ clearNamedTimeout,
+} = require("devtools/client/shared/widgets/view-helpers");
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const EVENTS = require("devtools/client/performance/events");
+const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+const {
+ TickUtils,
+} = require("devtools/client/performance/modules/waterfall-ticks");
+const {
+ MarkerDetails,
+} = require("devtools/client/performance/modules/widgets/marker-details");
+const {
+ DetailsSubview,
+} = require("devtools/client/performance/views/details-abstract-subview");
+
+const Waterfall = React.createFactory(
+ require("devtools/client/performance/components/Waterfall")
+);
+
+const MARKER_DETAILS_WIDTH = 200;
+// Units are in milliseconds.
+const WATERFALL_RESIZE_EVENTS_DRAIN = 100;
+
+/**
+ * Waterfall view containing the timeline markers, controlled by DetailsView.
+ */
+const WaterfallView = 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 = {}) {
+ const recording = PerformanceController.getCurrentRecording();
+ if (recording.isRecording()) {
+ return;
+ }
+ const startTime = interval.startTime || 0;
+ const endTime = interval.endTime || recording.getDuration();
+ const markers = recording.getMarkers();
+ const 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) {
+ const recording = PerformanceController.getCurrentRecording();
+ const frames = recording.getFrames();
+ const 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) {
+ PerformanceController.viewSourceInDebugger(
+ data.url,
+ data.line,
+ data.column
+ );
+ },
+
+ /**
+ * Called when MarkerDetails view emits an event to snap to allocations.
+ */
+ _onShowAllocations: function(data) {
+ let { endTime } = data;
+ let startTime = 0;
+ const recording = PerformanceController.getCurrentRecording();
+ const 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 (const 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) {
+ const cached = this._cache.get(markers);
+ if (cached) {
+ return cached;
+ }
+
+ const 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();
+
+ const doc = this.treeContainer.ownerDocument;
+ const startTime = interval.startTime | 0;
+ const endTime = interval.endTime | 0;
+ const dataScale = this.waterfallWidth / (endTime - startTime);
+
+ this.canvas = TickUtils.drawWaterfallBackground(
+ doc,
+ dataScale,
+ this.waterfallWidth
+ );
+
+ const 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);
+
+exports.WaterfallView = WaterfallView;