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