From 30d56eb7e850aa8d23318ac66b1b6dd54e352327 Mon Sep 17 00:00:00 2001 From: Brad Lassey Date: Mon, 23 Feb 2015 11:39:05 -0500 Subject: [PATCH] bug 1071880 - Notify user of addons that are slowing their browser down significantly r=mossop --- browser/components/nsBrowserGlue.js | 76 +++++++++++++++ .../en-US/chrome/browser/browser.properties | 11 +++ modules/libpref/init/all.js | 10 ++ toolkit/modules/AddonWatcher.jsm | 96 +++++++++++++++++++ toolkit/modules/moz.build | 1 + 5 files changed, 194 insertions(+) create mode 100644 toolkit/modules/AddonWatcher.jsm diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index 99027513b91c..f0cfaeff2bee 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -148,6 +148,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "WebChannel", XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent", "resource:///modules/ReaderParent.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AddonWatcher", + "resource://gre/modules/AddonWatcher.jsm"); + const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser"; const PREF_PLUGINS_UPDATEURL = "plugins.update.url"; @@ -587,6 +590,76 @@ BrowserGlue.prototype = { this._distributionCustomizer.applyPrefDefaults(); }, + _notifySlowAddon: function BG_notifySlowAddon(addonId) { + let addonCallback = function(addon) { + if (!addon) { + Cu.reportError("couldn't look up addon: " + addonId); + return; + } + let win = RecentWindow.getMostRecentBrowserWindow(); + + if (!win) { + return; + } + + let brandBundle = win.document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let message = win.gNavigatorBundle.getFormattedString("addonwatch.slow", [addon.name, brandShortName]); + let notificationBox = win.document.getElementById("global-notificationbox"); + let notificationId = 'addon-slow:' + addonId; + let notification = notificationBox.getNotificationWithValue(notificationId); + if(notification) { + notification.label = message; + } else { + let buttons = [ + { + label: win.gNavigatorBundle.getFormattedString("addonwatch.disable.label", [addon.name]), + accessKey: win.gNavigatorBundle.getString("addonwatch.disable.accesskey"), + callback: function() { + addon.userDisabled = true; + if (addon.pendingOperations != addon.PENDING_NONE) { + let restartMessage = win.gNavigatorBundle.getFormattedString("addonwatch.restart.message", [addon.name, brandShortName]); + let restartButton = [ + { + label: win.gNavigatorBundle.getFormattedString("addonwatch.restart.label", [brandShortName]), + accessKey: win.gNavigatorBundle.getString("addonwatch.restart.accesskey"), + callback: function() { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(appStartup.eForceQuit | appStartup.eRestart); + } + } + ]; + const priority = notificationBox.PRIORITY_WARNING_MEDIUM; + notificationBox.appendNotification(restartMessage, "restart-" + addonId, "", + priority, restartButton); + } + } + }, + { + label: win.gNavigatorBundle.getString("addonwatch.ignoreSession.label"), + accessKey: win.gNavigatorBundle.getString("addonwatch.ignoreSession.accesskey"), + callback: function() { + AddonWatcher.ignoreAddonForSession(addonId); + } + }, + { + label: win.gNavigatorBundle.getString("addonwatch.ignorePerm.label"), + accessKey: win.gNavigatorBundle.getString("addonwatch.ignorePerm.accesskey"), + callback: function() { + AddonWatcher.ignoreAddonPermanently(addonId); + } + }, + ]; + + const priority = notificationBox.PRIORITY_WARNING_MEDIUM; + notificationBox.appendNotification(message, notificationId, "", + priority, buttons); + } + }; + AddonManager.getAddonByID(addonId, addonCallback); + }, + // runs on startup, before the first command line handler is invoked // (i.e. before the first window is opened) _finalUIStartup: function BG__finalUIStartup() { @@ -642,6 +715,8 @@ BrowserGlue.prototype = { #endif Services.obs.notifyObservers(null, "browser-ui-startup-complete", ""); + + AddonWatcher.init(this._notifySlowAddon); }, _checkForOldBuildUpdates: function () { @@ -907,6 +982,7 @@ BrowserGlue.prototype = { #endif webrtcUI.uninit(); FormValidationHandler.uninit(); + AddonWatcher.uninit(); }, _initServiceDiscovery: function () { diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index f3cde721ceee..d940a3e9dafd 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -40,6 +40,17 @@ addonDownloadRestart=Restart Download;Restart Downloads addonDownloadRestart.accessKey=R addonDownloadCancelTooltip=Cancel +addonwatch.slow=%1$S might be making %2$S run slowly +addonwatch.disable.label=Disable %S +addonwatch.disable.accesskey=D +addonwatch.ignoreSession.label=Ignore for now +addonwatch.ignoreSession.accesskey=I +addonwatch.ignorePerm.label=Ignore permanently +addonwatch.ignorePerm.accesskey=p +addonwatch.restart.message=To disable %1$S you must restart %2$S +addonwatch.restart.label=Restart %S +addonwatch.restart.accesskey=R + # LOCALIZATION NOTE (addonsInstalled, addonsInstalledNeedsRestart): # Semicolon-separated list of plural forms. See: # http://developer.mozilla.org/en/docs/Localization_and_Plurals diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 0f47f36b1ce9..e3312fdd1a4f 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -4477,6 +4477,16 @@ pref("dom.mozSettings.SettingsService.verbose.enabled", false); // readwrite. pref("dom.mozSettings.allowForceReadOnly", false); +// The interval at which to check for slow running addons +#ifdef NIGHTLY_BUILD +pref("browser.addon-watch.interval", 15000); +#else +pref("browser.addon-watch.interval", -1); +#endif +pref("browser.addon-watch.ignore", "[\"mochikit@mozilla.org\",\"special-powers@mozilla.org\"]"); +// the percentage of time addons are allowed to use without being labeled slow +pref("browser.addon-watch.percentage-limit", 5); + // RequestSync API is disabled by default. pref("dom.requestSync.enabled", false); diff --git a/toolkit/modules/AddonWatcher.jsm b/toolkit/modules/AddonWatcher.jsm new file mode 100644 index 000000000000..d85016db596d --- /dev/null +++ b/toolkit/modules/AddonWatcher.jsm @@ -0,0 +1,96 @@ +// -*- 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"); + +let AddonWatcher = { + _lastAddonTime: {}, + _timer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer), + _callback: null, + _interval: 1500, + _ignoreList: null, + init: function(callback) { + if (!callback) { + return; + } + + if (this._callback) { + return; + } + + this._interval = Preferences.get("browser.addon-watch.interval", 15000); + if (this._interval == -1) { + return; + } + + this._callback = callback; + 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._timer.initWithCallback(this._checkAddons.bind(this), this._interval, Ci.nsITimer.TYPE_REPEATING_SLACK); + }, + uninit: function() { + if (this._timer) { + this._timer.cancel(); + this._timer = null; + } + }, + _checkAddons: function() { + let compartmentInfo = Cc["@mozilla.org/compartment-info;1"] + .getService(Ci.nsICompartmentInfo); + let compartments = compartmentInfo.getCompartments(); + let count = compartments.length; + let addons = {}; + for (let i = 0; i < count; i++) { + let compartment = compartments.queryElementAt(i, Ci.nsICompartment); + if (compartment.addonId) { + if (addons[compartment.addonId]) { + addons[compartment.addonId] += compartment.time; + } else { + addons[compartment.addonId] = compartment.time; + } + } + } + let limit = this._interval * Preferences.get("browser.addon-watch.percentage-limit", 75) * 10; + for (let addonId in addons) { + if (!this._ignoreList.has(addonId)) { + if (!this._lastAddonTime[addonId]) { + this._lastAddonTime[addonId] = 0; + } + if ((addons[addonId] - this._lastAddonTime[addonId]) > limit) { + this._callback(addonId); + } + this._lastAddonTime[addonId] = addons[addonId]; + } + } + }, + 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])); + } + } +}; diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index 584778a7ccb2..b61858006221 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -12,6 +12,7 @@ MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini'] SPHINX_TREES['toolkit_modules'] = 'docs' EXTRA_JS_MODULES += [ + 'AddonWatcher.jsm', 'Battery.jsm', 'BinarySearch.jsm', 'BrowserUtils.jsm',