summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutperformance/content/aboutPerformance.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/aboutperformance/content/aboutPerformance.js')
-rw-r--r--toolkit/components/aboutperformance/content/aboutPerformance.js1077
1 files changed, 1077 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();
+});