diff options
Diffstat (limited to 'devtools/client/performance/views/details-waterfall.js')
-rw-r--r-- | devtools/client/performance/views/details-waterfall.js | 252 |
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); |