summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutperformance
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/aboutperformance')
-rw-r--r--toolkit/components/aboutperformance/content/aboutPerformance.js1077
-rw-r--r--toolkit/components/aboutperformance/content/aboutPerformance.xhtml188
-rw-r--r--toolkit/components/aboutperformance/jar.mn7
-rw-r--r--toolkit/components/aboutperformance/moz.build9
-rw-r--r--toolkit/components/aboutperformance/tests/browser/.eslintrc.js7
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser.ini8
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js300
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_compartments.html20
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html12
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js29
-rw-r--r--toolkit/components/aboutperformance/tests/browser/head.js52
11 files changed, 1709 insertions, 0 deletions
diff --git a/toolkit/components/aboutperformance/content/aboutPerformance.js b/toolkit/components/aboutperformance/content/aboutPerformance.js
new file mode 100644
index 000000000..3b191d895
--- /dev/null
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.js
@@ -0,0 +1,1077 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+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", {});
+const { ObjectUtils } = Cu.import("resource://gre/modules/ObjectUtils.jsm", {});
+const { Memory } = Cu.import("resource://gre/modules/Memory.jsm");
+const { DownloadUtils } = Cu.import("resource://gre/modules/DownloadUtils.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 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";
+
+// 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";
+
+let tabFinder = {
+ update: function() {
+ this._map = new Map();
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ let tabbrowser = win.gBrowser;
+ for (let browser of tabbrowser.browsers) {
+ let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet
+ if (id != null) {
+ this._map.set(id, browser);
+ }
+ }
+ }
+ },
+
+ /**
+ * 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.
+ */
+ get: function(id) {
+ let browser = this._map.get(id);
+ if (!browser) {
+ return null;
+ }
+ let tabbrowser = browser.getTabBrowser();
+ return {tabbrowser, tab:tabbrowser.getTabForBrowser(browser)};
+ },
+
+ getAny: function(ids) {
+ for (let id of ids) {
+ let result = this.get(id);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ }
+};
+
+/**
+ * Returns a Promise that's resolved after the next turn of the event loop.
+ *
+ * Just returning a resolved Promise would mean that any `then` callbacks
+ * would be called right after the end of the current turn, so `setTimeout`
+ * is used to delay Promise resolution until the next turn.
+ *
+ * In mochi tests, it's possible for this to be called after the
+ * about:performance window has been torn down, which causes `setTimeout` to
+ * throw an NS_ERROR_NOT_INITIALIZED exception. In that case, returning
+ * `undefined` is fine.
+ */
+function wait(ms = 0) {
+ try {
+ let resolve;
+ let p = new Promise(resolve_ => { resolve = resolve_ });
+ setTimeout(resolve, ms);
+ return p;
+ } catch (e) {
+ dump("WARNING: wait aborted because of an invalid Window state in aboutPerformance.js.\n");
+ return undefined;
+ }
+}
+
+/**
+ * The performance of a webpage or an add-on between two instants.
+ *
+ * Clients should call `promiseInit()` before using the methods of this object.
+ *
+ * @param {PerformanceDiff} The underlying performance data.
+ * @param {"addons"|"webpages"} The kind of delta represented by this object.
+ * @param {Map<groupId, timestamp>} ageMap A map containing the oldest known
+ * appearance of each groupId, used to determine how long we have been monitoring
+ * this item.
+ * @param {Map<Delta key, Array>} alertMap A map containing the alerts that each
+ * item has already triggered in the past.
+ */
+function Delta(diff, kind, snapshotDate, ageMap, alertMap) {
+ if (kind != "addons" && kind != "webpages") {
+ throw new TypeError(`Unknown kind: ${kind}`);
+ }
+
+ /**
+ * Either "addons" or "webpages".
+ */
+ this.kind = kind;
+
+ /**
+ * The underlying PerformanceDiff.
+ * @type {PerformanceDiff}
+ */
+ this.diff = diff;
+
+ /**
+ * A key unique to the item (webpage or add-on), shared by successive
+ * instances of `Delta`.
+ * @type{string}
+ */
+ this.key = kind + diff.key;
+
+ // Find the oldest occurrence of this item.
+ let creationDate = snapshotDate;
+ for (let groupId of diff.groupIds) {
+ let date = ageMap.get(groupId);
+ if (date && date <= creationDate) {
+ creationDate = date;
+ }
+ }
+
+ /**
+ * The timestamp at which the data was measured.
+ */
+ this.creationDate = creationDate;
+
+ /**
+ * Number of milliseconds since the start of the measure.
+ */
+ this.age = snapshotDate - creationDate;
+
+ /**
+ * A UX-friendly, human-readable name for this item.
+ */
+ this.readableName = null;
+
+ /**
+ * A complete name, possibly useful for power users or debugging.
+ */
+ this.fullName = null;
+
+
+ // `true` once initialization is complete.
+ this._initialized = false;
+ // `true` if this item should be displayed
+ this._show = false;
+
+ /**
+ * All the alerts that this item has caused since about:performance
+ * was opened.
+ */
+ this.alerts = (alertMap.get(this.key) || []).slice();
+ switch (this.slowness) {
+ case 0: break;
+ case 1: this.alerts[0] = (this.alerts[0] || 0) + 1; break;
+ case 2: this.alerts[1] = (this.alerts[1] || 0) + 1; break;
+ default: throw new Error();
+ }
+}
+Delta.prototype = {
+ /**
+ * `true` if this item should be displayed, `false` otherwise.
+ */
+ get show() {
+ this._ensureInitialized();
+ return this._show;
+ },
+
+ /**
+ * Estimate the slowness of this item.
+ *
+ * @return 0 if the item has good performance.
+ * @return 1 if the item has average performance.
+ * @return 2 if the item has poor performance.
+ */
+ get slowness() {
+ if (Delta.compare(this, Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE) <= 0) {
+ return 0;
+ }
+ if (Delta.compare(this, Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE) <= 0) {
+ return 1;
+ }
+ return 2;
+ },
+ _ensureInitialized() {
+ if (!this._initialized) {
+ throw new Error();
+ }
+ },
+
+ /**
+ * Initialize, asynchronously.
+ */
+ promiseInit: function() {
+ if (this.kind == "webpages") {
+ return this._initWebpage();
+ } else if (this.kind == "addons") {
+ return this._promiseInitAddon();
+ }
+ throw new TypeError();
+ },
+ _initWebpage: function() {
+ this._initialized = true;
+ let found = tabFinder.getAny(this.diff.windowIds);
+ if (!found || found.tab.linkedBrowser.contentTitle == null) {
+ // Either this is not a real page or the page isn't restored yet.
+ return;
+ }
+
+ this.readableName = found.tab.linkedBrowser.contentTitle;
+ this.fullName = this.diff.names.join(", ");
+ this._show = true;
+ },
+ _promiseInitAddon: Task.async(function*() {
+ let found = yield (new Promise(resolve =>
+ AddonManager.getAddonByID(this.diff.addonId, a => {
+ if (a) {
+ this.readableName = a.name;
+ resolve(true);
+ } else {
+ resolve(false);
+ }
+ })));
+
+ this._initialized = true;
+
+ // If the add-on manager doesn't know about an add-on, it's
+ // probably not a real add-on.
+ this._show = found;
+ this.fullName = this.diff.addonId;
+ }),
+ toString: function() {
+ return `[Delta] ${this.diff.key} => ${this.readableName}, ${this.fullName}`;
+ }
+};
+
+Delta.compare = function(a, b) {
+ return (
+ (a.diff.jank.longestDuration - b.diff.jank.longestDuration) ||
+ (a.diff.jank.totalUserTime - b.diff.jank.totalUserTime) ||
+ (a.diff.jank.totalSystemTime - b.diff.jank.totalSystemTime) ||
+ (a.diff.cpow.totalCPOWTime - b.diff.cpow.totalCPOWTime) ||
+ (a.diff.ticks.ticks - b.diff.ticks.ticks) ||
+ 0
+ );
+};
+
+Delta.revCompare = function(a, b) {
+ return -Delta.compare(a, b);
+};
+
+/**
+ * The highest value considered "good performance".
+ */
+Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE = {
+ diff: {
+ 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".
+ */
+Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE = {
+ diff: {
+ 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
+ */
+var State = {
+ _monitor: PerformanceStats.getMonitor([
+ "jank", "cpow", "ticks",
+ ]),
+
+ /**
+ * Indexed by the number of minutes since the snapshot was taken.
+ *
+ * @type {Array<ApplicationSnapshot>}
+ */
+ _buffer: [],
+ /**
+ * The first snapshot since opening the page.
+ *
+ * @type ApplicationSnapshot
+ */
+ _oldest: null,
+
+ /**
+ * The latest snapshot.
+ *
+ * @type ApplicationSnapshot
+ */
+ _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<Delta key, Array<number>} A map in which the keys are provided
+ * by property `key` of instances of `Delta` and the values are arrays
+ * [number of moderate-impact alerts, number of high-impact alerts]
+ */
+ _alerts: new Map(),
+
+ /**
+ * 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, timestamp} A map in which keys are
+ * values for `delta.groupId` and values are approximate
+ * dates at which the group was first encountered, as provided
+ * by `Cu.now()``.
+ */
+ _firstSeen: new Map(),
+
+ /**
+ * Update the internal state.
+ *
+ * @return {Promise}
+ */
+ update: Task.async(function*() {
+ // 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._monitor.promiseSnapshot();
+ this._buffer.push(this._oldest);
+ yield wait(BUFFER_SAMPLING_RATE_MS * 1.1);
+ }
+
+
+ let now = Cu.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 = yield this._monitor.promiseSnapshot();
+ this._buffer.push(this._latest);
+ }
+
+ // 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}
+ * @resolve {{
+ * addons: Array<Delta>,
+ * webpages: Array<Delta>,
+ * deltas: Set<Delta key>,
+ * duration: number of milliseconds
+ * }}
+ */
+ _promiseDeltaSince: Task.async(function*(oldest) {
+ let current = this._latest;
+ if (!oldest) {
+ throw new TypeError();
+ }
+ if (!current) {
+ throw new TypeError();
+ }
+
+ tabFinder.update();
+ // We rebuild the maps 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();
+
+ let oldAlerts = this._alerts;
+ let cleanedUpAlerts = new Map();
+
+ let result = {
+ addons: [],
+ webpages: [],
+ deltas: new Set(),
+ duration: current.date - oldest.date
+ };
+
+ for (let kind of ["webpages", "addons"]) {
+ for (let [key, value] of current[kind]) {
+ let item = ObjectUtils.strict(new Delta(value.subtract(oldest[kind].get(key)), kind, current.date, oldFirstSeen, oldAlerts));
+ yield item.promiseInit();
+
+ if (!item.show) {
+ continue;
+ }
+ result[kind].push(item);
+ result.deltas.add(item.key);
+
+ for (let groupId of item.diff.groupIds) {
+ cleanedUpFirstSeen.set(groupId, item.creationDate);
+ }
+ cleanedUpAlerts.set(item.key, item.alerts);
+ }
+ }
+
+ this._firstSeen = cleanedUpFirstSeen;
+ this._alerts = cleanedUpAlerts;
+ return result;
+ }),
+};
+
+var 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} deltaKey The key for the item that we are displaying.
+ * @return {null} If the `deltaKey` doesn't have a component cached yet.
+ * Otherwise, the value stored with `set`.
+ */
+ get: function(deltaKey) {
+ return this._map.get(deltaKey);
+ },
+ set: function(deltaKey, value) {
+ this._map.set(deltaKey, value);
+ },
+ /**
+ * Remove all the elements whose key does not appear in `set`.
+ *
+ * @param {Set} set a set of deltaKey.
+ */
+ trimTo: function(set) {
+ let remove = [];
+ for (let key of this._map.keys()) {
+ if (!set.has(key)) {
+ remove.push(key);
+ }
+ }
+ for (let key of remove) {
+ this._map.delete(key);
+ }
+ }
+ },
+ /**
+ * 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, currentMode) {
+ subset = subset.slice().sort(Delta.revCompare);
+
+ let watcherAlerts = null;
+ if (nature == "addons") {
+ watcherAlerts = AddonWatcher.alerts;
+ }
+
+ // Grab everything from the DOM before cleaning up
+ this._setupStructure(id);
+
+ // An array of `cachedElements` that need to be added
+ let toAdd = [];
+ for (let delta of subset) {
+ if (!(delta instanceof Delta)) {
+ throw new TypeError();
+ }
+ 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.`
+
+ let processes = delta.diff.processes.map(proc => `${proc.processId} (${proc.isChildProcess?"child":"parent"})`);
+ cachedElements.eltProcess.textContent = `Processes: ${processes.join(", ")}`;
+ let jankSuffix = "";
+ if (watcherAlerts) {
+ let deltaAlerts = watcherAlerts.get(delta.diff.addonId);
+ if (deltaAlerts) {
+ if (deltaAlerts.occurrences) {
+ jankSuffix = ` (${deltaAlerts.occurrences} alerts)`;
+ }
+ }
+ }
+
+ let eltImpact = cachedElements.eltImpact;
+ if (currentMode == MODE_RECENT) {
+ cachedElements.eltRoot.setAttribute("impact", delta.diff.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}.`;
+ }
+
+ cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.diff.jank.longestDuration + 1}/${delta.diff.jank.durations.length}${jankSuffix}.`;
+ cachedElements.eltCPU.textContent = `CPU usage: ${Math.ceil(delta.diff.jank.totalCPUTime/delta.diff.deltaT/10)}%.`;
+ cachedElements.eltSystem.textContent = `System usage: ${Math.ceil(delta.diff.jank.totalSystemTime/delta.diff.deltaT/10)}%.`;
+ cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.diff.cpow.totalCPOWTime/delta.diff.deltaT/10)}%.`;
+ } else {
+ if (delta.alerts.length == 0) {
+ eltImpact.textContent = " has performed well so far.";
+ cachedElements.eltFPS.textContent = `Impact on framerate: no impact.`;
+ cachedElements.eltRoot.setAttribute("impact", 0);
+ } else {
+ let impact = 0;
+ let sum = /* medium impact */ delta.alerts[0] + /* high impact */ delta.alerts[1];
+ let frequency = sum * 1000 / delta.diff.deltaT;
+
+ let describeFrequency;
+ if (frequency <= MAX_FREQUENCY_FOR_NO_IMPACT) {
+ describeFrequency = `has no impact on the performance of ${BRAND_NAME}.`
+ } else {
+ let describeImpact;
+ if (frequency <= MAX_FREQUENCY_FOR_RARE) {
+ describeFrequency = `rarely slows down ${BRAND_NAME}.`;
+ impact += 1;
+ } else if (frequency <= MAX_FREQUENCY_FOR_FREQUENT) {
+ describeFrequency = `has slown down ${BRAND_NAME} frequently.`;
+ impact += 2.5;
+ } else {
+ describeFrequency = `seems to have slown down ${BRAND_NAME} very often.`;
+ impact += 5;
+ }
+ // At this stage, `sum != 0`
+ if (delta.alerts[1] / sum > MIN_PROPORTION_FOR_MAJOR_IMPACT) {
+ describeImpact = "When this happens, the slowdown is generally important."
+ impact *= 2;
+ } else {
+ describeImpact = "When this happens, the slowdown is generally noticeable."
+ }
+
+ eltImpact.textContent = ` ${describeFrequency} ${describeImpact}`;
+ cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.alerts[1] || 0} high-impacts, ${delta.alerts[0] || 0} medium-impact${jankSuffix}.`;
+ }
+ cachedElements.eltRoot.setAttribute("impact", Math.round(impact));
+ }
+
+ cachedElements.eltCPU.textContent = `CPU usage: ${Math.ceil(delta.diff.jank.totalCPUTime/delta.diff.deltaT/10)}% (total ${delta.diff.jank.totalUserTime}ms).`;
+ cachedElements.eltSystem.textContent = `System usage: ${Math.ceil(delta.diff.jank.totalSystemTime/delta.diff.deltaT/10)}% (total ${delta.diff.jank.totalSystemTime}ms).`;
+ cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.diff.cpow.totalCPOWTime/delta.diff.deltaT/10)}% (total ${delta.diff.cpow.totalCPOWTime}ms).`;
+ }
+ }
+ 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.key);
+ if (cachedElements) {
+ if (cachedElements.eltRoot.parentElement) {
+ cachedElements.eltRoot.parentElement.removeChild(cachedElements.eltRoot);
+ }
+ } else {
+ this.DOMCache.set(delta.key, cachedElements = {});
+
+ let eltDelta = document.createElement("li");
+ eltDelta.classList.add("delta");
+ cachedElements.eltRoot = eltDelta;
+
+ let eltSpan = document.createElement("span");
+ eltDelta.appendChild(eltSpan);
+
+ 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);
+
+ 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.diff.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"));
+
+ let eltCloseTab = document.createElement("button");
+ eltCloseTab.textContent = "Close tab";
+ eltSpan.appendChild(eltCloseTab);
+ let windowIds = delta.diff.windowIds;
+ eltCloseTab.addEventListener("click", () => {
+ let found = tabFinder.getAny(windowIds);
+ 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 = tabFinder.getAny(windowIds);
+ 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;
+ },
+};
+
+var Control = {
+ init: function() {
+ this._initAutorefresh();
+ this._initDisplayMode();
+ },
+ update: Task.async(function*() {
+ let mode = this._displayMode;
+ if (this._autoRefreshInterval || !State._buffer[0]) {
+ // Update the state only if we are not on pause.
+ yield State.update();
+ }
+ yield wait(0);
+ let state = yield (mode == MODE_GLOBAL?
+ State.promiseDeltaSinceStartOfTime():
+ State.promiseDeltaSinceStartOfBuffer());
+
+ for (let category of ["webpages", "addons"]) {
+ yield wait(0);
+ yield View.updateCategory(state[category], category, category, mode);
+ }
+ yield wait(0);
+
+ // Make sure that we do not keep obsolete stuff around.
+ View.DOMCache.trimTo(state.deltas);
+
+ yield wait(0);
+
+ // Inform watchers
+ Services.obs.notifyObservers(null, UPDATE_COMPLETE_TOPIC, mode);
+ }),
+ _setOptions: function(options) {
+ dump(`about:performance _setOptions ${JSON.stringify(options)}\n`);
+ 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,
+};
+
+/**
+ * This functionality gets memory related information of sub-processes and
+ * updates the performance table regularly.
+ * If the page goes hidden, it also handles visibility change by not
+ * querying the content processes unnecessarily.
+ */
+var SubprocessMonitor = {
+ _timeout: null,
+
+ /**
+ * Init will start the process of updating the table if the page is not hidden,
+ * and set up an event listener for handling visibility changes.
+ */
+ init: function() {
+ if (!document.hidden) {
+ SubprocessMonitor.updateTable();
+ }
+ document.addEventListener("visibilitychange", SubprocessMonitor.handleVisibilityChange);
+ },
+
+ /**
+ * This function updates the table after an interval if the page is visible
+ * and clears the interval otherwise.
+ */
+ handleVisibilityChange: function() {
+ if (!document.hidden) {
+ SubprocessMonitor.queueUpdate();
+ } else {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+ }
+ },
+
+ /**
+ * This function queues a timer to request the next summary using updateTable
+ * after some delay.
+ */
+ queueUpdate: function() {
+ this._timeout = setTimeout(() => this.updateTable(), UPDATE_INTERVAL_MS);
+ },
+
+ /**
+ * This is a helper function for updateTable, which updates a particular row.
+ * @param {<tr> node} row The row to be updated.
+ * @param {object} summaries The object with the updated RSS and USS values.
+ * @param {string} pid The pid represented by the row for which we update.
+ */
+ updateRow: function(row, summaries, pid) {
+ row.cells[0].textContent = pid;
+ let RSSval = DownloadUtils.convertByteUnits(summaries[pid].rss);
+ row.cells[1].textContent = RSSval.join(" ");
+ let USSval = DownloadUtils.convertByteUnits(summaries[pid].uss);
+ row.cells[2].textContent = USSval.join(" ");
+ },
+
+ /**
+ * This function adds a row to the subprocess-performance table for every new pid
+ * and populates and regularly updates it with RSS/USS measurements.
+ */
+ updateTable: function() {
+ if (!document.hidden) {
+ Memory.summary().then((summaries) => {
+ if (!(Object.keys(summaries).length)) {
+ // The summaries list was empty, which means we timed out getting
+ // the memory reports. We'll try again later.
+ SubprocessMonitor.queueUpdate();
+ return;
+ }
+ let resultTable = document.getElementById("subprocess-reports");
+ let recycle = [];
+ // We first iterate the table to check if summaries exist for rowPids,
+ // if yes, update them and delete the pid's summary or else hide the row
+ // for recycling it. Start at row 1 instead of 0 (to skip the header row).
+ for (let i = 1, row; row = resultTable.rows[i]; i++) {
+ let rowPid = row.dataset.pid;
+ let summary = summaries[rowPid];
+ if (summary) {
+ // Now we update the values in the row, which is hardcoded for now,
+ // but we might want to make this more adaptable in the future.
+ SubprocessMonitor.updateRow(row, summaries, rowPid);
+ delete summaries[rowPid];
+ } else {
+ // Take this unnecessary row, hide it and stash it for potential re-use.
+ row.hidden = true;
+ recycle.push(row);
+ }
+ }
+ // For the remaining pids in summaries, we choose from the recyclable
+ // (hidden) nodes, and if they get exhausted, append a row to the table.
+ for (let pid in summaries) {
+ let row = recycle.pop();
+ if (row) {
+ row.hidden = false;
+ } else {
+ // We create a new row here, and set it to row
+ row = document.createElement("tr");
+ // Insert cell for pid
+ row.insertCell();
+ // Insert a cell for USS.
+ row.insertCell();
+ // Insert another cell for RSS.
+ row.insertCell();
+ }
+ row.dataset.pid = pid;
+ // Update the row and put it at the bottom
+ SubprocessMonitor.updateRow(row, summaries, pid);
+ resultTable.appendChild(row);
+ }
+ });
+ SubprocessMonitor.queueUpdate();
+ }
+ },
+};
+
+var go = Task.async(function*() {
+
+ SubprocessMonitor.init();
+ 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 wait(BUFFER_SAMPLING_RATE_MS * 1.1);
+ yield Control.update();
+});
diff --git a/toolkit/components/aboutperformance/content/aboutPerformance.xhtml b/toolkit/components/aboutperformance/content/aboutPerformance.xhtml
new file mode 100644
index 000000000..6e0d14802
--- /dev/null
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.xhtml
@@ -0,0 +1,188 @@
+<?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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>about:performance</title>
+ <link rel="icon" type="image/png" id="favicon"
+ href="chrome://branding/content/icon32.png"/>
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"
+ type="text/css"/>
+ <script type="text/javascript;version=1.8" src="chrome://global/content/aboutPerformance.js"></script>
+ <style>
+ @import url("chrome://global/skin/in-content/common.css");
+
+ html {
+ --aboutSupport-table-background: #ebebeb;
+ background-color: var(--in-content-page-background);
+ }
+ body {
+ margin: 40px 48px;
+ }
+ .hidden {
+ display: none;
+ }
+ .summary .title {
+ font-weight: bold;
+ }
+ a {
+ text-decoration: none;
+ }
+ a.more {
+ margin-left: 2ch;
+ }
+ ul.hidden_additional_items {
+ padding-top: 0;
+ margin-top: 0;
+ }
+ ul.visible_items {
+ padding-bottom: 0;
+ margin-bottom: 0;
+ }
+ li.delta {
+ margin-top: .5em;
+ }
+ h2 {
+ margin-top: 1cm;
+ }
+ button.show_all_items {
+ margin-top: .5cm;
+ margin-left: 1cm;
+ }
+ body {
+ margin-left: 1cm;
+ }
+ div.measuring {
+ background: url(chrome://global/skin/media/throbber.png) no-repeat center;
+ min-width: 36px;
+ min-height: 36px;
+ }
+ 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);
+ }
+ 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);
+ }
+ 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);
+ }
+ li.delta[impact="8"] {
+ border-left-color: rgb(192, 63, 0);
+ }
+ li.delta[impact="9"] {
+ border-left-color: rgb(216, 39, 0);
+ }
+ li.delta[impact="10"] {
+ border-left-color: rgb(240, 15, 0);
+ }
+ li.delta[impact="11"] {
+ border-left-color: rgb(255, 0, 0);
+ }
+
+ #subprocess-reports {
+ background-color: var(--aboutSupport-table-background);
+ color: var(--in-content-text-color);
+ font: message-box;
+ text-align: start;
+ border: 1px solid var(--in-content-border-color);
+ border-spacing: 0px;
+ float: right;
+ margin-bottom: 20px;
+ -moz-margin-start: 20px;
+ -moz-margin-end: 0;
+ width: 100%;
+ }
+ #subprocess-reports:-moz-dir(rtl) {
+ float: left;
+ }
+ #subprocess-reports th,
+ #subprocess-reports td {
+ border: 1px solid var(--in-content-border-color);
+ padding: 4px;
+ }
+ #subprocess-reports thead th {
+ text-align: center;
+ }
+ #subprocess-reports th {
+ text-align: start;
+ background-color: var(--in-content-table-header-background);
+ color: var(--in-content-selected-text);
+ }
+ #subprocess-reports th.column {
+ white-space: nowrap;
+ width: 0px;
+ }
+ #subprocess-reports td {
+ background-color: #ebebeb;
+ text-align: start;
+ border-color: var(--in-content-table-border-dark-color);
+ border-spacing: 40px;
+ }
+ .options {
+ width: 100%;
+ }
+ .options > .toggle-container-with-text {
+ display: inline-flex;
+ }
+ .options > .toggle-container-with-text:not(:first-child) {
+ margin-inline-start: 2ch;
+ }
+ </style>
+ </head>
+ <body onload="go()">
+ <div>
+ <h2>Memory usage of Subprocesses</h2>
+ <table id="subprocess-reports">
+ <tr>
+ <th>Process ID</th>
+ <th title="RSS measures the pages resident in the main memory for the process">Resident Set Size</th>
+ <th title="USS gives a count of unshared pages, unique to the process">Unique Set Size</th>
+ </tr>
+ </table>
+ </div>
+ <div class="options">
+ <div class="toggle-container-with-text">
+ <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>
+ </div>
+ <div class="toggle-container-with-text">
+ <input type="checkbox" checked="true" id="check-autorefresh"></input>
+ <label for="check-autorefresh">Refresh automatically</label>
+ </div>
+ </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>
diff --git a/toolkit/components/aboutperformance/jar.mn b/toolkit/components/aboutperformance/jar.mn
new file mode 100644
index 000000000..96e046d8e
--- /dev/null
+++ b/toolkit/components/aboutperformance/jar.mn
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+ content/global/aboutPerformance.xhtml (content/aboutPerformance.xhtml)
+ content/global/aboutPerformance.js (content/aboutPerformance.js)
diff --git a/toolkit/components/aboutperformance/moz.build b/toolkit/components/aboutperformance/moz.build
new file mode 100644
index 000000000..d8e6acd95
--- /dev/null
+++ b/toolkit/components/aboutperformance/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
diff --git a/toolkit/components/aboutperformance/tests/browser/.eslintrc.js b/toolkit/components/aboutperformance/tests/browser/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/aboutperformance/tests/browser/browser.ini b/toolkit/components/aboutperformance/tests/browser/browser.ini
new file mode 100644
index 000000000..92f1d98e6
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head = head.js
+support-files =
+ browser_compartments.html
+ browser_compartments_frame.html
+ browser_compartments_script.js
+
+[browser_aboutperformance.js]
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js b/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js
new file mode 100644
index 000000000..60760ea7f
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js
@@ -0,0 +1,300 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://testing-common/ContentTask.jsm", this);
+
+const URL = "http://example.com/browser/toolkit/components/aboutperformance/tests/browser/browser_compartments.html?test=" + Math.random();
+
+// This function is injected as source as a frameScript
+function frameScript() {
+ "use strict";
+
+ addMessageListener("aboutperformance-test:done", () => {
+ 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:closeTab", ({data: options}) => {
+ let observer = function(subject, topic, mode) {
+ dump(`aboutperformance-test:closeTab 1 ${options.url}\n`);
+ Services.obs.removeObserver(observer, "about:performance-update-complete");
+
+ let exn;
+ let found = false;
+ try {
+ for (let eltContent of content.document.querySelectorAll("li.delta")) {
+ let eltName = eltContent.querySelector("li.name");
+ if (!eltName.textContent.includes(options.url)) {
+ continue;
+ }
+
+ found = true;
+ let [eltCloseTab, eltReloadTab] = eltContent.querySelectorAll("button");
+ let button;
+ if (options.mode == "reload") {
+ button = eltReloadTab;
+ } else if (options.mode == "close") {
+ button = eltCloseTab;
+ } else {
+ throw new TypeError(options.mode);
+ }
+ dump(`aboutperformance-test:closeTab clicking on ${button.textContent}\n`);
+ button.click();
+ return;
+ }
+ } catch (ex) {
+ dump(`aboutperformance-test:closeTab: error ${ex}\n`);
+ exn = ex;
+ } finally {
+ if (exn) {
+ sendAsyncMessage("aboutperformance-test:closeTab", { error: {message: exn.message, lineNumber: exn.lineNumber, fileName: exn.fileName}, found});
+ } else {
+ sendAsyncMessage("aboutperformance-test:closeTab", { ok: true, found });
+ }
+ }
+ }
+ Services.obs.addObserver(observer, "about:performance-update-complete", false);
+ Services.obs.notifyObservers(null, "test-about:performance-test-driver", JSON.stringify(options));
+ });
+
+ addMessageListener("aboutperformance-test:checkSanity", ({data: options}) => {
+ let exn = null;
+ try {
+ let reFullname = /Full name: (.+)/;
+ let reFps = /Impact on framerate: (\d+)\/10( \((\d+) alerts\))?/;
+ let reCpow = /Blocking process calls: (\d+)%( \((\d+) alerts\))?/;
+
+ let getContentOfSelector = function(eltContainer, selector, re) {
+ let elt = eltContainer.querySelector(selector);
+ if (!elt) {
+ throw new Error(`No item ${selector}`);
+ }
+
+ if (!re) {
+ return undefined;
+ }
+
+ let match = elt.textContent.match(re);
+ if (!match) {
+ throw new Error(`Item ${selector} doesn't match regexp ${re}: ${elt.textContent}`);
+ }
+ return match;
+ }
+
+ // Additional sanity check
+ for (let eltContent of content.document.querySelectorAll("delta")) {
+ // Do we have an attribute "impact"? Is it a number between 0 and 10?
+ let impact = eltContent.classList.getAttribute("impact");
+ let value = Number.parseInt(impact);
+ if (isNaN(value) || value < 0 || value > 10) {
+ throw new Error(`Incorrect value ${value}`);
+ }
+
+ // Do we have a button "more"?
+ getContentOfSelector(eltContent, "a.more");
+
+ // Do we have details?
+ getContentOfSelector(eltContent, "ul.details");
+
+ // Do we have a full name? Does it make sense?
+ getContentOfSelector(eltContent, "li.name", reFullname);
+
+ // Do we have an impact on framerate? Does it make sense?
+ let [, jankStr,, alertsStr] = getContentOfSelector(eltDetails, "li.fps", reFps);
+ let jank = Number.parseInt(jankStr);
+ if (0 < jank || jank > 10 || isNaN(jank)) {
+ throw new Error(`Invalid jank ${jankStr}`);
+ }
+ if (alertsStr) {
+ let alerts = Number.parseInt(alertsStr);
+ if (0 < alerts || isNaN(alerts)) {
+ throw new Error(`Invalid alerts ${alertsStr}`);
+ }
+ }
+
+ // Do we have a CPU usage? Does it make sense?
+ let [, cpuStr] = getContentOfSelector(eltDetails, "li.cpu", reCPU);
+ let cpu = Number.parseInt(cpuStr);
+ if (0 < cpu || isNaN(cpu)) { // Note that cpu can be > 100%.
+ throw new Error(`Invalid CPU ${cpuStr}`);
+ }
+
+ // Do we have CPOW? Does it make sense?
+ let [, cpowStr,, alertsStr2] = getContentOfSelector(eltDetails, "li.cpow", reCpow);
+ let cpow = Number.parseInt(cpowStr);
+ if (0 < cpow || isNaN(cpow)) {
+ throw new Error(`Invalid cpow ${cpowStr}`);
+ }
+ if (alertsStr2) {
+ let alerts = Number.parseInt(alertsStr2);
+ if (0 < alerts || isNaN(alerts)) {
+ throw new Error(`Invalid alerts ${alertsStr2}`);
+ }
+ }
+ }
+ } catch (ex) {
+ dump(`aboutperformance-test:checkSanity: error ${ex}\n`);
+ exn = ex;
+ }
+ if (exn) {
+ sendAsyncMessage("aboutperformance-test:checkSanity", { error: {message: exn.message, lineNumber: exn.lineNumber, fileName: exn.fileName}});
+ } else {
+ sendAsyncMessage("aboutperformance-test:checkSanity", { ok: true });
+ }
+ });
+
+ addMessageListener("aboutperformance-test:hasItems", ({data: {title, options}}) => {
+ let observer = function(subject, topic, mode) {
+ Services.obs.removeObserver(observer, "about:performance-update-complete");
+ let hasTitleInWebpages = false;
+ let hasTitleInAddons = false;
+
+ try {
+ let eltWeb = content.document.getElementById("webpages");
+ let eltAddons = content.document.getElementById("addons");
+ if (!eltWeb || !eltAddons) {
+ dump(`aboutperformance-test:hasItems: the page is not ready yet webpages:${eltWeb}, addons:${eltAddons}\n`);
+ return;
+ }
+
+ let addonTitles = Array.from(eltAddons.querySelectorAll("span.title"), elt => elt.textContent);
+ let webTitles = Array.from(eltWeb.querySelectorAll("span.title"), elt => elt.textContent);
+
+ hasTitleInAddons = addonTitles.includes(title);
+ hasTitleInWebpages = webTitles.includes(title);
+ } catch (ex) {
+ Cu.reportError("Error in content: " + ex);
+ Cu.reportError(ex.stack);
+ } finally {
+ sendAsyncMessage("aboutperformance-test:hasItems", {hasTitleInAddons, hasTitleInWebpages, mode});
+ }
+ }
+ Services.obs.addObserver(observer, "about:performance-update-complete", false);
+ Services.obs.notifyObservers(null, "test-about:performance-test-driver", JSON.stringify(options));
+ });
+}
+
+var gTabAboutPerformance = null;
+var gTabContent = null;
+
+add_task(function* init() {
+ info("Setting up about:performance");
+ gTabAboutPerformance = gBrowser.selectedTab = gBrowser.addTab("about:performance");
+ yield ContentTask.spawn(gTabAboutPerformance.linkedBrowser, null, frameScript);
+
+ info(`Setting up ${URL}`);
+ gTabContent = gBrowser.addTab(URL);
+ yield ContentTask.spawn(gTabContent.linkedBrowser, null, frameScript);
+});
+
+var promiseExpectContent = Task.async(function*(options) {
+ let title = "Testing about:performance " + Math.random();
+ for (let i = 0; i < 30; ++i) {
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield promiseContentResponse(gTabContent.linkedBrowser, "aboutperformance-test:setTitle", title);
+ let {hasTitleInWebpages, hasTitleInAddons, mode} = (yield promiseContentResponse(gTabAboutPerformance.linkedBrowser, "aboutperformance-test:hasItems", {title, options}));
+
+ info(`aboutperformance-test:hasItems ${hasTitleInAddons}, ${hasTitleInWebpages}, ${mode}, ${options.displayRecent}`);
+ if (!hasTitleInWebpages) {
+ info(`Title not found in webpages`);
+ continue;
+ }
+ if ((mode == "recent") != options.displayRecent) {
+ info(`Wrong mode`);
+ continue;
+ }
+ Assert.ok(!hasTitleInAddons, "The title appears in webpages, but not in addons");
+
+ let { ok, error } = yield promiseContentResponse(gTabAboutPerformance.linkedBrowser, "aboutperformance-test:checkSanity", {options});
+ if (ok) {
+ info("aboutperformance-test:checkSanity: success");
+ }
+ if (error) {
+ Assert.ok(false, `aboutperformance-test:checkSanity error: ${JSON.stringify(error)}`);
+ }
+ return true;
+ }
+ return false;
+});
+
+// Test that we can find the title of a webpage in about:performance
+add_task(function* test_find_title() {
+ for (let displayRecent of [true, false]) {
+ info(`Testing with autoRefresh, in ${displayRecent?"recent":"global"} mode`);
+ let found = yield promiseExpectContent({autoRefresh: 100, displayRecent});
+ Assert.ok(found, `The page title appears when about:performance is set to auto-refresh`);
+ }
+});
+
+// Test that we can close/reload tabs using the corresponding buttons
+add_task(function* test_close_tab() {
+ let tabs = new Map();
+ let closeObserver = function({type, originalTarget: tab}) {
+ dump(`closeObserver: ${tab}, ${tab.constructor.name}, ${tab.tagName}, ${type}\n`);
+ let cb = tabs.get(tab);
+ if (cb) {
+ cb(type);
+ }
+ };
+ let promiseTabClosed = function(tab) {
+ return new Promise(resolve => tabs.set(tab, resolve));
+ }
+ window.gBrowser.tabContainer.addEventListener("TabClose", closeObserver);
+ let promiseTabReloaded = function(tab) {
+ return new Promise(resolve =>
+ tab.linkedBrowser.contentDocument.addEventListener("readystatechange", resolve)
+ );
+ }
+ for (let displayRecent of [true, false]) {
+ for (let mode of ["close", "reload"]) {
+ let URL = `about:about?display-recent=${displayRecent}&mode=${mode}&salt=${Math.random()}`;
+ info(`Setting up ${URL}`);
+ let tab = gBrowser.addTab(URL);
+ yield ContentTask.spawn(tab.linkedBrowser, null, frameScript);
+ let promiseClosed = promiseTabClosed(tab);
+ let promiseReloaded = promiseTabReloaded(tab);
+
+ info(`Requesting close`);
+ do {
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield promiseContentResponse(tab.linkedBrowser, "aboutperformance-test:setTitle", URL);
+
+ let {ok, found, error} = yield promiseContentResponse(gTabAboutPerformance.linkedBrowser, "aboutperformance-test:closeTab", {url: URL, autoRefresh: true, mode, displayRecent});
+ Assert.ok(ok, `Message aboutperformance-test:closeTab was handled correctly ${JSON.stringify(error)}`);
+ info(`URL ${URL} ${found?"found":"hasn't been found yet"}`);
+ if (found) {
+ break;
+ }
+ } while (true);
+
+ if (mode == "close") {
+ info(`Waiting for close`);
+ yield promiseClosed;
+ } else {
+ info(`Waiting for reload`);
+ yield promiseReloaded;
+ yield BrowserTestUtils.removeTab(tab);
+ }
+ }
+ }
+});
+
+add_task(function* cleanup() {
+ // Cleanup
+ info("Cleaning up");
+ 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;
+});
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_compartments.html b/toolkit/components/aboutperformance/tests/browser/browser_compartments.html
new file mode 100644
index 000000000..a74a5745a
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_compartments.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>
+ Main frame for test browser_aboutperformance.js
+ </title>
+</head>
+<body>
+Main frame.
+
+<iframe src="browser_compartments_frame.html?frame=1">
+ Subframe 1
+</iframe>
+
+<iframe src="browser_compartments_frame.html?frame=2">
+ Subframe 2.
+</iframe>
+
+</body>
+</html>
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html b/toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html
new file mode 100644
index 000000000..69edfe871
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>
+ Subframe for test browser_compartments.html (do not change this title)
+ </title>
+ <script src="browser_compartments_script.js"></script>
+</head>
+<body>
+Subframe loaded.
+</body>
+</html>
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js b/toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js
new file mode 100644
index 000000000..3d5f7114f
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js
@@ -0,0 +1,29 @@
+
+var carryOn = true;
+
+window.addEventListener("message", e => {
+ console.log("frame content", "message", e);
+ if ("title" in e.data) {
+ document.title = e.data.title;
+ }
+ if ("stop" in e.data) {
+ carryOn = false;
+ }
+});
+
+// Use some CPU.
+var interval = window.setInterval(() => {
+ if (!carryOn) {
+ window.clearInterval(interval);
+ return;
+ }
+
+ // Compute an arbitrary value, print it out to make sure that the JS
+ // engine doesn't discard all our computation.
+ var date = Date.now();
+ var array = [];
+ var i = 0;
+ while (Date.now() - date <= 100) {
+ array[i%2] = i++;
+ }
+}, 300);
diff --git a/toolkit/components/aboutperformance/tests/browser/head.js b/toolkit/components/aboutperformance/tests/browser/head.js
new file mode 100644
index 000000000..a15536ffd
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/head.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+function promiseContentResponse(browser, name, message) {
+ let mm = browser.messageManager;
+ let promise = new Promise(resolve => {
+ function removeListener() {
+ mm.removeMessageListener(name, listener);
+ }
+
+ function listener(msg) {
+ removeListener();
+ resolve(msg.data);
+ }
+
+ mm.addMessageListener(name, listener);
+ registerCleanupFunction(removeListener);
+ });
+ mm.sendAsyncMessage(name, message);
+ return promise;
+}
+function promiseContentResponseOrNull(browser, name, message) {
+ if (!browser.messageManager) {
+ return null;
+ }
+ return promiseContentResponse(browser, name, message);
+}
+
+/**
+ * `true` if we are running an OS in which the OS performance
+ * clock has a low precision and might unpredictably
+ * never be updated during the execution of the test.
+ */
+function hasLowPrecision() {
+ let [sysName, sysVersion] = [Services.sysinfo.getPropertyAsAString("name"), Services.sysinfo.getPropertyAsDouble("version")];
+ info(`Running ${sysName} version ${sysVersion}`);
+
+ if (sysName == "Windows_NT" && sysVersion < 6) {
+ info("Running old Windows, need to deactivate tests due to bad precision.");
+ return true;
+ }
+ if (sysName == "Linux" && sysVersion <= 2.6) {
+ info("Running old Linux, need to deactivate tests due to bad precision.");
+ return true;
+ }
+ info("This platform has good precision.")
+ return false;
+}