Bug 1087877 - [timeline] User should be able to filter out any type of marker. r=vporof
☠☠ backed out by 3daf1f629233 ☠ ☠
authorPaul Rouget <paul@mozilla.com>
Mon, 15 Dec 2014 05:07:00 -0500
changeset 219740 cab12d762123475d5d2f8a04ff5230c19273f29b
parent 219739 0011a1d087cae5fc785df59fb18b955e12e03144
child 219741 9772a3da28b8955c9e39c1a10f93bf8c30a12e82
push id10401
push userryanvm@gmail.com
push dateMon, 15 Dec 2014 19:19:53 +0000
treeherderfx-team@9772a3da28b8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof
bugs1087877
milestone37.0a1
Bug 1087877 - [timeline] User should be able to filter out any type of marker. r=vporof
browser/app/profile/firefox.js
browser/devtools/shared/widgets/Graphs.jsm
browser/devtools/timeline/test/browser.ini
browser/devtools/timeline/test/browser_timeline_filters.js
browser/devtools/timeline/test/head.js
browser/devtools/timeline/timeline.js
browser/devtools/timeline/timeline.xul
browser/devtools/timeline/widgets/markers-overview.js
browser/devtools/timeline/widgets/waterfall.js
browser/locales/en-US/chrome/browser/devtools/timeline.dtd
browser/themes/linux/jar.mn
browser/themes/osx/jar.mn
browser/themes/shared/devtools/images/timeline-filter.svg
browser/themes/shared/devtools/timeline.inc.css
browser/themes/shared/devtools/toolbars.inc.css
browser/themes/windows/jar.mn
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1424,16 +1424,17 @@ pref("devtools.debugger.ui.variables-sea
 
 // Enable the Profiler and the Timeline
 pref("devtools.profiler.enabled", true);
 #ifdef MOZ_DEV_EDITION
 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
   pref("devtools.performance_dev.enabled", true);
 #else
   pref("devtools.performance_dev.enabled", false);
 #endif
 
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -611,24 +611,29 @@ AbstractCanvasGraph.prototype = {
     let { x } = this._cursor;
     return this._regions.find(({ start, end }) =>
       (start < end && start < x && end > x) ||
       (start > end && end < x && start > x));
   },
 
   /**
    * 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;
     }
 
     bounds.width = newWidth;
     bounds.height = newHeight;
     this._iframe.setAttribute("width", bounds.width);
--- a/browser/devtools/timeline/test/browser.ini
+++ b/browser/devtools/timeline/test/browser.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 subsuite = devtools
 support-files =
   doc_simple-test.html
   head.js
 
 [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]
 [browser_timeline_panels.js]
 [browser_timeline_recording-without-memory.js]
 [browser_timeline_recording.js]
 [browser_timeline_waterfall-background.js]
 [browser_timeline_waterfall-generic.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_filters.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests markers filtering mechanism.
+ */
+
+let test = Task.async(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;
+
+  // Select everything
+  overview.setSelection({ start: 0, end: overview.width })
+
+  $("#filter-button").click();
+
+  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");
+  ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker");
+  ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker");
+
+  let heightBefore = overview.fixedHeight;
+  EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin);
+  yield once(menuItem1, "command");
+  // 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");
+  ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker");
+  ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker");
+
+  heightBefore = overview.fixedHeight;
+  EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin);
+  yield once(menuItem2, "command");
+  is(overview.fixedHeight, heightBefore - 11, "Overview is smaller");
+  ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker");
+  ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker");
+  ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker");
+
+  heightBefore = overview.fixedHeight;
+  EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin);
+  yield once(menuItem3, "command");
+  is(overview.fixedHeight, heightBefore - 11, "Overview is smaller");
+  ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker");
+  ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker");
+  ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker");
+
+  for (let item of [menuItem1, menuItem2, menuItem3]) {
+    EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin);
+    yield once(item, "command");
+  }
+
+  ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker");
+  ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker");
+  ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker");
+
+  is(overview.fixedHeight, originalHeight, "Overview restored");
+
+  $(".waterfall-marker-bar[type=Styles]");
+
+  yield teardown(panel);
+  finish();
+});
--- a/browser/devtools/timeline/test/head.js
+++ b/browser/devtools/timeline/test/head.js
@@ -124,9 +124,50 @@ function waitUntil(predicate, interval =
   if (predicate()) {
     return promise.resolve(true);
   }
   let deferred = promise.defer();
   setTimeout(function() {
     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;
+}
--- a/browser/devtools/timeline/timeline.js
+++ b/browser/devtools/timeline/timeline.js
@@ -2,39 +2,50 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { 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",
   "devtools/toolkit/event-emitter");
 
 devtools.lazyRequireGetter(this, "MarkersOverview",
   "devtools/timeline/markers-overview", true);
 devtools.lazyRequireGetter(this, "MemoryOverview",
   "devtools/timeline/memory-overview", true);
 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");
 
 devtools.lazyImporter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
 
 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.
   RECORDING_STARTED: "Timeline:RecordingStarted",
   RECORDING_ENDED: "Timeline:RecordingEnded",
 
   // When the overview graphs are populated with new markers.
   OVERVIEW_UPDATED: "Timeline:OverviewUpdated",
@@ -245,32 +256,36 @@ let TimelineController = {
 /**
  * Functions handling the timeline frontend view.
  */
 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);
     this._onRefresh = this._onRefresh.bind(this);
     this.markersOverview.on("selecting", this._onSelecting);
     this.markersOverview.on("refresh", this._onRefresh);
     this.markerDetails.on("resize", this._onRefresh);
 
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this.waterfall.on("selected", this._onMarkerSelected);
     this.waterfall.on("unselected", this._onMarkerSelected);
 
     yield this.markersOverview.ready();
+
     yield this.waterfall.recalculateBounds();
+
+    this._buildFilterPopup();
   }),
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function() {
     this.markerDetails.off("resize", this._onRefresh);
     this.markerDetails.destroy();
@@ -429,17 +444,111 @@ let TimelineView = {
   },
 
   /**
    * Callback handling the "refresh" event on the timeline overview.
    */
   _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);
+    }
+  },
 };
 
 /**
  * Convenient way of emitting events from the panel window.
  */
 EventEmitter.decorate(this);
 
 /**
--- a/browser/devtools/timeline/timeline.xul
+++ b/browser/devtools/timeline/timeline.xul
@@ -12,26 +12,34 @@
   <!ENTITY % timelineDTD SYSTEM "chrome://browser/locale/devtools/timeline.dtd">
   %timelineDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script src="chrome://browser/content/devtools/theme-switching.js"/>
   <script type="application/javascript" src="timeline.js"/>
 
+  <popupset id="timelinePopupset">
+    <menupopup id="timelineFilterPopup" position="after_start"/>
+  </popupset>
+
   <vbox class="theme-body" flex="1">
     <toolbar id="timeline-toolbar"
              class="devtools-toolbar">
       <hbox id="recordings-controls"
             class="devtools-toolbarbutton-group"
             align="center">
         <toolbarbutton id="record-button"
                        class="devtools-toolbarbutton"
                        oncommand="TimelineController.toggleRecording()"
                        tooltiptext="&timelineUI.recordButton.tooltip;"/>
+        <toolbarbutton id="filter-button"
+                       popup="timelineFilterPopup"
+                       class="devtools-toolbarbutton"
+                       tooltiptext="&timelineUI.filterButton.tooltip;"/>
         <checkbox id="memory-checkbox"
                   label="&timelineUI.memoryCheckbox.label;"
                   oncommand="TimelineController.updateMemoryRecording()"
                   tooltiptext="&timelineUI.memoryCheckbox.tooltip;"/>
         <label id="record-label"
                value="&timelineUI.recordLabel;"/>
       </hbox>
     </toolbar>
--- a/browser/devtools/timeline/widgets/markers-overview.js
+++ b/browser/devtools/timeline/widgets/markers-overview.js
@@ -11,23 +11,21 @@
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource:///modules/devtools/Graphs.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
-loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
-  "devtools/timeline/global", true);
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const OVERVIEW_HEADER_HEIGHT = 14; // px
-const OVERVIEW_BODY_HEIGHT = 55; // 11px * 5 groups
+const OVERVIEW_ROW_HEIGHT = 11; // row height
 
 const OVERVIEW_BACKGROUND_COLOR = "#fff";
 const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
 const OVERVIEW_SELECTION_LINE_COLOR = "#555";
 const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
 const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
 
 const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
@@ -45,42 +43,45 @@ const OVERVIEW_MARKER_DURATION_MIN = 4; 
 const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
 const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)";
 
 /**
  * An overview for the markers data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
+ * @param Object blueprint
+ *        List of names and colors defining markers.
  */
-function MarkersOverview(parent, ...args) {
+function MarkersOverview(parent, blueprint, ...args) {
   AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
+
+  // Set the list of names, properties and colors used to paint this overview.
+  this.setBlueprint(blueprint);
+
   this.once("ready", () => {
-    // Set the list of names, properties and colors used to paint this overview.
-    this.setBlueprint(TIMELINE_BLUEPRINT);
-
     // Populate this overview with some dummy initial data.
     this.setData({ interval: { startTime: 0, endTime: 1000 }, markers: [] });
   });
 }
 
 MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
   clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
   selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
   selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
   selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
   headerHeight: OVERVIEW_HEADER_HEIGHT,
-  bodyHeight: OVERVIEW_BODY_HEIGHT,
+  rowHeight: OVERVIEW_ROW_HEIGHT,
   groupPadding: OVERVIEW_GROUP_VERTICAL_PADDING,
 
   /**
    * Compute the height of the overview.
    */
   get fixedHeight() {
-    return this.headerHeight + this.bodyHeight;
+    return this.headerHeight + this.rowHeight * (this._lastGroup + 1);
   },
 
   /**
    * List of names and colors used to paint this overview.
    * @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
    */
   setBlueprint: function(blueprint) {
     this._paintBatches = new Map();
@@ -114,24 +115,27 @@ MarkersOverview.prototype = Heritage.ext
     let canvasHeight = this._height;
     let safeBounds = OVERVIEW_HEADER_SAFE_BOUNDS * this._pixelRatio;
     let availableWidth = canvasWidth - safeBounds;
 
     // Group markers into separate paint batches. This is necessary to
     // draw all markers sharing the same style at once.
 
     for (let marker of markers) {
-      this._paintBatches.get(marker.name).batch.push(marker);
+      let markerType = this._paintBatches.get(marker.name);
+      if (markerType) {
+        markerType.batch.push(marker);
+      }
     }
 
     // Calculate each group's height, and the time-based scaling.
 
     let totalGroups = this._lastGroup + 1;
     let headerHeight = this.headerHeight * this._pixelRatio;
-    let groupHeight = this.bodyHeight * this._pixelRatio / totalGroups;
+    let groupHeight = this.rowHeight * this._pixelRatio;
     let groupPadding = this.groupPadding * this._pixelRatio;
 
     let totalTime = (endTime - startTime) || 0;
     let dataScale = this.dataScaleX = availableWidth / totalTime;
 
     // Draw the header and overview background.
 
     ctx.fillStyle = OVERVIEW_HEADER_BACKGROUND;
--- a/browser/devtools/timeline/widgets/waterfall.js
+++ b/browser/devtools/timeline/widgets/waterfall.js
@@ -7,18 +7,16 @@
  * This file contains the "waterfall" view, essentially a detailed list
  * of all the markers in the timeline data.
  */
 
 const {Ci, Cu} = require("chrome");
 
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
-loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
-  "devtools/timeline/global", true);
 
 loader.lazyImporter(this, "setNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 loader.lazyImporter(this, "clearNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
@@ -45,18 +43,20 @@ const WATERFALL_ROWCOUNT_ONPAGEUPDOWN = 
 
 /**
  * A detailed waterfall view for the timeline data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the waterfall.
  * @param nsIDOMNode container
  *        The container node that key events should be bound to.
+ * @param Object blueprint
+ *        List of names and colors defining markers.
  */
-function Waterfall(parent, container) {
+function Waterfall(parent, container, blueprint) {
   EventEmitter.decorate(this);
 
   this._parent = parent;
   this._document = parent.ownerDocument;
   this._container = container;
   this._fragment = this._document.createDocumentFragment();
   this._outstandingMarkers = [];
 
@@ -70,17 +70,17 @@ function Waterfall(parent, container) {
   this._parent.appendChild(this._listContents);
 
   this.setupKeys();
 
   this._isRTL = this._getRTL();
 
   // Lazy require is a bit slow, and these are hot objects.
   this._l10n = L10N;
-  this._blueprint = TIMELINE_BLUEPRINT;
+  this._blueprint = blueprint
   this._setNamedTimeout = setNamedTimeout;
   this._clearNamedTimeout = clearNamedTimeout;
 
   // Selected row index. By default, we want the first
   // row to be selected.
   this._selectedRowIdx = 0;
 
   // Default rowCount
@@ -116,16 +116,24 @@ Waterfall.prototype = {
 
     // Label the header as if the first possible marker was at T=0.
     this._buildHeader(this._headerContents, startTime - timeEpoch, dataScale);
     this._buildMarkers(this._listContents, markers, startTime, endTime, dataScale);
     this.selectRow(this._selectedRowIdx);
   },
 
   /**
+   * List of names and colors used to paint markers.
+   * @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
+   */
+  setBlueprint: function(blueprint) {
+    this._blueprint = blueprint;
+  },
+
+  /**
    * Keybindings.
    */
   setupKeys: function() {
     let pane = this._container;
     pane.parentNode.parentNode.addEventListener("keydown", e => {
       if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP) {
         e.preventDefault();
         this.selectNearestRow(this._selectedRowIdx - 1);
@@ -246,16 +254,20 @@ Waterfall.prototype = {
     let rowsCount = 0;
     let markerIdx = -1;
 
     for (let marker of markers) {
       markerIdx++;
       if (!isMarkerInRange(marker, startTime, endTime)) {
         continue;
       }
+      if (!(marker.name in this._blueprint)) {
+        continue;
+      }
+
       // Only build and display a finite number of markers initially, to
       // preserve a snappy UI. After a certain delay, continue building the
       // outstanding markers while there's (hopefully) no user interaction.
       let arguments_ = [this._fragment, marker, startTime, dataScale, markerIdx, rowsCount];
       if (rowsCount++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
         this._buildMarker.apply(this, arguments_);
       } else {
         this._outstandingMarkers.push(arguments_);
--- a/browser/locales/en-US/chrome/browser/devtools/timeline.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.dtd
@@ -14,25 +14,29 @@
 <!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
   -  on a button that starts a new recording. -->
 <!ENTITY timelineUI.recordButton.tooltip "Record timeline operations">
 
 <!-- LOCALIZATION NOTE (timelineUI.recordLabel): This string is displayed
   -  as a label to signal that a recording is in progress. -->
 <!ENTITY timelineUI.recordLabel "Recording…">
 
-<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.label): This string
+<!-- LOCALIZATION NOTE (timelineUI.memoryCheckbox.label): This string
   -  is displayed next to a checkbox determining whether or not memory
   -  measurements are enabled. -->
 <!ENTITY timelineUI.memoryCheckbox.label "Memory">
 
-<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.tooltip): This string
+<!-- LOCALIZATION NOTE (timelineUI.memoryCheckbox.tooltip): This string
   -  is displayed next to the memory checkbox -->
 <!ENTITY timelineUI.memoryCheckbox.tooltip "Enable memory measurements">
 
+<!-- LOCALIZATION NOTE (timelineUI.filterButton.tooltip): This string
+  -  is displayed next to the filter button-->
+<!ENTITY timelineUI.filterButton.tooltip "Select what data to display">
+
 <!-- LOCALIZATION NOTE (timelineUI.emptyNotice1/2): This is the label shown
   -  in the timeline view when empty. -->
 <!ENTITY timelineUI.emptyNotice1    "Click on the">
 <!ENTITY timelineUI.emptyNotice2    "button to start recording timeline events.">
 
 <!-- LOCALIZATION NOTE (timelineUI.stopNotice1/2): This is the label shown
   -  in the timeline view while recording. -->
 <!ENTITY timelineUI.stopNotice1    "Click on the">
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -265,16 +265,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
 * skin/classic/browser/devtools/canvasdebugger.css    (devtools/canvasdebugger.css)
 * skin/classic/browser/devtools/debugger.css          (devtools/debugger.css)
   skin/classic/browser/devtools/eyedropper.css        (../shared/devtools/eyedropper.css)
 * skin/classic/browser/devtools/netmonitor.css        (devtools/netmonitor.css)
 * skin/classic/browser/devtools/profiler.css          (devtools/profiler.css)
 * skin/classic/browser/devtools/performance.css       (devtools/performance.css)
 * skin/classic/browser/devtools/timeline.css          (devtools/timeline.css)
+  skin/classic/browser/devtools/timeline-filter.svg   (../shared/devtools/images/timeline-filter.svg)
 * skin/classic/browser/devtools/scratchpad.css        (devtools/scratchpad.css)
 * skin/classic/browser/devtools/shadereditor.css      (devtools/shadereditor.css)
 * skin/classic/browser/devtools/splitview.css         (../shared/devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css       (../shared/devtools/styleeditor.css)
   skin/classic/browser/devtools/storage.css           (../shared/devtools/storage.css)
 * skin/classic/browser/devtools/webaudioeditor.css    (devtools/webaudioeditor.css)
   skin/classic/browser/devtools/magnifying-glass.png        (../shared/devtools/images/magnifying-glass.png)
   skin/classic/browser/devtools/magnifying-glass@2x.png     (../shared/devtools/images/magnifying-glass@2x.png)
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -391,16 +391,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
 * skin/classic/browser/devtools/canvasdebugger.css          (devtools/canvasdebugger.css)
 * skin/classic/browser/devtools/debugger.css                (devtools/debugger.css)
   skin/classic/browser/devtools/eyedropper.css              (../shared/devtools/eyedropper.css)
 * skin/classic/browser/devtools/netmonitor.css              (devtools/netmonitor.css)
 * skin/classic/browser/devtools/profiler.css                (devtools/profiler.css)
 * skin/classic/browser/devtools/performance.css             (devtools/performance.css)
 * skin/classic/browser/devtools/timeline.css                (devtools/timeline.css)
+  skin/classic/browser/devtools/timeline-filter.svg         (../shared/devtools/images/timeline-filter.svg)
 * skin/classic/browser/devtools/scratchpad.css              (devtools/scratchpad.css)
 * skin/classic/browser/devtools/shadereditor.css            (devtools/shadereditor.css)
 * skin/classic/browser/devtools/splitview.css               (../shared/devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css             (../shared/devtools/styleeditor.css)
   skin/classic/browser/devtools/storage.css                 (../shared/devtools/storage.css)
 * skin/classic/browser/devtools/webaudioeditor.css          (devtools/webaudioeditor.css)
   skin/classic/browser/devtools/magnifying-glass.png        (../shared/devtools/images/magnifying-glass.png)
   skin/classic/browser/devtools/magnifying-glass@2x.png     (../shared/devtools/images/magnifying-glass@2x.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devtools/images/timeline-filter.svg
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink"
+     x="0px" y="0px"
+     width="16" height="16"
+     viewBox="0 0 16 16"
+     enable-background="new 0 0 16 16"
+     xml:space="preserve">
+<style>
+use:not(:target) {
+  display: none;
+}
+
+use {
+  fill: #EDF0F1;
+}
+
+use[id$="-disabled"] {
+  fill-opacity: 0.5;
+}
+
+use[id$="-open"] {
+  fill: #3BACE5;
+}
+
+</style>
+<defs style="display:none">
+  <path id="filter-shape"
+         d="M 2,2 v 3 l 5,4 v 6 h 2 v -6 l 5,-4 v -3 L 14,2 z"/>
+</defs>
+<use id="filter"              xlink:href="#filter-shape"/>
+<use id="filter-disabled"     xlink:href="#filter-shape"/>
+<use id="filter-open"         xlink:href="#filter-shape"/>
+</svg>
--- a/browser/themes/shared/devtools/timeline.inc.css
+++ b/browser/themes/shared/devtools/timeline.inc.css
@@ -1,25 +1,55 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 #record-button {
   list-style-image: url(profiler-stopwatch.svg);
+  min-width: 24px;
 }
 
 #record-button[checked] {
   list-style-image: url(profiler-stopwatch-checked.svg);
 }
 
 #record-button:not([checked]) ~ #record-label {
   visibility: hidden;
 }
 
+#memory-checkbox .checkbox-label {
+  line-height: 100%;
+}
+
+#filter-button {
+  list-style-image: url(timeline-filter.svg#filter);
+  min-width: 24px;
+}
+
+#filter-button[disabled] {
+  list-style-image: url(timeline-filter.svg#filter-disabled);
+}
+
+#filter-button[open] {
+  list-style-image: url(timeline-filter.svg#filter-open);
+}
+
+#timelineFilterPopup > menuitem:before {
+  content: "";
+  display: block;
+  width: 8px;
+  height: 8px;
+  margin: 0 8px;
+  border: 1px solid;
+  border-radius: 1px;
+  background-color: var(--bullet-bg);
+  border-color: var(--bullet-border);
+}
+
 .notice-container {
   font-size: 120%;
   padding-bottom: 35vh;
 }
 
 .theme-dark .notice-container {
   background: #343c45; /* Toolbars */
   color: #f5f7fa; /* Light foreground text */
@@ -180,17 +210,16 @@
   -moz-padding-end: 8px;
   padding-top: 8vh;
   overflow: auto;
 }
 
 .marker-details-bullet {
   width: 8px;
   height: 8px;
-  margin: 0 8px;
   border: 1px solid;
   border-radius: 1px;
 }
 
 .marker-details-start,
 .marker-details-end,
 .marker-details-duration {
   padding-top: 3px;
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -156,35 +156,35 @@
   background-color: rgba(170, 170, 170, .2); /* Splitter */
 }
 .theme-dark .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
 .theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image] {
   background-color: rgba(0, 0, 0, .2); /* Splitter */
 }
 
 /* Button States */
-.theme-dark .devtools-toolbarbutton:hover,
-.theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image]:hover,
-.theme-dark .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]):hover {
+.theme-dark .devtools-toolbarbutton:not([disabled]):hover,
+.theme-dark #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover,
+.theme-dark .devtools-toolbarbutton:not([disabled])[label]:not([text-as-image]):not([type=menu-button]):hover {
   background: rgba(0, 0, 0, .3); /* Splitters */
 }
-.theme-light .devtools-toolbarbutton:hover,
-.theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image]:hover,
-.theme-light .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]):hover {
+.theme-light .devtools-toolbarbutton:not([disabled]):hover,
+.theme-light #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover,
+.theme-light .devtools-toolbarbutton:not([disabled])[label]:not([text-as-image]):not([type=menu-button]):hover {
   background: rgba(170, 170, 170, .3); /* Splitters */
 }
 
-.theme-dark .devtools-toolbarbutton:hover:active,
-.theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image]:hover:active,
-.theme-dark .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]):hover:active {
+.theme-dark .devtools-toolbarbutton:not([disabled]):hover:active,
+.theme-dark #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover:active,
+.theme-dark .devtools-toolbarbutton:not([disabled])[label]:not([text-as-image]):not([type=menu-button]):hover:active {
   background: rgba(0, 0, 0, .4); /* Splitters */
 }
-.theme-light .devtools-toolbarbutton:hover:active,
-.theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image]:hover:active,
-.theme-light .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]):hover:active {
+.theme-light .devtools-toolbarbutton:not([disabled]):hover:active,
+.theme-light #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover:active,
+.theme-light .devtools-toolbarbutton:not([disabled])[label]:not([text-as-image]):not([type=menu-button]):hover:active {
   background: rgba(170, 170, 170, .4); /* Splitters */
 }
 
 /* Menu type buttons and checked states */
 .theme-dark .devtools-toolbarbutton[checked=true],
 .theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image][checked] {
   background: rgba(29, 79, 115, .7); /* Select highlight blue */
   color: var(--theme-selection-color);
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -301,16 +301,17 @@ browser.jar:
         skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
         skin/classic/browser/devtools/eyedropper.css                (../shared/devtools/eyedropper.css)
 *       skin/classic/browser/devtools/canvasdebugger.css            (devtools/canvasdebugger.css)
 *       skin/classic/browser/devtools/debugger.css                  (devtools/debugger.css)
 *       skin/classic/browser/devtools/netmonitor.css                (devtools/netmonitor.css)
 *       skin/classic/browser/devtools/profiler.css                  (devtools/profiler.css)
 *       skin/classic/browser/devtools/performance.css               (devtools/performance.css)
 *       skin/classic/browser/devtools/timeline.css                  (devtools/timeline.css)
+        skin/classic/browser/devtools/timeline-filter.svg           (../shared/devtools/images/timeline-filter.svg)
 *       skin/classic/browser/devtools/scratchpad.css                (devtools/scratchpad.css)
 *       skin/classic/browser/devtools/shadereditor.css              (devtools/shadereditor.css)
         skin/classic/browser/devtools/storage.css                   (../shared/devtools/storage.css)
 *       skin/classic/browser/devtools/splitview.css                 (../shared/devtools/splitview.css)
         skin/classic/browser/devtools/styleeditor.css               (../shared/devtools/styleeditor.css)
 *       skin/classic/browser/devtools/webaudioeditor.css            (devtools/webaudioeditor.css)
         skin/classic/browser/devtools/magnifying-glass.png          (../shared/devtools/images/magnifying-glass.png)
         skin/classic/browser/devtools/magnifying-glass@2x.png       (../shared/devtools/images/magnifying-glass@2x.png)
@@ -748,16 +749,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
 *       skin/classic/aero/browser/devtools/canvasdebugger.css        (devtools/canvasdebugger.css)
 *       skin/classic/aero/browser/devtools/debugger.css              (devtools/debugger.css)
         skin/classic/aero/browser/devtools/eyedropper.css            (../shared/devtools/eyedropper.css)
 *       skin/classic/aero/browser/devtools/netmonitor.css            (devtools/netmonitor.css)
 *       skin/classic/aero/browser/devtools/profiler.css              (devtools/profiler.css)
 *       skin/classic/aero/browser/devtools/performance.css           (devtools/performance.css)
 *       skin/classic/aero/browser/devtools/timeline.css              (devtools/timeline.css)
+        skin/classic/aero/browser/devtools/timeline-filter.svg       (../shared/devtools/images/timeline-filter.svg)
 *       skin/classic/aero/browser/devtools/scratchpad.css            (devtools/scratchpad.css)
 *       skin/classic/aero/browser/devtools/shadereditor.css          (devtools/shadereditor.css)
 *       skin/classic/aero/browser/devtools/splitview.css             (../shared/devtools/splitview.css)
         skin/classic/aero/browser/devtools/styleeditor.css           (../shared/devtools/styleeditor.css)
         skin/classic/aero/browser/devtools/storage.css               (../shared/devtools/storage.css)
 *       skin/classic/aero/browser/devtools/webaudioeditor.css        (devtools/webaudioeditor.css)
         skin/classic/aero/browser/devtools/magnifying-glass.png      (../shared/devtools/images/magnifying-glass.png)
         skin/classic/aero/browser/devtools/magnifying-glass@2x.png   (../shared/devtools/images/magnifying-glass@2x.png)