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.js252
1 files changed, 252 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 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);