diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index b5b727c6bf3..030dd91772d 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1439,6 +1439,7 @@ pref("devtools.timeline.enabled", true); #else pref("devtools.timeline.enabled", false); #endif +pref("devtools.timeline.hiddenMarkers", "[]"); // Enable perftools via build command #ifdef MOZ_DEVTOOLS_PERFTOOLS diff --git a/browser/components/migration/FirefoxProfileMigrator.js b/browser/components/migration/FirefoxProfileMigrator.js index 6e9966b21f5..daf0ff8601f 100644 --- a/browser/components/migration/FirefoxProfileMigrator.js +++ b/browser/components/migration/FirefoxProfileMigrator.js @@ -96,7 +96,8 @@ FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileD let places = getFileResource(types.HISTORY, ["places.sqlite"]); let cookies = getFileResource(types.COOKIES, ["cookies.sqlite"]); let passwords = getFileResource(types.PASSWORDS, - ["signons.sqlite", "logins.json", "key3.db"]); + ["signons.sqlite", "logins.json", "key3.db", + "signedInUser.json"]); let formData = getFileResource(types.FORMDATA, ["formhistory.sqlite"]); let bookmarksBackups = getFileResource(types.OTHERDATA, [PlacesBackups.profileRelativeFolderPath]); diff --git a/browser/devtools/shared/widgets/Graphs.jsm b/browser/devtools/shared/widgets/Graphs.jsm index ec80948677c..826ab553630 100644 --- a/browser/devtools/shared/widgets/Graphs.jsm +++ b/browser/devtools/shared/widgets/Graphs.jsm @@ -616,14 +616,19 @@ AbstractCanvasGraph.prototype = { /** * Updates this graph to reflect the new dimensions of the parent node. + * + * @param boolean options.force + * Force redrawing everything */ - refresh: function() { + refresh: function(options={}) { let bounds = this._parent.getBoundingClientRect(); let newWidth = this.fixedWidth || bounds.width; let newHeight = this.fixedHeight || bounds.height; - // Prevent redrawing everything if the graph's width & height won't change. - if (this._width == newWidth * this._pixelRatio && + // Prevent redrawing everything if the graph's width & height won't change, + // except if force=true. + if (!options.force && + this._width == newWidth * this._pixelRatio && this._height == newHeight * this._pixelRatio) { this.emit("refresh-cancelled"); return; diff --git a/browser/devtools/timeline/test/browser.ini b/browser/devtools/timeline/test/browser.ini index 691cc330408..baf7dfd1058 100644 --- a/browser/devtools/timeline/test/browser.ini +++ b/browser/devtools/timeline/test/browser.ini @@ -6,6 +6,7 @@ support-files = [browser_timeline_aaa_run_first_leaktest.js] [browser_timeline_blueprint.js] +[browser_timeline_filters.js] [browser_timeline_overview-initial-selection-01.js] [browser_timeline_overview-initial-selection-02.js] [browser_timeline_overview-update.js] diff --git a/browser/devtools/timeline/test/browser_timeline_filters.js b/browser/devtools/timeline/test/browser_timeline_filters.js new file mode 100644 index 00000000000..933c7e9dd38 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_filters.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests markers filtering mechanism. + */ + +add_task(function*() { + let { target, panel } = yield initTimelinePanel(SIMPLE_URL); + let { $, $$, TimelineController, TimelineView } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + yield waitUntil(() => { + // Wait until we get 3 different markers. + let markers = TimelineController.getMarkers(); + return markers.some(m => m.name == "Styles") && + markers.some(m => m.name == "Reflow") && + markers.some(m => m.name == "Paint"); + }); + + yield TimelineController.toggleRecording(); + + let overview = TimelineView.markersOverview; + let waterfall = TimelineView.waterfall; + + // Select everything + overview.setSelection({ start: 0, end: overview.width }) + + $("#filter-button").click(); + + yield waitUntil(() => !waterfall._outstandingMarkers.length); + + let menuItem1 = $("menuitem[marker-type=Styles]"); + let menuItem2 = $("menuitem[marker-type=Reflow]"); + let menuItem3 = $("menuitem[marker-type=Paint]"); + + let originalHeight = overview.fixedHeight; + + 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)"); + + let heightBefore = overview.fixedHeight; + EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin); + yield once(menuItem1, "command"); + + yield waitUntil(() => !waterfall._outstandingMarkers.length); + + // A row is 11px. See markers-overview.js + is(overview.fixedHeight, heightBefore - 11, "Overview is smaller"); + 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)"); + + heightBefore = overview.fixedHeight; + EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin); + yield once(menuItem2, "command"); + + yield waitUntil(() => !waterfall._outstandingMarkers.length); + + is(overview.fixedHeight, heightBefore - 11, "Overview is smaller"); + 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)"); + + heightBefore = overview.fixedHeight; + EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin); + yield once(menuItem3, "command"); + + yield waitUntil(() => !waterfall._outstandingMarkers.length); + + is(overview.fixedHeight, heightBefore - 11, "Overview is smaller"); + 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)"); + + for (let item of [menuItem1, menuItem2, menuItem3]) { + EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin); + yield once(item, "command"); + } + + yield waitUntil(() => !waterfall._outstandingMarkers.length); + + ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (5)"); + ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (5)"); + ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (5)"); + + is(overview.fixedHeight, originalHeight, "Overview restored"); + + $(".waterfall-marker-bar[type=Styles]"); +}); diff --git a/browser/devtools/timeline/test/head.js b/browser/devtools/timeline/test/head.js index 3ac9624e26b..74b185aa361 100644 --- a/browser/devtools/timeline/test/head.js +++ b/browser/devtools/timeline/test/head.js @@ -104,4 +104,45 @@ function waitUntil(predicate, interval = 10) { waitUntil(predicate).then(() => deferred.resolve(true)); }, interval); return deferred.promise; + +} + +/** + * Wait until next tick. + */ +function nextTick() { + let def = promise.defer(); + executeSoon(() => def.resolve()) + return def.promise; +} + +/** + * Wait for eventName on target. + * @param {Object} target An observable object that either supports on/off or + * addEventListener/removeEventListener + * @param {String} eventName + * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener + * @return A promise that resolves when the event has been handled + */ +function once(target, eventName, useCapture=false) { + info("Waiting for event: '" + eventName + "' on " + target + "."); + + let deferred = promise.defer(); + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in target) && (remove in target)) { + target[add](eventName, function onEvent(...aArgs) { + info("Got event: '" + eventName + "' on " + target + "."); + target[remove](eventName, onEvent, useCapture); + deferred.resolve.apply(deferred, aArgs); + }, useCapture); + break; + } + } + + return deferred.promise; } diff --git a/browser/devtools/timeline/timeline.js b/browser/devtools/timeline/timeline.js index ea94dac0c44..68fc4e7fb95 100644 --- a/browser/devtools/timeline/timeline.js +++ b/browser/devtools/timeline/timeline.js @@ -7,6 +7,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/devtools/Loader.jsm"); +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); devtools.lazyRequireGetter(this, "promise"); devtools.lazyRequireGetter(this, "EventEmitter", @@ -20,6 +21,8 @@ devtools.lazyRequireGetter(this, "Waterfall", "devtools/timeline/waterfall", true); devtools.lazyRequireGetter(this, "MarkerDetails", "devtools/timeline/marker-details", true); +devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT", + "devtools/timeline/global", true); devtools.lazyImporter(this, "CanvasGraphUtils", "resource:///modules/devtools/Graphs.jsm"); @@ -30,6 +33,14 @@ devtools.lazyImporter(this, "PluralForm", const OVERVIEW_UPDATE_INTERVAL = 200; const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15; +/** + * Preference for devtools.timeline.hiddenMarkers. + * Stores which markers should be hidden. + */ +const Prefs = new ViewHelpers.Prefs("devtools.timeline", { + hiddenMarkers: ["Json", "hiddenMarkers"] +}); + // The panel's window global is an EventEmitter firing the following events: const EVENTS = { // When a recording is started or stopped, via the `stopwatch` button. @@ -277,8 +288,9 @@ let TimelineView = { * Initialization function, called when the tool is started. */ initialize: Task.async(function*() { - this.markersOverview = new MarkersOverview($("#markers-overview")); - this.waterfall = new Waterfall($("#timeline-waterfall"), $("#timeline-pane")); + let blueprint = this._getFilteredBluePrint(); + this.markersOverview = new MarkersOverview($("#markers-overview"), blueprint); + this.waterfall = new Waterfall($("#timeline-waterfall"), $("#timeline-pane"), blueprint); this.markerDetails = new MarkerDetails($("#timeline-waterfall-details"), $("#timeline-waterfall-container > splitter")); this._onSelecting = this._onSelecting.bind(this); @@ -292,7 +304,10 @@ let TimelineView = { this.waterfall.on("unselected", this._onMarkerSelected); yield this.markersOverview.ready(); + yield this.waterfall.recalculateBounds(); + + this._buildFilterPopup(); }), /** @@ -465,7 +480,101 @@ let TimelineView = { _onRefresh: function() { this.waterfall.recalculateBounds(); this.updateWaterfall(); - } + }, + + /** + * Rebuild a blueprint without hidden markers. + */ + _getFilteredBluePrint: function() { + let hiddenMarkers = Prefs.hiddenMarkers; + let filteredBlueprint = Cu.cloneInto(TIMELINE_BLUEPRINT, {}); + 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 removedGroup of maybeRemovedGroups) { + let markerNames = Object.keys(filteredBlueprint); + let allGroupsRemoved = markerNames.every(e => filteredBlueprint[e].group != removedGroup); + if (allGroupsRemoved) { + removedGroups.add(removedGroup); + } + } + + // 3. Offset groups. + + for (let removedGroup of removedGroups) { + for (let [, markerDetails] of Iterator(filteredBlueprint)) { + if (markerDetails.group > removedGroup) { + markerDetails.group--; + } + } + } + + return filteredBlueprint; + + }, + + /** + * When the list of hidden markers changes, update waterfall + * and overview. + */ + _onHiddenMarkersChanged: function(e) { + let menuItems = $$("#timelineFilterPopup menuitem[marker-type]:not([checked])"); + let hiddenMarkers = Array.map(menuItems, e => e.getAttribute("marker-type")); + + Prefs.hiddenMarkers = hiddenMarkers; + let blueprint = this._getFilteredBluePrint(); + + this.waterfall.setBlueprint(blueprint); + this.updateWaterfall(); + + this.markersOverview.setBlueprint(blueprint); + this.markersOverview.refresh({ force: true }); + }, + + /** + * Creates the filter popup. + */ + _buildFilterPopup: function() { + let popup = $("#timelineFilterPopup"); + let button = $("#filter-button"); + + popup.addEventListener("popupshowing", () => button.setAttribute("open", "true")); + popup.addEventListener("popuphiding", () => button.removeAttribute("open")); + + this._onHiddenMarkersChanged = this._onHiddenMarkersChanged.bind(this); + + for (let [markerName, markerDetails] of Iterator(TIMELINE_BLUEPRINT)) { + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("closemenu", "none"); + menuitem.setAttribute("type", "checkbox"); + menuitem.setAttribute("marker-type", markerName); + menuitem.setAttribute("label", markerDetails.label); + menuitem.setAttribute("flex", "1"); + menuitem.setAttribute("align", "center"); + + menuitem.addEventListener("command", this._onHiddenMarkersChanged); + + if (Prefs.hiddenMarkers.indexOf(markerName) == -1) { + menuitem.setAttribute("checked", "true"); + } + + // Style used by pseudo element ::before in timeline.css.in + let bulletStyle = `--bullet-bg: ${markerDetails.fill};` + bulletStyle += `--bullet-border: ${markerDetails.stroke}`; + menuitem.setAttribute("style", bulletStyle); + + popup.appendChild(menuitem); + } + }, }; /** diff --git a/browser/devtools/timeline/timeline.xul b/browser/devtools/timeline/timeline.xul index 56df912b0a3..2d4b8e1939e 100644 --- a/browser/devtools/timeline/timeline.xul +++ b/browser/devtools/timeline/timeline.xul @@ -17,6 +17,10 @@