mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-22 09:45:41 +00:00
Bug 1168125 - Replace the waterfall view with a tree, r=jsantell
This commit is contained in:
parent
921789e0e7
commit
04cbd3898d
@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
305
browser/devtools/performance/modules/widgets/marker-view.js
Normal file
305
browser/devtools/performance/modules/widgets/marker-view.js
Normal 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;
|
@ -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
|
||||
|
@ -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);
|
||||
|
187
browser/devtools/performance/modules/widgets/waterfall-ticks.js
Normal file
187
browser/devtools/performance/modules/widgets/waterfall-ticks.js
Normal 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 };
|
@ -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;
|
@ -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'
|
||||
]
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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 -->
|
||||
|
@ -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]"
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user