--- a/devtools/client/performance/components/moz.build
+++ b/devtools/client/performance/components/moz.build
@@ -3,11 +3,15 @@
# 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/.
DevToolsModules(
'jit-optimizations-item.js',
'jit-optimizations.js',
'recording-button.js',
'recording-controls.js',
+ 'waterfall-header.js',
+ 'waterfall-tree-row.js',
+ 'waterfall-tree.js',
+ 'waterfall.js',
)
MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-header.js
@@ -0,0 +1,69 @@
+/* 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";
+
+/**
+ * The "waterfall ticks" view, a header for the markers displayed in the waterfall.
+ */
+
+const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../modules/global");
+const { TickUtils } = require("../modules/waterfall-ticks");
+
+// ms
+const WATERFALL_HEADER_TICKS_MULTIPLE = 5;
+// px
+const WATERFALL_HEADER_TICKS_SPACING_MIN = 50;
+// px
+const WATERFALL_HEADER_TEXT_PADDING = 3;
+
+function WaterfallHeader(props) {
+ let { startTime, dataScale, sidebarWidth, waterfallWidth } = props;
+
+ let tickInterval = TickUtils.findOptimalTickInterval({
+ ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
+ ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
+ dataScale: dataScale
+ });
+
+ let ticks = [];
+ 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 = dom.div({
+ className: "plain waterfall-header-tick",
+ style: { transform: `translateX(${left}px)` }
+ }, label);
+ ticks.push(node);
+ }
+
+ return dom.div(
+ { className: "waterfall-header" },
+ dom.div(
+ {
+ className: "waterfall-sidebar theme-sidebar waterfall-header-name",
+ style: { width: sidebarWidth + "px" }
+ },
+ L10N.getStr("timeline.records")
+ ),
+ dom.div(
+ { className: "waterfall-header-ticks waterfall-background-ticks" },
+ ticks
+ )
+ );
+}
+
+WaterfallHeader.displayName = "WaterfallHeader";
+
+WaterfallHeader.propTypes = {
+ startTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ waterfallWidth: PropTypes.number.isRequired,
+};
+
+module.exports = WaterfallHeader;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-tree-row.js
@@ -0,0 +1,107 @@
+/* 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";
+
+/**
+ * A single row (node) in the waterfall tree
+ */
+
+const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+const { MarkerBlueprintUtils } = require("../modules/marker-blueprint-utils");
+
+// px
+const LEVEL_INDENT = 10;
+// px
+const ARROW_NODE_OFFSET = -14;
+// px
+const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5;
+
+function buildMarkerSidebar(blueprint, props) {
+ const { marker, level, sidebarWidth } = props;
+
+ let bullet = dom.div({
+ className: `waterfall-marker-bullet marker-color-${blueprint.colorName}`,
+ style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
+ "data-type": marker.name
+ });
+
+ let label = MarkerBlueprintUtils.getMarkerLabel(marker);
+
+ let name = dom.div({
+ className: "plain waterfall-marker-name",
+ style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
+ title: label
+ }, label);
+
+ return dom.div({
+ className: "waterfall-sidebar theme-sidebar",
+ style: { width: sidebarWidth + "px" }
+ }, bullet, name);
+}
+
+function buildMarkerTimebar(blueprint, props) {
+ const { marker, startTime, dataScale, arrow } = props;
+ const offset = (marker.start - startTime) * dataScale + ARROW_NODE_OFFSET;
+ const width = Math.max((marker.end - marker.start) * dataScale,
+ WATERFALL_MARKER_TIMEBAR_WIDTH_MIN);
+
+ let bar = dom.div(
+ {
+ className: "waterfall-marker-wrap",
+ style: { transform: `translateX(${offset}px)` }
+ },
+ arrow,
+ dom.div({
+ className: `waterfall-marker-bar marker-color-${blueprint.colorName}`,
+ style: { width: `${width}px` },
+ "data-type": marker.name
+ })
+ );
+
+ return dom.div(
+ { className: "waterfall-marker waterfall-background-ticks" },
+ bar
+ );
+}
+
+function WaterfallTreeRow(props) {
+ const { marker, focused } = props;
+ const blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
+
+ let attrs = {
+ className: "waterfall-tree-item" + (focused ? " focused" : ""),
+ "data-otmt": marker.isOffMainThread
+ };
+
+ // Don't render an expando-arrow for leaf nodes.
+ let submarkers = marker.submarkers;
+ let hasDescendants = submarkers && submarkers.length > 0;
+ if (hasDescendants) {
+ attrs["data-expandable"] = "";
+ } else {
+ attrs["data-invisible"] = "";
+ }
+
+ return dom.div(
+ attrs,
+ buildMarkerSidebar(blueprint, props),
+ buildMarkerTimebar(blueprint, props)
+ );
+}
+
+WaterfallTreeRow.displayName = "WaterfallTreeRow";
+
+WaterfallTreeRow.propTypes = {
+ marker: PropTypes.object.isRequired,
+ level: PropTypes.number.isRequired,
+ arrow: PropTypes.element.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ focused: PropTypes.bool.isRequired,
+ startTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+};
+
+module.exports = WaterfallTreeRow;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-tree.js
@@ -0,0 +1,167 @@
+/* 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";
+
+const { createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const WaterfallTreeRow = createFactory(require("./waterfall-tree-row"));
+
+// px - keep in sync with var(--waterfall-tree-row-height) in performance.css
+const WATERFALL_TREE_ROW_HEIGHT = 15;
+
+/**
+ * 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 mStart = e.start | 0;
+ let mEnd = e.end | 0;
+
+ return (
+ // bounds inside
+ (mStart >= start && mEnd <= end) ||
+ // bounds outside
+ (mStart < start && mEnd > end) ||
+ // overlap start
+ (mStart < start && mEnd >= start && mEnd <= end) ||
+ // overlap end
+ (mEnd > end && mStart >= start && mStart <= end)
+ );
+}
+
+const WaterfallTree = createClass({
+ displayName: "WaterfallTree",
+
+ propTypes: {
+ marker: PropTypes.object.isRequired,
+ startTime: PropTypes.number.isRequired,
+ endTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ waterfallWidth: PropTypes.number.isRequired,
+ onFocus: PropTypes.func,
+ },
+
+ getInitialState() {
+ return {
+ focused: null,
+ expanded: new Set()
+ };
+ },
+
+ _getRoots(node) {
+ let roots = this.props.marker.submarkers || [];
+ return roots.filter(this._filter);
+ },
+
+ /**
+ * Find the parent node of 'node' with a depth-first search of the marker tree
+ */
+ _getParent(node) {
+ function findParent(marker) {
+ if (marker.submarkers) {
+ for (let submarker of marker.submarkers) {
+ if (submarker === node) {
+ return marker;
+ }
+
+ let parent = findParent(submarker);
+ if (parent) {
+ return parent;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ let rootMarker = this.props.marker;
+ let parent = findParent(rootMarker);
+
+ // We are interested only in parent markers that are rendered,
+ // which rootMarker is not. Return null if the parent is rootMarker.
+ return parent !== rootMarker ? parent : null;
+ },
+
+ _getChildren(node) {
+ let submarkers = node.submarkers || [];
+ return submarkers.filter(this._filter);
+ },
+
+ _getKey(node) {
+ return `marker-${node.index}`;
+ },
+
+ _isExpanded(node) {
+ return this.state.expanded.has(node);
+ },
+
+ _onExpand(node) {
+ this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.add(node);
+ return { expanded };
+ });
+ },
+
+ _onCollapse(node) {
+ this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.delete(node);
+ return { expanded };
+ });
+ },
+
+ _onFocus(node) {
+ this.setState({ focused: node });
+ if (this.props.onFocus) {
+ this.props.onFocus(node);
+ }
+ },
+
+ _filter(node) {
+ let { startTime, endTime } = this.props;
+ return isMarkerInRange(node, startTime, endTime);
+ },
+
+ _renderItem(marker, level, focused, arrow, expanded) {
+ let { startTime, dataScale, sidebarWidth } = this.props;
+ return WaterfallTreeRow({
+ marker,
+ level,
+ arrow,
+ expanded,
+ focused,
+ startTime,
+ dataScale,
+ sidebarWidth
+ });
+ },
+
+ render() {
+ return Tree({
+ getRoots: this._getRoots,
+ getParent: this._getParent,
+ getChildren: this._getChildren,
+ getKey: this._getKey,
+ isExpanded: this._isExpanded,
+ onExpand: this._onExpand,
+ onCollapse: this._onCollapse,
+ onFocus: this._onFocus,
+ renderItem: this._renderItem,
+ focused: this.state.focused,
+ itemHeight: WATERFALL_TREE_ROW_HEIGHT
+ });
+ }
+});
+
+module.exports = WaterfallTree;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/waterfall.js
@@ -0,0 +1,36 @@
+/* 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 { DOM: dom, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const WaterfallHeader = createFactory(require("./waterfall-header"));
+const WaterfallTree = createFactory(require("./waterfall-tree"));
+
+function Waterfall(props) {
+ return dom.div(
+ { className: "waterfall-markers" },
+ WaterfallHeader(props),
+ WaterfallTree(props)
+ );
+}
+
+Waterfall.displayName = "Waterfall";
+
+Waterfall.propTypes = {
+ marker: PropTypes.object.isRequired,
+ startTime: PropTypes.number.isRequired,
+ endTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ waterfallWidth: PropTypes.number.isRequired,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+};
+
+module.exports = Waterfall;
--- a/devtools/client/performance/modules/logic/waterfall-utils.js
+++ b/devtools/client/performance/modules/logic/waterfall-utils.js
@@ -46,22 +46,22 @@ function collapseMarkersIntoNode({ rootN
let parentNode = getCurrentParentNode();
let blueprint = MarkerBlueprintUtils.getBlueprintFor(curr);
let nestable = "nestable" in blueprint ? blueprint.nestable : true;
let collapsible = "collapsible" in blueprint ? blueprint.collapsible : true;
let finalized = false;
- // If this marker is collapsible, turn it into a parent marker.
- // If there are no children within it later, it will be turned
- // back into a normal node.
+ // Extend the marker with extra properties needed in the marker tree
+ let extendedProps = { index: i };
if (collapsible) {
- curr = createParentNode(curr);
+ extendedProps.submarkers = [];
}
+ curr = extend(curr, extendedProps);
// If not nestible, just push it inside the root node. Additionally,
// markers originating outside the main thread are considered to be
// "never collapsible", to avoid confusion.
// A beter solution would be to collapse every marker with its siblings
// from the same thread, but that would require a thread id attached
// to all markers, which is potentially expensive and rather useless at
// the moment, since we don't really have that many OTMT markers.
--- a/devtools/client/performance/modules/moz.build
+++ b/devtools/client/performance/modules/moz.build
@@ -13,9 +13,10 @@ DevToolsModules(
'constants.js',
'global.js',
'io.js',
'marker-blueprint-utils.js',
'marker-dom-utils.js',
'marker-formatters.js',
'markers.js',
'utils.js',
+ 'waterfall-ticks.js',
)
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/modules/waterfall-ticks.js
@@ -0,0 +1,98 @@
+/* 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";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+// ms
+const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
+const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+// px
+const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
+const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
+// byte
+const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
+// byte
+const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
+
+const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
+
+/**
+ * Creates the background displayed on the marker's waterfall.
+ */
+function drawWaterfallBackground(doc, dataScale, waterfallWidth) {
+ let canvas = doc.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+
+ // Nuke the context.
+ let canvasWidth = canvas.width = waterfallWidth;
+ // Awww yeah, 1px, repeats on Y axis.
+ let canvasHeight = canvas.height = 1;
+
+ // 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);
+
+ return 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.TickUtils = { findOptimalTickInterval, drawWaterfallBackground };
deleted file mode 100644
--- a/devtools/client/performance/modules/widgets/marker-view.js
+++ /dev/null
@@ -1,304 +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 "marker" view, essentially a detailed list
- * of all the markers in the timeline data.
- */
-
-const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
-const { AbstractTreeItem } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
-const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
-
-// px
-const LEVEL_INDENT = 10;
-// px
-const ARROW_NODE_OFFSET = -15;
-// px
-const WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS = 20;
-// px
-const WATERFALL_MARKER_SIDEBAR_WIDTH = 175;
-// px
-const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5;
-
-/**
- * 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
- - WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS;
- },
-
- /**
- * Sets a list of marker types to be filtered out of this view.
- * @param Array<String> filter
- */
- set filter(filter) {
- this.root._filter = filter;
- },
- get filter() {
- return this.root._filter;
- },
-
- /**
- * 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";
- targetNode.setAttribute("otmt", this.marker.isOffMainThread);
-
- 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 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];
-
- // Skip filtered markers
- if (!MarkerBlueprintUtils.shouldDisplayMarker(marker, this.filter)) {
- 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) {
- let marker = this.marker;
- let blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
- let startTime = this.root._interval.startTime;
- let endTime = this.root._interval.endTime;
-
- let sidebarCell = this._buildMarkerSidebar(doc, blueprint, marker);
- let timebarCell = this._buildMarkerTimebar(doc, blueprint, 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 = MarkerBlueprintUtils.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 mStart = e.start|0;
- let mEnd = e.end|0;
-
- return (
- // bounds inside
- (mStart >= start && mEnd <= end) ||
- // bounds outside
- (mStart < start && mEnd > end) ||
- // overlap start
- (mStart < start && mEnd >= start && mEnd <= end) ||
- // overlap end
- (mEnd > end && mStart >= start && mStart <= end)
- );
-}
-
-exports.MarkerView = MarkerView;
-exports.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS = WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS;
-exports.WATERFALL_MARKER_SIDEBAR_WIDTH = WATERFALL_MARKER_SIDEBAR_WIDTH;
--- a/devtools/client/performance/modules/widgets/markers-overview.js
+++ b/devtools/client/performance/modules/widgets/markers-overview.js
@@ -11,17 +11,17 @@
const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
const { colorUtils } = require("devtools/shared/css/color");
const { getColor } = require("devtools/client/shared/theme");
const ProfilerGlobal = require("devtools/client/performance/modules/global");
const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
-const { TickUtils } = require("devtools/client/performance/modules/widgets/waterfall-ticks");
+const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
const { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
// px
const OVERVIEW_HEADER_HEIGHT = 14;
// px
const OVERVIEW_ROW_HEIGHT = 11;
const OVERVIEW_SELECTION_LINE_COLOR = "#666";
--- a/devtools/client/performance/modules/widgets/moz.build
+++ b/devtools/client/performance/modules/widgets/moz.build
@@ -1,13 +1,11 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
'graphs.js',
'marker-details.js',
- 'marker-view.js',
'markers-overview.js',
'tree-view.js',
- 'waterfall-ticks.js',
)
deleted file mode 100644
--- a/devtools/client/performance/modules/widgets/waterfall-ticks.js
+++ /dev/null
@@ -1,193 +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 ticks" view, a header for the
- * markers displayed in the waterfall.
- */
-
-const { L10N } = require("devtools/client/performance/modules/global");
-const { WATERFALL_MARKER_SIDEBAR_WIDTH } = require("devtools/client/performance/modules/widgets/marker-view");
-
-const HTML_NS = "http://www.w3.org/1999/xhtml";
-
-const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
-
-// ms
-const WATERFALL_HEADER_TICKS_MULTIPLE = 5;
-// px
-const WATERFALL_HEADER_TICKS_SPACING_MIN = 50;
-// px
-const WATERFALL_HEADER_TEXT_PADDING = 3;
-
-// ms
-const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
-const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
-// px
-const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
-const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
-// byte
-const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
-// byte
-const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
-
-/**
- * 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;
- // Awww yeah, 1px, repeats on Y axis.
- let canvasHeight = canvas.height = 1;
-
- // 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 };
--- a/devtools/client/performance/performance-controller.js
+++ b/devtools/client/performance/performance-controller.js
@@ -22,43 +22,42 @@ var { gDevTools } = require("devtools/cl
var EVENTS = require("devtools/client/performance/events");
Object.defineProperty(this, "EVENTS", {
value: EVENTS,
enumerable: true,
writable: false
});
/* exported React, ReactDOM, JITOptimizationsView, RecordingControls, RecordingButton,
- Services, promise, EventEmitter, DevToolsUtils, system */
+ Waterfall, Services, promise, EventEmitter, DevToolsUtils, system */
var React = require("devtools/client/shared/vendor/react");
var ReactDOM = require("devtools/client/shared/vendor/react-dom");
+var Waterfall = React.createFactory(require("devtools/client/performance/components/waterfall"));
var JITOptimizationsView = React.createFactory(require("devtools/client/performance/components/jit-optimizations"));
var RecordingControls = React.createFactory(require("devtools/client/performance/components/recording-controls"));
var RecordingButton = React.createFactory(require("devtools/client/performance/components/recording-button"));
var Services = require("Services");
var promise = require("promise");
var EventEmitter = require("devtools/shared/event-emitter");
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
var flags = require("devtools/shared/flags");
var system = require("devtools/shared/system");
// Logic modules
/* exported L10N, PerformanceTelemetry, TIMELINE_BLUEPRINT, RecordingUtils,
- PerformanceUtils, OptimizationsGraph, GraphsController, WaterfallHeader, MarkerView,
+ PerformanceUtils, OptimizationsGraph, GraphsController,
MarkerDetails, MarkerBlueprintUtils, WaterfallUtils, FrameUtils, CallView, ThreadNode,
FrameNode */
var { L10N } = require("devtools/client/performance/modules/global");
var { PerformanceTelemetry } = require("devtools/client/performance/modules/logic/telemetry");
var { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
var RecordingUtils = require("devtools/shared/performance/recording-utils");
var PerformanceUtils = require("devtools/client/performance/modules/utils");
var { OptimizationsGraph, GraphsController } = require("devtools/client/performance/modules/widgets/graphs");
-var { WaterfallHeader } = require("devtools/client/performance/modules/widgets/waterfall-ticks");
-var { MarkerView } = require("devtools/client/performance/modules/widgets/marker-view");
var { MarkerDetails } = require("devtools/client/performance/modules/widgets/marker-details");
var { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
var WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
var FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
var { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
var { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
var { FrameNode } = require("devtools/client/performance/modules/logic/tree-model");
--- a/devtools/client/performance/performance.xul
+++ b/devtools/client/performance/performance.xul
@@ -245,20 +245,17 @@
value="&performanceUI.bufferStatusFull;"/>
</vbox>
<!-- Detail views -->
<deck id="details-pane" flex="1">
<!-- Waterfall -->
<hbox id="waterfall-view" flex="1">
- <vbox flex="1">
- <hbox id="waterfall-header" />
- <vbox id="waterfall-breakdown" flex="1" />
- </vbox>
+ <html:div xmlns="http://www.w3.org/1999/xhtml" id="waterfall-tree" />
<splitter class="devtools-side-splitter"/>
<vbox id="waterfall-details"
class="theme-sidebar"/>
</hbox>
<!-- JS Tree and JIT view -->
<hbox id="js-profile-view" flex="1">
<vbox id="js-calltree-view" flex="1">
--- a/devtools/client/performance/test/browser_timeline-waterfall-background.js
+++ b/devtools/client/performance/test/browser_timeline-waterfall-background.js
@@ -25,21 +25,17 @@ add_task(function* () {
// Ensure overview is rendering and some markers were received.
yield waitForOverviewRenderedWithMarkers(panel);
yield stopRecording(panel);
ok(true, "Recording has ended.");
// Test the waterfall background.
- ok(WaterfallView._waterfallHeader._canvas,
- "A canvas should be created after the recording ended.");
- ok(WaterfallView._waterfallHeader._ctx,
- "A 2d context should be created after the recording ended.");
+ ok(WaterfallView.canvas, "A canvas should be created after the recording ended.");
- is(WaterfallView._waterfallHeader._canvas.width,
- WaterfallView._markersRoot._waterfallWidth,
+ is(WaterfallView.canvas.width, WaterfallView.waterfallWidth,
"The canvas width is correct.");
- is(WaterfallView._waterfallHeader._canvas.height, 1,
+ is(WaterfallView.canvas.height, 1,
"The canvas height is correct.");
yield teardownToolboxAndRemoveTab(panel);
});
--- a/devtools/client/performance/test/browser_timeline-waterfall-generic.js
+++ b/devtools/client/performance/test/browser_timeline-waterfall-generic.js
@@ -1,17 +1,16 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests if the waterfall is properly built after finishing a recording.
*/
-const { WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS } = require("devtools/client/performance/modules/widgets/marker-view");
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
const { startRecording, stopRecording, waitForOverviewRenderedWithMarkers } = require("devtools/client/performance/test/helpers/actions");
const { once } = require("devtools/client/performance/test/helpers/event-utils");
add_task(function* () {
let { panel } = yield initPerformanceInNewTab({
url: SIMPLE_URL,
@@ -26,25 +25,23 @@ add_task(function* () {
// Ensure overview is rendering and some markers were received.
yield waitForOverviewRenderedWithMarkers(panel);
yield stopRecording(panel);
ok(true, "Recording has ended.");
// Test the header container.
- ok($(".waterfall-header-container"),
+ ok($(".waterfall-header"),
"A header container should have been created.");
// Test the header sidebar (left).
- ok($(".waterfall-header-container > .waterfall-sidebar"),
+ ok($(".waterfall-header > .waterfall-sidebar"),
"A header sidebar node should have been created.");
- ok($(".waterfall-header-container > .waterfall-sidebar > .waterfall-header-name"),
- "A header name label should have been created inside the sidebar.");
// Test the header ticks (right).
ok($(".waterfall-header-ticks"),
"A header ticks node should have been created.");
ok($$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
"Some header tick labels should have been created inside the tick node.");
@@ -56,55 +53,53 @@ add_task(function* () {
"Some marker color bullets should have been created inside the sidebar.");
ok($$(".waterfall-tree-item > .waterfall-sidebar > .waterfall-marker-name").length,
"Some marker name labels should have been created inside the sidebar.");
// Test the markers waterfall (right).
ok($$(".waterfall-tree-item > .waterfall-marker").length,
"Some marker waterfall nodes should have been created.");
- ok($$(".waterfall-tree-item > .waterfall-marker > .waterfall-marker-bar").length,
+ ok($$(".waterfall-tree-item > .waterfall-marker .waterfall-marker-bar").length,
"Some marker color bars should have been created inside the waterfall.");
// Test the sidebar.
let detailsView = WaterfallView.details;
- let markersRoot = WaterfallView._markersRoot;
// Make sure the bounds are up to date.
- markersRoot.recalculateBounds();
+ WaterfallView._recalculateBounds();
let parentWidthBefore = $("#waterfall-view").getBoundingClientRect().width;
let sidebarWidthBefore = $(".waterfall-sidebar").getBoundingClientRect().width;
let detailsWidthBefore = $("#waterfall-details").getBoundingClientRect().width;
ok(detailsView.hidden,
"The details view in the waterfall view is hidden by default.");
is(detailsWidthBefore, 0,
"The details view width should be 0 when hidden.");
- is(markersRoot._waterfallWidth,
- parentWidthBefore - sidebarWidthBefore - WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
+ is(WaterfallView.waterfallWidth,
+ parentWidthBefore - sidebarWidthBefore
+ - WaterfallView.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
"The waterfall width is correct (1).");
- let receivedFocusEvent = once(markersRoot, "focus");
let waterfallRerendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
- WaterfallView._markersRoot.getChild(0).focus();
- yield receivedFocusEvent;
+ $$(".waterfall-tree-item")[0].click();
yield waterfallRerendered;
let parentWidthAfter = $("#waterfall-view").getBoundingClientRect().width;
let sidebarWidthAfter = $(".waterfall-sidebar").getBoundingClientRect().width;
let detailsWidthAfter = $("#waterfall-details").getBoundingClientRect().width;
ok(!detailsView.hidden,
"The details view in the waterfall view is now visible.");
is(parentWidthBefore, parentWidthAfter,
"The parent view's width should not have changed.");
is(sidebarWidthBefore, sidebarWidthAfter,
"The sidebar view's width should not have changed.");
isnot(detailsWidthAfter, 0,
"The details view width should not be 0 when visible.");
- is(markersRoot._waterfallWidth,
+ is(WaterfallView.waterfallWidth,
parentWidthAfter - sidebarWidthAfter - detailsWidthAfter
- - WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
+ - WaterfallView.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
"The waterfall width is correct (2).");
yield teardownToolboxAndRemoveTab(panel);
});
--- a/devtools/client/performance/views/details-waterfall.js
+++ b/devtools/client/performance/views/details-waterfall.js
@@ -5,23 +5,29 @@
/* import-globals-from ../performance-view.js */
/* globals window, DetailsSubview */
"use strict";
const MARKER_DETAILS_WIDTH = 200;
// Units are in milliseconds.
const WATERFALL_RESIZE_EVENTS_DRAIN = 100;
+const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
+
/**
* Waterfall view containing the timeline markers, controlled by DetailsView.
*/
var WaterfallView = Heritage.extend(DetailsSubview, {
// Smallest unit of time between two markers. Larger by 10x^3 than Number.EPSILON.
MARKER_EPSILON: 0.000000000001,
+ // px
+ WATERFALL_MARKER_SIDEBAR_WIDTH: 175,
+ // px
+ WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS: 20,
observedPrefs: [
"hidden-markers"
],
rerenderPrefs: [
"hidden-markers"
],
@@ -38,18 +44,17 @@ var WaterfallView = Heritage.extend(Deta
this._cache = new WeakMap();
this._onMarkerSelected = this._onMarkerSelected.bind(this);
this._onResize = this._onResize.bind(this);
this._onViewSource = this._onViewSource.bind(this);
this._onShowAllocations = this._onShowAllocations.bind(this);
this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
- this.headerContainer = $("#waterfall-header");
- this.breakdownContainer = $("#waterfall-breakdown");
+ this.treeContainer = $("#waterfall-tree");
this.detailsContainer = $("#waterfall-details");
this.detailsSplitter = $("#waterfall-view > splitter");
this.details = new MarkerDetails($("#waterfall-details"),
$("#waterfall-view > splitter"));
this.details.hidden = true;
this.details.on("resize", this._onResize);
@@ -70,16 +75,18 @@ var WaterfallView = Heritage.extend(Deta
clearNamedTimeout("waterfall-resize");
this._cache = null;
this.details.off("resize", this._onResize);
this.details.off("view-source", this._onViewSource);
this.details.off("show-allocations", this._onShowAllocations);
window.removeEventListener("resize", this._onResize);
+
+ ReactDOM.unmountComponentAtNode(this.treeContainer);
},
/**
* Method for handling all the set up for rendering a new waterfall.
*
* @param object interval [optional]
* The { startTime, endTime }, in milliseconds.
*/
@@ -104,29 +111,27 @@ var WaterfallView = Heritage.extend(Deta
_onMarkerSelected: function (event, marker) {
let recording = PerformanceController.getCurrentRecording();
let frames = recording.getFrames();
let allocations = recording.getConfiguration().withAllocations;
if (event === "selected") {
this.details.render({ marker, frames, allocations });
this.details.hidden = false;
- this._lastSelected = marker;
}
if (event === "unselected") {
this.details.empty();
}
},
/**
* Called when the marker details view is resized.
*/
_onResize: function () {
setNamedTimeout("waterfall-resize", WATERFALL_RESIZE_EVENTS_DRAIN, () => {
- this._markersRoot.recalculateBounds();
this.render(OverviewView.getTimeInterval());
});
},
/**
* Called whenever an observed pref is changed.
*/
_onObservedPrefChange: function (_, prefName) {
@@ -201,49 +206,47 @@ var WaterfallView = Heritage.extend(Deta
filter: this._hiddenMarkers
});
this._cache.set(markers, rootMarkerNode);
return rootMarkerNode;
},
/**
+ * Calculates the available width for the waterfall.
+ * This should be invoked every time the container node is resized.
+ */
+ _recalculateBounds: function () {
+ this.waterfallWidth = this.treeContainer.clientWidth
+ - this.WATERFALL_MARKER_SIDEBAR_WIDTH
+ - this.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS;
+ },
+
+ /**
* Renders the waterfall tree.
*/
_populateWaterfallTree: function (rootMarkerNode, interval) {
- let root = new MarkerView({
+ this._recalculateBounds();
+
+ let doc = this.treeContainer.ownerDocument;
+ let startTime = interval.startTime | 0;
+ let endTime = interval.endTime | 0;
+ let dataScale = this.waterfallWidth / (endTime - startTime);
+
+ this.canvas = TickUtils.drawWaterfallBackground(doc, dataScale, this.waterfallWidth);
+
+ let treeView = Waterfall({
marker: rootMarkerNode,
- // The root node is irrelevant in a waterfall tree.
- hidden: true,
- // The waterfall tree should not expand by default.
- autoExpandDepth: 0
+ startTime,
+ endTime,
+ dataScale,
+ sidebarWidth: this.WATERFALL_MARKER_SIDEBAR_WIDTH,
+ waterfallWidth: this.waterfallWidth,
+ onFocus: node => this._onMarkerSelected("selected", node)
});
- let header = new WaterfallHeader(root);
-
- this._markersRoot = root;
- this._waterfallHeader = header;
-
- root.filter = this._hiddenMarkers;
- 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);
-
- // If an item was previously selected in this view, attempt to
- // re-select it by traversing the newly created tree.
- if (this._lastSelected) {
- let item = root.find(i => i.marker === this._lastSelected);
- if (item) {
- item.focus();
- }
- }
+ ReactDOM.render(treeView, this.treeContainer);
},
toString: () => "[object WaterfallView]"
});
EventEmitter.decorate(WaterfallView);
--- a/devtools/client/themes/performance.css
+++ b/devtools/client/themes/performance.css
@@ -393,20 +393,41 @@
}
.call-tree-category {
transform: scale(0.75);
transform-origin: center right;
}
/**
- * Waterfall ticks header
+ * Waterfall markers tree
*/
+#waterfall-tree {
+ /* DE-XUL: convert this to display: flex once performance.xul is converted to HTML */
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-flex: 1;
+}
+
+.waterfall-markers {
+ /* DE-XUL: convert this to display: flex once performance.xul is converted to HTML */
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-flex: 1;
+}
+
+.waterfall-header {
+ display: flex;
+}
+
.waterfall-header-ticks {
+ display: flex;
+ flex: auto;
+ align-items: center;
overflow: hidden;
}
.waterfall-header-name {
padding: 2px 4px;
font-size: 90%;
}
@@ -418,151 +439,176 @@
}
.waterfall-header-tick:not(:first-child) {
margin-inline-start: -100px !important; /* Don't affect layout. */
}
.waterfall-background-ticks {
/* Background created on a <canvas> in js. */
- /* @see devtools/client/timeline/widgets/waterfall.js */
+ /* @see devtools/client/performance/modules/widgets/waterfall-ticks.js */
background-image: -moz-element(#waterfall-background);
background-repeat: repeat-y;
background-position: -1px center;
}
/**
* Markers waterfall breakdown
*/
-#waterfall-breakdown {
+.waterfall-markers .tree {
+ /* DE-XUL: convert this to display: flex once performance.xul is converted to HTML */
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-flex: 1;
overflow-x: hidden;
overflow-y: auto;
+ --waterfall-tree-row-height: 15px;
}
-.theme-light .waterfall-tree-item:not([level="0"]) {
+.waterfall-markers .tree-node {
+ display: flex;
+ height: var(--waterfall-tree-row-height);
+ line-height: var(--waterfall-tree-row-height);
+}
+
+.waterfall-tree-item {
+ display: flex;
+ flex: auto;
+}
+
+.theme-light .waterfall-markers .tree-node:not([data-depth="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"]) {
+.theme-dark .waterfall-markers .tree-node:not([data-depth="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 {
+.theme-light .waterfall-tree-item[data-expandable] .waterfall-marker-bullet,
+.theme-light .waterfall-tree-item[data-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 {
+.theme-dark .waterfall-tree-item[data-expandable] .waterfall-marker-bullet,
+.theme-dark .waterfall-tree-item[data-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"] {
+.waterfall-markers .tree-node[data-expanded],
+.waterfall-markers .tree-node:not([data-depth="0"]) + .tree-node[data-depth="0"] {
box-shadow: 0 -1px var(--cell-border-color-light);
}
-.waterfall-tree-item:nth-child(2n) > .waterfall-marker {
+.tree-node-odd .waterfall-marker {
background-color: var(--row-alt-background-color);
}
-.waterfall-tree-item:hover {
+.waterfall-markers .tree-node:hover {
background-color: var(--row-hover-background-color);
}
-.waterfall-tree-item:last-child {
+.waterfall-markers .tree-node-last {
border-bottom: 1px solid var(--cell-border-color);
}
-.waterfall-tree-item:focus {
+.waterfall-tree-item.focused {
background-color: var(--theme-selection-background);
}
-.waterfall-tree-item:focus description {
- color: var(--theme-selection-color) !important;
-}
-
/**
* Marker left sidebar
*/
.waterfall-sidebar {
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
border-inline-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 {
+.waterfall-tree-item.focused > .waterfall-sidebar {
background: transparent;
}
+.waterfall-tree-item.focused > .waterfall-sidebar {
+ color: var(--theme-selection-color);
+}
+
.waterfall-marker-bullet {
width: 8px;
height: 8px;
margin-inline-start: 8px;
margin-inline-end: 6px;
border-radius: 1px;
+ box-sizing: border-box;
}
.waterfall-marker-name {
font-size: 95%;
padding-bottom: 1px !important;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
/**
* Marker timebar
*/
.waterfall-marker {
+ display: flex;
+ flex: auto;
overflow: hidden;
}
+.waterfall-marker-wrap {
+ display: flex;
+ align-items: center;
+ transform-origin: left center;
+}
+
.waterfall-marker-bar {
height: 9px;
- transform-origin: left center;
border-radius: 1px;
-}
-
-.waterfall-marker > .theme-twisty {
- /* Don't affect layout. */
- width: 14px;
- margin-inline-end: -14px;
+ box-sizing: border-box;
}
/**
* OTMT markers
*/
-.waterfall-tree-item[otmt=true] .waterfall-marker-bullet,
-.waterfall-tree-item[otmt=true] .waterfall-marker-bar {
+.waterfall-tree-item[data-otmt=true] .waterfall-marker-bullet,
+.waterfall-tree-item[data-otmt=true] .waterfall-marker-bar {
background-color: transparent;
border-width: 1px;
border-style: solid;
}
/**
* Marker details view
*/