diff options
Diffstat (limited to 'toolkit/components/perfmonitoring')
28 files changed, 5990 insertions, 0 deletions
diff --git a/toolkit/components/perfmonitoring/AddonWatcher.jsm b/toolkit/components/perfmonitoring/AddonWatcher.jsm new file mode 100644 index 000000000..58decba85 --- /dev/null +++ b/toolkit/components/perfmonitoring/AddonWatcher.jsm @@ -0,0 +1,239 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* 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"; + +this.EXPORTED_SYMBOLS = ["AddonWatcher"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Preferences", + "resource://gre/modules/Preferences.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/Console.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PerformanceWatcher", + "resource://gre/modules/PerformanceWatcher.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", + "@mozilla.org/base/telemetry;1", + Ci.nsITelemetry); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "IdleService", + "@mozilla.org/widget/idleservice;1", + Ci.nsIIdleService); + +/** + * Don't notify observers of slow add-ons if at least `SUSPICIOUSLY_MANY_ADDONS` + * show up at the same time. We assume that this indicates that the system itself + * is busy, and that add-ons are not responsible. + */ +let SUSPICIOUSLY_MANY_ADDONS = 5; + +this.AddonWatcher = { + /** + * Watch this topic to be informed when a slow add-on is detected and should + * be reported to the user. + * + * If you need finer-grained control, use PerformanceWatcher.jsm. + */ + TOPIC_SLOW_ADDON_DETECTED: "addon-watcher-detected-slow-addon", + + init: function() { + this._initializedTimeStamp = Cu.now(); + + try { + this._ignoreList = new Set(JSON.parse(Preferences.get("browser.addon-watch.ignore", null))); + } catch (ex) { + // probably some malformed JSON, ignore and carry on + this._ignoreList = new Set(); + } + + this._warmupPeriod = Preferences.get("browser.addon-watch.warmup-ms", 60 * 1000 /* 1 minute */); + this._idleThreshold = Preferences.get("browser.addon-watch.deactivate-after-idle-ms", 3000); + this.paused = false; + }, + uninit: function() { + this.paused = true; + }, + _initializedTimeStamp: 0, + + set paused(paused) { + if (paused) { + if (this._listener) { + PerformanceWatcher.removePerformanceListener({addonId: "*"}, this._listener); + } + this._listener = null; + } else { + this._listener = this._onSlowAddons.bind(this); + PerformanceWatcher.addPerformanceListener({addonId: "*"}, this._listener); + } + }, + get paused() { + return !this._listener; + }, + _listener: null, + + /** + * Provide the following object for each addon: + * {number} occurrences The total number of performance alerts recorded for this addon. + * {number} occurrencesSinceLastNotification The number of performances alerts recorded + * since we last notified the user. + * {number} latestNotificationTimeStamp The timestamp of the latest user notification + * that this add-on is slow. + */ + _getAlerts: function(addonId) { + let alerts = this._alerts.get(addonId); + if (!alerts) { + alerts = { + occurrences: 0, + occurrencesSinceLastNotification: 0, + latestNotificationTimeStamp: 0, + }; + this._alerts.set(addonId, alerts); + } + return alerts; + }, + _alerts: new Map(), + _onSlowAddons: function(addons) { + try { + if (IdleService.idleTime >= this._idleThreshold) { + // The application is idle. Maybe the computer is sleeping, or maybe + // the user isn't in front of it. Regardless, the user doesn't care + // about things that slow down her browser while she's not using it. + return; + } + + if (addons.length > SUSPICIOUSLY_MANY_ADDONS) { + // Heuristic: if we are notified of many slow addons at once, the issue + // is probably not with the add-ons themselves with the system. We may + // for instance be waking up from hibernation, or the system may be + // busy swapping. + return; + } + + let now = Cu.now(); + if (now - this._initializedTimeStamp < this._warmupPeriod) { + // Heuristic: do not report slowdowns during or just after startup. + return; + } + + // Report immediately to Telemetry, regardless of whether we report to + // the user. + for (let {source: {addonId}, details} of addons) { + Telemetry.getKeyedHistogramById("PERF_MONITORING_SLOW_ADDON_JANK_US"). + add(addonId, details.highestJank); + Telemetry.getKeyedHistogramById("PERF_MONITORING_SLOW_ADDON_CPOW_US"). + add(addonId, details.highestCPOW); + } + + // We expect that users don't care about real-time alerts unless their + // browser is going very, very slowly. Therefore, we use the following + // heuristic: + // - if jank is above freezeThreshold (e.g. 5 seconds), report immediately; otherwise + // - if jank is below jankThreshold (e.g. 128ms), disregard; otherwise + // - if the latest jank was more than prescriptionDelay (e.g. 5 minutes) ago, reset number of occurrences; + // - if we have had fewer than occurrencesBetweenAlerts janks (e.g. 3) since last alert, disregard; otherwise + // - if we have displayed an alert for this add-on less than delayBetweenAlerts ago (e.g. 6h), disregard; otherwise + // - also, don't report more than highestNumberOfAddonsToReport (e.g. 1) at once. + let freezeThreshold = Preferences.get("browser.addon-watch.freeze-threshold-micros", /* 5 seconds */ 5000000); + let jankThreshold = Preferences.get("browser.addon-watch.jank-threshold-micros", /* 256 ms == 8 frames*/ 256000); + let occurrencesBetweenAlerts = Preferences.get("browser.addon-watch.occurrences-between-alerts", 3); + let delayBetweenAlerts = Preferences.get("browser.addon-watch.delay-between-alerts-ms", 6 * 3600 * 1000 /* 6h */); + let delayBetweenFreezeAlerts = Preferences.get("browser.addon-watch.delay-between-freeze-alerts-ms", 2 * 60 * 1000 /* 2 min */); + let prescriptionDelay = Preferences.get("browser.addon-watch.prescription-delay", 5 * 60 * 1000 /* 5 minutes */); + let highestNumberOfAddonsToReport = Preferences.get("browser.addon-watch.max-simultaneous-reports", 1); + + addons = addons.filter(x => x.details.highestJank >= jankThreshold). + sort((a, b) => a.details.highestJank - b.details.highestJank); + + for (let {source: {addonId}, details} of addons) { + if (highestNumberOfAddonsToReport <= 0) { + return; + } + if (this._ignoreList.has(addonId)) { + // Add-on is ignored. + continue; + } + + let alerts = this._getAlerts(addonId); + if (now - alerts.latestOccurrence >= prescriptionDelay) { + // While this add-on has already caused slownesss, this + // was a long time ago, let's forgive. + alerts.occurrencesSinceLastNotification = 0; + } + + alerts.occurrencesSinceLastNotification++; + alerts.occurrences++; + + if (details.highestJank < freezeThreshold) { + if (alerts.occurrencesSinceLastNotification <= occurrencesBetweenAlerts) { + // While the add-on has caused jank at least once, we are only + // interested in repeat offenders. Store the data for future use. + continue; + } + if (now - alerts.latestNotificationTimeStamp <= delayBetweenAlerts) { + // We have already displayed an alert for this add-on recently. + // Wait a little before displaying another one. + continue; + } + } else if (now - alerts.latestNotificationTimeStamp <= delayBetweenFreezeAlerts) { + // Even in case of freeze, we want to avoid needlessly spamming the user. + // We have already displayed an alert for this add-on recently. + // Wait a little before displaying another one. + continue; + } + + // Ok, time to inform the user. + alerts.latestNotificationTimeStamp = now; + alerts.occurrencesSinceLastNotification = 0; + Services.obs.notifyObservers(null, this.TOPIC_SLOW_ADDON_DETECTED, addonId); + + highestNumberOfAddonsToReport--; + } + } catch (ex) { + Cu.reportError("Error in AddonWatcher._onSlowAddons " + ex); + Cu.reportError(Task.Debugging.generateReadableStack(ex.stack)); + } + }, + + ignoreAddonForSession: function(addonid) { + this._ignoreList.add(addonid); + }, + ignoreAddonPermanently: function(addonid) { + this._ignoreList.add(addonid); + try { + let ignoreList = JSON.parse(Preferences.get("browser.addon-watch.ignore", "[]")) + if (!ignoreList.includes(addonid)) { + ignoreList.push(addonid); + Preferences.set("browser.addon-watch.ignore", JSON.stringify(ignoreList)); + } + } catch (ex) { + Preferences.set("browser.addon-watch.ignore", JSON.stringify([addonid])); + } + }, + + /** + * The list of alerts for this session. + * + * @type {Map<String, Object>} A map associating addonId to + * objects with fields + * {number} occurrences The total number of performance alerts recorded for this addon. + * {number} occurrencesSinceLastNotification The number of performances alerts recorded + * since we last notified the user. + * {number} latestNotificationTimeStamp The timestamp of the latest user notification + * that this add-on is slow. + */ + get alerts() { + let result = new Map(); + for (let [k, v] of this._alerts) { + result.set(k, Cu.cloneInto(v, this)); + } + return result; + }, +}; diff --git a/toolkit/components/perfmonitoring/PerformanceStats-content.js b/toolkit/components/perfmonitoring/PerformanceStats-content.js new file mode 100644 index 000000000..9a6a2d81d --- /dev/null +++ b/toolkit/components/perfmonitoring/PerformanceStats-content.js @@ -0,0 +1,144 @@ +/* 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/. */ + +/** + * A proxy implementing communication between the PerformanceStats.jsm modules + * of the parent and children processes. + * + * This script is loaded in all processes but is essentially a NOOP in the + * parent process. + */ + +"use strict"; + +var { utils: Cu, classes: Cc, interfaces: Ci } = Components; +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); + +XPCOMUtils.defineLazyModuleGetter(this, "PerformanceStats", + "resource://gre/modules/PerformanceStats.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +/** + * A global performance monitor used by this process. + * + * For the sake of simplicity, rather than attempting to map each PerformanceMonitor + * of the parent to a PerformanceMonitor in each child process, we maintain a single + * PerformanceMonitor in each child process. Probes activation/deactivation for this + * monitor is controlled by the activation/deactivation of probes in the parent. + * + * In the parent, this is always an empty monitor. + */ +var gMonitor = PerformanceStats.getMonitor([]); + +/** + * `true` if this is a content process, `false` otherwise. + */ +var isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; + +/** + * Handle message `performance-stats-service-acquire`: ensure that the global + * monitor has a given probe. This message must be sent by the parent process + * whenever a probe is activated application-wide. + * + * Note that we may miss acquire messages if they are sent before this process is + * launched. For this reason, `performance-stats-service-collect` automatically + * re-acquires probes if it realizes that they are missing. + * + * This operation is a NOOP on the parent process. + * + * @param {{payload: Array<string>}} msg.data The message received. `payload` + * must be an array of probe names. + */ +Services.cpmm.addMessageListener("performance-stats-service-acquire", function(msg) { + if (!isContent) { + return; + } + let name = msg.data.payload; + ensureAcquired(name); +}); + +/** + * Handle message `performance-stats-service-release`: release a given probe + * from the global monitor. This message must be sent by the parent process + * whenever a probe is deactivated application-wide. + * + * Note that we may miss release messages if they are sent before this process is + * launched. This is ok, as probes are inactive by default: if we miss the release + * message, we have already missed the acquire message, and the effect of both + * messages together is to reset to the default state. + * + * This operation is a NOOP on the parent process. + * + * @param {{payload: Array<string>}} msg.data The message received. `payload` + * must be an array of probe names. + */ +Services.cpmm.addMessageListener("performance-stats-service-release", function(msg) { + if (!isContent) { + return; + } + + // Keep only the probes that do not appear in the payload + let probes = gMonitor.probeNames + .filter(x => msg.data.payload.indexOf(x) == -1); + gMonitor = PerformanceStats.getMonitor(probes); +}); + +/** + * Ensure that this process has all the probes it needs. + * + * @param {Array<string>} probeNames The name of all probes needed by the + * process. + */ +function ensureAcquired(probeNames) { + let alreadyAcquired = gMonitor.probeNames; + + // Algorithm is O(n^2) because we expect that n ≤ 3. + let shouldAcquire = []; + for (let probeName of probeNames) { + if (alreadyAcquired.indexOf(probeName) == -1) { + shouldAcquire.push(probeName) + } + } + + if (shouldAcquire.length == 0) { + return; + } + gMonitor = PerformanceStats.getMonitor([...alreadyAcquired, ...shouldAcquire]); +} + +/** + * Handle message `performance-stats-service-collected`: collect the data + * obtained by the monitor. This message must be sent by the parent process + * whenever we grab a performance snapshot of the application. + * + * This operation provides `null` on the parent process. + * + * @param {{data: {payload: Array<string>}}} msg The message received. `payload` + * must be an array of probe names. + */ +Services.cpmm.addMessageListener("performance-stats-service-collect", Task.async(function*(msg) { + let {id, payload: {probeNames}} = msg.data; + if (!isContent) { + // This message was sent by the parent process to itself. + // As per protocol, respond `null`. + Services.cpmm.sendAsyncMessage("performance-stats-service-collect", { + id, + data: null + }); + return; + } + + // We may have missed acquire messages if the process was loaded too late. + // Catch up now. + ensureAcquired(probeNames); + + // Collect and return data. + let data = yield gMonitor.promiseSnapshot({probeNames}); + Services.cpmm.sendAsyncMessage("performance-stats-service-collect", { + id, + data + }); +})); diff --git a/toolkit/components/perfmonitoring/PerformanceStats.jsm b/toolkit/components/perfmonitoring/PerformanceStats.jsm new file mode 100644 index 000000000..20f27a51b --- /dev/null +++ b/toolkit/components/perfmonitoring/PerformanceStats.jsm @@ -0,0 +1,1000 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* 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"; + +this.EXPORTED_SYMBOLS = ["PerformanceStats"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +/** + * API for querying and examining performance data. + * + * This API exposes data from several probes implemented by the JavaScript VM. + * See `PerformanceStats.getMonitor()` for information on how to monitor data + * from one or more probes and `PerformanceData` for the information obtained + * from the probes. + * + * Data is collected by "Performance Group". Typically, a Performance Group + * is an add-on, or a frame, or the internals of the application. + * + * Generally, if you have the choice between PerformanceStats and PerformanceWatcher, + * you should favor PerformanceWatcher. + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/ObjectUtils.jsm", this); +XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", + "resource://gre/modules/PromiseUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", + "resource://gre/modules/Timer.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout", + "resource://gre/modules/Timer.jsm"); + +// The nsIPerformanceStatsService provides lower-level +// access to SpiderMonkey and the probes. +XPCOMUtils.defineLazyServiceGetter(this, "performanceStatsService", + "@mozilla.org/toolkit/performance-stats-service;1", + Ci.nsIPerformanceStatsService); + +// The finalizer lets us automatically release (and when possible deactivate) +// probes when a monitor is garbage-collected. +XPCOMUtils.defineLazyServiceGetter(this, "finalizer", + "@mozilla.org/toolkit/finalizationwitness;1", + Ci.nsIFinalizationWitnessService +); + +// The topic used to notify that a PerformanceMonitor has been garbage-collected +// and that we can release/close the probes it holds. +const FINALIZATION_TOPIC = "performancemonitor-finalize"; + +const PROPERTIES_META_IMMUTABLE = ["addonId", "isSystem", "isChildProcess", "groupId", "processId"]; +const PROPERTIES_META = [...PROPERTIES_META_IMMUTABLE, "windowId", "title", "name"]; + +// How long we wait for children processes to respond. +const MAX_WAIT_FOR_CHILD_PROCESS_MS = 5000; + +var isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; +/** + * Access to a low-level performance probe. + * + * Each probe is dedicated to some form of performance monitoring. + * As each probe may have a performance impact, a probe is activated + * only when a client has requested a PerformanceMonitor for this probe, + * and deactivated once all clients are disposed of. + */ +function Probe(name, impl) { + this._name = name; + this._counter = 0; + this._impl = impl; +} +Probe.prototype = { + /** + * Acquire the probe on behalf of a client. + * + * If the probe was inactive, activate it. Note that activating a probe + * can incur a memory or performance cost. + */ + acquire: function() { + if (this._counter == 0) { + this._impl.isActive = true; + Process.broadcast("acquire", [this._name]); + } + this._counter++; + }, + + /** + * Release the probe on behalf of a client. + * + * If this was the last client for this probe, deactivate it. + */ + release: function() { + this._counter--; + if (this._counter == 0) { + try { + this._impl.isActive = false; + } catch (ex) { + if (ex && typeof ex == "object" && ex.result == Components.results.NS_ERROR_NOT_AVAILABLE) { + // The service has already been shutdown. Ignore further shutdown requests. + return; + } + throw ex; + } + Process.broadcast("release", [this._name]); + } + }, + + /** + * Obtain data from this probe, once it is available. + * + * @param {nsIPerformanceStats} xpcom A xpcom object obtained from + * SpiderMonkey. Only the fields updated by the low-level probe + * are in a specified state. + * @return {object} An object containing the data extracted from this + * probe. Actual format depends on the probe. + */ + extract: function(xpcom) { + if (!this._impl.isActive) { + throw new Error(`Probe is inactive: ${this._name}`); + } + return this._impl.extract(xpcom); + }, + + /** + * @param {object} a An object returned by `this.extract()`. + * @param {object} b An object returned by `this.extract()`. + * + * @return {true} If `a` and `b` hold identical values. + */ + isEqual: function(a, b) { + if (a == null && b == null) { + return true; + } + if (a != null && b != null) { + return this._impl.isEqual(a, b); + } + return false; + }, + + /** + * @param {object} a An object returned by `this.extract()`. May + * NOT be `null`. + * @param {object} b An object returned by `this.extract()`. May + * be `null`. + * + * @return {object} An object representing `a - b`. If `b` is + * `null`, this is `a`. + */ + subtract: function(a, b) { + if (a == null) { + throw new TypeError(); + } + if (b == null) { + return a; + } + return this._impl.subtract(a, b); + }, + + importChildCompartments: function(parent, children) { + if (!Array.isArray(children)) { + throw new TypeError(); + } + if (!parent || !(parent instanceof PerformanceDataLeaf)) { + throw new TypeError(); + } + return this._impl.importChildCompartments(parent, children); + }, + + /** + * The name of the probe. + */ + get name() { + return this._name; + }, + + compose: function(stats) { + if (!Array.isArray(stats)) { + throw new TypeError(); + } + return this._impl.compose(stats); + } +}; + +// Utility function. Return the position of the last non-0 item in an +// array, or -1 if there isn't any such item. +function lastNonZero(array) { + for (let i = array.length - 1; i >= 0; --i) { + if (array[i] != 0) { + return i; + } + } + return -1; +} + +/** + * The actual Probes implemented by SpiderMonkey. + */ +var Probes = { + /** + * A probe measuring jank. + * + * Data provided by this probe uses the following format: + * + * @field {number} totalCPUTime The total amount of time spent using the + * CPU for this performance group, in µs. + * @field {number} totalSystemTime The total amount of time spent in the + * kernel for this performance group, in µs. + * @field {Array<number>} durations An array containing at each position `i` + * the number of times execution of this component has lasted at least `2^i` + * milliseconds. + * @field {number} longestDuration The index of the highest non-0 value in + * `durations`. + */ + jank: new Probe("jank", { + set isActive(x) { + performanceStatsService.isMonitoringJank = x; + }, + get isActive() { + return performanceStatsService.isMonitoringJank; + }, + extract: function(xpcom) { + let durations = xpcom.getDurations(); + return { + totalUserTime: xpcom.totalUserTime, + totalSystemTime: xpcom.totalSystemTime, + totalCPUTime: xpcom.totalUserTime + xpcom.totalSystemTime, + durations: durations, + longestDuration: lastNonZero(durations) + } + }, + isEqual: function(a, b) { + // invariant: `a` and `b` are both non-null + if (a.totalUserTime != b.totalUserTime) { + return false; + } + if (a.totalSystemTime != b.totalSystemTime) { + return false; + } + for (let i = 0; i < a.durations.length; ++i) { + if (a.durations[i] != b.durations[i]) { + return false; + } + } + return true; + }, + subtract: function(a, b) { + // invariant: `a` and `b` are both non-null + let result = { + totalUserTime: a.totalUserTime - b.totalUserTime, + totalSystemTime: a.totalSystemTime - b.totalSystemTime, + totalCPUTime: a.totalCPUTime - b.totalCPUTime, + durations: [], + longestDuration: -1, + }; + for (let i = 0; i < a.durations.length; ++i) { + result.durations[i] = a.durations[i] - b.durations[i]; + } + result.longestDuration = lastNonZero(result.durations); + return result; + }, + importChildCompartments: function() { /* nothing to do */ }, + compose: function(stats) { + let result = { + totalUserTime: 0, + totalSystemTime: 0, + totalCPUTime: 0, + durations: [], + longestDuration: -1 + }; + for (let stat of stats) { + result.totalUserTime += stat.totalUserTime; + result.totalSystemTime += stat.totalSystemTime; + result.totalCPUTime += stat.totalCPUTime; + for (let i = 0; i < stat.durations.length; ++i) { + result.durations[i] += stat.durations[i]; + } + result.longestDuration = Math.max(result.longestDuration, stat.longestDuration); + } + return result; + } + }), + + /** + * A probe measuring CPOW activity. + * + * Data provided by this probe uses the following format: + * + * @field {number} totalCPOWTime The amount of wallclock time + * spent executing blocking cross-process calls, in µs. + */ + cpow: new Probe("cpow", { + set isActive(x) { + performanceStatsService.isMonitoringCPOW = x; + }, + get isActive() { + return performanceStatsService.isMonitoringCPOW; + }, + extract: function(xpcom) { + return { + totalCPOWTime: xpcom.totalCPOWTime + }; + }, + isEqual: function(a, b) { + return a.totalCPOWTime == b.totalCPOWTime; + }, + subtract: function(a, b) { + return { + totalCPOWTime: a.totalCPOWTime - b.totalCPOWTime + }; + }, + importChildCompartments: function() { /* nothing to do */ }, + compose: function(stats) { + let totalCPOWTime = 0; + for (let stat of stats) { + totalCPOWTime += stat.totalCPOWTime; + } + return { totalCPOWTime }; + }, + }), + + /** + * A probe measuring activations, i.e. the number + * of times code execution has entered a given + * PerformanceGroup. + * + * Note that this probe is always active. + * + * Data provided by this probe uses the following format: + * @type {number} ticks The number of times execution has entered + * this performance group. + */ + ticks: new Probe("ticks", { + set isActive(x) { /* this probe cannot be deactivated */ }, + get isActive() { return true; }, + extract: function(xpcom) { + return { + ticks: xpcom.ticks + }; + }, + isEqual: function(a, b) { + return a.ticks == b.ticks; + }, + subtract: function(a, b) { + return { + ticks: a.ticks - b.ticks + }; + }, + importChildCompartments: function() { /* nothing to do */ }, + compose: function(stats) { + let ticks = 0; + for (let stat of stats) { + ticks += stat.ticks; + } + return { ticks }; + }, + }), + + compartments: new Probe("compartments", { + set isActive(x) { + performanceStatsService.isMonitoringPerCompartment = x; + }, + get isActive() { + return performanceStatsService.isMonitoringPerCompartment; + }, + extract: function(xpcom) { + return null; + }, + isEqual: function(a, b) { + return true; + }, + subtract: function(a, b) { + return true; + }, + importChildCompartments: function(parent, children) { + parent.children = children; + }, + compose: function(stats) { + return null; + }, + }), +}; + +/** + * A monitor for a set of probes. + * + * Keeping probes active when they are unused is often a bad + * idea for performance reasons. Upon destruction, or whenever + * a client calls `dispose`, this monitor releases the probes, + * which may let the system deactivate them. + */ +function PerformanceMonitor(probes) { + this._probes = probes; + + // Activate low-level features as needed + for (let probe of probes) { + probe.acquire(); + } + + // A finalization witness. At some point after the garbage-collection of + // `this` object, a notification of `FINALIZATION_TOPIC` will be triggered + // with `id` as message. + this._id = PerformanceMonitor.makeId(); + this._finalizer = finalizer.make(FINALIZATION_TOPIC, this._id) + PerformanceMonitor._monitors.set(this._id, probes); +} +PerformanceMonitor.prototype = { + /** + * The names of probes activated in this monitor. + */ + get probeNames() { + return this._probes.map(probe => probe.name); + }, + + /** + * Return asynchronously a snapshot with the data + * for each probe monitored by this PerformanceMonitor. + * + * All numeric values are non-negative and can only increase. Depending on + * the probe and the underlying operating system, probes may not be available + * immediately and may miss some activity. + * + * Clients should NOT expect that the first call to `promiseSnapshot()` + * will return a `Snapshot` in which all values are 0. For most uses, + * the appropriate scenario is to perform a first call to `promiseSnapshot()` + * to obtain a baseline, and then watch evolution of the values by calling + * `promiseSnapshot()` and `subtract()`. + * + * On the other hand, numeric values are also monotonic across several instances + * of a PerformanceMonitor with the same probes. + * let a = PerformanceStats.getMonitor(someProbes); + * let snapshot1 = yield a.promiseSnapshot(); + * + * // ... + * let b = PerformanceStats.getMonitor(someProbes); // Same list of probes + * let snapshot2 = yield b.promiseSnapshot(); + * + * // all values of `snapshot2` are greater or equal to values of `snapshot1`. + * + * @param {object} options If provided, an object that may contain the following + * fields: + * {Array<string>} probeNames The subset of probes to use for this snapshot. + * These probes must be a subset of the probes active in the monitor. + * + * @return {Promise} + * @resolve {Snapshot} + */ + _checkBeforeSnapshot: function(options) { + if (!this._finalizer) { + throw new Error("dispose() has already been called, this PerformanceMonitor is not usable anymore"); + } + let probes; + if (options && options.probeNames || undefined) { + if (!Array.isArray(options.probeNames)) { + throw new TypeError(); + } + // Make sure that we only request probes that we have + for (let probeName of options.probeNames) { + let probe = this._probes.find(probe => probe.name == probeName); + if (!probe) { + throw new TypeError(`I need probe ${probeName} but I only have ${this.probeNames}`); + } + if (!probes) { + probes = []; + } + probes.push(probe); + } + } else { + probes = this._probes; + } + return probes; + }, + promiseContentSnapshot: function(options = null) { + this._checkBeforeSnapshot(options); + return (new ProcessSnapshot(performanceStatsService.getSnapshot())); + }, + promiseSnapshot: function(options = null) { + let probes = this._checkBeforeSnapshot(options); + return Task.spawn(function*() { + let childProcesses = yield Process.broadcastAndCollect("collect", {probeNames: probes.map(p => p.name)}); + let xpcom = performanceStatsService.getSnapshot(); + return new ApplicationSnapshot({ + xpcom, + childProcesses, + probes, + date: Cu.now() + }); + }); + }, + + /** + * Release the probes used by this monitor. + * + * Releasing probes as soon as they are unused is a good idea, as some probes + * cost CPU and/or memory. + */ + dispose: function() { + if (!this._finalizer) { + return; + } + this._finalizer.forget(); + PerformanceMonitor.dispose(this._id); + + // As a safeguard against double-release, reset everything to `null` + this._probes = null; + this._id = null; + this._finalizer = null; + } +}; +/** + * @type {Map<string, Array<string>>} A map from id (as produced by `makeId`) + * to list of probes. Used to deallocate a list of probes during finalization. + */ +PerformanceMonitor._monitors = new Map(); + +/** + * Create a `PerformanceMonitor` for a list of probes, register it for + * finalization. + */ +PerformanceMonitor.make = function(probeNames) { + // Sanity checks + if (!Array.isArray(probeNames)) { + throw new TypeError("Expected an array, got " + probes); + } + let probes = []; + for (let probeName of probeNames) { + if (!(probeName in Probes)) { + throw new TypeError("Probe not implemented: " + probeName); + } + probes.push(Probes[probeName]); + } + + return (new PerformanceMonitor(probes)); +}; + +/** + * Implementation of `dispose`. + * + * The actual implementation of `dispose` is as a method of `PerformanceMonitor`, + * rather than `PerformanceMonitor.prototype`, to avoid needing a strong reference + * to instances of `PerformanceMonitor`, which would defeat the purpose of + * finalization. + */ +PerformanceMonitor.dispose = function(id) { + let probes = PerformanceMonitor._monitors.get(id); + if (!probes) { + throw new TypeError("`dispose()` has already been called on this monitor"); + } + + PerformanceMonitor._monitors.delete(id); + for (let probe of probes) { + probe.release(); + } +} + +// Generate a unique id for each PerformanceMonitor. Used during +// finalization. +PerformanceMonitor._counter = 0; +PerformanceMonitor.makeId = function() { + return "PerformanceMonitor-" + (this._counter++); +} + +// Once a `PerformanceMonitor` has been garbage-collected, +// release the probes unless `dispose()` has already been called. +Services.obs.addObserver(function(subject, topic, value) { + PerformanceMonitor.dispose(value); +}, FINALIZATION_TOPIC, false); + +// Public API +this.PerformanceStats = { + /** + * Create a monitor for observing a set of performance probes. + */ + getMonitor: function(probes) { + return PerformanceMonitor.make(probes); + } +}; + + +/** + * Information on a single performance group. + * + * This offers the following fields: + * + * @field {string} name The name of the performance group: + * - for the process itself, "<process>"; + * - for platform code, "<platform>"; + * - for an add-on, the identifier of the addon (e.g. "myaddon@foo.bar"); + * - for a webpage, the url of the page. + * + * @field {string} addonId The identifier of the addon (e.g. "myaddon@foo.bar"). + * + * @field {string|null} title The title of the webpage to which this code + * belongs. Note that this is the title of the entire webpage (i.e. the tab), + * even if the code is executed in an iframe. Also note that this title may + * change over time. + * + * @field {number} windowId The outer window ID of the top-level nsIDOMWindow + * to which this code belongs. May be 0 if the code doesn't belong to any + * nsIDOMWindow. + * + * @field {boolean} isSystem `true` if the component is a system component (i.e. + * an add-on or platform-code), `false` otherwise (i.e. a webpage). + * + * @field {object|undefined} activations See the documentation of probe "ticks". + * `undefined` if this probe is not active. + * + * @field {object|undefined} jank See the documentation of probe "jank". + * `undefined` if this probe is not active. + * + * @field {object|undefined} cpow See the documentation of probe "cpow". + * `undefined` if this probe is not active. + */ +function PerformanceDataLeaf({xpcom, json, probes}) { + if (xpcom && json) { + throw new TypeError("Cannot import both xpcom and json data"); + } + let source = xpcom || json; + for (let k of PROPERTIES_META) { + this[k] = source[k]; + } + if (xpcom) { + for (let probe of probes) { + this[probe.name] = probe.extract(xpcom); + } + this.isChildProcess = false; + } else { + for (let probe of probes) { + this[probe.name] = json[probe.name]; + } + this.isChildProcess = true; + } + this.owner = null; +} +PerformanceDataLeaf.prototype = { + /** + * Compare two instances of `PerformanceData` + * + * @return `true` if `this` and `to` have equal values in all fields. + */ + equals: function(to) { + if (!(to instanceof PerformanceDataLeaf)) { + throw new TypeError(); + } + for (let probeName of Object.keys(Probes)) { + let probe = Probes[probeName]; + if (!probe.isEqual(this[probeName], to[probeName])) { + return false; + } + } + return true; + }, + + /** + * Compute the delta between two instances of `PerformanceData`. + * + * @param {PerformanceData|null} to. If `null`, assumed an instance of + * `PerformanceData` in which all numeric values are 0. + * + * @return {PerformanceDiff} The performance usage between `to` and `this`. + */ + subtract: function(to = null) { + return (new PerformanceDiffLeaf(this, to)); + } +}; + +function PerformanceData(timestamp) { + this._parent = null; + this._content = new Map(); + this._all = []; + this._timestamp = timestamp; +} +PerformanceData.prototype = { + addChild: function(stat) { + if (!(stat instanceof PerformanceDataLeaf)) { + throw new TypeError(); // FIXME + } + if (!stat.isChildProcess) { + throw new TypeError(); // FIXME + } + this._content.set(stat.groupId, stat); + this._all.push(stat); + stat.owner = this; + }, + setParent: function(stat) { + if (!(stat instanceof PerformanceDataLeaf)) { + throw new TypeError(); // FIXME + } + if (stat.isChildProcess) { + throw new TypeError(); // FIXME + } + this._parent = stat; + this._all.push(stat); + stat.owner = this; + }, + equals: function(to) { + if (this._parent && !to._parent) { + return false; + } + if (!this._parent && to._parent) { + return false; + } + if (this._content.size != to._content.size) { + return false; + } + if (this._parent && !this._parent.equals(to._parent)) { + return false; + } + for (let [k, v] of this._content) { + let v2 = to._content.get(k); + if (!v2) { + return false; + } + if (!v.equals(v2)) { + return false; + } + } + return true; + }, + subtract: function(to = null) { + return (new PerformanceDiff(this, to)); + }, + get addonId() { + return this._all[0].addonId; + }, + get title() { + return this._all[0].title; + } +}; + +function PerformanceDiff(current, old = null) { + this.addonId = current.addonId; + this.title = current.title; + this.windowId = current.windowId; + this.deltaT = old ? current._timestamp - old._timestamp : Infinity; + this._all = []; + + // Handle the parent, if any. + if (current._parent) { + this._parent = old?current._parent.subtract(old._parent):current._parent; + this._all.push(this._parent); + this._parent.owner = this; + } else { + this._parent = null; + } + + // Handle the children, if any. + this._content = new Map(); + for (let [k, stat] of current._content) { + let diff = stat.subtract(old ? old._content.get(k) : null); + this._content.set(k, diff); + this._all.push(diff); + diff.owner = this; + } + + // Now consolidate data + for (let k of Object.keys(Probes)) { + if (!(k in this._all[0])) { + // The stats don't contain data from this probe. + continue; + } + let data = this._all.map(item => item[k]); + let probe = Probes[k]; + this[k] = probe.compose(data); + } +} +PerformanceDiff.prototype = { + toString: function() { + return `[PerformanceDiff] ${this.key}`; + }, + get windowIds() { + return this._all.map(item => item.windowId).filter(x => !!x); + }, + get groupIds() { + return this._all.map(item => item.groupId); + }, + get key() { + if (this.addonId) { + return this.addonId; + } + if (this._parent) { + return this._parent.windowId; + } + return this._all[0].groupId; + }, + get names() { + return this._all.map(item => item.name); + }, + get processes() { + return this._all.map(item => ({ isChildProcess: item.isChildProcess, processId: item.processId})); + } +}; + +/** + * The delta between two instances of `PerformanceDataLeaf`. + * + * Used to monitor resource usage between two timestamps. + */ +function PerformanceDiffLeaf(current, old = null) { + for (let k of PROPERTIES_META) { + this[k] = current[k]; + } + + for (let probeName of Object.keys(Probes)) { + let other = null; + if (old && probeName in old) { + other = old[probeName]; + } + + if (probeName in current) { + this[probeName] = Probes[probeName].subtract(current[probeName], other); + } + } +} + +/** + * A snapshot of a single process. + */ +function ProcessSnapshot({xpcom, probes}) { + this.componentsData = []; + + let subgroups = new Map(); + let enumeration = xpcom.getComponentsData().enumerate(); + while (enumeration.hasMoreElements()) { + let xpcom = enumeration.getNext().QueryInterface(Ci.nsIPerformanceStats); + let stat = (new PerformanceDataLeaf({xpcom, probes})); + + if (!xpcom.parentId) { + this.componentsData.push(stat); + } else { + let siblings = subgroups.get(xpcom.parentId); + if (!siblings) { + subgroups.set(xpcom.parentId, (siblings = [])); + } + siblings.push(stat); + } + } + + for (let group of this.componentsData) { + for (let probe of probes) { + probe.importChildCompartments(group, subgroups.get(group.groupId) || []); + } + } + + this.processData = (new PerformanceDataLeaf({xpcom: xpcom.getProcessData(), probes})); +} + +/** + * A snapshot of the performance usage of the application. + * + * @param {nsIPerformanceSnapshot} xpcom The data acquired from this process. + * @param {Array<Object>} childProcesses The data acquired from children processes. + * @param {Array<Probe>} probes The active probes. + */ +function ApplicationSnapshot({xpcom, childProcesses, probes, date}) { + ProcessSnapshot.call(this, {xpcom, probes}); + + this.addons = new Map(); + this.webpages = new Map(); + this.date = date; + + // Child processes + for (let {componentsData} of (childProcesses || [])) { + // We are only interested in `componentsData` for the time being. + for (let json of componentsData) { + let leaf = (new PerformanceDataLeaf({json, probes})); + this.componentsData.push(leaf); + } + } + + for (let leaf of this.componentsData) { + let key, map; + if (leaf.addonId) { + key = leaf.addonId; + map = this.addons; + } else if (leaf.windowId) { + key = leaf.windowId; + map = this.webpages; + } else { + continue; + } + + let combined = map.get(key); + if (!combined) { + combined = new PerformanceData(date); + map.set(key, combined); + } + if (leaf.isChildProcess) { + combined.addChild(leaf); + } else { + combined.setParent(leaf); + } + } +} + +/** + * Communication with other processes + */ +var Process = { + // a counter used to match responses to requests + _idcounter: 0, + _loader: null, + /** + * If we are in a child process, return `null`. + * Otherwise, return the global parent process message manager + * and load the script to connect to children processes. + */ + get loader() { + if (isContent) { + return null; + } + if (this._loader) { + return this._loader; + } + Services.ppmm.loadProcessScript("resource://gre/modules/PerformanceStats-content.js", + true/* including future processes*/); + return this._loader = Services.ppmm; + }, + + /** + * Broadcast a message to all children processes. + * + * NOOP if we are in a child process. + */ + broadcast: function(topic, payload) { + if (!this.loader) { + return; + } + this.loader.broadcastAsyncMessage("performance-stats-service-" + topic, {payload}); + }, + + /** + * Brodcast a message to all children processes and wait for answer. + * + * NOOP if we are in a child process, or if we have no children processes, + * in which case we return `undefined`. + * + * @return {undefined} If we have no children processes, in particular + * if we are in a child process. + * @return {Promise<Array<Object>>} If we have children processes, an + * array of objects with a structure similar to PerformanceData. Note + * that the array may be empty if no child process responded. + */ + broadcastAndCollect: Task.async(function*(topic, payload) { + if (!this.loader || this.loader.childCount == 1) { + return undefined; + } + const TOPIC = "performance-stats-service-" + topic; + let id = this._idcounter++; + + // The number of responses we are expecting. Note that we may + // not receive all responses if a process is too long to respond. + let expecting = this.loader.childCount; + + // The responses we have collected, in arbitrary order. + let collected = []; + let deferred = PromiseUtils.defer(); + + let observer = function({data, target}) { + if (data.id != id) { + // Collision between two collections, + // ignore the other one. + return; + } + if (data.data) { + collected.push(data.data) + } + if (--expecting > 0) { + // We are still waiting for at least one response. + return; + } + deferred.resolve(); + }; + this.loader.addMessageListener(TOPIC, observer); + this.loader.broadcastAsyncMessage( + TOPIC, + {id, payload} + ); + + // Processes can die/freeze/be busy loading a page..., so don't expect + // that they will always respond. + let timeout = setTimeout(() => { + if (expecting == 0) { + return; + } + deferred.resolve(); + }, MAX_WAIT_FOR_CHILD_PROCESS_MS); + + deferred.promise.then(() => { + clearTimeout(timeout); + }); + + yield deferred.promise; + this.loader.removeMessageListener(TOPIC, observer); + + return collected; + }) +}; diff --git a/toolkit/components/perfmonitoring/PerformanceWatcher-content.js b/toolkit/components/perfmonitoring/PerformanceWatcher-content.js new file mode 100644 index 000000000..2956cf5d0 --- /dev/null +++ b/toolkit/components/perfmonitoring/PerformanceWatcher-content.js @@ -0,0 +1,54 @@ +/* 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"; + +/** + * An API for being informed of slow add-ons and tabs + * (content process scripts). + */ + +const { utils: Cu, classes: Cc, interfaces: Ci } = Components; +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +/** + * `true` if this is a content process, `false` otherwise. + */ +let isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; + +if (isContent) { + +const { PerformanceWatcher } = Cu.import("resource://gre/modules/PerformanceWatcher.jsm", {}); + +let toMsg = function(alerts) { + let result = []; + for (let {source, details} of alerts) { + // Convert xpcom values to serializable data. + let serializableSource = {}; + for (let k of ["groupId", "name", "addonId", "windowId", "isSystem", "processId", "isContentProcess"]) { + serializableSource[k] = source[k]; + } + + let serializableDetails = {}; + for (let k of ["reason", "highestJank", "highestCPOW"]) { + serializableDetails[k] = details[k]; + } + result.push({source:serializableSource, details:serializableDetails}); + } + return result; +} + +PerformanceWatcher.addPerformanceListener({addonId: "*"}, alerts => { + Services.cpmm.sendAsyncMessage("performancewatcher-propagate-notifications", + {addons: toMsg(alerts)} + ); +}); + +PerformanceWatcher.addPerformanceListener({windowId: 0}, alerts => { + Services.cpmm.sendAsyncMessage("performancewatcher-propagate-notifications", + {windows: toMsg(alerts)} + ); +}); + +} diff --git a/toolkit/components/perfmonitoring/PerformanceWatcher.jsm b/toolkit/components/perfmonitoring/PerformanceWatcher.jsm new file mode 100644 index 000000000..d0d034974 --- /dev/null +++ b/toolkit/components/perfmonitoring/PerformanceWatcher.jsm @@ -0,0 +1,367 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* 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"; + +/** + * An API for being informed of slow add-ons and tabs. + * + * Generally, this API is both more CPU-efficient and more battery-efficient + * than PerformanceStats. As PerformanceStats, this API does not provide any + * information during the startup or shutdown of Firefox. + * + * = Examples = + * + * Example use: reporting whenever a specific add-on slows down Firefox. + * let listener = function(source, details) { + * // This listener is triggered whenever the addon causes Firefox to miss + * // frames. Argument `source` contains information about the source of the + * // slowdown (including the process in which it happens), while `details` + * // contains performance statistics. + * console.log(`Oops, add-on ${source.addonId} seems to be slowing down Firefox.`, details); + * }; + * PerformanceWatcher.addPerformanceListener({addonId: "myaddon@myself.name"}, listener); + * + * Example use: reporting whenever any webpage slows down Firefox. + * let listener = function(alerts) { + * // This listener is triggered whenever any window causes Firefox to miss + * // frames. FieldArgument `source` contains information about the source of the + * // slowdown (including the process in which it happens), while `details` + * // contains performance statistics. + * for (let {source, details} of alerts) { + * console.log(`Oops, window ${source.windowId} seems to be slowing down Firefox.`, details); + * }; + * // Special windowId 0 lets us to listen to all webpages. + * PerformanceWatcher.addPerformanceListener({windowId: 0}, listener); + * + * + * = How this works = + * + * This high-level API is based on the lower-level nsIPerformanceStatsService. + * At the end of each event (including micro-tasks), the nsIPerformanceStatsService + * updates its internal performance statistics and determines whether any + * add-on/window in the current process has exceeded the jank threshold. + * + * The PerformanceWatcher maintains low-level performance observers in each + * process and forwards alerts to the main process. Internal observers collate + * low-level main process alerts and children process alerts and notify clients + * of this API. + */ + +this.EXPORTED_SYMBOLS = ["PerformanceWatcher"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +let { PerformanceStats, performanceStatsService } = Cu.import("resource://gre/modules/PerformanceStats.jsm", {}); +let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +// `true` if the code is executed in content, `false` otherwise +let isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; + +if (!isContent) { + // Initialize communication with children. + // + // To keep the protocol simple, the children inform the parent whenever a slow + // add-on/tab is detected. We do not attempt to implement thresholds. + Services.ppmm.loadProcessScript("resource://gre/modules/PerformanceWatcher-content.js", + true/* including future processes*/); + + Services.ppmm.addMessageListener("performancewatcher-propagate-notifications", + (...args) => ChildManager.notifyObservers(...args) + ); +} + +// Configure the performance stats service to inform us in case of jank. +performanceStatsService.jankAlertThreshold = 64000 /* us */; + + +/** + * Handle communications with child processes. Handle listening to + * either a single add-on id (including the special add-on id "*", + * which is notified for all add-ons) or a single window id (including + * the special window id 0, which is notified for all windows). + * + * Acquire through `ChildManager.getAddon` and `ChildManager.getWindow`. + */ +function ChildManager(map, key) { + this.key = key; + this._map = map; + this._listeners = new Set(); +} +ChildManager.prototype = { + /** + * Add a listener, which will be notified whenever a child process + * reports a slow performance alert for this addon/window. + */ + addListener: function(listener) { + this._listeners.add(listener); + }, + /** + * Remove a listener. + */ + removeListener: function(listener) { + let deleted = this._listeners.delete(listener); + if (!deleted) { + throw new Error("Unknown listener"); + } + }, + + listeners: function() { + return this._listeners.values(); + } +}; + +/** + * Dispatch child alerts to observers. + * + * Triggered by messages from content processes. + */ +ChildManager.notifyObservers = function({data: {addons, windows}}) { + if (addons && addons.length > 0) { + // Dispatch the entire list to universal listeners + this._notify(ChildManager.getAddon("*").listeners(), addons); + + // Dispatch individual alerts to individual listeners + for (let {source, details} of addons) { + this._notify(ChildManager.getAddon(source.addonId).listeners(), source, details); + } + } + if (windows && windows.length > 0) { + // Dispatch the entire list to universal listeners + this._notify(ChildManager.getWindow(0).listeners(), windows); + + // Dispatch individual alerts to individual listeners + for (let {source, details} of windows) { + this._notify(ChildManager.getWindow(source.windowId).listeners(), source, details); + } + } +}; + +ChildManager._notify = function(targets, ...args) { + for (let target of targets) { + target(...args); + } +}; + +ChildManager.getAddon = function(key) { + return this._get(this._addons, key); +}; +ChildManager._addons = new Map(); + +ChildManager.getWindow = function(key) { + return this._get(this._windows, key); +}; +ChildManager._windows = new Map(); + +ChildManager._get = function(map, key) { + let result = map.get(key); + if (!result) { + result = new ChildManager(map, key); + map.set(key, result); + } + return result; +}; +let gListeners = new WeakMap(); + +/** + * An object in charge of managing all the observables for a single + * target (window/addon/all windows/all addons). + * + * In a content process, a target is represented by a single observable. + * The situation is more sophisticated in a parent process, as a target + * has both an in-process observable and several observables across children + * processes. + * + * This class abstracts away the difference to simplify the work of + * (un)registering observers for targets. + * + * @param {object} target The target being observed, as an object + * with one of the following fields: + * - {string} addonId Either "*" for the universal add-on observer + * or the add-on id of an addon. Note that this class does not + * check whether the add-on effectively exists, and that observers + * may be registered for an add-on before the add-on is installed + * or started. + * - {xul:tab} tab A single tab. It must already be initialized. + * - {number} windowId Either 0 for the universal window observer + * or the outer window id of the window. + */ +function Observable(target) { + // A mapping from `listener` (function) to `Observer`. + this._observers = new Map(); + if ("addonId" in target) { + this._key = `addonId: ${target.addonId}`; + this._process = performanceStatsService.getObservableAddon(target.addonId); + this._children = isContent ? null : ChildManager.getAddon(target.addonId); + this._isBuffered = target.addonId == "*"; + } else if ("tab" in target || "windowId" in target) { + let windowId; + if ("tab" in target) { + windowId = target.tab.linkedBrowser.outerWindowID; + // By convention, outerWindowID may not be 0. + } else if ("windowId" in target) { + windowId = target.windowId; + } + if (windowId == undefined || windowId == null) { + throw new TypeError(`No outerWindowID. Perhaps the target is a tab that is not initialized yet.`); + } + this._key = `tab-windowId: ${windowId}`; + this._process = performanceStatsService.getObservableWindow(windowId); + this._children = isContent ? null : ChildManager.getWindow(windowId); + this._isBuffered = windowId == 0; + } else { + throw new TypeError("Unexpected target"); + } +} +Observable.prototype = { + addJankObserver: function(listener) { + if (this._observers.has(listener)) { + throw new TypeError(`Listener already registered for target ${this._key}`); + } + if (this._children) { + this._children.addListener(listener); + } + let observer = this._isBuffered ? new BufferedObserver(listener) + : new Observer(listener); + // Store the observer to be able to call `this._process.removeJankObserver`. + this._observers.set(listener, observer); + + this._process.addJankObserver(observer); + }, + removeJankObserver: function(listener) { + let observer = this._observers.get(listener); + if (!observer) { + throw new TypeError(`No listener for target ${this._key}`); + } + this._observers.delete(listener); + + if (this._children) { + this._children.removeListener(listener); + } + + this._process.removeJankObserver(observer); + observer.dispose(); + }, +}; + +/** + * Get a cached observable for a given target. + */ +Observable.get = function(target) { + let key; + if ("addonId" in target) { + key = target.addonId; + } else if ("tab" in target) { + // We do not want to use a tab as a key, as this would prevent it from + // being garbage-collected. + key = target.tab.linkedBrowser.outerWindowID; + } else if ("windowId" in target) { + key = target.windowId; + } + if (key == null) { + throw new TypeError(`Could not extract a key from ${JSON.stringify(target)}. Could the target be an unitialized tab?`); + } + let observable = this._cache.get(key); + if (!observable) { + observable = new Observable(target); + this._cache.set(key, observable); + } + return observable; +}; +Observable._cache = new Map(); + +/** + * Wrap a listener callback as an unbuffered nsIPerformanceObserver. + * + * Each observation is propagated immediately to the listener. + */ +function Observer(listener) { + // Make sure that monitoring stays alive (in all processes) at least as + // long as the observer. + this._monitor = PerformanceStats.getMonitor(["jank", "cpow"]); + this._listener = listener; +} +Observer.prototype = { + observe: function(...args) { + this._listener(...args); + }, + dispose: function() { + this._monitor.dispose(); + this.observe = function poison() { + throw new Error("Internal error: I should have stopped receiving notifications"); + } + }, +}; + +/** + * Wrap a listener callback as an buffered nsIPerformanceObserver. + * + * Observations are buffered and dispatch in the next tick to the listener. + */ +function BufferedObserver(listener) { + Observer.call(this, listener); + this._buffer = []; + this._isDispatching = false; + this._pending = null; +} +BufferedObserver.prototype = Object.create(Observer.prototype); +BufferedObserver.prototype.observe = function(source, details) { + this._buffer.push({source, details}); + if (!this._isDispatching) { + this._isDispatching = true; + Services.tm.mainThread.dispatch(() => { + // Grab buffer, in case something in the listener could modify it. + let buffer = this._buffer; + this._buffer = []; + + // As of this point, any further observations need to use the new buffer + // and a new dispatcher. + this._isDispatching = false; + + this._listener(buffer); + }, Ci.nsIThread.DISPATCH_NORMAL); + } +}; + +this.PerformanceWatcher = { + /** + * Add a listener informed whenever we receive a slow performance alert + * in the application. + * + * @param {object} target An object with one of the following fields: + * - {string} addonId Either "*" to observe all add-ons or a full add-on ID. + * to observe a single add-on. + * - {number} windowId Either 0 to observe all windows or an outer window ID + * to observe a single tab. + * - {xul:browser} tab To observe a single tab. + * @param {function} listener A function that will be triggered whenever + * the target causes a slow performance notification. The notification may + * have originated in any process of the application. + * + * If the listener listens to a single add-on/webpage, it is triggered with + * the following arguments: + * source: {groupId, name, addonId, windowId, isSystem, processId} + * Information on the source of the notification. + * details: {reason, highestJank, highestCPOW} Information on the + * notification. + * + * If the listener listens to all add-ons/all webpages, it is triggered with + * an array of {source, details}, as described above. + */ + addPerformanceListener: function(target, listener) { + if (typeof listener != "function") { + throw new TypeError(); + } + let observable = Observable.get(target); + observable.addJankObserver(listener); + }, + removePerformanceListener: function(target, listener) { + if (typeof listener != "function") { + throw new TypeError(); + } + let observable = Observable.get(target); + observable.removeJankObserver(listener); + }, +}; diff --git a/toolkit/components/perfmonitoring/README.md b/toolkit/components/perfmonitoring/README.md new file mode 100644 index 000000000..abd109e45 --- /dev/null +++ b/toolkit/components/perfmonitoring/README.md @@ -0,0 +1,120 @@ +This directory is part of the implementation of the Performance Monitoring API + +# What is the Performance Monitoring API? + +The Performance Monitoring API is a set of interfaces designed to let front-end code find out if the application or a specific process is currently janky, quantify this jank and its evolution, and investigate what is causing jank (system code? a webpage? an add-on? CPOW?). In other words, this is a form of minimal profiler, designed to be lightweight enough to be enabled at all times in production code. + +In Firefox Nightly, the Performance Monitoring API is used to: +- inform users if their machine janks because of an add-on; +- upload add-on performance to Telemetry for the benefit of AMO maintainers and add-on developers; +- let users inspect the performance of their browser through about:performance. + +# How can I use the API? + +The API is designed mainly to be used from JavaScript client code, using PerformanceStats.jsm. If you really need to use it from C++ code, you should use the performance stats service defined in nsIPerformanceStats.idl. Note that PerformanceStats.jsm contains support for entire e10s-enabled applications, while nsIPerformanceStats.idl only supports one process at a time. + + +# How does the Performance Monitoring API work? + +At the time of this writing, the implementation of this API monitors only performance information related to the execution of JavaScript code, and only in the main thread. This is performed by an instrumentation of js/, orchestrated by toolkit/. + +At low-level, the unit of code used for monitoring is the JS Compartment: one .jsm module, one XPCOM component, one sandbox, one script in an iframe, ... When executing code in a compartment, it is possible to inspect either the compartment or the script itself to find out who this compartment belongs to: a `<xul:browser>`, an add-on, etc. + +At higher-level, the unit of code used for monitoring is the Performance Group. One Performance Group represents one or more JS Compartments, grouped together because we are interested in their performance. The current implementation uses Performance Groups to represent individual JS Compartments, entire add-ons, entire webpages including iframes and entire threads. Other applications have been discussed to represent entire eTLD+1 domains (e.g. to monitor the cost of ads), etc. + +A choice was made to represent the CPU cost in *clock cycles* at low-level, as extracting a number of clock cycles has a very low latency (typically a few dozen cycles on recent architectures) and is much more precise than `getrusage`-style CPU clocks (which are often limited to a precision of 16ms). The drawback of this choice is that distinct CPUs/cores may, depending on the architecture, have entirely unrelated clock cycles count. We assume that the risk of false positives is reasonably low, and bound the uncertainty by renormalizing the result with the actual CPU clocks once per event. + +## SpiderMonkey-level + +The instrumentation of SpiderMonkey lives in `js/src/vm/Stopwatch.*`. As SpiderMonkey does not know about the Gecko event loop, or DOM events, or windows, so any such information must be provided by the embedding. To communicate with higher levels, SpiderMonkey exposes a virtual class `js::PerformanceGroup` designed to be subclassed and instantiated by the embedding based on its interests. + +An instance of `js::PerformanceGroup` may be acquired (to mark that it is currently being monitored) and released (once monitoring is complete or cancelled) by SpiderMonkey. Furthermore, a `js::PerformanceGroup` can be marked as active (to mark that the embedding is currently interested in its performance) or inactive (otherwise) by the embedding. + +Each `js::Performance` holds a total CPU cost measured in *clock cycles* and a total CPOW cost measured in *microseconds*. Both costs can only increase while measuring data, and can be reset to 0 by the embedding, once we have finished execution of the event loop. + +### Upon starting to execute code in a JS Compartment `cx` +1. If global monitoring is deactivated, bailout; +2. If XPConnect has informed us that we are entering a nested event loop, cancel any ongoing measure on the outer event loop and proceed with the current measure; +3. If we do not know to which performance groups `cx` is associated, request the information from the embedding; +4. For each performance group `group` to which `cx` belongs *and* that is not acquired *and* for which monitoring is active, acquire the group; +5. If no group was acquired, bailout; +6. Capture a timestamp for the CPU cost of `cx`, in *clock cycles*. This value is provided directly by the CPU; +7. Capture a timestamp for the CPOW cost of `cx`, in *CPOW microseconds*. This value is provided by the CPOW-level embedding. + +### Upon stopping execution of the code in the JS compartment `cx` +1. If global monitoring is deactivated, bailout; +2. If the measure has been canceled, bailout; +3. If no group was acquired, bailout; +4. Capture a timestamp for the CPU cost of `cx`, use it to update the total CPU cost of each of the groups acquired; +5. Capture a timestamp for the CPOW cost of `cx`, use it to update the total CPOW cost of each of the groups acquired; +6. Mark acquired groups as executed recently; +7. Release groups. + +### When informed by the embedding that the iteration of the event loop is complete +1. Commit all the groups executed recently to the embedding; +2. Release all groups; +3. Reset all CPU/CPOW costs to 0. + +## Cross-Process Object Wrapper-level + +The instrumentation of CPOW lives in `js/ipc/src`. It maintains a CPOW clock that increases whenever the process is blocked by a CPOW call. + +## XPConnect-level + +The instrumentation of XPConnect lives in `js/xpconnect/src/XPCJSContext.cpp`. + +### When we enter a nested event loop + +1. Inform the SpiderMonkey-level instrumentation, to let it cancel any ongoing measure. + +### When we finish executing an iteration of the event loop, including microtasks: + +1. Inform the SpiderMonkey-level instrumentation, to let it commit its recent data. + +## nsIPerformanceStatsService-level + +This code lives in `toolkit/components/perfmonitoring/ns*`. Its role is to orchestrate the information provided by SpiderMonkey at the scale of a single thread of a single process. At the time of this writing, this instrumentation is only activated on the main thread, for all Gecko processes. + +The service defines a class `nsPerformanceGroup`, designed to be the sole concrete implementation of `js::PerformanceGroup`. `nsPerformanceGroup` extends `js::PerformanceGroup` with the global performance information gathered for the group since the start of the service. The information is: +- total CPU time measured; +- total CPOW time measured; +- number of times CPU time exceeded 1ms; +- number of times CPU time exceeded 2ms; +- number of times CPU time exceeded 4ms; +- ... +- number of times CPU time exceeded 2^9ms. + +Also, `nsPerformanceGroup` extends `js::PerformanceGroup` with high-level identification: +- id of the window that executed the code, if any; +- id of the add-on that provided the code, if any. + +### When the SpiderMonkey-level instrumentation requests a list of PerformanceGroup for a compartment + +Return a list with the following groups: +* all compartments are associated with the "top group", which represents the entire thread; +* find out if the compartment is code from a window, if so add a group shared by all compartments for this specific window; +* find out if the compartment is code from an add-on, if so add a group shared by all compartments for this add-on; +* add a group representing this specific compartment. + +For performance reasons, groups representing a single compartment are inactive by default, while all other groups are active by default. + +Performance groups are refcounted and destroyed with the implementation of `delete` used by toolkit/. + +### When the SpiderMonkey-level instrumentation commits a list of PerformanceGroups + +For each group in the list: +1. transfer recent CPU time and recent CPOW time to total CPU time, total CPOW time, number of times CPU time exceeded *n* ms; +2. reset group. + +Future versions are expected to trigger low-performance alerts at this stage. + +### Snapshotting + +(to be documented) + +## PerformanceStats.jsm-level + +PerformanceStats provides a JS-friendly API on top of nsIPerformanceStatsService. The main differences are: +- utilities for subtracting snapshots; +- tracking clients that need specific measures; +- synchronization between e10s processes. diff --git a/toolkit/components/perfmonitoring/moz.build b/toolkit/components/perfmonitoring/moz.build new file mode 100644 index 000000000..1effe5f00 --- /dev/null +++ b/toolkit/components/perfmonitoring/moz.build @@ -0,0 +1,35 @@ +# -*- 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/. + +BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini'] + +XPIDL_MODULE = 'toolkit_perfmonitoring' + +EXTRA_JS_MODULES += [ + 'AddonWatcher.jsm', + 'PerformanceStats-content.js', + 'PerformanceStats.jsm', + 'PerformanceWatcher-content.js', + 'PerformanceWatcher.jsm', +] + +XPIDL_SOURCES += [ + 'nsIPerformanceStats.idl', +] + +UNIFIED_SOURCES += [ + 'nsPerformanceStats.cpp' +] + +EXPORTS += [ + 'nsPerformanceStats.h' +] + +LOCAL_INCLUDES += [ + '/dom/base', +] + +FINAL_LIBRARY = 'xul' diff --git a/toolkit/components/perfmonitoring/nsIPerformanceStats.idl b/toolkit/components/perfmonitoring/nsIPerformanceStats.idl new file mode 100644 index 000000000..2effd5403 --- /dev/null +++ b/toolkit/components/perfmonitoring/nsIPerformanceStats.idl @@ -0,0 +1,333 @@ +/* -*- 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/. */ + +#include "nsISupports.idl" +#include "nsIArray.idl" +#include "nsIDOMWindow.idl" + +/** + * Mechanisms for querying the current process about performance + * information. + * + * JavaScript clients should rather use PerformanceStats.jsm. + */ + +/** + * Identification details for a performance group. + * + * A performance group is a set of JavaScript compartments whose + * performance is observed as a single entity. Typical examples of + * performance groups: an add-on, a webpage without its frames, a + * webpage with all its frames, the entire JS runtime, ... + */ +[scriptable, builtinclass, uuid(994c56be-939a-4f20-8364-124f6422d86a)] +interface nsIPerformanceGroupDetails: nsISupports { + /** + * An identifier unique to the component. + * + * This identifier is somewhat human-readable to aid with debugging, + * but clients should not rely upon the format. + */ + readonly attribute AString groupId; + + /** + * A somewhat human-readable name for the component. + */ + readonly attribute AString name; + + /** + * If the component is an add-on, the ID of the addon, + * otherwise an empty string. + */ + readonly attribute AString addonId; + + /** + * If the component is code executed in a window, the ID of the topmost + * outer window (i.e. the tab), otherwise 0. + */ + readonly attribute uint64_t windowId; + + /** + * `true` if this component is executed with system privileges + * (e.g. the platform itself or an add-on), `false` otherwise + * (e.g. webpages). + */ + readonly attribute bool isSystem; + + /** + * The process running this group. + */ + readonly attribute unsigned long long processId; + + /** + * `true` if the code is executed in a content process, `false` otherwise. + */ + readonly attribute bool isContentProcess; +}; + +/** + * Snapshot of the performance of a component, e.g. an add-on, a web + * page, system built-ins, a module or the entire process itself. + * + * All values are monotonic and are updated only when + * `nsIPerformanceStatsService.isStopwatchActive` is `true`. + */ +[scriptable, builtinclass, uuid(8a635d4b-aa56-466b-9a7d-9f91ca9405ef)] +interface nsIPerformanceStats: nsIPerformanceGroupDetails { + /** + * Total amount of time spent executing code in this group, in + * microseconds. + */ + readonly attribute unsigned long long totalUserTime; + readonly attribute unsigned long long totalSystemTime; + readonly attribute unsigned long long totalCPOWTime; + + /** + * Total number of times code execution entered this group, + * since process launch. This may be greater than the number + * of times we have entered the event loop. + */ + readonly attribute unsigned long long ticks; + + /** + * Jank indicator. + * + * durations[i] == number of times execution of this group + * lasted at lest 2^i ms. + */ + void getDurations([optional] out unsigned long aCount, + [retval, array, size_is(aCount)]out unsigned long long aNumberOfOccurrences); +}; + +/** + * A snapshot of the performance data of the process. + */ +[scriptable, builtinclass, uuid(13cc235b-739e-4690-b0e3-d89cbe036a93)] +interface nsIPerformanceSnapshot: nsISupports { + /** + * Data on all individual components. + */ + nsIArray getComponentsData(); + + /** + * Information on the process itself. + * + * This contains the total amount of time spent executing JS code, + * the total amount of time spent waiting for system calls while + * executing JS code, the total amount of time performing blocking + * inter-process calls, etc. + */ + nsIPerformanceStats getProcessData(); +}; + +/** + * A performance alert. + */ +[scriptable, builtinclass, uuid(a85706ab-d703-4687-8865-78cd771eab93)] +interface nsIPerformanceAlert: nsISupports { + /** + * A slowdown was detected. + * + * See REASON_JANK_* for details on whether this slowdown was user-noticeable. + */ + const unsigned long REASON_SLOWDOWN = 1; + + /** + * This alert was triggered during a jank in animation. + * + * In the current implementation, we consider that there is a jank + * in animation if delivery of the vsync message to the main thread + * has been delayed too much (see + * nsIPerformanceStatsService.animationJankLevelThreshold). + * + * Note that this is a heuristic which may provide false positives, + * so clients of this API are expected to perform post-processing to + * filter out such false positives. + */ + const unsigned long REASON_JANK_IN_ANIMATION = 2; + + /** + * This alert was triggered during a jank in user input. + * + * In the current implementation, we consider that there is a jank + * in animation if a user input was received either immediately + * before executing the offending code (see + * nsIPerformanceStatsService.userInputDelayThreshold) or while + * executing the offending code. + * + * Note that this is a heuristic which may provide false positives, + * so clients of this API are expected to perform post-processing to + * filter out such false positives. + */ + const unsigned long REASON_JANK_IN_INPUT = 4; + + /** + * The reason for the alert, as a bitwise or of the various REASON_* + * constants. + */ + readonly attribute unsigned long reason; + + /** + * Longest interval spent executing code in this group + * since the latest alert, in microseconds. + * + * Note that the underlying algorithm is probabilistic and may + * provide false positives, so clients of this API are expected to + * perform post-processing to filter out such false positives. In + * particular, a high system load will increase the noise level on + * this measure. + */ + readonly attribute unsigned long long highestJank; + + /** + * Longest interval spent executing CPOW in this group + * since the latest alert, in microseconds. + * + * This measure is reliable and involves no heuristics. However, + * note that the duration of CPOWs is increased by high system + * loads. + */ + readonly attribute unsigned long long highestCPOW; +}; + + +/** + * An observer for slow performance alerts. + */ +[scriptable, function, uuid(b746a929-3fec-420b-8ed8-c35d71995e05)] +interface nsIPerformanceObserver: nsISupports { + /** + * @param target The performance group that caused the jank. + * @param alert The performance cost that triggered the alert. + */ + void observe(in nsIPerformanceGroupDetails target, in nsIPerformanceAlert alert); +}; + + +/** + * A part of the system that may be observed for slow performance. + */ +[scriptable, builtinclass, uuid(b85720d0-e328-4342-9e46-8ca1acf8c70e)] +interface nsIPerformanceObservable: nsISupports { + /** + * If a single group is being observed, information on this group. + */ + readonly attribute nsIPerformanceGroupDetails target; + + /** + * Add an observer that will be informed in case of jank. + * + * Set `jankAlertThreshold` to determine how much jank is needed + * to trigger alerts. + * + * If the same observer is added more than once, it will be + * triggered as many times as it has been added. + */ + void addJankObserver(in nsIPerformanceObserver observer); + + /** + * Remove an observer previously added with `addJankObserver`. + * + * Noop if the observer hasn't been added. + */ + void removeJankObserver(in nsIPerformanceObserver observer); +}; + + +[scriptable, uuid(505bc42e-be38-4a53-baba-92cb33690cde)] +interface nsIPerformanceStatsService : nsISupports { + /** + * `true` if we should monitor CPOW, `false` otherwise. + */ + [implicit_jscontext] attribute bool isMonitoringCPOW; + + /** + * `true` if we should monitor jank, `false` otherwise. + */ + [implicit_jscontext] attribute bool isMonitoringJank; + + /** + * `true` if all compartments need to be monitored individually, + * `false` if only performance groups (i.e. entire add-ons, entire + * webpages, etc.) need to be monitored. + */ + [implicit_jscontext] attribute bool isMonitoringPerCompartment; + + /** + * Capture a snapshot of the performance data. + */ + [implicit_jscontext] nsIPerformanceSnapshot getSnapshot(); + + /** + * The threshold, in microseconds, above which a performance group is + * considered "slow" and should raise performance alerts. + */ + attribute unsigned long long jankAlertThreshold; + + /** + * If a user is seeing an animation and we spend too long executing + * JS code while blocking refresh, this will be visible to the user. + * + * We assume that any jank during an animation and lasting more than + * 2^animationJankLevelThreshold ms will be visible. + */ + attribute short animationJankLevelThreshold; + + /** + * If a user performs an input (e.g. clicking, pressing a key, but + * *NOT* moving the mouse), and we spend too long executing JS code + * before displaying feedback, this will be visible to the user even + * if there is no ongoing animation. + * + * We assume that any jank during `userInputDelayThreshold` us after + * the user input will be visible. + */ + attribute unsigned long long userInputDelayThreshold; + + /** + * A buffering delay, in milliseconds, used by the service to + * regroup performance alerts, before observers are actually + * noticed. Higher delays let the system avoid redundant + * notifications for the same group, and are generally better for + * performance. + */ + attribute unsigned long jankAlertBufferingDelay; + + /** + * Get a nsIPerformanceObservable representing an add-on. This + * observable may then be used to (un)register for watching + * performance alerts for this add-on. + * + * Note that this method has no way of finding out whether an add-on with this + * id is installed on the system. Also note that this covers only the current + * process. + * + * Use special add-on name "*" to get an observable that may be used + * to (un)register for watching performance alerts of all add-ons at + * once. + */ + nsIPerformanceObservable getObservableAddon(in AString addonId); + + /** + * Get a nsIPerformanceObservable representing a DOM window. This + * observable may then be used to (un)register for watching + * performance alerts for this window. + * + * Note that this covers only the current process. + * + * Use special window id 0 to get an observable that may be used to + * (un)register for watching performance alerts of all windows at + * once. + */ + nsIPerformanceObservable getObservableWindow(in unsigned long long windowId); +}; + + +%{C++ +#define NS_TOOLKIT_PERFORMANCESTATSSERVICE_CID {0xfd7435d4, 0x9ec4, 0x4699, \ + {0xad, 0xd4, 0x1b, 0xe8, 0x3d, 0xd6, 0x8e, 0xf3} } +#define NS_TOOLKIT_PERFORMANCESTATSSERVICE_CONTRACTID "@mozilla.org/toolkit/performance-stats-service;1" +%} diff --git a/toolkit/components/perfmonitoring/nsPerformanceStats.cpp b/toolkit/components/perfmonitoring/nsPerformanceStats.cpp new file mode 100644 index 000000000..eb924de46 --- /dev/null +++ b/toolkit/components/perfmonitoring/nsPerformanceStats.cpp @@ -0,0 +1,1620 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- *//* 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/. */ + +#include "nsPerformanceStats.h" + +#include "nsMemory.h" +#include "nsLiteralString.h" +#include "nsCRTGlue.h" +#include "nsServiceManagerUtils.h" + +#include "nsCOMArray.h" +#include "nsContentUtils.h" +#include "nsIMutableArray.h" +#include "nsReadableUtils.h" + +#include "jsapi.h" +#include "nsJSUtils.h" +#include "xpcpublic.h" +#include "jspubtd.h" + +#include "nsIDOMWindow.h" +#include "nsGlobalWindow.h" +#include "nsRefreshDriver.h" + +#include "mozilla/Unused.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/Services.h" +#include "mozilla/Telemetry.h" + +#if defined(XP_WIN) +#include <processthreadsapi.h> +#include <windows.h> +#else +#include <unistd.h> +#endif // defined(XP_WIN) + +#if defined(XP_MACOSX) +#include <mach/mach_init.h> +#include <mach/mach_interface.h> +#include <mach/mach_port.h> +#include <mach/mach_types.h> +#include <mach/message.h> +#include <mach/thread_info.h> +#elif defined(XP_UNIX) +#include <sys/time.h> +#include <sys/resource.h> +#endif // defined(XP_UNIX) +/* ------------------------------------------------------ + * + * Utility functions. + * + */ + +namespace { + +/** + * Get the private window for the current compartment. + * + * @return null if the code is not executed in a window or in + * case of error, a nsPIDOMWindow otherwise. + */ +already_AddRefed<nsPIDOMWindowOuter> +GetPrivateWindow(JSContext* cx) { + nsGlobalWindow* win = xpc::CurrentWindowOrNull(cx); + if (!win) { + return nullptr; + } + + nsPIDOMWindowOuter* outer = win->AsInner()->GetOuterWindow(); + if (!outer) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowOuter> top = outer->GetTop(); + if (!top) { + return nullptr; + } + + return top.forget(); +} + +bool +URLForGlobal(JSContext* cx, JS::Handle<JSObject*> global, nsAString& url) { + nsCOMPtr<nsIPrincipal> principal = nsContentUtils::ObjectPrincipal(global); + if (!principal) { + return false; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = principal->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv) || !uri) { + return false; + } + + nsAutoCString spec; + rv = uri->GetSpec(spec); + if (NS_FAILED(rv)) { + return false; + } + + url.Assign(NS_ConvertUTF8toUTF16(spec)); + return true; +} + +/** + * Extract a somewhat human-readable name from the current context. + */ +void +CompartmentName(JSContext* cx, JS::Handle<JSObject*> global, nsAString& name) { + // Attempt to use the URL as name. + if (URLForGlobal(cx, global, name)) { + return; + } + + // Otherwise, fallback to XPConnect's less readable but more + // complete naming scheme. + nsAutoCString cname; + xpc::GetCurrentCompartmentName(cx, cname); + name.Assign(NS_ConvertUTF8toUTF16(cname)); +} + +/** + * Generate a unique-to-the-application identifier for a group. + */ +void +GenerateUniqueGroupId(const JSContext* cx, uint64_t uid, uint64_t processId, nsAString& groupId) { + uint64_t contextId = reinterpret_cast<uintptr_t>(cx); + + groupId.AssignLiteral("process: "); + groupId.AppendInt(processId); + groupId.AppendLiteral(", thread: "); + groupId.AppendInt(contextId); + groupId.AppendLiteral(", group: "); + groupId.AppendInt(uid); +} + +static const char* TOPICS[] = { + "profile-before-change", + "quit-application", + "quit-application-granted", + "xpcom-will-shutdown" +}; + +} // namespace + +/* ------------------------------------------------------ + * + * class nsPerformanceObservationTarget + * + */ + + +NS_IMPL_ISUPPORTS(nsPerformanceObservationTarget, nsIPerformanceObservable) + + + +NS_IMETHODIMP +nsPerformanceObservationTarget::GetTarget(nsIPerformanceGroupDetails** _result) { + if (mDetails) { + NS_IF_ADDREF(*_result = mDetails); + } + return NS_OK; +}; + +void +nsPerformanceObservationTarget::SetTarget(nsPerformanceGroupDetails* details) { + MOZ_ASSERT(!mDetails); + mDetails = details; +}; + +NS_IMETHODIMP +nsPerformanceObservationTarget::AddJankObserver(nsIPerformanceObserver* observer) { + if (!mObservers.append(observer)) { + MOZ_CRASH(); + } + return NS_OK; +}; + +NS_IMETHODIMP +nsPerformanceObservationTarget::RemoveJankObserver(nsIPerformanceObserver* observer) { + for (auto iter = mObservers.begin(), end = mObservers.end(); iter < end; ++iter) { + if (*iter == observer) { + mObservers.erase(iter); + return NS_OK; + } + } + return NS_OK; +}; + +bool +nsPerformanceObservationTarget::HasObservers() const { + return !mObservers.empty(); +} + +void +nsPerformanceObservationTarget::NotifyJankObservers(nsIPerformanceGroupDetails* source, nsIPerformanceAlert* gravity) { + // Copy the vector to make sure that it won't change under our feet. + mozilla::Vector<nsCOMPtr<nsIPerformanceObserver>> observers; + if (!observers.appendAll(mObservers)) { + MOZ_CRASH(); + } + + // Now actually notify. + for (auto iter = observers.begin(), end = observers.end(); iter < end; ++iter) { + nsCOMPtr<nsIPerformanceObserver> observer = *iter; + mozilla::Unused << observer->Observe(source, gravity); + } +} + +/* ------------------------------------------------------ + * + * class nsGroupHolder + * + */ + +nsPerformanceObservationTarget* +nsGroupHolder::ObservationTarget() { + if (!mPendingObservationTarget) { + mPendingObservationTarget = new nsPerformanceObservationTarget(); + } + return mPendingObservationTarget; +} + +nsPerformanceGroup* +nsGroupHolder::GetGroup() { + return mGroup; +} + +void +nsGroupHolder::SetGroup(nsPerformanceGroup* group) { + MOZ_ASSERT(!mGroup); + mGroup = group; + group->SetObservationTarget(ObservationTarget()); + mPendingObservationTarget->SetTarget(group->Details()); +} + +/* ------------------------------------------------------ + * + * struct PerformanceData + * + */ + +PerformanceData::PerformanceData() + : mTotalUserTime(0) + , mTotalSystemTime(0) + , mTotalCPOWTime(0) + , mTicks(0) +{ + mozilla::PodArrayZero(mDurations); +} + +/* ------------------------------------------------------ + * + * class nsPerformanceGroupDetails + * + */ + +NS_IMPL_ISUPPORTS(nsPerformanceGroupDetails, nsIPerformanceGroupDetails) + +const nsAString& +nsPerformanceGroupDetails::Name() const { + return mName; +} + +const nsAString& +nsPerformanceGroupDetails::GroupId() const { + return mGroupId; +} + +const nsAString& +nsPerformanceGroupDetails::AddonId() const { + return mAddonId; +} + +uint64_t +nsPerformanceGroupDetails::WindowId() const { + return mWindowId; +} + +uint64_t +nsPerformanceGroupDetails::ProcessId() const { + return mProcessId; +} + +bool +nsPerformanceGroupDetails::IsSystem() const { + return mIsSystem; +} + +bool +nsPerformanceGroupDetails::IsAddon() const { + return mAddonId.Length() != 0; +} + +bool +nsPerformanceGroupDetails::IsWindow() const { + return mWindowId != 0; +} + +bool +nsPerformanceGroupDetails::IsContentProcess() const { + return XRE_GetProcessType() == GeckoProcessType_Content; +} + +/* readonly attribute AString name; */ +NS_IMETHODIMP +nsPerformanceGroupDetails::GetName(nsAString& aName) { + aName.Assign(Name()); + return NS_OK; +}; + +/* readonly attribute AString groupId; */ +NS_IMETHODIMP +nsPerformanceGroupDetails::GetGroupId(nsAString& aGroupId) { + aGroupId.Assign(GroupId()); + return NS_OK; +}; + +/* readonly attribute AString addonId; */ +NS_IMETHODIMP +nsPerformanceGroupDetails::GetAddonId(nsAString& aAddonId) { + aAddonId.Assign(AddonId()); + return NS_OK; +}; + +/* readonly attribute uint64_t windowId; */ +NS_IMETHODIMP +nsPerformanceGroupDetails::GetWindowId(uint64_t *aWindowId) { + *aWindowId = WindowId(); + return NS_OK; +} + +/* readonly attribute bool isSystem; */ +NS_IMETHODIMP +nsPerformanceGroupDetails::GetIsSystem(bool *_retval) { + *_retval = IsSystem(); + return NS_OK; +} + +/* + readonly attribute unsigned long long processId; +*/ +NS_IMETHODIMP +nsPerformanceGroupDetails::GetProcessId(uint64_t* processId) { + *processId = ProcessId(); + return NS_OK; +} + +/* readonly attribute bool IsContentProcess; */ +NS_IMETHODIMP +nsPerformanceGroupDetails::GetIsContentProcess(bool *_retval) { + *_retval = IsContentProcess(); + return NS_OK; +} + + +/* ------------------------------------------------------ + * + * class nsPerformanceStats + * + */ + +class nsPerformanceStats final: public nsIPerformanceStats +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPERFORMANCESTATS + NS_FORWARD_NSIPERFORMANCEGROUPDETAILS(mDetails->) + + nsPerformanceStats(nsPerformanceGroupDetails* item, + const PerformanceData& aPerformanceData) + : mDetails(item) + , mPerformanceData(aPerformanceData) + { + } + + +private: + RefPtr<nsPerformanceGroupDetails> mDetails; + PerformanceData mPerformanceData; + + ~nsPerformanceStats() {} +}; + +NS_IMPL_ISUPPORTS(nsPerformanceStats, nsIPerformanceStats, nsIPerformanceGroupDetails) + +/* readonly attribute unsigned long long totalUserTime; */ +NS_IMETHODIMP +nsPerformanceStats::GetTotalUserTime(uint64_t *aTotalUserTime) { + *aTotalUserTime = mPerformanceData.mTotalUserTime; + return NS_OK; +}; + +/* readonly attribute unsigned long long totalSystemTime; */ +NS_IMETHODIMP +nsPerformanceStats::GetTotalSystemTime(uint64_t *aTotalSystemTime) { + *aTotalSystemTime = mPerformanceData.mTotalSystemTime; + return NS_OK; +}; + +/* readonly attribute unsigned long long totalCPOWTime; */ +NS_IMETHODIMP +nsPerformanceStats::GetTotalCPOWTime(uint64_t *aCpowTime) { + *aCpowTime = mPerformanceData.mTotalCPOWTime; + return NS_OK; +}; + +/* readonly attribute unsigned long long ticks; */ +NS_IMETHODIMP +nsPerformanceStats::GetTicks(uint64_t *aTicks) { + *aTicks = mPerformanceData.mTicks; + return NS_OK; +}; + +/* void getDurations (out unsigned long aCount, [array, size_is (aCount), retval] out unsigned long long aNumberOfOccurrences); */ +NS_IMETHODIMP +nsPerformanceStats::GetDurations(uint32_t *aCount, uint64_t **aNumberOfOccurrences) { + const size_t length = mozilla::ArrayLength(mPerformanceData.mDurations); + if (aCount) { + *aCount = length; + } + *aNumberOfOccurrences = new uint64_t[length]; + for (size_t i = 0; i < length; ++i) { + (*aNumberOfOccurrences)[i] = mPerformanceData.mDurations[i]; + } + return NS_OK; +}; + + +/* ------------------------------------------------------ + * + * struct nsPerformanceSnapshot + * + */ + +class nsPerformanceSnapshot final : public nsIPerformanceSnapshot +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPERFORMANCESNAPSHOT + + nsPerformanceSnapshot() {} + + /** + * Append statistics to the list of components data. + */ + void AppendComponentsStats(nsIPerformanceStats* stats); + + /** + * Set the statistics attached to process data. + */ + void SetProcessStats(nsIPerformanceStats* group); + +private: + ~nsPerformanceSnapshot() {} + +private: + /** + * The data for all components. + */ + nsCOMArray<nsIPerformanceStats> mComponentsData; + + /** + * The data for the process. + */ + nsCOMPtr<nsIPerformanceStats> mProcessData; +}; + +NS_IMPL_ISUPPORTS(nsPerformanceSnapshot, nsIPerformanceSnapshot) + + +/* nsIArray getComponentsData (); */ +NS_IMETHODIMP +nsPerformanceSnapshot::GetComponentsData(nsIArray * *aComponents) +{ + const size_t length = mComponentsData.Length(); + nsCOMPtr<nsIMutableArray> components = do_CreateInstance(NS_ARRAY_CONTRACTID); + for (size_t i = 0; i < length; ++i) { + nsCOMPtr<nsIPerformanceStats> stats = mComponentsData[i]; + mozilla::DebugOnly<nsresult> rv = components->AppendElement(stats, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + components.forget(aComponents); + return NS_OK; +} + +/* nsIPerformanceStats getProcessData (); */ +NS_IMETHODIMP +nsPerformanceSnapshot::GetProcessData(nsIPerformanceStats * *aProcess) +{ + NS_IF_ADDREF(*aProcess = mProcessData); + return NS_OK; +} + +void +nsPerformanceSnapshot::AppendComponentsStats(nsIPerformanceStats* stats) +{ + mComponentsData.AppendElement(stats); +} + +void +nsPerformanceSnapshot::SetProcessStats(nsIPerformanceStats* stats) +{ + mProcessData = stats; +} + + + +/* ------------------------------------------------------ + * + * class PerformanceAlert + * + */ +class PerformanceAlert final: public nsIPerformanceAlert { +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPERFORMANCEALERT + + PerformanceAlert(const uint32_t reason, nsPerformanceGroup* source); +private: + ~PerformanceAlert() {} + + const uint32_t mReason; + + // The highest values reached by this group since the latest alert, + // in microseconds. + const uint64_t mHighestJank; + const uint64_t mHighestCPOW; +}; + +NS_IMPL_ISUPPORTS(PerformanceAlert, nsIPerformanceAlert); + +PerformanceAlert::PerformanceAlert(const uint32_t reason, nsPerformanceGroup* source) + : mReason(reason) + , mHighestJank(source->HighestRecentJank()) + , mHighestCPOW(source->HighestRecentCPOW()) +{ } + +NS_IMETHODIMP +PerformanceAlert::GetHighestJank(uint64_t* result) { + *result = mHighestJank; + return NS_OK; +} + +NS_IMETHODIMP +PerformanceAlert::GetHighestCPOW(uint64_t* result) { + *result = mHighestCPOW; + return NS_OK; +} + +NS_IMETHODIMP +PerformanceAlert::GetReason(uint32_t* result) { + *result = mReason; + return NS_OK; +} +/* ------------------------------------------------------ + * + * class PendingAlertsCollector + * + */ + +/** + * A timer callback in charge of collecting the groups in + * `mPendingAlerts` and triggering dispatch of performance alerts. + */ +class PendingAlertsCollector final: public nsITimerCallback { +public: + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + + explicit PendingAlertsCollector(nsPerformanceStatsService* service) + : mService(service) + , mPending(false) + { } + + nsresult Start(uint32_t timerDelayMS); + nsresult Dispose(); + +private: + ~PendingAlertsCollector() {} + + RefPtr<nsPerformanceStatsService> mService; + bool mPending; + + nsCOMPtr<nsITimer> mTimer; + + mozilla::Vector<uint64_t> mJankLevels; +}; + +NS_IMPL_ISUPPORTS(PendingAlertsCollector, nsITimerCallback); + +NS_IMETHODIMP +PendingAlertsCollector::Notify(nsITimer*) { + mPending = false; + mService->NotifyJankObservers(mJankLevels); + return NS_OK; +} + +nsresult +PendingAlertsCollector::Start(uint32_t timerDelayMS) { + if (mPending) { + // Collector is already started. + return NS_OK; + } + + if (!mTimer) { + mTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + } + + nsresult rv = mTimer->InitWithCallback(this, timerDelayMS, nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + return rv; + } + + mPending = true; + { + mozilla::DebugOnly<bool> result = nsRefreshDriver::GetJankLevels(mJankLevels); + MOZ_ASSERT(result); + } + + return NS_OK; +} + +nsresult +PendingAlertsCollector::Dispose() { + if (mTimer) { + mozilla::Unused << mTimer->Cancel(); + mTimer = nullptr; + } + mService = nullptr; + return NS_OK; +} + + + +/* ------------------------------------------------------ + * + * class nsPerformanceStatsService + * + */ + +NS_IMPL_ISUPPORTS(nsPerformanceStatsService, nsIPerformanceStatsService, nsIObserver) + +nsPerformanceStatsService::nsPerformanceStatsService() + : mIsAvailable(false) + , mDisposed(false) +#if defined(XP_WIN) + , mProcessId(GetCurrentProcessId()) +#else + , mProcessId(getpid()) +#endif + , mContext(mozilla::dom::danger::GetJSContext()) + , mUIdCounter(0) + , mTopGroup(nsPerformanceGroup::Make(mContext, + this, + NS_LITERAL_STRING("<process>"), // name + NS_LITERAL_STRING(""), // addonid + 0, // windowId + mProcessId, + true, // isSystem + nsPerformanceGroup::GroupScope::RUNTIME // scope + )) + , mIsHandlingUserInput(false) + , mProcessStayed(0) + , mProcessMoved(0) + , mProcessUpdateCounter(0) + , mIsMonitoringPerCompartment(false) + , mJankAlertThreshold(mozilla::MaxValue<uint64_t>::value) // By default, no alerts + , mJankAlertBufferingDelay(1000 /* ms */) + , mJankLevelVisibilityThreshold(/* 2 ^ */ 8 /* ms */) + , mMaxExpectedDurationOfInteractionUS(150 * 1000) +{ + mPendingAlertsCollector = new PendingAlertsCollector(this); + + // Attach some artificial group information to the universal listeners, to aid with debugging. + nsString groupIdForAddons; + GenerateUniqueGroupId(mContext, GetNextId(), mProcessId, groupIdForAddons); + mUniversalTargets.mAddons-> + SetTarget(new nsPerformanceGroupDetails(NS_LITERAL_STRING("<universal add-on listener>"), + groupIdForAddons, + NS_LITERAL_STRING("<universal add-on listener>"), + 0, // window id + mProcessId, + false)); + + + nsString groupIdForWindows; + GenerateUniqueGroupId(mContext, GetNextId(), mProcessId, groupIdForWindows); + mUniversalTargets.mWindows-> + SetTarget(new nsPerformanceGroupDetails(NS_LITERAL_STRING("<universal window listener>"), + groupIdForWindows, + NS_LITERAL_STRING("<universal window listener>"), + 0, // window id + mProcessId, + false)); +} + +nsPerformanceStatsService::~nsPerformanceStatsService() +{ } + +/** + * Clean up the service. + * + * Called during shutdown. Idempotent. + */ +void +nsPerformanceStatsService::Dispose() +{ + // Make sure that we do not accidentally destroy `this` while we are + // cleaning up back references. + RefPtr<nsPerformanceStatsService> kungFuDeathGrip(this); + mIsAvailable = false; + + if (mDisposed) { + // Make sure that we don't double-dispose. + return; + } + mDisposed = true; + + // Disconnect from nsIObserverService. + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + for (size_t i = 0; i < mozilla::ArrayLength(TOPICS); ++i) { + mozilla::Unused << obs->RemoveObserver(this, TOPICS[i]); + } + } + + // Clear up and disconnect from JSAPI. + JSContext* cx = mContext; + js::DisposePerformanceMonitoring(cx); + + mozilla::Unused << js::SetStopwatchIsMonitoringCPOW(cx, false); + mozilla::Unused << js::SetStopwatchIsMonitoringJank(cx, false); + + mozilla::Unused << js::SetStopwatchStartCallback(cx, nullptr, nullptr); + mozilla::Unused << js::SetStopwatchCommitCallback(cx, nullptr, nullptr); + mozilla::Unused << js::SetGetPerformanceGroupsCallback(cx, nullptr, nullptr); + + // Clear up and disconnect the alerts collector. + if (mPendingAlertsCollector) { + mPendingAlertsCollector->Dispose(); + mPendingAlertsCollector = nullptr; + } + mPendingAlerts.clear(); + + // Disconnect universal observers. Per-group observers will be + // disconnected below as part of `group->Dispose()`. + mUniversalTargets.mAddons = nullptr; + mUniversalTargets.mWindows = nullptr; + + // At this stage, the JS VM may still be holding references to + // instances of PerformanceGroup on the stack. To let the service be + // collected, we need to break the references from these groups to + // `this`. + mTopGroup->Dispose(); + mTopGroup = nullptr; + + // Copy references to the groups to a vector to ensure that we do + // not modify the hashtable while iterating it. + GroupVector groups; + for (auto iter = mGroups.Iter(); !iter.Done(); iter.Next()) { + if (!groups.append(iter.Get()->GetKey())) { + MOZ_CRASH(); + } + } + for (auto iter = groups.begin(), end = groups.end(); iter < end; ++iter) { + RefPtr<nsPerformanceGroup> group = *iter; + group->Dispose(); + } + + // Any remaining references to PerformanceGroup will be released as + // the VM unrolls the stack. If there are any nested event loops, + // this may take time. +} + +nsresult +nsPerformanceStatsService::Init() +{ + nsresult rv = InitInternal(); + if (NS_FAILED(rv)) { + // Attempt to clean up. + Dispose(); + } + return rv; +} + +nsresult +nsPerformanceStatsService::InitInternal() +{ + // Make sure that we release everything during shutdown. + // We are a bit defensive here, as we know that some strange behavior can break the + // regular shutdown order. + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + for (size_t i = 0; i < mozilla::ArrayLength(TOPICS); ++i) { + mozilla::Unused << obs->AddObserver(this, TOPICS[i], false); + } + } + + // Connect to JSAPI. + JSContext* cx = mContext; + if (!js::SetStopwatchStartCallback(cx, StopwatchStartCallback, this)) { + return NS_ERROR_UNEXPECTED; + } + if (!js::SetStopwatchCommitCallback(cx, StopwatchCommitCallback, this)) { + return NS_ERROR_UNEXPECTED; + } + if (!js::SetGetPerformanceGroupsCallback(cx, GetPerformanceGroupsCallback, this)) { + return NS_ERROR_UNEXPECTED; + } + + mTopGroup->setIsActive(true); + mIsAvailable = true; + + return NS_OK; +} + +// Observe shutdown events. +NS_IMETHODIMP +nsPerformanceStatsService::Observe(nsISupports *aSubject, const char *aTopic, + const char16_t *aData) +{ + MOZ_ASSERT(strcmp(aTopic, "profile-before-change") == 0 + || strcmp(aTopic, "quit-application") == 0 + || strcmp(aTopic, "quit-application-granted") == 0 + || strcmp(aTopic, "xpcom-will-shutdown") == 0); + + Dispose(); + return NS_OK; +} + +/*static*/ bool +nsPerformanceStatsService::IsHandlingUserInput() { + if (mozilla::EventStateManager::LatestUserInputStart().IsNull()) { + return false; + } + bool result = mozilla::TimeStamp::Now() - mozilla::EventStateManager::LatestUserInputStart() <= mozilla::TimeDuration::FromMicroseconds(mMaxExpectedDurationOfInteractionUS); + return result; +} + +/* [implicit_jscontext] attribute bool isMonitoringCPOW; */ +NS_IMETHODIMP +nsPerformanceStatsService::GetIsMonitoringCPOW(JSContext* cx, bool *aIsStopwatchActive) +{ + if (!mIsAvailable) { + return NS_ERROR_NOT_AVAILABLE; + } + + *aIsStopwatchActive = js::GetStopwatchIsMonitoringCPOW(cx); + return NS_OK; +} +NS_IMETHODIMP +nsPerformanceStatsService::SetIsMonitoringCPOW(JSContext* cx, bool aIsStopwatchActive) +{ + if (!mIsAvailable) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!js::SetStopwatchIsMonitoringCPOW(cx, aIsStopwatchActive)) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; +} + +/* [implicit_jscontext] attribute bool isMonitoringJank; */ +NS_IMETHODIMP +nsPerformanceStatsService::GetIsMonitoringJank(JSContext* cx, bool *aIsStopwatchActive) +{ + if (!mIsAvailable) { + return NS_ERROR_NOT_AVAILABLE; + } + + *aIsStopwatchActive = js::GetStopwatchIsMonitoringJank(cx); + return NS_OK; +} +NS_IMETHODIMP +nsPerformanceStatsService::SetIsMonitoringJank(JSContext* cx, bool aIsStopwatchActive) +{ + if (!mIsAvailable) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!js::SetStopwatchIsMonitoringJank(cx, aIsStopwatchActive)) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; +} + +/* [implicit_jscontext] attribute bool isMonitoringPerCompartment; */ +NS_IMETHODIMP +nsPerformanceStatsService::GetIsMonitoringPerCompartment(JSContext*, bool *aIsMonitoringPerCompartment) +{ + if (!mIsAvailable) { + return NS_ERROR_NOT_AVAILABLE; + } + + *aIsMonitoringPerCompartment = mIsMonitoringPerCompartment; + return NS_OK; +} +NS_IMETHODIMP +nsPerformanceStatsService::SetIsMonitoringPerCompartment(JSContext*, bool aIsMonitoringPerCompartment) +{ + if (!mIsAvailable) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (aIsMonitoringPerCompartment == mIsMonitoringPerCompartment) { + return NS_OK; + } + + // Relatively slow update: walk the entire lost of performance groups, + // update the active flag of those that have changed. + // + // Alternative strategies could be envisioned to make the update + // much faster, at the expense of the speed of calling `isActive()`, + // (e.g. deferring `isActive()` to the nsPerformanceStatsService), + // but we expect that `isActive()` can be called thousands of times + // per second, while `SetIsMonitoringPerCompartment` is not called + // at all during most Firefox runs. + + for (auto iter = mGroups.Iter(); !iter.Done(); iter.Next()) { + RefPtr<nsPerformanceGroup> group = iter.Get()->GetKey(); + if (group->Scope() == nsPerformanceGroup::GroupScope::COMPARTMENT) { + group->setIsActive(aIsMonitoringPerCompartment); + } + } + mIsMonitoringPerCompartment = aIsMonitoringPerCompartment; + return NS_OK; +} + +NS_IMETHODIMP +nsPerformanceStatsService::GetJankAlertThreshold(uint64_t* result) { + *result = mJankAlertThreshold; + return NS_OK; +} + +NS_IMETHODIMP +nsPerformanceStatsService::SetJankAlertThreshold(uint64_t value) { + mJankAlertThreshold = value; + return NS_OK; +} + +NS_IMETHODIMP +nsPerformanceStatsService::GetJankAlertBufferingDelay(uint32_t* result) { + *result = mJankAlertBufferingDelay; + return NS_OK; +} + +NS_IMETHODIMP +nsPerformanceStatsService::SetJankAlertBufferingDelay(uint32_t value) { + mJankAlertBufferingDelay = value; + return NS_OK; +} + +nsresult +nsPerformanceStatsService::UpdateTelemetry() +{ + // Promote everything to floating-point explicitly before dividing. + const double processStayed = mProcessStayed; + const double processMoved = mProcessMoved; + + if (processStayed <= 0 || processMoved <= 0 || processStayed + processMoved <= 0) { + // Overflow/underflow/nothing to report + return NS_OK; + } + + const double proportion = (100 * processStayed) / (processStayed + processMoved); + if (proportion < 0 || proportion > 100) { + // Overflow/underflow + return NS_OK; + } + + mozilla::Telemetry::Accumulate(mozilla::Telemetry::PERF_MONITORING_TEST_CPU_RESCHEDULING_PROPORTION_MOVED, (uint32_t)proportion); + return NS_OK; +} + + +/* static */ nsIPerformanceStats* +nsPerformanceStatsService::GetStatsForGroup(const js::PerformanceGroup* group) +{ + return GetStatsForGroup(nsPerformanceGroup::Get(group)); +} + +/* static */ nsIPerformanceStats* +nsPerformanceStatsService::GetStatsForGroup(const nsPerformanceGroup* group) +{ + return new nsPerformanceStats(group->Details(), group->data); +} + +/* [implicit_jscontext] nsIPerformanceSnapshot getSnapshot (); */ +NS_IMETHODIMP +nsPerformanceStatsService::GetSnapshot(JSContext* cx, nsIPerformanceSnapshot * *aSnapshot) +{ + if (!mIsAvailable) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<nsPerformanceSnapshot> snapshot = new nsPerformanceSnapshot(); + snapshot->SetProcessStats(GetStatsForGroup(mTopGroup)); + + for (auto iter = mGroups.Iter(); !iter.Done(); iter.Next()) { + auto* entry = iter.Get(); + nsPerformanceGroup* group = entry->GetKey(); + if (group->isActive()) { + snapshot->AppendComponentsStats(GetStatsForGroup(group)); + } + } + + js::GetPerfMonitoringTestCpuRescheduling(cx, &mProcessStayed, &mProcessMoved); + + if (++mProcessUpdateCounter % 10 == 0) { + mozilla::Unused << UpdateTelemetry(); + } + + snapshot.forget(aSnapshot); + + return NS_OK; +} + +uint64_t +nsPerformanceStatsService::GetNextId() { + return ++mUIdCounter; +} + +/* static*/ bool +nsPerformanceStatsService::GetPerformanceGroupsCallback(JSContext* cx, + js::PerformanceGroupVector& out, + void* closure) +{ + RefPtr<nsPerformanceStatsService> self = reinterpret_cast<nsPerformanceStatsService*>(closure); + return self->GetPerformanceGroups(cx, out); +} + +bool +nsPerformanceStatsService::GetPerformanceGroups(JSContext* cx, + js::PerformanceGroupVector& out) +{ + JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + if (!global) { + // While it is possible for a compartment to have no global + // (e.g. atoms), this compartment is not very interesting for us. + return true; + } + + // All compartments belong to the top group. + if (!out.append(mTopGroup)) { + JS_ReportOutOfMemory(cx); + return false; + } + + nsAutoString name; + CompartmentName(cx, global, name); + bool isSystem = nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(global)); + + // Find out if the compartment is executed by an add-on. If so, its + // duration should count towards the total duration of the add-on. + JSAddonId* jsaddonId = AddonIdOfObject(global); + nsString addonId; + if (jsaddonId) { + AssignJSFlatString(addonId, (JSFlatString*)jsaddonId); + auto entry = mAddonIdToGroup.PutEntry(addonId); + if (!entry->GetGroup()) { + nsString addonName = name; + addonName.AppendLiteral(" (as addon "); + addonName.Append(addonId); + addonName.AppendLiteral(")"); + entry-> + SetGroup(nsPerformanceGroup::Make(mContext, this, + addonName, addonId, 0, + mProcessId, isSystem, + nsPerformanceGroup::GroupScope::ADDON) + ); + } + if (!out.append(entry->GetGroup())) { + JS_ReportOutOfMemory(cx); + return false; + } + } + + // Find out if the compartment is executed by a window. If so, its + // duration should count towards the total duration of the window. + uint64_t windowId = 0; + if (nsCOMPtr<nsPIDOMWindowOuter> ptop = GetPrivateWindow(cx)) { + windowId = ptop->WindowID(); + auto entry = mWindowIdToGroup.PutEntry(windowId); + if (!entry->GetGroup()) { + nsString windowName = name; + windowName.AppendLiteral(" (as window "); + windowName.AppendInt(windowId); + windowName.AppendLiteral(")"); + entry-> + SetGroup(nsPerformanceGroup::Make(mContext, this, + windowName, EmptyString(), windowId, + mProcessId, isSystem, + nsPerformanceGroup::GroupScope::WINDOW) + ); + } + if (!out.append(entry->GetGroup())) { + JS_ReportOutOfMemory(cx); + return false; + } + } + + // All compartments have their own group. + auto group = + nsPerformanceGroup::Make(mContext, this, + name, addonId, windowId, + mProcessId, isSystem, + nsPerformanceGroup::GroupScope::COMPARTMENT); + if (!out.append(group)) { + JS_ReportOutOfMemory(cx); + return false; + } + + return true; +} + +/*static*/ bool +nsPerformanceStatsService::StopwatchStartCallback(uint64_t iteration, void* closure) { + RefPtr<nsPerformanceStatsService> self = reinterpret_cast<nsPerformanceStatsService*>(closure); + return self->StopwatchStart(iteration); +} + +bool +nsPerformanceStatsService::StopwatchStart(uint64_t iteration) { + mIteration = iteration; + + mIsHandlingUserInput = IsHandlingUserInput(); + mUserInputCount = mozilla::EventStateManager::UserInputCount(); + + nsresult rv = GetResources(&mUserTimeStart, &mSystemTimeStart); + if (NS_FAILED(rv)) { + return false; + } + + return true; +} + +/*static*/ bool +nsPerformanceStatsService::StopwatchCommitCallback(uint64_t iteration, + js::PerformanceGroupVector& recentGroups, + void* closure) +{ + RefPtr<nsPerformanceStatsService> self = reinterpret_cast<nsPerformanceStatsService*>(closure); + return self->StopwatchCommit(iteration, recentGroups); +} + +bool +nsPerformanceStatsService::StopwatchCommit(uint64_t iteration, + js::PerformanceGroupVector& recentGroups) +{ + MOZ_ASSERT(iteration == mIteration); + MOZ_ASSERT(!recentGroups.empty()); + + uint64_t userTimeStop, systemTimeStop; + nsresult rv = GetResources(&userTimeStop, &systemTimeStop); + if (NS_FAILED(rv)) { + return false; + } + + // `GetResources` is not guaranteed to be monotonic, so round up + // any negative result to 0 milliseconds. + uint64_t userTimeDelta = 0; + if (userTimeStop > mUserTimeStart) + userTimeDelta = userTimeStop - mUserTimeStart; + + uint64_t systemTimeDelta = 0; + if (systemTimeStop > mSystemTimeStart) + systemTimeDelta = systemTimeStop - mSystemTimeStart; + + MOZ_ASSERT(mTopGroup->isUsedInThisIteration()); + const uint64_t totalRecentCycles = mTopGroup->recentCycles(iteration); + + const bool isHandlingUserInput = mIsHandlingUserInput || mozilla::EventStateManager::UserInputCount() > mUserInputCount; + + // We should only reach this stage if `group` has had some activity. + MOZ_ASSERT(mTopGroup->recentTicks(iteration) > 0); + for (auto iter = recentGroups.begin(), end = recentGroups.end(); iter != end; ++iter) { + RefPtr<nsPerformanceGroup> group = nsPerformanceGroup::Get(*iter); + CommitGroup(iteration, userTimeDelta, systemTimeDelta, totalRecentCycles, isHandlingUserInput, group); + } + + // Make sure that `group` was treated along with the other items of `recentGroups`. + MOZ_ASSERT(!mTopGroup->isUsedInThisIteration()); + MOZ_ASSERT(mTopGroup->recentTicks(iteration) == 0); + + if (!mPendingAlerts.empty()) { + mPendingAlertsCollector->Start(mJankAlertBufferingDelay); + } + + return true; +} + +void +nsPerformanceStatsService::CommitGroup(uint64_t iteration, + uint64_t totalUserTimeDelta, uint64_t totalSystemTimeDelta, + uint64_t totalCyclesDelta, + bool isHandlingUserInput, + nsPerformanceGroup* group) { + + MOZ_ASSERT(group->isUsedInThisIteration()); + + const uint64_t ticksDelta = group->recentTicks(iteration); + const uint64_t cpowTimeDelta = group->recentCPOW(iteration); + const uint64_t cyclesDelta = group->recentCycles(iteration); + group->resetRecentData(); + + // We have now performed all cleanup and may `return` at any time without fear of leaks. + + if (group->iteration() != iteration) { + // Stale data, don't commit. + return; + } + + // When we add a group as changed, we immediately set its + // `recentTicks` from 0 to 1. If we have `ticksDelta == 0` at + // this stage, we have already called `resetRecentData` but we + // haven't removed it from the list. + MOZ_ASSERT(ticksDelta != 0); + MOZ_ASSERT(cyclesDelta <= totalCyclesDelta); + if (cyclesDelta == 0 || totalCyclesDelta == 0) { + // Nothing useful, don't commit. + return; + } + + double proportion = (double)cyclesDelta / (double)totalCyclesDelta; + MOZ_ASSERT(proportion <= 1); + + const uint64_t userTimeDelta = proportion * totalUserTimeDelta; + const uint64_t systemTimeDelta = proportion * totalSystemTimeDelta; + + group->data.mTotalUserTime += userTimeDelta; + group->data.mTotalSystemTime += systemTimeDelta; + group->data.mTotalCPOWTime += cpowTimeDelta; + group->data.mTicks += ticksDelta; + + const uint64_t totalTimeDelta = userTimeDelta + systemTimeDelta + cpowTimeDelta; + uint64_t duration = 1000; // 1ms in µs + for (size_t i = 0; + i < mozilla::ArrayLength(group->data.mDurations) && duration < totalTimeDelta; + ++i, duration *= 2) { + group->data.mDurations[i]++; + } + + group->RecordJank(totalTimeDelta); + group->RecordCPOW(cpowTimeDelta); + if (isHandlingUserInput) { + group->RecordUserInput(); + } + + if (totalTimeDelta >= mJankAlertThreshold) { + if (!group->HasPendingAlert()) { + if (mPendingAlerts.append(group)) { + group->SetHasPendingAlert(true); + } + return; + } + } + + return; +} + +nsresult +nsPerformanceStatsService::GetResources(uint64_t* userTime, + uint64_t* systemTime) const { + MOZ_ASSERT(userTime); + MOZ_ASSERT(systemTime); + +#if defined(XP_MACOSX) + // On MacOS X, to get we per-thread data, we need to + // reach into the kernel. + + mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT; + thread_basic_info_data_t info; + mach_port_t port = mach_thread_self(); + kern_return_t err = + thread_info(/* [in] targeted thread*/ port, + /* [in] nature of information*/ THREAD_BASIC_INFO, + /* [out] thread information */ (thread_info_t)&info, + /* [inout] number of items */ &count); + + // We do not need ability to communicate with the thread, so + // let's release the port. + mach_port_deallocate(mach_task_self(), port); + + if (err != KERN_SUCCESS) + return NS_ERROR_FAILURE; + + *userTime = info.user_time.microseconds + info.user_time.seconds * 1000000; + *systemTime = info.system_time.microseconds + info.system_time.seconds * 1000000; + +#elif defined(XP_UNIX) + struct rusage rusage; +#if defined(RUSAGE_THREAD) + // Under Linux, we can obtain per-thread statistics + int err = getrusage(RUSAGE_THREAD, &rusage); +#else + // Under other Unices, we need to do with more noisy + // per-process statistics. + int err = getrusage(RUSAGE_SELF, &rusage); +#endif // defined(RUSAGE_THREAD) + + if (err) + return NS_ERROR_FAILURE; + + *userTime = rusage.ru_utime.tv_usec + rusage.ru_utime.tv_sec * 1000000; + *systemTime = rusage.ru_stime.tv_usec + rusage.ru_stime.tv_sec * 1000000; + +#elif defined(XP_WIN) + // Under Windows, we can obtain per-thread statistics. Experience + // seems to suggest that they are not very accurate under Windows + // XP, though. + FILETIME creationFileTime; // Ignored + FILETIME exitFileTime; // Ignored + FILETIME kernelFileTime; + FILETIME userFileTime; + BOOL success = GetThreadTimes(GetCurrentThread(), + &creationFileTime, &exitFileTime, + &kernelFileTime, &userFileTime); + + if (!success) + return NS_ERROR_FAILURE; + + ULARGE_INTEGER kernelTimeInt; + kernelTimeInt.LowPart = kernelFileTime.dwLowDateTime; + kernelTimeInt.HighPart = kernelFileTime.dwHighDateTime; + // Convert 100 ns to 1 us. + *systemTime = kernelTimeInt.QuadPart / 10; + + ULARGE_INTEGER userTimeInt; + userTimeInt.LowPart = userFileTime.dwLowDateTime; + userTimeInt.HighPart = userFileTime.dwHighDateTime; + // Convert 100 ns to 1 us. + *userTime = userTimeInt.QuadPart / 10; + +#endif // defined(XP_MACOSX) || defined(XP_UNIX) || defined(XP_WIN) + + return NS_OK; +} + +void +nsPerformanceStatsService::NotifyJankObservers(const mozilla::Vector<uint64_t>& aPreviousJankLevels) { + GroupVector alerts; + mPendingAlerts.swap(alerts); + if (!mPendingAlertsCollector) { + // We are shutting down. + return; + } + + // Find out if we have noticed any user-noticeable delay in an + // animation recently (i.e. since the start of the execution of JS + // code that caused this collector to start). If so, we'll mark any + // alert as part of a user-noticeable jank. Note that this doesn't + // mean with any certainty that the alert is the only cause of jank, + // or even the main cause of jank. + mozilla::Vector<uint64_t> latestJankLevels; + { + mozilla::DebugOnly<bool> result = nsRefreshDriver::GetJankLevels(latestJankLevels); + MOZ_ASSERT(result); + } + MOZ_ASSERT(latestJankLevels.length() == aPreviousJankLevels.length()); + + bool isJankInAnimation = false; + for (size_t i = mJankLevelVisibilityThreshold; i < latestJankLevels.length(); ++i) { + if (latestJankLevels[i] > aPreviousJankLevels[i]) { + isJankInAnimation = true; + break; + } + } + + MOZ_ASSERT(!alerts.empty()); + const bool hasUniversalAddonObservers = mUniversalTargets.mAddons->HasObservers(); + const bool hasUniversalWindowObservers = mUniversalTargets.mWindows->HasObservers(); + for (auto iter = alerts.begin(); iter < alerts.end(); ++iter) { + MOZ_ASSERT(iter); + RefPtr<nsPerformanceGroup> group = *iter; + group->SetHasPendingAlert(false); + + RefPtr<nsPerformanceGroupDetails> details = group->Details(); + nsPerformanceObservationTarget* targets[3] = { + hasUniversalAddonObservers && details->IsAddon() ? mUniversalTargets.mAddons.get() : nullptr, + hasUniversalWindowObservers && details->IsWindow() ? mUniversalTargets.mWindows.get() : nullptr, + group->ObservationTarget() + }; + + bool isJankInInput = group->HasRecentUserInput(); + + RefPtr<PerformanceAlert> alert; + for (nsPerformanceObservationTarget* target : targets) { + if (!target) { + continue; + } + if (!alert) { + const uint32_t reason = nsIPerformanceAlert::REASON_SLOWDOWN + | (isJankInAnimation ? nsIPerformanceAlert::REASON_JANK_IN_ANIMATION : 0) + | (isJankInInput ? nsIPerformanceAlert::REASON_JANK_IN_INPUT : 0); + // Wait until we are sure we need to allocate before we allocate. + alert = new PerformanceAlert(reason, group); + } + target->NotifyJankObservers(details, alert); + } + + group->ResetRecent(); + } + +} + +NS_IMETHODIMP +nsPerformanceStatsService::GetObservableAddon(const nsAString& addonId, + nsIPerformanceObservable** result) { + if (addonId.Equals(NS_LITERAL_STRING("*"))) { + NS_IF_ADDREF(*result = mUniversalTargets.mAddons); + } else { + auto entry = mAddonIdToGroup.PutEntry(addonId); + NS_IF_ADDREF(*result = entry->ObservationTarget()); + } + return NS_OK; +} + +NS_IMETHODIMP +nsPerformanceStatsService::GetObservableWindow(uint64_t windowId, + nsIPerformanceObservable** result) { + if (windowId == 0) { + NS_IF_ADDREF(*result = mUniversalTargets.mWindows); + } else { + auto entry = mWindowIdToGroup.PutEntry(windowId); + NS_IF_ADDREF(*result = entry->ObservationTarget()); + } + return NS_OK; +} + +NS_IMETHODIMP +nsPerformanceStatsService::GetAnimationJankLevelThreshold(short* result) { + *result = mJankLevelVisibilityThreshold; + return NS_OK; +} + +NS_IMETHODIMP +nsPerformanceStatsService::SetAnimationJankLevelThreshold(short value) { + mJankLevelVisibilityThreshold = value; + return NS_OK; +} + +NS_IMETHODIMP +nsPerformanceStatsService::GetUserInputDelayThreshold(uint64_t* result) { + *result = mMaxExpectedDurationOfInteractionUS; + return NS_OK; +} + +NS_IMETHODIMP +nsPerformanceStatsService::SetUserInputDelayThreshold(uint64_t value) { + mMaxExpectedDurationOfInteractionUS = value; + return NS_OK; +} + + + +nsPerformanceStatsService::UniversalTargets::UniversalTargets() + : mAddons(new nsPerformanceObservationTarget()) + , mWindows(new nsPerformanceObservationTarget()) +{ } + +/* ------------------------------------------------------ + * + * Class nsPerformanceGroup + * + */ + +/*static*/ nsPerformanceGroup* +nsPerformanceGroup::Make(JSContext* cx, + nsPerformanceStatsService* service, + const nsAString& name, + const nsAString& addonId, + uint64_t windowId, + uint64_t processId, + bool isSystem, + GroupScope scope) +{ + nsString groupId; + ::GenerateUniqueGroupId(cx, service->GetNextId(), processId, groupId); + return new nsPerformanceGroup(service, name, groupId, addonId, windowId, processId, isSystem, scope); +} + +nsPerformanceGroup::nsPerformanceGroup(nsPerformanceStatsService* service, + const nsAString& name, + const nsAString& groupId, + const nsAString& addonId, + uint64_t windowId, + uint64_t processId, + bool isSystem, + GroupScope scope) + : mDetails(new nsPerformanceGroupDetails(name, groupId, addonId, windowId, processId, isSystem)) + , mService(service) + , mScope(scope) + , mHighestJank(0) + , mHighestCPOW(0) + , mHasRecentUserInput(false) + , mHasPendingAlert(false) +{ + mozilla::Unused << mService->mGroups.PutEntry(this); + +#if defined(DEBUG) + if (scope == GroupScope::ADDON) { + MOZ_ASSERT(mDetails->IsAddon()); + MOZ_ASSERT(!mDetails->IsWindow()); + } else if (scope == GroupScope::WINDOW) { + MOZ_ASSERT(mDetails->IsWindow()); + MOZ_ASSERT(!mDetails->IsAddon()); + } else if (scope == GroupScope::RUNTIME) { + MOZ_ASSERT(!mDetails->IsWindow()); + MOZ_ASSERT(!mDetails->IsAddon()); + } +#endif // defined(DEBUG) + setIsActive(mScope != GroupScope::COMPARTMENT || mService->mIsMonitoringPerCompartment); +} + +void +nsPerformanceGroup::Dispose() { + if (!mService) { + // We have already called `Dispose()`. + return; + } + if (mObservationTarget) { + mObservationTarget = nullptr; + } + + // Remove any reference to the service. + RefPtr<nsPerformanceStatsService> service; + service.swap(mService); + + // Remove any dangling pointer to `this`. + service->mGroups.RemoveEntry(this); + + if (mScope == GroupScope::ADDON) { + MOZ_ASSERT(mDetails->IsAddon()); + service->mAddonIdToGroup.RemoveEntry(mDetails->AddonId()); + } else if (mScope == GroupScope::WINDOW) { + MOZ_ASSERT(mDetails->IsWindow()); + service->mWindowIdToGroup.RemoveEntry(mDetails->WindowId()); + } +} + +nsPerformanceGroup::~nsPerformanceGroup() { + Dispose(); +} + +nsPerformanceGroup::GroupScope +nsPerformanceGroup::Scope() const { + return mScope; +} + +nsPerformanceGroupDetails* +nsPerformanceGroup::Details() const { + return mDetails; +} + +void +nsPerformanceGroup::SetObservationTarget(nsPerformanceObservationTarget* target) { + MOZ_ASSERT(!mObservationTarget); + mObservationTarget = target; +} + +nsPerformanceObservationTarget* +nsPerformanceGroup::ObservationTarget() const { + return mObservationTarget; +} + +bool +nsPerformanceGroup::HasPendingAlert() const { + return mHasPendingAlert; +} + +void +nsPerformanceGroup::SetHasPendingAlert(bool value) { + mHasPendingAlert = value; +} + + +void +nsPerformanceGroup::RecordJank(uint64_t jank) { + if (jank > mHighestJank) { + mHighestJank = jank; + } +} + +void +nsPerformanceGroup::RecordCPOW(uint64_t cpow) { + if (cpow > mHighestCPOW) { + mHighestCPOW = cpow; + } +} + +uint64_t +nsPerformanceGroup::HighestRecentJank() { + return mHighestJank; +} + +uint64_t +nsPerformanceGroup::HighestRecentCPOW() { + return mHighestCPOW; +} + +bool +nsPerformanceGroup::HasRecentUserInput() { + return mHasRecentUserInput; +} + +void +nsPerformanceGroup::RecordUserInput() { + mHasRecentUserInput = true; +} + +void +nsPerformanceGroup::ResetRecent() { + mHighestJank = 0; + mHighestCPOW = 0; + mHasRecentUserInput = false; +} diff --git a/toolkit/components/perfmonitoring/nsPerformanceStats.h b/toolkit/components/perfmonitoring/nsPerformanceStats.h new file mode 100644 index 000000000..c82a3e92c --- /dev/null +++ b/toolkit/components/perfmonitoring/nsPerformanceStats.h @@ -0,0 +1,825 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsPerformanceStats_h +#define nsPerformanceStats_h + +#include "jsapi.h" + +#include "nsHashKeys.h" +#include "nsTHashtable.h" + +#include "nsIObserver.h" +#include "nsPIDOMWindow.h" + +#include "nsIPerformanceStats.h" + +class nsPerformanceGroup; +class nsPerformanceGroupDetails; + +typedef mozilla::Vector<RefPtr<nsPerformanceGroup>> GroupVector; + +/** + * A data structure for registering observers interested in + * performance alerts. + * + * Each performance group owns a single instance of this class. + * Additionally, the service owns instances designed to observe the + * performance alerts in all add-ons (respectively webpages). + */ +class nsPerformanceObservationTarget final: public nsIPerformanceObservable { +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPERFORMANCEOBSERVABLE + + /** + * `true` if this target has at least once performance observer + * registered, `false` otherwise. + */ + bool HasObservers() const; + + /** + * Notify all the observers that jank has happened. + */ + void NotifyJankObservers(nsIPerformanceGroupDetails* source, nsIPerformanceAlert* gravity); + + /** + * Set the details on the group being observed. + */ + void SetTarget(nsPerformanceGroupDetails* details); + +private: + ~nsPerformanceObservationTarget() {} + + // The observers for this target. We hold them as a vector, despite + // the linear removal cost, as we expect that the typical number of + // observers will be lower than 3, and that (un)registrations will + // be fairly infrequent. + mozilla::Vector<nsCOMPtr<nsIPerformanceObserver>> mObservers; + + // Details on the group being observed. May be `nullptr`. + RefPtr<nsPerformanceGroupDetails> mDetails; +}; + +/** + * The base class for entries of maps from addon id/window id to + * performance group. + * + * Performance observers may be registered before their group is + * created (e.g., one may register an observer for an add-on before + * all its modules are loaded, or even before the add-on is loaded at + * all or for an observer for a webpage before all its iframes are + * loaded). This class serves to hold the observation target until the + * performance group may be created, and then to associate the + * observation target and the performance group. + */ +class nsGroupHolder { +public: + nsGroupHolder() + : mGroup(nullptr) + , mPendingObservationTarget(nullptr) + { } + + /** + * Get the observation target, creating it if necessary. + */ + nsPerformanceObservationTarget* ObservationTarget(); + + /** + * Get the group, if it has been created. + * + * May return `null` if the group hasn't been created yet. + */ + class nsPerformanceGroup* GetGroup(); + + /** + * Set the group. + * + * Once this method has been called, calling + * `this->ObservationTarget()` and `group->ObservationTarget()` is equivalent. + * + * Must only be called once. + */ + void SetGroup(class nsPerformanceGroup*); +private: + // The group. Initially `nullptr`, until we have called `SetGroup`. + class nsPerformanceGroup* mGroup; + + // The observation target. Instantiated by the first call to + // `ObservationTarget()`. + RefPtr<nsPerformanceObservationTarget> mPendingObservationTarget; +}; + +/** + * An implementation of the nsIPerformanceStatsService. + * + * Note that this implementation is not thread-safe. + */ +class nsPerformanceStatsService final : public nsIPerformanceStatsService, + public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPERFORMANCESTATSSERVICE + NS_DECL_NSIOBSERVER + + nsPerformanceStatsService(); + nsresult Init(); + +private: + nsresult InitInternal(); + void Dispose(); + ~nsPerformanceStatsService(); + +protected: + friend nsPerformanceGroup; + + /** + * `false` until `Init()` and after `Dispose()`, `true` inbetween. + */ + bool mIsAvailable; + + /** + * `true` once we have called `Dispose()`. + */ + bool mDisposed; + + /** + * A unique identifier for the process. + * + * Process HANDLE under Windows, pid under Unix. + */ + const uint64_t mProcessId; + + /** + * The JS Context for the main thread. + */ + JSContext* const mContext; + + /** + * Generate unique identifiers. + */ + uint64_t GetNextId(); + uint64_t mUIdCounter; + + + + /** + * Extract a snapshot of performance statistics from a performance group. + */ + static nsIPerformanceStats* GetStatsForGroup(const js::PerformanceGroup* group); + static nsIPerformanceStats* GetStatsForGroup(const nsPerformanceGroup* group); + + + + /** + * Get the performance groups associated to a given JS compartment. + * + * A compartment is typically associated to the following groups: + * - the top group, shared by the entire process; + * - the window group, if the code is executed in a window, shared + * by all compartments for that window (typically, all frames); + * - the add-on group, if the code is executed as an add-on, shared + * by all compartments for that add-on (typically, all modules); + * - the compartment's own group. + * + * Pre-condition: the VM must have entered the JS compartment. + * + * The caller is expected to cache the results of this method, as + * calling it more than once may not return the same instances of + * performance groups. + */ + bool GetPerformanceGroups(JSContext* cx, js::PerformanceGroupVector&); + static bool GetPerformanceGroupsCallback(JSContext* cx, js::PerformanceGroupVector&, void* closure); + + + + /********************************************************** + * + * Sets of all performance groups, indexed by several keys. + * + * These sets do not keep the performance groups alive. Rather, a + * performance group is inserted in the relevant sets upon + * construction and removed from the sets upon destruction or when + * we Dispose() of the service. + * + * A `nsPerformanceGroup` is typically kept alive (as a + * `js::PerformanceGroup`) by the JSCompartment to which it is + * associated. It may also temporarily be kept alive by the JS + * stack, in particular in case of nested event loops. + */ + + /** + * Set of performance groups associated to add-ons, indexed + * by add-on id. Each item is shared by all the compartments + * that belong to the add-on. + */ + struct AddonIdToGroup: public nsStringHashKey, + public nsGroupHolder { + explicit AddonIdToGroup(const nsAString* key) + : nsStringHashKey(key) + { } + }; + nsTHashtable<AddonIdToGroup> mAddonIdToGroup; + + /** + * Set of performance groups associated to windows, indexed by outer + * window id. Each item is shared by all the compartments that + * belong to the window. + */ + struct WindowIdToGroup: public nsUint64HashKey, + public nsGroupHolder { + explicit WindowIdToGroup(const uint64_t* key) + : nsUint64HashKey(key) + {} + }; + nsTHashtable<WindowIdToGroup> mWindowIdToGroup; + + /** + * Set of all performance groups. + */ + struct Groups: public nsPtrHashKey<nsPerformanceGroup> { + explicit Groups(const nsPerformanceGroup* key) + : nsPtrHashKey<nsPerformanceGroup>(key) + {} + }; + nsTHashtable<Groups> mGroups; + + /** + * The performance group representing the runtime itself. All + * compartments are associated to this group. + */ + RefPtr<nsPerformanceGroup> mTopGroup; + + /********************************************************** + * + * Measuring and recording the CPU use of the system. + * + */ + + /** + * Get the OS-reported time spent in userland/systemland, in + * microseconds. On most platforms, this data is per-thread, + * but on some platforms we need to fall back to per-process. + * + * Data is not guaranteed to be monotonic. + */ + nsresult GetResources(uint64_t* userTime, uint64_t* systemTime) const; + + /** + * Amount of user/system CPU time used by the thread (or process, + * for platforms that don't support per-thread measure) since start. + * Updated by `StopwatchStart` at most once per event. + * + * Unit: microseconds. + */ + uint64_t mUserTimeStart; + uint64_t mSystemTimeStart; + + bool mIsHandlingUserInput; + + /** + * The number of user inputs since the start of the process. Used to + * determine whether the current iteration has triggered a + * (JS-implemented) user input. + */ + uint64_t mUserInputCount; + + /********************************************************** + * + * Callbacks triggered by the JS VM when execution of JavaScript + * code starts/completes. + * + * As measures of user CPU time/system CPU time have low resolution + * (and are somewhat slow), we measure both only during the calls to + * `StopwatchStart`/`StopwatchCommit` and we make the assumption + * that each group's user/system CPU time is proportional to the + * number of clock cycles spent executing code in the group between + * `StopwatchStart`/`StopwatchCommit`. + * + * The results may be skewed by the thread being rescheduled to a + * different CPU during the measure, but we expect that on average, + * the skew will have limited effects, and will generally tend to + * make already-slow executions appear slower. + */ + + /** + * Execution of JavaScript code has started. This may happen several + * times in succession if the JavaScript code contains nested event + * loops, in which case only the innermost call will receive + * `StopwatchCommitCallback`. + * + * @param iteration The number of times we have started executing + * JavaScript code. + */ + static bool StopwatchStartCallback(uint64_t iteration, void* closure); + bool StopwatchStart(uint64_t iteration); + + /** + * Execution of JavaScript code has reached completion (including + * enqueued microtasks). In cse of tested event loops, any ongoing + * measurement on outer loops is silently cancelled without any call + * to this method. + * + * @param iteration The number of times we have started executing + * JavaScript code. + * @param recentGroups The groups that have seen activity during this + * event. + */ + static bool StopwatchCommitCallback(uint64_t iteration, + js::PerformanceGroupVector& recentGroups, + void* closure); + bool StopwatchCommit(uint64_t iteration, js::PerformanceGroupVector& recentGroups); + + /** + * The number of times we have started executing JavaScript code. + */ + uint64_t mIteration; + + /** + * Commit performance measures of a single group. + * + * Data is transfered from `group->recent*` to `group->data`. + * + * + * @param iteration The current iteration. + * @param userTime The total user CPU time for this thread (or + * process, if per-thread data is not available) between the + * calls to `StopwatchStart` and `StopwatchCommit`. + * @param systemTime The total system CPU time for this thread (or + * process, if per-thread data is not available) between the + * calls to `StopwatchStart` and `StopwatchCommit`. + * @param cycles The total number of cycles for this thread + * between the calls to `StopwatchStart` and `StopwatchCommit`. + * @param isJankVisible If `true`, expect that the user will notice + * any slowdown. + * @param group The group containing the data to commit. + */ + void CommitGroup(uint64_t iteration, + uint64_t userTime, uint64_t systemTime, uint64_t cycles, + bool isJankVisible, + nsPerformanceGroup* group); + + + + + /********************************************************** + * + * To check whether our algorithm makes sense, we keep count of the + * number of times the process has been rescheduled to another CPU + * while we were monitoring the performance of a group and we upload + * this data through Telemetry. + */ + nsresult UpdateTelemetry(); + + uint64_t mProcessStayed; + uint64_t mProcessMoved; + uint32_t mProcessUpdateCounter; + + /********************************************************** + * + * Options controlling measurements. + */ + + /** + * Determine if we are measuring the performance of every individual + * compartment (in particular, every individual module, frame, + * sandbox). Note that this makes measurements noticeably slower. + */ + bool mIsMonitoringPerCompartment; + + + /********************************************************** + * + * Determining whether jank is user-visible. + */ + + /** + * `true` if we believe that any slowdown can cause a noticeable + * delay in handling user-input. + * + * In the current implementation, we return `true` if the latest + * user input was less than MAX_DURATION_OF_INTERACTION_MS ago. This + * includes all inputs (mouse, keyboard, other devices), with the + * exception of mousemove. + */ + bool IsHandlingUserInput(); + + +public: + /********************************************************** + * + * Letting observers register themselves to watch for performance + * alerts. + * + * To avoid saturating clients with alerts (or even creating loops + * of alerts), each alert is buffered. At the end of each iteration + * of the event loop, groups that have caused performance alerts + * are registered in a set of pending alerts, and the collection + * timer hasn't been started yet, it is started. Once the timer + * firers, we gather all the pending alerts, empty the set and + * dispatch to observers. + */ + + /** + * Clear the set of pending alerts and dispatch the pending alerts + * to observers. + */ + void NotifyJankObservers(const mozilla::Vector<uint64_t>& previousJankLevels); + +private: + /** + * The set of groups for which we know that an alert should be + * raised. This set is cleared once `mPendingAlertsCollector` + * fires. + * + * Invariant: no group may appear twice in this vector. + */ + GroupVector mPendingAlerts; + + /** + * A timer callback in charge of collecting the groups in + * `mPendingAlerts` and triggering `NotifyJankObservers` to dispatch + * performance alerts. + */ + RefPtr<class PendingAlertsCollector> mPendingAlertsCollector; + + + /** + * Observation targets that are not attached to a specific group. + */ + struct UniversalTargets { + UniversalTargets(); + /** + * A target for observers interested in watching all addons. + */ + RefPtr<nsPerformanceObservationTarget> mAddons; + + /** + * A target for observers interested in watching all windows. + */ + RefPtr<nsPerformanceObservationTarget> mWindows; + }; + UniversalTargets mUniversalTargets; + + /** + * The threshold, in microseconds, above which a performance group is + * considered "slow" and should raise performance alerts. + */ + uint64_t mJankAlertThreshold; + + /** + * A buffering delay, in milliseconds, used by the service to + * regroup performance alerts, before observers are actually + * noticed. Higher delays let the system avoid redundant + * notifications for the same group, and are generally better for + * performance. + */ + uint32_t mJankAlertBufferingDelay; + + /** + * The threshold above which jank, as reported by the refresh drivers, + * is considered user-visible. + * + * A value of n means that any jank above 2^n ms will be considered + * user visible. + */ + short mJankLevelVisibilityThreshold; + + /** + * The number of microseconds during which we assume that a + * user-interaction can keep the code jank-critical. Any user + * interaction that lasts longer than this duration is expected to + * either have already caused jank or have caused a nested event + * loop. + * + * In either case, we consider that monitoring + * jank-during-interaction after this duration is useless. + */ + uint64_t mMaxExpectedDurationOfInteractionUS; +}; + + + +/** + * Container for performance data. + * + * All values are monotonic. + * + * All values are updated after running to completion. + */ +struct PerformanceData { + /** + * Number of times we have spent at least 2^n consecutive + * milliseconds executing code in this group. + * durations[0] is increased whenever we spend at least 1 ms + * executing code in this group + * durations[1] whenever we spend 2ms+ + * ... + * durations[i] whenever we spend 2^ims+ + */ + uint64_t mDurations[10]; + + /** + * Total amount of time spent executing code in this group, in + * microseconds. + */ + uint64_t mTotalUserTime; + uint64_t mTotalSystemTime; + uint64_t mTotalCPOWTime; + + /** + * Total number of times code execution entered this group, since + * process launch. This may be greater than the number of times we + * have entered the event loop. + */ + uint64_t mTicks; + + PerformanceData(); + PerformanceData(const PerformanceData& from) = default; + PerformanceData& operator=(const PerformanceData& from) = default; +}; + + + +/** + * Identification information for an item that can hold performance + * data. + */ +class nsPerformanceGroupDetails final: public nsIPerformanceGroupDetails { +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPERFORMANCEGROUPDETAILS + + nsPerformanceGroupDetails(const nsAString& aName, + const nsAString& aGroupId, + const nsAString& aAddonId, + const uint64_t aWindowId, + const uint64_t aProcessId, + const bool aIsSystem) + : mName(aName) + , mGroupId(aGroupId) + , mAddonId(aAddonId) + , mWindowId(aWindowId) + , mProcessId(aProcessId) + , mIsSystem(aIsSystem) + { } +public: + const nsAString& Name() const; + const nsAString& GroupId() const; + const nsAString& AddonId() const; + uint64_t WindowId() const; + uint64_t ProcessId() const; + bool IsAddon() const; + bool IsWindow() const; + bool IsSystem() const; + bool IsContentProcess() const; +private: + ~nsPerformanceGroupDetails() {} + + const nsString mName; + const nsString mGroupId; + const nsString mAddonId; + const uint64_t mWindowId; + const uint64_t mProcessId; + const bool mIsSystem; +}; + +/** + * The kind of compartments represented by this group. + */ +enum class PerformanceGroupScope { + /** + * This group represents the entire runtime (i.e. the thread). + */ + RUNTIME, + + /** + * This group represents all the compartments executed in a window. + */ + WINDOW, + + /** + * This group represents all the compartments provided by an addon. + */ + ADDON, + + /** + * This group represents a single compartment. + */ + COMPARTMENT, +}; + +/** + * A concrete implementation of `js::PerformanceGroup`, also holding + * performance data. Instances may represent individual compartments, + * windows, addons or the entire runtime. + * + * This class is intended to be the sole implementation of + * `js::PerformanceGroup`. + */ +class nsPerformanceGroup final: public js::PerformanceGroup { +public: + + // Ideally, we would define the enum class in nsPerformanceGroup, + // but this seems to choke some versions of gcc. + typedef PerformanceGroupScope GroupScope; + + /** + * Construct a performance group. + * + * @param cx The container context. Used to generate a unique identifier. + * @param service The performance service. Used during destruction to + * cleanup the hash tables. + * @param name A name for the group, designed mostly for debugging purposes, + * so it should be at least somewhat human-readable. + * @param addonId The identifier of the add-on. Should be "" when the + * group is not part of an add-on, + * @param windowId The identifier of the window. Should be 0 when the + * group is not part of a window. + * @param processId A unique identifier for the process. + * @param isSystem `true` if the code of the group is executed with + * system credentials, `false` otherwise. + * @param scope the scope of this group. + */ + static nsPerformanceGroup* + Make(JSContext* cx, + nsPerformanceStatsService* service, + const nsAString& name, + const nsAString& addonId, + uint64_t windowId, + uint64_t processId, + bool isSystem, + GroupScope scope); + + /** + * Utility: type-safer conversion from js::PerformanceGroup to nsPerformanceGroup. + */ + static inline nsPerformanceGroup* Get(js::PerformanceGroup* self) { + return static_cast<nsPerformanceGroup*>(self); + } + static inline const nsPerformanceGroup* Get(const js::PerformanceGroup* self) { + return static_cast<const nsPerformanceGroup*>(self); + } + + /** + * The performance data committed to this group. + */ + PerformanceData data; + + /** + * The scope of this group. Used to determine whether the group + * should be (de)activated. + */ + GroupScope Scope() const; + + /** + * Identification details for this group. + */ + nsPerformanceGroupDetails* Details() const; + + /** + * Cleanup any references. + */ + void Dispose(); + + /** + * Set the observation target for this group. + * + * This method must be called exactly once, when the performance + * group is attached to its `nsGroupHolder`. + */ + void SetObservationTarget(nsPerformanceObservationTarget*); + + + /** + * `true` if we have already noticed that a performance alert should + * be raised for this group but we have not dispatched it yet, + * `false` otherwise. + */ + bool HasPendingAlert() const; + void SetHasPendingAlert(bool value); + +protected: + nsPerformanceGroup(nsPerformanceStatsService* service, + const nsAString& name, + const nsAString& groupId, + const nsAString& addonId, + uint64_t windowId, + uint64_t processId, + bool isSystem, + GroupScope scope); + + + /** + * Virtual implementation of `delete`, to make sure that objects are + * destoyed with an implementation of `delete` compatible with the + * implementation of `new` used to allocate them. + * + * Called by SpiderMonkey. + */ + virtual void Delete() override { + delete this; + } + ~nsPerformanceGroup(); + +private: + /** + * Identification details for this group. + */ + RefPtr<nsPerformanceGroupDetails> mDetails; + + /** + * The stats service. Used to perform cleanup during destruction. + */ + RefPtr<nsPerformanceStatsService> mService; + + /** + * The scope of this group. Used to determine whether the group + * should be (de)activated. + */ + const GroupScope mScope; + +// Observing performance alerts. + +public: + /** + * The observation target, used to register observers. + */ + nsPerformanceObservationTarget* ObservationTarget() const; + + /** + * Record a jank duration. + * + * Update the highest recent jank if necessary. + */ + void RecordJank(uint64_t jank); + uint64_t HighestRecentJank(); + + /** + * Record a CPOW duration. + * + * Update the highest recent CPOW if necessary. + */ + void RecordCPOW(uint64_t cpow); + uint64_t HighestRecentCPOW(); + + /** + * Record that this group has recently been involved in handling + * user input. Note that heuristics are involved here, so the + * result is not 100% accurate. + */ + void RecordUserInput(); + bool HasRecentUserInput(); + + /** + * Reset recent values (recent highest CPOW and jank, involvement in + * user input). + */ + void ResetRecent(); +private: + /** + * The target used by observers to register for watching slow + * performance alerts caused by this group. + * + * May be nullptr for groups that cannot be watched (the top group). + */ + RefPtr<class nsPerformanceObservationTarget> mObservationTarget; + + /** + * The highest jank encountered since jank observers for this group + * were last called, in microseconds. + */ + uint64_t mHighestJank; + + /** + * The highest CPOW encountered since jank observers for this group + * were last called, in microseconds. + */ + uint64_t mHighestCPOW; + + /** + * `true` if this group has been involved in handling user input, + * `false` otherwise. + * + * Note that we use heuristics to determine whether a group is + * involved in handling user input, so this value is not 100% + * accurate. + */ + bool mHasRecentUserInput; + + /** + * `true` if this group has caused a performance alert and this alert + * hasn't been dispatched yet. + * + * We use this as part of the buffering of performance alerts. If + * the group generates several alerts several times during the + * buffering delay, we only wish to add the group once to the list + * of alerts. + */ + bool mHasPendingAlert; +}; + +#endif diff --git a/toolkit/components/perfmonitoring/tests/browser/.eslintrc.js b/toolkit/components/perfmonitoring/tests/browser/.eslintrc.js new file mode 100644 index 000000000..7c8021192 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/browser.eslintrc.js" + ] +}; diff --git a/toolkit/components/perfmonitoring/tests/browser/browser.ini b/toolkit/components/perfmonitoring/tests/browser/browser.ini new file mode 100644 index 000000000..7f4ac8514 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser.ini @@ -0,0 +1,15 @@ +[DEFAULT] +head = head.js +tags = addons +support-files = + browser_Addons_sample.xpi + browser_compartments.html + browser_compartments_frame.html + browser_compartments_script.js + +[browser_AddonWatcher.js] +[browser_compartments.js] +skip-if = os == "linux" && !debug && e10s # Bug 1230018 +[browser_addonPerformanceAlerts.js] +[browser_addonPerformanceAlerts_2.js] +[browser_webpagePerformanceAlerts.js] diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js b/toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js new file mode 100644 index 000000000..b4e80faa7 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests for AddonWatcher.jsm + +"use strict"; + +requestLongerTimeout(2); + +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/AddonManager.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); + +const ADDON_URL = "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi"; +const ADDON_ID = "addonwatcher-test@mozilla.com"; + +add_task(function* init() { + info("Installing test add-on"); + let installer = yield new Promise(resolve => AddonManager.getInstallForURL(ADDON_URL, resolve, "application/x-xpinstall")); + if (installer.error) { + throw installer.error; + } + let installed = new Promise((resolve, reject) => installer.addListener({ + onInstallEnded: (_, addon) => resolve(addon), + onInstallFailed: reject, + onDownloadFailed: reject + })); + + // We also need to wait for the add-on to report that it's ready + // to be used in the test. + let ready = TestUtils.topicObserved("test-addonwatcher-ready"); + installer.install(); + + info("Waiting for installation to terminate"); + let addon = yield installed; + + yield ready; + + registerCleanupFunction(() => { + info("Uninstalling test add-on"); + addon.uninstall() + }); + + Preferences.set("browser.addon-watch.warmup-ms", 0); + Preferences.set("browser.addon-watch.freeze-threshold-micros", 0); + Preferences.set("browser.addon-watch.jank-threshold-micros", 0); + Preferences.set("browser.addon-watch.occurrences-between-alerts", 0); + Preferences.set("browser.addon-watch.delay-between-alerts-ms", 0); + Preferences.set("browser.addon-watch.delay-between-freeze-alerts-ms", 0); + Preferences.set("browser.addon-watch.max-simultaneous-reports", 10000); + Preferences.set("browser.addon-watch.deactivate-after-idle-ms", 100000000); + registerCleanupFunction(() => { + for (let k of [ + "browser.addon-watch.warmup-ms", + "browser.addon-watch.freeze-threshold-micros", + "browser.addon-watch.jank-threshold-micros", + "browser.addon-watch.occurrences-between-alerts", + "browser.addon-watch.delay-between-alerts-ms", + "browser.addon-watch.delay-between-freeze-alerts-ms", + "browser.addon-watch.max-simultaneous-reports", + "browser.addon-watch.deactivate-after-idle-ms" + ]) { + Preferences.reset(k); + } + }); + + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + AddonWatcher.init(); + + registerCleanupFunction(function () { + AddonWatcher.paused = true; + Services.telemetry.canRecordExtended = oldCanRecord; + }); +}); + +// Utility function to burn some resource, trigger a reaction of the add-on watcher +// and check both its notification and telemetry. +let burn_rubber = Task.async(function*({histogramName, topic, expectedMinSum}) { + let detected = false; + let observer = (_, topic, id) => { + Assert.equal(id, ADDON_ID, "The add-on watcher has detected the misbehaving addon"); + detected = true; + }; + + try { + info("Preparing add-on watcher"); + + Services.obs.addObserver(observer, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED, false); + + let histogram = Services.telemetry.getKeyedHistogramById(histogramName); + histogram.clear(); + let snap1 = histogram.snapshot(ADDON_ID); + Assert.equal(snap1.sum, 0, `Histogram ${histogramName} is initially empty for the add-on`); + + let histogramUpdated = false; + do { + info(`Burning some CPU with ${topic}. This should cause an add-on watcher notification`); + yield new Promise(resolve => setTimeout(resolve, 100)); + Services.obs.notifyObservers(null, topic, ""); + yield new Promise(resolve => setTimeout(resolve, 100)); + + let snap2 = histogram.snapshot(ADDON_ID); + histogramUpdated = snap2.sum > 0; + info(`For the moment, histogram ${histogramName} shows ${snap2.sum} => ${histogramUpdated}`); + info(`For the moment, we have ${detected?"":"NOT "}detected the slow add-on`); + } while (!histogramUpdated || !detected); + + let snap3 = histogram.snapshot(ADDON_ID); + Assert.ok(snap3.sum >= expectedMinSum, `Histogram ${histogramName} recorded a gravity of ${snap3.sum}, expecting at least ${expectedMinSum}.`); + } finally { + Services.obs.removeObserver(observer, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED); + } +}); + +// Test that burning CPU will cause the add-on watcher to notice that +// the add-on is misbehaving. +add_task(function* test_burn_CPU() { + yield burn_rubber({ + histogramName: "PERF_MONITORING_SLOW_ADDON_JANK_US", + topic: "test-addonwatcher-burn-some-cpu", + expectedMinSum: 7, + }); +}); + +// Test that burning content CPU will cause the add-on watcher to notice that +// the add-on is misbehaving. +/* +Blocked by bug 1227283. +add_task(function* test_burn_content_CPU() { + yield burn_rubber({ + histogramName: "PERF_MONITORING_SLOW_ADDON_JANK_US", + topic: "test-addonwatcher-burn-some-content-cpu", + expectedMinSum: 7, + }); +}); +*/ + +// Test that burning CPOW will cause the add-on watcher to notice that +// the add-on is misbehaving. +add_task(function* test_burn_CPOW() { + if (!gMultiProcessBrowser) { + info("This is a single-process Firefox, we can't test for CPOW"); + return; + } + yield burn_rubber({ + histogramName: "PERF_MONITORING_SLOW_ADDON_CPOW_US", + topic: "test-addonwatcher-burn-some-cpow", + expectedMinSum: 400, + }); +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi Binary files differnew file mode 100644 index 000000000..ae5bcc5ff --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js new file mode 100644 index 000000000..9a5575827 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sample for browser_AddonWatcher.js + +"use strict"; + +const {utils: Cu, classes: Cc, interfaces: Ci} = Components; + +const TOPIC_BURNCPU = "test-addonwatcher-burn-some-cpu"; +const TOPIC_BURNCPOW = "test-addonwatcher-burn-some-cpow"; +const TOPIC_BURNCONTENTCPU = "test-addonwatcher-burn-some-content-cpu"; +const TOPIC_READY = "test-addonwatcher-ready"; + +const MESSAGE_BURNCPOW = "test-addonwatcher-cpow:init"; +const URL_FRAMESCRIPT = "chrome://addonwatcher-test/content/framescript.js"; + +Cu.import("resource://gre/modules/Services.jsm", this); +const { setTimeout } = Cu.import("resource://gre/modules/Timer.jsm", {}); +Cu.import("resource://gre/modules/Task.jsm", this); + +let globalMM = Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); + +/** + * Spend some time using CPU. + */ +function burnCPU() { + let ignored = []; + let start = Date.now(); + let i = 0; + while (Date.now() - start < 1000) { + ignored[i++ % 2] = i; + } +} + +/** + * Spend some time in CPOW. + */ +function burnCPOW() { + gBurnCPOW(); +} +let gBurnCPOW = null; + +function burnContentCPU() { + setTimeout(() => { try { + gBurnContentCPU() + } catch (ex) { + dump(`test-addon error: ${ex}\n`); + } }, 0); +} +let gBurnContentCPU = null; + +let gTab = null; +let gTabBrowser = null; + +function startup() { + Services.obs.addObserver(burnCPU, TOPIC_BURNCPU, false); + Services.obs.addObserver(burnCPOW, TOPIC_BURNCPOW, false); + Services.obs.addObserver(burnContentCPU, TOPIC_BURNCONTENTCPU, false); + + let windows = Services.wm.getEnumerator("navigator:browser"); + let win = windows.getNext(); + gTabBrowser = win.gBrowser; + gTab = gTabBrowser.addTab("about:robots"); + gBurnContentCPU = function() { + gTab.linkedBrowser.messageManager.sendAsyncMessage("test-addonwatcher-burn-some-content-cpu", {}); + } + + gTab.linkedBrowser.messageManager.loadFrameScript(URL_FRAMESCRIPT, false); + globalMM.loadFrameScript(URL_FRAMESCRIPT, false); + + if (Services.appinfo.browserTabsRemoteAutostart) { + // This profile has e10s enabled, which means we'll want to + // test CPOW traffic. + globalMM.addMessageListener("test-addonwatcher-cpow:init", function waitForCPOW(msg) { + if (Components.utils.isCrossProcessWrapper(msg.objects.burnCPOW)) { + gBurnCPOW = msg.objects.burnCPOW; + globalMM.removeMessageListener("test-addonwatcher-cpow:init", waitForCPOW); + Services.obs.notifyObservers(null, TOPIC_READY, null); + } else { + Cu.reportError("test-addonwatcher-cpow:init didn't give us a CPOW! Expect timeouts."); + } + }); + } else { + // e10s is not enabled, so a CPOW is not necessary - we can report ready + // right away. + Services.obs.notifyObservers(null, TOPIC_READY, null); + } +} + +function shutdown() { + Services.obs.removeObserver(burnCPU, TOPIC_BURNCPU); + Services.obs.removeObserver(burnCPOW, TOPIC_BURNCPOW); + Services.obs.removeObserver(burnContentCPU, TOPIC_BURNCONTENTCPU); + gTabBrowser.removeTab(gTab); +} + +function install() { + // Nothing to do +} + +function uninstall() { + // Nothing to do +} diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh new file mode 100644 index 000000000..28d52ea3a --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh @@ -0,0 +1,4 @@ +echo "Rebuilding browser_Addons_sample.xpi..." +zip -r ../browser_Addons_sample.xpi . +echo " +Done! Don't forget to sign it: https://wiki.mozilla.org/EngineeringProductivity/HowTo/SignExtensions"
\ No newline at end of file diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest new file mode 100644 index 000000000..9f53da861 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest @@ -0,0 +1 @@ +content addonwatcher-test content/ diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js new file mode 100644 index 000000000..e7ebc2a61 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js @@ -0,0 +1,23 @@ +/* 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"; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function burnCPOW(msg) { + dump(`Addon: content burnCPU start ${Math.sin(Math.random())}\n`); + let start = content.performance.now(); + let ignored = []; + while (content.performance.now() - start < 5000) { + ignored[ignored.length % 2] = ignored.length; + } + dump(`Addon: content burnCPU done: ${content.performance.now() - start}\n`); +} + +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + sendAsyncMessage("test-addonwatcher-cpow:init", {}, {burnCPOW}); +} + +addMessageListener("test-addonwatcher-burn-some-content-cpu", burnCPOW); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf new file mode 100644 index 000000000..cae10ace6 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>addonwatcher-test@mozilla.com</em:id> + <em:version>1.1</em:version> + + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>0.3</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <em:targetApplication> + <Description> + <em:id>toolkit@mozilla.org</em:id> + <em:minVersion>1</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <em:bootstrap>true</em:bootstrap> + + <em:name>Sample for browser_AddonWatcher.js</em:name> + + </Description> +</RDF> diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js new file mode 100644 index 000000000..af78f0074 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js @@ -0,0 +1,91 @@ +"use strict"; + +/** + * Tests for PerformanceWatcher watching slow addons. + */ + +add_task(function* init() { + // Get rid of buffering. + let service = Cc["@mozilla.org/toolkit/performance-stats-service;1"].getService( + Ci.nsIPerformanceStatsService); + let oldDelay = service.jankAlertBufferingDelay; + + service.jankAlertBufferingDelay = 0 /* ms */; + registerCleanupFunction(() => { + info("Cleanup"); + service.jankAlertBufferingDelay = oldDelay; + }); +}); + +add_task(function* test_install_addon_then_watch_it() { + for (let topic of ["burnCPU", "promiseBurnContentCPU", "promiseBurnCPOW"]) { + info(`Starting subtest ${topic}`); + info("Spawning fake add-on, making sure that the compartment is initialized"); + let addon = new AddonBurner(); + yield addon.promiseInitialized; + addon.burnCPU(); + + info(`Check that burning CPU triggers the real listener, but not the fake listener ${topic}`); + let realListener = new AddonListener(addon.addonId, (group, details) => { + if (group.addonId == addon.addonId) { + return details.highestJank; + } + throw new Error(`I shouldn't have been called with addon ${group.addonId}`); + }); + let fakeListener = new AddonListener(addon.addonId + "-fake-" + Math.random(), group => true); // This listener should never be triggered. + let universalListener = new AddonListener("*", alerts => { + info(`AddonListener: received alerts ${JSON.stringify(alerts)}`); + let alert = alerts.find(({source}) => { + return source.addonId == addon.addonId; + }); + if (alert) { + info(`AddonListener: I found an alert for ${addon.addonId}`); + return alert.details.highestJank; + } + info(`AddonListener: I didn't find any alert for ${addon.addonId}`); + return null; + }); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield addon.run(topic, 10, realListener); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + + Assert.ok(realListener.triggered, `1. The real listener was triggered ${topic}`); + Assert.ok(universalListener.triggered, `1. The universal listener was triggered ${topic}`); + Assert.ok(!fakeListener.triggered, `1. The fake listener was not triggered ${topic}`); + Assert.ok(realListener.result >= addon.jankThreshold, `1. jank is at least ${addon.jankThreshold/1000}ms (${realListener.result/1000}ms) ${topic}`); + + info(`Attempting to remove a performance listener incorrectly, check that this does not hurt our real listener ${topic}`); + Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId}, () => {})); + Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId + "-unbound-id-" + Math.random()}, realListener.listener)); + + yield addon.run(topic, 10, realListener); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 300)); + + Assert.ok(realListener.triggered, `2. The real listener was triggered ${topic}`); + Assert.ok(universalListener.triggered, `2. The universal listener was triggered ${topic}`); + Assert.ok(!fakeListener.triggered, `2. The fake listener was not triggered ${topic}`); + Assert.ok(realListener.result >= 200000, `2. jank is at least 300ms (${realListener.result/1000}ms) ${topic}`); + + info(`Attempting to remove correctly, check if the listener is still triggered ${topic}`); + realListener.unregister(); + yield addon.run(topic, 3, realListener); + Assert.ok(!realListener.triggered, `3. After being unregistered, the real listener was not triggered ${topic}`); + Assert.ok(universalListener.triggered, `3. The universal listener is still triggered ${topic}`); + + info("Unregistering universal listener"); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + universalListener.unregister(); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield addon.run(topic, 3, realListener); + Assert.ok(!universalListener.triggered, `4. After being unregistered, the universal listener is not triggered ${topic}`); + + fakeListener.unregister(); + addon.dispose(); + } +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js new file mode 100644 index 000000000..d39c38b1f --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js @@ -0,0 +1,25 @@ +"use strict"; + +/** + * Tests for PerformanceWatcher watching slow addons. + */ + +add_task(function* test_watch_addon_then_install_it() { + for (let topic of ["burnCPU", "promiseBurnContentCPU", "promiseBurnCPOW"]) { + let addonId = "addon:test_watch_addons_before_installing" + Math.random(); + let realListener = new AddonListener(addonId, (group, details) => { + if (group.addonId == addonId) { + return details.highestJank; + } + throw new Error(`I shouldn't have been called with addon ${group.addonId}`); + }); + + info("Now install the add-on, *after* having installed the listener"); + let addon = new AddonBurner(addonId); + + Assert.ok((yield addon.run(topic, 10, realListener)), `5. The real listener was triggered ${topic}`); + Assert.ok(realListener.result >= addon.jankThreshold, `5. jank is at least ${addon.jankThreshold/1000}ms (${realListener.result}µs) ${topic}`); + realListener.unregister(); + addon.dispose(); + } +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html new file mode 100644 index 000000000..d7ee6c418 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> + <title> + Main frame for test browser_compartments.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/perfmonitoring/tests/browser/browser_compartments.js b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js new file mode 100644 index 000000000..f04fefb33 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js @@ -0,0 +1,312 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we see jank that takes place in a webpage, + * and that jank from several iframes are actually charged + * to the top window. + */ +Cu.import("resource://gre/modules/PerformanceStats.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://testing-common/ContentTask.jsm", this); + + +const URL = "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html?test=" + Math.random(); +const PARENT_TITLE = `Main frame for test browser_compartments.js ${Math.random()}`; +const FRAME_TITLE = `Subframe for test browser_compartments.js ${Math.random()}`; + +const PARENT_PID = Services.appinfo.processID; + +// This function is injected as source as a frameScript +function frameScript() { + try { + "use strict"; + + const { utils: Cu, classes: Cc, interfaces: Ci } = Components; + Cu.import("resource://gre/modules/PerformanceStats.jsm"); + Cu.import("resource://gre/modules/Services.jsm"); + + // Make sure that the stopwatch is now active. + let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks", "compartments"]); + + addMessageListener("compartments-test:getStatistics", () => { + try { + monitor.promiseSnapshot().then(snapshot => { + sendAsyncMessage("compartments-test:getStatistics", {snapshot, pid: Services.appinfo.processID}); + }); + } catch (ex) { + Cu.reportError("Error in content (getStatistics): " + ex); + Cu.reportError(ex.stack); + } + }); + + addMessageListener("compartments-test:setTitles", titles => { + try { + content.document.title = titles.data.parent; + for (let i = 0; i < content.frames.length; ++i) { + content.frames[i].postMessage({title: titles.data.frames}, "*"); + } + console.log("content", "Done setting titles", content.document.title); + sendAsyncMessage("compartments-test:setTitles"); + } catch (ex) { + Cu.reportError("Error in content (setTitles): " + ex); + Cu.reportError(ex.stack); + } + }); + } catch (ex) { + Cu.reportError("Error in content (setup): " + ex); + Cu.reportError(ex.stack); + } +} + +// A variant of `Assert` that doesn't spam the logs +// in case of success. +var SilentAssert = { + equal: function(a, b, msg) { + if (a == b) { + return; + } + Assert.equal(a, b, msg); + }, + notEqual: function(a, b, msg) { + if (a != b) { + return; + } + Assert.notEqual(a, b, msg); + }, + ok: function(a, msg) { + if (a) { + return; + } + Assert.ok(a, msg); + }, + leq: function(a, b, msg) { + this.ok(a <= b, `${msg}: ${a} <= ${b}`); + } +}; + +var isShuttingDown = false; +function monotinicity_tester(source, testName) { + // In the background, check invariants: + // - numeric data can only ever increase; + // - the name, addonId, isSystem of a component never changes; + // - the name, addonId, isSystem of the process data; + // - there is at most one component with a combination of `name` and `addonId`; + // - types, etc. + let previous = { + processData: null, + componentsMap: new Map(), + }; + + let sanityCheck = function(prev, next) { + if (prev == null) { + return; + } + for (let k of ["groupId", "addonId", "isSystem"]) { + SilentAssert.equal(prev[k], next[k], `Sanity check (${testName}): ${k} hasn't changed (${prev.name}).`); + } + for (let [probe, k] of [ + ["jank", "totalUserTime"], + ["jank", "totalSystemTime"], + ["cpow", "totalCPOWTime"], + ["ticks", "ticks"] + ]) { + SilentAssert.equal(typeof next[probe][k], "number", `Sanity check (${testName}): ${k} is a number.`); + SilentAssert.leq(prev[probe][k], next[probe][k], `Sanity check (${testName}): ${k} is monotonic.`); + SilentAssert.leq(0, next[probe][k], `Sanity check (${testName}): ${k} is >= 0.`) + } + SilentAssert.equal(prev.jank.durations.length, next.jank.durations.length); + for (let i = 0; i < next.jank.durations.length; ++i) { + SilentAssert.ok(typeof next.jank.durations[i] == "number" && next.jank.durations[i] >= 0, + `Sanity check (${testName}): durations[${i}] is a non-negative number.`); + SilentAssert.leq(prev.jank.durations[i], next.jank.durations[i], + `Sanity check (${testName}): durations[${i}] is monotonic.`); + } + for (let i = 0; i < next.jank.durations.length - 1; ++i) { + SilentAssert.leq(next.jank.durations[i + 1], next.jank.durations[i], + `Sanity check (${testName}): durations[${i}] >= durations[${i + 1}].`) + } + }; + let iteration = 0; + let frameCheck = Task.async(function*() { + if (isShuttingDown) { + window.clearInterval(interval); + return; + } + let name = `${testName}: ${iteration++}`; + let result = yield source(); + if (!result) { + // This can happen at the end of the test when we attempt + // to communicate too late with the content process. + window.clearInterval(interval); + return; + } + let {pid, snapshot} = result; + + // Sanity check on the process data. + sanityCheck(previous.processData, snapshot.processData); + SilentAssert.equal(snapshot.processData.isSystem, true); + SilentAssert.equal(snapshot.processData.name, "<process>"); + SilentAssert.equal(snapshot.processData.addonId, ""); + SilentAssert.equal(snapshot.processData.processId, pid); + previous.procesData = snapshot.processData; + + // Sanity check on components data. + let map = new Map(); + for (let item of snapshot.componentsData) { + for (let [probe, k] of [ + ["jank", "totalUserTime"], + ["jank", "totalSystemTime"], + ["cpow", "totalCPOWTime"] + ]) { + // Note that we cannot expect components data to be always smaller + // than process data, as `getrusage` & co are not monotonic. + SilentAssert.leq(item[probe][k], 3 * snapshot.processData[probe][k], + `Sanity check (${name}): ${k} of component is not impossibly larger than that of process`); + } + + let isCorrectPid = (item.processId == pid && !item.isChildProcess) + || (item.processId != pid && item.isChildProcess); + SilentAssert.ok(isCorrectPid, `Pid check (${name}): the item comes from the right process`); + + let key = item.groupId; + if (map.has(key)) { + let old = map.get(key); + Assert.ok(false, `Component ${key} has already been seen. Latest: ${item.addonId||item.name}, previous: ${old.addonId||old.name}`); + } + map.set(key, item); + } + for (let item of snapshot.componentsData) { + if (!item.parentId) { + continue; + } + let parent = map.get(item.parentId); + SilentAssert.ok(parent, `The parent exists ${item.parentId}`); + + for (let [probe, k] of [ + ["jank", "totalUserTime"], + ["jank", "totalSystemTime"], + ["cpow", "totalCPOWTime"] + ]) { + // Note that we cannot expect components data to be always smaller + // than parent data, as `getrusage` & co are not monotonic. + SilentAssert.leq(item[probe][k], 2 * parent[probe][k], + `Sanity check (${testName}): ${k} of component is not impossibly larger than that of parent`); + } + } + for (let [key, item] of map) { + sanityCheck(previous.componentsMap.get(key), item); + previous.componentsMap.set(key, item); + } + }); + let interval = window.setInterval(frameCheck, 300); + registerCleanupFunction(() => { + window.clearInterval(interval); + }); +} + +add_task(function* test() { + let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks"]); + + info("Extracting initial state"); + let stats0 = yield monitor.promiseSnapshot(); + Assert.notEqual(stats0.componentsData.length, 0, "There is more than one component"); + Assert.ok(!stats0.componentsData.find(stat => stat.name.indexOf(URL) != -1), + "The url doesn't appear yet"); + + let newTab = gBrowser.addTab(); + let browser = newTab.linkedBrowser; + // Setup monitoring in the tab + info("Setting up monitoring in the tab"); + yield ContentTask.spawn(newTab.linkedBrowser, null, frameScript); + + info("Opening URL"); + newTab.linkedBrowser.loadURI(URL); + + if (Services.sysinfo.getPropertyAsAString("name") == "Windows_NT") { + info("Deactivating sanity checks under Windows (bug 1151240)"); + } else { + info("Setting up sanity checks"); + monotinicity_tester(() => monitor.promiseSnapshot().then(snapshot => ({snapshot, pid: PARENT_PID})), "parent process"); + monotinicity_tester(() => promiseContentResponseOrNull(browser, "compartments-test:getStatistics", null), "content process" ); + } + + let skipTotalUserTime = hasLowPrecision(); + + + while (true) { + yield new Promise(resolve => setTimeout(resolve, 100)); + + // We may have race conditions with DOM loading. + // Don't waste too much brainpower here, let's just ask + // repeatedly for the title to be changed, until this works. + info("Setting titles"); + yield promiseContentResponse(browser, "compartments-test:setTitles", { + parent: PARENT_TITLE, + frames: FRAME_TITLE + }); + info("Titles set"); + + let {snapshot: stats} = (yield promiseContentResponse(browser, "compartments-test:getStatistics", null)); + + // Attach titles to components. + let titles = []; + let map = new Map(); + let windows = Services.wm.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + let window = windows.getNext(); + let tabbrowser = window.gBrowser; + for (let browser of tabbrowser.browsers) { + let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet + if (id != null) { + map.set(id, browser); + } + } + } + for (let stat of stats.componentsData) { + if (!stat.windowId) { + continue; + } + let browser = map.get(stat.windowId); + if (!browser) { + continue; + } + let title = browser.contentTitle; + if (title) { + stat.title = title; + titles.push(title); + } + } + + // While the webpage consists in three compartments, we should see only + // one `PerformanceData` in `componentsData`. Its `name` is undefined + // (could be either the main frame or one of its subframes), but its + // `title` should be the title of the main frame. + info(`Searching for frame title '${FRAME_TITLE}' in ${JSON.stringify(titles)} (I hope not to find it)`); + Assert.ok(!titles.includes(FRAME_TITLE), "Searching by title, the frames don't show up in the list of components"); + + info(`Searching for window title '${PARENT_TITLE}' in ${JSON.stringify(titles)} (I hope to find it)`); + let parent = stats.componentsData.find(x => x.title == PARENT_TITLE); + if (!parent) { + info("Searching by title, we didn't find the main frame"); + continue; + } + info("Found the main frame"); + + if (skipTotalUserTime) { + info("Not looking for total user time on this platform, we're done"); + break; + } else if (parent.jank.totalUserTime > 1000) { + info("Enough CPU time detected, we're done"); + break; + } else { + info(`Not enough CPU time detected: ${parent.jank.totalUserTime}`); + } + } + isShuttingDown = true; + + // Cleanup + gBrowser.removeTab(newTab, {skipPermitUnload: true}); +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments_frame.html b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_frame.html new file mode 100644 index 000000000..69edfe871 --- /dev/null +++ b/toolkit/components/perfmonitoring/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/perfmonitoring/tests/browser/browser_compartments_script.js b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_script.js new file mode 100644 index 000000000..3d5f7114f --- /dev/null +++ b/toolkit/components/perfmonitoring/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/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js b/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js new file mode 100644 index 000000000..eb908c8db --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js @@ -0,0 +1,111 @@ +"use strict"; + +/** + * Tests for PerformanceWatcher watching slow web pages. + */ + + /** + * Simulate a slow webpage. + */ +function WebpageBurner() { + CPUBurner.call(this, "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html?test=" + Math.random(), 300000); +} +WebpageBurner.prototype = Object.create(CPUBurner.prototype); +WebpageBurner.prototype.promiseBurnContentCPU = function() { + return promiseContentResponse(this._browser, "test-performance-watcher:burn-content-cpu", {}); +}; + +function WebpageListener(windowId, accept) { + info(`Creating WebpageListener for ${windowId}`); + AlertListener.call(this, accept, { + register: () => PerformanceWatcher.addPerformanceListener({windowId}, this.listener), + unregister: () => PerformanceWatcher.removePerformanceListener({windowId}, this.listener) + }); +} +WebpageListener.prototype = Object.create(AlertListener.prototype); + +add_task(function* init() { + // Get rid of buffering. + let service = Cc["@mozilla.org/toolkit/performance-stats-service;1"].getService( + Ci.nsIPerformanceStatsService); + let oldDelay = service.jankAlertBufferingDelay; + + service.jankAlertBufferingDelay = 0 /* ms */; + registerCleanupFunction(() => { + info("Cleanup"); + service.jankAlertBufferingDelay = oldDelay; + }); +}); + +add_task(function* test_open_window_then_watch_it() { + let burner = new WebpageBurner(); + yield burner.promiseInitialized; + yield burner.promiseBurnContentCPU(); + + info(`Check that burning CPU triggers the real listener, but not the fake listener`); + let realListener = new WebpageListener(burner.windowId, (group, details) => { + info(`test: realListener for ${burner.tab.linkedBrowser.outerWindowID}: ${group}, ${details}\n`); + Assert.equal(group.windowId, burner.windowId, "We should not receive data meant for another group"); + return details; + }); // This listener should be triggered. + + info(`Creating fake burner`); + let otherTab = gBrowser.addTab(); + yield BrowserTestUtils.browserLoaded(otherTab.linkedBrowser); + info(`Check that burning CPU triggers the real listener, but not the fake listener`); + let fakeListener = new WebpageListener(otherTab.linkedBrowser.outerWindowID, group => group.windowId == burner.windowId); // This listener should never be triggered. + let universalListener = new WebpageListener(0, alerts => + alerts.find(alert => alert.source.windowId == burner.windowId) + ); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + + yield burner.run("promiseBurnContentCPU", 20, realListener); + Assert.ok(realListener.triggered, `1. The real listener was triggered`); + Assert.ok(universalListener.triggered, `1. The universal listener was triggered`); + Assert.ok(!fakeListener.triggered, `1. The fake listener was not triggered`); + + if (realListener.result) { + Assert.ok(realListener.result.highestJank >= 300, `1. jank is at least 300ms (${realListener.result.highestJank}ms)`); + } + + info(`Attempting to remove a performance listener incorrectly, check that this does not hurt our real listener`); + Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId}, () => {})); + Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId + "-unbound-id-" + Math.random()}, realListener.listener)); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield burner.run("promiseBurnContentCPU", 20, realListener); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + + Assert.ok(realListener.triggered, `2. The real listener was triggered`); + Assert.ok(universalListener.triggered, `2. The universal listener was triggered`); + Assert.ok(!fakeListener.triggered, `2. The fake listener was not triggered`); + if (realListener.result) { + Assert.ok(realListener.result.highestJank >= 300, `2. jank is at least 300ms (${realListener.jank}ms)`); + } + + info(`Attempting to remove correctly, check if the listener is still triggered`); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + realListener.unregister(); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield burner.run("promiseBurnContentCPU", 3, realListener); + Assert.ok(!realListener.triggered, `3. After being unregistered, the real listener was not triggered`); + Assert.ok(universalListener.triggered, `3. The universal listener is still triggered`); + + universalListener.unregister(); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield burner.run("promiseBurnContentCPU", 3, realListener); + Assert.ok(!universalListener.triggered, `4. After being unregistered, the universal listener is not triggered`); + + fakeListener.unregister(); + burner.dispose(); + gBrowser.removeTab(otherTab); +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/head.js b/toolkit/components/perfmonitoring/tests/browser/head.js new file mode 100644 index 000000000..92258fd1b --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/head.js @@ -0,0 +1,287 @@ +/* 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/Promise.jsm", this); +Cu.import("resource://gre/modules/AddonManager.jsm", this); +Cu.import("resource://gre/modules/AddonWatcher.jsm", this); +Cu.import("resource://gre/modules/PerformanceWatcher.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://testing-common/ContentTaskUtils.jsm", this); + +/** + * Base class for simulating slow addons/webpages. + */ +function CPUBurner(url, jankThreshold) { + info(`CPUBurner: Opening tab for ${url}\n`); + this.url = url; + this.tab = gBrowser.addTab(url); + this.jankThreshold = jankThreshold; + let browser = this.tab.linkedBrowser; + this._browser = browser; + ContentTask.spawn(this._browser, null, CPUBurner.frameScript); + this.promiseInitialized = BrowserTestUtils.browserLoaded(browser); +} +CPUBurner.prototype = { + get windowId() { + return this._browser.outerWindowID; + }, + /** + * Burn CPU until it triggers a listener with the specified jank threshold. + */ + run: Task.async(function*(burner, max, listener) { + listener.reset(); + for (let i = 0; i < max; ++i) { + yield new Promise(resolve => setTimeout(resolve, 50)); + try { + yield this[burner](); + } catch (ex) { + return false; + } + if (listener.triggered && listener.result >= this.jankThreshold) { + return true; + } + } + return false; + }), + dispose: function() { + info(`CPUBurner: Closing tab for ${this.url}\n`); + gBrowser.removeTab(this.tab); + } +}; +// This function is injected in all frames +CPUBurner.frameScript = function() { + try { + "use strict"; + + const { utils: Cu, classes: Cc, interfaces: Ci } = Components; + let sandboxes = new Map(); + let getSandbox = function(addonId) { + let sandbox = sandboxes.get(addonId); + if (!sandbox) { + sandbox = Components.utils.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { addonId }); + sandboxes.set(addonId, sandbox); + } + return sandbox; + }; + + let burnCPU = function() { + var start = Date.now(); + var ignored = []; + while (Date.now() - start < 500) { + ignored[ignored.length % 2] = ignored.length; + } + }; + let burnCPUInSandbox = function(addonId) { + let sandbox = getSandbox(addonId); + Cu.evalInSandbox(burnCPU.toSource() + "()", sandbox); + }; + + { + let topic = "test-performance-watcher:burn-content-cpu"; + addMessageListener(topic, function(msg) { + try { + if (msg.data && msg.data.addonId) { + burnCPUInSandbox(msg.data.addonId); + } else { + burnCPU(); + } + sendAsyncMessage(topic, {}); + } catch (ex) { + dump(`This is the content attempting to burn CPU: error ${ex}\n`); + dump(`${ex.stack}\n`); + } + }); + } + + // Bind the function to the global context or it might be GC'd during test + // causing failures (bug 1230027) + this.burnCPOWInSandbox = function(addonId) { + try { + burnCPUInSandbox(addonId); + } catch (ex) { + dump(`This is the addon attempting to burn CPOW: error ${ex}\n`); + dump(`${ex.stack}\n`); + } + } + + sendAsyncMessage("test-performance-watcher:cpow-init", {}, { + burnCPOWInSandbox: this.burnCPOWInSandbox + }); + + } catch (ex) { + Cu.reportError("This is the addon: error " + ex); + Cu.reportError(ex.stack); + } +}; + +/** + * Base class for listening to slow group alerts + */ +function AlertListener(accept, {register, unregister}) { + this.listener = (...args) => { + if (this._unregistered) { + throw new Error("Listener was unregistered"); + } + let result = accept(...args); + if (!result) { + return; + } + this.result = result; + this.triggered = true; + return; + }; + this.triggered = false; + this.result = null; + this._unregistered = false; + this._unregister = unregister; + registerCleanupFunction(() => { + this.unregister(); + }); + register(); +} +AlertListener.prototype = { + unregister: function() { + this.reset(); + if (this._unregistered) { + info(`head.js: No need to unregister, we're already unregistered.\n`); + return; + } + info(`head.js: Unregistering listener.\n`); + this._unregistered = true; + this._unregister(); + info(`head.js: Unregistration complete.\n`); + }, + reset: function() { + this.triggered = false; + this.result = null; + }, +}; + +/** + * Simulate a slow add-on. + */ +function AddonBurner(addonId = "fake add-on id: " + Math.random()) { + this.jankThreshold = 200000; + CPUBurner.call(this, `http://example.com/?uri=${addonId}`, this.jankThreshold); + this._addonId = addonId; + this._sandbox = Components.utils.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { addonId: this._addonId }); + this._CPOWBurner = null; + + this._promiseCPOWBurner = new Promise(resolve => { + this._browser.messageManager.addMessageListener("test-performance-watcher:cpow-init", msg => { + // Note that we cannot resolve Promises with CPOWs now that they + // have been outlawed in bug 1233497, so we stash it in the + // AddonBurner instance instead. + this._CPOWBurner = msg.objects.burnCPOWInSandbox; + resolve(); + }); + }); +} +AddonBurner.prototype = Object.create(CPUBurner.prototype); +Object.defineProperty(AddonBurner.prototype, "addonId", { + get: function() { + return this._addonId; + } +}); + +/** + * Simulate slow code being executed by the add-on in the chrome. + */ +AddonBurner.prototype.burnCPU = function() { + Cu.evalInSandbox(AddonBurner.burnCPU.toSource() + "()", this._sandbox); +}; + +/** + * Simulate slow code being executed by the add-on in a CPOW. + */ +AddonBurner.prototype.promiseBurnCPOW = Task.async(function*() { + yield this._promiseCPOWBurner; + ok(this._CPOWBurner, "Got the CPOW burner"); + let burner = this._CPOWBurner; + info("Parent: Preparing to burn CPOW"); + try { + yield burner(this._addonId); + info("Parent: Done burning CPOW"); + } catch (ex) { + info(`Parent: Error burning CPOW: ${ex}\n`); + info(ex.stack + "\n"); + } +}); + +/** + * Simulate slow code being executed by the add-on in the content. + */ +AddonBurner.prototype.promiseBurnContentCPU = function() { + return promiseContentResponse(this._browser, "test-performance-watcher:burn-content-cpu", {addonId: this._addonId}); +}; +AddonBurner.burnCPU = function() { + var start = Date.now(); + var ignored = []; + while (Date.now() - start < 500) { + ignored[ignored.length % 2] = ignored.length; + } +}; + + +function AddonListener(addonId, accept) { + let target = {addonId}; + AlertListener.call(this, accept, { + register: () => { + info(`AddonListener: registering ${JSON.stringify(target, null, "\t")}`); + PerformanceWatcher.addPerformanceListener({addonId}, this.listener); + }, + unregister: () => { + info(`AddonListener: unregistering ${JSON.stringify(target, null, "\t")}`); + PerformanceWatcher.removePerformanceListener({addonId}, this.listener); + } + }); +} +AddonListener.prototype = Object.create(AlertListener.prototype); + +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; +} diff --git a/toolkit/components/perfmonitoring/tests/browser/install.rdf b/toolkit/components/perfmonitoring/tests/browser/install.rdf new file mode 100644 index 000000000..65add014f --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/install.rdf @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>addonwatcher-test@mozilla.com</em:id> + <em:version>1.0</em:version> + + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>0.3</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <em:targetApplication> + <Description> + <em:id>toolkit@mozilla.org</em:id> + <em:minVersion>1</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <em:bootstrap>true</em:bootstrap> + + <em:name>Sample for browser_AddonWatcher.js</em:name> + + </Description> +</RDF> |