From 91df9dbb13e9b79b2b78d8dcca1f0a2bdba8de3e Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Tue, 9 Jun 2015 20:34:51 -0700 Subject: [PATCH 01/16] Bug 1152992 - If markers do not have a definition, classify them as "Unknown" in the perftools. r=vp --- .../performance/modules/logic/marker-utils.js | 45 ++++++++-- .../modules/logic/recording-utils.js | 50 ----------- .../modules/logic/waterfall-utils.js | 14 ++- .../devtools/performance/modules/markers.js | 12 ++- .../performance/modules/widgets/graphs.js | 15 ++-- .../modules/widgets/marker-details.js | 2 - .../modules/widgets/marker-view.js | 36 +++----- .../modules/widgets/markers-overview.js | 66 +++++++++----- .../performance/performance-controller.js | 10 --- browser/devtools/performance/test/browser.ini | 2 - .../performance/test/browser_marker-utils.js | 70 --------------- .../test/browser_timeline-blueprint.js | 34 ------- .../test/browser_timeline-filters.js | 36 +++++++- browser/devtools/performance/test/head.js | 1 + .../devtools/performance/test/unit/head.js | 3 + .../test/unit/test_marker-blueprint.js | 29 ++++++ .../test/unit/test_marker-utils.js | 88 +++++++++++++++++++ .../performance/test/unit/xpcshell.ini | 4 +- .../performance/views/details-waterfall.js | 9 +- .../devtools/performance/views/overview.js | 6 +- .../browser/devtools/timeline.properties | 1 + 21 files changed, 284 insertions(+), 249 deletions(-) delete mode 100644 browser/devtools/performance/test/browser_marker-utils.js delete mode 100644 browser/devtools/performance/test/browser_timeline-blueprint.js create mode 100644 browser/devtools/performance/test/unit/test_marker-blueprint.js create mode 100644 browser/devtools/performance/test/unit/test_marker-utils.js diff --git a/browser/devtools/performance/modules/logic/marker-utils.js b/browser/devtools/performance/modules/logic/marker-utils.js index 43608eaac9df..55a4edbe20cf 100644 --- a/browser/devtools/performance/modules/logic/marker-utils.js +++ b/browser/devtools/performance/modules/logic/marker-utils.js @@ -8,7 +8,7 @@ * and parsing out the blueprint to generate correct values for markers. */ -const { Ci } = require("chrome"); +const { Cu, Ci } = require("chrome"); loader.lazyRequireGetter(this, "L10N", "devtools/performance/global", true); @@ -22,6 +22,18 @@ loader.lazyRequireGetter(this, "WebConsoleUtils", // String used to fill in platform data when it should be hidden. const GECKO_SYMBOL = "(Gecko)"; +/** + * Takes a marker, blueprint, and filter list and + * determines if this marker should be filtered or not. + */ +function isMarkerValid (marker, filter) { + let isUnknown = !(marker.name in TIMELINE_BLUEPRINT); + if (isUnknown) { + return filter.indexOf("UNKNOWN") === -1; + } + return filter.indexOf(marker.name) === -1; +} + /** * Returns the correct label to display for passed in marker, based * off of the blueprints. @@ -30,7 +42,7 @@ const GECKO_SYMBOL = "(Gecko)"; * @return {string} */ function getMarkerLabel (marker) { - let blueprint = TIMELINE_BLUEPRINT[marker.name]; + let blueprint = getBlueprintFor(marker); // Either use the label function in the blueprint, or use it directly // as a string. return typeof blueprint.label === "function" ? blueprint.label(marker) : blueprint.label; @@ -44,7 +56,7 @@ function getMarkerLabel (marker) { * @return {string} */ function getMarkerClassName (type) { - let blueprint = TIMELINE_BLUEPRINT[type]; + let blueprint = getBlueprintFor({ name: type }); // Either use the label function in the blueprint, or use it directly // as a string. let className = typeof blueprint.label === "function" ? blueprint.label() : blueprint.label; @@ -72,7 +84,7 @@ function getMarkerClassName (type) { * @return {Array} */ function getMarkerFields (marker) { - let blueprint = TIMELINE_BLUEPRINT[marker.name]; + let blueprint = getBlueprintFor(marker); // If blueprint.fields is a function, use that if (typeof blueprint.fields === "function") { @@ -111,7 +123,7 @@ const DOM = { * @return {Array} */ buildFields: function (doc, marker) { - let blueprint = TIMELINE_BLUEPRINT[marker.name]; + let blueprint = getBlueprintFor(marker); let fields = getMarkerFields(marker); return fields.map(({ label, value }) => DOM.buildNameValueLabel(doc, label, value)); @@ -125,7 +137,7 @@ const DOM = { * @return {Element} */ buildTitle: function (doc, marker) { - let blueprint = TIMELINE_BLUEPRINT[marker.name]; + let blueprint = getBlueprintFor(marker); let hbox = doc.createElement("hbox"); hbox.setAttribute("align", "center"); @@ -377,6 +389,14 @@ const JS_MARKER_MAP = { * A series of formatters used by the blueprint. */ const Formatters = { + /** + * Uses the marker name as the label for markers that do not have + * a blueprint entry. Uses "Other" in the marker filter menu. + */ + UnknownLabel: function (marker={}) { + return marker.name || L10N.getStr("timeline.label.unknown"); + }, + GCLabel: function (marker={}) { let label = L10N.getStr("timeline.label.garbageCollection"); // Only if a `nonincrementalReason` exists, do we want to label @@ -444,9 +464,22 @@ const Formatters = { }, }; +/** + * Takes a marker and returns the definition for that marker type, + * falling back to the UNKNOWN definition if undefined. + * + * @param {Marker} marker + * @return {object} + */ +function getBlueprintFor (marker) { + return TIMELINE_BLUEPRINT[marker.name] || TIMELINE_BLUEPRINT.UNKNOWN; +} + +exports.isMarkerValid = isMarkerValid; exports.getMarkerLabel = getMarkerLabel; exports.getMarkerClassName = getMarkerClassName; exports.getMarkerFields = getMarkerFields; exports.DOM = DOM; exports.CollapseFunctions = CollapseFunctions; exports.Formatters = Formatters; +exports.getBlueprintFor = getBlueprintFor; diff --git a/browser/devtools/performance/modules/logic/recording-utils.js b/browser/devtools/performance/modules/logic/recording-utils.js index af7f7fc3d563..5569d99b0750 100644 --- a/browser/devtools/performance/modules/logic/recording-utils.js +++ b/browser/devtools/performance/modules/logic/recording-utils.js @@ -196,55 +196,6 @@ function getProfileThreadFromAllocations(allocations) { return thread; } -/** - * Gets the current timeline blueprint without the hidden markers. - * - * @param blueprint - * The default timeline blueprint. - * @param array hiddenMarkers - * A list of hidden markers' names. - * @return object - * The filtered timeline blueprint. - */ -function getFilteredBlueprint({ blueprint, hiddenMarkers }) { - // Clone functions here just to prevent an error, as the blueprint - // contains functions (even though we do not use them). - let filteredBlueprint = Cu.cloneInto(blueprint, {}, { cloneFunctions: true }); - let maybeRemovedGroups = new Set(); - let removedGroups = new Set(); - - // 1. Remove hidden markers from the blueprint. - - for (let hiddenMarkerName of hiddenMarkers) { - maybeRemovedGroups.add(filteredBlueprint[hiddenMarkerName].group); - delete filteredBlueprint[hiddenMarkerName]; - } - - // 2. Get a list of all the groups that will be removed. - - for (let maybeRemovedGroup of maybeRemovedGroups) { - let markerNames = Object.keys(filteredBlueprint); - let isGroupRemoved = markerNames.every(e => filteredBlueprint[e].group != maybeRemovedGroup); - if (isGroupRemoved) { - removedGroups.add(maybeRemovedGroup); - } - } - - // 3. Offset groups so that their indices are consecutive. - - for (let removedGroup of removedGroups) { - let markerNames = Object.keys(filteredBlueprint); - for (let markerName of markerNames) { - let markerDetails = filteredBlueprint[markerName]; - if (markerDetails.group > removedGroup) { - markerDetails.group--; - } - } - } - - return filteredBlueprint; -}; - /** * Deduplicates a profile by deduplicating stacks, frames, and strings. * @@ -571,7 +522,6 @@ exports.offsetSampleTimes = offsetSampleTimes; exports.offsetMarkerTimes = offsetMarkerTimes; exports.offsetAndScaleTimestamps = offsetAndScaleTimestamps; exports.getProfileThreadFromAllocations = getProfileThreadFromAllocations; -exports.getFilteredBlueprint = getFilteredBlueprint; exports.deflateProfile = deflateProfile; exports.deflateThread = deflateThread; exports.UniqueStrings = UniqueStrings; diff --git a/browser/devtools/performance/modules/logic/waterfall-utils.js b/browser/devtools/performance/modules/logic/waterfall-utils.js index d2228fb43061..b1bf06f654dd 100644 --- a/browser/devtools/performance/modules/logic/waterfall-utils.js +++ b/browser/devtools/performance/modules/logic/waterfall-utils.js @@ -7,27 +7,25 @@ * Utility functions for collapsing markers into a waterfall. */ -loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT", - "devtools/performance/markers", true); +loader.lazyRequireGetter(this, "getBlueprintFor", + "devtools/performance/marker-utils", true); /** * Collapses markers into a tree-like structure. * @param object markerNode * @param array markersList - * @param ?object blueprint */ -function collapseMarkersIntoNode({ markerNode, markersList, blueprint }) { +function collapseMarkersIntoNode({ markerNode, markersList }) { let { getCurrentParentNode, collapseMarker, addParentNode, popParentNode } = createParentNodeFactory(markerNode); - blueprint = blueprint || TIMELINE_BLUEPRINT; for (let i = 0, len = markersList.length; i < len; i++) { let curr = markersList[i]; let parentNode = getCurrentParentNode(); - let def = blueprint[curr.name]; - let collapse = def.collapseFunc || (() => null); + let blueprint = getBlueprintFor(curr); + + let collapse = blueprint.collapseFunc || (() => null); let peek = distance => markersList[i + distance]; - let foundParent = false; let collapseInfo = collapse(parentNode, curr, peek); if (collapseInfo) { diff --git a/browser/devtools/performance/modules/markers.js b/browser/devtools/performance/modules/markers.js index 984eb4aa79bc..177a4d4fb6f0 100644 --- a/browser/devtools/performance/modules/markers.js +++ b/browser/devtools/performance/modules/markers.js @@ -55,6 +55,16 @@ const { Formatters, CollapseFunctions: collapse } = require("devtools/performanc * updated as well. */ const TIMELINE_BLUEPRINT = { + /* Default definition used for markers that occur but + * are not defined here. Should ultimately be defined, but this gives + * us room to work on the front end separately from the platform. */ + "UNKNOWN": { + group: 2, + colorName: "graphs-grey", + collapseFunc: collapse.child, + label: Formatters.UnknownLabel + }, + /* Group 0 - Reflow and Rendering pipeline */ "Styles": { group: 0, @@ -131,7 +141,7 @@ const TIMELINE_BLUEPRINT = { /* Group 2 - User Controlled */ "ConsoleTime": { group: 2, - colorName: "graphs-grey", + colorName: "graphs-blue", label: sublabelForProperty(L10N.getStr("timeline.label.consoleTime"), "causeName"), fields: [{ property: "causeName", diff --git a/browser/devtools/performance/modules/widgets/graphs.js b/browser/devtools/performance/modules/widgets/graphs.js index 468ab8dd7e49..bb5b50b126fe 100644 --- a/browser/devtools/performance/modules/widgets/graphs.js +++ b/browser/devtools/performance/modules/widgets/graphs.js @@ -135,8 +135,8 @@ MemoryGraph.prototype = Heritage.extend(PerformanceGraph.prototype, { } }); -function TimelineGraph(parent, blueprint) { - MarkersOverview.call(this, parent, blueprint); +function TimelineGraph(parent, filter) { + MarkersOverview.call(this, parent, filter); } TimelineGraph.prototype = Heritage.extend(MarkersOverview.prototype, { @@ -163,7 +163,6 @@ const GRAPH_DEFINITIONS = { timeline: { constructor: TimelineGraph, selector: "#markers-overview", - needsBlueprints: true, primaryLink: true } }; @@ -174,15 +173,15 @@ const GRAPH_DEFINITIONS = { * * @param {object} definition * @param {DOMElement} root - * @param {function} getBlueprint + * @param {function} getFilter * @param {function} getTheme */ -function GraphsController ({ definition, root, getBlueprint, getTheme }) { +function GraphsController ({ definition, root, getFilter, getTheme }) { this._graphs = {}; this._enabled = new Set(); this._definition = definition || GRAPH_DEFINITIONS; this._root = root; - this._getBlueprint = getBlueprint; + this._getFilter = getFilter; this._getTheme = getTheme; this._primaryLink = Object.keys(this._definition).filter(name => this._definition[name].primaryLink)[0]; this.$ = root.ownerDocument.querySelector.bind(root.ownerDocument); @@ -369,8 +368,8 @@ GraphsController.prototype = { _construct: Task.async(function *(graphName) { let def = this._definition[graphName]; let el = this.$(def.selector); - let blueprint = def.needsBlueprints ? this._getBlueprint() : void 0; - let graph = this._graphs[graphName] = new def.constructor(el, blueprint); + let filter = this._getFilter(); + let graph = this._graphs[graphName] = new def.constructor(el, filter); graph.graphName = graphName; yield graph.ready(); diff --git a/browser/devtools/performance/modules/widgets/marker-details.js b/browser/devtools/performance/modules/widgets/marker-details.js index 5772ace46882..12780ea2a346 100644 --- a/browser/devtools/performance/modules/widgets/marker-details.js +++ b/browser/devtools/performance/modules/widgets/marker-details.js @@ -13,8 +13,6 @@ loader.lazyRequireGetter(this, "EventEmitter", "devtools/toolkit/event-emitter"); loader.lazyRequireGetter(this, "L10N", "devtools/performance/global", true); -loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT", - "devtools/performance/markers", true); loader.lazyRequireGetter(this, "MarkerUtils", "devtools/performance/marker-utils"); diff --git a/browser/devtools/performance/modules/widgets/marker-view.js b/browser/devtools/performance/modules/widgets/marker-view.js index 7a33eecafd69..4f8d1058ce52 100644 --- a/browser/devtools/performance/modules/widgets/marker-view.js +++ b/browser/devtools/performance/modules/widgets/marker-view.js @@ -11,11 +11,10 @@ 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/markers"); - loader.lazyRequireGetter(this, "MarkerUtils", "devtools/performance/marker-utils"); + const HTML_NS = "http://www.w3.org/1999/xhtml"; const LEVEL_INDENT = 10; // px @@ -60,15 +59,14 @@ MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, { }, /** - * Sets a list of names and colors used to paint markers. - * @see TIMELINE_BLUEPRINT in timeline/widgets/global.js - * @param object blueprint + * Sets a list of marker types to be filtered out of this view. + * @param Array filter */ - set blueprint(blueprint) { - this.root._blueprint = blueprint; + set filter(filter) { + this.root._filter = filter; }, - get blueprint() { - return this.root._blueprint; + get filter() { + return this.root._filter; }, /** @@ -139,7 +137,6 @@ MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, { 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; @@ -147,17 +144,15 @@ MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, { 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}.`); - } + // Skip filtered markers + if (!MarkerUtils.isMarkerValid(marker, this.filter)) { continue; } + if (!isMarkerInRange(marker, startTime|0, endTime|0)) { continue; } + children.push(new MarkerView({ owner: this, marker: marker, @@ -175,15 +170,12 @@ MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, { */ _buildMarkerCells: function(doc, targetNode, arrowNode) { let marker = this.marker; - let style = this.root._blueprint[marker.name]; + let blueprint = MarkerUtils.getBlueprintFor(marker); 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); + let sidebarCell = this._buildMarkerSidebar(doc, blueprint, marker); + let timebarCell = this._buildMarkerTimebar(doc, blueprint, marker, startTime, endTime, arrowNode); targetNode.appendChild(sidebarCell); targetNode.appendChild(timebarCell); diff --git a/browser/devtools/performance/modules/widgets/markers-overview.js b/browser/devtools/performance/modules/widgets/markers-overview.js index 3d75a5569640..53bfbf2ad1b2 100644 --- a/browser/devtools/performance/modules/widgets/markers-overview.js +++ b/browser/devtools/performance/modules/widgets/markers-overview.js @@ -21,6 +21,10 @@ loader.lazyRequireGetter(this, "L10N", "devtools/performance/global", true); loader.lazyRequireGetter(this, "TickUtils", "devtools/performance/waterfall-ticks", true); +loader.lazyRequireGetter(this, "MarkerUtils", + "devtools/performance/marker-utils"); +loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT", + "devtools/performance/markers", true); const OVERVIEW_HEADER_HEIGHT = 14; // px const OVERVIEW_ROW_HEIGHT = 11; // px @@ -28,14 +32,12 @@ const OVERVIEW_ROW_HEIGHT = 11; // px const OVERVIEW_SELECTION_LINE_COLOR = "#666"; const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555"; -const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100; const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif"; const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px -const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1]; const OVERVIEW_MARKER_WIDTH_MIN = 4; // px const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px @@ -44,13 +46,13 @@ const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px * * @param nsIDOMNode parent * The parent node holding the overview. - * @param Object blueprint - * List of names and colors defining markers. + * @param Array filter + * List of names of marker types that should not be shown. */ -function MarkersOverview(parent, blueprint, ...args) { +function MarkersOverview(parent, filter=[], ...args) { AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]); this.setTheme(); - this.setBlueprint(blueprint); + this.setFilter(filter); } MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { @@ -64,21 +66,36 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { * Compute the height of the overview. */ get fixedHeight() { - return this.headerHeight + this.rowHeight * (this._lastGroup + 1); + return this.headerHeight + this.rowHeight * this._numberOfGroups; }, /** - * List of names and colors used to paint this overview. - * @see TIMELINE_BLUEPRINT in timeline/widgets/global.js + * List of marker types that should not be shown in the graph. */ - setBlueprint: function(blueprint) { + setFilter: function (filter) { this._paintBatches = new Map(); - this._lastGroup = 0; + this._filter = filter; + this._groupMap = Object.create(null); - for (let type in blueprint) { - this._paintBatches.set(type, { style: blueprint[type], batch: [] }); - this._lastGroup = Math.max(this._lastGroup, blueprint[type].group || 0); + let observedGroups = new Set(); + + for (let type in TIMELINE_BLUEPRINT) { + if (filter.indexOf(type) !== -1) { + continue; + } + this._paintBatches.set(type, { definition: TIMELINE_BLUEPRINT[type], batch: [] }); + observedGroups.add(TIMELINE_BLUEPRINT[type].group); } + + // Take our set of observed groups and order them and map + // the group numbers to fill in the holes via `_groupMap`. + // This normalizes our rows by removing rows that aren't used + // if filters are enabled. + let actualPosition = 0; + for (let groupNumber of Array.from(observedGroups).sort()) { + this._groupMap[groupNumber] = actualPosition++; + } + this._numberOfGroups = Object.keys(this._groupMap).length; }, /** @@ -103,17 +120,19 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { // Group markers into separate paint batches. This is necessary to // draw all markers sharing the same style at once. - for (let marker of markers) { - let markerType = this._paintBatches.get(marker.name); - if (markerType) { - markerType.batch.push(marker); + // Again skip over markers that we're filtering -- we don't want them + // to be labeled as "Unknown" + if (!MarkerUtils.isMarkerValid(marker, this._filter)) { + continue; } + + let markerType = this._paintBatches.get(marker.name) || this._paintBatches.get("UNKNOWN"); + markerType.batch.push(marker); } // Calculate each row's height, and the time-based scaling. - let totalGroups = this._lastGroup + 1; let groupHeight = this.rowHeight * this._pixelRatio; let groupPadding = this.groupPadding * this._pixelRatio; let headerHeight = this.headerHeight * this._pixelRatio; @@ -132,7 +151,7 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { ctx.fillStyle = this.alternatingBackgroundColor; ctx.beginPath(); - for (let i = 0; i < totalGroups; i += 2) { + for (let i = 0; i < this._numberOfGroups; i += 2) { let top = headerHeight + i * groupHeight; ctx.rect(0, top, canvasWidth, groupHeight); } @@ -172,11 +191,12 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { // Draw the timeline markers. - for (let [, { style, batch }] of this._paintBatches) { - let top = headerHeight + style.group * groupHeight + groupPadding / 2; + for (let [, { definition, batch }] of this._paintBatches) { + let group = this._groupMap[definition.group]; + let top = headerHeight + group * groupHeight + groupPadding / 2; let height = groupHeight - groupPadding; - let color = getColor(style.colorName, this.theme); + let color = getColor(definition.colorName, this.theme); ctx.fillStyle = color; ctx.beginPath(); diff --git a/browser/devtools/performance/performance-controller.js b/browser/devtools/performance/performance-controller.js index f43f9dfa78df..4fbc1e758007 100644 --- a/browser/devtools/performance/performance-controller.js +++ b/browser/devtools/performance/performance-controller.js @@ -398,16 +398,6 @@ let PerformanceController = { return null; }, - /** - * Gets the current timeline blueprint without the hidden markers. - * @return object - */ - getTimelineBlueprint: function() { - let blueprint = TIMELINE_BLUEPRINT; - let hiddenMarkers = this.getPref("hidden-markers"); - return RecordingUtils.getFilteredBlueprint({ blueprint, hiddenMarkers }); - }, - /** * Fired from RecordingsView, we listen on the PerformanceController so we can * set it here and re-emit on the controller, where all views can listen. diff --git a/browser/devtools/performance/test/browser.ini b/browser/devtools/performance/test/browser.ini index b296a57dd350..825586de938e 100644 --- a/browser/devtools/performance/test/browser.ini +++ b/browser/devtools/performance/test/browser.ini @@ -13,7 +13,6 @@ support-files = # that need to be moved over to performance tool [browser_aaa-run-first-leaktest.js] -[browser_marker-utils.js] [browser_markers-cycle-collection.js] [browser_markers-gc.js] [browser_markers-parse-html.js] @@ -137,7 +136,6 @@ support-files = [browser_profiler_tree-view-08.js] [browser_profiler_tree-view-09.js] [browser_profiler_tree-view-10.js] -[browser_timeline-blueprint.js] [browser_timeline-filters.js] [browser_timeline-waterfall-background.js] [browser_timeline-waterfall-generic.js] diff --git a/browser/devtools/performance/test/browser_marker-utils.js b/browser/devtools/performance/test/browser_marker-utils.js deleted file mode 100644 index 86f7e5d89c05..000000000000 --- a/browser/devtools/performance/test/browser_marker-utils.js +++ /dev/null @@ -1,70 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -/** - * Tests the marker utils methods. - */ - -function* spawnTest() { - let { TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/markers"); - let Utils = devtools.require("devtools/performance/marker-utils"); - - Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false); - - is(Utils.getMarkerLabel({ name: "DOMEvent" }), "DOM Event", - "getMarkerLabel() returns a simple label"); - is(Utils.getMarkerLabel({ name: "Javascript", causeName: "setTimeout handler" }), "setTimeout", - "getMarkerLabel() returns a label defined via function"); - - ok(Utils.getMarkerFields({ name: "Paint" }).length === 0, - "getMarkerFields() returns an empty array when no fields defined"); - - let fields = Utils.getMarkerFields({ name: "ConsoleTime", causeName: "snowstorm" }); - is(fields[0].label, "Timer Name:", "getMarkerFields() returns an array with proper label"); - is(fields[0].value, "snowstorm", "getMarkerFields() returns an array with proper value"); - - fields = Utils.getMarkerFields({ name: "DOMEvent", type: "mouseclick" }); - is(fields.length, 1, "getMarkerFields() ignores fields that are not found on marker"); - is(fields[0].label, "Event Type:", "getMarkerFields() returns an array with proper label"); - is(fields[0].value, "mouseclick", "getMarkerFields() returns an array with proper value"); - - fields = Utils.getMarkerFields({ name: "DOMEvent", eventPhase: Ci.nsIDOMEvent.AT_TARGET, type: "mouseclick" }); - is(fields.length, 2, "getMarkerFields() returns multiple fields when using a fields function"); - is(fields[0].label, "Event Type:", "getMarkerFields() correctly returns fields via function (1)"); - is(fields[0].value, "mouseclick", "getMarkerFields() correctly returns fields via function (2)"); - is(fields[1].label, "Phase:", "getMarkerFields() correctly returns fields via function (3)"); - is(fields[1].value, "Target", "getMarkerFields() correctly returns fields via function (4)"); - - is(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "(Gecko)", - "Correctly obfuscates JS markers when platform data is off."); - Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true); - is(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "Some Platform Field", - "Correctly deobfuscates JS markers when platform data is on."); - - is(Utils.getMarkerClassName("Javascript"), "Function Call", - "getMarkerClassName() returns correct string when defined via function"); - is(Utils.getMarkerClassName("GarbageCollection"), "GC Event", - "getMarkerClassName() returns correct string when defined via function"); - is(Utils.getMarkerClassName("Reflow"), "Layout", - "getMarkerClassName() returns correct string when defined via string"); - - TIMELINE_BLUEPRINT["fakemarker"] = { group: 0 }; - try { - Utils.getMarkerClassName("fakemarker"); - ok(false, "getMarkerClassName() should throw when no label on blueprint."); - } catch (e) { - ok(true, "getMarkerClassName() should throw when no label on blueprint."); - } - - TIMELINE_BLUEPRINT["fakemarker"] = { group: 0, label: () => void 0}; - try { - Utils.getMarkerClassName("fakemarker"); - ok(false, "getMarkerClassName() should throw when label function returnd undefined."); - } catch (e) { - ok(true, "getMarkerClassName() should throw when label function returnd undefined."); - } - - delete TIMELINE_BLUEPRINT["fakemarker"]; - - finish(); -} diff --git a/browser/devtools/performance/test/browser_timeline-blueprint.js b/browser/devtools/performance/test/browser_timeline-blueprint.js deleted file mode 100644 index 5336e6bdd256..000000000000 --- a/browser/devtools/performance/test/browser_timeline-blueprint.js +++ /dev/null @@ -1,34 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -/** - * Tests if the timeline blueprint has a correct structure. - */ - -function* spawnTest() { - let { TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/markers"); - - ok(TIMELINE_BLUEPRINT, - "A timeline blueprint should be available."); - - ok(Object.keys(TIMELINE_BLUEPRINT).length, - "The timeline blueprint has at least one entry."); - - for (let [key, value] of Iterator(TIMELINE_BLUEPRINT)) { - 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 7f61a968a408..e09efb164d02 100644 --- a/browser/devtools/performance/test/browser_timeline-filters.js +++ b/browser/devtools/performance/test/browser_timeline-filters.js @@ -5,6 +5,8 @@ * Tests markers filtering mechanism. */ +const EPSILON = 0.00000001; + function* spawnTest() { let { panel } = yield initPerformance(SIMPLE_URL); let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin; @@ -24,20 +26,34 @@ function* spawnTest() { yield stopRecording(panel); + // Push some fake markers of a type we do not have a blueprint for + let markers = PerformanceController.getCurrentRecording().getMarkers(); + let endTime = markers[markers.length - 1].end; + markers.push({ name: "CustomMarker", start: endTime + EPSILON, end: endTime + (EPSILON * 2) }); + markers.push({ name: "CustomMarker", start: endTime + (EPSILON * 3), end: endTime + (EPSILON * 4) }); + + // Invalidate marker cache + WaterfallView._cache.delete(markers); + // Select everything - OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE }) + let waterfallRendered = WaterfallView.once(EVENTS.WATERFALL_RENDERED); + OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE }); $("#filter-button").click(); let menuItem1 = $("menuitem[marker-type=Styles]"); let menuItem2 = $("menuitem[marker-type=Reflow]"); let menuItem3 = $("menuitem[marker-type=Paint]"); + let menuItem4 = $("menuitem[marker-type=UNKNOWN]"); let overview = OverviewView.graphs.get("timeline"); let originalHeight = overview.fixedHeight; + yield waterfallRendered; + ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (1)"); ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (1)"); ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (1)"); + ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (1)"); let heightBefore = overview.fixedHeight; EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin); @@ -47,6 +63,7 @@ function* spawnTest() { ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (2)"); ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (2)"); ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (2)"); + ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (2)"); heightBefore = overview.fixedHeight; EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin); @@ -56,6 +73,7 @@ function* spawnTest() { ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (3)"); ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (3)"); ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (3)"); + ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (3)"); heightBefore = overview.fixedHeight; EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin); @@ -65,15 +83,25 @@ function* spawnTest() { ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)"); ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (4)"); ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (4)"); + ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (4)"); + + EventUtils.synthesizeMouseAtCenter(menuItem4, {type: "mouseup"}, panel.panelWin); + yield waitForOverviewAndCommand(overview, menuItem4); + + ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (5)"); + ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (5)"); + ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (5)"); + ok(!$(".waterfall-marker-bar[type=CustomMarker]"), "No 'Unknown' marker (5)"); for (let item of [menuItem1, menuItem2, menuItem3]) { EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin); yield waitForOverviewAndCommand(overview, item); } - 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)"); + ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (6)"); + ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (6)"); + ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (6)"); + ok(!$(".waterfall-marker-bar[type=CustomMarker]"), "No 'Unknown' marker (6)"); is(overview.fixedHeight, originalHeight, "Overview restored"); diff --git a/browser/devtools/performance/test/head.js b/browser/devtools/performance/test/head.js index 9e39e58683e8..7f2eb057304c 100644 --- a/browser/devtools/performance/test/head.js +++ b/browser/devtools/performance/test/head.js @@ -61,6 +61,7 @@ let DEFAULT_PREFS = [ "devtools.performance.profiler.buffer-size", "devtools.performance.profiler.sample-frequency-khz", "devtools.performance.ui.experimental", + "devtools.performance.timeline.hidden-markers", ].reduce((prefs, pref) => { prefs[pref] = Preferences.get(pref); return prefs; diff --git a/browser/devtools/performance/test/unit/head.js b/browser/devtools/performance/test/unit/head.js index de1ac9b2daad..14d39eef80cd 100644 --- a/browser/devtools/performance/test/unit/head.js +++ b/browser/devtools/performance/test/unit/head.js @@ -5,8 +5,11 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); const RecordingUtils = devtools.require("devtools/performance/recording-utils"); +const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data"; + /** * Get a path in a FrameNode call tree. */ diff --git a/browser/devtools/performance/test/unit/test_marker-blueprint.js b/browser/devtools/performance/test/unit/test_marker-blueprint.js new file mode 100644 index 000000000000..bd93e3036de2 --- /dev/null +++ b/browser/devtools/performance/test/unit/test_marker-blueprint.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the timeline blueprint has a correct structure. + */ + +function run_test() { + run_next_test(); +} + +add_task(function () { + let { TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/markers"); + + ok(TIMELINE_BLUEPRINT, + "A timeline blueprint should be available."); + + ok(Object.keys(TIMELINE_BLUEPRINT).length, + "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."); + } +}); diff --git a/browser/devtools/performance/test/unit/test_marker-utils.js b/browser/devtools/performance/test/unit/test_marker-utils.js new file mode 100644 index 000000000000..f9e3d1efbc33 --- /dev/null +++ b/browser/devtools/performance/test/unit/test_marker-utils.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the marker utils methods. + */ + +function run_test() { + run_next_test(); +} + +add_task(function () { + let { TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/markers"); + let Utils = devtools.require("devtools/performance/marker-utils"); + + Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false); + + equal(Utils.getMarkerLabel({ name: "DOMEvent" }), "DOM Event", + "getMarkerLabel() returns a simple label"); + equal(Utils.getMarkerLabel({ name: "Javascript", causeName: "setTimeout handler" }), "setTimeout", + "getMarkerLabel() returns a label defined via function"); + + ok(Utils.getMarkerFields({ name: "Paint" }).length === 0, + "getMarkerFields() returns an empty array when no fields defined"); + + let fields = Utils.getMarkerFields({ name: "ConsoleTime", causeName: "snowstorm" }); + equal(fields[0].label, "Timer Name:", "getMarkerFields() returns an array with proper label"); + equal(fields[0].value, "snowstorm", "getMarkerFields() returns an array with proper value"); + + fields = Utils.getMarkerFields({ name: "DOMEvent", type: "mouseclick" }); + equal(fields.length, 1, "getMarkerFields() ignores fields that are not found on marker"); + equal(fields[0].label, "Event Type:", "getMarkerFields() returns an array with proper label"); + equal(fields[0].value, "mouseclick", "getMarkerFields() returns an array with proper value"); + + fields = Utils.getMarkerFields({ name: "DOMEvent", eventPhase: Ci.nsIDOMEvent.AT_TARGET, type: "mouseclick" }); + equal(fields.length, 2, "getMarkerFields() returns multiple fields when using a fields function"); + equal(fields[0].label, "Event Type:", "getMarkerFields() correctly returns fields via function (1)"); + equal(fields[0].value, "mouseclick", "getMarkerFields() correctly returns fields via function (2)"); + equal(fields[1].label, "Phase:", "getMarkerFields() correctly returns fields via function (3)"); + equal(fields[1].value, "Target", "getMarkerFields() correctly returns fields via function (4)"); + + equal(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "(Gecko)", + "Correctly obfuscates JS markers when platform data is off."); + Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true); + equal(Utils.getMarkerFields({ name: "Javascript", causeName: "Some Platform Field" })[0].value, "Some Platform Field", + "Correctly deobfuscates JS markers when platform data is on."); + + equal(Utils.getMarkerClassName("Javascript"), "Function Call", + "getMarkerClassName() returns correct string when defined via function"); + equal(Utils.getMarkerClassName("GarbageCollection"), "GC Event", + "getMarkerClassName() returns correct string when defined via function"); + equal(Utils.getMarkerClassName("Reflow"), "Layout", + "getMarkerClassName() returns correct string when defined via string"); + + TIMELINE_BLUEPRINT["fakemarker"] = { group: 0 }; + try { + Utils.getMarkerClassName("fakemarker"); + ok(false, "getMarkerClassName() should throw when no label on blueprint."); + } catch (e) { + ok(true, "getMarkerClassName() should throw when no label on blueprint."); + } + + TIMELINE_BLUEPRINT["fakemarker"] = { group: 0, label: () => void 0}; + try { + Utils.getMarkerClassName("fakemarker"); + ok(false, "getMarkerClassName() should throw when label function returnd undefined."); + } catch (e) { + ok(true, "getMarkerClassName() should throw when label function returnd undefined."); + } + + delete TIMELINE_BLUEPRINT["fakemarker"]; + + let customBlueprint = { + UNKNOWN: { + group: 1, + label: "MyDefault" + }, + custom: { + group: 0, + label: "MyCustom" + } + }; + + equal(Utils.getBlueprintFor({ name: "Reflow" }).label, "Layout", + "Utils.getBlueprintFor() should return marker def for passed in marker."); + equal(Utils.getBlueprintFor({ name: "Not sure!" }).label(), "Unknown", + "Utils.getBlueprintFor() should return a default marker def if the marker is undefined."); +}); diff --git a/browser/devtools/performance/test/unit/xpcshell.ini b/browser/devtools/performance/test/unit/xpcshell.ini index b84740d0f699..acefb997ea97 100644 --- a/browser/devtools/performance/test/unit/xpcshell.ini +++ b/browser/devtools/performance/test/unit/xpcshell.ini @@ -5,9 +5,11 @@ tail = firefox-appdir = browser skip-if = toolkit == 'android' || toolkit == 'gonk' -[test_profiler-categories.js] [test_frame-utils-01.js] [test_frame-utils-02.js] +[test_marker-blueprint.js] +[test_marker-utils.js] +[test_profiler-categories.js] [test_tree-model-01.js] [test_tree-model-02.js] [test_tree-model-03.js] diff --git a/browser/devtools/performance/views/details-waterfall.js b/browser/devtools/performance/views/details-waterfall.js index eb26fc4affca..0896b7b111d4 100644 --- a/browser/devtools/performance/views/details-waterfall.js +++ b/browser/devtools/performance/views/details-waterfall.js @@ -32,6 +32,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, { this._onMarkerSelected = this._onMarkerSelected.bind(this); this._onResize = this._onResize.bind(this); this._onViewSource = this._onViewSource.bind(this); + this._hiddenMarkers = PerformanceController.getPref("hidden-markers"); this.headerContainer = $("#waterfall-header"); this.breakdownContainer = $("#waterfall-breakdown"); @@ -111,8 +112,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, { * Called whenever an observed pref is changed. */ _onObservedPrefChange: function(_, prefName) { - let blueprint = PerformanceController.getTimelineBlueprint(); - this._markersRoot.blueprint = blueprint; + this._hiddenMarkers = PerformanceController.getPref("hidden-markers"); }, /** @@ -136,7 +136,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, { WaterfallUtils.collapseMarkersIntoNode({ markerNode: rootMarkerNode, - markersList: markers + markersList: markers, }); this._cache.set(markers, rootMarkerNode); @@ -160,8 +160,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, { this._markersRoot = root; this._waterfallHeader = header; - let blueprint = PerformanceController.getTimelineBlueprint(); - root.blueprint = blueprint; + root.filter = this._hiddenMarkers; root.interval = interval; root.on("selected", this._onMarkerSelected); root.on("unselected", this._onMarkerSelected); diff --git a/browser/devtools/performance/views/overview.js b/browser/devtools/performance/views/overview.js index 1f05eaf9b487..5f017f7e008c 100644 --- a/browser/devtools/performance/views/overview.js +++ b/browser/devtools/performance/views/overview.js @@ -42,7 +42,7 @@ let OverviewView = { initialize: function () { this.graphs = new GraphsController({ root: $("#overview-pane"), - getBlueprint: () => PerformanceController.getTimelineBlueprint(), + getFilter: () => PerformanceController.getPref("hidden-markers"), getTheme: () => PerformanceController.getTheme(), }); @@ -331,8 +331,8 @@ let OverviewView = { case "hidden-markers": { let graph; if (graph = yield this.graphs.isAvailable("timeline")) { - let blueprint = PerformanceController.getTimelineBlueprint(); - graph.setBlueprint(blueprint); + let filter = PerformanceController.getPref("hidden-markers"); + graph.setFilter(filter); graph.refresh({ force: true }); } break; diff --git a/browser/locales/en-US/chrome/browser/devtools/timeline.properties b/browser/locales/en-US/chrome/browser/devtools/timeline.properties index 77b745b39c15..774be3add109 100644 --- a/browser/locales/en-US/chrome/browser/devtools/timeline.properties +++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties @@ -46,6 +46,7 @@ timeline.label.domevent=DOM Event timeline.label.consoleTime=Console timeline.label.garbageCollection=GC Event timeline.label.timestamp=Timestamp +timeline.label.unknown=Unknown # LOCALIZATION NOTE (graphs.memory): # This string is displayed in the memory graph of the Performance tool, From 98693e4d13492739941d4d322e1cd67fa1ff5515 Mon Sep 17 00:00:00 2001 From: Alessio Placitelli Date: Fri, 12 Jun 2015 03:10:00 +0200 Subject: [PATCH 02/16] Bug 1174111 - |test_sendTimeout| in test_TelemetryControllerShutdown.js must not wait on ping submission. r=gfritzsche --- .../telemetry/tests/unit/test_TelemetryControllerShutdown.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js index c6754c19b4b7..37817af920b7 100644 --- a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js @@ -61,11 +61,13 @@ add_task(function* test_sendTimeout() { yield TelemetryController.setup(); TelemetrySend.setServer("http://localhost:" + httpServer.identity.primaryPort); - yield TelemetryController.submitExternalPing("test-ping-type", {}); + let submissionPromise = TelemetryController.submitExternalPing("test-ping-type", {}); // Trigger the AsyncShutdown phase TelemetryController hangs off. AsyncShutdown.profileBeforeChange._trigger(); AsyncShutdown.sendTelemetry._trigger(); + // Now wait for the ping submission. + yield submissionPromise; // If we get here, we didn't time out in the shutdown routines. Assert.ok(true, "Didn't time out on shutdown."); From 61d2710314df595722cf7d0b782b0ac676ac74a3 Mon Sep 17 00:00:00 2001 From: Alessio Placitelli Date: Wed, 3 Jun 2015 04:41:00 +0200 Subject: [PATCH 03/16] Bug 1169159 - Make xpcshells run_test_in_child() and do_await_remote_message() return promises. r=ted,gfritzsche --- testing/xpcshell/head.js | 78 +++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/testing/xpcshell/head.js b/testing/xpcshell/head.js index 829bc1134345..b80bae4ca441 100644 --- a/testing/xpcshell/head.js +++ b/testing/xpcshell/head.js @@ -1232,54 +1232,74 @@ function do_load_child_test_harness() * Runs an entire xpcshell unit test in a child process (rather than in chrome, * which is the default). * - * This function returns immediately, before the test has completed. + * This function returns immediately, before the test has completed. * * @param testFile * The name of the script to run. Path format same as load(). * @param optionalCallback. * Optional function to be called (in parent) when test on child is * complete. If provided, the function must call do_test_finished(); + * @return Promise Resolved when the test in the child is complete. */ -function run_test_in_child(testFile, optionalCallback) +function run_test_in_child(testFile, optionalCallback) { - var callback = (typeof optionalCallback == 'undefined') ? - do_test_finished : optionalCallback; + return new Promise((resolve) => { + var callback = () => { + resolve(); + if (typeof optionalCallback == 'undefined') { + do_test_finished(); + } else { + optionalCallback(); + } + }; - do_load_child_test_harness(); + do_load_child_test_harness(); - var testPath = do_get_file(testFile).path.replace(/\\/g, "/"); - do_test_pending("run in child"); - sendCommand("_testLogger.info('CHILD-TEST-STARTED'); " - + "const _TEST_FILE=['" + testPath + "']; " - + "_execute_test(); " - + "_testLogger.info('CHILD-TEST-COMPLETED');", - callback); + var testPath = do_get_file(testFile).path.replace(/\\/g, "/"); + do_test_pending("run in child"); + sendCommand("_testLogger.info('CHILD-TEST-STARTED'); " + + "const _TEST_FILE=['" + testPath + "']; " + + "_execute_test(); " + + "_testLogger.info('CHILD-TEST-COMPLETED');", + callback); + }); } /** * Execute a given function as soon as a particular cross-process message is received. * Must be paired with do_send_remote_message or equivalent ProcessMessageManager calls. + * + * @param optionalCallback + * Optional callback that is invoked when the message is received. If provided, + * the function must call do_test_finished(). + * @return Promise Promise that is resolved when the message is received. */ -function do_await_remote_message(name, callback) +function do_await_remote_message(name, optionalCallback) { - var listener = { - receiveMessage: function(message) { - if (message.name == name) { - mm.removeMessageListener(name, listener); - callback(); - do_test_finished(); + return new Promise((resolve) => { + var listener = { + receiveMessage: function(message) { + if (message.name == name) { + mm.removeMessageListener(name, listener); + resolve(); + if (optionalCallback) { + optionalCallback(); + } else { + do_test_finished(); + } + } } - } - }; + }; - var mm; - if (runningInParent) { - mm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster); - } else { - mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender); - } - do_test_pending(); - mm.addMessageListener(name, listener); + var mm; + if (runningInParent) { + mm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster); + } else { + mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender); + } + do_test_pending(); + mm.addMessageListener(name, listener); + }); } /** From 5e87aa2a8abfd7a086f1e2a29996216ec5e7662b Mon Sep 17 00:00:00 2001 From: Alessio Placitelli Date: Mon, 8 Jun 2015 00:12:00 +0200 Subject: [PATCH 04/16] Bug 1169159 - Add basic test coverage for Telemetry child payloads. r=gfritzsche --- .../unit/test_geolocation_reset_accuracy.js | 1 + .../telemetry/TelemetryController.jsm | 31 +++++-- .../components/telemetry/TelemetrySession.jsm | 19 ++-- .../components/telemetry/tests/unit/head.js | 12 +-- .../tests/unit/test_ChildHistograms.js | 89 +++++++++++++++++++ .../telemetry/tests/unit/xpcshell.ini | 2 + 6 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 toolkit/components/telemetry/tests/unit/test_ChildHistograms.js diff --git a/dom/tests/unit/test_geolocation_reset_accuracy.js b/dom/tests/unit/test_geolocation_reset_accuracy.js index 75272ab7a04e..abeee6e03518 100644 --- a/dom/tests/unit/test_geolocation_reset_accuracy.js +++ b/dom/tests/unit/test_geolocation_reset_accuracy.js @@ -97,6 +97,7 @@ function run_test() function stop_high_accuracy_watch() { geolocation.clearWatch(watchID2); check_results(); + do_test_finished(); } function check_results() diff --git a/toolkit/components/telemetry/TelemetryController.jsm b/toolkit/components/telemetry/TelemetryController.jsm index 750fbae6e928..fd9055383c6d 100644 --- a/toolkit/components/telemetry/TelemetryController.jsm +++ b/toolkit/components/telemetry/TelemetryController.jsm @@ -155,6 +155,13 @@ this.TelemetryController = Object.freeze({ return Impl.setupTelemetry(true); }, + /** + * Used only for testing purposes. + */ + setupContent: function() { + return Impl.setupContentTelemetry(true); + }, + /** * Send a notification. */ @@ -689,6 +696,21 @@ let Impl = { return this._delayedInitTaskDeferred.promise; }, + /** + * This triggers basic telemetry initialization for content processes. + * @param {Boolean} [testing=false] True if we are in test mode, false otherwise. + */ + setupContentTelemetry: function (testing = false) { + this._testMode = testing; + + // We call |enableTelemetryRecording| here to make sure that Telemetry.canRecord* flags + // are in sync between chrome and content processes. + if (!this.enableTelemetryRecording()) { + this._log.trace("setupContentTelemetry - Content process recording disabled."); + return; + } + }, + // Do proper shutdown waiting and cleanup. _cleanupOnShutdown: Task.async(function*() { if (!this._initialized) { @@ -760,13 +782,8 @@ let Impl = { // profile-after-change is only registered for chrome processes. return this.setupTelemetry(); case "app-startup": - // app-startup is only registered for content processes. We call - // |enableTelemetryRecording| here to make sure that Telemetry.canRecord* flags - // are in sync between chrome and content processes. - if (!this.enableTelemetryRecording()) { - this._log.trace("observe - Content process recording disabled."); - return; - } + // app-startup is only registered for content processes. + return this.setupContentTelemetry(); break; } }, diff --git a/toolkit/components/telemetry/TelemetrySession.jsm b/toolkit/components/telemetry/TelemetrySession.jsm index d4a16cd2aaa5..e66640452f32 100644 --- a/toolkit/components/telemetry/TelemetrySession.jsm +++ b/toolkit/components/telemetry/TelemetrySession.jsm @@ -58,7 +58,7 @@ const MIN_SUBSESSION_LENGTH_MS = 10 * 60 * 1000; #expand const HISTOGRAMS_FILE_VERSION = "__HISTOGRAMS_FILE_VERSION__"; const LOGGER_NAME = "Toolkit.Telemetry"; -const LOGGER_PREFIX = "TelemetrySession::"; +const LOGGER_PREFIX = "TelemetrySession" + (IS_CONTENT_PROCESS ? "#content::" : "::"); const PREF_BRANCH = "toolkit.telemetry."; const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID"; @@ -775,7 +775,7 @@ this.TelemetrySession = Object.freeze({ let Impl = { _histograms: {}, _initialized: false, - _log: null, + _logger: null, _prevValues: {}, // Regex that matches histograms we care about during startup. // Keep this in sync with gen-histogram-bucket-ranges.py. @@ -817,6 +817,13 @@ let Impl = { // Used to serialize session state writes to disk. _stateSaveSerializer: new SaveSerializer(), + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); + } + return this._logger; + }, + /** * Gets a series of simple measurements (counters). At the moment, this * only returns startup data from nsIAppStartup.getStartupInfo(). @@ -1415,10 +1422,6 @@ let Impl = { */ setupChromeProcess: function setupChromeProcess(testing) { this._initStarted = true; - if (testing && !this._log) { - this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); - } - this._log.trace("setupChromeProcess"); if (this._delayedInitTask) { @@ -1718,10 +1721,6 @@ let Impl = { * This observer drives telemetry. */ observe: function (aSubject, aTopic, aData) { - if (!this._log) { - this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); - } - // Prevent the cycle collector begin topic from cluttering the log. if (aTopic != TOPIC_CYCLE_COLLECTOR_BEGIN) { this._log.trace("observe - " + aTopic + " notified."); diff --git a/toolkit/components/telemetry/tests/unit/head.js b/toolkit/components/telemetry/tests/unit/head.js index ec315b83d286..e2a353d2229c 100644 --- a/toolkit/components/telemetry/tests/unit/head.js +++ b/toolkit/components/telemetry/tests/unit/head.js @@ -186,12 +186,14 @@ function promiseRejects(promise) { return promise.then(() => false, () => true); } -// Set logging preferences for all the tests. -Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace"); -TelemetryController.initLogging(); +if (runningInParent) { + // Set logging preferences for all the tests. + Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace"); + // Telemetry archiving should be on. + Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true); +} -// Telemetry archiving should be on. -Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true); +TelemetryController.initLogging(); // Avoid timers interrupting test behavior. fakeSchedulerTimer(() => {}, () => {}); diff --git a/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js new file mode 100644 index 000000000000..2c29c567bf0f --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js @@ -0,0 +1,89 @@ + +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySession.jsm", this); +Cu.import("resource://gre/modules/PromiseUtils.jsm", this); + +const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry); + +const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload"; + +const PLATFORM_VERSION = "1.9.2"; +const APP_VERSION = "1"; +const APP_ID = "xpcshell@tests.mozilla.org"; +const APP_NAME = "XPCShell"; + +function run_child_test() { + // Setup histograms with some fixed values. + let flagHist = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG"); + flagHist.add(1); + let countHist = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT"); + countHist.add(); + countHist.add(); + + let flagKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG"); + flagKeyed.add("a", 1); + flagKeyed.add("b", 1); + let countKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT"); + countKeyed.add("a"); + countKeyed.add("b"); + countKeyed.add("b"); + + // Check payload values. + const payload = TelemetrySession.getPayload("test-ping"); + check_histogram_values(payload); +} + +function check_histogram_values(payload) { + const hs = payload.histograms; + Assert.ok("TELEMETRY_TEST_COUNT" in hs, "Should have count test histogram."); + Assert.ok("TELEMETRY_TEST_FLAG" in hs, "Should have flag test histogram."); + Assert.equal(hs["TELEMETRY_TEST_COUNT"].sum, 2, + "Count test histogram should have the right value."); + Assert.equal(hs["TELEMETRY_TEST_FLAG"].sum, 1, + "Flag test histogram should have the right value."); + + const kh = payload.keyedHistograms; + Assert.ok("TELEMETRY_TEST_KEYED_COUNT" in kh, "Should have keyed count test histogram."); + Assert.ok("TELEMETRY_TEST_KEYED_FLAG" in kh, "Should have keyed flag test histogram."); + Assert.equal(kh["TELEMETRY_TEST_KEYED_COUNT"]["a"].sum, 1, + "Keyed count test histogram should have the right value."); + Assert.equal(kh["TELEMETRY_TEST_KEYED_COUNT"]["b"].sum, 2, + "Keyed count test histogram should have the right value."); + Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["a"].sum, 1, + "Keyed flag test histogram should have the right value."); + Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["b"].sum, 1, + "Keyed flag test histogram should have the right value."); +} + +add_task(function*() { + if (!runningInParent) { + TelemetryController.setupContent(); + run_child_test(); + return; + } + + // Setup. + do_get_profile(true); + loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION); + Services.prefs.setBoolPref("toolkit.telemetry.enabled", true); + yield TelemetryController.setup(); + yield TelemetrySession.setup(); + + // Run test in child and wait until it is finished. + yield run_test_in_child("test_ChildHistograms.js"); + + // Gather payload from child. + let promiseMessage = do_await_remote_message(MESSAGE_TELEMETRY_PAYLOAD); + TelemetrySession.requestChildPayloads(); + yield promiseMessage; + + // Check child payload. + const payload = TelemetrySession.getPayload("test-ping"); + Assert.ok("childPayloads" in payload, "Should have child payloads."); + Assert.equal(payload.childPayloads.length, 1, "Should have received one child payload so far."); + Assert.ok("histograms" in payload.childPayloads[0], "Child payload should have histograms."); + Assert.ok("keyedHistograms" in payload.childPayloads[0], "Child payload should have keyed histograms."); + check_histogram_values(payload.childPayloads[0]); + + do_test_finished(); +}); diff --git a/toolkit/components/telemetry/tests/unit/xpcshell.ini b/toolkit/components/telemetry/tests/unit/xpcshell.ini index 03ad74a8b0ae..219cda4ab2c3 100644 --- a/toolkit/components/telemetry/tests/unit/xpcshell.ini +++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini @@ -50,3 +50,5 @@ skip-if = os == "android" # Disabled due to intermittent orange on Android skip-if = android_version == "18" [test_ThreadHangStats.js] run-sequentially = Bug 1046307, test can fail intermittently when CPU load is high +[test_ChildHistograms.js] +skip-if = os == "android" From 5560e83bc3f1e0f07da044e1c54c87384b64b65d Mon Sep 17 00:00:00 2001 From: Alessio Placitelli Date: Fri, 12 Jun 2015 06:09:00 +0200 Subject: [PATCH 05/16] Bug 1169159 - Refactor the |enableTelemetryRecording| logic in TelemetryController.jsm. r=gfritzsche --- .../telemetry/TelemetryController.jsm | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/toolkit/components/telemetry/TelemetryController.jsm b/toolkit/components/telemetry/TelemetryController.jsm index fd9055383c6d..deb3d9b1508a 100644 --- a/toolkit/components/telemetry/TelemetryController.jsm +++ b/toolkit/components/telemetry/TelemetryController.jsm @@ -594,21 +594,20 @@ let Impl = { Telemetry.canRecordBase = enabled || IS_UNIFIED_TELEMETRY; #ifdef MOZILLA_OFFICIAL - if (!Telemetry.isOfficialTelemetry && !this._testMode) { - // We can't send data; no point in initializing observers etc. - // Only do this for official builds so that e.g. developer builds - // still enable Telemetry based on prefs. - Telemetry.canRecordExtended = false; - this._log.config("enableTelemetryRecording - Can't send data, disabling extended Telemetry recording."); - } + // Enable extended telemetry if: + // * the telemetry preference is set and + // * this is an official build or we are in test-mode + // We only do the latter check for official builds so that e.g. developer builds + // still enable Telemetry based on prefs. + Telemetry.canRecordExtended = enabled && (Telemetry.isOfficialTelemetry || this._testMode); +#else + // Turn off extended telemetry recording if disabled by preferences or if base/telemetry + // telemetry recording is off. + Telemetry.canRecordExtended = enabled; #endif - if (!enabled || !Telemetry.canRecordBase) { - // Turn off extended telemetry recording if disabled by preferences or if base/telemetry - // telemetry recording is off. - Telemetry.canRecordExtended = false; - this._log.config("enableTelemetryRecording - Disabling extended Telemetry recording."); - } + this._log.config("enableTelemetryRecording - canRecordBase:" + Telemetry.canRecordBase + + ", canRecordExtended: " + Telemetry.canRecordExtended); return Telemetry.canRecordBase; }, From 14464863a6c68dc73d3b830c5a065043ce7ee741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eddy=20Bru=C3=ABl?= Date: Mon, 15 Jun 2015 12:18:35 +0200 Subject: [PATCH 06/16] Bug 1164564 - Implement WorkerActor.attachThread;r=jlong --- browser/devtools/debugger/test/browser.ini | 4 + .../test/browser_dbg_WorkerActor.attach.js | 1 - .../browser_dbg_WorkerActor.attachThread.js | 89 +++++++++++++++++++ .../code_WorkerActor.attachThread-worker.js | 16 ++++ .../debugger/test/code_frame-script.js | 12 +++ .../doc_WorkerActor.attachThread-tab.html | 8 ++ browser/devtools/debugger/test/head.js | 64 ++++++++++++- toolkit/devtools/client/dbg-client.jsm | 32 +++++-- toolkit/devtools/server/actors/script.js | 9 +- .../server/actors/utils/TabSources.js | 2 +- toolkit/devtools/server/actors/worker.js | 43 ++++++++- toolkit/devtools/server/main.js | 86 +++++++++++++++++- toolkit/devtools/server/moz.build | 1 + toolkit/devtools/server/worker.js | 66 ++++++++++++++ toolkit/devtools/worker-loader.js | 6 +- 15 files changed, 421 insertions(+), 18 deletions(-) create mode 100644 browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js create mode 100644 browser/devtools/debugger/test/code_WorkerActor.attachThread-worker.js create mode 100644 browser/devtools/debugger/test/doc_WorkerActor.attachThread-tab.html create mode 100644 toolkit/devtools/server/worker.js diff --git a/browser/devtools/debugger/test/browser.ini b/browser/devtools/debugger/test/browser.ini index e9ef792bdfae..6243c7831151 100644 --- a/browser/devtools/debugger/test/browser.ini +++ b/browser/devtools/debugger/test/browser.ini @@ -45,6 +45,7 @@ support-files = code_ugly-8^headers^ code_WorkerActor.attach-worker1.js code_WorkerActor.attach-worker2.js + code_WorkerActor.attachThread-worker.js doc_auto-pretty-print-01.html doc_auto-pretty-print-02.html doc_binary_search.html @@ -107,6 +108,7 @@ support-files = doc_with-frame.html doc_WorkerActor.attach-tab1.html doc_WorkerActor.attach-tab2.html + doc_WorkerActor.attachThread-tab.html head.js sjs_random-javascript.sjs testactors.js @@ -566,3 +568,5 @@ skip-if = e10s && debug skip-if = e10s && debug [browser_dbg_WorkerActor.attach.js] skip-if = e10s && debug +[browser_dbg_WorkerActor.attachThread.js] +skip-if = e10s && debug diff --git a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js index fe0d839518c5..a9ca7a91544d 100644 --- a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js +++ b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js @@ -27,7 +27,6 @@ function test() { // registered. Instead, we have to wait for the promise returned by // createWorker in the tab to be resolved. yield createWorkerInTab(tab, WORKER1_URL); - let { workers } = yield listWorkers(tabClient); let [, workerClient1] = yield attachWorker(tabClient, findWorker(workers, WORKER1_URL)); diff --git a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js new file mode 100644 index 000000000000..fd7f1e839223 --- /dev/null +++ b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js @@ -0,0 +1,89 @@ +let TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html"; +let WORKER_URL = "code_WorkerActor.attachThread-worker.js"; + +function test() { + Task.spawn(function* () { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + + let client1 = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client1); + let client2 = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client2); + + let tab = yield addTab(TAB_URL); + let { tabs: tabs1 } = yield listTabs(client1); + let [, tabClient1] = yield attachTab(client1, findTab(tabs1, TAB_URL)); + let { tabs: tabs2 } = yield listTabs(client2); + let [, tabClient2] = yield attachTab(client2, findTab(tabs2, TAB_URL)); + + yield listWorkers(tabClient1); + yield listWorkers(tabClient2); + yield createWorkerInTab(tab, WORKER_URL); + let { workers: workers1 } = yield listWorkers(tabClient1); + let [, workerClient1] = yield attachWorker(tabClient1, + findWorker(workers1, WORKER_URL)); + let { workers: workers2 } = yield listWorkers(tabClient2); + let [, workerClient2] = yield attachWorker(tabClient2, + findWorker(workers2, WORKER_URL)); + + let location = { line: 5 }; + + let [, threadClient1] = yield attachThread(workerClient1); + let sources1 = yield getSources(threadClient1); + let sourceClient1 = threadClient1.source(findSource(sources1, + EXAMPLE_URL + WORKER_URL)); + let [, breakpointClient1] = yield setBreakpoint(sourceClient1, location); + yield resume(threadClient1); + + let [, threadClient2] = yield attachThread(workerClient2); + let sources2 = yield getSources(threadClient2); + let sourceClient2 = threadClient2.source(findSource(sources2, + EXAMPLE_URL + WORKER_URL)); + let [, breakpointClient2] = yield setBreakpoint(sourceClient2, location); + yield resume(threadClient2); + + postMessageToWorkerInTab(tab, WORKER_URL, "ping"); + yield Promise.all([ + waitForPause(threadClient1).then((packet) => { + is(packet.type, "paused"); + let why = packet.why; + is(why.type, "breakpoint"); + is(why.actors.length, 1); + is(why.actors[0], breakpointClient1.actor); + let frame = packet.frame; + let where = frame.where; + is(where.source.actor, sourceClient1.actor); + is(where.line, location.line); + let variables = frame.environment.bindings.variables; + is(variables.a.value, 1); + is(variables.b.value.type, "undefined"); + is(variables.c.value.type, "undefined"); + return resume(threadClient1); + }), + waitForPause(threadClient2).then((packet) => { + is(packet.type, "paused"); + let why = packet.why; + is(why.type, "breakpoint"); + is(why.actors.length, 1); + is(why.actors[0], breakpointClient2.actor); + let frame = packet.frame; + let where = frame.where; + is(where.source.actor, sourceClient2.actor); + is(where.line, location.line); + let variables = frame.environment.bindings.variables; + is(variables.a.value, 1); + is(variables.b.value.type, "undefined"); + is(variables.c.value.type, "undefined"); + return resume(threadClient2); + }), + ]); + + terminateWorkerInTab(tab, WORKER_URL); + yield waitForWorkerClose(workerClient1); + yield waitForWorkerClose(workerClient2); + yield close(client1); + yield close(client2); + finish(); + }); +} diff --git a/browser/devtools/debugger/test/code_WorkerActor.attachThread-worker.js b/browser/devtools/debugger/test/code_WorkerActor.attachThread-worker.js new file mode 100644 index 000000000000..4c115749dfe8 --- /dev/null +++ b/browser/devtools/debugger/test/code_WorkerActor.attachThread-worker.js @@ -0,0 +1,16 @@ +"use strict"; + +function f() { + var a = 1; + var b = 2; + var c = 3; +} + +self.onmessage = function (event) { + if (event.data == "ping") { + f() + postMessage("pong"); + } +}; + +postMessage("load"); diff --git a/browser/devtools/debugger/test/code_frame-script.js b/browser/devtools/debugger/test/code_frame-script.js index 529328cbd0f2..eb9a85cf6da4 100644 --- a/browser/devtools/debugger/test/code_frame-script.js +++ b/browser/devtools/debugger/test/code_frame-script.js @@ -83,3 +83,15 @@ addMessageListener("jsonrpc", function ({ data: { method, params, id } }) { }); }); }); + +addMessageListener("test:postMessageToWorker", function (message) { + dump("Posting message '" + message.data.message + "' to worker with url '" + + message.data.url + "'.\n"); + + let worker = workers[message.data.url]; + worker.postMessage(message.data.message); + worker.addEventListener("message", function listener() { + worker.removeEventListener("message", listener); + sendAsyncMessage("test:postMessageToWorker"); + }); +}); diff --git a/browser/devtools/debugger/test/doc_WorkerActor.attachThread-tab.html b/browser/devtools/debugger/test/doc_WorkerActor.attachThread-tab.html new file mode 100644 index 000000000000..62ab9be7d2e8 --- /dev/null +++ b/browser/devtools/debugger/test/doc_WorkerActor.attachThread-tab.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/browser/devtools/debugger/test/head.js b/browser/devtools/debugger/test/head.js index 1002c36717c6..857b844a189f 100644 --- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -512,9 +512,13 @@ function getTab(aTarget, aWindow) { } function getSources(aClient) { + info("Getting sources."); + let deferred = promise.defer(); - aClient.getSources(({sources}) => deferred.resolve(sources)); + aClient.getSources((packet) => { + deferred.resolve(packet.sources); + }); return deferred.promise; } @@ -1129,6 +1133,15 @@ function waitForWorkerListChanged(tabClient) { }); } +function attachThread(workerClient, options) { + info("Attaching to thread."); + return new Promise(function(resolve, reject) { + workerClient.attachThread(options, function (response, threadClient) { + resolve([response, threadClient]); + }); + }); +} + function waitForWorkerClose(workerClient) { info("Waiting for worker to close."); return new Promise(function (resolve) { @@ -1156,3 +1169,52 @@ function waitForWorkerThaw(workerClient) { }); }); } + +function resume(threadClient) { + info("Resuming thread."); + return rdpInvoke(threadClient, threadClient.resume); +} + +function findSource(sources, url) { + info("Finding source with url '" + url + "'.\n"); + for (let source of sources) { + if (source.url === url) { + return source; + } + } + return null; +} + +function setBreakpoint(sourceClient, location) { + info("Setting breakpoint.\n"); + return new Promise(function (resolve) { + sourceClient.setBreakpoint(location, function (response, breakpointClient) { + resolve([response, breakpointClient]); + }); + }); +} + +function waitForEvent(client, type, predicate) { + return new Promise(function (resolve) { + function listener(type, packet) { + if (!predicate(packet)) { + return; + } + client.removeListener(listener); + resolve(packet); + } + + if (predicate) { + client.addListener(type, listener); + } else { + client.addOneTimeListener(type, function (type, packet) { + resolve(packet); + }); + } + }); +} + +function waitForPause(threadClient) { + info("Waiting for pause.\n"); + return waitForEvent(threadClient, "paused"); +} diff --git a/toolkit/devtools/client/dbg-client.jsm b/toolkit/devtools/client/dbg-client.jsm index 2f1f0b59eb3e..833e26a5b136 100644 --- a/toolkit/devtools/client/dbg-client.jsm +++ b/toolkit/devtools/client/dbg-client.jsm @@ -1360,7 +1360,7 @@ TabClient.prototype = { eventSource(TabClient.prototype); function WorkerClient(aClient, aForm) { - this._client = aClient; + this.client = aClient; this._actor = aForm.from; this._isClosed = false; this._isFrozen = aForm.isFrozen; @@ -1376,11 +1376,11 @@ function WorkerClient(aClient, aForm) { WorkerClient.prototype = { get _transport() { - return this._client._transport; + return this.client._transport; }, get request() { - return this._client.request; + return this.client.request; }, get actor() { @@ -1397,19 +1397,41 @@ WorkerClient.prototype = { detach: DebuggerClient.requester({ type: "detach" }, { after: function (aResponse) { - this._client.unregisterClient(this); + this.client.unregisterClient(this); return aResponse; }, telemetry: "WORKERDETACH" }), + attachThread: function(aOptions = {}, aOnResponse = noop) { + if (this.thread) { + DevToolsUtils.executeSoon(() => aOnResponse({ + type: "connected", + threadActor: this.thread._actor, + }, this.thread)); + return; + } + + this.request({ + to: this._actor, + type: "connect", + options: aOptions, + }, (aResponse) => { + if (!aResponse.error) { + this.thread = new ThreadClient(this, aResponse.threadActor); + this.client.registerClient(this.thread); + } + aOnResponse(aResponse, this.thread); + }); + }, + _onClose: function () { this.removeListener("close", this._onClose); this.removeListener("freeze", this._onFreeze); this.removeListener("thaw", this._onThaw); - this._client.unregisterClient(this); + this.client.unregisterClient(this); this._closed = true; }, diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js index 0c861ffae160..04779a87aed3 100644 --- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -15,6 +15,7 @@ const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); const { dbg_assert, dumpn, update, fetch } = DevToolsUtils; const { dirname, joinURI } = require("devtools/toolkit/path"); const promise = require("promise"); +const PromiseDebugging = require("PromiseDebugging"); const xpcInspector = require("xpcInspector"); const ScriptStore = require("./utils/ScriptStore"); const {DevToolsWorker} = require("devtools/toolkit/shared/worker.js"); @@ -1494,7 +1495,7 @@ ThreadActor.prototype = { // Clear DOM event breakpoints. // XPCShell tests don't use actual DOM windows for globals and cause // removeListenerForAllEvents to throw. - if (this.global && !this.global.toString().includes("Sandbox")) { + if (!isWorker && this.global && !this.global.toString().includes("Sandbox")) { let els = Cc["@mozilla.org/eventlistenerservice;1"] .getService(Ci.nsIEventListenerService); els.removeListenerForAllEvents(this.global, this._allEventsListener, true); @@ -1933,7 +1934,7 @@ ThreadActor.prototype = { } if (promises.length > 0) { - this.synchronize(Promise.all(promises)); + this.synchronize(promise.all(promises)); } return true; @@ -2870,10 +2871,10 @@ SourceActor.prototype = { actor, GeneratedLocation.fromOriginalLocation(originalLocation) )) { - return Promise.resolve(null); + return promise.resolve(null); } - return Promise.resolve(originalLocation); + return promise.resolve(originalLocation); } else { return this.sources.getAllGeneratedLocations(originalLocation) .then((generatedLocations) => { diff --git a/toolkit/devtools/server/actors/utils/TabSources.js b/toolkit/devtools/server/actors/utils/TabSources.js index 1368a6218bb7..d03022ab375f 100644 --- a/toolkit/devtools/server/actors/utils/TabSources.js +++ b/toolkit/devtools/server/actors/utils/TabSources.js @@ -10,7 +10,7 @@ const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); const { dbg_assert, fetch } = DevToolsUtils; const EventEmitter = require("devtools/toolkit/event-emitter"); const { OriginalLocation, GeneratedLocation, getOffsetColumn } = require("devtools/server/actors/common"); -const { resolve } = Promise; +const { resolve } = require("promise"); loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/script", true); loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/script", true); diff --git a/toolkit/devtools/server/actors/worker.js b/toolkit/devtools/server/actors/worker.js index 8f87fc79c721..5d32e633d305 100644 --- a/toolkit/devtools/server/actors/worker.js +++ b/toolkit/devtools/server/actors/worker.js @@ -1,6 +1,7 @@ "use strict"; let { Ci, Cu } = require("chrome"); +let { DebuggerServer } = require("devtools/server/main"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -28,6 +29,8 @@ function matchWorkerDebugger(dbg, options) { function WorkerActor(dbg) { this._dbg = dbg; this._isAttached = false; + this._threadActor = null; + this._transport = null; } WorkerActor.prototype = { @@ -66,6 +69,33 @@ WorkerActor.prototype = { return { type: "detached" }; }, + onConnect: function (request) { + if (!this._isAttached) { + return { error: "wrongState" }; + } + + if (this._threadActor !== null) { + return { + type: "connected", + threadActor: this._threadActor + }; + } + + return DebuggerServer.connectToWorker( + this.conn, this._dbg, this.actorID, request.options + ).then(({ threadActor, transport }) => { + this._threadActor = threadActor; + this._transport = transport; + + return { + type: "connected", + threadActor: this._threadActor + }; + }, (error) => { + return { error: error.toString() }; + }); + }, + onClose: function () { if (this._isAttached) { this._detach(); @@ -74,6 +104,10 @@ WorkerActor.prototype = { this.conn.sendActorEvent(this.actorID, "close"); }, + onError: function (filename, lineno, message) { + reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n"); + }, + onFreeze: function () { this.conn.sendActorEvent(this.actorID, "freeze"); }, @@ -83,6 +117,12 @@ WorkerActor.prototype = { }, _detach: function () { + if (this._threadActor !== null) { + this._transport.close(); + this._transport = null; + this._threadActor = null; + } + this._dbg.removeListener(this); this._isAttached = false; } @@ -90,7 +130,8 @@ WorkerActor.prototype = { WorkerActor.prototype.requestTypes = { "attach": WorkerActor.prototype.onAttach, - "detach": WorkerActor.prototype.onDetach + "detach": WorkerActor.prototype.onDetach, + "connect": WorkerActor.prototype.onConnect }; exports.WorkerActor = WorkerActor; diff --git a/toolkit/devtools/server/main.js b/toolkit/devtools/server/main.js index 0b8f902caf5c..9c6c3738e6ba 100644 --- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -14,7 +14,7 @@ let { Ci, Cc, CC, Cu, Cr } = require("chrome"); let Services = require("Services"); let { ActorPool, OriginalLocation, RegisteredActorFactory, ObservedActorFactory } = require("devtools/server/actors/common"); -let { LocalDebuggerTransport, ChildDebuggerTransport } = +let { LocalDebuggerTransport, ChildDebuggerTransport, WorkerDebuggerTransport } = require("devtools/toolkit/transport/transport"); let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); let { dumpn, dumpv, dbg_assert } = DevToolsUtils; @@ -685,10 +685,13 @@ var DebuggerServer = { * "debug::packet", and all its actors will have names * beginning with "/". */ - connectToParent: function(aPrefix, aMessageManager) { + connectToParent: function(aPrefix, aScopeOrManager) { this._checkInit(); - let transport = new ChildDebuggerTransport(aMessageManager, aPrefix); + let transport = isWorker ? + new WorkerDebuggerTransport(aScopeOrManager, aPrefix) : + new ChildDebuggerTransport(aScopeOrManager, aPrefix); + return this._onConnection(transport, aPrefix, true); }, @@ -755,6 +758,83 @@ var DebuggerServer = { return deferred.promise; }, + connectToWorker: function (aConnection, aDbg, aId, aOptions) { + return new Promise((resolve, reject) => { + // Step 1: Initialize the worker debugger. + aDbg.initialize("resource://gre/modules/devtools/server/worker.js"); + + // Step 2: Send a connect request to the worker debugger. + aDbg.postMessage(JSON.stringify({ + type: "connect", + id: aId, + options: aOptions + })); + + // Steps 3-5 are performed on the worker thread (see worker.js). + + // Step 6: Wait for a response from the worker debugger. + let listener = { + onClose: () => { + aDbg.removeListener(listener); + + reject("closed"); + }, + + onMessage: (message) => { + let packet = JSON.parse(message); + if (packet.type !== "message" || packet.id !== aId) { + return; + } + + message = packet.message; + if (message.error) { + reject(error); + } + + if (message.type !== "paused") { + return; + } + + aDbg.removeListener(listener); + + // Step 7: Create a transport for the connection to the worker. + let transport = new WorkerDebuggerTransport(aDbg, aId); + transport.ready(); + transport.hooks = { + onClosed: () => { + if (!aDbg.isClosed) { + aDbg.postMessage(JSON.stringify({ + type: "disconnect", + id: aId + })); + } + + aConnection.cancelForwarding(aId); + }, + + onPacket: (packet) => { + // Ensure that any packets received from the server on the worker + // thread are forwarded to the client on the main thread, as if + // they had been sent by the server on the main thread. + aConnection.send(packet); + } + }; + + // Ensure that any packets received from the client on the main thread + // to actors on the worker thread are forwarded to the server on the + // worker thread. + aConnection.setForwarding(aId, transport); + + resolve({ + threadActor: message.from, + transport: transport + }); + } + }; + aDbg.addListener(listener); + }); + }, + /** * Check if the caller is running in a content child process. * diff --git a/toolkit/devtools/server/moz.build b/toolkit/devtools/server/moz.build index 1503896e66da..417b34ddcb98 100644 --- a/toolkit/devtools/server/moz.build +++ b/toolkit/devtools/server/moz.build @@ -51,6 +51,7 @@ EXTRA_JS_MODULES.devtools.server += [ 'content-globals.js', 'main.js', 'protocol.js', + 'worker.js' ] EXTRA_JS_MODULES.devtools.server.actors += [ diff --git a/toolkit/devtools/server/worker.js b/toolkit/devtools/server/worker.js new file mode 100644 index 000000000000..48f414e4828d --- /dev/null +++ b/toolkit/devtools/server/worker.js @@ -0,0 +1,66 @@ +"use strict" + +loadSubScript("resource://gre/modules/devtools/worker-loader.js"); + +let { ActorPool } = worker.require("devtools/server/actors/common"); +let { ThreadActor } = worker.require("devtools/server/actors/script"); +let { TabSources } = worker.require("devtools/server/actors/utils/TabSources"); +let makeDebugger = worker.require("devtools/server/actors/utils/make-debugger"); +let { DebuggerServer } = worker.require("devtools/server/main"); + +DebuggerServer.init(); +DebuggerServer.createRootActor = function () { + throw new Error("Should never get here!"); +}; + +let connections = Object.create(null); + +this.addEventListener("message", function (event) { + let packet = JSON.parse(event.data); + switch (packet.type) { + case "connect": + // Step 3: Create a connection to the parent. + let connection = DebuggerServer.connectToParent(packet.id, this); + connections[packet.id] = connection; + + // Step 4: Create a thread actor for the connection to the parent. + let pool = new ActorPool(connection); + connection.addActorPool(pool); + + let sources = null; + + let actor = new ThreadActor({ + makeDebugger: makeDebugger.bind(null, { + findDebuggees: () => { + return [this.global]; + }, + + shouldAddNewGlobalAsDebuggee: () => { + return true; + }, + }), + + get sources() { + if (sources === null) { + sources = new TabSources(actor); + } + return sources; + } + }, global); + + pool.addActor(actor); + + // Step 5: Attach to the thread actor. + // + // This will cause a packet to be sent over the connection to the parent. + // Because this connection uses WorkerDebuggerTransport internally, this + // packet will be sent using WorkerDebuggerGlobalScope.postMessage, causing + // an onMessage event to be fired on the WorkerDebugger in the main thread. + actor.onAttach({}); + break; + + case "disconnect": + connections[packet.id].close(); + break; + }; +}); diff --git a/toolkit/devtools/worker-loader.js b/toolkit/devtools/worker-loader.js index 6652c38aab3f..f5932a17aae6 100644 --- a/toolkit/devtools/worker-loader.js +++ b/toolkit/devtools/worker-loader.js @@ -435,6 +435,8 @@ let { } else { // Worker thread let requestors = []; + let scope = this; + let xpcInspector = { get lastNestRequestor() { return requestors.length === 0 ? null : requestors[0]; @@ -442,13 +444,13 @@ let { enterNestedEventLoop: function (requestor) { requestors.push(requestor); - this.enterEventLoop(); + scope.enterEventLoop(); return requestors.length; }, exitNestedEventLoop: function () { requestors.pop(); - this.leaveEventLoop(); + scope.leaveEventLoop(); return requestors.length; } }; From cc921d71fa15385c712767a4bf7ac8291574b208 Mon Sep 17 00:00:00 2001 From: Gijs Kruitbosch Date: Mon, 8 Jun 2015 16:56:34 +0100 Subject: [PATCH 07/16] Bug 1172270 - don't cause extra flushes for reader mode, r=margaret,smaug --HG-- rename : browser/base/content/test/general/browser_readerMode.js => browser/base/content/test/general/browser_readerMode_hidden_nodes.js rename : browser/base/content/test/general/readerModeArticle.html => browser/base/content/test/general/readerModeArticleHiddenNodes.html extra : rebase_source : 184f260d55a83e866b20befda517ffe42a704002 extra : histedit_source : 4da59cfad20b681e6af20d53df03d4ed4b1ab2fb --- browser/base/content/tab-content.js | 31 +++++++++++ browser/base/content/test/general/browser.ini | 3 ++ .../browser_readerMode_hidden_nodes.js | 45 ++++++++++++++++ .../general/readerModeArticleHiddenNodes.html | 22 ++++++++ .../BrowserTestUtils/ContentTaskUtils.jsm | 52 ++++++++++++++++++- toolkit/components/reader/ReaderMode.jsm | 11 +--- 6 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 browser/base/content/test/general/browser_readerMode_hidden_nodes.js create mode 100644 browser/base/content/test/general/readerModeArticleHiddenNodes.html diff --git a/browser/base/content/tab-content.js b/browser/base/content/tab-content.js index e67ce7a05cc3..2316b34b4a1e 100644 --- a/browser/base/content/tab-content.js +++ b/browser/base/content/tab-content.js @@ -337,6 +337,7 @@ let AboutReaderListener = { break; case "pagehide": + this.cancelPotentialPendingReadabilityCheck(); sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false }); break; @@ -353,12 +354,42 @@ let AboutReaderListener = { } }, + + /** + * NB: this function will update the state of the reader button asynchronously + * after the next mozAfterPaint call (assuming reader mode is enabled and + * this is a suitable document). Calling it on things which won't be + * painted is not going to work. + */ updateReaderButton: function(forceNonArticle) { if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader || !(content.document instanceof content.HTMLDocument) || content.document.mozSyntheticDocument) { return; } + + this.scheduleReadabilityCheckPostPaint(forceNonArticle); + }, + + cancelPotentialPendingReadabilityCheck: function() { + if (this._pendingReadabilityCheck) { + removeEventListener("MozAfterPaint", this._pendingReadabilityCheck); + delete this._pendingReadabilityCheck; + } + }, + + scheduleReadabilityCheckPostPaint: function(forceNonArticle) { + if (this._pendingReadabilityCheck) { + // We need to stop this check before we re-add one because we don't know + // if forceNonArticle was true or false last time. + this.cancelPotentialPendingReadabilityCheck(); + } + this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(this, forceNonArticle); + addEventListener("MozAfterPaint", this._pendingReadabilityCheck); + }, + + onPaintWhenWaitedFor: function(forceNonArticle) { + this.cancelPotentialPendingReadabilityCheck(); // Only send updates when there are articles; there's no point updating with // |false| all the time. if (ReaderMode.isProbablyReaderable(content.document)) { diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index cc3598212b25..a7ebc9f3665c 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -478,6 +478,9 @@ skip-if = e10s # bug 1100687 - test directly manipulates content (content.docume [browser_readerMode.js] support-files = readerModeArticle.html +[browser_readerMode_hidden_nodes.js] +support-files = + readerModeArticleHiddenNodes.html [browser_bug1124271_readerModePinnedTab.js] support-files = readerModeArticle.html diff --git a/browser/base/content/test/general/browser_readerMode_hidden_nodes.js b/browser/base/content/test/general/browser_readerMode_hidden_nodes.js new file mode 100644 index 000000000000..fb791f211d6f --- /dev/null +++ b/browser/base/content/test/general/browser_readerMode_hidden_nodes.js @@ -0,0 +1,45 @@ +/* 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/. */ + +/** + * Test that the reader mode button appears and works properly on + * reader-able content, and that ReadingList button can open and close + * its Sidebar UI. + */ +const TEST_PREFS = [ + ["reader.parse-on-load.enabled", true], + ["browser.reader.detectedFirstArticle", false], +]; + +const TEST_PATH = "http://example.com/browser/browser/base/content/test/general/"; + +let readerButton = document.getElementById("reader-mode-button"); + +add_task(function* test_reader_button() { + registerCleanupFunction(function() { + // Reset test prefs. + TEST_PREFS.forEach(([name, value]) => { + Services.prefs.clearUserPref(name); + }); + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } + }); + + // Set required test prefs. + TEST_PREFS.forEach(([name, value]) => { + Services.prefs.setBoolPref(name, value); + }); + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + is_element_hidden(readerButton, "Reader mode button is not present on a new tab"); + // Point tab to a test page that is not reader-able due to hidden nodes. + let url = TEST_PATH + "readerModeArticleHiddenNodes.html"; + yield promiseTabLoadEvent(tab, url); + yield ContentTask.spawn(tab.linkedBrowser, "", function() { + return ContentTaskUtils.waitForEvent(content, "MozAfterPaint"); + }); + + is_element_hidden(readerButton, "Reader mode button is still not present on tab with unreadable content."); +}); diff --git a/browser/base/content/test/general/readerModeArticleHiddenNodes.html b/browser/base/content/test/general/readerModeArticleHiddenNodes.html new file mode 100644 index 000000000000..92441b797807 --- /dev/null +++ b/browser/base/content/test/general/readerModeArticleHiddenNodes.html @@ -0,0 +1,22 @@ + + + +Article title + + + + +
Site header
+
+

Article title

+

by Jane Doe

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.

+

Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.

+

Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.

+

Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.

+

Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.

+
+ + diff --git a/testing/mochitest/BrowserTestUtils/ContentTaskUtils.jsm b/testing/mochitest/BrowserTestUtils/ContentTaskUtils.jsm index 712b00a3a71f..f94cf0250f36 100644 --- a/testing/mochitest/BrowserTestUtils/ContentTaskUtils.jsm +++ b/testing/mochitest/BrowserTestUtils/ContentTaskUtils.jsm @@ -66,5 +66,55 @@ this.ContentTaskUtils = { tries++; }, interval); }); - } + }, + + /** + * Waits for an event to be fired on a specified element. + * + * Usage: + * let promiseEvent = ContentTasKUtils.waitForEvent(element, "eventName"); + * // Do some processing here that will cause the event to be fired + * // ... + * // Now yield until the Promise is fulfilled + * let receivedEvent = yield promiseEvent; + * + * @param {Element} subject + * The element that should receive the event. + * @param {string} eventName + * Name of the event to listen to. + * @param {bool} capture [optional] + * True to use a capturing listener. + * @param {function} checkFn [optional] + * Called with the Event object as argument, should return true if the + * event is the expected one, or false if it should be ignored and + * listening should continue. If not specified, the first event with + * the specified name resolves the returned promise. + * + * @note Because this function is intended for testing, any error in checkFn + * will cause the returned promise to be rejected instead of waiting for + * the next event, since this is probably a bug in the test. + * + * @returns {Promise} + * @resolves The Event object. + */ + waitForEvent(subject, eventName, capture, checkFn) { + return new Promise((resolve, reject) => { + subject.addEventListener(eventName, function listener(event) { + try { + if (checkFn && !checkFn(event)) { + return; + } + subject.removeEventListener(eventName, listener, capture); + resolve(event); + } catch (ex) { + try { + subject.removeEventListener(eventName, listener, capture); + } catch (ex2) { + // Maybe the provided object does not support removeEventListener. + } + reject(ex); + } + }, capture); + }); + }, }; diff --git a/toolkit/components/reader/ReaderMode.jsm b/toolkit/components/reader/ReaderMode.jsm index c5e31e72b29a..960906fbbba8 100644 --- a/toolkit/components/reader/ReaderMode.jsm +++ b/toolkit/components/reader/ReaderMode.jsm @@ -132,20 +132,11 @@ this.ReaderMode = { // We pass in a helper function to determine if a node is visible, because // it uses gecko APIs that the engine-agnostic readability code can't rely // upon. - // NB: we need to do a flush the first time we call this, so we keep track of - // this using a property: - this._needFlushForVisibilityCheck = true; return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils)); }, isNodeVisible: function(utils, node) { - let bounds; - if (this._needFlushForVisibilityCheck) { - bounds = node.getBoundingClientRect(); - this._needFlushForVisibilityCheck = false; - } else { - bounds = utils.getBoundsWithoutFlushing(node); - } + let bounds = utils.getBoundsWithoutFlushing(node); return bounds.height > 0 && bounds.width > 0; }, From 774b49304f9168f69fc3047bc2901be9b47229f5 Mon Sep 17 00:00:00 2001 From: Patrick Brosset Date: Thu, 11 Jun 2015 15:45:57 +0200 Subject: [PATCH 08/16] Bug 1155663 - Show animations as synchronized time blocks in animation inspector; r=bgrins This is the first step towards the animation-inspector UI v3 (bug 1153271). The new UI is still hidden behind a pref, and this change doesn't implement everything that is in the current v2 UI. This introduces a new Timeline graph to represent all currently animated nodes below the currently selected node. v2 used to show them as independent player widgets. With this patch, we now show them as synchronized time blocks on a common time scale. Each animation has a preview of the animated node in the left sidebar, and a time block on the right, the width of which represents its duration. The animation name is also displayed. There's also a time graduations header and background that gives the user information about how long do the animations last. This change does *not* provide a way to know what's the currentTime nor a way to set it yet. This also makes the existing animationinspector tests that still make sense with the new timeline-based UI run with the new UI pref on. --HG-- extra : rebase_source : 65634e8f5e618f15e8d33c36a90217ba07a310f4 --- .../animation-controller.js | 17 +- .../animationinspector/animation-panel.js | 88 +-- .../devtools/animationinspector/components.js | 510 ++++++++++++++---- browser/devtools/animationinspector/moz.build | 1 + ...rowser_animation_empty_on_invalid_nodes.js | 39 +- .../test/browser_animation_panel_exists.js | 9 + ...imation_participate_in_inspector_update.js | 13 +- ...tion_playerWidgets_appear_on_panel_init.js | 11 +- ...er_animation_playerWidgets_target_nodes.js | 23 +- ...er_animation_refresh_on_added_animation.js | 26 +- ..._animation_refresh_on_removed_animation.js | 29 +- .../browser_animation_refresh_when_active.js | 19 +- ...me_nb_of_playerWidgets_and_playerFronts.js | 12 + ...er_animation_shows_player_on_valid_node.js | 14 +- ...owser_animation_target_highlight_select.js | 36 +- ...mation_toggle_button_toggles_animations.js | 2 +- .../test/browser_animation_toolbar_exists.js | 2 +- ..._ui_updates_when_animation_data_changes.js | 60 ++- .../devtools/animationinspector/test/head.js | 87 ++- browser/devtools/animationinspector/utils.js | 135 +++++ .../devtools/animationinspector.properties | 6 + .../shared/devtools/animationinspector.css | 179 +++++- toolkit/devtools/server/actors/animation.js | 129 +++-- 23 files changed, 1206 insertions(+), 241 deletions(-) create mode 100644 browser/devtools/animationinspector/utils.js diff --git a/browser/devtools/animationinspector/animation-controller.js b/browser/devtools/animationinspector/animation-controller.js index bfa01ccc58ae..5fb8911ec812 100644 --- a/browser/devtools/animationinspector/animation-controller.js +++ b/browser/devtools/animationinspector/animation-controller.js @@ -114,6 +114,7 @@ let AnimationsController = { "setPlaybackRate"); this.hasTargetNode = yield target.actorHasMethod("domwalker", "getNodeFromActor"); + this.isNewUI = Services.prefs.getBoolPref("devtools.inspector.animationInspectorV3"); if (this.destroyed) { console.warn("Could not fully initialize the AnimationsController"); @@ -240,11 +241,15 @@ let AnimationsController = { for (let {type, player} of changes) { if (type === "added") { this.animationPlayers.push(player); - player.startAutoRefresh(); + if (!this.isNewUI) { + player.startAutoRefresh(); + } } if (type === "removed") { - player.stopAutoRefresh(); + if (!this.isNewUI) { + player.stopAutoRefresh(); + } yield player.release(); let index = this.animationPlayers.indexOf(player); this.animationPlayers.splice(index, 1); @@ -256,12 +261,20 @@ let AnimationsController = { }), startAllAutoRefresh: function() { + if (this.isNewUI) { + return; + } + for (let front of this.animationPlayers) { front.startAutoRefresh(); } }, stopAllAutoRefresh: function() { + if (this.isNewUI) { + return; + } + for (let front of this.animationPlayers) { front.stopAutoRefresh(); } diff --git a/browser/devtools/animationinspector/animation-panel.js b/browser/devtools/animationinspector/animation-panel.js index 7814612ea3c4..3d6f1a95b9c1 100644 --- a/browser/devtools/animationinspector/animation-panel.js +++ b/browser/devtools/animationinspector/animation-panel.js @@ -3,14 +3,17 @@ /* 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/. */ +/* globals AnimationsController, document, performance, promise, + gToolbox, gInspector, requestAnimationFrame, cancelAnimationFrame, L10N */ "use strict"; +const {createNode} = require("devtools/animationinspector/utils"); const { PlayerMetaDataHeader, PlaybackRateSelector, AnimationTargetNode, - createNode + AnimationsTimeline } = require("devtools/animationinspector/components"); /** @@ -22,7 +25,8 @@ let AnimationsPanel = { initialize: Task.async(function*() { if (AnimationsController.destroyed) { - console.warn("Could not initialize the animation-panel, controller was destroyed"); + console.warn("Could not initialize the animation-panel, controller " + + "was destroyed"); return; } if (this.initialized) { @@ -45,13 +49,18 @@ let AnimationsPanel = { this.togglePicker = hUtils.togglePicker.bind(hUtils); this.onPickerStarted = this.onPickerStarted.bind(this); this.onPickerStopped = this.onPickerStopped.bind(this); - this.createPlayerWidgets = this.createPlayerWidgets.bind(this); + this.refreshAnimations = this.refreshAnimations.bind(this); this.toggleAll = this.toggleAll.bind(this); this.onTabNavigated = this.onTabNavigated.bind(this); this.startListeners(); - yield this.createPlayerWidgets(); + if (AnimationsController.isNewUI) { + this.animationsTimelineComponent = new AnimationsTimeline(gInspector); + this.animationsTimelineComponent.init(this.playersEl); + } + + yield this.refreshAnimations(); this.initialized.resolve(); @@ -69,6 +78,11 @@ let AnimationsPanel = { this.destroyed = promise.defer(); this.stopListeners(); + + if (this.animationsTimelineComponent) { + this.animationsTimelineComponent.destroy(); + this.animationsTimelineComponent = null; + } yield this.destroyPlayerWidgets(); this.playersEl = this.errorMessageEl = null; @@ -79,7 +93,7 @@ let AnimationsPanel = { startListeners: function() { AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT, - this.createPlayerWidgets); + this.refreshAnimations); this.pickerButtonEl.addEventListener("click", this.togglePicker, false); gToolbox.on("picker-started", this.onPickerStarted); @@ -91,7 +105,7 @@ let AnimationsPanel = { stopListeners: function() { AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT, - this.createPlayerWidgets); + this.refreshAnimations); this.pickerButtonEl.removeEventListener("click", this.togglePicker, false); gToolbox.off("picker-started", this.onPickerStarted); @@ -122,16 +136,18 @@ let AnimationsPanel = { toggleAll: Task.async(function*() { let btnClass = this.toggleAllButtonEl.classList; - // Toggling all animations is async and it may be some time before each of - // the current players get their states updated, so toggle locally too, to - // avoid the timelines from jumping back and forth. - if (this.playerWidgets) { - let currentWidgetStateChange = []; - for (let widget of this.playerWidgets) { - currentWidgetStateChange.push(btnClass.contains("paused") - ? widget.play() : widget.pause()); + if (!AnimationsController.isNewUI) { + // Toggling all animations is async and it may be some time before each of + // the current players get their states updated, so toggle locally too, to + // avoid the timelines from jumping back and forth. + if (this.playerWidgets) { + let currentWidgetStateChange = []; + for (let widget of this.playerWidgets) { + currentWidgetStateChange.push(btnClass.contains("paused") + ? widget.play() : widget.pause()); + } + yield promise.all(currentWidgetStateChange).catch(Cu.reportError); } - yield promise.all(currentWidgetStateChange).catch(Cu.reportError); } btnClass.toggle("paused"); @@ -142,14 +158,21 @@ let AnimationsPanel = { this.toggleAllButtonEl.classList.remove("paused"); }, - createPlayerWidgets: Task.async(function*() { + refreshAnimations: Task.async(function*() { let done = gInspector.updating("animationspanel"); // Empty the whole panel first. this.hideErrorMessage(); yield this.destroyPlayerWidgets(); - // If there are no players to show, show the error message instead and return. + // Re-render the timeline component. + if (this.animationsTimelineComponent) { + this.animationsTimelineComponent.render( + AnimationsController.animationPlayers); + } + + // If there are no players to show, show the error message instead and + // return. if (!AnimationsController.animationPlayers.length) { this.displayErrorMessage(); this.emit(this.UI_UPDATED_EVENT); @@ -157,17 +180,21 @@ let AnimationsPanel = { return; } - // Otherwise, create player widgets. - this.playerWidgets = []; - let initPromises = []; + // Otherwise, create player widgets (only when isNewUI is false, the + // timeline has already been re-rendered). + if (!AnimationsController.isNewUI) { + this.playerWidgets = []; + let initPromises = []; - for (let player of AnimationsController.animationPlayers) { - let widget = new PlayerWidget(player, this.playersEl); - initPromises.push(widget.initialize()); - this.playerWidgets.push(widget); + for (let player of AnimationsController.animationPlayers) { + let widget = new PlayerWidget(player, this.playersEl); + initPromises.push(widget.initialize()); + this.playerWidgets.push(widget); + } + + yield initPromises; } - yield initPromises; this.emit(this.UI_UPDATED_EVENT); done(); }), @@ -392,9 +419,8 @@ PlayerWidget.prototype = { onPlayPauseBtnClick: function() { if (this.player.state.playState === "running") { return this.pause(); - } else { - return this.play(); } + return this.play(); }, onRewindBtnClick: function() { @@ -406,7 +432,7 @@ PlayerWidget.prototype = { let time = state.duration; if (state.iterationCount) { - time = state.iterationCount * state.duration; + time = state.iterationCount * state.duration; } this.setCurrentTime(time, true); }, @@ -466,7 +492,8 @@ PlayerWidget.prototype = { */ setCurrentTime: Task.async(function*(time, shouldPause) { if (!AnimationsController.hasSetCurrentTime) { - throw new Error("This server version doesn't support setting animations' currentTime"); + throw new Error("This server version doesn't support setting " + + "animations' currentTime"); } if (shouldPause) { @@ -492,7 +519,8 @@ PlayerWidget.prototype = { */ setPlaybackRate: function(rate) { if (!AnimationsController.hasSetPlaybackRate) { - throw new Error("This server version doesn't support setting animations' playbackRate"); + throw new Error("This server version doesn't support setting " + + "animations' playbackRate"); } return this.player.setPlaybackRate(rate); diff --git a/browser/devtools/animationinspector/components.js b/browser/devtools/animationinspector/components.js index 78ebda5527f3..f2c155a6744e 100644 --- a/browser/devtools/animationinspector/components.js +++ b/browser/devtools/animationinspector/components.js @@ -3,6 +3,7 @@ /* 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/. */ +/* globals ViewHelpers */ "use strict"; @@ -19,11 +20,19 @@ // 4. destroy the component: // c.destroy(); -const {Cu} = require('chrome'); +const {Cu} = require("chrome"); Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); +const { + createNode, + drawGraphElementBackground, + findOptimalTimeInterval +} = require("devtools/animationinspector/utils"); const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties"; const L10N = new ViewHelpers.L10N(STRINGS_URI); +const MILLIS_TIME_FORMAT_MAX_DURATION = 4000; +// The minimum spacing between 2 time graduation headers in the timeline (ms). +const TIME_GRADUATION_MIN_SPACING = 40; /** * UI component responsible for displaying and updating the player meta-data: @@ -75,9 +84,9 @@ PlayerMetaDataHeader.prototype = { // Animation duration. this.durationLabel = createNode({ parent: metaData, - nodeType: "span" + nodeType: "span", + textContent: L10N.getStr("player.animationDurationLabel") }); - this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel"); this.durationValue = createNode({ parent: metaData, @@ -90,9 +99,9 @@ PlayerMetaDataHeader.prototype = { nodeType: "span", attributes: { "style": "display:none;" - } + }, + textContent: L10N.getStr("player.animationDelayLabel") }); - this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel"); this.delayValue = createNode({ parent: metaData, @@ -106,9 +115,9 @@ PlayerMetaDataHeader.prototype = { nodeType: "span", attributes: { "style": "display:none;" - } + }, + textContent: L10N.getStr("player.animationIterationCountLabel") }); - this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel"); this.iterationValue = createNode({ parent: metaData, @@ -224,7 +233,7 @@ PlaybackRateSelector.prototype = { * different from the existing presets. */ getCurrentPresets: function({playbackRate}) { - return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b); + return [...new Set([...this.PRESETS, playbackRate])].sort((a, b) => a > b); }, render: function(state) { @@ -248,9 +257,9 @@ PlaybackRateSelector.prototype = { nodeType: "option", attributes: { value: preset, - } + }, + textContent: L10N.getFormatStr("player.playbackRateLabel", preset) }); - option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset); if (preset === state.playbackRate) { option.setAttribute("selected", ""); } @@ -261,7 +270,7 @@ PlaybackRateSelector.prototype = { this.currentRate = state.playbackRate; }, - onSelectionChanged: function(e) { + onSelectionChanged: function() { this.emit("rate-changed", parseFloat(this.el.value)); } }; @@ -272,9 +281,13 @@ PlaybackRateSelector.prototype = { * @param {InspectorPanel} inspector Requires a reference to the inspector-panel * to highlight and select the node, as well as refresh it when there are * mutations. + * @param {Object} options Supported properties are: + * - compact {Boolean} Defaults to false. If true, nodes will be previewed like + * tag#id.class instead of */ -function AnimationTargetNode(inspector) { +function AnimationTargetNode(inspector, options={}) { this.inspector = inspector; + this.options = options; this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this); this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this); @@ -313,7 +326,9 @@ AnimationTargetNode.prototype = { nodeType: "span" }); - this.previewEl.appendChild(document.createTextNode("<")); + if (!this.options.compact) { + this.previewEl.appendChild(document.createTextNode("<")); + } // Tag name. this.tagNameEl = createNode({ @@ -330,15 +345,26 @@ AnimationTargetNode.prototype = { nodeType: "span" }); - createNode({ - parent: this.idEl, - nodeType: "span", - attributes: { - "class": "attribute-name theme-fg-color2" - } - }).textContent = "id"; - - this.idEl.appendChild(document.createTextNode("=\"")); + if (!this.options.compact) { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "id" + }); + this.idEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color2" + }, + textContent: "#" + }); + } createNode({ parent: this.idEl, @@ -348,7 +374,9 @@ AnimationTargetNode.prototype = { } }); - this.idEl.appendChild(document.createTextNode("\"")); + if (!this.options.compact) { + this.idEl.appendChild(document.createTextNode("\"")); + } // Class attribute container. this.classEl = createNode({ @@ -356,15 +384,26 @@ AnimationTargetNode.prototype = { nodeType: "span" }); - createNode({ - parent: this.classEl, - nodeType: "span", - attributes: { - "class": "attribute-name theme-fg-color2" - } - }).textContent = "class"; - - this.classEl.appendChild(document.createTextNode("=\"")); + if (!this.options.compact) { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "class" + }); + this.classEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color6" + }, + textContent: "." + }); + } createNode({ parent: this.classEl, @@ -374,9 +413,10 @@ AnimationTargetNode.prototype = { } }); - this.classEl.appendChild(document.createTextNode("\"")); - - this.previewEl.appendChild(document.createTextNode(">")); + if (!this.options.compact) { + this.classEl.appendChild(document.createTextNode("\"")); + this.previewEl.appendChild(document.createTextNode(">")); + } // Init events for highlighting and selecting the node. this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver); @@ -430,73 +470,357 @@ AnimationTargetNode.prototype = { } }, - render: function(playerFront) { + render: Task.async(function*(playerFront) { this.playerFront = playerFront; - this.inspector.walker.getNodeFromActor(playerFront.actorID, ["node"]).then(nodeFront => { - // We might have been destroyed in the meantime, or the node might not be found. - if (!this.el || !nodeFront) { - return; - } + this.nodeFront = undefined; - this.nodeFront = nodeFront; - let {tagName, attributes} = nodeFront; - - this.tagNameEl.textContent = tagName.toLowerCase(); - - let idIndex = attributes.findIndex(({name}) => name === "id"); - if (idIndex > -1 && attributes[idIndex].value) { - this.idEl.querySelector(".attribute-value").textContent = - attributes[idIndex].value; - this.idEl.style.display = "inline"; - } else { - this.idEl.style.display = "none"; - } - - let classIndex = attributes.findIndex(({name}) => name === "class"); - if (classIndex > -1 && attributes[classIndex].value) { - this.classEl.querySelector(".attribute-value").textContent = - attributes[classIndex].value; - this.classEl.style.display = "inline"; - } else { - this.classEl.style.display = "none"; - } - - this.emit("target-retrieved"); - }, e => { - this.nodeFront = null; + try { + this.nodeFront = yield this.inspector.walker.getNodeFromActor( + playerFront.actorID, ["node"]); + } catch (e) { + // We might have been destroyed in the meantime, or the node might not be + // found. if (!this.el) { - console.warn("Cound't retrieve the animation target node, widget destroyed"); - } else { - console.error(e); + console.warn("Cound't retrieve the animation target node, widget " + + "destroyed"); } - }); + console.error(e); + return; + } + + if (!this.nodeFront || !this.el) { + return; + } + + let {tagName, attributes} = this.nodeFront; + + this.tagNameEl.textContent = tagName.toLowerCase(); + + let idIndex = attributes.findIndex(({name}) => name === "id"); + if (idIndex > -1 && attributes[idIndex].value) { + this.idEl.querySelector(".attribute-value").textContent = + attributes[idIndex].value; + this.idEl.style.display = "inline"; + } else { + this.idEl.style.display = "none"; + } + + let classIndex = attributes.findIndex(({name}) => name === "class"); + if (classIndex > -1 && attributes[classIndex].value) { + let value = attributes[classIndex].value; + if (this.options.compact) { + value = value.split(" ").join("."); + } + + this.classEl.querySelector(".attribute-value").textContent = value; + this.classEl.style.display = "inline"; + } else { + this.classEl.style.display = "none"; + } + + this.emit("target-retrieved"); + }) +}; + +/** + * The TimeScale helper object is used to know which size should something be + * displayed with in the animation panel, depending on the animations that are + * currently displayed. + * If there are 5 animations displayed, and the first one starts at 10000ms and + * the last one ends at 20000ms, then this helper can be used to convert any + * time in this range to a distance in pixels. + * + * For the helper to know how to convert, it needs to know all the animations. + * Whenever a new animation is added to the panel, addAnimation(state) should be + * called. reset() can be called to start over. + */ +let TimeScale = { + minStartTime: Infinity, + maxEndTime: 0, + + /** + * Add a new animation to time scale. + * @param {Object} state A PlayerFront.state object. + */ + addAnimation: function({startTime, delay, duration, iterationCount}) { + this.minStartTime = Math.min(this.minStartTime, startTime); + let length = delay + (duration * (!iterationCount ? 1 : iterationCount)); + this.maxEndTime = Math.max(this.maxEndTime, startTime + length); + }, + + /** + * Reset the current time scale. + */ + reset: function() { + this.minStartTime = Infinity; + this.maxEndTime = 0; + }, + + /** + * Convert a startTime to a distance in pixels, in the current time scale. + * @param {Number} time + * @param {Number} containerWidth The width of the container element. + * @return {Number} + */ + startTimeToDistance: function(time, containerWidth) { + time -= this.minStartTime; + return this.durationToDistance(time, containerWidth); + }, + + /** + * Convert a duration to a distance in pixels, in the current time scale. + * @param {Number} time + * @param {Number} containerWidth The width of the container element. + * @return {Number} + */ + durationToDistance: function(duration, containerWidth) { + return containerWidth * duration / (this.maxEndTime - this.minStartTime); + }, + + /** + * Convert a distance in pixels to a time, in the current time scale. + * @param {Number} distance + * @param {Number} containerWidth The width of the container element. + * @return {Number} + */ + distanceToTime: function(distance, containerWidth) { + return this.minStartTime + + ((this.maxEndTime - this.minStartTime) * distance / containerWidth); + }, + + /** + * Convert a distance in pixels to a time, in the current time scale. + * The time will be relative to the current minimum start time. + * @param {Number} distance + * @param {Number} containerWidth The width of the container element. + * @return {Number} + */ + distanceToRelativeTime: function(distance, containerWidth) { + let time = this.distanceToTime(distance, containerWidth); + return time - this.minStartTime; + }, + + /** + * Depending on the time scale, format the given time as milliseconds or + * seconds. + * @param {Number} time + * @return {String} The formatted time string. + */ + formatTime: function(time) { + let duration = this.maxEndTime - this.minStartTime; + + // Format in milliseconds if the total duration is short enough. + if (duration <= MILLIS_TIME_FORMAT_MAX_DURATION) { + return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0)); + } + + // Otherwise format in seconds. + return L10N.getFormatStr("player.timeLabel", (time / 1000).toFixed(1)); } }; /** - * DOM node creation helper function. - * @param {Object} Options to customize the node to be created. - * - nodeType {String} Optional, defaults to "div", - * - attributes {Object} Optional attributes object like - * {attrName1:value1, attrName2: value2, ...} - * - parent {DOMNode} Mandatory node to append the newly created node to. - * @return {DOMNode} The newly created node. + * UI component responsible for displaying a timeline for animations. + * The timeline is essentially a graph with time along the x axis and animations + * along the y axis. + * The time is represented with a graduation header at the top and a current + * time play head. + * Animations are organized by lines, with a left margin containing the preview + * of the target DOM element the animation applies to. */ -function createNode(options) { - if (!options.parent) { - throw new Error("Missing parent DOMNode to create new node"); - } +function AnimationsTimeline(inspector) { + this.animations = []; + this.targetNodes = []; + this.inspector = inspector; - let type = options.nodeType || "div"; - let node = options.parent.ownerDocument.createElement(type); - - for (let name in options.attributes || {}) { - let value = options.attributes[name]; - node.setAttribute(name, value); - } - - options.parent.appendChild(node); - return node; + this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this); } -exports.createNode = createNode; +exports.AnimationsTimeline = AnimationsTimeline; + +AnimationsTimeline.prototype = { + init: function(containerEl) { + this.win = containerEl.ownerDocument.defaultView; + + this.rootWrapperEl = createNode({ + parent: containerEl, + attributes: { + "class": "animation-timeline" + } + }); + + this.timeHeaderEl = createNode({ + parent: this.rootWrapperEl, + attributes: { + "class": "time-header" + } + }); + + this.animationsEl = createNode({ + parent: this.rootWrapperEl, + nodeType: "ul", + attributes: { + "class": "animations" + } + }); + }, + + destroy: function() { + this.unrender(); + + this.rootWrapperEl.remove(); + this.animations = []; + + this.rootWrapperEl = null; + this.timeHeaderEl = null; + this.animationsEl = null; + this.win = null; + this.inspector = null; + }, + + destroyTargetNodes: function() { + for (let targetNode of this.targetNodes) { + targetNode.destroy(); + } + this.targetNodes = []; + }, + + unrender: function() { + for (let animation of this.animations) { + animation.off("changed", this.onAnimationStateChanged); + } + + TimeScale.reset(); + this.destroyTargetNodes(); + this.animationsEl.innerHTML = ""; + }, + + render: function(animations) { + this.unrender(); + + this.animations = animations; + if (!this.animations.length) { + return; + } + + // Loop first to set the time scale for all current animations. + for (let {state} of animations) { + TimeScale.addAnimation(state); + } + + this.drawHeaderAndBackground(); + + for (let animation of this.animations) { + animation.on("changed", this.onAnimationStateChanged); + + // Each line contains the target animated node and the animation time + // block. + let animationEl = createNode({ + parent: this.animationsEl, + nodeType: "li", + attributes: { + "class": "animation" + } + }); + + // Left sidebar for the animated node. + let animatedNodeEl = createNode({ + parent: animationEl, + attributes: { + "class": "target" + } + }); + + let timeBlockEl = createNode({ + parent: animationEl, + attributes: { + "class": "time-block" + } + }); + + this.drawTimeBlock(animation, timeBlockEl); + + // Draw the animated node target. + let targetNode = new AnimationTargetNode(this.inspector, {compact: true}); + targetNode.init(animatedNodeEl); + targetNode.render(animation); + + // Save the targetNode so it can be destroyed later. + this.targetNodes.push(targetNode); + } + }, + + onAnimationStateChanged: function() { + // For now, simply re-render the component. The animation front's state has + // already been updated. + this.render(this.animations); + }, + + drawHeaderAndBackground: function() { + let width = this.timeHeaderEl.offsetWidth; + let scale = width / (TimeScale.maxEndTime - TimeScale.minStartTime); + drawGraphElementBackground(this.win.document, "time-graduations", width, scale); + + // And the time graduation header. + this.timeHeaderEl.innerHTML = ""; + let interval = findOptimalTimeInterval(scale, TIME_GRADUATION_MIN_SPACING); + for (let i = 0; i < width; i += interval) { + createNode({ + parent: this.timeHeaderEl, + nodeType: "span", + attributes: { + "class": "time-tick", + "style": `left:${i}px` + }, + textContent: TimeScale.formatTime( + TimeScale.distanceToRelativeTime(i, width)) + }); + } + }, + + drawTimeBlock: function({state}, el) { + let width = el.offsetWidth; + + // Container for all iterations and delay. Positioned at the right start + // time. + let x = TimeScale.startTimeToDistance(state.startTime + (state.delay || 0), + width); + // With the right width (duration*duration). + let count = state.iterationCount || 1; + let w = TimeScale.durationToDistance(state.duration, width); + + let iterations = createNode({ + parent: el, + attributes: { + "class": "iterations" + (state.iterationCount ? "" : " infinite"), + // Individual iterations are represented by setting the size of the + // repeating linear-gradient. + "style": `left:${x}px; + width:${w * count}px; + background-size:${Math.max(w, 2)}px 100%;` + } + }); + + // The animation name is displayed over the iterations. + createNode({ + parent: iterations, + attributes: { + "class": "name" + }, + textContent: state.name + }); + + // Delay. + if (state.delay) { + let delay = TimeScale.durationToDistance(state.delay, width); + createNode({ + parent: iterations, + attributes: { + "class": "delay", + "style": `left:-${delay}px; + width:${delay}px;` + } + }); + } + } +}; diff --git a/browser/devtools/animationinspector/moz.build b/browser/devtools/animationinspector/moz.build index 4b1e70b7a076..b69b5c83d074 100644 --- a/browser/devtools/animationinspector/moz.build +++ b/browser/devtools/animationinspector/moz.build @@ -8,4 +8,5 @@ BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] EXTRA_JS_MODULES.devtools.animationinspector += [ 'components.js', + 'utils.js', ] diff --git a/browser/devtools/animationinspector/test/browser_animation_empty_on_invalid_nodes.js b/browser/devtools/animationinspector/test/browser_animation_empty_on_invalid_nodes.js index 926bf12844df..f8d9be8e510e 100644 --- a/browser/devtools/animationinspector/test/browser_animation_empty_on_invalid_nodes.js +++ b/browser/devtools/animationinspector/test/browser_animation_empty_on_invalid_nodes.js @@ -8,17 +8,44 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {inspector, panel} = yield openAnimationInspector(); + let {inspector, panel} = yield openAnimationInspector(); + yield testEmptyPanel(inspector, panel); + + ({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI(); + yield testEmptyPanel(inspector, panel, true); +}); + +function* testEmptyPanel(inspector, panel, isNewUI=false) { info("Select node .still and check that the panel is empty"); let stillNode = yield getNodeFront(".still", inspector); + let onUpdated = panel.once(panel.UI_UPDATED_EVENT); yield selectNode(stillNode, inspector); - ok(!panel.playerWidgets || !panel.playerWidgets.length, - "No player widgets displayed for a still node"); + yield onUpdated; + + if (isNewUI) { + is(panel.animationsTimelineComponent.animations.length, 0, + "No animation players stored in the timeline component for a still node"); + is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0, + "No animation displayed in the timeline component for a still node"); + } else { + ok(!panel.playerWidgets || !panel.playerWidgets.length, + "No player widgets displayed for a still node"); + } info("Select the comment text node and check that the panel is empty"); let commentNode = yield inspector.walker.previousSibling(stillNode); + onUpdated = panel.once(panel.UI_UPDATED_EVENT); yield selectNode(commentNode, inspector); - ok(!panel.playerWidgets || !panel.playerWidgets.length, - "No player widgets displayed for a text node"); -}); + yield onUpdated; + + if (isNewUI) { + is(panel.animationsTimelineComponent.animations.length, 0, + "No animation players stored in the timeline component for a text node"); + is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0, + "No animation displayed in the timeline component for a text node"); + } else { + ok(!panel.playerWidgets || !panel.playerWidgets.length, + "No player widgets displayed for a text node"); + } +} diff --git a/browser/devtools/animationinspector/test/browser_animation_panel_exists.js b/browser/devtools/animationinspector/test/browser_animation_panel_exists.js index 9b8f2c2db2c1..ffd86c72c71e 100644 --- a/browser/devtools/animationinspector/test/browser_animation_panel_exists.js +++ b/browser/devtools/animationinspector/test/browser_animation_panel_exists.js @@ -15,4 +15,13 @@ add_task(function*() { ok(panel, "The animation panel exists"); ok(panel.playersEl, "The animation panel has been initialized"); + + ({panel, controller}) = yield closeAnimationInspectorAndRestartWithNewUI(); + + ok(controller, "The animation controller exists"); + ok(controller.animationsFront, "The animation controller has been initialized"); + + ok(panel, "The animation panel exists"); + ok(panel.playersEl, "The animation panel has been initialized"); + ok(panel.animationsTimelineComponent, "The animation panel has been initialized"); }); diff --git a/browser/devtools/animationinspector/test/browser_animation_participate_in_inspector_update.js b/browser/devtools/animationinspector/test/browser_animation_participate_in_inspector_update.js index c04c06102812..3297f88125ee 100644 --- a/browser/devtools/animationinspector/test/browser_animation_participate_in_inspector_update.js +++ b/browser/devtools/animationinspector/test/browser_animation_participate_in_inspector_update.js @@ -10,8 +10,15 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {inspector, panel, controller} = yield openAnimationInspector(); + let ui = yield openAnimationInspector(); + yield testEventsOrder(ui); + + ui = yield closeAnimationInspectorAndRestartWithNewUI(); + yield testEventsOrder(ui); +}); + +function* testEventsOrder({inspector, panel, controller}) { info("Listen for the players-updated, ui-updated and inspector-updated events"); let receivedEvents = []; controller.once(controller.PLAYERS_UPDATED_EVENT, () => { @@ -19,7 +26,7 @@ add_task(function*() { }); panel.once(panel.UI_UPDATED_EVENT, () => { receivedEvents.push(panel.UI_UPDATED_EVENT); - }) + }); inspector.once("inspector-updated", () => { receivedEvents.push("inspector-updated"); }); @@ -36,4 +43,4 @@ add_task(function*() { "The second event received was the ui-updated event"); is(receivedEvents[2], "inspector-updated", "The third event received was the inspector-updated event"); -}); +} diff --git a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js index 8bddf258d344..256c742c40dc 100644 --- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js +++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js @@ -9,7 +9,14 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_body_animation.html"); - let {panel} = yield openAnimationInspector(); - is(panel.playerWidgets.length, 1, "One animation player is displayed after init"); + let {panel} = yield openAnimationInspector(); + is(panel.playerWidgets.length, 1, + "One animation player is displayed after init"); + + ({panel}) = yield closeAnimationInspectorAndRestartWithNewUI(); + is(panel.animationsTimelineComponent.animations.length, 1, + "One animation is handled by the timeline after init"); + is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 1, + "One animation is displayed after init"); }); diff --git a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_target_nodes.js b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_target_nodes.js index 36d2b54880b0..7480247180f0 100644 --- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_target_nodes.js +++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_target_nodes.js @@ -27,5 +27,26 @@ add_task(function*() { "The target element's content is correct"); let selectorEl = targetEl.querySelector(".node-selector"); - ok(selectorEl, "The icon to select the target element in the inspector exists"); + ok(selectorEl, + "The icon to select the target element in the inspector exists"); + + info("Test again with the new timeline UI"); + ({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI(); + + info("Select the simple animated node"); + yield selectNode(".animated", inspector); + + let targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0]; + // Make sure to wait for the target-retrieved event if the nodeFront hasn't + // yet been retrieved by the TargetNodeComponent. + if (!targetNodeComponent.nodeFront) { + yield targetNodeComponent.once("target-retrieved"); + } + + is(targetNodeComponent.el.textContent, "div#.ball.animated", + "The target element's content is correct"); + + selectorEl = targetNodeComponent.el.querySelector(".node-selector"); + ok(selectorEl, + "The icon to select the target element in the inspector exists"); }); diff --git a/browser/devtools/animationinspector/test/browser_animation_refresh_on_added_animation.js b/browser/devtools/animationinspector/test/browser_animation_refresh_on_added_animation.js index db2765227075..bca5e6f9e171 100644 --- a/browser/devtools/animationinspector/test/browser_animation_refresh_on_added_animation.js +++ b/browser/devtools/animationinspector/test/browser_animation_refresh_on_added_animation.js @@ -8,13 +8,19 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {toolbox, inspector, panel} = yield openAnimationInspector(); + let {inspector, panel} = yield openAnimationInspector(); + yield testRefreshOnNewAnimation(inspector, panel); + + ({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI(); + yield testRefreshOnNewAnimation(inspector, panel); +}); + +function* testRefreshOnNewAnimation(inspector, panel) { info("Select a non animated node"); yield selectNode(".still", inspector); - is(panel.playersEl.querySelectorAll(".player-widget").length, 0, - "There are no player widgets in the panel"); + assertAnimationsDisplayed(panel, 0); info("Listen to the next UI update event"); let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); @@ -29,6 +35,14 @@ add_task(function*() { yield onPanelUpdated; ok(true, "The panel update event was fired"); - is(panel.playersEl.querySelectorAll(".player-widget").length, 1, - "There is one player widget in the panel"); -}); + assertAnimationsDisplayed(panel, 1); + + info("Remove the animation class on the node"); + onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); + yield executeInContent("devtools:test:setAttribute", { + selector: ".ball.animated", + attributeName: "class", + attributeValue: "ball still" + }); + yield onPanelUpdated; +} diff --git a/browser/devtools/animationinspector/test/browser_animation_refresh_on_removed_animation.js b/browser/devtools/animationinspector/test/browser_animation_refresh_on_removed_animation.js index 868c0472fcba..49586206b7c2 100644 --- a/browser/devtools/animationinspector/test/browser_animation_refresh_on_removed_animation.js +++ b/browser/devtools/animationinspector/test/browser_animation_refresh_on_removed_animation.js @@ -8,13 +8,21 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {toolbox, inspector, panel} = yield openAnimationInspector(); + let {inspector, panel} = yield openAnimationInspector(); + yield testRefreshOnRemove(inspector, panel); + yield testAddedAnimationWorks(inspector, panel); + + info("Reload and test again with the new UI"); + ({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI(true); + yield testRefreshOnRemove(inspector, panel, true); +}); + +function* testRefreshOnRemove(inspector, panel) { info("Select a animated node"); yield selectNode(".animated", inspector); - is(panel.playersEl.querySelectorAll(".player-widget").length, 1, - "There is one player widget in the panel"); + assertAnimationsDisplayed(panel, 1); info("Listen to the next UI update event"); let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); @@ -29,23 +37,24 @@ add_task(function*() { yield onPanelUpdated; ok(true, "The panel update event was fired"); - is(panel.playersEl.querySelectorAll(".player-widget").length, 0, - "There are no player widgets in the panel anymore"); + assertAnimationsDisplayed(panel, 0); info("Add an finite animation on the node again, and wait for it to appear"); onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); yield executeInContent("devtools:test:setAttribute", { selector: ".test-node", attributeName: "class", - attributeValue: "ball short" + attributeValue: "ball short test-node" }); yield onPanelUpdated; - is(panel.playersEl.querySelectorAll(".player-widget").length, 1, - "There is one player widget in the panel again"); + assertAnimationsDisplayed(panel, 1); +} + +function* testAddedAnimationWorks(inspector, panel) { info("Now wait until the animation finishes"); let widget = panel.playerWidgets[0]; - yield waitForPlayState(widget.player, "finished") + yield waitForPlayState(widget.player, "finished"); is(panel.playersEl.querySelectorAll(".player-widget").length, 1, "There is still a player widget in the panel after the animation finished"); @@ -59,4 +68,4 @@ add_task(function*() { EventUtils.synthesizeMouseAtCenter(input, {type: "mousedown"}, win); yield onPaused; ok(widget.el.classList.contains("paused"), "The widget is in paused mode"); -}); +} diff --git a/browser/devtools/animationinspector/test/browser_animation_refresh_when_active.js b/browser/devtools/animationinspector/test/browser_animation_refresh_when_active.js index dd203b16df08..567f15f2482d 100644 --- a/browser/devtools/animationinspector/test/browser_animation_refresh_when_active.js +++ b/browser/devtools/animationinspector/test/browser_animation_refresh_when_active.js @@ -8,8 +8,15 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {toolbox, inspector, panel} = yield openAnimationInspector(); + let {inspector, panel} = yield openAnimationInspector(); + yield testRefresh(inspector, panel); + + ({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI(); + yield testRefresh(inspector, panel); +}); + +function* testRefresh(inspector, panel) { info("Select a non animated node"); yield selectNode(".still", inspector); @@ -19,14 +26,14 @@ add_task(function*() { info("Select the animated node now"); yield selectNode(".animated", inspector); - ok(!panel.playerWidgets || !panel.playerWidgets.length, + assertAnimationsDisplayed(panel, 0, "The panel doesn't show the animation data while inactive"); info("Switch to the animation panel"); inspector.sidebar.select("animationinspector"); yield panel.once(panel.UI_UPDATED_EVENT); - is(panel.playerWidgets.length, 1, + assertAnimationsDisplayed(panel, 1, "The panel shows the animation data after selecting it"); info("Switch again to the rule-view"); @@ -35,13 +42,13 @@ add_task(function*() { info("Select the non animated node again"); yield selectNode(".still", inspector); - is(panel.playerWidgets.length, 1, + assertAnimationsDisplayed(panel, 1, "The panel still shows the previous animation data since it is inactive"); info("Switch to the animation panel again"); inspector.sidebar.select("animationinspector"); yield panel.once(panel.UI_UPDATED_EVENT); - ok(!panel.playerWidgets || !panel.playerWidgets.length, + assertAnimationsDisplayed(panel, 0, "The panel is now empty after refreshing"); -}); +} diff --git a/browser/devtools/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js b/browser/devtools/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js index 24a005796686..4a2453591901 100644 --- a/browser/devtools/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js +++ b/browser/devtools/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js @@ -22,4 +22,16 @@ add_task(function*() { is(widget.el.parentNode, panel.playersEl, "The player widget has been appended to the panel"); } + + info("Test again with the new UI, making sure the same number of " + + "animation timelines is created"); + ({inspector, panel, controller}) = yield closeAnimationInspectorAndRestartWithNewUI(); + let timeline = panel.animationsTimelineComponent; + + info("Selecting the test animated node again"); + yield selectNode(".multi", inspector); + + is(controller.animationPlayers.length, + timeline.animationsEl.querySelectorAll(".animation").length, + "As many timeline elements were created as there are playerFronts"); }); diff --git a/browser/devtools/animationinspector/test/browser_animation_shows_player_on_valid_node.js b/browser/devtools/animationinspector/test/browser_animation_shows_player_on_valid_node.js index 3227623d2e9e..f92a5a3857e4 100644 --- a/browser/devtools/animationinspector/test/browser_animation_shows_player_on_valid_node.js +++ b/browser/devtools/animationinspector/test/browser_animation_shows_player_on_valid_node.js @@ -9,12 +9,18 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {inspector, panel} = yield openAnimationInspector(); + let {inspector, panel} = yield openAnimationInspector(); + yield testShowsAnimations(inspector, panel); + + ({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI(); + yield testShowsAnimations(inspector, panel); +}); + +function* testShowsAnimations(inspector, panel) { info("Select node .animated and check that the panel is not empty"); let node = yield getNodeFront(".animated", inspector); yield selectNode(node, inspector); - is(panel.playerWidgets.length, 1, - "Exactly 1 player widget is shown for animated node"); -}); + assertAnimationsDisplayed(panel, 1); +} diff --git a/browser/devtools/animationinspector/test/browser_animation_target_highlight_select.js b/browser/devtools/animationinspector/test/browser_animation_target_highlight_select.js index 9660ea612bd6..0759496b3855 100644 --- a/browser/devtools/animationinspector/test/browser_animation_target_highlight_select.js +++ b/browser/devtools/animationinspector/test/browser_animation_target_highlight_select.js @@ -9,14 +9,26 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {toolbox, inspector, panel} = yield openAnimationInspector(); + let ui = yield openAnimationInspector(); + yield testTargetNode(ui); + + ui = yield closeAnimationInspectorAndRestartWithNewUI(); + yield testTargetNode(ui, true); +}); + +function* testTargetNode({toolbox, inspector, panel}, isNewUI) { info("Select the simple animated node"); yield selectNode(".animated", inspector); // Make sure to wait for the target-retrieved event if the nodeFront hasn't // yet been retrieved by the TargetNodeComponent. - let targetNodeComponent = panel.playerWidgets[0].targetNodeComponent; + let targetNodeComponent; + if (isNewUI) { + targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0]; + } else { + targetNodeComponent = panel.playerWidgets[0].targetNodeComponent; + } if (!targetNodeComponent.nodeFront) { yield targetNodeComponent.once("target-retrieved"); } @@ -33,21 +45,29 @@ add_task(function*() { ok(true, "The node-highlight event was fired"); is(targetNodeComponent.nodeFront, nodeFront, "The highlighted node is the one stored on the animation widget"); - is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName"); - is(nodeFront.attributes[0].name, "class", "The highlighted node has the correct attributes"); - is(nodeFront.attributes[0].value, "ball animated", "The highlighted node has the correct class"); + is(nodeFront.tagName, "DIV", + "The highlighted node has the correct tagName"); + is(nodeFront.attributes[0].name, "class", + "The highlighted node has the correct attributes"); + is(nodeFront.attributes[0].value, "ball animated", + "The highlighted node has the correct class"); info("Select the body node in order to have the list of all animations"); yield selectNode("body", inspector); // Make sure to wait for the target-retrieved event if the nodeFront hasn't // yet been retrieved by the TargetNodeComponent. - targetNodeComponent = panel.playerWidgets[0].targetNodeComponent; + if (isNewUI) { + targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0]; + } else { + targetNodeComponent = panel.playerWidgets[0].targetNodeComponent; + } if (!targetNodeComponent.nodeFront) { yield targetNodeComponent.once("target-retrieved"); } - info("Click on the first animation widget's selector icon and wait for the selection to change"); + info("Click on the first animation widget's selector icon and wait for the " + + "selection to change"); let onSelection = inspector.selection.once("new-node-front"); let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); let selectIconEl = targetNodeComponent.selectNodeEl; @@ -59,4 +79,4 @@ add_task(function*() { "The selected node is the one stored on the animation widget"); yield onPanelUpdated; -}); +} diff --git a/browser/devtools/animationinspector/test/browser_animation_toggle_button_toggles_animations.js b/browser/devtools/animationinspector/test/browser_animation_toggle_button_toggles_animations.js index 5707806bb9a3..d46afd7ebcac 100644 --- a/browser/devtools/animationinspector/test/browser_animation_toggle_button_toggles_animations.js +++ b/browser/devtools/animationinspector/test/browser_animation_toggle_button_toggles_animations.js @@ -11,7 +11,7 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {inspector, panel} = yield openAnimationInspector(); + let {panel} = yield openAnimationInspector(); info("Click the toggle button"); yield panel.toggleAll(); diff --git a/browser/devtools/animationinspector/test/browser_animation_toolbar_exists.js b/browser/devtools/animationinspector/test/browser_animation_toolbar_exists.js index fa51cfbe41ff..9257f322258b 100644 --- a/browser/devtools/animationinspector/test/browser_animation_toolbar_exists.js +++ b/browser/devtools/animationinspector/test/browser_animation_toolbar_exists.js @@ -9,7 +9,7 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {inspector, panel, window} = yield openAnimationInspector(); + let {inspector, window} = yield openAnimationInspector(); let doc = window.document; let toolbar = doc.querySelector("#toolbar"); diff --git a/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js b/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js index b6af2e4d737d..6e4bbb8dbe7d 100644 --- a/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js +++ b/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js @@ -9,36 +9,64 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {panel, inspector} = yield openAnimationInspector(); + let ui = yield openAnimationInspector(); + yield testDataUpdates(ui); + + info("Close the toolbox, reload the tab, and try again with the new UI"); + ui = yield closeAnimationInspectorAndRestartWithNewUI(true); + yield testDataUpdates(ui, true); +}); + +function* testDataUpdates({panel, controller, inspector}, isNewUI=false) { info("Select the test node"); yield selectNode(".animated", inspector); - info("Get the player widget"); - let widget = panel.playerWidgets[0]; + let animation = controller.animationPlayers[0]; + yield setStyle(animation, "animationDuration", "5.5s", isNewUI); + yield setStyle(animation, "animationIterationCount", "300", isNewUI); + yield setStyle(animation, "animationDelay", "45s", isNewUI); - yield setStyle(widget, "animationDuration", "5.5s"); - is(widget.metaDataComponent.durationValue.textContent, "5.50s", - "The widget shows the new duration"); + if (isNewUI) { + let animationsEl = panel.animationsTimelineComponent.animationsEl; + let timeBlockEl = animationsEl.querySelector(".time-block"); - yield setStyle(widget, "animationIterationCount", "300"); - is(widget.metaDataComponent.iterationValue.textContent, "300", - "The widget shows the new iteration count"); + // 45s delay + (300 * 5.5)s duration + let expectedTotalDuration = 1695 * 1000; + let timeRatio = expectedTotalDuration / timeBlockEl.offsetWidth; - yield setStyle(widget, "animationDelay", "45s"); - is(widget.metaDataComponent.delayValue.textContent, "45s", - "The widget shows the new delay"); -}); + // XXX: the nb and size of each iteration cannot be tested easily (displayed + // using a linear-gradient background and capped at 2px wide). They should + // be tested in bug 1173761. + let delayWidth = parseFloat(timeBlockEl.querySelector(".delay").style.width); + is(Math.round(delayWidth * timeRatio), 45 * 1000, + "The timeline has the right delay"); + } else { + let widget = panel.playerWidgets[0]; + is(widget.metaDataComponent.durationValue.textContent, "5.50s", + "The widget shows the new duration"); + is(widget.metaDataComponent.iterationValue.textContent, "300", + "The widget shows the new iteration count"); + is(widget.metaDataComponent.delayValue.textContent, "45s", + "The widget shows the new delay"); + } +} -function* setStyle(widget, name, value) { +function* setStyle(animation, name, value, isNewUI=false) { info("Change the animation style via the content DOM. Setting " + name + " to " + value); + + let onAnimationChanged = once(animation, "changed"); yield executeInContent("devtools:test:setStyle", { selector: ".animated", propertyName: name, propertyValue: value }); + yield onAnimationChanged; - info("Wait for the next state update"); - yield onceNextPlayerRefresh(widget.player); + // If this is the playerWidget-based UI, wait for the auto-refresh event too + // to make sure the UI has updated. + if (!isNewUI) { + yield once(animation, animation.AUTO_REFRESH_EVENT); + } } diff --git a/browser/devtools/animationinspector/test/head.js b/browser/devtools/animationinspector/test/head.js index d8b39c959db3..fdc1c0e20d82 100644 --- a/browser/devtools/animationinspector/test/head.js +++ b/browser/devtools/animationinspector/test/head.js @@ -9,7 +9,7 @@ const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const TargetFactory = devtools.TargetFactory; -const {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {}); +const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); const {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {}); // All tests are asynchronous @@ -19,17 +19,20 @@ const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinsp const ROOT_TEST_DIR = getRootDirectory(gTestPath); const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js"; const COMMON_FRAME_SCRIPT_URL = "chrome://browser/content/devtools/frame-script-utils.js"; +const NEW_UI_PREF = "devtools.inspector.animationInspectorV3"; // Auto clean-up when a test ends registerCleanupFunction(function*() { - let target = TargetFactory.forTab(gBrowser.selectedTab); - yield gDevTools.closeToolbox(target); + yield closeAnimationInspector(); while (gBrowser.tabs.length > 1) { gBrowser.removeCurrentTab(); } }); +// Make sure the new UI is off by default. +Services.prefs.setBoolPref(NEW_UI_PREF, false); + // Uncomment this pref to dump all devtools emitted events to the console. // Services.prefs.setBoolPref("devtools.dump.emit", true); @@ -45,6 +48,7 @@ registerCleanupFunction(() => gDevTools.testing = false); registerCleanupFunction(() => { Services.prefs.clearUserPref("devtools.dump.emit"); Services.prefs.clearUserPref("devtools.debugger.log"); + Services.prefs.clearUserPref(NEW_UI_PREF); }); /** @@ -77,6 +81,13 @@ function addTab(url) { return def.promise; } +/** + * Switch ON the new UI pref. + */ +function enableNewUI() { + Services.prefs.setBoolPref(NEW_UI_PREF, true); +} + /** * Reload the current tab location. */ @@ -119,6 +130,25 @@ let selectNode = Task.async(function*(data, inspector, reason="test") { yield updated; }); +/** + * Check if there are the expected number of animations being displayed in the + * panel right now. + * @param {AnimationsPanel} panel + * @param {Number} nbAnimations The expected number of animations. + * @param {String} msg An optional string to be used as the assertion message. + */ +function assertAnimationsDisplayed(panel, nbAnimations, msg="") { + let isNewUI = Services.prefs.getBoolPref(NEW_UI_PREF); + msg = msg || `There are ${nbAnimations} animations in the panel`; + if (isNewUI) { + is(panel.animationsTimelineComponent.animationsEl.childNodes.length, + nbAnimations, msg); + } else { + is(panel.playersEl.querySelectorAll(".player-widget").length, + nbAnimations, msg); + } +} + /** * Takes an Inspector panel that was just created, and waits * for a "inspector-updated" event as well as the animation inspector @@ -131,10 +161,9 @@ let waitForAnimationInspectorReady = Task.async(function*(inspector) { let win = inspector.sidebar.getWindowForTab("animationinspector"); let updated = inspector.once("inspector-updated"); - // In e10s, if we wait for underlying toolbox actors to - // load (by setting gDevTools.testing to true), we miss the "animationinspector-ready" - // event on the sidebar, so check to see if the iframe - // is already loaded. + // In e10s, if we wait for underlying toolbox actors to load (by setting + // gDevTools.testing to true), we miss the "animationinspector-ready" event on + // the sidebar, so check to see if the iframe is already loaded. let tabReady = win.document.readyState === "complete" ? promise.resolve() : inspector.sidebar.once("animationinspector-ready"); @@ -145,7 +174,7 @@ let waitForAnimationInspectorReady = Task.async(function*(inspector) { /** * Open the toolbox, with the inspector tool visible and the animationinspector * sidebar selected. - * @return a promise that resolves when the inspector is ready + * @return a promise that resolves when the inspector is ready. */ let openAnimationInspector = Task.async(function*() { let target = TargetFactory.forTab(gBrowser.selectedTab); @@ -185,6 +214,35 @@ let openAnimationInspector = Task.async(function*() { }; }); +/** + * Close the toolbox. + * @return a promise that resolves when the toolbox has closed. + */ +let closeAnimationInspector = Task.async(function*() { + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); +}); + +/** + * During the time period we migrate from the playerWidgets-based UI to the new + * AnimationTimeline UI, we'll want to run certain tests against both UI. + * This closes the toolbox, switch the new UI pref ON, and opens the toolbox + * again, with the animation inspector panel selected. + * @param {Boolean} reload Optionally reload the page after the toolbox was + * closed and before it is opened again. + * @return a promise that resolves when the animation inspector is ready. + */ +let closeAnimationInspectorAndRestartWithNewUI = Task.async(function*(reload) { + info("Close the toolbox and test again with the new UI"); + yield closeAnimationInspector(); + if (reload) { + yield reloadTab(); + } + enableNewUI(); + return yield openAnimationInspector(); +}); + + /** * Wait for the toolbox frame to receive focus after it loads * @param {Toolbox} toolbox @@ -214,7 +272,7 @@ function hasSideBarTab(inspector, id) { * @param {Object} target An observable object that either supports on/off or * addEventListener/removeEventListener * @param {String} eventName - * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener + * @param {Boolean} useCapture Optional, for add/removeEventListener * @return A promise that resolves when the event has been handled */ function once(target, eventName, useCapture=false) { @@ -278,9 +336,9 @@ function executeInContent(name, data={}, objects={}, expectResponse=true) { mm.sendAsyncMessage(name, data, objects); if (expectResponse) { return waitForContentMessage(name); - } else { - return promise.resolve(); } + + return promise.resolve(); } function onceNextPlayerRefresh(player) { @@ -293,7 +351,9 @@ function onceNextPlayerRefresh(player) { * Simulate a click on the playPause button of a playerWidget. */ let togglePlayPauseButton = Task.async(function*(widget) { - let nextState = widget.player.state.playState === "running" ? "paused" : "running"; + let nextState = widget.player.state.playState === "running" + ? "paused" + : "running"; // Note that instead of simulating a real event here, the callback is just // called. This is better because the callback returns a promise, so we know @@ -344,7 +404,8 @@ let waitForStateCondition = Task.async(function*(player, conditionCheck, desc="" * provided string. * @param {AnimationPlayerFront} player * @param {String} playState The playState to expect. - * @return {Promise} Resolves when the playState has changed to the expected value. + * @return {Promise} Resolves when the playState has changed to the expected + * value. */ function waitForPlayState(player, playState) { return waitForStateCondition(player, state => { diff --git a/browser/devtools/animationinspector/utils.js b/browser/devtools/animationinspector/utils.js new file mode 100644 index 000000000000..137745a45aa6 --- /dev/null +++ b/browser/devtools/animationinspector/utils.js @@ -0,0 +1,135 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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"; + +// How many times, maximum, can we loop before we find the optimal time +// interval in the timeline graph. +const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100; +// Background time graduations should be multiple of this number of millis. +const TIME_INTERVAL_MULTIPLE = 10; +const TIME_INTERVAL_SCALES = 3; +// The default minimum spacing between time graduations in px. +const TIME_GRADUATION_MIN_SPACING = 10; +// RGB color for the time interval background. +const TIME_INTERVAL_COLOR = [128, 136, 144]; +const TIME_INTERVAL_OPACITY_MIN = 32; // byte +const TIME_INTERVAL_OPACITY_ADD = 32; // byte + +/** + * DOM node creation helper function. + * @param {Object} Options to customize the node to be created. + * - nodeType {String} Optional, defaults to "div", + * - attributes {Object} Optional attributes object like + * {attrName1:value1, attrName2: value2, ...} + * - parent {DOMNode} Mandatory node to append the newly created node to. + * - textContent {String} Optional text for the node. + * @return {DOMNode} The newly created node. + */ +function createNode(options) { + if (!options.parent) { + throw new Error("Missing parent DOMNode to create new node"); + } + + let type = options.nodeType || "div"; + let node = options.parent.ownerDocument.createElement(type); + + for (let name in options.attributes || {}) { + let value = options.attributes[name]; + node.setAttribute(name, value); + } + + if (options.textContent) { + node.textContent = options.textContent; + } + + options.parent.appendChild(node); + return node; +} + +exports.createNode = createNode; + +/** + * Given a data-scale, draw the background for a graph (vertical lines) into a + * canvas and set that canvas as an image-element with an ID that can be used + * from CSS. + * @param {Document} document The document where the image-element should be set. + * @param {String} id The ID for the image-element. + * @param {Number} graphWidth The width of the graph. + * @param {Number} timeScale How many px is 1ms in the graph. + */ +function drawGraphElementBackground(document, id, graphWidth, timeScale) { + let canvas = document.createElement("canvas"); + let ctx = canvas.getContext("2d"); + + // Set the canvas width (as requested) and height (1px, repeated along the Y + // axis). + canvas.width = graphWidth; + canvas.height = 1; + + // Create the image data array which will receive the pixels. + let imageData = ctx.createImageData(canvas.width, canvas.height); + 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] = TIME_INTERVAL_COLOR; + let alphaComponent = TIME_INTERVAL_OPACITY_MIN; + let interval = findOptimalTimeInterval(timeScale); + + // Insert one pixel for each division on each scale. + for (let i = 1; i <= TIME_INTERVAL_SCALES; i++) { + let increment = interval * Math.pow(2, i); + for (let x = 0; x < canvas.width; x += increment) { + let position = x | 0; + view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r; + } + alphaComponent += TIME_INTERVAL_OPACITY_ADD; + } + + // Flush the image data and cache the waterfall background. + pixelArray.set(view8bit); + ctx.putImageData(imageData, 0, 0); + document.mozSetImageElement(id, canvas); +} + +exports.drawGraphElementBackground = drawGraphElementBackground; + +/** + * Find the optimal interval between time graduations in the animation timeline + * graph based on a time scale and a minimum spacing. + * @param {Number} timeScale How many px is 1ms in the graph. + * @param {Number} minSpacing The minimum spacing between 2 graduations, + * defaults to TIME_GRADUATION_MIN_SPACING. + * @return {Number} The optional interval, in pixels. + */ +function findOptimalTimeInterval(timeScale, + minSpacing=TIME_GRADUATION_MIN_SPACING) { + let timingStep = TIME_INTERVAL_MULTIPLE; + let maxIters = OPTIMAL_TIME_INTERVAL_MAX_ITERS; + let numIters = 0; + + if (timeScale > minSpacing) { + return timeScale; + } + + while (true) { + let scaledStep = timeScale * timingStep; + if (++numIters > maxIters) { + return scaledStep; + } + if (scaledStep < minSpacing) { + timingStep *= 2; + continue; + } + return scaledStep; + } +} + +exports.findOptimalTimeInterval = findOptimalTimeInterval; diff --git a/browser/locales/en-US/chrome/browser/devtools/animationinspector.properties b/browser/locales/en-US/chrome/browser/devtools/animationinspector.properties index c878e6ac696c..6db75d2cbf2d 100644 --- a/browser/locales/en-US/chrome/browser/devtools/animationinspector.properties +++ b/browser/locales/en-US/chrome/browser/devtools/animationinspector.properties @@ -52,3 +52,9 @@ player.timeLabel=%Ss # drop-down list items that can be used to change the rate at which the # animation runs (1x being the default, 2x being twice as fast). player.playbackRateLabel=%Sx + +# LOCALIZATION NOTE (timeline.timeGraduationLabel): +# This string is displayed at the top of the animation panel, next to each time +# graduation, to indicate what duration (in milliseconds) this graduation +# corresponds to. +timeline.timeGraduationLabel=%Sms diff --git a/browser/themes/shared/devtools/animationinspector.css b/browser/themes/shared/devtools/animationinspector.css index 6cc0bdbdb17e..4764458f7dd3 100644 --- a/browser/themes/shared/devtools/animationinspector.css +++ b/browser/themes/shared/devtools/animationinspector.css @@ -1,3 +1,17 @@ +/* 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/. */ + +/* Animation-inspector specific theme variables */ + +.theme-dark { + --even-animation-timeline-background-color: rgba(255,255,255,0.03); +} + +.theme-light { + --even-animation-timeline-background-color: rgba(128,128,128,0.03); +} + html { height: 100%; } @@ -32,6 +46,13 @@ body { min-height: 20px; } +/* The main animations container */ + +#players { + height: calc(100% - 20px); + overflow: auto; +} + /* The error message, shown when an invalid/unanimated element is selected */ #error-message { @@ -44,12 +65,6 @@ body { display: none; } -/* The animation players container */ - -#players { - flex: 1; - overflow: auto; -} /* Element picker and toggle-all buttons */ @@ -99,6 +114,156 @@ body { } } +/* Animation timeline component */ + +.animation-timeline { + height: 100%; + overflow: hidden; + /* The timeline gets its background-image from a canvas element created in + /browser/devtools/animationinspector/utils.js drawGraphElementBackground + thanks to document.mozSetImageElement("time-graduations", canvas) + This is done so that the background can be built dynamically from script */ + background-image: -moz-element(#time-graduations); + background-repeat: repeat-y; + /* The animations are drawn 150px from the left edge so that animated nodes + can be displayed in a sidebar */ + background-position: 150px 0; + display: flex; + flex-direction: column; +} + +.animation-timeline .time-header { + margin-left: 150px; + height: 20px; + overflow: hidden; + position: relative; + border-bottom: 1px solid var(--theme-splitter-color); +} + +.animation-timeline .time-header .time-tick { + position: absolute; + top: 3px; +} + +.animation-timeline .animations { + width: 100%; + overflow-y: auto; + overflow-x: hidden; + margin: 0; + padding: 0; + list-style-type: none; +} + +/* Animation block widgets */ + +.animation-timeline .animation { + margin: 4px 0; + height: 20px; + position: relative; +} + +.animation-timeline .animation:nth-child(2n) { + background-color: var(--even-animation-timeline-background-color); +} + +.animation-timeline .animation .target { + width: 150px; + overflow: hidden; + height: 100%; +} + +.animation-timeline .animation-target { + background-color: transparent; +} + +.animation-timeline .animation .time-block { + position: absolute; + top: 0; + left: 150px; + right: 0; + height: 100%; +} + +/* Animation iterations */ + +.animation-timeline .animation .iterations { + position: relative; + height: 100%; + border: 1px solid var(--theme-highlight-lightorange); + box-sizing: border-box; + background: var(--theme-contrast-background); + /* Iterations are displayed with a repeating linear-gradient which size is + dynamically changed from JS */ + background-image: + linear-gradient(to right, + var(--theme-highlight-lightorange) 0, + var(--theme-highlight-lightorange) 1px, + transparent 1px, + transparent 2px); + background-repeat: repeat-x; + background-position: -1px 0; +} + +.animation-timeline .animation .iterations.infinite { + border-right-width: 0; +} + +.animation-timeline .animation .iterations.infinite::before, +.animation-timeline .animation .iterations.infinite::after { + content: ""; + position: absolute; + top: 0; + right: 0; + width: 0; + height: 0; + border-right: 4px solid var(--theme-body-background); + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; +} + +.animation-timeline .animation .iterations.infinite::after { + bottom: 0; + top: unset; +} + +.animation-timeline .animation .animation-title { + height: 1.5em; + width: 100%; + box-sizing: border-box; + overflow: hidden; +} + +.animation-timeline .animation .delay { + position: absolute; + top: 0; + height: 100%; + background-image: linear-gradient(to bottom, + transparent, + transparent 9px, + var(--theme-highlight-lightorange) 9px, + var(--theme-highlight-lightorange) 11px, + transparent 11px, + transparent); +} + +.animation-timeline .animation .delay::before { + position: absolute; + content: ""; + left: 0; + width: 2px; + height: 8px; + top: 50%; + margin-top: -4px; + background: var(--theme-highlight-lightorange); +} + +.animation-timeline .animation .name { + position: absolute; + z-index: 1; + padding: 2px; + white-space: nowrap; +} + /* Animation target node gutter, contains a preview of the dom node */ .animation-target { @@ -253,4 +418,4 @@ body { width: 50px; border-left: 1px solid var(--theme-splitter-color); background: var(--theme-toolbar-background); -} +} \ No newline at end of file diff --git a/toolkit/devtools/server/actors/animation.js b/toolkit/devtools/server/actors/animation.js index 121df2e46607..71b16ca323be 100644 --- a/toolkit/devtools/server/actors/animation.js +++ b/toolkit/devtools/server/actors/animation.js @@ -5,7 +5,8 @@ "use strict"; /** - * Set of actors that expose the Web Animations API to devtools protocol clients. + * Set of actors that expose the Web Animations API to devtools protocol + * clients. * * The |Animations| actor is the main entry point. It is used to discover * animation players on given nodes. @@ -29,11 +30,15 @@ const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); const {setInterval, clearInterval} = require("sdk/timers"); const protocol = require("devtools/server/protocol"); -const {ActorClass, Actor, FrontClass, Front, Arg, method, RetVal, types} = protocol; +const {ActorClass, Actor, FrontClass, Front, + Arg, method, RetVal, types} = protocol; +// Make sure the nodeActor type is know here. const {NodeActor} = require("devtools/server/actors/inspector"); const events = require("sdk/event/core"); -const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500; // ms +// How long (in ms) should we wait before polling again the state of an +// animationPlayer. +const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500; /** * The AnimationPlayerActor provides information about a given animation: its @@ -47,6 +52,13 @@ const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500; // ms let AnimationPlayerActor = ActorClass({ typeName: "animationplayer", + events: { + "changed": { + type: "changed", + state: Arg(0, "json") + } + }, + /** * @param {AnimationsActor} The main AnimationsActor instance * @param {AnimationPlayer} The player object returned by getAnimationPlayers @@ -58,14 +70,29 @@ let AnimationPlayerActor = ActorClass({ initialize: function(animationsActor, player, playerIndex) { Actor.prototype.initialize.call(this, animationsActor.conn); + this.onAnimationMutation = this.onAnimationMutation.bind(this); + + this.tabActor = animationsActor.tabActor; this.player = player; this.node = player.effect.target; this.playerIndex = playerIndex; - this.styles = this.node.ownerDocument.defaultView.getComputedStyle(this.node); + + let win = this.node.ownerDocument.defaultView; + this.styles = win.getComputedStyle(this.node); + + // Listen to animation mutations on the node to alert the front when the + // current animation changes. + this.observer = new win.MutationObserver(this.onAnimationMutation); + this.observer.observe(this.node, {animations: true}); }, destroy: function() { - this.player = this.node = this.styles = null; + // Only try to disconnect the observer if it's not already dead (i.e. if the + // container view hasn't navigated since). + if (this.observer && !Cu.isDeadWrapper(this.observer)) { + this.observer.disconnect(); + } + this.tabActor = this.player = this.node = this.styles = this.observer = null; Actor.prototype.destroy.call(this); }, @@ -95,14 +122,14 @@ let AnimationPlayerActor = ActorClass({ */ getPlayerIndex: function() { let names = this.styles.animationName; + if (names === "none") { + names = this.styles.transitionProperty; + } - // If no names are found, then it's probably a transition, in which case we - // can't find the actual index, so just trust the playerIndex passed by - // the AnimationsActor at initialization time. - // Note that this may be incorrect if by the time the AnimationPlayerActor - // is initialized, one of the transitions has ended, but it's the best we - // can do for now. - if (!names) { + // If we still don't have a name, let's fall back to the provided index + // which may, by now, be wrong, but it's the best we can do until the waapi + // gives us a way to get duration, delay, ... directly. + if (!names || names === "none") { return this.playerIndex; } @@ -114,7 +141,7 @@ let AnimationPlayerActor = ActorClass({ // If there are several names, retrieve the index of the animation name in // the list. names = names.split(",").map(n => n.trim()); - for (let i = 0; i < names.length; i ++) { + for (let i = 0; i < names.length; i++) { if (names[i] === this.player.effect.name) { return i; } @@ -244,6 +271,27 @@ let AnimationPlayerActor = ActorClass({ } }), + /** + * Executed when the current animation changes, used to emit the new state + * the the front. + */ + onAnimationMutation: function(mutations) { + let hasChanged = false; + for (let {changedAnimations} of mutations) { + if (!changedAnimations.length) { + return; + } + if (changedAnimations.some(animation => animation === this.player)) { + hasChanged = true; + break; + } + } + + if (hasChanged) { + events.emit(this, "changed", this.getCurrentState()); + } + }, + /** * Pause the player. */ @@ -348,9 +396,18 @@ let AnimationPlayerFront = FrontClass(AnimationPlayerActor, { delay: this._form.delay, iterationCount: this._form.iterationCount, isRunningOnCompositor: this._form.isRunningOnCompositor - } + }; }, + /** + * Executed when the AnimationPlayerActor emits a "changed" event. Used to + * update the local knowledge of the state. + */ + onChanged: protocol.preEvent("changed", function(partialState) { + let {state} = this.reconstructState(partialState); + this.state = state; + }), + // About auto-refresh: // // The AnimationPlayerFront is capable of automatically refreshing its state @@ -416,19 +473,28 @@ let AnimationPlayerFront = FrontClass(AnimationPlayerActor, { */ getCurrentState: protocol.custom(function() { this.currentStateHasChanged = false; - return this._getCurrentState().then(data => { - for (let key in this.state) { - if (typeof data[key] === "undefined") { - data[key] = this.state[key]; - } else if (data[key] !== this.state[key]) { - this.currentStateHasChanged = true; - } - } - return data; + return this._getCurrentState().then(partialData => { + let {state, hasChanged} = this.reconstructState(partialData); + this.currentStateHasChanged = hasChanged; + return state; }); }, { impl: "_getCurrentState" }), + + reconstructState: function(data) { + let hasChanged = false; + + for (let key in this.state) { + if (typeof data[key] === "undefined") { + data[key] = this.state[key]; + } else if (data[key] !== this.state[key]) { + hasChanged = true; + } + } + + return {state: data, hasChanged}; + } }); /** @@ -449,7 +515,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({ typeName: "animations", events: { - "mutations" : { + "mutations": { type: "mutations", changes: Arg(0, "array:animationMutationChange") } @@ -500,7 +566,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({ // No care is taken here to destroy the previously stored actors because it // is assumed that the client is responsible for lifetimes of actors. this.actors = []; - for (let i = 0; i < animations.length; i ++) { + for (let i = 0; i < animations.length; i++) { // XXX: for now the index is passed along as the AnimationPlayerActor uses // it to retrieve animation information from CSS. let actor = AnimationPlayerActor(this, animations[i], i); @@ -532,7 +598,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({ onAnimationMutation: function(mutations) { let eventData = []; - for (let {addedAnimations, changedAnimations, removedAnimations} of mutations) { + for (let {addedAnimations, removedAnimations} of mutations) { for (let player of removedAnimations) { // Note that animations are reported as removed either when they are // actually removed from the node (e.g. css class removed) or when they @@ -588,9 +654,9 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({ }, /** - * After the client has called getAnimationPlayersForNode for a given DOM node, - * the actor starts sending animation mutations for this node. If the client - * doesn't want this to happen anymore, it should call this method. + * After the client has called getAnimationPlayersForNode for a given DOM + * node, the actor starts sending animation mutations for this node. If the + * client doesn't want this to happen anymore, it should call this method. */ stopAnimationPlayerUpdates: method(function() { if (this.observer && !Cu.isDeadWrapper(this.observer)) { @@ -666,7 +732,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({ /** * Play all animations in the current tabActor's frames. - * This method only returns when the animations have left their pending states. + * This method only returns when animations have left their pending states. */ playAll: method(function() { let readyPromises = []; @@ -687,9 +753,8 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({ toggleAll: method(function() { if (this.allAnimationsPaused) { return this.playAll(); - } else { - return this.pauseAll(); } + return this.pauseAll(); }, { request: {}, response: {} From 66995680f39091b3506c69f7da518ed484814ba1 Mon Sep 17 00:00:00 2001 From: Patrick Brosset Date: Mon, 15 Jun 2015 12:03:54 +0200 Subject: [PATCH 09/16] Bug 1155663 - More tests for the timeline-based animation inspector UI; r=bgrins --HG-- rename : browser/devtools/animationinspector/test/browser_animation_timeline_waits_for_delay.js => browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_delayed.js rename : browser/devtools/animationinspector/test/browser_animation_timeline_is_enabled.js => browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_enabled.js rename : browser/devtools/animationinspector/test/browser_animation_timeline_animates.js => browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_moves.js extra : rebase_source : 0dc84ca300bc1180998defd99664ae2ef29032fb --- .../devtools/animationinspector/components.js | 3 + browser/devtools/animationinspector/moz.build | 1 + .../animationinspector/test/browser.ini | 11 +- ...imation_playerWidgets_scrubber_delayed.js} | 0 ...imation_playerWidgets_scrubber_enabled.js} | 0 ...animation_playerWidgets_scrubber_moves.js} | 0 ...r_animation_timeline_displays_with_pref.js | 24 +++ .../test/browser_animation_timeline_header.js | 47 +++++ .../browser_animation_timeline_shows_delay.js | 30 +++ ...ser_animation_timeline_shows_iterations.js | 51 +++++ .../test/browser_animation_timeline_ui.js | 41 ++++ .../devtools/animationinspector/test/head.js | 11 + .../animationinspector/test/unit/.eslintrc | 4 + .../test/unit/test_findOptimalTimeInterval.js | 85 ++++++++ .../test/unit/test_timeScale.js | 191 ++++++++++++++++++ .../animationinspector/test/unit/xpcshell.ini | 9 + browser/devtools/animationinspector/utils.js | 5 +- 17 files changed, 507 insertions(+), 6 deletions(-) rename browser/devtools/animationinspector/test/{browser_animation_timeline_waits_for_delay.js => browser_animation_playerWidgets_scrubber_delayed.js} (100%) rename browser/devtools/animationinspector/test/{browser_animation_timeline_is_enabled.js => browser_animation_playerWidgets_scrubber_enabled.js} (100%) rename browser/devtools/animationinspector/test/{browser_animation_timeline_animates.js => browser_animation_playerWidgets_scrubber_moves.js} (100%) create mode 100644 browser/devtools/animationinspector/test/browser_animation_timeline_displays_with_pref.js create mode 100644 browser/devtools/animationinspector/test/browser_animation_timeline_header.js create mode 100644 browser/devtools/animationinspector/test/browser_animation_timeline_shows_delay.js create mode 100644 browser/devtools/animationinspector/test/browser_animation_timeline_shows_iterations.js create mode 100644 browser/devtools/animationinspector/test/browser_animation_timeline_ui.js create mode 100644 browser/devtools/animationinspector/test/unit/.eslintrc create mode 100644 browser/devtools/animationinspector/test/unit/test_findOptimalTimeInterval.js create mode 100644 browser/devtools/animationinspector/test/unit/test_timeScale.js create mode 100644 browser/devtools/animationinspector/test/unit/xpcshell.ini diff --git a/browser/devtools/animationinspector/components.js b/browser/devtools/animationinspector/components.js index f2c155a6744e..048c65f06e8a 100644 --- a/browser/devtools/animationinspector/components.js +++ b/browser/devtools/animationinspector/components.js @@ -22,6 +22,7 @@ const {Cu} = require("chrome"); Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); +const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); const { createNode, drawGraphElementBackground, @@ -619,6 +620,8 @@ let TimeScale = { } }; +exports.TimeScale = TimeScale; + /** * UI component responsible for displaying a timeline for animations. * The timeline is essentially a graph with time along the x axis and animations diff --git a/browser/devtools/animationinspector/moz.build b/browser/devtools/animationinspector/moz.build index b69b5c83d074..d22e8eb60ce3 100644 --- a/browser/devtools/animationinspector/moz.build +++ b/browser/devtools/animationinspector/moz.build @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] +XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] EXTRA_JS_MODULES.devtools.animationinspector += [ 'components.js', diff --git a/browser/devtools/animationinspector/test/browser.ini b/browser/devtools/animationinspector/test/browser.ini index 0d83896b0ec4..3dc870489274 100644 --- a/browser/devtools/animationinspector/test/browser.ini +++ b/browser/devtools/animationinspector/test/browser.ini @@ -19,6 +19,9 @@ support-files = [browser_animation_playerWidgets_dont_show_time_after_duration.js] [browser_animation_playerWidgets_have_control_buttons.js] [browser_animation_playerWidgets_meta_data.js] +[browser_animation_playerWidgets_scrubber_delayed.js] +[browser_animation_playerWidgets_scrubber_enabled.js] +[browser_animation_playerWidgets_scrubber_moves.js] [browser_animation_playerWidgets_state_after_pause.js] [browser_animation_playerWidgets_target_nodes.js] [browser_animation_rate_select_shows_presets.js] @@ -30,9 +33,11 @@ support-files = [browser_animation_setting_playbackRate_works.js] [browser_animation_shows_player_on_valid_node.js] [browser_animation_target_highlight_select.js] -[browser_animation_timeline_animates.js] -[browser_animation_timeline_is_enabled.js] -[browser_animation_timeline_waits_for_delay.js] +[browser_animation_timeline_displays_with_pref.js] +[browser_animation_timeline_header.js] +[browser_animation_timeline_shows_delay.js] +[browser_animation_timeline_shows_iterations.js] +[browser_animation_timeline_ui.js] [browser_animation_toggle_button_resets_on_navigate.js] [browser_animation_toggle_button_toggles_animations.js] [browser_animation_toggle_button_updates_playerWidgets.js] diff --git a/browser/devtools/animationinspector/test/browser_animation_timeline_waits_for_delay.js b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_delayed.js similarity index 100% rename from browser/devtools/animationinspector/test/browser_animation_timeline_waits_for_delay.js rename to browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_delayed.js diff --git a/browser/devtools/animationinspector/test/browser_animation_timeline_is_enabled.js b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_enabled.js similarity index 100% rename from browser/devtools/animationinspector/test/browser_animation_timeline_is_enabled.js rename to browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_enabled.js diff --git a/browser/devtools/animationinspector/test/browser_animation_timeline_animates.js b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_moves.js similarity index 100% rename from browser/devtools/animationinspector/test/browser_animation_timeline_animates.js rename to browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_moves.js diff --git a/browser/devtools/animationinspector/test/browser_animation_timeline_displays_with_pref.js b/browser/devtools/animationinspector/test/browser_animation_timeline_displays_with_pref.js new file mode 100644 index 000000000000..673e8e7a4e22 --- /dev/null +++ b/browser/devtools/animationinspector/test/browser_animation_timeline_displays_with_pref.js @@ -0,0 +1,24 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the timeline-based UI is displayed instead of the playerwidget- +// based UI when the "devtools.inspector.animationInspectorV3" is set. + +add_task(function*() { + yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspectorNewUI(); + + info("Selecting the test node"); + yield selectNode(".animated", inspector); + + let timeline = panel.animationsTimelineComponent; + + ok(timeline, "The timeline components was created"); + is(timeline.rootWrapperEl.parentNode, panel.playersEl, + "The timeline component was appended in the DOM"); + is(panel.playersEl.querySelectorAll(".player-widget").length, 0, + "There are no playerWidgets in the DOM"); +}); diff --git a/browser/devtools/animationinspector/test/browser_animation_timeline_header.js b/browser/devtools/animationinspector/test/browser_animation_timeline_header.js new file mode 100644 index 000000000000..8277440b2777 --- /dev/null +++ b/browser/devtools/animationinspector/test/browser_animation_timeline_header.js @@ -0,0 +1,47 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the timeline-based UI shows correct time graduations in the +// header. + +const {findOptimalTimeInterval} = require("devtools/animationinspector/utils"); +const {TimeScale} = require("devtools/animationinspector/components"); +// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in components.js +const TIME_GRADUATION_MIN_SPACING = 40; + +add_task(function*() { + yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); + let {panel} = yield openAnimationInspectorNewUI(); + + let timeline = panel.animationsTimelineComponent; + let headerEl = timeline.timeHeaderEl; + + info("Find out how many time graduations should there be"); + let width = headerEl.offsetWidth; + let scale = width / (TimeScale.maxEndTime - TimeScale.minStartTime); + // Note that findOptimalTimeInterval is tested separately in xpcshell test + // test_findOptimalTimeInterval.js, so we assume that it works here. + let interval = findOptimalTimeInterval(scale, TIME_GRADUATION_MIN_SPACING); + let nb = Math.ceil(width / interval); + + is(headerEl.querySelectorAll(".time-tick").length, nb, + "The expected number of time ticks were found"); + + info("Make sure graduations are evenly distributed and show the right times"); + [...headerEl.querySelectorAll(".time-tick")].forEach((tick, i) => { + let left = parseFloat(tick.style.left); + is(Math.round(left), Math.round(i * interval), + "Graduation " + i + " is positioned correctly"); + + // Note that the distancetoRelativeTime and formatTime functions are tested + // separately in xpcshell test test_timeScale.js, so we assume that they + // work here. + let formattedTime = TimeScale.formatTime( + TimeScale.distanceToRelativeTime(i * interval, width)); + is(tick.textContent, formattedTime, + "Graduation " + i + " has the right text content"); + }); +}); diff --git a/browser/devtools/animationinspector/test/browser_animation_timeline_shows_delay.js b/browser/devtools/animationinspector/test/browser_animation_timeline_shows_delay.js new file mode 100644 index 000000000000..51d0a882c0d4 --- /dev/null +++ b/browser/devtools/animationinspector/test/browser_animation_timeline_shows_delay.js @@ -0,0 +1,30 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that animation delay is visualized in the timeline-based UI when the +// animation is delayed. + +add_task(function*() { + yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspectorNewUI(); + + info("Selecting a delayed animated node"); + yield selectNode(".delayed", inspector); + + info("Getting the animation and delay elements from the panel"); + let timelineEl = panel.animationsTimelineComponent.rootWrapperEl; + let delay = timelineEl.querySelector(".delay"); + + ok(delay, "The animation timeline contains the delay element"); + + info("Selecting a no-delay animated node"); + yield selectNode(".animated", inspector); + + info("Getting the animation and delay elements from the panel again"); + delay = timelineEl.querySelector(".delay"); + + ok(!delay, "The animation timeline contains no delay element"); +}); diff --git a/browser/devtools/animationinspector/test/browser_animation_timeline_shows_iterations.js b/browser/devtools/animationinspector/test/browser_animation_timeline_shows_iterations.js new file mode 100644 index 000000000000..0b47bb808dda --- /dev/null +++ b/browser/devtools/animationinspector/test/browser_animation_timeline_shows_iterations.js @@ -0,0 +1,51 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the timeline-based UI is displays as many iteration elements as +// there are iterations in an animation. + +add_task(function*() { + yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspectorNewUI(); + + info("Selecting the test node"); + yield selectNode(".delayed", inspector); + + info("Getting the animation element from the panel"); + let timelineEl = panel.animationsTimelineComponent.rootWrapperEl; + let animation = timelineEl.querySelector(".time-block"); + let iterations = animation.querySelector(".iterations"); + + // Iterations are rendered with a repeating linear-gradient, so we need to + // calculate how many iterations are represented by looking at the background + // size. + let iterationCount = getIterationCountFromBackground(iterations); + + is(iterationCount, 10, + "The animation timeline contains the right number of iterations"); + ok(!iterations.classList.contains("infinite"), + "The iteration element doesn't have the infinite class"); + + info("Selecting another test node with an infinite animation"); + yield selectNode(".animated", inspector); + + info("Getting the animation element from the panel again"); + animation = timelineEl.querySelector(".time-block"); + iterations = animation.querySelector(".iterations"); + + iterationCount = getIterationCountFromBackground(iterations); + + is(iterationCount, 1, + "The animation timeline contains just one iteration"); + ok(iterations.classList.contains("infinite"), + "The iteration element has the infinite class"); +}); + +function getIterationCountFromBackground(el) { + let backgroundSize = parseFloat(el.style.backgroundSize.split(" ")[0]); + let width = el.offsetWidth; + return Math.round(width / backgroundSize); +} diff --git a/browser/devtools/animationinspector/test/browser_animation_timeline_ui.js b/browser/devtools/animationinspector/test/browser_animation_timeline_ui.js new file mode 100644 index 000000000000..fb3be2e5a69c --- /dev/null +++ b/browser/devtools/animationinspector/test/browser_animation_timeline_ui.js @@ -0,0 +1,41 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the timeline-based UI contains the right elements. + +add_task(function*() { + yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); + let {panel} = yield openAnimationInspectorNewUI(); + + let timeline = panel.animationsTimelineComponent; + let el = timeline.rootWrapperEl; + + ok(el.querySelector(".time-header"), + "The header element is in the DOM of the timeline"); + ok(el.querySelectorAll(".time-header .time-tick").length, + "The header has some time graduations"); + + ok(el.querySelector(".animations"), + "The animations container is in the DOM of the timeline"); + is(el.querySelectorAll(".animations .animation").length, + timeline.animations.length, + "The number of animations displayed matches the number of animations"); + + for (let i = 0; i < timeline.animations.length; i++) { + let animation = timeline.animations[i]; + let animationEl = el.querySelectorAll(".animations .animation")[i]; + + ok(animationEl.querySelector(".target"), + "The animated node target element is in the DOM"); + ok(animationEl.querySelector(".time-block"), + "The timeline element is in the DOM"); + is(animationEl.querySelector(".name").textContent, + animation.state.name, + "The name on the timeline is correct"); + ok(animationEl.querySelector(".iterations"), + "The timeline has iterations displayed"); + } +}); diff --git a/browser/devtools/animationinspector/test/head.js b/browser/devtools/animationinspector/test/head.js index fdc1c0e20d82..19dc8dbfcdd4 100644 --- a/browser/devtools/animationinspector/test/head.js +++ b/browser/devtools/animationinspector/test/head.js @@ -7,6 +7,7 @@ const Cu = Components.utils; const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +const {require} = devtools; const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const TargetFactory = devtools.TargetFactory; const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); @@ -214,6 +215,16 @@ let openAnimationInspector = Task.async(function*() { }; }); +/** + * Turn on the new timeline-based UI pref ON, and then open the toolbox, with + * the inspector tool visible and the animationinspector sidebar selected. + * @return a promise that resolves when the inspector is ready. + */ +function openAnimationInspectorNewUI() { + enableNewUI(); + return openAnimationInspector(); +} + /** * Close the toolbox. * @return a promise that resolves when the toolbox has closed. diff --git a/browser/devtools/animationinspector/test/unit/.eslintrc b/browser/devtools/animationinspector/test/unit/.eslintrc new file mode 100644 index 000000000000..44135af644ce --- /dev/null +++ b/browser/devtools/animationinspector/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/animationinspector/test/unit/test_findOptimalTimeInterval.js b/browser/devtools/animationinspector/test/unit/test_findOptimalTimeInterval.js new file mode 100644 index 000000000000..7373eccc898a --- /dev/null +++ b/browser/devtools/animationinspector/test/unit/test_findOptimalTimeInterval.js @@ -0,0 +1,85 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-eval:0 */ + +"use strict"; + +const Cu = Components.utils; +const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +const {require} = devtools; + +const {findOptimalTimeInterval} = require("devtools/animationinspector/utils"); + +// This test array contains objects that are used to test the +// findOptimalTimeInterval function. Each object should have the following +// properties: +// - desc: an optional string that will be printed out +// - timeScale: a number that represents how many pixels is 1ms +// - minSpacing: an optional number that represents the minim space between 2 +// time graduations +// - expectedInterval: a number that you expect the findOptimalTimeInterval +// function to return as a result. +// Optionally you can pass a string where `interval` is the calculated +// interval, this string will be eval'd and tested to be truthy. +const TEST_DATA = [{ + desc: "With 1px being 1ms and no minSpacing, expect the interval to be the " + + "default min spacing", + timeScale: 1, + minSpacing: undefined, + expectedInterval: 10 +}, { + desc: "With 1px being 1ms and a custom minSpacing being a multiple of 10 " + + "expect the interval to be the custom min spacing", + timeScale: 1, + minSpacing: 40, + expectedInterval: 40 +}, { + desc: "With 1px being 1ms and a custom minSpacing not being multiple of 10 " + + "expect the interval to be the next multiple of 10", + timeScale: 1, + minSpacing: 13, + expectedInterval: 20 +}, { + desc: "If 1ms corresponds to a distance that is greater than the min " + + "spacing then, expect the interval to be this distance", + timeScale: 20, + minSpacing: undefined, + expectedInterval: 20 +}, { + desc: "If 1ms corresponds to a distance that is greater than the min " + + "spacing then, expect the interval to be this distance, even if it " + + "isn't a multiple of 10", + timeScale: 33, + minSpacing: undefined, + expectedInterval: 33 +}, { + desc: "If 1ms is a very small distance, then expect this distance to be " + + "multiplied by 10, 20, 40, 80, etc... until it goes over the min " + + "spacing", + timeScale: 0.001, + minSpacing: undefined, + expectedInterval: 10.24 +}, { + desc: "If the time scale is such that we need to iterate more than the " + + "maximum allowed number of iterations, then expect an interval lower " + + "than the minimum one", + timeScale: 1e-31, + minSpacing: undefined, + expectedInterval: "interval < 10" +}]; + +function run_test() { + for (let {timeScale, desc, minSpacing, expectedInterval} of TEST_DATA) { + do_print("Testing timeScale: " + timeScale + " and minSpacing: " + + minSpacing + ". Expecting " + expectedInterval + "."); + + let interval = findOptimalTimeInterval(timeScale, minSpacing); + if (typeof expectedInterval == "string") { + ok(eval(expectedInterval), desc); + } else { + equal(interval, expectedInterval, desc); + } + } +} diff --git a/browser/devtools/animationinspector/test/unit/test_timeScale.js b/browser/devtools/animationinspector/test/unit/test_timeScale.js new file mode 100644 index 000000000000..9a5335a165cd --- /dev/null +++ b/browser/devtools/animationinspector/test/unit/test_timeScale.js @@ -0,0 +1,191 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cu = Components.utils; +const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +const {require} = devtools; + +const {TimeScale} = require("devtools/animationinspector/components"); + +const TEST_ANIMATIONS = [{ + startTime: 500, + delay: 0, + duration: 1000, + iterationCount: 1 +}, { + startTime: 400, + delay: 100, + duration: 10, + iterationCount: 100 +}, { + startTime: 50, + delay: 1000, + duration: 100, + iterationCount: 20 +}]; +const EXPECTED_MIN_START = 50; +const EXPECTED_MAX_END = 3050; + +const TEST_STARTTIME_TO_DISTANCE = [{ + time: 50, + width: 100, + expectedDistance: 0 +}, { + time: 50, + width: 0, + expectedDistance: 0 +}, { + time: 3050, + width: 200, + expectedDistance: 200 +}, { + time: 1550, + width: 200, + expectedDistance: 100 +}]; + +const TEST_DURATION_TO_DISTANCE = [{ + time: 3000, + width: 100, + expectedDistance: 100 +}, { + time: 0, + width: 100, + expectedDistance: 0 +}]; + +const TEST_DISTANCE_TO_TIME = [{ + distance: 100, + width: 100, + expectedTime: 3050 +}, { + distance: 0, + width: 100, + expectedTime: 50 +}, { + distance: 25, + width: 200, + expectedTime: 425 +}]; + +const TEST_DISTANCE_TO_RELATIVE_TIME = [{ + distance: 100, + width: 100, + expectedTime: 3000 +}, { + distance: 0, + width: 100, + expectedTime: 0 +}, { + distance: 25, + width: 200, + expectedTime: 375 +}]; + +const TEST_FORMAT_TIME_MS = [{ + time: 0, + expectedFormattedTime: "0ms" +}, { + time: 3540.341, + expectedFormattedTime: "3540ms" +}, { + time: 1.99, + expectedFormattedTime: "2ms" +}, { + time: 4000, + expectedFormattedTime: "4000ms" +}]; + +const TEST_FORMAT_TIME_S = [{ + time: 0, + expectedFormattedTime: "0.0s" +}, { + time: 3540.341, + expectedFormattedTime: "3.5s" +}, { + time: 1.99, + expectedFormattedTime: "0.0s" +}, { + time: 4000, + expectedFormattedTime: "4.0s" +}, { + time: 102540, + expectedFormattedTime: "102.5s" +}, { + time: 102940, + expectedFormattedTime: "102.9s" +}]; + +function run_test() { + do_print("Check the default min/max range values"); + equal(TimeScale.minStartTime, Infinity); + equal(TimeScale.maxEndTime, 0); + + do_print("Test adding a few animations"); + for (let {startTime, delay, duration, iterationCount} of TEST_ANIMATIONS) { + TimeScale.addAnimation({startTime, delay, duration, iterationCount}); + } + equal(TimeScale.minStartTime, EXPECTED_MIN_START); + equal(TimeScale.maxEndTime, EXPECTED_MAX_END); + + do_print("Test reseting the animations"); + TimeScale.reset(); + equal(TimeScale.minStartTime, Infinity); + equal(TimeScale.maxEndTime, 0); + + do_print("Test adding the animations again"); + for (let {startTime, delay, duration, iterationCount} of TEST_ANIMATIONS) { + TimeScale.addAnimation({startTime, delay, duration, iterationCount}); + } + equal(TimeScale.minStartTime, EXPECTED_MIN_START); + equal(TimeScale.maxEndTime, EXPECTED_MAX_END); + + do_print("Test converting start times to distances"); + for (let {time, width, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) { + let distance = TimeScale.startTimeToDistance(time, width); + equal(distance, expectedDistance); + } + + do_print("Test converting durations to distances"); + for (let {time, width, expectedDistance} of TEST_DURATION_TO_DISTANCE) { + let distance = TimeScale.durationToDistance(time, width); + equal(distance, expectedDistance); + } + + do_print("Test converting distances to times"); + for (let {distance, width, expectedTime} of TEST_DISTANCE_TO_TIME) { + let time = TimeScale.distanceToTime(distance, width); + equal(time, expectedTime); + } + + do_print("Test converting distances to relative times"); + for (let {distance, width, expectedTime} of TEST_DISTANCE_TO_RELATIVE_TIME) { + let time = TimeScale.distanceToRelativeTime(distance, width); + equal(time, expectedTime); + } + + do_print("Test formatting times (millis)"); + for (let {time, expectedFormattedTime} of TEST_FORMAT_TIME_MS) { + let formattedTime = TimeScale.formatTime(time); + equal(formattedTime, expectedFormattedTime); + } + + // Add 1 more animation to increase the range and test more time formatting + // cases. + TimeScale.addAnimation({ + startTime: 3000, + duration: 5000, + delay: 0, + iterationCount: 1 + }); + + do_print("Test formatting times (seconds)"); + for (let {time, expectedFormattedTime} of TEST_FORMAT_TIME_S) { + let formattedTime = TimeScale.formatTime(time); + equal(formattedTime, expectedFormattedTime); + } +} diff --git a/browser/devtools/animationinspector/test/unit/xpcshell.ini b/browser/devtools/animationinspector/test/unit/xpcshell.ini new file mode 100644 index 000000000000..eed4a1b1f70e --- /dev/null +++ b/browser/devtools/animationinspector/test/unit/xpcshell.ini @@ -0,0 +1,9 @@ +[DEFAULT] +tags = devtools +head = +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' || toolkit == 'gonk' + +[test_findOptimalTimeInterval.js] +[test_timeScale.js] diff --git a/browser/devtools/animationinspector/utils.js b/browser/devtools/animationinspector/utils.js index 137745a45aa6..33cff3884ca7 100644 --- a/browser/devtools/animationinspector/utils.js +++ b/browser/devtools/animationinspector/utils.js @@ -107,12 +107,11 @@ exports.drawGraphElementBackground = drawGraphElementBackground; * @param {Number} timeScale How many px is 1ms in the graph. * @param {Number} minSpacing The minimum spacing between 2 graduations, * defaults to TIME_GRADUATION_MIN_SPACING. - * @return {Number} The optional interval, in pixels. + * @return {Number} The optimal interval, in pixels. */ function findOptimalTimeInterval(timeScale, minSpacing=TIME_GRADUATION_MIN_SPACING) { let timingStep = TIME_INTERVAL_MULTIPLE; - let maxIters = OPTIMAL_TIME_INTERVAL_MAX_ITERS; let numIters = 0; if (timeScale > minSpacing) { @@ -121,7 +120,7 @@ function findOptimalTimeInterval(timeScale, while (true) { let scaledStep = timeScale * timingStep; - if (++numIters > maxIters) { + if (++numIters > OPTIMAL_TIME_INTERVAL_MAX_ITERS) { return scaledStep; } if (scaledStep < minSpacing) { From 05025d4d2c585b4790eb21350c5f79d99d9c2889 Mon Sep 17 00:00:00 2001 From: "Carsten \"Tomcat\" Book" Date: Mon, 15 Jun 2015 15:16:34 +0200 Subject: [PATCH 10/16] Backed out changeset 0b55b1cac565 (bug 1164564) for dt failures in browser_dbg_WorkerActor.attachThread.js --- browser/devtools/debugger/test/browser.ini | 4 - .../test/browser_dbg_WorkerActor.attach.js | 1 + .../browser_dbg_WorkerActor.attachThread.js | 89 ------------------- .../code_WorkerActor.attachThread-worker.js | 16 ---- .../debugger/test/code_frame-script.js | 12 --- .../doc_WorkerActor.attachThread-tab.html | 8 -- browser/devtools/debugger/test/head.js | 64 +------------ toolkit/devtools/client/dbg-client.jsm | 32 ++----- toolkit/devtools/server/actors/script.js | 9 +- .../server/actors/utils/TabSources.js | 2 +- toolkit/devtools/server/actors/worker.js | 43 +-------- toolkit/devtools/server/main.js | 86 +----------------- toolkit/devtools/server/moz.build | 1 - toolkit/devtools/server/worker.js | 66 -------------- toolkit/devtools/worker-loader.js | 6 +- 15 files changed, 18 insertions(+), 421 deletions(-) delete mode 100644 browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js delete mode 100644 browser/devtools/debugger/test/code_WorkerActor.attachThread-worker.js delete mode 100644 browser/devtools/debugger/test/doc_WorkerActor.attachThread-tab.html delete mode 100644 toolkit/devtools/server/worker.js diff --git a/browser/devtools/debugger/test/browser.ini b/browser/devtools/debugger/test/browser.ini index 6243c7831151..e9ef792bdfae 100644 --- a/browser/devtools/debugger/test/browser.ini +++ b/browser/devtools/debugger/test/browser.ini @@ -45,7 +45,6 @@ support-files = code_ugly-8^headers^ code_WorkerActor.attach-worker1.js code_WorkerActor.attach-worker2.js - code_WorkerActor.attachThread-worker.js doc_auto-pretty-print-01.html doc_auto-pretty-print-02.html doc_binary_search.html @@ -108,7 +107,6 @@ support-files = doc_with-frame.html doc_WorkerActor.attach-tab1.html doc_WorkerActor.attach-tab2.html - doc_WorkerActor.attachThread-tab.html head.js sjs_random-javascript.sjs testactors.js @@ -568,5 +566,3 @@ skip-if = e10s && debug skip-if = e10s && debug [browser_dbg_WorkerActor.attach.js] skip-if = e10s && debug -[browser_dbg_WorkerActor.attachThread.js] -skip-if = e10s && debug diff --git a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js index a9ca7a91544d..fe0d839518c5 100644 --- a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js +++ b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js @@ -27,6 +27,7 @@ function test() { // registered. Instead, we have to wait for the promise returned by // createWorker in the tab to be resolved. yield createWorkerInTab(tab, WORKER1_URL); + let { workers } = yield listWorkers(tabClient); let [, workerClient1] = yield attachWorker(tabClient, findWorker(workers, WORKER1_URL)); diff --git a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js deleted file mode 100644 index fd7f1e839223..000000000000 --- a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js +++ /dev/null @@ -1,89 +0,0 @@ -let TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html"; -let WORKER_URL = "code_WorkerActor.attachThread-worker.js"; - -function test() { - Task.spawn(function* () { - DebuggerServer.init(); - DebuggerServer.addBrowserActors(); - - let client1 = new DebuggerClient(DebuggerServer.connectPipe()); - yield connect(client1); - let client2 = new DebuggerClient(DebuggerServer.connectPipe()); - yield connect(client2); - - let tab = yield addTab(TAB_URL); - let { tabs: tabs1 } = yield listTabs(client1); - let [, tabClient1] = yield attachTab(client1, findTab(tabs1, TAB_URL)); - let { tabs: tabs2 } = yield listTabs(client2); - let [, tabClient2] = yield attachTab(client2, findTab(tabs2, TAB_URL)); - - yield listWorkers(tabClient1); - yield listWorkers(tabClient2); - yield createWorkerInTab(tab, WORKER_URL); - let { workers: workers1 } = yield listWorkers(tabClient1); - let [, workerClient1] = yield attachWorker(tabClient1, - findWorker(workers1, WORKER_URL)); - let { workers: workers2 } = yield listWorkers(tabClient2); - let [, workerClient2] = yield attachWorker(tabClient2, - findWorker(workers2, WORKER_URL)); - - let location = { line: 5 }; - - let [, threadClient1] = yield attachThread(workerClient1); - let sources1 = yield getSources(threadClient1); - let sourceClient1 = threadClient1.source(findSource(sources1, - EXAMPLE_URL + WORKER_URL)); - let [, breakpointClient1] = yield setBreakpoint(sourceClient1, location); - yield resume(threadClient1); - - let [, threadClient2] = yield attachThread(workerClient2); - let sources2 = yield getSources(threadClient2); - let sourceClient2 = threadClient2.source(findSource(sources2, - EXAMPLE_URL + WORKER_URL)); - let [, breakpointClient2] = yield setBreakpoint(sourceClient2, location); - yield resume(threadClient2); - - postMessageToWorkerInTab(tab, WORKER_URL, "ping"); - yield Promise.all([ - waitForPause(threadClient1).then((packet) => { - is(packet.type, "paused"); - let why = packet.why; - is(why.type, "breakpoint"); - is(why.actors.length, 1); - is(why.actors[0], breakpointClient1.actor); - let frame = packet.frame; - let where = frame.where; - is(where.source.actor, sourceClient1.actor); - is(where.line, location.line); - let variables = frame.environment.bindings.variables; - is(variables.a.value, 1); - is(variables.b.value.type, "undefined"); - is(variables.c.value.type, "undefined"); - return resume(threadClient1); - }), - waitForPause(threadClient2).then((packet) => { - is(packet.type, "paused"); - let why = packet.why; - is(why.type, "breakpoint"); - is(why.actors.length, 1); - is(why.actors[0], breakpointClient2.actor); - let frame = packet.frame; - let where = frame.where; - is(where.source.actor, sourceClient2.actor); - is(where.line, location.line); - let variables = frame.environment.bindings.variables; - is(variables.a.value, 1); - is(variables.b.value.type, "undefined"); - is(variables.c.value.type, "undefined"); - return resume(threadClient2); - }), - ]); - - terminateWorkerInTab(tab, WORKER_URL); - yield waitForWorkerClose(workerClient1); - yield waitForWorkerClose(workerClient2); - yield close(client1); - yield close(client2); - finish(); - }); -} diff --git a/browser/devtools/debugger/test/code_WorkerActor.attachThread-worker.js b/browser/devtools/debugger/test/code_WorkerActor.attachThread-worker.js deleted file mode 100644 index 4c115749dfe8..000000000000 --- a/browser/devtools/debugger/test/code_WorkerActor.attachThread-worker.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; - -function f() { - var a = 1; - var b = 2; - var c = 3; -} - -self.onmessage = function (event) { - if (event.data == "ping") { - f() - postMessage("pong"); - } -}; - -postMessage("load"); diff --git a/browser/devtools/debugger/test/code_frame-script.js b/browser/devtools/debugger/test/code_frame-script.js index eb9a85cf6da4..529328cbd0f2 100644 --- a/browser/devtools/debugger/test/code_frame-script.js +++ b/browser/devtools/debugger/test/code_frame-script.js @@ -83,15 +83,3 @@ addMessageListener("jsonrpc", function ({ data: { method, params, id } }) { }); }); }); - -addMessageListener("test:postMessageToWorker", function (message) { - dump("Posting message '" + message.data.message + "' to worker with url '" + - message.data.url + "'.\n"); - - let worker = workers[message.data.url]; - worker.postMessage(message.data.message); - worker.addEventListener("message", function listener() { - worker.removeEventListener("message", listener); - sendAsyncMessage("test:postMessageToWorker"); - }); -}); diff --git a/browser/devtools/debugger/test/doc_WorkerActor.attachThread-tab.html b/browser/devtools/debugger/test/doc_WorkerActor.attachThread-tab.html deleted file mode 100644 index 62ab9be7d2e8..000000000000 --- a/browser/devtools/debugger/test/doc_WorkerActor.attachThread-tab.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/browser/devtools/debugger/test/head.js b/browser/devtools/debugger/test/head.js index 857b844a189f..1002c36717c6 100644 --- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -512,13 +512,9 @@ function getTab(aTarget, aWindow) { } function getSources(aClient) { - info("Getting sources."); - let deferred = promise.defer(); - aClient.getSources((packet) => { - deferred.resolve(packet.sources); - }); + aClient.getSources(({sources}) => deferred.resolve(sources)); return deferred.promise; } @@ -1133,15 +1129,6 @@ function waitForWorkerListChanged(tabClient) { }); } -function attachThread(workerClient, options) { - info("Attaching to thread."); - return new Promise(function(resolve, reject) { - workerClient.attachThread(options, function (response, threadClient) { - resolve([response, threadClient]); - }); - }); -} - function waitForWorkerClose(workerClient) { info("Waiting for worker to close."); return new Promise(function (resolve) { @@ -1169,52 +1156,3 @@ function waitForWorkerThaw(workerClient) { }); }); } - -function resume(threadClient) { - info("Resuming thread."); - return rdpInvoke(threadClient, threadClient.resume); -} - -function findSource(sources, url) { - info("Finding source with url '" + url + "'.\n"); - for (let source of sources) { - if (source.url === url) { - return source; - } - } - return null; -} - -function setBreakpoint(sourceClient, location) { - info("Setting breakpoint.\n"); - return new Promise(function (resolve) { - sourceClient.setBreakpoint(location, function (response, breakpointClient) { - resolve([response, breakpointClient]); - }); - }); -} - -function waitForEvent(client, type, predicate) { - return new Promise(function (resolve) { - function listener(type, packet) { - if (!predicate(packet)) { - return; - } - client.removeListener(listener); - resolve(packet); - } - - if (predicate) { - client.addListener(type, listener); - } else { - client.addOneTimeListener(type, function (type, packet) { - resolve(packet); - }); - } - }); -} - -function waitForPause(threadClient) { - info("Waiting for pause.\n"); - return waitForEvent(threadClient, "paused"); -} diff --git a/toolkit/devtools/client/dbg-client.jsm b/toolkit/devtools/client/dbg-client.jsm index 833e26a5b136..2f1f0b59eb3e 100644 --- a/toolkit/devtools/client/dbg-client.jsm +++ b/toolkit/devtools/client/dbg-client.jsm @@ -1360,7 +1360,7 @@ TabClient.prototype = { eventSource(TabClient.prototype); function WorkerClient(aClient, aForm) { - this.client = aClient; + this._client = aClient; this._actor = aForm.from; this._isClosed = false; this._isFrozen = aForm.isFrozen; @@ -1376,11 +1376,11 @@ function WorkerClient(aClient, aForm) { WorkerClient.prototype = { get _transport() { - return this.client._transport; + return this._client._transport; }, get request() { - return this.client.request; + return this._client.request; }, get actor() { @@ -1397,41 +1397,19 @@ WorkerClient.prototype = { detach: DebuggerClient.requester({ type: "detach" }, { after: function (aResponse) { - this.client.unregisterClient(this); + this._client.unregisterClient(this); return aResponse; }, telemetry: "WORKERDETACH" }), - attachThread: function(aOptions = {}, aOnResponse = noop) { - if (this.thread) { - DevToolsUtils.executeSoon(() => aOnResponse({ - type: "connected", - threadActor: this.thread._actor, - }, this.thread)); - return; - } - - this.request({ - to: this._actor, - type: "connect", - options: aOptions, - }, (aResponse) => { - if (!aResponse.error) { - this.thread = new ThreadClient(this, aResponse.threadActor); - this.client.registerClient(this.thread); - } - aOnResponse(aResponse, this.thread); - }); - }, - _onClose: function () { this.removeListener("close", this._onClose); this.removeListener("freeze", this._onFreeze); this.removeListener("thaw", this._onThaw); - this.client.unregisterClient(this); + this._client.unregisterClient(this); this._closed = true; }, diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js index 04779a87aed3..0c861ffae160 100644 --- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -15,7 +15,6 @@ const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); const { dbg_assert, dumpn, update, fetch } = DevToolsUtils; const { dirname, joinURI } = require("devtools/toolkit/path"); const promise = require("promise"); -const PromiseDebugging = require("PromiseDebugging"); const xpcInspector = require("xpcInspector"); const ScriptStore = require("./utils/ScriptStore"); const {DevToolsWorker} = require("devtools/toolkit/shared/worker.js"); @@ -1495,7 +1494,7 @@ ThreadActor.prototype = { // Clear DOM event breakpoints. // XPCShell tests don't use actual DOM windows for globals and cause // removeListenerForAllEvents to throw. - if (!isWorker && this.global && !this.global.toString().includes("Sandbox")) { + if (this.global && !this.global.toString().includes("Sandbox")) { let els = Cc["@mozilla.org/eventlistenerservice;1"] .getService(Ci.nsIEventListenerService); els.removeListenerForAllEvents(this.global, this._allEventsListener, true); @@ -1934,7 +1933,7 @@ ThreadActor.prototype = { } if (promises.length > 0) { - this.synchronize(promise.all(promises)); + this.synchronize(Promise.all(promises)); } return true; @@ -2871,10 +2870,10 @@ SourceActor.prototype = { actor, GeneratedLocation.fromOriginalLocation(originalLocation) )) { - return promise.resolve(null); + return Promise.resolve(null); } - return promise.resolve(originalLocation); + return Promise.resolve(originalLocation); } else { return this.sources.getAllGeneratedLocations(originalLocation) .then((generatedLocations) => { diff --git a/toolkit/devtools/server/actors/utils/TabSources.js b/toolkit/devtools/server/actors/utils/TabSources.js index d03022ab375f..1368a6218bb7 100644 --- a/toolkit/devtools/server/actors/utils/TabSources.js +++ b/toolkit/devtools/server/actors/utils/TabSources.js @@ -10,7 +10,7 @@ const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); const { dbg_assert, fetch } = DevToolsUtils; const EventEmitter = require("devtools/toolkit/event-emitter"); const { OriginalLocation, GeneratedLocation, getOffsetColumn } = require("devtools/server/actors/common"); -const { resolve } = require("promise"); +const { resolve } = Promise; loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/script", true); loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/script", true); diff --git a/toolkit/devtools/server/actors/worker.js b/toolkit/devtools/server/actors/worker.js index 5d32e633d305..8f87fc79c721 100644 --- a/toolkit/devtools/server/actors/worker.js +++ b/toolkit/devtools/server/actors/worker.js @@ -1,7 +1,6 @@ "use strict"; let { Ci, Cu } = require("chrome"); -let { DebuggerServer } = require("devtools/server/main"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -29,8 +28,6 @@ function matchWorkerDebugger(dbg, options) { function WorkerActor(dbg) { this._dbg = dbg; this._isAttached = false; - this._threadActor = null; - this._transport = null; } WorkerActor.prototype = { @@ -69,33 +66,6 @@ WorkerActor.prototype = { return { type: "detached" }; }, - onConnect: function (request) { - if (!this._isAttached) { - return { error: "wrongState" }; - } - - if (this._threadActor !== null) { - return { - type: "connected", - threadActor: this._threadActor - }; - } - - return DebuggerServer.connectToWorker( - this.conn, this._dbg, this.actorID, request.options - ).then(({ threadActor, transport }) => { - this._threadActor = threadActor; - this._transport = transport; - - return { - type: "connected", - threadActor: this._threadActor - }; - }, (error) => { - return { error: error.toString() }; - }); - }, - onClose: function () { if (this._isAttached) { this._detach(); @@ -104,10 +74,6 @@ WorkerActor.prototype = { this.conn.sendActorEvent(this.actorID, "close"); }, - onError: function (filename, lineno, message) { - reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n"); - }, - onFreeze: function () { this.conn.sendActorEvent(this.actorID, "freeze"); }, @@ -117,12 +83,6 @@ WorkerActor.prototype = { }, _detach: function () { - if (this._threadActor !== null) { - this._transport.close(); - this._transport = null; - this._threadActor = null; - } - this._dbg.removeListener(this); this._isAttached = false; } @@ -130,8 +90,7 @@ WorkerActor.prototype = { WorkerActor.prototype.requestTypes = { "attach": WorkerActor.prototype.onAttach, - "detach": WorkerActor.prototype.onDetach, - "connect": WorkerActor.prototype.onConnect + "detach": WorkerActor.prototype.onDetach }; exports.WorkerActor = WorkerActor; diff --git a/toolkit/devtools/server/main.js b/toolkit/devtools/server/main.js index 9c6c3738e6ba..0b8f902caf5c 100644 --- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -14,7 +14,7 @@ let { Ci, Cc, CC, Cu, Cr } = require("chrome"); let Services = require("Services"); let { ActorPool, OriginalLocation, RegisteredActorFactory, ObservedActorFactory } = require("devtools/server/actors/common"); -let { LocalDebuggerTransport, ChildDebuggerTransport, WorkerDebuggerTransport } = +let { LocalDebuggerTransport, ChildDebuggerTransport } = require("devtools/toolkit/transport/transport"); let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); let { dumpn, dumpv, dbg_assert } = DevToolsUtils; @@ -685,13 +685,10 @@ var DebuggerServer = { * "debug::packet", and all its actors will have names * beginning with "/". */ - connectToParent: function(aPrefix, aScopeOrManager) { + connectToParent: function(aPrefix, aMessageManager) { this._checkInit(); - let transport = isWorker ? - new WorkerDebuggerTransport(aScopeOrManager, aPrefix) : - new ChildDebuggerTransport(aScopeOrManager, aPrefix); - + let transport = new ChildDebuggerTransport(aMessageManager, aPrefix); return this._onConnection(transport, aPrefix, true); }, @@ -758,83 +755,6 @@ var DebuggerServer = { return deferred.promise; }, - connectToWorker: function (aConnection, aDbg, aId, aOptions) { - return new Promise((resolve, reject) => { - // Step 1: Initialize the worker debugger. - aDbg.initialize("resource://gre/modules/devtools/server/worker.js"); - - // Step 2: Send a connect request to the worker debugger. - aDbg.postMessage(JSON.stringify({ - type: "connect", - id: aId, - options: aOptions - })); - - // Steps 3-5 are performed on the worker thread (see worker.js). - - // Step 6: Wait for a response from the worker debugger. - let listener = { - onClose: () => { - aDbg.removeListener(listener); - - reject("closed"); - }, - - onMessage: (message) => { - let packet = JSON.parse(message); - if (packet.type !== "message" || packet.id !== aId) { - return; - } - - message = packet.message; - if (message.error) { - reject(error); - } - - if (message.type !== "paused") { - return; - } - - aDbg.removeListener(listener); - - // Step 7: Create a transport for the connection to the worker. - let transport = new WorkerDebuggerTransport(aDbg, aId); - transport.ready(); - transport.hooks = { - onClosed: () => { - if (!aDbg.isClosed) { - aDbg.postMessage(JSON.stringify({ - type: "disconnect", - id: aId - })); - } - - aConnection.cancelForwarding(aId); - }, - - onPacket: (packet) => { - // Ensure that any packets received from the server on the worker - // thread are forwarded to the client on the main thread, as if - // they had been sent by the server on the main thread. - aConnection.send(packet); - } - }; - - // Ensure that any packets received from the client on the main thread - // to actors on the worker thread are forwarded to the server on the - // worker thread. - aConnection.setForwarding(aId, transport); - - resolve({ - threadActor: message.from, - transport: transport - }); - } - }; - aDbg.addListener(listener); - }); - }, - /** * Check if the caller is running in a content child process. * diff --git a/toolkit/devtools/server/moz.build b/toolkit/devtools/server/moz.build index 417b34ddcb98..1503896e66da 100644 --- a/toolkit/devtools/server/moz.build +++ b/toolkit/devtools/server/moz.build @@ -51,7 +51,6 @@ EXTRA_JS_MODULES.devtools.server += [ 'content-globals.js', 'main.js', 'protocol.js', - 'worker.js' ] EXTRA_JS_MODULES.devtools.server.actors += [ diff --git a/toolkit/devtools/server/worker.js b/toolkit/devtools/server/worker.js deleted file mode 100644 index 48f414e4828d..000000000000 --- a/toolkit/devtools/server/worker.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict" - -loadSubScript("resource://gre/modules/devtools/worker-loader.js"); - -let { ActorPool } = worker.require("devtools/server/actors/common"); -let { ThreadActor } = worker.require("devtools/server/actors/script"); -let { TabSources } = worker.require("devtools/server/actors/utils/TabSources"); -let makeDebugger = worker.require("devtools/server/actors/utils/make-debugger"); -let { DebuggerServer } = worker.require("devtools/server/main"); - -DebuggerServer.init(); -DebuggerServer.createRootActor = function () { - throw new Error("Should never get here!"); -}; - -let connections = Object.create(null); - -this.addEventListener("message", function (event) { - let packet = JSON.parse(event.data); - switch (packet.type) { - case "connect": - // Step 3: Create a connection to the parent. - let connection = DebuggerServer.connectToParent(packet.id, this); - connections[packet.id] = connection; - - // Step 4: Create a thread actor for the connection to the parent. - let pool = new ActorPool(connection); - connection.addActorPool(pool); - - let sources = null; - - let actor = new ThreadActor({ - makeDebugger: makeDebugger.bind(null, { - findDebuggees: () => { - return [this.global]; - }, - - shouldAddNewGlobalAsDebuggee: () => { - return true; - }, - }), - - get sources() { - if (sources === null) { - sources = new TabSources(actor); - } - return sources; - } - }, global); - - pool.addActor(actor); - - // Step 5: Attach to the thread actor. - // - // This will cause a packet to be sent over the connection to the parent. - // Because this connection uses WorkerDebuggerTransport internally, this - // packet will be sent using WorkerDebuggerGlobalScope.postMessage, causing - // an onMessage event to be fired on the WorkerDebugger in the main thread. - actor.onAttach({}); - break; - - case "disconnect": - connections[packet.id].close(); - break; - }; -}); diff --git a/toolkit/devtools/worker-loader.js b/toolkit/devtools/worker-loader.js index f5932a17aae6..6652c38aab3f 100644 --- a/toolkit/devtools/worker-loader.js +++ b/toolkit/devtools/worker-loader.js @@ -435,8 +435,6 @@ let { } else { // Worker thread let requestors = []; - let scope = this; - let xpcInspector = { get lastNestRequestor() { return requestors.length === 0 ? null : requestors[0]; @@ -444,13 +442,13 @@ let { enterNestedEventLoop: function (requestor) { requestors.push(requestor); - scope.enterEventLoop(); + this.enterEventLoop(); return requestors.length; }, exitNestedEventLoop: function () { requestors.pop(); - scope.leaveEventLoop(); + this.leaveEventLoop(); return requestors.length; } }; From 4cf3440434bfa52e08472b136e5a4443005c6eb3 Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Wed, 10 Jun 2015 08:48:01 -0400 Subject: [PATCH 11/16] Bug 1170851 - Warn about add-ons detected as no longer signed. r=mfinkle,Mossop --HG-- extra : rebase_source : b5895802073f699215483e0f1ec284752cbe91fd --- mobile/android/chrome/content/browser.js | 48 ++++++++++++++++--- .../locales/en-US/chrome/browser.properties | 7 +++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 27b0fa8a3b14..9539af96e7cb 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -6189,19 +6189,21 @@ let HealthReportStatusListener = { }; var XPInstallObserver = { - init: function xpi_init() { - Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false); - Services.obs.addObserver(XPInstallObserver, "addon-install-started", false); + init: function() { + Services.obs.addObserver(this, "addon-install-blocked", false); + Services.obs.addObserver(this, "addon-install-started", false); + Services.obs.addObserver(this, "xpi-signature-changed", false); + Services.obs.addObserver(this, "browser-delayed-startup-finished", false); - AddonManager.addInstallListener(XPInstallObserver); + AddonManager.addInstallListener(this); }, - observe: function xpi_observer(aSubject, aTopic, aData) { + observe: function(aSubject, aTopic, aData) { switch (aTopic) { case "addon-install-started": NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsDownloading"), "short"); break; - case "addon-install-blocked": + case "addon-install-blocked": { let installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo); let tab = BrowserApp.getTabForBrowser(installInfo.browser); if (!tab) @@ -6268,9 +6270,43 @@ var XPInstallObserver = { } NativeWindow.doorhanger.show(message, aTopic, buttons, tab.id); break; + } + case "xpi-signature-changed": { + if (JSON.parse(aData).disabled.length) { + this._notifyUnsignedAddonsDisabled(); + } + break; + } + case "browser-delayed-startup-finished": { + let disabledAddons = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED); + for (let id of disabledAddons) { + if (AddonManager.getAddonByID(id).signedState <= AddonManager.SIGNEDSTATE_MISSING) { + this._notifyUnsignedAddonsDisabled(); + break; + } + } + break; + } } }, + _notifyUnsignedAddonsDisabled: function() { + new Prompt({ + window: window, + title: Strings.browser.GetStringFromName("unsignedAddonsDisabled.title"), + message: Strings.browser.GetStringFromName("unsignedAddonsDisabled.message"), + buttons: [ + Strings.browser.GetStringFromName("unsignedAddonsDisabled.viewAddons"), + Strings.browser.GetStringFromName("unsignedAddonsDisabled.dismiss") + ] + }).show((data) => { + if (data.button === 0) { + // TODO: Open about:addons to show only unsigned add-ons? + BrowserApp.addTab("about:addons", { parentId: BrowserApp.selectedTab.id }); + } + }); + }, + onInstallEnded: function(aInstall, aAddon) { // Don't create a notification for distribution add-ons. if (Distribution.pendingAddonInstalls.has(aInstall)) { diff --git a/mobile/android/locales/en-US/chrome/browser.properties b/mobile/android/locales/en-US/chrome/browser.properties index c6daab42df44..32c25b9ca6cc 100644 --- a/mobile/android/locales/en-US/chrome/browser.properties +++ b/mobile/android/locales/en-US/chrome/browser.properties @@ -45,6 +45,13 @@ addonError.titleError=Error addonError.titleBlocked=Blocked add-on addonError.learnMore=Learn more +# LOCALIZATION NOTE (unsignedAddonsDisabled.title, unsignedAddonsDisabled.message): +# These strings will appear in a dialog when Firefox detects that installed add-ons cannot be verified. +unsignedAddonsDisabled.title=Unverified add-ons +unsignedAddonsDisabled.message=One or more installed add-ons cannot be verified and have been disabled. +unsignedAddonsDisabled.dismiss=Dismiss +unsignedAddonsDisabled.viewAddons=View add-ons + # LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4, addonError-5): # #1 is the add-on name, #2 is the add-on host, #3 is the application name addonError-1=The add-on could not be downloaded because of a connection failure on #2. From cf1008339be279e330f1bd220ed2e8931d5dd6b6 Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Thu, 11 Jun 2015 11:52:41 -0400 Subject: [PATCH 12/16] Bug 1173895 - Hide enable/disable context menu items for app disabled add-ons. r=liuche --HG-- extra : rebase_source : ef56d1b01d71fe33dc0b124e9a1eda6040c8347e --- mobile/android/chrome/content/aboutAddons.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/mobile/android/chrome/content/aboutAddons.js b/mobile/android/chrome/content/aboutAddons.js index a66c7e0c7904..7740c45e8e53 100644 --- a/mobile/android/chrome/content/aboutAddons.js +++ b/mobile/android/chrome/content/aboutAddons.js @@ -58,6 +58,14 @@ var ContextMenus = { document.getElementById("contextmenu-uninstall").removeAttribute("hidden"); } + // Hide the enable/disable context menu items if the add-on was disabled by + // Firefox (e.g. unsigned or blocklisted add-on). + if (addon.appDisabled) { + document.getElementById("contextmenu-enable").setAttribute("hidden", "true"); + document.getElementById("contextmenu-disable").setAttribute("hidden", "true"); + return; + } + let enabled = this.target.getAttribute("isDisabled") != "true"; if (enabled) { document.getElementById("contextmenu-enable").setAttribute("hidden", "true"); @@ -311,16 +319,18 @@ var Addons = { gStringBundle.formatStringFromName("addonStatus.uninstalled", [addon.name], 1); let enableBtn = document.getElementById("enable-btn"); - if (addon.appDisabled) + if (addon.appDisabled) { enableBtn.setAttribute("disabled", "true"); - else + } else { enableBtn.removeAttribute("disabled"); + } let uninstallBtn = document.getElementById("uninstall-btn"); - if (addon.scope == AddonManager.SCOPE_APPLICATION) + if (addon.scope == AddonManager.SCOPE_APPLICATION) { uninstallBtn.setAttribute("disabled", "true"); - else + } else { uninstallBtn.removeAttribute("disabled"); + } let box = document.querySelector("#addons-details > .addon-item .options-box"); box.innerHTML = ""; From 5fe27ac7e1d502b339faf32333291807e01df2ed Mon Sep 17 00:00:00 2001 From: Sami Jaktholm Date: Sat, 13 Jun 2015 09:01:04 +0300 Subject: [PATCH 13/16] Bug 1173196 - Add 'transitionend' listener to the toolbox frame before triggering the transition to avoid missing the event. r=pbrosset --- browser/devtools/framework/toolbox-hosts.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/browser/devtools/framework/toolbox-hosts.js b/browser/devtools/framework/toolbox-hosts.js index c8996df91848..9bc76106fcee 100644 --- a/browser/devtools/framework/toolbox-hosts.js +++ b/browser/devtools/framework/toolbox-hosts.js @@ -106,14 +106,13 @@ BottomHost.prototype = { } this.isMinimized = true; - this.frame.style.marginBottom = -this.frame.height + height + "px"; - this._splitter.classList.add("disabled"); - let onTransitionEnd = () => { this.frame.removeEventListener("transitionend", onTransitionEnd); this.emit("minimized"); }; this.frame.addEventListener("transitionend", onTransitionEnd); + this.frame.style.marginBottom = -this.frame.height + height + "px"; + this._splitter.classList.add("disabled"); }, /** @@ -126,14 +125,13 @@ BottomHost.prototype = { } this.isMinimized = false; - this.frame.style.marginBottom = "0"; - this._splitter.classList.remove("disabled"); - let onTransitionEnd = () => { this.frame.removeEventListener("transitionend", onTransitionEnd); this.emit("maximized"); }; this.frame.addEventListener("transitionend", onTransitionEnd); + this.frame.style.marginBottom = "0"; + this._splitter.classList.remove("disabled"); }, /** From bf15f241b768a7e643ef985ee2596e17d471a1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A3o=20Gottwald?= Date: Fri, 12 Jun 2015 05:09:00 -0400 Subject: [PATCH 14/16] Bug 1173749 - Use a lighter blue for URLs in the URL bar's autocomplete popup. r=Gijs --- browser/themes/windows/browser.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index b2a317c18953..fed80cccf298 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -1502,6 +1502,13 @@ richlistitem[type~="action"][actiontype="searchengine"] > .ac-title-box > .ac-si } } +@media (-moz-os-version: windows-win10) and (-moz-windows-default-theme) { + .ac-url-text:not([selected="true"]), + .ac-action-text:not([selected="true"]) { + color: Highlight; + } +} + richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon { list-style-image: url("chrome://browser/skin/actionicon-tab.png"); -moz-image-region: rect(0, 16px, 11px, 0); From 8538d005f344be62298fd5d2b13867a044775094 Mon Sep 17 00:00:00 2001 From: Simon Lindholm Date: Fri, 12 Jun 2015 13:09:00 -0400 Subject: [PATCH 15/16] Bug 1174289 - Remove fake focus-ring after e10s findbar is closed. r=evilpie --- toolkit/modules/RemoteFinder.jsm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/toolkit/modules/RemoteFinder.jsm b/toolkit/modules/RemoteFinder.jsm index e6c58a019629..dc46b62dc366 100644 --- a/toolkit/modules/RemoteFinder.jsm +++ b/toolkit/modules/RemoteFinder.jsm @@ -235,6 +235,10 @@ RemoteFinderListener.prototype = { this._finder.highlight(data.highlight, data.word); break; + case "Finder:EnableSelection": + this._finder.enableSelection(); + break; + case "Finder:RemoveSelection": this._finder.removeSelection(); break; From 8b2b1647a03002876ffa10eb6179e84260e402c8 Mon Sep 17 00:00:00 2001 From: Simon Lindholm Date: Fri, 12 Jun 2015 13:07:00 -0400 Subject: [PATCH 16/16] Bug 1174291 - Fix ctrl-return for e10s findbar. r=evilpie --- toolkit/modules/RemoteFinder.jsm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/toolkit/modules/RemoteFinder.jsm b/toolkit/modules/RemoteFinder.jsm index dc46b62dc366..d1d27d4395ab 100644 --- a/toolkit/modules/RemoteFinder.jsm +++ b/toolkit/modules/RemoteFinder.jsm @@ -152,6 +152,9 @@ RemoteFinder.prototype = { keyPress: function (aEvent) { this._browser.messageManager.sendAsyncMessage("Finder:KeyPress", { keyCode: aEvent.keyCode, + ctrlKey: aEvent.ctrlKey, + metaKey: aEvent.metaKey, + altKey: aEvent.altKey, shiftKey: aEvent.shiftKey }); },