diff options
Diffstat (limited to 'toolkit/components/perfmonitoring/PerformanceWatcher.jsm')
-rw-r--r-- | toolkit/components/perfmonitoring/PerformanceWatcher.jsm | 367 |
1 files changed, 367 insertions, 0 deletions
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); + }, +}; |