Files
tubestation/devtools/client/performance/views/details-waterfall.js

253 lines
7.5 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 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);