bug 1071880 - Notify user of addons that are slowing their browser down significantly r=mossop
authorBrad Lassey <blassey@mozilla.com>
Thu, 12 Feb 2015 19:22:53 -0500
changeset 242746 2439c229cacea574a5b398c8c766df447732b90b
parent 242745 82abcbb019d7a7961cc7b35616c62467551546f6
child 242747 980b169f455a7dae19f461884d5babacd2c69644
child 242788 0f829e7ccd2ff21fa02a9feb521ab5b61fef0ef1
push id682
push userjryans@gmail.com
push dateFri, 13 Feb 2015 21:27:38 +0000
reviewersmossop
bugs1071880
milestone38.0a1
bug 1071880 - Notify user of addons that are slowing their browser down significantly r=mossop
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.properties
modules/libpref/init/all.js
toolkit/modules/AddonWatcher.jsm
toolkit/modules/moz.build
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -133,16 +133,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource:///modules/FormValidationHandler.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
                                   "resource://gre/modules/WebChannel.jsm");
 
 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";
 
 // Seconds of idle before trying to create a bookmarks backup.
 const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 8 * 60;
 // Minimum interval between backups.  We try to not create more than one backup
 // per interval.
 const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
@@ -559,16 +562,86 @@ BrowserGlue.prototype = {
   },
 
   _onAppDefaults: function BG__onAppDefaults() {
     // apply distribution customizations (prefs)
     // other customizations are applied in _finalUIStartup()
     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() {
     this._sanitizer.onStartup();
     // check if we're in safe mode
     if (Services.appinfo.inSafeMode) {
       Services.ww.openWindow(null, "chrome://browser/content/safeMode.xul", 
                              "_blank", "chrome,centerscreen,modal,resizable=no", null);
@@ -607,16 +680,18 @@ BrowserGlue.prototype = {
     LoginManagerParent.init();
     ReaderParent.init();
 
 #ifdef NIGHTLY_BUILD
     Services.prefs.addObserver(POLARIS_ENABLED, this, false);
 #endif
 
     Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
+
+    AddonWatcher.init(this._notifySlowAddon);
   },
 
   _checkForOldBuildUpdates: function () {
     // check for update if our build is old
     if (Services.prefs.getBoolPref("app.update.enabled") &&
         Services.prefs.getBoolPref("app.update.checkInstallTime")) {
 
       let buildID = Services.appinfo.appBuildID;
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -35,16 +35,27 @@ xpinstallDisabledButton.accesskey=n
 # http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # Also see https://bugzilla.mozilla.org/show_bug.cgi?id=570012 for mockups
 addonDownloading=Add-on downloading;Add-ons downloading
 addonDownloadCancelled=Add-on download cancelled.;Add-on downloads cancelled.
 addonDownloadRestart=Restart Download;Restart Downloads
 addonDownloadRestart.accessKey=R
 addonDownloadCancelTooltip=Cancel
 
+addonwatch.slow=%S might be making %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 %S you must restart %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
 # #1 first add-on's name, #2 number of add-ons, #3 application name
 addonsInstalled=#1 has been installed successfully.;#2 add-ons have been installed successfully.
 addonsInstalledNeedsRestart=#1 will be installed after you restart #3.;#2 add-ons will be installed after you restart #3.
 addonInstallRestartButton=Restart Now
 addonInstallRestartButton.accesskey=R
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4461,16 +4461,22 @@ pref("dom.mozSettings.SettingsManager.ve
 pref("dom.mozSettings.SettingsRequestManager.verbose.enabled", false);
 pref("dom.mozSettings.SettingsService.verbose.enabled", false);
 
 // Controlling whether we want to allow forcing some Settings
 // IndexedDB transactions to be opened as readonly or keep everything as
 // readwrite.
 pref("dom.mozSettings.allowForceReadOnly", false);
 
+// The interval at which to check for slow running addons
+pref("browser.addon-watch.interval", 120000);
+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", 1);
+
 // RequestSync API is disabled by default.
 pref("dom.requestSync.enabled", false);
 
 // Search service settings
 pref("browser.search.log", false);
 pref("browser.search.update", true);
 pref("browser.search.update.log", false);
 pref("browser.search.update.interval", 21600);
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/AddonWatcher.jsm
@@ -0,0 +1,83 @@
+// -*- 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._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._interval = Preferences.get("browser.addon-watch.interval", 15000);
+    this._timer.initWithCallback(this._checkAddons.bind(this), this._interval, Ci.nsITimer.TYPE_REPEATING_SLACK);
+  },
+  _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] && (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]));
+    }
+  }
+};
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -7,16 +7,17 @@
 XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
 MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
 
 SPHINX_TREES['toolkit_modules'] = 'docs'
 
 EXTRA_JS_MODULES += [
+    'AddonWatcher.jsm',
     'Battery.jsm',
     'BinarySearch.jsm',
     'BrowserUtils.jsm',
     'CharsetMenu.jsm',
     'debug.js',
     'DeferredTask.jsm',
     'Deprecated.jsm',
     'Dict.jsm',