Bug 1214408 - Telemetry on SessionStore:update OOM;r=ttaubert
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Fri, 16 Oct 2015 21:44:54 +0200
changeset 270555 8fea00e5b7784abc955e5426e9253e5ed094317c
parent 270554 7270d5b3283a501d4e1be6f2be1ad0e1ae2cb784
child 270556 dec44b4525eb56e6ef4d423f57a6193ddf099206
push id16032
push usercbook@mozilla.com
push dateMon, 02 Nov 2015 08:02:14 +0000
treeherderfx-team@dec44b4525eb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersttaubert
bugs1214408
milestone45.0a1
Bug 1214408 - Telemetry on SessionStore:update OOM;r=ttaubert
browser/components/sessionstore/SessionStore.jsm
browser/components/sessionstore/content/content-sessionStore.js
browser/components/sessionstore/test/browser.ini
browser/components/sessionstore/test/browser_send_async_message_oom.js
toolkit/components/telemetry/Histograms.json
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -67,50 +67,62 @@ const MESSAGES = [
   // consider restoring another tab in the queue. The document has
   // been restored, and forms have been filled. We trigger
   // SSTabRestored at this time.
   "SessionStore:restoreTabContentComplete",
 
   // A crashed tab was revived by navigating to a different page. Remove its
   // browser from the list of crashed browsers to stop ignoring its messages.
   "SessionStore:crashedTabRevived",
+
+  // The content script encountered an error.
+  "SessionStore:error",
 ];
 
 // The list of messages we accept from <xul:browser>s that have no tab
 // assigned. Those are for example the ones that preload about:newtab pages.
 const NOTAB_MESSAGES = new Set([
   // For a description see above.
   "SessionStore:setupSyncHandler",
 
   // For a description see above.
   "SessionStore:crashedTabRevived",
 
   // For a description see above.
   "SessionStore:update",
+
+  // For a description see above.
+  "SessionStore:error",
 ]);
 
 // The list of messages we accept without an "epoch" parameter.
 // See getCurrentEpoch() and friends to find out what an "epoch" is.
 const NOEPOCH_MESSAGES = new Set([
   // For a description see above.
   "SessionStore:setupSyncHandler",
 
   // For a description see above.
   "SessionStore:crashedTabRevived",
+
+  // For a description see above.
+  "SessionStore:error",
 ]);
 
 // The list of messages we want to receive even during the short period after a
 // frame has been removed from the DOM and before its frame script has finished
 // unloading.
 const CLOSED_MESSAGES = new Set([
   // For a description see above.
   "SessionStore:crashedTabRevived",
 
   // For a description see above.
   "SessionStore:update",
+
+  // For a description see above.
+  "SessionStore:error",
 ]);
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
   "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
@@ -803,16 +815,19 @@ var SessionStoreInternal = {
         SessionStoreInternal._resetLocalTabRestoringState(tab);
         SessionStoreInternal.restoreNextTab();
 
         this._sendTabRestoredNotification(tab);
         break;
       case "SessionStore:crashedTabRevived":
         this._crashedBrowsers.delete(browser.permanentKey);
         break;
+      case "SessionStore:error":
+        this.reportInternalError(data);
+        break;
       default:
         throw new Error(`received unknown message '${aMessage.name}'`);
         break;
     }
   },
 
   /**
    * Record telemetry measurements stored in an object.
@@ -3843,16 +3858,29 @@ var SessionStoreInternal = {
 
   /**
    * Resets the epoch for a given <browser>. We need to this every time we
    * receive a hint that a new docShell has been loaded into the browser as
    * the frame script starts out with epoch=0.
    */
   resetEpoch(browser) {
     this._browserEpochs.delete(browser.permanentKey);
+  },
+
+  /**
+   * Handle an error report from a content process.
+   */
+  reportInternalError(data) {
+    // For the moment, we only report errors through Telemetry.
+    if (data.telemetry) {
+      for (let key of Object.keys(data.telemetry)) {
+        let histogram = Telemetry.getHistogramById(key);
+        histogram.add(data.telemetry[key]);
+      }
+    }
   }
 };
 
 /**
  * Priority queue that keeps track of a list of tabs to restore and returns
  * the tab we should restore next, based on priority rules. We decide between
  * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only
  * restored with restore_hidden_tabs=true.
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -24,20 +24,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource:///modules/sessionstore/PageStyle.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
   "resource://gre/modules/ScrollPosition.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
   "resource:///modules/sessionstore/SessionHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
   "resource:///modules/sessionstore/SessionStorage.jsm");
 
-XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
-                                   "@mozilla.org/childprocessmessagemanager;1",
-                                   "nsISyncMessageSender");
-
 Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
 var gFrameTree = new FrameTree(this);
 
 Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this);
 XPCOMUtils.defineLazyGetter(this, 'gContentRestore',
                             () => { return new ContentRestore(this) });
 
 // The current epoch.
@@ -687,22 +683,31 @@ var MessageQueue = {
       data[key] = this._data.get(key)();
     }
 
     durationMs = Date.now() - durationMs;
     let telemetry = {
       FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS: durationMs
     }
 
-    // Send all data to the parent process.
-    sendMessage("SessionStore:update", {
-      id: this._id, data, telemetry, flushID,
-      isFinal: options.isFinal || false,
-      epoch: gCurrentEpoch
-    });
+    try {
+      // Send all data to the parent process.
+      sendMessage("SessionStore:update", {
+        id: this._id, data, telemetry, flushID,
+        isFinal: options.isFinal || false,
+        epoch: gCurrentEpoch
+      });
+    } catch (ex if ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) {
+      let telemetry = {
+        FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM: 1
+      };
+      sendMessage("SessionStore:error", {
+        telemetry
+      });
+    }
 
     // Increase our unique message ID.
     this._id++;
   },
 
   /**
    * This function is used to make the message queue flush all queue data that
    * hasn't been sent to the parent process, yet.
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -206,8 +206,9 @@ skip-if = true
 [browser_464620_b.js]
 skip-if = true
 
 # Disabled on OS X:
 [browser_625016.js]
 skip-if = os == "mac"
 
 [browser_911547.js]
+[browser_send_async_message_oom.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_send_async_message_oom.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const HISTOGRAM_NAME = "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM";
+
+/**
+ * Test that an OOM in sendAsyncMessage in a framescript will be reported
+ * to Telemetry.
+ */
+
+add_task(function* init() {
+  Services.telemetry.canRecordExtended = true;
+});
+
+function frameScript() {
+  // Make send[A]syncMessage("SessionStore:update", ...) simulate OOM.
+  // Other operations are unaffected.
+  let mm = docShell.sameTypeRootTreeItem.
+    QueryInterface(Ci.nsIDocShell).
+    QueryInterface(Ci.nsIInterfaceRequestor).
+    getInterface(Ci.nsIContentFrameMessageManager);
+
+  let wrap = function(original) {
+    return function(name, ...args) {
+      if (name != "SessionStore:update") {
+        return original(name, ...args);
+      }
+      throw new Components.Exception("Simulated OOM", Cr.NS_ERROR_OUT_OF_MEMORY);
+    }
+  }
+
+  mm.sendAsyncMessage = wrap(mm.sendAsyncMessage);
+  mm.sendSyncMessage = wrap(mm.sendSyncMessage);
+}
+
+add_task(function*() {
+  // Capture original state.
+  let snapshot = Services.telemetry.getHistogramById(HISTOGRAM_NAME).snapshot();
+
+  // Open a browser, configure it to cause OOM.
+  let newTab = gBrowser.addTab("about:robots");
+  let browser = newTab.linkedBrowser;
+  yield ContentTask.spawn(browser, null, frameScript);
+
+
+  let promiseReported = new Promise(resolve => {
+    browser.messageManager.addMessageListener("SessionStore:error", resolve);
+  });
+
+  // Attempt to flush. This should fail.
+  let promiseFlushed = TabStateFlusher.flush(browser);
+  promiseFlushed.then(() => {throw new Error("Flush should have failed")});
+
+  // The frame script should report an error.
+  yield promiseReported;
+
+  // Give us some time to handle that error.
+  yield new Promise(resolve => setTimeout(resolve, 10));
+
+  // By now, Telemetry should have been updated.
+  let snapshot2 = Services.telemetry.getHistogramById(HISTOGRAM_NAME).snapshot();
+  gBrowser.removeTab(newTab);
+
+  Assert.ok(snapshot2.sum > snapshot.sum);
+});
+
+add_task(function* cleanup() {
+  Services.telemetry.canRecordExtended = false;
+});
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -4347,16 +4347,22 @@
     "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
     "expires_in_version": "default",
     "kind": "exponential",
     "high": "3000",
     "n_buckets": 10,
     "extended_statistics_ok": true,
     "description": "Session restore: Time spent blocking the main thread while restoring a window state (ms)"
   },
+  "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM": {
+    "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+    "expires_in_version": "default",
+    "kind": "count",
+    "description": "Count of messages sent by SessionRestore from child frames to the parent and that cannot be transmitted as they eat up too much memory."
+  },
   "FX_TABLETMODE_PAGE_LOAD": {
     "expires_in_version": "47",
     "kind": "exponential",
     "high": 100000,
     "n_buckets": 30,
     "keyed": true,
     "description": "Number of toplevel location changes in tablet and desktop mode (only used on win10 where tablet mode is available)"
   },