null);
+ let peek = distance => markersList[i + distance];
+ let collapseInfo = collapse(parentNode, curr, peek);
+
+ if (collapseInfo) {
+ let { toParent, withData, forceNew, forceEnd } = collapseInfo;
+
+ // If the `forceNew` prop is set on the collapse info, then a new parent
+ // marker needs to be created even if there is one already available.
+ if (forceNew) {
+ clearParentNode();
+ }
+ // If the `toParent` prop is set on the collapse info, then this marker
+ // can be collapsed into a higher-level parent marker.
+ if (toParent) {
+ let parentNode = getOrCreateParentNode(markerNode, toParent, curr.start);
+ parentNode.end = curr.end;
+ parentNode.submarkers.push(curr);
+ for (let key in withData) {
+ parentNode[key] = withData[key];
+ }
+ }
+ // If the `forceEnd` prop is set on the collapse info, then the higher-level
+ // parent marker is full and should be finalized.
+ if (forceEnd) {
+ clearParentNode();
+ }
+ } else {
+ clearParentNode();
+ markerNode.submarkers.push(curr);
+ }
+ }
+}
+
+/**
+ * Creates an empty parent marker, which functions like a regular marker,
+ * but is able to hold additional child markers.
+ * @param string name
+ * @param number start [optional]
+ * @param number end [optional]
+ * @return object
+ */
+function makeEmptyMarkerNode(name, start, end) {
+ return {
+ name: name,
+ start: start,
+ end: end,
+ submarkers: []
+ };
+}
+
+/**
+ * Creates a factory for markers containing other markers.
+ * @return array[function]
+ */
+function makeParentNodeFactory() {
+ let marker;
+
+ return [
+ /**
+ * Gets the current parent marker for the given marker name. If it doesn't
+ * exist, it creates it and appends it to another parent marker.
+ * @param object owner
+ * @param string name
+ * @param number start
+ * @return object
+ */
+ function getOrCreateParentNode(owner, name, start) {
+ if (marker && marker.name == name) {
+ return marker;
+ } else {
+ marker = makeEmptyMarkerNode(name, start);
+ owner.submarkers.push(marker);
+ return marker;
+ }
+ },
+
+ /**
+ * Gets the current marker marker.
+ * @return object
+ */
+ function getCurrentParentNode() {
+ return marker;
+ },
+
+ /**
+ * Clears the current marker marker.
+ */
+ function clearParentNode() {
+ marker = null;
+ }
+ ];
+}
+
+exports.makeEmptyMarkerNode = makeEmptyMarkerNode;
+exports.collapseMarkersIntoNode = collapseMarkersIntoNode;
diff --git a/browser/devtools/performance/modules/widgets/marker-details.js b/browser/devtools/performance/modules/widgets/marker-details.js
index 19572c1dc46..f1eb49c7135 100644
--- a/browser/devtools/performance/modules/widgets/marker-details.js
+++ b/browser/devtools/performance/modules/widgets/marker-details.js
@@ -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");
+ }
};
/**
diff --git a/browser/devtools/performance/modules/widgets/marker-view.js b/browser/devtools/performance/modules/widgets/marker-view.js
new file mode 100644
index 00000000000..98a5428990c
--- /dev/null
+++ b/browser/devtools/performance/modules/widgets/marker-view.js
@@ -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;
diff --git a/browser/devtools/performance/modules/widgets/markers-overview.js b/browser/devtools/performance/modules/widgets/markers-overview.js
index f4fd6d359e1..3d75a556964 100644
--- a/browser/devtools/performance/modules/widgets/markers-overview.js
+++ b/browser/devtools/performance/modules/widgets/markers-overview.js
@@ -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
@@ -75,7 +77,7 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
for (let type in blueprint) {
this._paintBatches.set(type, { style: blueprint[type], batch: [] });
- this._lastGroup = Math.max(this._lastGroup, blueprint[type].group);
+ this._lastGroup = Math.max(this._lastGroup, blueprint[type].group || 0);
}
},
@@ -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
diff --git a/browser/devtools/performance/modules/widgets/tree-view.js b/browser/devtools/performance/modules/widgets/tree-view.js
index b240176040c..d80cb67fb44 100644
--- a/browser/devtools/performance/modules/widgets/tree-view.js
+++ b/browser/devtools/performance/modules/widgets/tree-view.js
@@ -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);
diff --git a/browser/devtools/performance/modules/widgets/waterfall-ticks.js b/browser/devtools/performance/modules/widgets/waterfall-ticks.js
new file mode 100644
index 00000000000..1143683adb1
--- /dev/null
+++ b/browser/devtools/performance/modules/widgets/waterfall-ticks.js
@@ -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 };
diff --git a/browser/devtools/performance/modules/widgets/waterfall.js b/browser/devtools/performance/modules/widgets/waterfall.js
deleted file mode 100644
index d0b5715eaec..00000000000
--- a/browser/devtools/performance/modules/widgets/waterfall.js
+++ /dev/null
@@ -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;
diff --git a/browser/devtools/performance/moz.build b/browser/devtools/performance/moz.build
index 5d844974f5f..12ee4a81dd1 100644
--- a/browser/devtools/performance/moz.build
+++ b/browser/devtools/performance/moz.build
@@ -15,11 +15,13 @@ EXTRA_JS_MODULES.devtools.performance += [
'modules/logic/recording-model.js',
'modules/logic/recording-utils.js',
'modules/logic/tree-model.js',
+ 'modules/logic/waterfall-utils.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'
]
diff --git a/browser/devtools/performance/performance-controller.js b/browser/devtools/performance/performance-controller.js
index df30704cd21..405fa4ce5b6 100644
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -25,12 +25,16 @@ 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",
"devtools/performance/marker-utils");
+loader.lazyRequireGetter(this, "WaterfallUtils",
+ "devtools/performance/waterfall-utils");
loader.lazyRequireGetter(this, "CallView",
"devtools/performance/tree-view", true);
loader.lazyRequireGetter(this, "ThreadNode",
diff --git a/browser/devtools/performance/performance.xul b/browser/devtools/performance/performance.xul
index 3b7f24c0c6c..0b5226cbb1e 100644
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -241,12 +241,13 @@
-
+
+
+
+
+ class="theme-sidebar"/>
diff --git a/browser/devtools/performance/test/.eslintrc b/browser/devtools/performance/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/performance/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/performance/test/browser.ini b/browser/devtools/performance/test/browser.ini
index 6f8f20f9959..ac0f1a8f5e6 100644
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -145,4 +145,6 @@ skip-if = e10s # GC events seem unreliable in multiprocess
[browser_timeline-filters.js]
[browser_timeline-waterfall-background.js]
[browser_timeline-waterfall-generic.js]
+[browser_timeline-waterfall-rerender.js]
[browser_timeline-waterfall-sidebar.js]
+[browser_waterfall-collapse.js]
diff --git a/browser/devtools/performance/test/browser_perf-front-basic-timeline-01.js b/browser/devtools/performance/test/browser_perf-front-basic-timeline-01.js
index eb85fa884ca..0fc6d3b4997 100644
--- a/browser/devtools/performance/test/browser_perf-front-basic-timeline-01.js
+++ b/browser/devtools/performance/test/browser_perf-front-basic-timeline-01.js
@@ -68,8 +68,11 @@ function* spawnTest() {
counters.ticks.push({ delta, timestamps });
lastTickDelta = delta;
}
+ else if (name === "frames") {
+ // Nothing to do here.
+ }
else {
- throw new Error("unknown event " + name);
+ ok(false, `Received unknown event: ${name}`);
}
if (name === "markers" && counters[name].length === 1 ||
diff --git a/browser/devtools/performance/test/browser_profiler_tree-abstract-01.js b/browser/devtools/performance/test/browser_profiler_tree-abstract-01.js
index c507b7746c2..d327fca5b80 100644
--- a/browser/devtools/performance/test/browser_profiler_tree-abstract-01.js
+++ b/browser/devtools/performance/test/browser_profiler_tree-abstract-01.js
@@ -18,6 +18,11 @@ function* spawnTest() {
let treeRoot = new MyCustomTreeItem(gDataSrc, { parent: null });
treeRoot.attachTo(container);
+ ok(!treeRoot.expanded,
+ "The root node should not be expanded yet.");
+ ok(!treeRoot.populated,
+ "The root node should not be populated yet.");
+
is(container.childNodes.length, 1,
"The container node should have one child available.");
is(container.childNodes[0], treeRoot.target,
diff --git a/browser/devtools/performance/test/browser_profiler_tree-abstract-02.js b/browser/devtools/performance/test/browser_profiler_tree-abstract-02.js
index fb2d5125f49..0235be85629 100644
--- a/browser/devtools/performance/test/browser_profiler_tree-abstract-02.js
+++ b/browser/devtools/performance/test/browser_profiler_tree-abstract-02.js
@@ -16,14 +16,9 @@ function* spawnTest() {
// Populate the tree and test `expand`, `collapse` and `getChild`...
let treeRoot = new MyCustomTreeItem(gDataSrc, { parent: null });
+ treeRoot.autoExpandDepth = 1;
treeRoot.attachTo(container);
- ok(!treeRoot.expanded,
- "The root node should not be expanded yet.");
- ok(!treeRoot.populated,
- "The root node should not be populated yet.");
-
- treeRoot.expand();
ok(treeRoot.expanded,
"The root node should now be expanded.");
ok(treeRoot.populated,
diff --git a/browser/devtools/performance/test/browser_timeline-blueprint.js b/browser/devtools/performance/test/browser_timeline-blueprint.js
index 7eed359e80b..7eb6f179758 100644
--- a/browser/devtools/performance/test/browser_timeline-blueprint.js
+++ b/browser/devtools/performance/test/browser_timeline-blueprint.js
@@ -15,11 +15,20 @@ function* spawnTest() {
"The timeline blueprint has at least one entry.");
for (let [key, value] of Iterator(TIMELINE_BLUEPRINT)) {
- ok("group" in value,
- "Each entry in the timeline blueprint contains a `group` key.");
- ok("colorName" in value,
- "Each entry in the timeline blueprint contains a `colorName` key.");
- ok("label" in value,
- "Each entry in the timeline blueprint contains a `label` key.");
+ if (key.startsWith("meta::")) {
+ ok(!("group" in value),
+ "No meta entry in the timeline blueprint can contain a `group` key.");
+ ok("colorName" in value,
+ "Each meta entry in the timeline blueprint contains a `colorName` key.");
+ ok("label" in value,
+ "Each meta entry in the timeline blueprint contains a `label` key.");
+ } else {
+ ok("group" in value,
+ "Each entry in the timeline blueprint contains a `group` key.");
+ ok("colorName" in value,
+ "Each entry in the timeline blueprint contains a `colorName` key.");
+ ok("label" in value,
+ "Each entry in the timeline blueprint contains a `label` key.");
+ }
}
}
diff --git a/browser/devtools/performance/test/browser_timeline-filters.js b/browser/devtools/performance/test/browser_timeline-filters.js
index d6a45211deb..7f61a968a40 100644
--- a/browser/devtools/performance/test/browser_timeline-filters.js
+++ b/browser/devtools/performance/test/browser_timeline-filters.js
@@ -7,7 +7,7 @@
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
- let { $, $$, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
let { TimelineGraph } = devtools.require("devtools/performance/graphs");
let { rowHeight: MARKERS_GRAPH_ROW_HEIGHT } = TimelineGraph.prototype;
@@ -24,20 +24,15 @@ function* spawnTest() {
yield stopRecording(panel);
- let overview = OverviewView.graphs.get("timeline");
- let waterfall = WaterfallView.waterfall;
-
// Select everything
OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE })
$("#filter-button").click();
-
- yield waitUntil(() => !waterfall._outstandingMarkers.length);
-
let menuItem1 = $("menuitem[marker-type=Styles]");
let menuItem2 = $("menuitem[marker-type=Reflow]");
let menuItem3 = $("menuitem[marker-type=Paint]");
+ let overview = OverviewView.graphs.get("timeline");
let originalHeight = overview.fixedHeight;
ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (1)");
@@ -46,9 +41,7 @@ function* spawnTest() {
let heightBefore = overview.fixedHeight;
EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin);
- yield once(menuItem1, "command");
-
- yield waitUntil(() => !waterfall._outstandingMarkers.length);
+ yield waitForOverviewAndCommand(overview, menuItem1);
is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (2)");
@@ -57,9 +50,7 @@ function* spawnTest() {
heightBefore = overview.fixedHeight;
EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin);
- yield once(menuItem2, "command");
-
- yield waitUntil(() => !waterfall._outstandingMarkers.length);
+ yield waitForOverviewAndCommand(overview, menuItem2);
is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (3)");
@@ -68,9 +59,7 @@ function* spawnTest() {
heightBefore = overview.fixedHeight;
EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin);
- yield once(menuItem3, "command");
-
- yield waitUntil(() => !waterfall._outstandingMarkers.length);
+ yield waitForOverviewAndCommand(overview, menuItem3);
is(overview.fixedHeight, heightBefore - MARKERS_GRAPH_ROW_HEIGHT, "Overview is smaller");
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)");
@@ -79,11 +68,9 @@ function* spawnTest() {
for (let item of [menuItem1, menuItem2, menuItem3]) {
EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin);
- yield once(item, "command");
+ yield waitForOverviewAndCommand(overview, item);
}
- yield waitUntil(() => !waterfall._outstandingMarkers.length);
-
ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (5)");
ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (5)");
ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (5)");
@@ -93,3 +80,9 @@ function* spawnTest() {
yield teardown(panel);
finish();
}
+
+function waitForOverviewAndCommand(overview, item) {
+ let overviewRendered = overview.once("refresh");
+ let menuitemCommandDispatched = once(item, "command");
+ return Promise.all([overviewRendered, menuitemCommandDispatched]);
+}
diff --git a/browser/devtools/performance/test/browser_timeline-waterfall-background.js b/browser/devtools/performance/test/browser_timeline-waterfall-background.js
index 4a447c195ac..db7010df706 100644
--- a/browser/devtools/performance/test/browser_timeline-waterfall-background.js
+++ b/browser/devtools/performance/test/browser_timeline-waterfall-background.js
@@ -25,27 +25,30 @@ function* spawnTest() {
DetailsView.selectView("waterfall"),
once(WaterfallView, EVENTS.WATERFALL_RENDERED)
]);
+
yield stopRecording(panel);
ok(true, "Recording has ended.");
+
yield rendered;
+ ok(true, "Recording has rendered.");
// Test the waterfall background.
let parentWidth = $("#waterfall-view").getBoundingClientRect().width;
let sidebarWidth = $(".waterfall-sidebar").getBoundingClientRect().width;
let detailsWidth = $("#waterfall-details").getBoundingClientRect().width;
- let waterfallWidth = WaterfallView.waterfall._waterfallWidth;
+ let waterfallWidth = WaterfallView._markersRoot._waterfallWidth;
is(waterfallWidth, parentWidth - sidebarWidth - detailsWidth,
"The waterfall width is correct.")
- ok(WaterfallView.waterfall._canvas,
+ ok(WaterfallView._waterfallHeader._canvas,
"A canvas should be created after the recording ended.");
- ok(WaterfallView.waterfall._ctx,
+ ok(WaterfallView._waterfallHeader._ctx,
"A 2d context should be created after the recording ended.");
- is(WaterfallView.waterfall._canvas.width, waterfallWidth,
+ is(WaterfallView._waterfallHeader._canvas.width, waterfallWidth,
"The canvas width is correct.");
- is(WaterfallView.waterfall._canvas.height, 1,
+ is(WaterfallView._waterfallHeader._canvas.height, 1,
"The canvas height is correct.");
yield teardown(panel);
diff --git a/browser/devtools/performance/test/browser_timeline-waterfall-generic.js b/browser/devtools/performance/test/browser_timeline-waterfall-generic.js
index 03b73d8d700..c0c4402085e 100644
--- a/browser/devtools/performance/test/browser_timeline-waterfall-generic.js
+++ b/browser/devtools/performance/test/browser_timeline-waterfall-generic.js
@@ -42,26 +42,22 @@ function* spawnTest() {
ok($$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
"Some header tick labels should have been created inside the tick node.");
- // Test the markers container.
-
- ok($(".waterfall-marker-container"),
- "A marker container should have been created.");
-
// Test the markers sidebar (left).
- ok($$(".waterfall-marker-container > .waterfall-sidebar").length,
+ ok($$(".waterfall-tree-item > .waterfall-sidebar").length,
"Some marker sidebar nodes should have been created.");
- ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-bullet").length,
+ ok($$(".waterfall-tree-item > .waterfall-sidebar > .waterfall-marker-bullet").length,
"Some marker color bullets should have been created inside the sidebar.");
- ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-name").length,
+ 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-marker-item").length,
+ ok($$(".waterfall-tree-item > .waterfall-marker").length,
"Some marker waterfall nodes should have been created.");
- ok($$(".waterfall-marker-item:not(spacer) > .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.");
+
yield teardown(panel);
finish();
}
diff --git a/browser/devtools/performance/test/browser_timeline-waterfall-rerender.js b/browser/devtools/performance/test/browser_timeline-waterfall-rerender.js
new file mode 100644
index 00000000000..ac195af4fe5
--- /dev/null
+++ b/browser/devtools/performance/test/browser_timeline-waterfall-rerender.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall remembers the selection when rerendering.
+ */
+
+function* spawnTest() {
+ let { target, panel } = yield initPerformance(SIMPLE_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ let updated = 0;
+ OverviewView.on(EVENTS.OVERVIEW_RENDERED, () => updated++);
+
+ ok((yield waitUntil(() => updated > 0)),
+ "The overview graphs were updated a bunch of times.");
+ ok((yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length > 0)),
+ "There are some markers available.");
+
+ yield stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ let initialBarsCount = $$(".waterfall-marker-bar").length;
+
+ // Select a portion of the overview.
+ let timeline = OverviewView.graphs.get("timeline");
+ let rerendered = WaterfallView.once(EVENTS.WATERFALL_RENDERED);
+ timeline.setSelection({ start: 0, end: timeline.width / 2 })
+ yield rerendered;
+
+ // Focus the second item in the tree.
+ WaterfallView._markersRoot.getChild(1).focus();
+
+ let beforeResizeBarsCount = $$(".waterfall-marker-bar").length;
+ ok(beforeResizeBarsCount < initialBarsCount,
+ "A subset of the total markers was selected.");
+
+ is(Array.indexOf($$(".waterfall-tree-item"), $(".waterfall-tree-item:focus")), 2,
+ "The correct item was focused in the tree.");
+
+ rerendered = WaterfallView.once(EVENTS.WATERFALL_RENDERED);
+ EventUtils.sendMouseEvent({ type: "mouseup" }, WaterfallView.detailsSplitter);
+ yield rerendered;
+
+ let afterResizeBarsCount = $$(".waterfall-marker-bar").length;
+ is(afterResizeBarsCount, beforeResizeBarsCount,
+ "The same subset of the total markers remained visible.");
+
+ is(Array.indexOf($$(".waterfall-tree-item"), $(".waterfall-tree-item:focus")), 2,
+ "The correct item is still focused in the tree.");
+
+ yield teardown(panel);
+ finish();
+}
diff --git a/browser/devtools/performance/test/browser_timeline-waterfall-sidebar.js b/browser/devtools/performance/test/browser_timeline-waterfall-sidebar.js
index 4f2f1e54f1d..1a40655992e 100644
--- a/browser/devtools/performance/test/browser_timeline-waterfall-sidebar.js
+++ b/browser/devtools/performance/test/browser_timeline-waterfall-sidebar.js
@@ -7,10 +7,18 @@
function* spawnTest() {
let { target, panel } = yield initPerformance(SIMPLE_URL);
- let { $, $$, EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
let { L10N, TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/global");
let { getMarkerLabel } = devtools.require("devtools/performance/marker-utils");
+ // Hijack the markers massaging part of creating the waterfall view,
+ // to prevent collapsing markers and allowing this test to verify
+ // everything individually. A better solution would be to just expand
+ // all markers first and then skip the meta nodes, but I'm lazy.
+ WaterfallView._prepareWaterfallTree = markers => {
+ return { submarkers: markers };
+ };
+
yield startRecording(panel);
ok(true, "Recording has started.");
@@ -26,34 +34,39 @@ function* spawnTest() {
ok(true, "Recording has ended.");
// Select everything
- OverviewView.graphs.get("timeline").setSelection({ start: 0, end: OverviewView.graphs.get("timeline").width })
+ let timeline = OverviewView.graphs.get("timeline");
+ let rerendered = WaterfallView.once(EVENTS.WATERFALL_RENDERED);
+ timeline.setSelection({ start: 0, end: timeline.width })
+ yield rerendered;
- let bars = $$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar");
+ let bars = $$(".waterfall-marker-bar");
let markers = PerformanceController.getCurrentRecording().getMarkers();
- ok(bars.length > 2, "got at least 3 markers");
+ ok(bars.length > 2, "Got at least 3 markers (1)");
+ ok(markers.length > 2, "Got at least 3 markers (2)");
- let sidebar = $("#waterfall-details");
for (let i = 0; i < bars.length; i++) {
let bar = bars[i];
- bar.click();
let m = markers[i];
+ EventUtils.sendMouseEvent({ type: "mousedown" }, bar);
is($("#waterfall-details .marker-details-type").getAttribute("value"), getMarkerLabel(m),
- "sidebar title matches markers name");
+ "Sidebar title matches markers name.");
let tooltip = $(".marker-details-duration").getAttribute("tooltiptext");
- let printedDuration = $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
+ let duration = $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
// Values are rounded. We don't use a strict equality.
- is(toMs(m.end - m.start), printedDuration, "sidebar duration is valid");
+ is(toMs(m.end - m.start), duration, "Sidebar duration is valid.");
+
// For some reason, anything that creates "→" here turns it into a "â" for some reason.
// So just check that start and end time are in there somewhere.
- ok(tooltip.indexOf(toMs(m.start)) !== -1, "tooltip has start time");
- ok(tooltip.indexOf(toMs(m.end)) !== -1, "tooltip has end time");
+ ok(tooltip.indexOf(toMs(m.start)) !== -1, "Tooltip has start time.");
+ ok(tooltip.indexOf(toMs(m.end)) !== -1, "Tooltip has end time.");
}
+
yield teardown(panel);
finish();
}
diff --git a/browser/devtools/performance/test/browser_waterfall-collapse.js b/browser/devtools/performance/test/browser_waterfall-collapse.js
new file mode 100644
index 00000000000..cdc408cf6f4
--- /dev/null
+++ b/browser/devtools/performance/test/browser_waterfall-collapse.js
@@ -0,0 +1,358 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall collapsing logic works properly.
+ */
+
+function test() {
+ const WaterfallUtils = devtools.require("devtools/performance/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.makeEmptyMarkerNode("(root)");
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ markerNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ is(rootMarkerNode.toSource(), gExpectedOutput.toSource(),
+ "The markers didn't collapse properly.");
+
+ finish();
+}
+
+const gTestMarkers = [
+// Test collapsing Style markers
+{
+ start: 1,
+ end: 2,
+ name: "Styles"
+},
+{
+ start: 3,
+ end: 4,
+ name: "Styles"
+},
+// Test collapsing Reflow markers
+{
+ start: 5,
+ end: 6,
+ name: "Reflow"
+},
+{
+ start: 7,
+ end: 8,
+ name: "Reflow"
+},
+// Test collapsing Paint markers
+{
+ start: 9,
+ end: 10,
+ name: "Paint"
+}, {
+ start: 11,
+ end: 12,
+ name: "Paint"
+},
+// Test standalone DOMEvent markers followed by a different marker
+{
+ start: 13,
+ end: 14,
+ name: "DOMEvent",
+ eventPhase: 1,
+ type: "foo1"
+},
+{
+ start: 15,
+ end: 16,
+ name: "TimeStamp"
+},
+// Test a DOMEvent marker followed by a Javascript marker.
+{
+ start: 17,
+ end: 18,
+ name: "DOMEvent",
+ eventPhase: 2,
+ type: "foo2"
+}, {
+ start: 19,
+ end: 20,
+ name: "Javascript",
+ stack: 1,
+ endStack: 2
+},
+// Test another DOMEvent marker followed by a Javascript marker.
+{
+ start: 21,
+ end: 22,
+ name: "DOMEvent",
+ eventPhase: 3,
+ type: "foo3"
+}, {
+ start: 23,
+ end: 24,
+ name: "Javascript",
+ stack: 3,
+ endStack: 4
+},
+// Test a DOMEvent marker followed by multiple Javascript markers.
+{
+ start: 25,
+ end: 26,
+ name: "DOMEvent",
+ eventPhase: 4,
+ type: "foo4"
+}, {
+ start: 27,
+ end: 28,
+ name: "Javascript",
+ stack: 5,
+ endStack: 6
+}, {
+ start: 29,
+ end: 30,
+ name: "Javascript",
+ stack: 7,
+ endStack: 8
+}, {
+ start: 31,
+ end: 32,
+ name: "Javascript",
+ stack: 9,
+ endStack: 10
+},
+// Test multiple DOMEvent markers followed by multiple Javascript markers.
+{
+ start: 33,
+ end: 34,
+ name: "DOMEvent",
+ eventPhase: 5,
+ type: "foo5"
+}, {
+ start: 35,
+ end: 36,
+ name: "DOMEvent",
+ eventPhase: 6,
+ type: "foo6"
+}, {
+ start: 37,
+ end: 38,
+ name: "DOMEvent",
+ eventPhase: 7,
+ type: "foo6"
+}, {
+ start: 39,
+ end: 40,
+ name: "Javascript",
+ stack: 11,
+ endStack: 12
+}, {
+ start: 41,
+ end: 42,
+ name: "Javascript",
+ stack: 13,
+ endStack: 14
+}, {
+ start: 43,
+ end: 44,
+ name: "Javascript",
+ stack: 15,
+ endStack: 16
+},
+// Test a lonely marker at the end.
+{
+ start: 45,
+ end: 46,
+ name: "GarbageCollection"
+}
+];
+
+const gExpectedOutput = {
+ name: "(root)",
+ start: (void 0),
+ end: (void 0),
+ submarkers: [{
+ name: "Styles",
+ start: 1,
+ end: 4,
+ submarkers: [{
+ start: 1,
+ end: 2,
+ name: "Styles"
+ }, {
+ start: 3,
+ end: 4,
+ name: "Styles"
+ }]
+ }, {
+ name: "Reflow",
+ start: 5,
+ end: 8,
+ submarkers: [{
+ start: 5,
+ end: 6,
+ name: "Reflow"
+ }, {
+ start: 7,
+ end: 8,
+ name: "Reflow"
+ }]
+ }, {
+ name: "Paint",
+ start: 9,
+ end: 12,
+ submarkers: [{
+ start: 9,
+ end: 10,
+ name: "Paint"
+ }, {
+ start: 11,
+ end: 12,
+ name: "Paint"
+ }]
+ }, {
+ start: 13,
+ end: 14,
+ name: "DOMEvent",
+ eventPhase: 1,
+ type: "foo1"
+ }, {
+ start: 15,
+ end: 16,
+ name: "TimeStamp"
+ }, {
+ name: "meta::DOMEvent+JS",
+ start: 17,
+ end: 20,
+ submarkers: [{
+ start: 17,
+ end: 18,
+ name: "DOMEvent",
+ eventPhase: 2,
+ type: "foo2"
+ }, {
+ start: 19,
+ end: 20,
+ name: "Javascript",
+ stack: 1,
+ endStack: 2
+ }],
+ type: "foo2",
+ eventPhase: 2,
+ stack: 1,
+ endStack: 2
+ }, {
+ name: "meta::DOMEvent+JS",
+ start: 21,
+ end: 24,
+ submarkers: [{
+ start: 21,
+ end: 22,
+ name: "DOMEvent",
+ eventPhase: 3,
+ type: "foo3"
+ }, {
+ start: 23,
+ end: 24,
+ name: "Javascript",
+ stack: 3,
+ endStack: 4
+ }],
+ type: "foo3",
+ eventPhase: 3,
+ stack: 3,
+ endStack: 4
+ }, {
+ name: "meta::DOMEvent+JS",
+ start: 25,
+ end: 28,
+ submarkers: [{
+ start: 25,
+ end: 26,
+ name: "DOMEvent",
+ eventPhase: 4,
+ type: "foo4"
+ }, {
+ start: 27,
+ end: 28,
+ name: "Javascript",
+ stack: 5,
+ endStack: 6
+ }],
+ type: "foo4",
+ eventPhase: 4,
+ stack: 5,
+ endStack: 6
+ }, {
+ name: "Javascript",
+ start: 29,
+ end: 32,
+ submarkers: [{
+ start: 29,
+ end: 30,
+ name: "Javascript",
+ stack: 7,
+ endStack: 8
+ }, {
+ start: 31,
+ end: 32,
+ name: "Javascript",
+ stack: 9,
+ endStack: 10
+ }]
+ }, {
+ start: 33,
+ end: 34,
+ name: "DOMEvent",
+ eventPhase: 5,
+ type: "foo5"
+ }, {
+ start: 35,
+ end: 36,
+ name: "DOMEvent",
+ eventPhase: 6,
+ type: "foo6"
+ }, {
+ name: "meta::DOMEvent+JS",
+ start: 37,
+ end: 40,
+ submarkers: [{
+ start: 37,
+ end: 38,
+ name: "DOMEvent",
+ eventPhase: 7,
+ type: "foo6"
+ }, {
+ start: 39,
+ end: 40,
+ name: "Javascript",
+ stack: 11,
+ endStack: 12
+ }],
+ type: "foo6",
+ eventPhase: 7,
+ stack: 11,
+ endStack: 12
+ }, {
+ name: "Javascript",
+ start: 41,
+ end: 44,
+ submarkers: [{
+ start: 41,
+ end: 42,
+ name: "Javascript",
+ stack: 13,
+ endStack: 14
+ }, {
+ start: 43,
+ end: 44,
+ name: "Javascript",
+ stack: 15,
+ endStack: 16
+ }]
+ }, {
+ start: 45,
+ end: 46,
+ name: "GarbageCollection"
+ }]
+};
diff --git a/browser/devtools/performance/views/details-waterfall.js b/browser/devtools/performance/views/details-waterfall.js
index 49764caac43..e7a5bdd0cbe 100644
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -3,7 +3,8 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const MARKER_DETAILS_WIDTH = 300;
+const WATERFALL_RESIZE_EVENTS_DRAIN = 100; // ms
+const MARKER_DETAILS_WIDTH = 200;
/**
* Waterfall view containing the timeline markers, controlled by DetailsView.
@@ -26,24 +27,22 @@ 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);
+ window.addEventListener("resize", this._onResize);
- 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,10 +51,9 @@ 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);
+ window.removeEventListener("resize", this._onResize);
},
/**
@@ -69,7 +67,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,18 +79,15 @@ 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") {
this.details.render({ toolbox: gToolbox, marker, frames });
+ this._selected = marker;
}
if (event === "unselected") {
this.details.empty();
+ this._selected = null;
}
},
@@ -98,8 +95,10 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
* Called when the marker details view is resized.
*/
_onResize: function () {
- this.waterfall.recalculateBounds();
- this.render();
+ setNamedTimeout("waterfall-resize", WATERFALL_RESIZE_EVENTS_DRAIN, () => {
+ this._markersRoot.recalculateBounds();
+ this.render(OverviewView.getTimeInterval());
+ });
},
/**
@@ -107,7 +106,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
*/
_onObservedPrefChange: function(_, prefName) {
let blueprint = PerformanceController.getTimelineBlueprint();
- this.waterfall.setBlueprint(blueprint);
+ this._markersRoot.blueprint = blueprint;
},
/**
@@ -117,5 +116,59 @@ 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) {
+ let rootMarkerNode = WaterfallUtils.makeEmptyMarkerNode("(root)");
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ markerNode: rootMarkerNode,
+ markersList: markers
+ });
+
+ return rootMarkerNode;
+ },
+
+ /**
+ * 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);
+
+ // If an item was previously selected in this view, attempt to
+ // re-select it by traversing the newly created tree.
+ if (this._selected) {
+ let item = root.find(i => i.marker == this._selected);
+ if (item) {
+ item.focus();
+ }
+ }
+ },
+
toString: () => "[object WaterfallView]"
});
diff --git a/browser/devtools/projecteditor/test/.eslintrc b/browser/devtools/projecteditor/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/projecteditor/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/responsivedesign/test/.eslintrc b/browser/devtools/responsivedesign/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/scratchpad/test/.eslintrc b/browser/devtools/scratchpad/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/scratchpad/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/shadereditor/test/.eslintrc b/browser/devtools/shadereditor/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/shadereditor/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/shared/test/.eslintrc b/browser/devtools/shared/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/shared/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/shared/test/unit/.eslintrc b/browser/devtools/shared/test/unit/.eslintrc
new file mode 100644
index 00000000000..44135af644c
--- /dev/null
+++ b/browser/devtools/shared/test/unit/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the common devtools xpcshell eslintrc config.
+ "extends": "../../../.eslintrc.xpcshell"
+}
\ No newline at end of file
diff --git a/browser/devtools/shared/widgets/AbstractTreeItem.jsm b/browser/devtools/shared/widgets/AbstractTreeItem.jsm
index 308ef303612..e6f1efbabef 100644
--- a/browser/devtools/shared/widgets/AbstractTreeItem.jsm
+++ b/browser/devtools/shared/widgets/AbstractTreeItem.jsm
@@ -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,13 +121,12 @@ 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);
- EventEmitter.decorate(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 = {
@@ -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);
},
@@ -292,6 +325,33 @@ AbstractTreeItem.prototype = {
return this._childTreeItems[index];
},
+ /**
+ * Calls the provided function on all the descendants of this item.
+ * If this item was never expanded, then no descendents exist yet.
+ * @param function cb
+ */
+ traverse: function(cb) {
+ for (let child of this._childTreeItems) {
+ cb(child);
+ child.bfs();
+ }
+ },
+
+ /**
+ * Calls the provided function on all descendants of this item until
+ * a truthy value is returned by the predicate.
+ * @param function predicate
+ * @return AbstractTreeItem
+ */
+ find: function(predicate) {
+ for (let child of this._childTreeItems) {
+ if (predicate(child) || child.find(predicate)) {
+ return child;
+ }
+ }
+ return null;
+ },
+
/**
* Shows or hides all the children of this item in the tree. If neessary,
* populates this item with children.
@@ -315,17 +375,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 +398,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 +439,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 +459,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 +535,6 @@ AbstractTreeItem.prototype = {
if (!e.target.classList.contains("arrow")) {
this._onArrowClick(e);
}
-
this.focus();
},
@@ -477,5 +577,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);
}
};
diff --git a/browser/devtools/shared/widgets/FlameGraph.js b/browser/devtools/shared/widgets/FlameGraph.js
index 1ac1348a23c..dccd2f8569b 100644
--- a/browser/devtools/shared/widgets/FlameGraph.js
+++ b/browser/devtools/shared/widgets/FlameGraph.js
@@ -3,19 +3,32 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const { ViewHelpers } = require("resource:///modules/devtools/ViewHelpers.jsm");
-const { AbstractCanvasGraph, GraphArea, GraphAreaDragger } = require("resource:///modules/devtools/Graphs.jsm");
-const { Promise } = require("resource://gre/modules/Promise.jsm");
const { Task } = require("resource://gre/modules/Task.jsm");
-const { getColor } = require("devtools/shared/theme");
-const EventEmitter = require("devtools/toolkit/event-emitter");
-const FrameUtils = require("devtools/performance/frame-utils");
+const { ViewHelpers } = require("resource:///modules/devtools/ViewHelpers.jsm");
+const { setNamedTimeout, clearNamedTimeout } = require("resource:///modules/devtools/ViewHelpers.jsm");
+
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "EventEmitter",
+ "devtools/toolkit/event-emitter");
+
+loader.lazyRequireGetter(this, "getColor",
+ "devtools/shared/theme", true);
loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
"devtools/performance/global", true);
+loader.lazyRequireGetter(this, "FrameUtils",
+ "devtools/performance/frame-utils");
+
+loader.lazyImporter(this, "AbstractCanvasGraph",
+ "resource:///modules/devtools/Graphs.jsm");
+loader.lazyImporter(this, "GraphArea",
+ "resource:///modules/devtools/Graphs.jsm");
+loader.lazyImporter(this, "GraphAreaDragger",
+ "resource:///modules/devtools/Graphs.jsm");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
+
const L10N = new ViewHelpers.L10N();
const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
@@ -112,7 +125,7 @@ function FlameGraph(parent, sharpness) {
EventEmitter.decorate(this);
this._parent = parent;
- this._ready = Promise.defer();
+ this._ready = promise.defer();
this.setTheme();
diff --git a/browser/devtools/shared/widgets/graphs-frame.xhtml b/browser/devtools/shared/widgets/graphs-frame.xhtml
index d9835742bce..2dcac970972 100644
--- a/browser/devtools/shared/widgets/graphs-frame.xhtml
+++ b/browser/devtools/shared/widgets/graphs-frame.xhtml
@@ -15,6 +15,7 @@
overflow: hidden;
margin: 0;
padding: 0;
+ font-size: 0;
}
diff --git a/browser/devtools/sourceeditor/test/.eslintrc b/browser/devtools/sourceeditor/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/storage/test/.eslintrc b/browser/devtools/storage/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/storage/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/styleeditor/test/.eslintrc b/browser/devtools/styleeditor/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/styleeditor/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/styleinspector/rule-view.js b/browser/devtools/styleinspector/rule-view.js
index b8193624aeb..e49096a2483 100644
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -2685,13 +2685,45 @@ RuleEditor.prototype = {
return;
}
+ let ruleView = this.ruleView;
+ let elementStyle = ruleView._elementStyle;
+ let element = elementStyle.element;
+ let supportsUnmatchedRules =
+ this.rule.domRule.supportsModifySelectorUnmatched;
+
this.isEditing = true;
- this.rule.domRule.modifySelector(aValue).then(isModified => {
+ this.rule.domRule.modifySelector(element, aValue).then(response => {
this.isEditing = false;
- if (isModified) {
- this.ruleView.refreshPanel();
+ if (!supportsUnmatchedRules) {
+ if (response) {
+ this.ruleView.refreshPanel();
+ }
+ return;
+ }
+
+ let {ruleProps, isMatching} = response;
+ if (!ruleProps) {
+ return;
+ }
+
+ let newRule = new Rule(elementStyle, ruleProps);
+ let editor = new RuleEditor(ruleView, newRule);
+ let rules = elementStyle.rules;
+
+ rules.splice(rules.indexOf(this.rule), 1);
+ rules.push(newRule);
+ elementStyle._changed();
+
+ editor.element.setAttribute("unmatched", !isMatching);
+ this.element.parentNode.replaceChild(editor.element, this.element);
+
+ // Remove highlight for modified selector
+ if (ruleView.highlightedSelector &&
+ ruleView.highlightedSelector == this.rule.selectorText) {
+ ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
+ ruleView.highlightedSelector);
}
}).then(null, err => {
this.isEditing = false;
diff --git a/browser/devtools/styleinspector/test/.eslintrc b/browser/devtools/styleinspector/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/styleinspector/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/styleinspector/test/browser.ini b/browser/devtools/styleinspector/test/browser.ini
index 1bfc4fdc3ea..90709b1278a 100644
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -87,6 +87,9 @@ skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work wit
[browser_ruleview_edit-selector-commit.js]
[browser_ruleview_edit-selector_01.js]
[browser_ruleview_edit-selector_02.js]
+[browser_ruleview_edit-selector_03.js]
+[browser_ruleview_edit-selector_04.js]
+[browser_ruleview_edit-selector_05.js]
[browser_ruleview_eyedropper.js]
[browser_ruleview_filtereditor-appears-on-swatch-click.js]
[browser_ruleview_filtereditor-commit-on-ENTER.js]
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_add-rule_02.js b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_02.js
index ed5ab33c394..1c38c45e719 100644
--- a/browser/devtools/styleinspector/test/browser_ruleview_add-rule_02.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_02.js
@@ -26,7 +26,7 @@ add_task(function*() {
info("Selecting the test element");
yield selectNode("#testid", inspector);
- info("Waiting for rule view to change");
+ info("Waiting for rule view to update");
let onRuleViewChanged = once(view, "ruleview-changed");
info("Adding the new rule");
@@ -36,7 +36,7 @@ add_task(function*() {
yield testEditSelector(view, "span");
- info("Selecting the modified element");
+ info("Selecting the modified element with the new rule");
yield selectNode("span", inspector);
yield checkModifiedElement(view, "span");
});
@@ -49,14 +49,15 @@ function* testEditSelector(view, name) {
info("Entering a new selector name and committing");
editor.value = name;
- info("Waiting for rule view to refresh");
- let onRuleViewRefresh = once(view, "ruleview-refreshed");
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
- yield onRuleViewRefresh;
+ yield onRuleViewChanged;
- is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
}
function* checkModifiedElement(view, name) {
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_add-rule_03.js b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_03.js
index 0854e7d731e..a875b1639b3 100644
--- a/browser/devtools/styleinspector/test/browser_ruleview_add-rule_03.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_03.js
@@ -79,14 +79,14 @@ function* testEditSelector(view, name) {
info("Entering a new selector name: " + name);
editor.input.value = name;
- info("Waiting for rule view to refresh");
- let onRuleViewRefresh = once(view, "ruleview-refreshed");
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
- yield onRuleViewRefresh;
+ yield onRuleViewChanged;
- is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
}
function* checkModifiedElement(view, name, index) {
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js
index 0939cd2b65a..8cba65dbfab 100644
--- a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js
@@ -48,12 +48,7 @@ const TEST_DATA = [
];
add_task(function*() {
- yield addTab("data:text/html;charset=utf-8,test escaping selector change reverts back to original value");
-
- info("Creating the test document");
- content.document.body.innerHTML = PAGE_CONTENT;
-
- info("Opening the rule-view");
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
let {toolbox, inspector, view} = yield openRuleView();
info("Iterating over the test data");
@@ -88,7 +83,7 @@ function* runTestData(inspector, view, data) {
"Value is as expected: " + expected);
is(idRuleEditor.isEditing, false, "Selector is not being edited.")
} else {
- yield once(view, "ruleview-refreshed");
+ yield once(view, "ruleview-changed");
ok(getRuleViewRule(view, expected),
"Rule with " + name + " selector exists.");
}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js
index 0496f165fba..32806e909f5 100644
--- a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js
@@ -13,24 +13,19 @@ let PAGE_CONTENT = [
' }',
'',
'
Styled Node
',
- '
This is a span'
+ '
This is a span',
].join("\n");
add_task(function*() {
- yield addTab("data:text/html;charset=utf-8,test rule view selector changes");
-
- info("Creating the test document");
- content.document.body.innerHTML = PAGE_CONTENT;
-
- info("Opening the rule-view");
- let {toolbox, inspector, view} = yield openRuleView();
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
+ let {inspector, view} = yield openRuleView();
info("Selecting the test element");
yield selectNode("#testid", inspector);
yield testEditSelector(view, "span");
- info("Selecting the modified element");
- yield selectNode("#testid2", inspector);
+ info("Selecting the modified element with the new rule");
+ yield selectNode("span", inspector);
yield checkModifiedElement(view, "span");
});
@@ -48,16 +43,17 @@ function* testEditSelector(view, name) {
info("Entering a new selector name and committing");
editor.input.value = name;
- info("Waiting for rule view to refresh");
- let onRuleViewRefresh = once(view, "ruleview-refreshed");
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
- yield onRuleViewRefresh;
+ yield onRuleViewChanged;
- is(view._elementStyle.rules.length, 1, "Should have 1 rule.");
- is(getRuleViewRule(view, name), undefined,
- name + " selector has been removed.");
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
}
function* checkModifiedElement(view, name) {
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
index 494a2eff8e3..1ef554956c8 100644
--- a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
@@ -12,7 +12,7 @@ let PAGE_CONTENT = [
' .testclass {',
' text-align: center;',
' }',
- ' #testid3:first-letter {',
+ ' #testid3::first-letter {',
' text-decoration: "italic"',
' }',
'',
@@ -23,21 +23,16 @@ let PAGE_CONTENT = [
].join("\n");
add_task(function*() {
- yield addTab("data:text/html;charset=utf-8,test rule view selector changes");
-
- info("Creating the test document");
- content.document.body.innerHTML = PAGE_CONTENT;
-
- info("Opening the rule-view");
- let {toolbox, inspector, view} = yield openRuleView();
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
+ let {inspector, view} = yield openRuleView();
info("Selecting the test element");
yield selectNode(".testclass", inspector);
- yield testEditSelector(view, "div:nth-child(2)");
+ yield testEditSelector(view, "div:nth-child(1)");
info("Selecting the modified element");
yield selectNode("#testid", inspector);
- yield checkModifiedElement(view, "div:nth-child(2)");
+ yield checkModifiedElement(view, "div:nth-child(1)");
info("Selecting the test element");
yield selectNode("#testid3", inspector);
@@ -63,16 +58,20 @@ function* testEditSelector(view, name) {
info("Entering a new selector name: " + name);
editor.input.value = name;
- info("Waiting for rule view to refresh");
- let onRuleViewRefresh = once(view, "ruleview-refreshed");
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
- yield onRuleViewRefresh;
+ yield onRuleViewChanged;
- is(view._elementStyle.rules.length, 1, "Should have 1 rule.");
- is(getRuleViewRule(view, name), undefined,
- name + " selector has been removed.");
+ is(view._elementStyle.rules.length, 2, "Should have 2 rule.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+
+ let newRuleEditor = getRuleViewRuleEditor(view, 1) ||
+ getRuleViewRuleEditor(view, 1, 0);
+ ok(newRuleEditor.element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
}
function* checkModifiedElement(view, name) {
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_03.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_03.js
new file mode 100644
index 00000000000..14e88786aab
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_03.js
@@ -0,0 +1,46 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing selector inplace-editor behaviors in the rule-view with invalid
+// selectors
+
+let TEST_URI = [
+ '',
+ '
Styled Node
',
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+ yield selectNode(".testclass", inspector);
+ yield testEditSelector(view, "asd@:::!");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ is(getRuleViewRule(view, name), undefined,
+ "Rule with " + name + " selector should not exist.");
+ ok(getRuleViewRule(view, ".testclass"),
+ "Rule with .testclass selector exists.");
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_04.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_04.js
new file mode 100644
index 00000000000..84c9f9774fe
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_04.js
@@ -0,0 +1,67 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the selector highlighter is removed when modifying a selector and
+// the selector highlighter works for the newly added unmatched rule.
+
+const TEST_URI = [
+ '',
+ '
Test the selector highlighter
'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ ok(!view.selectorHighlighter, "No selectorhighlighter exist in the rule-view");
+
+ yield selectNode("p", inspector);
+ yield testSelectorHighlight(view, "p");
+ yield testEditSelector(view, "body");
+ yield testSelectorHighlight(view, "body");
+});
+
+function* testSelectorHighlight(view, name) {
+ info("Test creating selector highlighter");
+
+ info("Clicking on a selector icon");
+ let icon = getRuleViewSelectorHighlighterIcon(view, name);
+
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+ EventUtils.synthesizeMouseAtCenter(icon, {}, view.doc.defaultView);
+ let isVisible = yield onToggled;
+
+ ok(view.selectorHighlighter, "The selectorhighlighter instance was created");
+ ok(isVisible, "The toggle event says the highlighter is visible");
+}
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Waiting for rule view to update");
+ let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ let isVisible = yield onToggled;
+
+ ok(!view.highlightedSelector, "The selectorhighlighter instance was removed");
+ ok(!isVisible, "The toggle event says the highlighter is not visible");
+}
diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_05.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_05.js
new file mode 100644
index 00000000000..b93fddf9e15
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_05.js
@@ -0,0 +1,108 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that adding a new property of an unmatched rule works properly.
+
+let TEST_URI = [
+ '',
+ '
Styled Node
',
+ '
This is a span'
+].join("\n");
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openRuleView();
+
+ info("Selecting the test element");
+ yield selectNode("#testid", inspector);
+ yield testEditSelector(view, "span");
+ yield testAddProperty(view);
+
+ info("Selecting the modified element with the new rule");
+ yield selectNode("span", inspector);
+ yield checkModifiedElement(view, "span");
+});
+
+function* testEditSelector(view, name) {
+ info("Test editing existing selector fields");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing an existing selector name in the rule-view");
+ let editor = yield focusEditableField(ruleEditor.selectorText);
+
+ is(inplaceEditor(ruleEditor.selectorText), editor,
+ "The selector editor got focused");
+
+ info("Entering a new selector name and committing");
+ editor.input.value = name;
+
+ info("Waiting for rule view to update");
+ let onRuleViewChanged = once(view, "ruleview-changed");
+
+ info("Entering the commit key");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield onRuleViewChanged;
+
+ is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+ ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
+ "Rule with " + name + " does not match the current element.");
+}
+
+function* checkModifiedElement(view, name) {
+ is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
+ ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
+
+function* testAddProperty(view) {
+ info("Test creating a new property");
+
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ info("Focusing a new property name in the rule-view");
+ let editor = yield focusEditableField(ruleEditor.closeBrace);
+
+ is(inplaceEditor(ruleEditor.newPropSpan), editor,
+ "The new property editor got focused");
+ let input = editor.input;
+
+ info("Entering text-align in the property name editor");
+ input.value = "text-align";
+
+ info("Pressing return to commit and focus the new value field");
+ let onValueFocus = once(ruleEditor.element, "focus", true);
+ let onModifications = ruleEditor.rule._applyingModifications;
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+ yield onValueFocus;
+ yield onModifications;
+
+ // Getting the new value editor after focus
+ editor = inplaceEditor(view.doc.activeElement);
+ let textProp = ruleEditor.rule.textProps[0];
+
+ is(ruleEditor.rule.textProps.length, 1, "Created a new text property.");
+ is(ruleEditor.propertyList.children.length, 1, "Created a property editor.");
+ is(editor, inplaceEditor(textProp.editor.valueSpan),
+ "Editing the value span now.");
+
+ info("Entering a value and bluring the field to expect a rule change");
+ editor.input.value = "center";
+ let onBlur = once(editor.input, "blur");
+ onModifications = ruleEditor.rule._applyingModifications;
+ editor.input.blur();
+ yield onBlur;
+ yield onModifications;
+
+ is(textProp.value, "center", "Text prop should have been changed.");
+ is(textProp.overridden, false, "Property should not be overridden");
+}
diff --git a/browser/devtools/styleinspector/test/unit/.eslintrc b/browser/devtools/styleinspector/test/unit/.eslintrc
new file mode 100644
index 00000000000..44135af644c
--- /dev/null
+++ b/browser/devtools/styleinspector/test/unit/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the common devtools xpcshell eslintrc config.
+ "extends": "../../../.eslintrc.xpcshell"
+}
\ No newline at end of file
diff --git a/browser/devtools/tilt/test/.eslintrc b/browser/devtools/tilt/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/tilt/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/webaudioeditor/test/.eslintrc b/browser/devtools/webaudioeditor/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/webaudioeditor/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/webconsole/test/.eslintrc b/browser/devtools/webconsole/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/webconsole/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/webide/test/.eslintrc b/browser/devtools/webide/test/.eslintrc
new file mode 100644
index 00000000000..e6d23d3777b
--- /dev/null
+++ b/browser/devtools/webide/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../.eslintrc.mochitests"
+}
diff --git a/browser/devtools/webide/themes/throbber.svg b/browser/devtools/webide/themes/throbber.svg
index 2f9a41181e8..d89fb3851ac 100644
--- a/browser/devtools/webide/themes/throbber.svg
+++ b/browser/devtools/webide/themes/throbber.svg
@@ -1,18 +1,22 @@
-