Bug 1168125 - Replace the waterfall view with a tree, r=jsantell

This commit is contained in:
Victor Porof 2015-05-27 17:23:53 -04:00
parent 921789e0e7
commit 04cbd3898d
12 changed files with 845 additions and 808 deletions

View File

@ -8,7 +8,6 @@
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
@ -29,27 +28,29 @@ loader.lazyRequireGetter(this, "MarkerUtils",
*/
function MarkerDetails(parent, splitter) {
EventEmitter.decorate(this);
this._onClick = this._onClick.bind(this);
this._document = parent.ownerDocument;
this._parent = parent;
this._splitter = splitter;
this._splitter.addEventListener("mouseup", () => this.emit("resize"));
this._onClick = this._onClick.bind(this);
this._onSplitterMouseUp = this._onSplitterMouseUp.bind(this);
this._parent.addEventListener("click", this._onClick);
this._splitter.addEventListener("mouseup", this._onSplitterMouseUp);
}
MarkerDetails.prototype = {
/**
* Removes any node references from this view.
* Sets this view's width.
* @param boolean
*/
destroy: function() {
this.empty();
this._parent.removeEventListener("click", this._onClick);
this._parent = null;
this._splitter = null;
set width(value) {
this._parent.setAttribute("width", value);
},
/**
* Clears the view.
* Clears the marker details from this view.
*/
empty: function() {
this._parent.innerHTML = "";
@ -60,8 +61,8 @@ MarkerDetails.prototype = {
*
* @param object params
* An options object holding:
* marker - The marker to display.
* frames - Array of stack frame information; see stack.js.
* - marker: The marker to display.
* - frames: Array of stack frame information; see stack.js.
*/
render: function({ marker, frames }) {
this.empty();
@ -69,10 +70,10 @@ MarkerDetails.prototype = {
let elements = [];
elements.push(MarkerUtils.DOM.buildTitle(this._document, marker));
elements.push(MarkerUtils.DOM.buildDuration(this._document, marker));
MarkerUtils.DOM.buildFields(this._document, marker).forEach(field => elements.push(field));
MarkerUtils.DOM.buildFields(this._document, marker).forEach(f => elements.push(f));
// Build a stack element -- and use the "startStack" label if
// we have both a star and endStack.
// we have both a startStack and endStack.
if (marker.stack) {
let type = marker.endStack ? "startStack" : "stack";
elements.push(MarkerUtils.DOM.buildStackTrace(this._document, {
@ -98,6 +99,13 @@ MarkerDetails.prototype = {
this.emit("view-source", data.url, data.line);
}
},
/**
* Handles the "mouseup" event on the marker details view splitter.
*/
_onSplitterMouseUp: function() {
this.emit("resize");
}
};
/**

View File

@ -0,0 +1,305 @@
/* 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/. */
"use strict";
/**
* This file contains the "marker" view, essentially a detailed list
* of all the markers in the timeline data.
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
const { Heritage } = require("resource:///modules/devtools/ViewHelpers.jsm");
const { AbstractTreeItem } = require("resource:///modules/devtools/AbstractTreeItem.jsm");
const { TIMELINE_BLUEPRINT: ORIGINAL_BP } = require("devtools/performance/global");
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const LEVEL_INDENT = 10; // px
const ARROW_NODE_OFFSET = -15; // px
const WATERFALL_MARKER_SIDEBAR_WIDTH = 175; // px
const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5; // px
/**
* A detailed waterfall view for the timeline data.
*
* @param MarkerView owner
* The MarkerView considered the "owner" marker. This newly created
* instance will be represent the "submarker". Should be null for root nodes.
* @param object marker
* Details about this marker, like { name, start, end, submarkers } etc.
* @param number level [optional]
* The indentation level in the waterfall tree. The root node is at level 0.
* @param boolean hidden [optional]
* Whether this node should be hidden and not contribute to depth/level
* calculations. Defaults to false.
*/
function MarkerView({ owner, marker, level, hidden }) {
AbstractTreeItem.call(this, {
parent: owner,
level: level|0 - (hidden ? 1 : 0)
});
this.marker = marker;
this.hidden = !!hidden;
this._onItemBlur = this._onItemBlur.bind(this);
this._onItemFocus = this._onItemFocus.bind(this);
}
MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
/**
* Calculates and stores the available width for the waterfall.
* This should be invoked every time the container node is resized.
*/
recalculateBounds: function() {
this.root._waterfallWidth = this.bounds.width - WATERFALL_MARKER_SIDEBAR_WIDTH;
},
/**
* Sets a list of names and colors used to paint markers.
* @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
* @param object blueprint
*/
set blueprint(blueprint) {
this.root._blueprint = blueprint;
},
get blueprint() {
return this.root._blueprint;
},
/**
* Sets the { startTime, endTime }, in milliseconds.
* @param object interval
*/
set interval(interval) {
this.root._interval = interval;
},
get interval() {
return this.root._interval;
},
/**
* Gets the current waterfall width.
* @return number
*/
getWaterfallWidth: function() {
return this._waterfallWidth;
},
/**
* Gets the data scale amount for the current width and interval.
* @return number
*/
getDataScale: function() {
let startTime = this.root._interval.startTime|0;
let endTime = this.root._interval.endTime|0;
return this.root._waterfallWidth / (endTime - startTime);
},
/**
* Creates the view for this waterfall node.
* @param nsIDOMNode document
* @param nsIDOMNode arrowNode
* @return nsIDOMNode
*/
_displaySelf: function(document, arrowNode) {
let targetNode = document.createElement("hbox");
targetNode.className = "waterfall-tree-item";
if (this == this.root) {
// Bounds are needed for properly positioning and scaling markers in
// the waterfall, but it's sufficient to make those calculations only
// for the root node.
this.root.recalculateBounds();
// The AbstractTreeItem propagates events to the root, so we don't
// need to listen them on descendant items in the tree.
this._addEventListeners();
} else {
// Root markers are an implementation detail and shouldn't be shown.
this._buildMarkerCells(document, targetNode, arrowNode);
}
if (this.hidden) {
targetNode.style.display = "none";
}
return targetNode;
},
/**
* Populates this node in the waterfall tree with the corresponding "markers".
* @param array:AbstractTreeItem children
*/
_populateSelf: function(children) {
let submarkers = this.marker.submarkers;
if (!submarkers || !submarkers.length) {
return;
}
let blueprint = this.root._blueprint;
let startTime = this.root._interval.startTime;
let endTime = this.root._interval.endTime;
let newLevel = this.level + 1;
for (let i = 0, len = submarkers.length; i < len; i++) {
let marker = submarkers[i];
// If this marker isn't in the global timeline blueprint, don't display
// it, but dump a warning message to the console.
if (!(marker.name in blueprint)) {
if (!(marker.name in ORIGINAL_BP)) {
console.warn(`Marker not found in timeline blueprint: ${marker.name}.`);
}
continue;
}
if (!isMarkerInRange(marker, startTime|0, endTime|0)) {
continue;
}
children.push(new MarkerView({
owner: this,
marker: marker,
level: newLevel,
inverted: this.inverted
}));
}
},
/**
* Builds all the nodes representing a marker in the waterfall.
* @param nsIDOMNode document
* @param nsIDOMNode targetNode
* @param nsIDOMNode arrowNode
*/
_buildMarkerCells: function(doc, targetNode, arrowNode) {
// Root markers are an implementation detail and shouldn't be shown.
let marker = this.marker;
if (marker.name == "(root)") {
return;
}
let style = this.root._blueprint[marker.name];
let startTime = this.root._interval.startTime;
let endTime = this.root._interval.endTime;
let sidebarCell = this._buildMarkerSidebar(
doc, style, marker);
let timebarCell = this._buildMarkerTimebar(
doc, style, marker, startTime, endTime, arrowNode);
targetNode.appendChild(sidebarCell);
targetNode.appendChild(timebarCell);
// Don't render an expando-arrow for leaf nodes.
let submarkers = this.marker.submarkers;
let hasDescendants = submarkers && submarkers.length > 0;
if (hasDescendants) {
targetNode.setAttribute("expandable", "");
} else {
arrowNode.setAttribute("invisible", "");
}
targetNode.setAttribute("level", this.level);
},
/**
* Functions creating each cell in this waterfall view.
* Invoked by `_displaySelf`.
*/
_buildMarkerSidebar: function(doc, style, marker) {
let cell = doc.createElement("hbox");
cell.className = "waterfall-sidebar theme-sidebar";
cell.setAttribute("width", WATERFALL_MARKER_SIDEBAR_WIDTH);
cell.setAttribute("align", "center");
let bullet = doc.createElement("hbox");
bullet.className = `waterfall-marker-bullet marker-color-${style.colorName}`;
bullet.style.transform = `translateX(${this.level * LEVEL_INDENT}px)`;
bullet.setAttribute("type", marker.name);
cell.appendChild(bullet);
let name = doc.createElement("description");
let label = MarkerUtils.getMarkerLabel(marker);
name.className = "plain waterfall-marker-name";
name.style.transform = `translateX(${this.level * LEVEL_INDENT}px)`;
name.setAttribute("crop", "end");
name.setAttribute("flex", "1");
name.setAttribute("value", label);
name.setAttribute("tooltiptext", label);
cell.appendChild(name);
return cell;
},
_buildMarkerTimebar: function(doc, style, marker, startTime, endTime, arrowNode) {
let cell = doc.createElement("hbox");
cell.className = "waterfall-marker waterfall-background-ticks";
cell.setAttribute("align", "center");
cell.setAttribute("flex", "1");
let dataScale = this.getDataScale();
let offset = (marker.start - startTime) * dataScale;
let width = (marker.end - marker.start) * dataScale;
arrowNode.style.transform =`translateX(${offset + ARROW_NODE_OFFSET}px)`;
cell.appendChild(arrowNode);
let bar = doc.createElement("hbox");
bar.className = `waterfall-marker-bar marker-color-${style.colorName}`;
bar.style.transform = `translateX(${offset}px)`;
bar.setAttribute("type", marker.name);
bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_TIMEBAR_WIDTH_MIN));
cell.appendChild(bar);
return cell;
},
/**
* Adds the event listeners for this particular tree item.
*/
_addEventListeners: function() {
this.on("focus", this._onItemFocus);
this.on("blur", this._onItemBlur);
},
/**
* Handler for the "blur" event on the root item.
*/
_onItemBlur: function() {
this.root.emit("unselected");
},
/**
* Handler for the "mousedown" event on the root item.
*/
_onItemFocus: function(e, item) {
this.root.emit("selected", item.marker);
}
});
/**
* Checks if a given marker is in the specified time range.
*
* @param object e
* The marker containing the { start, end } timestamps.
* @param number start
* The earliest allowed time.
* @param number end
* The latest allowed time.
* @return boolean
* True if the marker fits inside the specified time range.
*/
function isMarkerInRange(e, start, end) {
let m_start = e.start|0;
let m_end = e.end|0;
return (m_start >= start && m_end <= end) || // bounds inside
(m_start < start && m_end > end) || // bounds outside
(m_start < start && m_end >= start && m_end <= end) || // overlap start
(m_end > end && m_start >= start && m_start <= end); // overlap end
}
exports.MarkerView = MarkerView;
exports.WATERFALL_MARKER_SIDEBAR_WIDTH = WATERFALL_MARKER_SIDEBAR_WIDTH;

View File

@ -19,6 +19,8 @@ loader.lazyRequireGetter(this, "getColor",
"devtools/shared/theme", true);
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "TickUtils",
"devtools/performance/waterfall-ticks", true);
const OVERVIEW_HEADER_HEIGHT = 14; // px
const OVERVIEW_ROW_HEIGHT = 11; // px
@ -143,7 +145,12 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
let tickInterval = this._findOptimalTickInterval(dataScale);
let tickInterval = TickUtils.findOptimalTickInterval({
ticksMultiple: OVERVIEW_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: OVERVIEW_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
ctx.textBaseline = "middle";
ctx.font = fontSize + "px " + fontFamily;
@ -190,32 +197,6 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
return canvas;
},
/**
* Finds the optimal tick interval between time markers in this overview.
*/
_findOptimalTickInterval: function(dataScale) {
let timingStep = OVERVIEW_HEADER_TICKS_MULTIPLE;
let spacingMin = OVERVIEW_HEADER_TICKS_SPACING_MIN * this._pixelRatio;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > spacingMin) {
return dataScale;
}
while (true) {
let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < spacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
},
/**
* Sets the theme via `theme` to either "light" or "dark",
* and updates the internal styling to match. Requires a redraw

View File

@ -56,11 +56,11 @@ const sum = vals => vals.reduce((a, b) => a + b, 0);
* parent node is used for all rows.
*
* @param CallView caller
* The CallView considered the "caller" frame. This instance will be
* represent the "callee". Should be null for root nodes.
* The CallView considered the "caller" frame. This newly created
* instance will be represent the "callee". Should be null for root nodes.
* @param ThreadNode | FrameNode frame
* Details about this function, like { samples, duration, calls } etc.
* @param number level
* @param number level [optional]
* The indentation level in the call tree. The root node is at level 0.
* @param boolean hidden [optional]
* Whether this node should be hidden and not contribute to depth/level
@ -213,7 +213,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
* Invoked by `_displaySelf`.
*/
_createTimeCell: function(doc, duration, isSelf = false) {
let cell = doc.createElement("label");
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", isSelf ? "self-duration" : "duration");
cell.setAttribute("crop", "end");
@ -221,7 +221,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
return cell;
},
_createExecutionCell: function(doc, percentage, isSelf = false) {
let cell = doc.createElement("label");
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", isSelf ? "self-percentage" : "percentage");
cell.setAttribute("crop", "end");
@ -229,7 +229,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
return cell;
},
_createAllocationsCell: function(doc, count, isSelf = false) {
let cell = doc.createElement("label");
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", isSelf ? "self-allocations" : "allocations");
cell.setAttribute("crop", "end");
@ -237,7 +237,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
return cell;
},
_createSamplesCell: function(doc, count) {
let cell = doc.createElement("label");
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", "samples");
cell.setAttribute("crop", "end");
@ -254,7 +254,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
// Don't render a name label node if there's no function name. A different
// location label node will be rendered instead.
if (frameName) {
let nameNode = doc.createElement("label");
let nameNode = doc.createElement("description");
nameNode.className = "plain call-tree-name";
nameNode.setAttribute("flex", "1");
nameNode.setAttribute("crop", "end");
@ -277,7 +277,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
},
_appendFunctionDetailsCells: function(doc, cell, frameInfo) {
if (frameInfo.fileName) {
let urlNode = doc.createElement("label");
let urlNode = doc.createElement("description");
urlNode.className = "plain call-tree-url";
urlNode.setAttribute("flex", "1");
urlNode.setAttribute("crop", "end");
@ -288,21 +288,21 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
}
if (frameInfo.line) {
let lineNode = doc.createElement("label");
let lineNode = doc.createElement("description");
lineNode.className = "plain call-tree-line";
lineNode.setAttribute("value", ":" + frameInfo.line);
cell.appendChild(lineNode);
}
if (frameInfo.column) {
let columnNode = doc.createElement("label");
let columnNode = doc.createElement("description");
columnNode.className = "plain call-tree-column";
columnNode.setAttribute("value", ":" + frameInfo.column);
cell.appendChild(columnNode);
}
if (frameInfo.host) {
let hostNode = doc.createElement("label");
let hostNode = doc.createElement("description");
hostNode.className = "plain call-tree-host";
hostNode.setAttribute("value", frameInfo.host);
cell.appendChild(hostNode);
@ -313,7 +313,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
cell.appendChild(spacerNode);
if (frameInfo.categoryData.label) {
let categoryNode = doc.createElement("label");
let categoryNode = doc.createElement("description");
categoryNode.className = "plain call-tree-category";
categoryNode.style.color = frameInfo.categoryData.color;
categoryNode.setAttribute("value", frameInfo.categoryData.label);

View File

@ -0,0 +1,187 @@
/* 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/. */
"use strict";
/**
* This file contains the "waterfall ticks" view, a header for the
* markers displayed in the waterfall.
*/
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "WATERFALL_MARKER_SIDEBAR_WIDTH",
"devtools/performance/marker-view", true);
const HTML_NS = "http://www.w3.org/1999/xhtml";
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
const WATERFALL_HEADER_TEXT_PADDING = 3; // px
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
/**
* A header for a markers waterfall.
*
* @param MarkerView root
* The root item of the waterfall tree.
*/
function WaterfallHeader(root) {
this.root = root;
}
WaterfallHeader.prototype = {
/**
* Creates and appends this header as the first element of the specified
* parent element.
*
* @param nsIDOMNode parentNode
* The parent element for this header.
*/
attachTo: function(parentNode) {
let document = parentNode.ownerDocument;
let startTime = this.root.interval.startTime;
let dataScale = this.root.getDataScale();
let waterfallWidth = this.root.getWaterfallWidth();
let header = this._buildNode(document, startTime, dataScale, waterfallWidth);
parentNode.insertBefore(header, parentNode.firstChild);
this._drawWaterfallBackground(document, dataScale, waterfallWidth);
},
/**
* Creates the node displaying this view.
*/
_buildNode: function(doc, startTime, dataScale, waterfallWidth) {
let container = doc.createElement("hbox");
container.className = "waterfall-header-container";
container.setAttribute("flex", "1");
let sidebar = doc.createElement("hbox");
sidebar.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_MARKER_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
container.appendChild(sidebar);
let name = doc.createElement("description");
name.className = "plain waterfall-header-name";
name.setAttribute("value", L10N.getStr("timeline.records"));
sidebar.appendChild(name);
let ticks = doc.createElement("hbox");
ticks.className = "waterfall-header-ticks waterfall-background-ticks";
ticks.setAttribute("align", "center");
ticks.setAttribute("flex", "1");
container.appendChild(ticks);
let tickInterval = findOptimalTickInterval({
ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
for (let x = 0; x < waterfallWidth; x += tickInterval) {
let left = x + WATERFALL_HEADER_TEXT_PADDING;
let time = Math.round(x / dataScale + startTime);
let label = L10N.getFormatStr("timeline.tick", time);
let node = doc.createElement("description");
node.className = "plain waterfall-header-tick";
node.style.transform = "translateX(" + left + "px)";
node.setAttribute("value", label);
ticks.appendChild(node);
}
return container;
},
/**
* Creates the background displayed on the marker's waterfall.
*/
_drawWaterfallBackground: function(doc, dataScale, waterfallWidth) {
if (!this._canvas || !this._ctx) {
this._canvas = doc.createElementNS(HTML_NS, "canvas");
this._ctx = this._canvas.getContext("2d");
}
let canvas = this._canvas;
let ctx = this._ctx;
// Nuke the context.
let canvasWidth = canvas.width = waterfallWidth;
let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
// Start over.
let imageData = ctx.createImageData(canvasWidth, canvasHeight);
let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length);
let view8bit = new Uint8ClampedArray(buf);
let view32bit = new Uint32Array(buf);
// Build new millisecond tick lines...
let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
let tickInterval = findOptimalTickInterval({
ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
dataScale: dataScale
});
// Insert one pixel for each division on each scale.
for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
let increment = tickInterval * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) {
let position = x | 0;
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
}
// Flush the image data and cache the waterfall background.
pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0);
doc.mozSetImageElement("waterfall-background", canvas);
}
};
/**
* Finds the optimal tick interval between time markers in this timeline.
*
* @param number ticksMultiple
* @param number ticksSpacingMin
* @param number dataScale
* @return number
*/
function findOptimalTickInterval({ ticksMultiple, ticksSpacingMin, dataScale }) {
let timingStep = ticksMultiple;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > ticksSpacingMin) {
return dataScale;
}
while (true) {
let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < ticksSpacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
}
exports.WaterfallHeader = WaterfallHeader;
exports.TickUtils = { findOptimalTickInterval };

View File

@ -1,620 +0,0 @@
/* 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/. */
"use strict";
/**
* This file contains the "waterfall" view, essentially a detailed list
* of all the markers in the timeline data.
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
loader.lazyImporter(this, "setNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
loader.lazyImporter(this, "clearNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const WATERFALL_SIDEBAR_WIDTH = 200; // px
const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
const WATERFALL_HEADER_TEXT_PADDING = 3; // px
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
const WATERFALL_MARKER_BAR_WIDTH_MIN = 5; // px
const WATERFALL_ROWCOUNT_ONPAGEUPDOWN = 10;
/**
* A detailed waterfall view for the timeline data.
*
* @param nsIDOMNode parent
* The parent node holding the waterfall.
* @param nsIDOMNode container
* The container node that key events should be bound to.
* @param Object blueprint
* List of names and colors defining markers.
*/
function Waterfall(parent, container, blueprint) {
EventEmitter.decorate(this);
this._parent = parent;
this._document = parent.ownerDocument;
this._container = container;
this._fragment = this._document.createDocumentFragment();
this._outstandingMarkers = [];
this._headerContents = this._document.createElement("hbox");
this._headerContents.className = "waterfall-header-contents";
this._parent.appendChild(this._headerContents);
this._listContents = this._document.createElement("vbox");
this._listContents.className = "waterfall-list-contents";
this._listContents.setAttribute("flex", "1");
this._parent.appendChild(this._listContents);
this.setupKeys();
this._isRTL = this._getRTL();
// Lazy require is a bit slow, and these are hot objects.
this._l10n = L10N;
this._blueprint = blueprint;
this._setNamedTimeout = setNamedTimeout;
this._clearNamedTimeout = clearNamedTimeout;
// Selected row index. By default, we want the first
// row to be selected.
this._selectedRowIdx = 0;
// Default rowCount
this.rowCount = WATERFALL_ROWCOUNT_ONPAGEUPDOWN;
}
Waterfall.prototype = {
/**
* Removes any node references from this view.
*/
destroy: function() {
this._parent = this._document = this._container = null;
},
/**
* Populates this view with the provided data source.
*
* @param object data
* An object containing the following properties:
* - markers: a list of markers received from the controller
* - interval: the { startTime, endTime }, in milliseconds
*/
setData: function({ markers, interval }) {
this.clearView();
this._markers = markers;
this._interval = interval;
let { startTime, endTime } = interval;
let dataScale = this._waterfallWidth / (endTime - startTime);
this._drawWaterfallBackground(dataScale);
this._buildHeader(this._headerContents, startTime, dataScale);
this._buildMarkers(this._listContents, markers, startTime, endTime, dataScale);
this.selectRow(this._selectedRowIdx);
},
/**
* List of names and colors used to paint markers.
* @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
*/
setBlueprint: function(blueprint) {
this._blueprint = blueprint;
},
/**
* Keybindings.
*/
setupKeys: function() {
let pane = this._container;
pane.addEventListener("keydown", e => {
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx - 1);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx + 1);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_HOME) {
e.preventDefault();
this.selectNearestRow(0);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_END) {
e.preventDefault();
this.selectNearestRow(this._listContents.children.length);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx - this.rowCount);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx + this.rowCount);
}
}, true);
},
/**
* Depopulates this view.
*/
clearView: function() {
while (this._headerContents.hasChildNodes()) {
this._headerContents.firstChild.remove();
}
while (this._listContents.hasChildNodes()) {
this._listContents.firstChild.remove();
}
this._listContents.scrollTop = 0;
this._outstandingMarkers.length = 0;
this._clearNamedTimeout("flush-outstanding-markers");
},
/**
* Calculates and stores the available width for the waterfall.
* This should be invoked every time the container window is resized.
*/
recalculateBounds: function() {
let bounds = this._parent.getBoundingClientRect();
this._waterfallWidth = bounds.width - WATERFALL_SIDEBAR_WIDTH;
},
/**
* Creates the header part of this view.
*
* @param nsIDOMNode parent
* The parent node holding the header.
* @param number startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildHeader: function(parent, startTime, dataScale) {
let container = this._document.createElement("hbox");
container.className = "waterfall-header-container";
container.setAttribute("flex", "1");
let sidebar = this._document.createElement("hbox");
sidebar.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
container.appendChild(sidebar);
let name = this._document.createElement("label");
name.className = "plain waterfall-header-name";
name.setAttribute("value", this._l10n.getStr("timeline.records"));
sidebar.appendChild(name);
let ticks = this._document.createElement("hbox");
ticks.className = "waterfall-header-ticks waterfall-background-ticks";
ticks.setAttribute("align", "center");
ticks.setAttribute("flex", "1");
container.appendChild(ticks);
let offset = this._isRTL ? this._waterfallWidth : 0;
let direction = this._isRTL ? -1 : 1;
let tickInterval = this._findOptimalTickInterval({
ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
let left = x + direction * WATERFALL_HEADER_TEXT_PADDING;
let time = Math.round(x / dataScale + startTime);
let label = this._l10n.getFormatStr("timeline.tick", time);
let node = this._document.createElement("label");
node.className = "plain waterfall-header-tick";
node.style.transform = "translateX(" + (left - offset) + "px)";
node.setAttribute("value", label);
ticks.appendChild(node);
}
parent.appendChild(container);
},
/**
* Creates the markers part of this view.
*
* @param nsIDOMNode parent
* The parent node holding the markers.
* @param number startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildMarkers: function(parent, markers, startTime, endTime, dataScale) {
let rowsCount = 0;
let markerIdx = -1;
for (let marker of markers) {
markerIdx++;
if (!isMarkerInRange(marker, startTime, endTime)) {
continue;
}
if (!(marker.name in this._blueprint)) {
continue;
}
// Only build and display a finite number of markers initially, to
// preserve a snappy UI. After a certain delay, continue building the
// outstanding markers while there's (hopefully) no user interaction.
let arguments_ = [this._fragment, marker, startTime, dataScale, markerIdx, rowsCount];
if (rowsCount++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
this._buildMarker.apply(this, arguments_);
} else {
this._outstandingMarkers.push(arguments_);
}
}
// If there are no outstanding markers, add a dummy "spacer" at the end
// to fill up any remaining available space in the UI.
if (!this._outstandingMarkers.length) {
this._buildMarker(this._fragment, null);
}
// Otherwise prepare flushing the outstanding markers after a small delay.
else {
let delay = WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY;
let func = () => this._buildOutstandingMarkers(parent);
this._setNamedTimeout("flush-outstanding-markers", delay, func);
}
parent.appendChild(this._fragment);
},
/**
* Finishes building the outstanding markers in this view.
* @see Waterfall.prototype._buildMarkers
*/
_buildOutstandingMarkers: function(parent) {
if (!this._outstandingMarkers.length) {
return;
}
for (let args of this._outstandingMarkers) {
this._buildMarker.apply(this, args);
}
this._outstandingMarkers.length = 0;
parent.appendChild(this._fragment);
this.selectRow(this._selectedRowIdx);
},
/**
* Creates a single marker in this view.
*
* @param nsIDOMNode parent
* The parent node holding the marker.
* @param object marker
* The { name, start, end } marker in the data source.
* @param startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
* @param number markerIdx
* Index of the marker in this._markers
* @param number rowIdx
* Index of current row
*/
_buildMarker: function(parent, marker, startTime, dataScale, markerIdx, rowIdx) {
let container = this._document.createElement("hbox");
container.setAttribute("markerIdx", markerIdx);
container.className = "waterfall-marker-container";
if (marker) {
this._buildMarkerSidebar(container, marker);
this._buildMarkerWaterfall(container, marker, startTime, dataScale, markerIdx);
container.onclick = () => this.selectRow(rowIdx);
} else {
this._buildMarkerSpacer(container);
container.setAttribute("flex", "1");
container.setAttribute("is-spacer", "");
}
parent.appendChild(container);
},
/**
* Select first row.
*/
resetSelection: function() {
this.selectRow(0);
},
/**
* Select a marker in the waterfall.
*
* @param number idx
* Index of the row to select. -1 clears the selection.
*/
selectRow: function(idx) {
let prev = this._listContents.children[this._selectedRowIdx];
if (prev) {
prev.classList.remove("selected");
}
this._selectedRowIdx = idx;
let row = this._listContents.children[idx];
if (row && !row.hasAttribute("is-spacer")) {
row.focus();
row.classList.add("selected");
let markerIdx = row.getAttribute("markerIdx");
this.emit("selected", this._markers[markerIdx]);
this.ensureRowIsVisible(row);
} else {
this.emit("unselected");
}
},
/**
* Find a valid row to select.
*
* @param number idx
* Index of the row to select.
*/
selectNearestRow: function(idx) {
if (this._listContents.children.length == 0) {
return;
}
idx = Math.max(idx, 0);
idx = Math.min(idx, this._listContents.children.length - 1);
let row = this._listContents.children[idx];
if (row && row.hasAttribute("is-spacer")) {
if (idx > 0) {
return this.selectNearestRow(idx - 1);
} else {
return;
}
}
this.selectRow(idx);
},
/**
* Scroll waterfall to ensure row is in the viewport.
*
* @param number idx
* Index of the row to select.
*/
ensureRowIsVisible: function(row) {
let parent = row.parentNode;
let parentRect = parent.getBoundingClientRect();
let rowRect = row.getBoundingClientRect();
let yDelta = rowRect.top - parentRect.top;
if (yDelta < 0) {
parent.scrollTop += yDelta;
}
yDelta = parentRect.bottom - rowRect.bottom;
if (yDelta < 0) {
parent.scrollTop -= yDelta;
}
},
/**
* Creates the sidebar part of a marker in this view.
*
* @param nsIDOMNode container
* The container node representing the marker in this view.
* @param object marker
* @see Waterfall.prototype._buildMarker
*/
_buildMarkerSidebar: function(container, marker) {
let blueprint = this._blueprint[marker.name];
let sidebar = this._document.createElement("hbox");
sidebar.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
let bullet = this._document.createElement("hbox");
bullet.className = `waterfall-marker-bullet marker-color-${blueprint.colorName}`;
bullet.setAttribute("type", marker.name);
sidebar.appendChild(bullet);
let name = this._document.createElement("label");
name.setAttribute("crop", "end");
name.setAttribute("flex", "1");
name.className = "plain waterfall-marker-name";
let label = MarkerUtils.getMarkerLabel(marker);
name.setAttribute("value", label);
name.setAttribute("tooltiptext", label);
sidebar.appendChild(name);
container.appendChild(sidebar);
},
/**
* Creates the waterfall part of a marker in this view.
*
* @param nsIDOMNode container
* The container node representing the marker.
* @param object marker
* @see Waterfall.prototype._buildMarker
* @param startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_buildMarkerWaterfall: function(container, marker, startTime, dataScale) {
let blueprint = this._blueprint[marker.name];
let waterfall = this._document.createElement("hbox");
waterfall.className = "waterfall-marker-item waterfall-background-ticks";
waterfall.setAttribute("align", "center");
waterfall.setAttribute("flex", "1");
let start = (marker.start - startTime) * dataScale;
let width = (marker.end - marker.start) * dataScale;
let offset = this._isRTL ? this._waterfallWidth : 0;
let bar = this._document.createElement("hbox");
bar.className = `waterfall-marker-bar marker-color-${blueprint.colorName}`;
bar.style.transform = "translateX(" + (start - offset) + "px)";
bar.setAttribute("type", marker.name);
bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_BAR_WIDTH_MIN));
waterfall.appendChild(bar);
container.appendChild(waterfall);
},
/**
* Creates a dummy spacer as an empty marker.
*
* @param nsIDOMNode container
* The container node representing the marker.
*/
_buildMarkerSpacer: function(container) {
let sidebarSpacer = this._document.createElement("spacer");
sidebarSpacer.className = "waterfall-sidebar theme-sidebar";
sidebarSpacer.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
let waterfallSpacer = this._document.createElement("spacer");
waterfallSpacer.className = "waterfall-marker-item waterfall-background-ticks";
waterfallSpacer.setAttribute("flex", "1");
container.appendChild(sidebarSpacer);
container.appendChild(waterfallSpacer);
},
/**
* Creates the background displayed on the marker's waterfall.
*
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_drawWaterfallBackground: function(dataScale) {
if (!this._canvas || !this._ctx) {
this._canvas = this._document.createElementNS(HTML_NS, "canvas");
this._ctx = this._canvas.getContext("2d");
}
let canvas = this._canvas;
let ctx = this._ctx;
// Nuke the context.
let canvasWidth = canvas.width = this._waterfallWidth;
let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
// Start over.
let imageData = ctx.createImageData(canvasWidth, canvasHeight);
let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length);
let view8bit = new Uint8ClampedArray(buf);
let view32bit = new Uint32Array(buf);
// Build new millisecond tick lines...
let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
let tickInterval = this._findOptimalTickInterval({
ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
dataScale: dataScale
});
// Insert one pixel for each division on each scale.
for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
let increment = tickInterval * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) {
let position = x | 0;
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
}
// Flush the image data and cache the waterfall background.
pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0);
this._document.mozSetImageElement("waterfall-background", canvas);
},
/**
* Finds the optimal tick interval between time markers in this timeline.
*
* @param number ticksMultiple
* @param number ticksSpacingMin
* @param number dataScale
* @return number
*/
_findOptimalTickInterval: function({ ticksMultiple, ticksSpacingMin, dataScale }) {
let timingStep = ticksMultiple;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > ticksSpacingMin) {
return dataScale;
}
while (true) {
let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < ticksSpacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
},
/**
* Returns true if this is document is in RTL mode.
* @return boolean
*/
_getRTL: function() {
let win = this._document.defaultView;
let doc = this._document.documentElement;
return win.getComputedStyle(doc, null).direction == "rtl";
}
};
/**
* Checks if a given marker is in the specified time range.
*
* @param object e
* The marker containing the { start, end } timestamps.
* @param number start
* The earliest allowed time.
* @param number end
* The latest allowed time.
* @return boolean
* True if the marker fits inside the specified time range.
*/
function isMarkerInRange(e, start, end) {
return (e.start >= start && e.end <= end) || // bounds inside
(e.start < start && e.end > end) || // bounds outside
(e.start < start && e.end >= start && e.end <= end) || // overlap start
(e.end > end && e.start >= start && e.start <= end); // overlap end
}
exports.Waterfall = Waterfall;

View File

@ -17,9 +17,10 @@ EXTRA_JS_MODULES.devtools.performance += [
'modules/logic/tree-model.js',
'modules/widgets/graphs.js',
'modules/widgets/marker-details.js',
'modules/widgets/marker-view.js',
'modules/widgets/markers-overview.js',
'modules/widgets/tree-view.js',
'modules/widgets/waterfall.js',
'modules/widgets/waterfall-ticks.js',
'panel.js'
]

View File

@ -25,8 +25,10 @@ loader.lazyRequireGetter(this, "RecordingModel",
"devtools/performance/recording-model", true);
loader.lazyRequireGetter(this, "GraphsController",
"devtools/performance/graphs", true);
loader.lazyRequireGetter(this, "Waterfall",
"devtools/performance/waterfall", true);
loader.lazyRequireGetter(this, "WaterfallHeader",
"devtools/performance/waterfall-ticks", true);
loader.lazyRequireGetter(this, "MarkerView",
"devtools/performance/marker-view", true);
loader.lazyRequireGetter(this, "MarkerDetails",
"devtools/performance/marker-details", true);
loader.lazyRequireGetter(this, "MarkerUtils",

View File

@ -241,12 +241,13 @@
<!-- Waterfall -->
<hbox id="waterfall-view" flex="1">
<vbox flex="1">
<hbox id="waterfall-header" />
<vbox id="waterfall-breakdown" flex="1" />
</vbox>
<splitter class="devtools-side-splitter"/>
<vbox id="waterfall-details"
class="theme-sidebar"
width="150"
height="150"/>
class="theme-sidebar"/>
</hbox>
<!-- JS Tree and JIT view -->

View File

@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const MARKER_DETAILS_WIDTH = 300;
const MARKER_DETAILS_WIDTH = 200;
/**
* Waterfall view containing the timeline markers, controlled by DetailsView.
@ -26,24 +26,21 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
initialize: function () {
DetailsSubview.initialize.call(this);
// TODO bug 1167093 save the previously set width, and ensure minimum width
$("#waterfall-details").setAttribute("width", MARKER_DETAILS_WIDTH);
this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"));
this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
this._onMarkerSelected = this._onMarkerSelected.bind(this);
this._onResize = this._onResize.bind(this);
this._onViewSource = this._onViewSource.bind(this);
this.waterfall.on("selected", this._onMarkerSelected);
this.waterfall.on("unselected", this._onMarkerSelected);
this.headerContainer = $("#waterfall-header");
this.breakdownContainer = $("#waterfall-breakdown");
this.detailsContainer = $("#waterfall-details");
this.detailsSplitter = $("#waterfall-view > splitter");
this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
this.details.on("resize", this._onResize);
this.details.on("view-source", this._onViewSource);
let blueprint = PerformanceController.getTimelineBlueprint();
this.waterfall.setBlueprint(blueprint);
this.waterfall.recalculateBounds();
// TODO bug 1167093 save the previously set width, and ensure minimum width
this.details.width = MARKER_DETAILS_WIDTH;
},
/**
@ -52,8 +49,6 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
destroy: function () {
DetailsSubview.destroy.call(this);
this.waterfall.off("selected", this._onMarkerSelected);
this.waterfall.off("unselected", this._onMarkerSelected);
this.details.off("resize", this._onResize);
this.details.off("view-source", this._onViewSource);
},
@ -69,7 +64,9 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
let startTime = interval.startTime || 0;
let endTime = interval.endTime || recording.getDuration();
let markers = recording.getMarkers();
this.waterfall.setData({ markers, interval: { startTime, endTime } });
let rootMarkerNode = this._prepareWaterfallTree(markers);
this._populateWaterfallTree(rootMarkerNode, { startTime, endTime });
this.emit(EVENTS.WATERFALL_RENDERED);
},
@ -79,11 +76,6 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
*/
_onMarkerSelected: function (event, marker) {
let recording = PerformanceController.getCurrentRecording();
// Race condition in tests due to lazy rendering of markers in the
// waterfall? intermittent bug 1157523
if (!recording) {
return;
}
let frames = recording.getFrames();
if (event === "selected") {
@ -98,7 +90,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
* Called when the marker details view is resized.
*/
_onResize: function () {
this.waterfall.recalculateBounds();
this._markersRoot.recalculateBounds();
this.render();
},
@ -107,7 +99,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
*/
_onObservedPrefChange: function(_, prefName) {
let blueprint = PerformanceController.getTimelineBlueprint();
this.waterfall.setBlueprint(blueprint);
this._markersRoot.blueprint = blueprint;
},
/**
@ -117,5 +109,42 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
gToolbox.viewSourceInDebugger(file, line);
},
/**
* Called when the recording is stopped and prepares data to
* populate the waterfall tree.
*/
_prepareWaterfallTree: function(markers) {
},
/**
* Renders the waterfall tree.
*/
_populateWaterfallTree: function(rootMarkerNode, interval) {
let root = new MarkerView({
marker: rootMarkerNode,
// The root node is irrelevant in a waterfall tree.
hidden: true,
// The waterfall tree should not expand by default.
autoExpandDepth: 0
});
let header = new WaterfallHeader(root);
this._markersRoot = root;
this._waterfallHeader = header;
let blueprint = PerformanceController.getTimelineBlueprint();
root.blueprint = blueprint;
root.interval = interval;
root.on("selected", this._onMarkerSelected);
root.on("unselected", this._onMarkerSelected);
this.breakdownContainer.innerHTML = "";
root.attachTo(this.breakdownContainer);
this.headerContainer.innerHTML = "";
header.attachTo(this.headerContainer);
},
toString: () => "[object WaterfallView]"
});

View File

@ -5,12 +5,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cu = Components.utils;
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://gre/modules/devtools/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
this.EXPORTED_SYMBOLS = ["AbstractTreeItem"];
@ -117,14 +121,13 @@ function AbstractTreeItem({ parent, level }) {
this._parentItem = parent;
this._level = level || 0;
this._childTreeItems = [];
this._onArrowClick = this._onArrowClick.bind(this);
this._onClick = this._onClick.bind(this);
this._onDoubleClick = this._onDoubleClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._onFocus = this._onFocus.bind(this);
// Events are always propagated through the root item. Decorating every
// tree item as an event emitter is a very costly operation.
if (this == this._rootItem) {
EventEmitter.decorate(this);
}
}
AbstractTreeItem.prototype = {
_containerNode: null,
@ -150,7 +153,8 @@ AbstractTreeItem.prototype = {
* @return nsIDOMNode
*/
_displaySelf: function(document, arrowNode) {
throw "This method needs to be implemented by inheriting classes.";
throw new Error(
"The `_displaySelf` method needs to be implemented by inheriting classes.");
},
/**
@ -162,7 +166,16 @@ AbstractTreeItem.prototype = {
* @param array:AbstractTreeItem children
*/
_populateSelf: function(children) {
throw "This method needs to be implemented by inheriting classes.";
throw new Error(
"The `_populateSelf` method needs to be implemented by inheriting classes.");
},
/**
* Gets the this tree's owner document.
* @return Document
*/
get document() {
return this._containerNode.ownerDocument;
},
/**
@ -221,18 +234,36 @@ AbstractTreeItem.prototype = {
return this._expanded;
},
/**
* Gets the bounds for this tree's container without flushing.
* @return object
*/
get bounds() {
let win = this.document.defaultView;
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
return utils.getBoundsWithoutFlushing(this._containerNode);
},
/**
* Creates and appends this tree item to the specified parent element.
*
* @param nsIDOMNode containerNode
* The parent element for this tree item (and every other tree item).
* @param nsIDOMNode beforeNode
* The child element which should succeed this tree item.
* @param nsIDOMNode fragmentNode [optional]
* An optional document fragment temporarily holding this tree item in
* the current batch. Defaults to the `containerNode`.
* @param nsIDOMNode beforeNode [optional]
* An optional child element which should succeed this tree item.
*/
attachTo: function(containerNode, beforeNode = null) {
attachTo: function(containerNode, fragmentNode = containerNode, beforeNode = null) {
this._containerNode = containerNode;
this._constructTargetNode();
containerNode.insertBefore(this._targetNode, beforeNode);
if (beforeNode) {
fragmentNode.insertBefore(this._targetNode, beforeNode);
} else {
fragmentNode.appendChild(this._targetNode);
}
if (this._level < this.autoExpandDepth) {
this.expand();
@ -265,6 +296,7 @@ AbstractTreeItem.prototype = {
}
this._expanded = true;
this._arrowNode.setAttribute("open", "");
this._targetNode.setAttribute("expanded", "");
this._toggleChildren(true);
this._rootItem.emit("expand", this);
},
@ -278,6 +310,7 @@ AbstractTreeItem.prototype = {
}
this._expanded = false;
this._arrowNode.removeAttribute("open");
this._targetNode.removeAttribute("expanded", "");
this._toggleChildren(false);
this._rootItem.emit("collapse", this);
},
@ -315,17 +348,16 @@ AbstractTreeItem.prototype = {
* Shows all children of this item in the tree.
*/
_showChildren: function() {
let childTreeItems = this._childTreeItems;
let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
let nextNode = this._getSiblingAtDelta(1);
// First append the child items, and afterwards append any descendants.
// Otherwise, the tree will become garbled and nodes will intertwine.
for (let item of childTreeItems) {
item.attachTo(this._containerNode, nextNode);
// If this is the root item and we're not expanding any child nodes,
// it is safe to append everything at once.
if (this == this._rootItem && this.autoExpandDepth == 0) {
this._appendChildrenBatch();
}
for (let item of expandedChildTreeItems) {
item._showChildren();
// Otherwise, append the child items and their descendants successively;
// if not, the tree will become garbled and nodes will intertwine,
// since all the tree items are sharing a single container node.
else {
this._appendChildrenSuccessive();
}
},
@ -339,6 +371,40 @@ AbstractTreeItem.prototype = {
}
},
/**
* Appends all children in a single batch.
* This only works properly for root nodes when no child nodes will expand.
*/
_appendChildrenBatch: function() {
if (this._fragment === undefined) {
this._fragment = this.document.createDocumentFragment();
}
let childTreeItems = this._childTreeItems;
for (let i = 0, len = childTreeItems.length; i < len; i++) {
childTreeItems[i].attachTo(this._containerNode, this._fragment);
}
this._containerNode.appendChild(this._fragment);
},
/**
* Appends all children successively.
*/
_appendChildrenSuccessive: function() {
let childTreeItems = this._childTreeItems;
let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
let nextNode = this._getSiblingAtDelta(1);
for (let i = 0, len = childTreeItems.length; i < len; i++) {
childTreeItems[i].attachTo(this._containerNode, undefined, nextNode);
}
for (let i = 0, len = expandedChildTreeItems.length; i < len; i++) {
expandedChildTreeItems[i]._showChildren();
}
},
/**
* Constructs and stores the target node displaying this tree item.
*/
@ -346,7 +412,14 @@ AbstractTreeItem.prototype = {
if (this._constructed) {
return;
}
let document = this._containerNode.ownerDocument;
this._onArrowClick = this._onArrowClick.bind(this);
this._onClick = this._onClick.bind(this);
this._onDoubleClick = this._onDoubleClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._onFocus = this._onFocus.bind(this);
this._onBlur = this._onBlur.bind(this);
let document = this.document;
let arrowNode = this._arrowNode = document.createElement("hbox");
arrowNode.className = "arrow theme-twisty";
@ -359,6 +432,7 @@ AbstractTreeItem.prototype = {
targetNode.addEventListener("dblclick", this._onDoubleClick);
targetNode.addEventListener("keypress", this._onKeyPress);
targetNode.addEventListener("focus", this._onFocus);
targetNode.addEventListener("blur", this._onBlur);
this._constructed = true;
},
@ -434,7 +508,6 @@ AbstractTreeItem.prototype = {
if (!e.target.classList.contains("arrow")) {
this._onArrowClick(e);
}
this.focus();
},
@ -477,5 +550,12 @@ AbstractTreeItem.prototype = {
*/
_onFocus: function(e) {
this._rootItem.emit("focus", this);
},
/**
* Handler for the "blur" event on the element displaying this tree item.
*/
_onBlur: function(e) {
this._rootItem.emit("blur", this);
}
};

View File

@ -6,6 +6,7 @@
/* CSS Variables specific to this panel that aren't defined by the themes */
.theme-dark {
--cell-border-color: rgba(255,255,255,0.15);
--cell-border-color-light: rgba(255,255,255,0.1);
--focus-cell-border-color: rgba(255,255,255,0.5);
--row-alt-background-color: rgba(29,79,115,0.15);
--row-hover-background-color: rgba(29,79,115,0.25);
@ -13,6 +14,7 @@
.theme-light {
--cell-border-color: rgba(0,0,0,0.15);
--cell-border-color-light: rgba(0,0,0,0.1);
--focus-cell-border-color: rgba(0,0,0,0.3);
--row-alt-background-color: rgba(76,158,217,0.1);
--row-hover-background-color: rgba(76,158,217,0.2);
@ -183,8 +185,6 @@
/* Profile call tree */
.call-tree-cells-container {
/* Hack: force hardware acceleration */
transform: translateZ(1px);
overflow: auto;
}
@ -256,7 +256,7 @@
background-color: var(--theme-tab-toolbar-background);
}
.call-tree-item:last-child:not(:focus) {
.call-tree-item:last-child {
border-bottom: 1px solid var(--cell-border-color);
}
@ -272,7 +272,7 @@
background-color: var(--theme-selection-background);
}
.call-tree-item:focus label {
.call-tree-item:focus description {
color: var(--theme-selection-color) !important;
}
@ -325,64 +325,13 @@
}
/**
* Details Waterfall Styles
* Waterfall ticks header
*/
#waterfall-breakdown {
overflow: auto;
}
.waterfall-list-contents {
/* Hack: force hardware acceleration */
transform: translateZ(1px);
overflow-x: hidden;
overflow-y: auto;
}
.waterfall-header-contents {
overflow-x: hidden;
}
.waterfall-background-ticks {
/* Background created on a <canvas> in js. */
/* @see browser/devtools/timeline/widgets/waterfall.js */
background-image: -moz-element(#waterfall-background);
background-repeat: repeat-y;
background-position: -1px center;
}
.waterfall-marker-container[is-spacer] {
pointer-events: none;
}
.theme-dark .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
background-color: rgba(255,255,255,0.03);
}
.theme-light .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
background-color: rgba(128,128,128,0.03);
}
.theme-dark .waterfall-marker-container:hover {
background-color: rgba(255,255,255,0.1) !important;
}
.theme-light .waterfall-marker-container:hover {
background-color: rgba(128,128,128,0.1) !important;
}
.waterfall-marker-item {
.waterfall-header-ticks {
overflow: hidden;
}
.waterfall-sidebar {
-moz-border-end: 1px solid var(--theme-splitter-color);
}
.waterfall-marker-container:hover > .waterfall-sidebar {
background-color: transparent;
}
.waterfall-header-name {
padding: 2px 4px;
font-size: 90%;
@ -399,6 +348,104 @@
-moz-margin-start: -100px !important; /* Don't affect layout. */
}
.waterfall-background-ticks {
/* Background created on a <canvas> in js. */
/* @see browser/devtools/timeline/widgets/waterfall.js */
background-image: -moz-element(#waterfall-background);
background-repeat: repeat-y;
background-position: -1px center;
}
/**
* Markers waterfall breakdown
*/
#waterfall-breakdown {
overflow-x: hidden;
overflow-y: auto;
}
.theme-light .waterfall-tree-item:not([level="0"]) {
background-image: repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 2px,
rgba(0,0,0,0.025) 2px,
rgba(0,0,0,0.025) 4px
);
}
.theme-dark .waterfall-tree-item:not([level="0"]) {
background-image: repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 2px,
rgba(255,255,255,0.05) 2px,
rgba(255,255,255,0.05) 4px
);
}
.theme-light .waterfall-tree-item[expandable] .waterfall-marker-bullet,
.theme-light .waterfall-tree-item[expandable] .waterfall-marker-bar {
background-image: repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 5px,
rgba(255,255,255,0.35) 5px,
rgba(255,255,255,0.35) 10px
);
}
.theme-dark .waterfall-tree-item[expandable] .waterfall-marker-bullet,
.theme-dark .waterfall-tree-item[expandable] .waterfall-marker-bar {
background-image: repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 5px,
rgba(0,0,0,0.35) 5px,
rgba(0,0,0,0.35) 10px
);
}
.waterfall-tree-item[expanded],
.waterfall-tree-item:not([level="0"]) + .waterfall-tree-item[level="0"] {
box-shadow: 0 -1px var(--cell-border-color-light);
}
.waterfall-tree-item:nth-child(2n) > .waterfall-marker {
background-color: var(--row-alt-background-color);
}
.waterfall-tree-item:hover {
background-color: var(--row-hover-background-color);
}
.waterfall-tree-item:last-child {
border-bottom: 1px solid var(--cell-border-color);
}
.waterfall-tree-item:focus {
background-color: var(--theme-selection-background);
}
.waterfall-tree-item:focus description {
color: var(--theme-selection-color) !important;
}
/**
* Marker left sidebar
*/
.waterfall-sidebar {
-moz-border-end: 1px solid var(--cell-border-color);
}
.waterfall-tree-item > .waterfall-sidebar:hover,
.waterfall-tree-item:hover > .waterfall-sidebar,
.waterfall-tree-item:focus > .waterfall-sidebar {
background: transparent;
}
.waterfall-marker-bullet {
width: 8px;
height: 8px;
@ -412,18 +459,30 @@
padding-bottom: 1px !important;
}
/**
* Marker timebar
*/
.waterfall-marker {
overflow: hidden;
}
.waterfall-marker-bar {
height: 9px;
transform-origin: left center;
border-radius: 1px;
}
.waterfall-marker-container.selected > .waterfall-sidebar,
.waterfall-marker-container.selected > .waterfall-marker-item {
background-color: var(--theme-selection-background);
color: var(--theme-selection-color);
.waterfall-marker > .theme-twisty {
/* Don't affect layout. */
width: 14px;
-moz-margin-end: -14px;
}
/**
* Marker details view
*/
#waterfall-details {
-moz-padding-start: 8px;
-moz-padding-end: 8px;
@ -432,12 +491,33 @@
min-width: 50px;
}
#waterfall-details > * {
padding-top: 3px;
}
.marker-details-bullet {
width: 8px;
height: 8px;
border-radius: 1px;
}
.marker-details-labelname {
-moz-padding-end: 4px;
}
.marker-details-type {
font-size: 1.2em;
font-weight: bold;
}
.marker-details-duration {
font-weight: bold;
}
/**
* Marker colors
*/
menuitem.marker-color-graphs-purple:before,
.marker-color-graphs-purple {
background-color: var(--theme-graphs-purple);
@ -463,23 +543,6 @@ menuitem.marker-color-graphs-blue:before,
background-color: var(--theme-graphs-blue);
}
#waterfall-details > * {
padding-top: 3px;
}
.marker-details-labelname {
-moz-padding-end: 4px;
}
.marker-details-type {
font-size: 1.2em;
font-weight: bold;
}
.marker-details-duration {
font-weight: bold;
}
/**
* JIT View
*/