diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/components/aboutperformance | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/aboutperformance')
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 0000000000..3b191d8950 --- /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 0000000000..6e0d148021 --- /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 0000000000..96e046d8ee --- /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 0000000000..d8e6acd958 --- /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 0000000000..7c80211924 --- /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 0000000000..92f1d98e6a --- /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 0000000000..60760ea7f8 --- /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 0000000000..a74a5745af --- /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 0000000000..69edfe871b --- /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 0000000000..3d5f7114f6 --- /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 0000000000..a15536ffd3 --- /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; +} |