summaryrefslogtreecommitdiff
path: root/toolkit/components/perfmonitoring/PerformanceWatcher.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/perfmonitoring/PerformanceWatcher.jsm')
-rw-r--r--toolkit/components/perfmonitoring/PerformanceWatcher.jsm367
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 0000000000..d0d0349749
--- /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);
+ },
+};