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