// -*- 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); }, };