Bug 1157009 - Redesign about:performance. r=felipe
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Wed, 01 Jul 2015 21:44:40 +0200
changeset 287067 9940e2af9ed70d951c95c418bd1a77341477cdb7
parent 287066 0a51403a45e98730384f18b580c3ca97887db651
child 287068 7e990e527c4a8abdc1fdfdd124dd0541331ba687
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe
bugs1157009
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1157009 - Redesign about:performance. r=felipe
toolkit/components/aboutperformance/content/aboutPerformance.js
toolkit/components/aboutperformance/content/aboutPerformance.xhtml
toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js
--- a/toolkit/components/aboutperformance/content/aboutPerformance.js
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.js
@@ -12,328 +12,782 @@ const { AddonManager } = Cu.import("reso
 const { AddonWatcher } = Cu.import("resource://gre/modules/AddonWatcher.jsm", {});
 const { PerformanceStats } = Cu.import("resource://gre/modules/PerformanceStats.jsm", {});
 const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 // about:performance observes notifications on this topic.
 // if a notification is sent, this causes the page to be updated immediately,
 // regardless of whether the page is on pause.
-const UPDATE_IMMEDIATELY_TOPIC = "about:performance-update-immediately";
+const TEST_DRIVER_TOPIC = "test-about:performance-test-driver";
 
 // about:performance posts notifications on this topic whenever the page
 // is updated.
 const UPDATE_COMPLETE_TOPIC = "about:performance-update-complete";
 
-/**
- * The various measures we display.
- */
-const MEASURES = [
-  {probe: "jank", key: "longestDuration", percentOfDeltaT: false, label: "Jank level"},
-  {probe: "jank", key: "totalUserTime", percentOfDeltaT: true, label: "User (%)"},
-  {probe: "jank", key: "totalSystemTime", percentOfDeltaT: true, label: "System (%)"},
-  {probe: "cpow", key: "totalCPOWTime", percentOfDeltaT: true, label: "Cross-Process (%)"},
-  {probe: "ticks",key: "ticks", percentOfDeltaT: false, label: "Activations"},
-];
+// How often we should add a sample to our buffer.
+const BUFFER_SAMPLING_RATE_MS = 1000;
+
+// The age of the oldest sample to keep.
+const BUFFER_DURATION_MS = 10000;
+
+// How often we should update
+const UPDATE_INTERVAL_MS = 5000;
+
+// The name of the application
+const BRAND_BUNDLE = Services.strings.createBundle(
+  "chrome://branding/locale/brand.properties");
+const BRAND_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName");
+
+// The maximal number of items to display before showing a "Show All"
+// button.
+const MAX_NUMBER_OF_ITEMS_TO_DISPLAY = 3;
+
+// If the frequency of alerts is below this value,
+// we consider that the feature has no impact.
+const MAX_FREQUENCY_FOR_NO_IMPACT = .05;
+// If the frequency of alerts is above `MAX_FREQUENCY_FOR_NO_IMPACT`
+// and below this value, we consider that the feature impacts the
+// user rarely.
+const MAX_FREQUENCY_FOR_RARE = .1;
+// If the frequency of alerts is above `MAX_FREQUENCY_FOR_FREQUENT`
+// and below this value, we consider that the feature impacts the
+// user frequently. Anything above is consider permanent.
+const MAX_FREQUENCY_FOR_FREQUENT = .5;
+
+// If the number of high-impact alerts among all alerts is above
+// this value, we consider that the feature has a major impact
+// on user experience.
+const MIN_PROPORTION_FOR_MAJOR_IMPACT = .05;
+// Otherwise and if the number of medium-impact alerts among all
+// alerts is above this value, we consider that the feature has
+// a noticeable impact on user experience.
+const MIN_PROPORTION_FOR_NOTICEABLE_IMPACT = .1;
+
+// The current mode. Either `MODE_GLOBAL` to display a summary of results
+// since we opened about:performance or `MODE_RECENT` to display the latest
+// BUFFER_DURATION_MS ms.
+const MODE_GLOBAL = "global";
+const MODE_RECENT = "recent";
 
 /**
- * Used to control the live updates in the performance page.
+ * Find the <xul:tab> for a window id.
+ *
+ * This is useful e.g. for reloading or closing tabs.
+ *
+ * @return null If the xul:tab could not be found, e.g. if the
+ * windowId is that of a chrome window.
+ * @return {{tabbrowser: <xul:tabbrowser>, tab: <xul.tab>}} The
+ * tabbrowser and tab if the latter could be found.
  */
-let AutoUpdate = {
+function findTabFromWindow(windowId) {
+  let windows = Services.wm.getEnumerator("navigator:browser");
+  while (windows.hasMoreElements()) {
+    let win = windows.getNext();
+    let tabbrowser = win.gBrowser;
+    let foundBrowser = tabbrowser.getBrowserForOuterWindowID(windowId);
+    if (foundBrowser) {
+      return {tabbrowser, tab: tabbrowser.getTabForBrowser(foundBrowser)};
+    }
+  }
+  return null;
+}
 
+/**
+ * Utilities for dealing with PerformanceDiff
+ */
+let Delta = {
+  compare: function(a, b) {
+    // By definition, if either `a` or `b` has CPOW time and the other
+    // doesn't, it is larger.
+    if (a.cpow.totalCPOWTime && !b.cpow.totalCPOWTime) {
+      return 1;
+    }
+    if (b.cpow.totalCPOWTime && !a.cpow.totalCPOWTime) {
+      return -1;
+    }
+    return (
+      (a.jank.longestDuration - b.jank.longestDuration) ||
+      (a.jank.totalUserTime - b.jank.totalUserTime) ||
+      (a.jank.totalSystemTime - b.jank.totalSystemTime) ||
+      (a.cpow.totalCPOWTime - b.cpow.totalCPOWTime) ||
+      (a.ticks.ticks - b.ticks.ticks) ||
+      0
+    );
+  },
+  revCompare: function(a, b) {
+    return -Delta.compare(a, b);
+  },
   /**
-   * The timer that is created when setInterval is called.
+   * The highest value considered "good performance".
+   */
+  MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE: {
+    cpow: {
+      totalCPOWTime: 0,
+    },
+    jank: {
+      longestDuration: 3,
+      totalUserTime: Number.POSITIVE_INFINITY,
+      totalSystemTime: Number.POSITIVE_INFINITY
+    },
+    ticks: {
+      ticks: Number.POSITIVE_INFINITY,
+    }
+  },
+  /**
+   * The highest value considered "average performance".
    */
-  _timerId: null,
+  MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE: {
+    cpow: {
+      totalCPOWTime: Number.POSITIVE_INFINITY,
+    },
+    jank: {
+      longestDuration: 7,
+      totalUserTime: Number.POSITIVE_INFINITY,
+      totalSystemTime: Number.POSITIVE_INFINITY
+    },
+    ticks: {
+      ticks: Number.POSITIVE_INFINITY,
+    }
+  }
+}
+
+/**
+ * Utilities for dealing with state
+ */
+let State = {
+  _monitor: PerformanceStats.getMonitor(["jank", "cpow", "ticks"]),
 
   /**
-   * The dropdown DOM element.
+   * Indexed by the number of minutes since the snapshot was taken.
+   *
+   * @type {Array<{snapshot: PerformanceData, date: Date}>}
+   */
+  _buffer: [],
+  /**
+   * The first snapshot since opening the page.
+   *
+   * @type {snapshot: PerformnceData, date: Date}
    */
-  _intervalDropdown: null,
+  _oldest: null,
+
+  /**
+   * The latest snapshot.
+   *
+   * @type As _oldest
+   */
+  _latest: null,
+
+  /**
+   * The performance alerts for each group.
+   *
+   * This map is cleaned up during each update to avoid leaking references
+   * to groups that have been gc-ed.
+   *
+   * @type{Map<string, Array<number>} A map in which keys are
+   * values for `delta.groupId` and values are arrays
+   * [number of moderate-impact alerts, number of high-impact alerts]
+   */
+  _alerts: new Map(),
 
   /**
-   * Starts updating the performance data if the updates are paused.
+   * The date at which each group was first seen.
+   *
+   * This map is cleaned up during each update to avoid leaking references
+   * to groups that have been gc-ed.
+   *
+   * @type{Map<string, Date} A map in which keys are
+   * values for `delta.groupId` and values are approximate
+   * dates at which the group was first encountered.
    */
-  start: function () {
-    if (AutoUpdate._intervalDropdown == null){
-      AutoUpdate._intervalDropdown = document.getElementById("intervalDropdown");
-    }
-
-    if (AutoUpdate._timerId == null) {
-      let dropdownIndex = AutoUpdate._intervalDropdown.selectedIndex;
-      let dropdownValue = AutoUpdate._intervalDropdown.options[dropdownIndex].value;
-      AutoUpdate._timerId = window.setInterval(update, dropdownValue);
-    }
-  },
+  _firstSeen: new Map(),
 
   /**
-   * Stops the updates if the data is updating.
+   * Produce an annotated performance snapshot.
+   *
+   * @return {Object} An object extending `PerformanceDiff`
+   * with the following fields:
+   *  {Date} date The current date.
+   *  {Map<groupId, performanceData>} A map of performance data,
+   *  for faster access.
    */
-  stop: function () {
-    if (AutoUpdate._timerId == null) {
-      return;
+  _promiseSnapshot: Task.async(function*() {
+    let snapshot = yield this._monitor.promiseSnapshot();
+    snapshot.date = Date.now();
+    let componentsMap = new Map();
+    snapshot.componentsMap = componentsMap;
+    for (let component of snapshot.componentsData) {
+      componentsMap.set(component.groupId, component);
     }
-    clearInterval(AutoUpdate._timerId);
-    AutoUpdate._timerId = null;
+    return snapshot;
+  }),
+
+  /**
+   * Classify the "slowness" of a component.
+   *
+   * @return {number} 0 For a "fast" component.
+   * @return {number} 1 For an "average" component.
+   * @return {number} 2 For a "slow" component.
+   */
+  _getSlowness: function(component) {
+    if (Delta.compare(component, Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE) <= 0) {
+      return 0;
+    }
+    if (Delta.compare(component, Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE) <= 0) {
+      return 1;
+    }
+    return 2;
   },
 
   /**
-   * Updates the refresh interval when the dropdown selection is changed.
-   */
-  updateRefreshRate: function () {
-    AutoUpdate.stop();
-    AutoUpdate.start();
-  }
-
-};
-
-let State = {
-  _monitor: PerformanceStats.getMonitor([
-    "jank", "cpow", "ticks",
-    "jank-content", "cpow-content", "ticks-content",
-  ]),
-
-  /**
-   * @type{PerformanceData}
-   */
-  _processData: null,
-  /**
-   * A mapping from name to PerformanceData
-   *
-   * @type{Map}
-   */
-  _componentsData: new Map(),
-
-  /**
-   * A number of milliseconds since the high-performance epoch.
-   */
-  _date: window.performance.now(),
-
-  /**
-   * Fetch the latest information, compute diffs.
+   * Update the internal state.
    *
    * @return {Promise}
-   * @resolve An object with the following fields:
-   * - `components`: an array of `PerformanceDiff` representing
-   *   the components, sorted by `longestDuration`, then by `totalUserTime`
-   * - `process`: a `PerformanceDiff` representing the entire process;
-   * - `deltaT`: the number of milliseconds elapsed since the data
-   *   was last displayed.
    */
   update: Task.async(function*() {
-    let snapshot = yield this._monitor.promiseSnapshot();
-    let newData = new Map();
-    let deltas = [];
-    for (let componentNew of snapshot.componentsData) {
-      let key = componentNew.groupId;
-      let componentOld = State._componentsData.get(key);
-      deltas.push(componentNew.subtract(componentOld));
-      newData.set(key, componentNew);
+    // If the buffer is empty, add one value for bootstraping purposes.
+    if (this._buffer.length == 0) {
+      if (this._oldest) {
+        throw new Error("Internal Error, we shouldn't have a `_oldest` value yet.");
+      }
+      this._latest = this._oldest = yield this._promiseSnapshot();
+      this._buffer.push(this._oldest);
+      yield new Promise(resolve => setTimeout(resolve, BUFFER_SAMPLING_RATE_MS * 1.1));
     }
-    State._componentsData = newData;
-    let now = window.performance.now();
-    let process = snapshot.processData.subtract(State._processData);
-    let result = {
-      components: deltas.filter(x => x.ticks.ticks > 0),
-      process: snapshot.processData.subtract(State._processData),
-      deltaT: now - State._date
-    };
-    result.components.sort((a, b) => {
-      if (a.longestDuration < b.longestDuration) {
-        return true;
-      }
-      if (a.longestDuration == b.longestDuration) {
-        return a.totalUserTime <= b.totalUserTime
-      }
-      return false;
-    });
-    State._processData = snapshot.processData;
-    State._date = now;
-    return result;
-  })
-};
 
 
-let update = Task.async(function*() {
-  yield updateLiveData();
-  yield updateSlowAddons();
-  Services.obs.notifyObservers(null, UPDATE_COMPLETE_TOPIC, "");
-});
+    let now = Date.now();
+    
+    // If we haven't sampled in a while, add a sample to the buffer.
+    let latestInBuffer = this._buffer[this._buffer.length - 1];
+    let deltaT = now - latestInBuffer.date;
+    if (deltaT > BUFFER_SAMPLING_RATE_MS) {
+      this._latest = this._oldest = yield this._promiseSnapshot();
+      this._buffer.push(this._latest);
+
+      // Update alerts
+      let cleanedUpAlerts = new Map(); // A new map of alerts, without dead components.
+      for (let component of this._latest.componentsData) {
+        let slowness = this._getSlowness(component, deltaT);
+        let myAlerts = this._alerts.get(component.groupId) || [0, 0];
+        if (slowness > 0) {
+          myAlerts[slowness - 1]++;
+        }
+        cleanedUpAlerts.set(component.groupId, myAlerts);
+        this._alerts = cleanedUpAlerts;
+      }
+    }
+
+    // If we have too many samples, remove the oldest sample.
+    let oldestInBuffer = this._buffer[0];
+    if (oldestInBuffer.date + BUFFER_DURATION_MS < this._latest.date) {
+      this._buffer.shift();
+    }
+  }),
+
+  /**
+   * @return {Promise}
+   */
+  promiseDeltaSinceStartOfTime: function() {
+    return this._promiseDeltaSince(this._oldest);
+  },
+
+  /**
+   * @return {Promise}
+   */
+  promiseDeltaSinceStartOfBuffer: function() {
+    return this._promiseDeltaSince(this._buffer[0]);
+  },
+
+  /**
+   * @return {Promise}
+   */
+  _promiseDeltaSince: Task.async(function*(oldest) {
+    let current = this._latest;
+    if (!oldest) {
+      throw new TypeError();
+    }
+    if (!current) {
+      throw new TypeError();
+    }
 
-/**
- * Update the list of slow addons
- */
-let updateSlowAddons = Task.async(function*() {
-  try {
-    let data = AddonWatcher.alerts;
-    if (data.size == 0) {
-      // Nothing to display.
-      return;
-    }
-    let alerts = 0;
-    for (let [addonId, details] of data) {
-      for (let k of Object.keys(details.alerts)) {
-        alerts += details.alerts[k];
+    let addons = [];
+    let webpages = [];
+    let system = [];
+    let groups = new Set();
+
+    // We rebuild the map during each iteration to make sure that
+    // we do not maintain references to groups that has been removed
+    // (e.g. pages that have been closed).
+    let oldFirstSeen = this._firstSeen;
+    let cleanedUpFirstSeen = new Map();
+    this._firstSeen = cleanedUpFirstSeen;
+    for (let component of current.componentsData) {
+      // Enrich `delta` with `alerts`.
+      let delta = component.subtract(oldest.componentsMap.get(component.groupId));
+      delta.alerts = (this._alerts.get(component.groupId) || []).slice();
+
+      // Enrich `delta` with `age`.
+      let creationDate = oldFirstSeen.get(component.groupId) || current.date;
+      cleanedUpFirstSeen.set(component.groupId, creationDate);
+      delta.age = current.date - creationDate;
+
+      groups.add(component.groupId);
+
+      // Enrich `delta` with `fullName` and `readableName`.
+      delta.fullName = delta.name;
+      delta.readableName = delta.name;
+      if (component.addonId) {
+        let found = yield new Promise(resolve =>
+          AddonManager.getAddonByID(delta.addonId, a => {
+            if (a) {
+              delta.readableName = a.name;
+              resolve(true);
+            } else {
+              resolve(false);
+            }
+          }));
+        delta.fullName = delta.addonId;
+        if (found) {
+          // If the add-on manager doesn't know about an add-on, it's
+          // probably not a real add-on.
+          addons.push(delta);
+        }
+      } else if (!delta.isSystem || delta.title) {
+        // Wallpaper hack. For some reason, about:performance (and only about:performance)
+        // appears twice in the list. Only one of them is a window.
+        if (delta.title == document.title) {
+          if (!findTabFromWindow(delta.windowId)) {
+            // Not a real page.
+            system.push(delta);
+            continue;
+          }
+        }
+        delta.readableName = delta.title || delta.name;
+        webpages.push(delta);
+      } else {
+        system.push(delta);
       }
     }
 
-    if (!alerts) {
-      // Still nothing to display.
-      return;
-    }
-
-
-    let elData = document.getElementById("slowAddonsList");
-    elData.innerHTML = "";
-    let elTable = document.createElement("table");
-    elData.appendChild(elTable);
+    return {
+      addons,
+      webpages,
+      system,
+      groups,
+      duration: current.date - oldest.date
+    };
+  }),
+};
 
-    // Generate header
-    let elHeader = document.createElement("tr");
-    elTable.appendChild(elHeader);
-    for (let name of [
-      "Alerts",
-      "Jank level alerts",
-      "(highest jank)",
-      "Cross-Process alerts",
-      "(highest CPOW)"
-    ]) {
-      let elName = document.createElement("td");
-      elName.textContent = name;
-      elHeader.appendChild(elName);
-      elName.classList.add("header");
-    }
-    for (let [addonId, details] of data) {
-      let elAddon = document.createElement("tr");
-
-      // Display the number of occurrences of each alerts
-      let elTotal = document.createElement("td");
-      let total = 0;
-      for (let k of Object.keys(details.alerts)) {
-        total += details.alerts[k];
-      }
-      elTotal.textContent = total;
-      elAddon.appendChild(elTotal);
-
-      for (let filter of ["longestDuration", "totalCPOWTime"]) {
-        for (let stat of ["alerts", "peaks"]) {
-          let el = document.createElement("td");
-          el.textContent = details[stat][filter] || 0;
-          elAddon.appendChild(el);
+let View = {
+  /**
+   * A cache for all the per-item DOM elements that are reused across refreshes.
+   *
+   * Reusing the same elements means that elements that were hidden (respectively
+   * visible) in an iteration remain hidden (resp visible) in the next iteration.
+   */
+  DOMCache: {
+    _map: new Map(),
+    /**
+     * @param {string} groupId The groupId for the item that we are displaying.
+     * @return {null} If the `groupId` doesn't have a component cached yet.
+     * Otherwise, the value stored with `set`.
+     */
+    get: function(groupId) {
+      return this._map.get(groupId);
+    },
+    set: function(groupId, value) {
+      this._map.set(groupId, value);
+    },
+    /**
+     * Remove all the elements whose key does not appear in `set`.
+     *
+     * @param {Set} set a set of groupId.
+     */
+    trimTo: function(set) {
+      let remove = [];
+      for (let key of this._map.keys()) {
+        if (!set.has(key)) {
+          remove.push(key);
         }
       }
-
-      // Display the name of the add-on
-      let elName = document.createElement("td");
-      elAddon.appendChild(elName);
-      AddonManager.getAddonByID(addonId, a => {
-        elName.textContent = a ? a.name : addonId
-      });
-
-      elTable.appendChild(elAddon);
+      for (let key of remove) {
+        this._map.delete(key);
+      }
     }
-  } catch (ex) {
-    console.error(ex);
-  }
-});
-
-/**
- * Update the table of live data.
- */
-let updateLiveData = Task.async(function*() {
-  try {
-    let dataElt = document.getElementById("liveData");
-    dataElt.innerHTML = "";
+  },
+  /**
+   * Display the items in a category.
+   *
+   * @param {Array<PerformanceDiff>} subset The items to display. They will
+   * be displayed in the order of `subset`.
+   * @param {string} id The id of the DOM element that will contain the items.
+   * @param {string} nature The nature of the subset. One of "addons", "webpages" or "system".
+   * @param {string} currentMode The current display mode. One of MODE_GLOBAL or MODE_RECENT.
+   */
+  updateCategory: function(subset, id, nature, deltaT, currentMode) {
+    subset = subset.slice().sort(Delta.revCompare);
 
-    // Generate table headers
-    let headerElt = document.createElement("tr");
-    dataElt.appendChild(headerElt);
-    headerElt.classList.add("header");
-    for (let column of [...MEASURES, {key: "name", name: ""}, {key: "process", name: ""}]) {
-      let el = document.createElement("td");
-      el.classList.add(column.key);
-      el.textContent = column.label;
-      headerElt.appendChild(el);
-    }
-
-    let deltas = yield State.update();
-
-    for (let item of [deltas.process, ...deltas.components]) {
-      let row = document.createElement("tr");
-      if (item.addonId) {
-        row.classList.add("addon");
-      } else if (item.isSystem) {
-        row.classList.add("platform");
-      } else {
-        row.classList.add("content");
-      }
-      dataElt.appendChild(row);
+    // Grab everything from the DOM before cleaning up
+    let eltContainer = this._setupStructure(id);
 
-      // Measures
-      for (let {probe, key, percentOfDeltaT} of MEASURES) {
-        let el = document.createElement("td");
-        el.classList.add(key);
-        el.classList.add("contents");
-        row.appendChild(el);
-
-        let rawValue = item[probe][key];
-        let value = percentOfDeltaT ? Math.round(rawValue / deltas.deltaT) : rawValue;
-        if (key == "longestDuration") {
-          value += 1;
-          el.classList.add("jank" + value);
+    // An array of `cachedElements` that need to be added
+    let toAdd = [];
+    for (let delta of subset) {
+      let cachedElements = this._grabOrCreateElements(delta, nature);
+      toAdd.push(cachedElements);
+      cachedElements.eltTitle.textContent = delta.readableName;
+      cachedElements.eltName.textContent = `Full name: ${delta.fullName}.`;
+      cachedElements.eltLoaded.textContent = `Measure start: ${Math.round(delta.age/1000)} seconds ago.`
+      cachedElements.eltProcess.textContent = `Process: ${delta.processId} (${delta.isChildProcess?"child":"parent"}).`;
+      let eltImpact = cachedElements.eltImpact;
+      if (currentMode == MODE_RECENT) {
+        cachedElements.eltRoot.setAttribute("impact", delta.jank.longestDuration + 1);
+        if (Delta.compare(delta, Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE) <= 0) {
+          eltImpact.textContent = ` currently performs well.`;
+        } else if (Delta.compare(delta, Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE)) {
+          eltImpact.textContent = ` may currently be slowing down ${BRAND_NAME}.`;
+        } else {
+          eltImpact.textContent = ` is currently considerably slowing down ${BRAND_NAME}.`;
         }
-        el.textContent = value;
-      }
+        cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.jank.longestDuration + 1}/${delta.jank.durations.length}.`;
+        cachedElements.eltCPU.textContent = `CPU usage: ${Math.min(100, Math.ceil(delta.jank.totalUserTime/deltaT))}%.`;
+        cachedElements.eltSystem.textContent = `System usage: ${Math.min(100, Math.ceil(delta.jank.totalSystemTime/deltaT))}%.`;
+        cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.cpow.totalCPOWTime/deltaT)}%.`;
+      } else {
+        if (delta.alerts.length == 0) {
+          eltImpact.textContent = " has performed well so far.";
+          cachedElements.eltFPS.textContent = `Impact on framerate: no impact.`;
+        } else {
+          let sum = /* medium impact */ delta.alerts[0] + /* high impact */ delta.alerts[1];
+          let frequency = sum * 1000 / delta.age;
 
-      {
-        // Name
-        let el = document.createElement("td");
-        let id = item.id;
-        el.classList.add("contents");
-        el.classList.add("name");
-        row.appendChild(el);
-        if (item.addonId) {
-          let _el = el;
-          let _item = item;
-          AddonManager.getAddonByID(item.addonId, a => {
-            _el.textContent = a ? a.name : _item.name
-          });
-        } else {
-          el.textContent = item.title || item.name;
-        }
-      }
+          let describeFrequency;
+          if (frequency <= MAX_FREQUENCY_FOR_NO_IMPACT) {
+            describeFrequency = `has no impact on the performance of ${BRAND_NAME}.`
+            cachedElements.eltRoot.classList.add("impact0");
+          } else {
+            let describeImpact;
+            if (frequency <= MAX_FREQUENCY_FOR_RARE) {
+              describeFrequency = `rarely slows down ${BRAND_NAME}.`;
+            } else if (frequency <= MAX_FREQUENCY_FOR_FREQUENT) {
+              describeFrequency = `has slown down ${BRAND_NAME} frequently.`;
+            } else {
+              describeFrequency = `seems to have slown down ${BRAND_NAME} very often.`;
+            }
+            // At this stage, `sum != 0`
+            if (delta.alerts[1] / sum > MIN_PROPORTION_FOR_MAJOR_IMPACT) {
+              describeImpact = "When this happens, the slowdown is generally important."
+            } else {
+              describeImpact = "When this happens, the slowdown is generally noticeable."
+            }
 
-      {
-        // Process information.
-        let el = document.createElement("td");
-        el.classList.add("contents");
-        el.classList.add("process");
-        row.appendChild(el);
-        if (item.isChildProcess) {
-          el.textContent = "(child)";
-          row.classList.add("child");
-        } else {
-          el.textContent = "(parent)";
-          row.classList.add("parent");
+            eltImpact.textContent = ` ${describeFrequency} ${describeImpact}`;
+            cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.alerts[1]} high-impacts, ${delta.alerts[0]} medium-impact.`;
+          }
+          cachedElements.eltCPU.textContent = `CPU usage: ${Math.min(100, Math.ceil(delta.jank.totalUserTime/delta.age))}% (total ${delta.jank.totalUserTime}ms).`;
+          cachedElements.eltSystem.textContent = `System usage: ${Math.min(100, Math.ceil(delta.jank.totalSystemTime/delta.age))}% (total ${delta.jank.totalSystemTime}ms).`;
+          cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.cpow.totalCPOWTime/delta.age)}% (total ${delta.cpow.totalCPOWTime}ms).`;
         }
       }
     }
-  } catch (ex) {
-    console.error(ex);
-  }
-});
+    this._insertElements(toAdd, id);
+  },
+
+  _insertElements: function(elements, id) {
+    let eltContainer = document.getElementById(id);
+    eltContainer.classList.remove("measuring");
+    eltContainer.eltVisibleContent.innerHTML = "";
+    eltContainer.eltHiddenContent.innerHTML = "";
+    eltContainer.appendChild(eltContainer.eltShowMore);
+
+    for (let i = 0; i < elements.length && i < MAX_NUMBER_OF_ITEMS_TO_DISPLAY; ++i) {
+      let cachedElements = elements[i];
+      eltContainer.eltVisibleContent.appendChild(cachedElements.eltRoot);
+    }
+    for (let i = MAX_NUMBER_OF_ITEMS_TO_DISPLAY; i < elements.length; ++i) {
+      let cachedElements = elements[i];
+      eltContainer.eltHiddenContent.appendChild(cachedElements.eltRoot);
+    }
+    if (elements.length <= MAX_NUMBER_OF_ITEMS_TO_DISPLAY) {
+      eltContainer.eltShowMore.classList.add("hidden");
+    } else {
+      eltContainer.eltShowMore.classList.remove("hidden");
+    }
+    if (elements.length == 0) {
+      eltContainer.textContent = "Nothing";
+    }
+  },
+  _setupStructure: function(id) {
+    let eltContainer = document.getElementById(id);
+    if (!eltContainer.eltVisibleContent) {
+      eltContainer.eltVisibleContent = document.createElement("ul");
+      eltContainer.eltVisibleContent.classList.add("visible_items");
+      eltContainer.appendChild(eltContainer.eltVisibleContent);
+    }
+    if (!eltContainer.eltHiddenContent) {
+      eltContainer.eltHiddenContent = document.createElement("ul");
+      eltContainer.eltHiddenContent.classList.add("hidden");
+      eltContainer.eltHiddenContent.classList.add("hidden_additional_items");
+      eltContainer.appendChild(eltContainer.eltHiddenContent);
+    }
+    if (!eltContainer.eltShowMore) {
+      eltContainer.eltShowMore = document.createElement("button");
+      eltContainer.eltShowMore.textContent = "Show all";
+      eltContainer.eltShowMore.classList.add("show_all_items");
+      eltContainer.appendChild(eltContainer.eltShowMore);
+      eltContainer.eltShowMore.addEventListener("click", function() {
+        if (eltContainer.eltHiddenContent.classList.contains("hidden")) {
+          eltContainer.eltHiddenContent.classList.remove("hidden");
+          eltContainer.eltShowMore.textContent = "Hide";
+        } else {
+          eltContainer.eltHiddenContent.classList.add("hidden");
+          eltContainer.eltShowMore.textContent = "Show all";
+        }
+      });
+    }
+    return eltContainer;
+  },
+
+  _grabOrCreateElements: function(delta, nature) {
+    let cachedElements = this.DOMCache.get(delta.groupId);
+    if (cachedElements) {
+      if (cachedElements.eltRoot.parentElement) {
+        cachedElements.eltRoot.parentElement.removeChild(cachedElements.eltRoot);
+      }
+    } else {
+      this.DOMCache.set(delta.groupId, cachedElements = {});
+
+      let eltDelta = document.createElement("li");
+      eltDelta.classList.add("delta");
+      cachedElements.eltRoot = eltDelta;
+
+      let eltSpan = document.createElement("span");
+      eltDelta.appendChild(eltSpan);
 
-function go() {
-  document.getElementById("playButton").addEventListener("click", () => AutoUpdate.start());
-  document.getElementById("pauseButton").addEventListener("click", () => AutoUpdate.stop());
+      let eltSummary = document.createElement("span");
+      eltSummary.classList.add("summary");
+      eltSpan.appendChild(eltSummary);
+
+      let eltTitle = document.createElement("span");
+      eltTitle.classList.add("title");
+      eltSummary.appendChild(eltTitle);
+      cachedElements.eltTitle = eltTitle;
+
+      let eltImpact = document.createElement("span");
+      eltImpact.classList.add("impact");
+      eltSummary.appendChild(eltImpact);
+      cachedElements.eltImpact = eltImpact;
+
+      let eltShowMore = document.createElement("a");
+      eltShowMore.classList.add("more");
+      eltSpan.appendChild(eltShowMore);
+      eltShowMore.textContent = "more";
+      eltShowMore.href = "";
+      eltShowMore.addEventListener("click", () => {
+        if (eltDetails.classList.contains("hidden")) {
+          eltDetails.classList.remove("hidden");
+          eltShowMore.textContent = "less";
+        } else {
+          eltDetails.classList.add("hidden");
+          eltShowMore.textContent = "more";
+        }
+      });
+
+      // Add buttons
+      if (nature == "addons") {
+        eltSpan.appendChild(document.createElement("br"));
+        let eltDisable = document.createElement("button");
+        eltDisable.textContent = "Disable";
+        eltSpan.appendChild(eltDisable);
 
-  document.getElementById("intervalDropdown").addEventListener("change", () => AutoUpdate.updateRefreshRate());
+        let eltUninstall = document.createElement("button");
+        eltUninstall.textContent = "Uninstall";
+        eltSpan.appendChild(eltUninstall);
+
+        let eltRestart = document.createElement("button");
+        eltRestart.textContent = `Restart ${BRAND_NAME} to apply your changes.`
+        eltRestart.classList.add("hidden");
+        eltSpan.appendChild(eltRestart);
+
+        eltRestart.addEventListener("click", () => {
+          Services.startup.quit(Services.startup.eForceQuit | Services.startup.eRestart);
+        });
+        AddonManager.getAddonByID(delta.addonId, addon => {
+          eltDisable.addEventListener("click", () => {
+            addon.userDisabled = true;
+            if (addon.pendingOperations == addon.PENDING_NONE) {
+              // Restartless add-on is now disabled.
+              return;
+            }
+            eltDisable.classList.add("hidden");
+            eltUninstall.classList.add("hidden");
+            eltRestart.classList.remove("hidden");
+          });
+
+          eltUninstall.addEventListener("click", () => {
+            addon.uninstall();
+            if (addon.pendingOperations == addon.PENDING_NONE) {
+              // Restartless add-on is now disabled.
+              return;
+            }
+            eltDisable.classList.add("hidden");
+            eltUninstall.classList.add("hidden");
+            eltRestart.classList.remove("hidden");
+          });
+        });
+      } else if (nature == "webpages") {
+        eltSpan.appendChild(document.createElement("br"));
 
-  // Compute initial state immediately, then wait a little
-  // before we start computing diffs and refreshing.
-  State.update();
-  window.setTimeout(update, 500);
+        let eltCloseTab = document.createElement("button");
+        eltCloseTab.textContent = "Close tab";
+        eltSpan.appendChild(eltCloseTab);
+        let windowId = delta.windowId;
+        eltCloseTab.addEventListener("click", () => {
+          let found = findTabFromWindow(windowId);
+          if (!found) {
+            // Cannot find the tab. Maybe it is closed already?
+            return;
+          }
+          let {tabbrowser, tab} = found;
+          tabbrowser.removeTab(tab);
+        });
+
+        let eltReloadTab = document.createElement("button");
+        eltReloadTab.textContent = "Reload tab";
+        eltSpan.appendChild(eltReloadTab);
+        eltReloadTab.addEventListener("click", () => {
+          let found = findTabFromWindow(windowId);
+          if (!found) {
+            // Cannot find the tab. Maybe it is closed already?
+            return;
+          }
+          let {tabbrowser, tab} = found;
+          tabbrowser.reloadTab(tab);
+        });
+      }
+
+      // Prepare details
+      let eltDetails = document.createElement("ul");
+      eltDetails.classList.add("details");
+      eltDetails.classList.add("hidden");
+      eltSpan.appendChild(eltDetails);
+
+      for (let [name, className] of [
+        ["eltName", "name"],
+        ["eltFPS", "fps"],
+        ["eltCPU", "cpu"],
+        ["eltSystem", "system"],
+        ["eltCPOW", "cpow"],
+        ["eltLoaded", "loaded"],
+        ["eltProcess", "process"]
+      ]) {
+        let elt = document.createElement("li");
+        elt.classList.add(className);
+        eltDetails.appendChild(elt);
+        cachedElements[name] = elt;
+      }
+    }
+
+    return cachedElements;
+  },
+};
+
+let Control = {
+  init: function() {
+    this._initAutorefresh();
+    this._initDisplayMode();
+  },
+  update: Task.async(function*() {
+    let mode = this._displayMode;
+    if (this._autoRefreshInterval) {
+      // Update the state only if we are not on pause.
+      yield State.update();
+    }
+    let state = yield (mode == MODE_GLOBAL?
+      State.promiseDeltaSinceStartOfTime():
+      State.promiseDeltaSinceStartOfBuffer());
+
+    for (let category of ["webpages", "addons"]) {
+      yield Promise.resolve();
+      yield View.updateCategory(state[category], category, category, state.duration, mode);
+    }
+    yield Promise.resolve();
 
-  let observer = update;
-  
-  Services.obs.addObserver(update, UPDATE_IMMEDIATELY_TOPIC, false);
-  window.addEventListener("unload", () => Services.obs.removeObserver(update, UPDATE_IMMEDIATELY_TOPIC));
-}
+    // Make sure that we do not keep obsolete stuff around.
+    View.DOMCache.trimTo(state.groups);
+
+    // Inform watchers
+    Services.obs.notifyObservers(null, UPDATE_COMPLETE_TOPIC, mode);
+  }),
+  _setOptions: function(options) {
+    let eltRefresh = document.getElementById("check-autorefresh");
+    if ((options.autoRefresh > 0) != eltRefresh.checked) {
+      eltRefresh.click();
+    }
+    let eltCheckRecent = document.getElementById("check-display-recent");
+    if (!!options.displayRecent != eltCheckRecent.checked) {
+      eltCheckRecent.click();
+    }
+  },
+  _initAutorefresh: function() {
+    let onRefreshChange = (shouldUpdateNow = false) => {
+      if (eltRefresh.checked == !!this._autoRefreshInterval) {
+        // Nothing to change.
+        return;
+      }
+      if (eltRefresh.checked) {
+        this._autoRefreshInterval = window.setInterval(() => Control.update(), UPDATE_INTERVAL_MS);
+        if (shouldUpdateNow) {
+          Control.update();
+        }
+      } else {
+        window.clearInterval(this._autoRefreshInterval);
+        this._autoRefreshInterval = null;
+      }
+    }
+
+    let eltRefresh = document.getElementById("check-autorefresh");
+    eltRefresh.addEventListener("change", () => onRefreshChange(true));
+
+    onRefreshChange(false);
+  },
+  _autoRefreshInterval: null,
+  _initDisplayMode: function() {
+    let onModeChange = (shouldUpdateNow) => {
+      if (eltCheckRecent.checked) {
+        this._displayMode = MODE_RECENT;
+      } else {
+        this._displayMode = MODE_GLOBAL;
+      }
+      if (shouldUpdateNow) {
+        Control.update();
+      }
+    };
+
+    let eltCheckRecent = document.getElementById("check-display-recent");
+    let eltLabelRecent = document.getElementById("label-display-recent");
+    eltCheckRecent.addEventListener("click", () => onModeChange(true));
+    eltLabelRecent.textContent = `Display only the latest ${Math.round(BUFFER_DURATION_MS/1000)}s`;
+
+    onModeChange(false);
+  },
+  // The display mode. One of `MODE_GLOBAL` or `MODE_RECENT`.
+  _displayMode: MODE_GLOBAL,
+};
+
+let go = Task.async(function*() {
+  Control.init();
+
+  // Setup a hook to allow tests to configure and control this page
+  let testUpdate = function(subject, topic, value) {
+    let options = JSON.parse(value);
+    Control._setOptions(options);
+    Control.update();
+  };
+  Services.obs.addObserver(testUpdate, TEST_DRIVER_TOPIC, false);
+  window.addEventListener("unload", () => Services.obs.removeObserver(testUpdate, TEST_DRIVER_TOPIC));
+
+  yield Control.update();
+  yield new Promise(resolve => setTimeout(resolve, BUFFER_SAMPLING_RATE_MS * 1.1));
+  yield Control.update();
+});
--- a/toolkit/components/aboutperformance/content/aboutPerformance.xhtml
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.xhtml
@@ -4,106 +4,110 @@
    - 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/. -->
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <title>about:performance</title>
     <script type="text/javascript;version=1.8" src="chrome://global/content/aboutPerformance.js"></script>
     <style>
-      td.addon {
-        display: inline-block;
-        width: 400px;
-      }
-      td.time {
-        display: inline-block;
-        width: 100px;
-      }
-      td.cpow {
-        display: inline-block;
-        width: 100px;
-      }
-      .header {
-        font-weight: bold;
-      }
-      tr.details {
-        font-weight: lighter;
-        color: gray;
+      @import url("chrome://global/skin/in-content/common.css");
+      .hidden {
         display: none;
       }
-      tr.addon {
-        background-color: white;
-      }
-      tr.platform {
-        background-color: rgb(255, 255, 200);
-      }
-      tr.content {
-        background-color: rgb(200, 255, 255);
-      }
-      td.jank0 {
-        color: rgb(0, 0, 0);
+      .summary .title {
         font-weight: bold;
       }
-      td.jank1 {
-        color: rgb(25, 0, 0);
-        font-weight: bold;
+      a {
+        text-decoration: none;
+      }
+      a.more {
+        margin-left: 2ch;
       }
-      td.jank2 {
-        color: rgb(50, 0, 0);
-        font-weight: bold;
+      ul.hidden_additional_items {
+        padding-top: 0;
+        margin-top: 0;
+      }
+      ul.visible_items {
+        padding-bottom: 0;
+        margin-bottom: 0;
       }
-      td.jank3 {
-        color: rgb(75, 0, 0);
-        font-weight: bold;
+      li.delta {
+        margin-top: .5em;
+      }
+      h2 {
+        margin-top: 1cm;
       }
-      td.jank4 {
-        color: rgb(100, 0, 0);
-        font-weight: bold;
+      button.show_all_items {
+        margin-top: .5cm;
+        margin-left: 1cm;
       }
-      td.jank5 {
-        color: rgb(125, 0, 0);
-        font-weight: bold;
+      body {
+        margin-left: 1cm;
+      }
+      div.measuring {
+         background: url(chrome://global/skin/media/throbber.png) no-repeat center;
+         min-width: 36px;
+         min-height: 36px;
       }
-      td.jank6 {
-        color: rgb(150, 0, 0);
-        font-weight: bold;
+      li.delta {
+        border-left-width: 5px;
+        border-left-style: solid;
+        padding-left: 1em;
+        list-style: none;
+      }
+      li.delta[impact="0"] {
+        border-left-color: rgb(0, 255, 0);
       }
-      td.jank7 {
-        color: rgb(175, 0, 0);
-        font-weight: bold;
+      li.delta[impact="1"] {
+        border-left-color: rgb(24, 231, 0);
+      }
+      li.delta[impact="2"] {
+        border-left-color: rgb(48, 207, 0);
+      }
+      li.delta[impact="3"] {
+        border-left-color: rgb(72, 183, 0);
+      }
+      li.delta[impact="4"] {
+        border-left-color: rgb(96, 159, 0);
       }
-      td.jank8 {
-        color: rgb(200, 0, 0);
-        font-weight: bold;
+      li.delta[impact="5"] {
+        border-left-color: rgb(120, 135, 0);
+      }
+      li.delta[impact="6"] {
+        border-left-color: rgb(144, 111, 0);
+      }
+      li.delta[impact="7"] {
+        border-left-color: rgb(168, 87, 0);
       }
-      td.jank9 {
-        color: rgb(225, 0, 0);
-        font-weight: bold;
+      li.delta[impact="8"] {
+        border-left-color: rgb(192, 63, 0);
+      }
+      li.delta[impact="9"] {
+        border-left-color: rgb(216, 39, 0);
       }
-      td.jank10 {
-        color: rgb(255, 0, 0);
-        font-weight: bold;
+      li.delta[impact="10"] {
+        border-left-color: rgb(240, 15, 0);
+      }
+      li.delta[impact="11"] {
+        border-left-color: rgb(255, 0, 0);
       }
     </style>
   </head>
   <body onload="go()">
-
-    <h1>Performance monitor</h1>
-
-    <input type="button" id="playButton" value="Play" />
-    <input type="button" id="pauseButton" value="Pause" />
-    <select id="intervalDropdown">
-      <option value="500">0.5 seconds</option>
-      <option value="1000">1 second</option>
-      <option value="2000">2 seconds</option>
-      <option value="5000" selected="selected">5 seconds</option>
-      <option value="10000">10 seconds</option>
-    </select>
-    <table id="liveData">
-    </table>
-
-    <h1>Slow add-ons alerts</h1>
-    <div id="slowAddonsList">
-      (none)
+    <div>
+      <input type="checkbox" checked="false" id="check-display-recent"></input> 
+      <label for="check-display-recent" id="label-display-recent">Display only the last few seconds.</label>
+      <input type="checkbox" checked="true" id="check-autorefresh"></input>
+      <label for="check-autorefresh">Refresh automatically</label>
     </div>
-
+    <div>
+      <h2>Performance of Add-ons</h2>
+      <div id="addons" class="measuring">
+      </div>
+    </div>
+    <div>
+      <h2>Performance of Web pages</h2>
+      <div id="webpages" class="measuring">
+      </div>
+    </div>
   </body>
 </html>
--- a/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js
+++ b/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js
@@ -15,73 +15,89 @@ function frameScript() {
     content.postMessage("stop", "*");
     sendAsyncMessage("aboutperformance-test:done", null);
   });
   addMessageListener("aboutperformance-test:setTitle", ({data: title}) => {
     content.document.title = title;
     sendAsyncMessage("aboutperformance-test:setTitle", null);
   });
   
-  addMessageListener("aboutperformance-test:hasItems", ({data: title}) => {
-    let observer = function() {
+  addMessageListener("aboutperformance-test:hasItems", ({data: {title, options}}) => {
+    let observer = function(subject, topic, mode) {
       Services.obs.removeObserver(observer, "about:performance-update-complete");
-      let hasPlatform = false;
-      let hasTitle = false;
+      let hasTitleInWebpages = false;
+      let hasTitleInAddons = false;
 
       try {
-        let eltData = content.document.getElementById("liveData");
-        if (!eltData) {
+        let eltWeb = content.document.getElementById("webpages");
+        let eltAddons = content.document.getElementById("addons");
+        if (!eltWeb || !eltAddons) {
           return;
         }
 
-        // Find if we have a row for "platform"
-        hasPlatform = eltData.querySelector("tr.platform") != null;
+        let addonTitles = [for (eltContent of eltAddons.querySelectorAll("span.title")) eltContent.textContent];
+        let webTitles = [for (eltContent of eltWeb.querySelectorAll("span.title")) eltContent.textContent];
 
-        // Find if we have a row for our content page
-        let titles = [for (eltContent of eltData.querySelectorAll("td.contents.name")) eltContent.textContent];
+        hasTitleInAddons = addonTitles.includes(title);
+        hasTitleInWebpages = webTitles.includes(title);
 
-        hasTitle = titles.includes(title);
       } catch (ex) {
         Cu.reportError("Error in content: " + ex);
         Cu.reportError(ex.stack);
       } finally {
-        sendAsyncMessage("aboutperformance-test:hasItems", {hasPlatform, hasTitle});
+        sendAsyncMessage("aboutperformance-test:hasItems", {hasTitleInAddons, hasTitleInWebpages, mode});
       }
     }
     Services.obs.addObserver(observer, "about:performance-update-complete", false);
-    Services.obs.notifyObservers(null, "about:performance-update-immediately", "");
+    Services.obs.notifyObservers(null, "test-about:performance-test-driver", JSON.stringify(options));
   });
 }
 
+let gTabAboutPerformance = null;
+let gTabContent = null;
 
-add_task(function* go() {
+add_task(function* init() {
   info("Setting up about:performance");
-  let tabAboutPerformance = gBrowser.selectedTab = gBrowser.addTab("about:performance");
-  yield ContentTask.spawn(tabAboutPerformance.linkedBrowser, null, frameScript);
+  gTabAboutPerformance = gBrowser.selectedTab = gBrowser.addTab("about:performance");
+  yield ContentTask.spawn(gTabAboutPerformance.linkedBrowser, null, frameScript);
 
   info(`Setting up ${URL}`);
-  let tabContent = gBrowser.addTab(URL);
-  yield ContentTask.spawn(tabContent.linkedBrowser, null, frameScript);
+  gTabContent = gBrowser.addTab(URL);
+  yield ContentTask.spawn(gTabContent.linkedBrowser, null, frameScript);
+});
 
+let promiseExpectContent = Task.async(function*(options) {
   let title = "Testing about:performance " + Math.random();
-  info(`Setting up title ${title}`);
-  while (true) {
-    yield promiseContentResponse(tabContent.linkedBrowser, "aboutperformance-test:setTitle", title);
+  for (let i = 0; i < 30; ++i) {
+    yield promiseContentResponse(gTabContent.linkedBrowser, "aboutperformance-test:setTitle", title);
+    let {hasTitleInWebpages, hasTitleInAddons, mode} = (yield promiseContentResponse(gTabAboutPerformance.linkedBrowser, "aboutperformance-test:hasItems", {title, options}));
+    if (hasTitleInWebpages && ((mode == "recent") == options.displayRecent)) {
+      Assert.ok(!hasTitleInAddons, "The title appears in webpages, but not in addons");
+      return true;
+    }
+    info(`Title not found, trying again ${i}/30`);
     yield new Promise(resolve => setTimeout(resolve, 100));
-    let {hasPlatform, hasTitle} = (yield promiseContentResponse(tabAboutPerformance.linkedBrowser, "aboutperformance-test:hasItems", title));
-    info(`Platform: ${hasPlatform}, title: ${hasTitle}`);
-    if (hasPlatform && hasTitle) {
-      Assert.ok(true, "Found a row for <platform> and a row for our page");
-      break;
+  }
+  return false;
+});
+
+add_task(function* tests() {
+    for (let autoRefresh of [100, -1]) {
+      for (let displayRecent of [true, false]) {
+        info(`Testing ${autoRefresh > 0?"with":"without"} autoRefresh, in ${displayRecent?"recent":"global"} mode`);
+        let found = yield promiseExpectContent({autoRefresh, displayRecent});
+        Assert.equal(found, autoRefresh > 0, "The page title appears iff about:performance is set to auto-refresh");
+      }
     }
-  }
+});
 
+add_task(function* cleanup() {
   // Cleanup
   info("Cleaning up");
-  yield promiseContentResponse(tabAboutPerformance.linkedBrowser, "aboutperformance-test:done", null);
+  yield promiseContentResponse(gTabAboutPerformance.linkedBrowser, "aboutperformance-test:done", null);
 
   info("Closing tabs");
   for (let tab of gBrowser.tabs) {
     yield BrowserTestUtils.removeTab(tab);
   }
 
   info("Done");
   gBrowser.selectedTab = null;