Bug 787767 - Implement runtime performance monitoring for Worker API abuse. r=felipe
☠☠ backed out by 1772372ea527 ☠ ☠
authorJared Wein <jwein@mozilla.com>
Sun, 21 Oct 2012 17:26:11 -0700
changeset 111141 ca101d9262407f62618bc3eda1510d0f149ffbe1
parent 111140 d82e14184e9faf313617a4cad51ecc50c615e87f
child 111142 ca0bbaaf02821fdca0f783d7543a0c10f6fb9592
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
reviewersfelipe
bugs787767
milestone19.0a1
Bug 787767 - Implement runtime performance monitoring for Worker API abuse. r=felipe
browser/base/content/test/Makefile.in
browser/base/content/test/browser_social_usageMonitor.js
browser/base/content/test/social_worker.js
modules/libpref/src/init/all.js
toolkit/components/social/WorkerAPI.jsm
toolkit/locales/en-US/chrome/global/social.properties
toolkit/locales/jar.mn
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -272,16 +272,17 @@ endif
                  browser_bug734076.js \
                  browser_social_toolbar.js \
                  browser_social_shareButton.js \
                  browser_social_sidebar.js \
                  browser_social_flyout.js \
                  browser_social_mozSocial_API.js \
                  browser_social_isVisible.js \
                  browser_social_chatwindow.js \
+                 browser_social_usageMonitor.js \
                  social_panel.html \
                  social_share_image.png \
                  social_sidebar.html \
                  social_chat.html \
                  social_flyout.html \
                  social_window.html \
                  social_worker.js \
                  $(NULL)
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_social_usageMonitor.js
@@ -0,0 +1,121 @@
+/* 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/. */
+
+// A mock notifications server.  Based on:
+// dom/tests/mochitest/notification/notification_common.js
+const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"].
+    getService(Ci.nsIUUIDGenerator).generateUUID();
+
+const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
+const ALERTS_SERVICE_CID = Components.ID(Cc[ALERTS_SERVICE_CONTRACT_ID].number);
+
+function MockAlertsService() {}
+
+MockAlertsService.prototype = {
+
+    showAlertNotification: function(imageUrl, title, text, textClickable,
+                                    cookie, alertListener, name) {
+        let obData = JSON.stringify({
+          imageUrl: imageUrl,
+          title: title,
+          text: text,
+          textClickable: textClickable,
+          cookie: cookie,
+          name: name
+        });
+        Services.obs.notifyObservers(null, "social-test:notification-alert", obData);
+    },
+
+    QueryInterface: function(aIID) {
+        if (aIID.equals(Ci.nsISupports) ||
+            aIID.equals(Ci.nsIAlertsService))
+            return this;
+        throw Cr.NS_ERROR_NO_INTERFACE;
+    }
+};
+
+var factory = {
+    createInstance: function(aOuter, aIID) {
+        if (aOuter != null)
+            throw Cr.NS_ERROR_NO_AGGREGATION;
+        return new MockAlertsService().QueryInterface(aIID);
+    }
+};
+
+function replacePromptService() {
+  Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
+            .registerFactory(FAKE_CID, "",
+                             ALERTS_SERVICE_CONTRACT_ID,
+                             factory)
+}
+
+function restorePromptService() {
+  Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
+            .registerFactory(ALERTS_SERVICE_CID, "",
+                             ALERTS_SERVICE_CONTRACT_ID,
+                             null);
+}
+// end of alerts service mock.
+
+function test() {
+  waitForExplicitFinish();
+
+  let manifest = { // normal provider
+    name: "provider 1",
+    origin: "https://example.com",
+    sidebarURL: "https://example.com/browser/browser/base/content/test/social_sidebar.html",
+    workerURL: "https://example.com/browser/browser/base/content/test/social_worker.js",
+    iconURL: "https://example.com/browser/browser/base/content/test/moz.png"
+  };
+  Services.prefs.setBoolPref("social.debug.monitorUsage", true);
+  Services.prefs.setIntPref("social.debug.monitorUsageTimeLimitMS", 1000);
+  replacePromptService();
+  registerCleanupFunction(function() {
+    Services.prefs.clearUserPref("social.debug.monitorUsage");
+    Services.prefs.clearUserPref("social.debug.monitorUsageTimeLimitMS");
+    restorePromptService();
+  });
+
+  runSocialTestWithProvider(manifest, function (finishcb) {
+    runSocialTests(tests, undefined, undefined, finishcb);
+  });
+}
+
+var tests = {
+  testWorkerAPIAbuse: function(next) {
+    let port = Social.provider.getWorkerPort();
+    ok(port, "provider has a port");
+    Services.obs.addObserver(function abuseObserver(subject, topic, data) {
+      Services.obs.removeObserver(abuseObserver, "social-test:notification-alert");
+      data = JSON.parse(data);
+      is(data.title, "provider 1", "Abusive provider name should match");
+      is(data.text,
+         "Social API performance warning: More than 10 calls to social.cookies-get in less than 10 seconds.",
+         "Usage warning should mention social.cookies-get");
+      next();
+    }, "social-test:notification-alert", false);
+
+    for (let i = 0; i < 15; i++)
+      port.postMessage({topic: "test-worker-spam-message"});
+  },
+  testTimeBetweenFirstAndLastMoreThanLimit: function(next) {
+    let port = Social.provider.getWorkerPort();
+    ok(port, "provider has a port");
+    Services.obs.addObserver(function abuseObserver(subject, topic, data) {
+      Services.obs.removeObserver(abuseObserver, "social-test:notification-alert");
+      data = JSON.parse(data);
+      is(data.title, "provider 1", "Abusive provider name should match");
+      is(data.text,
+         "Social API performance warning: More than 10 calls to social.cookies-get in less than 10 seconds.",
+         "Usage warning should mention social.cookies-get");
+      next();
+    }, "social-test:notification-alert", false);
+
+    port.postMessage({topic: "test-worker-spam-message"});
+    setTimeout(function() {
+      for (let i = 0; i < 15; i++)
+        port.postMessage({topic: "test-worker-spam-message"});
+    }, 2000);
+  }
+}
--- a/browser/base/content/test/social_worker.js
+++ b/browser/base/content/test/social_worker.js
@@ -71,16 +71,20 @@ onconnect = function(e) {
         testPort.postMessage({topic:"got-flyout-visibility", result: event.data.result});
         break;
       case "test-flyout-close":
         sidebarPort.postMessage({topic:"test-flyout-close"});
         break;
       case "test-worker-chat":
         apiPort.postMessage({topic: "social.request-chat", data: event.data.data });
         break;
+      case "test-worker-spam-message":
+        // Just use a random api message, but one that has little side-effects.
+        apiPort.postMessage({topic: "social.cookies-get"});
+        break;
       case "social.initialize":
         // This is the workerAPI port, respond and set up a notification icon.
         apiPort = port;
         let profile = {
           portrait: "https://example.com/portrait.jpg",
           userName: "trickster",
           displayName: "Kuma Lisa",
           profileURL: "http://en.wikipedia.org/wiki/Kuma_Lisa"
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -3786,16 +3786,18 @@ pref("memory.low_memory_notification_int
 #endif
 
 // How long must we wait before declaring that a window is a "ghost" (i.e., a
 // likely leak)?  This should be longer than it usually takes for an eligible
 // window to be collected via the GC/CC.
 pref("memory.ghost_window_timeout_seconds", 60);
 
 pref("social.enabled", false);
+pref("social.debug.monitorUsage", false);
+pref("social.debug.monitorUsageTimeThresholdMS", 10000);
 
 // Disable idle observer fuzz, because only privileged content can access idle
 // observers (bug 780507).
 pref("dom.idle-observers-api.fuzz_time.disabled", true);
 
 pref("toolkit.identity.debug", false);
 
 // Setting that to true grant elevated privileges to apps that ask
--- a/toolkit/components/social/WorkerAPI.jsm
+++ b/toolkit/components/social/WorkerAPI.jsm
@@ -17,16 +17,19 @@ const EXPORTED_SYMBOLS = ["WorkerAPI"];
 
 function WorkerAPI(provider, port) {
   if (!port)
     throw new Error("Can't initialize WorkerAPI with a null port");
 
   this._provider = provider;
   this._port = port;
   this._port.onmessage = this._handleMessage.bind(this);
+  this._usageMonitor = Services.prefs.getBoolPref("social.debug.monitorUsage") ?
+    new WorkerAPIUsageMonitor(provider) :
+    null;
 
   // Send an "intro" message so the worker knows this is the port
   // used for the api.
   // later we might even include an API version - version 0 for now!
   this._port.postMessage({topic: "social.initialize"});
 }
 
 WorkerAPI.prototype = {
@@ -37,16 +40,18 @@ WorkerAPI.prototype = {
   _handleMessage: function _handleMessage(event) {
     let {topic, data} = event.data;
     let handler = this.handlers[topic];
     if (!handler) {
       Cu.reportError("WorkerAPI: topic doesn't have a handler: '" + topic + "'");
       return;
     }
     try {
+      if (this._usageMonitor)
+        this._usageMonitor.logMessage(topic);
       handler.call(this, data);
     } catch (ex) {
       Cu.reportError("WorkerAPI: failed to handle message '" + topic + "': " + ex);
     }
   },
 
   handlers: {
     "social.reload-worker": function(data) {
@@ -125,8 +130,40 @@ WorkerAPI.prototype = {
                                           !!action, // text clickable if an
                                                     // action was provided.
                                           null,
                                           listener,
                                           type); 
     },
   }
 }
+
+function WorkerAPIUsageMonitor(provider) {
+  if (!provider)
+    throw new Error("Can't initialize WorkerAPIUsageMonitor with a null provider");
+  this._providerName = provider.name;
+  this.TIME_THRESHOLD_MS = Services.prefs.getIntPref("social.debug.monitorUsageTimeThresholdMS");
+  this._messages = {};
+}
+
+WorkerAPIUsageMonitor.prototype = {
+  logMessage: function WorkerAPIUsage_logMessage(aMessage) {
+    if (!(aMessage in this._messages)) {
+      this._messages[aMessage] = [];
+    }
+    let messageList = this._messages[aMessage];
+    messageList.push(Date.now());
+    if (messageList.length > 10) {
+      if (messageList[9] - messageList[0] < this.TIME_THRESHOLD_MS) {
+        let alertsService = Cc["@mozilla.org/alerts-service;1"]
+                              .getService(Ci.nsIAlertsService);
+        const SOCIAL_BUNDLE = "chrome://global/locale/social.properties";
+        let socialBundle = Services.strings.createBundle(SOCIAL_BUNDLE);
+        let seconds = (this.TIME_THRESHOLD_MS / 1000).toString();
+        let text = socialBundle.formatStringFromName("social.usageAbuse",
+                                                     [aMessage, seconds], 2);
+        alertsService.showAlertNotification("chrome://branding/content/icon48.png",
+                                            this._providerName, text);
+      }
+      messageList.shift();
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/chrome/global/social.properties
@@ -0,0 +1,6 @@
+# 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/.
+
+# LOCALIZATION NOTE (social.usage): This is only used when debugging.
+social.usageAbuse=Social API performance warning: More than 10 calls to %S in less than %S seconds.
--- a/toolkit/locales/jar.mn
+++ b/toolkit/locales/jar.mn
@@ -53,16 +53,17 @@
   locale/@AB_CD@/global/printjoboptions.dtd             (%chrome/global/printjoboptions.dtd)
   locale/@AB_CD@/global/printPageSetup.dtd              (%chrome/global/printPageSetup.dtd)
   locale/@AB_CD@/global/printPreview.dtd                (%chrome/global/printPreview.dtd)
   locale/@AB_CD@/global/printPreviewProgress.dtd        (%chrome/global/printPreviewProgress.dtd)
   locale/@AB_CD@/global/printdialog.properties          (%chrome/global/printdialog.properties)
   locale/@AB_CD@/global/printProgress.dtd               (%chrome/global/printProgress.dtd)
   locale/@AB_CD@/global/regionNames.properties          (%chrome/global/regionNames.properties)
   locale/@AB_CD@/global/resetProfile.dtd                (%chrome/global/resetProfile.dtd)
+  locale/@AB_CD@/global/social.properties               (%chrome/global/social.properties)
   locale/@AB_CD@/global/dialog.properties               (%chrome/global/dialog.properties)
   locale/@AB_CD@/global/tree.dtd                        (%chrome/global/tree.dtd)
   locale/@AB_CD@/global/textcontext.dtd                 (%chrome/global/textcontext.dtd)
   locale/@AB_CD@/global/videocontrols.dtd               (%chrome/global/videocontrols.dtd)
   locale/@AB_CD@/global/viewSource.dtd                  (%chrome/global/viewSource.dtd)
   locale/@AB_CD@/global/viewSource.properties           (%chrome/global/viewSource.properties)
   locale/@AB_CD@/global/webapps.properties              (%chrome/global/webapps.properties)
   locale/@AB_CD@/global/wizard.dtd                      (%chrome/global/wizard.dtd)