Bug 960641 - Queue browser API calls before remote frame is shown. r=fabrice, a=1.3+
authorPatrick Wang(Chih-Kai Wang) <kk1fff@patrickz.net>
Thu, 16 Jan 2014 03:05:00 +0800
changeset 174951 5fb1fa58180a0dc486fcb54b20d526659539b1a0
parent 174950 2d9d1d3878544972881c6f4df3f59dad1a7662be
child 174952 bb2a6925b7124997394ff2f64483995b4e12e9be
push id3224
push userlsblakk@mozilla.com
push dateTue, 04 Feb 2014 01:06:49 +0000
treeherdermozilla-beta@60c04d0987f1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfabrice, 1
bugs960641
milestone28.0a2
Bug 960641 - Queue browser API calls before remote frame is shown. r=fabrice, a=1.3+
content/base/src/nsFrameLoader.cpp
content/base/src/nsFrameLoader.h
dom/browser-element/BrowserElementParent.js
dom/browser-element/BrowserElementParent.jsm
--- a/content/base/src/nsFrameLoader.cpp
+++ b/content/base/src/nsFrameLoader.cpp
@@ -279,16 +279,17 @@ nsFrameLoader::nsFrameLoader(Element* aO
   , mRemoteBrowserInitialized(false)
   , mObservingOwnerContent(false)
   , mVisible(true)
   , mCurrentRemoteFrame(nullptr)
   , mRemoteBrowser(nullptr)
   , mChildID(0)
   , mRenderMode(RENDER_MODE_DEFAULT)
   , mEventMode(EVENT_MODE_NORMAL_DISPATCH)
+  , mPendingFrameSent(false)
 {
   ResetPermissionManagerStatus();
 }
 
 nsFrameLoader::~nsFrameLoader()
 {
   mNeedsAsyncDestroy = true;
   if (mMessageManager) {
@@ -453,18 +454,27 @@ nsFrameLoader::ReallyStartLoadingInterna
 
   nsresult rv = MaybeCreateDocShell();
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   if (mRemoteFrame) {
     if (!mRemoteBrowser) {
+      if (!mPendingFrameSent) {
+        nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+        if (OwnerIsBrowserOrAppFrame() && os && !mRemoteBrowserInitialized) {
+          os->NotifyObservers(NS_ISUPPORTS_CAST(nsIFrameLoader*, this),
+                              "remote-browser-frame-pending", nullptr);
+          mPendingFrameSent = true;
+        }
+      }
       if (Preferences::GetBool("dom.ipc.processPrelaunch.enabled", false) &&
           !ContentParent::PreallocatedProcessReady()) {
+
         ContentParent::RunAfterPreallocatedProcessReady(
             new DelayedStartLoadingRunnable(this));
         return NS_ERROR_FAILURE;
       }
 
       TryRemoteBrowser();
 
       if (!mRemoteBrowser) {
@@ -982,16 +992,21 @@ nsFrameLoader::ShowRemoteFrame(const nsI
 
     mRemoteBrowser->Show(size);
     mRemoteBrowserShown = true;
 
     EnsureMessageManager();
 
     nsCOMPtr<nsIObserverService> os = services::GetObserverService();
     if (OwnerIsBrowserOrAppFrame() && os && !mRemoteBrowserInitialized) {
+      if (!mPendingFrameSent) {
+        os->NotifyObservers(NS_ISUPPORTS_CAST(nsIFrameLoader*, this),
+                            "remote-browser-frame-pending", nullptr);
+        mPendingFrameSent = true;
+      }
       os->NotifyObservers(NS_ISUPPORTS_CAST(nsIFrameLoader*, this),
                           "remote-browser-frame-shown", nullptr);
       mRemoteBrowserInitialized = true;
     }
   } else {
     nsRect dimensions;
     NS_ENSURE_SUCCESS(GetWindowDimensions(dimensions), false);
 
--- a/content/base/src/nsFrameLoader.h
+++ b/content/base/src/nsFrameLoader.h
@@ -454,11 +454,14 @@ private:
   // See nsIFrameLoader.idl.  Short story, if !(mRenderMode &
   // RENDER_MODE_ASYNC_SCROLL), all the fields below are ignored in
   // favor of what content tells.
   uint32_t mRenderMode;
 
   // See nsIFrameLoader.idl. EVENT_MODE_NORMAL_DISPATCH automatically
   // forwards some input events to out-of-process content.
   uint32_t mEventMode;
+
+  // Indicate if we have sent 'remote-browser-frame-pending'.
+  bool mPendingFrameSent;
 };
 
 #endif
--- a/dom/browser-element/BrowserElementParent.js
+++ b/dom/browser-element/BrowserElementParent.js
@@ -60,56 +60,61 @@ BrowserElementParentFactory.prototype = 
     debug("_init");
     this._initialized = true;
 
     // Maps frame elements to BrowserElementParent objects.  We never look up
     // anything in this map; the purpose is to keep the BrowserElementParent
     // alive for as long as its frame element lives.
     this._bepMap = new WeakMap();
 
-    Services.obs.addObserver(this, 'remote-browser-frame-shown', /* ownsWeak = */ true);
+    Services.obs.addObserver(this, 'remote-browser-frame-pending', /* ownsWeak = */ true);
     Services.obs.addObserver(this, 'in-process-browser-or-app-frame-shown', /* ownsWeak = */ true);
   },
 
   _browserFramesPrefEnabled: function() {
     try {
       return Services.prefs.getBoolPref(BROWSER_FRAMES_ENABLED_PREF);
     }
     catch(e) {
       return false;
     }
   },
 
   _observeInProcessBrowserFrameShown: function(frameLoader) {
     debug("In-process browser frame shown " + frameLoader);
-    this._createBrowserElementParent(frameLoader, /* hasRemoteFrame = */ false);
+    this._createBrowserElementParent(frameLoader,
+                                     /* hasRemoteFrame = */ false,
+                                     /* pending frame */ false);
   },
 
-  _observeRemoteBrowserFrameShown: function(frameLoader) {
+  _observeRemoteBrowserFramePending: function(frameLoader) {
     debug("Remote browser frame shown " + frameLoader);
-    this._createBrowserElementParent(frameLoader, /* hasRemoteFrame = */ true);
+    this._createBrowserElementParent(frameLoader,
+                                     /* hasRemoteFrame = */ true,
+                                     /* pending frame */ true);
   },
 
-  _createBrowserElementParent: function(frameLoader, hasRemoteFrame) {
+  _createBrowserElementParent: function(frameLoader, hasRemoteFrame, isPendingFrame) {
     let frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement;
-    this._bepMap.set(frameElement, BrowserElementParentBuilder.create(frameLoader, hasRemoteFrame));
+    this._bepMap.set(frameElement, BrowserElementParentBuilder.create(
+      frameLoader, hasRemoteFrame, isPendingFrame));
   },
 
   observe: function(subject, topic, data) {
     switch(topic) {
     case 'app-startup':
       this._init();
       break;
     case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
       if (data == BROWSER_FRAMES_ENABLED_PREF) {
         this._init();
       }
       break;
-    case 'remote-browser-frame-shown':
-      this._observeRemoteBrowserFrameShown(subject);
+    case 'remote-browser-frame-pending':
+      this._observeRemoteBrowserFramePending(subject);
       break;
     case 'in-process-browser-or-app-frame-shown':
       this._observeInProcessBrowserFrameShown(subject);
       break;
     case 'content-document-global-created':
       this._observeContentGlobalCreated(subject);
       break;
     }
--- a/dom/browser-element/BrowserElementParent.jsm
+++ b/dom/browser-element/BrowserElementParent.jsm
@@ -82,117 +82,91 @@ function visibilityChangeHandler(e) {
   }
 
   for (let i = 0; i < beps.length; i++) {
     beps[i]._ownerVisibilityChange();
   }
 }
 
 this.BrowserElementParentBuilder = {
-  create: function create(frameLoader, hasRemoteFrame) {
+  create: function create(frameLoader, hasRemoteFrame, isPendingFrame) {
     return new BrowserElementParent(frameLoader, hasRemoteFrame);
   }
 }
 
 
 // The active input method iframe.
 let activeInputFrame = null;
 
-function BrowserElementParent(frameLoader, hasRemoteFrame) {
+function BrowserElementParent(frameLoader, hasRemoteFrame, isPendingFrame) {
   debug("Creating new BrowserElementParent object for " + frameLoader);
   this._domRequestCounter = 0;
   this._pendingDOMRequests = {};
   this._hasRemoteFrame = hasRemoteFrame;
   this._nextPaintListeners = [];
 
   this._frameLoader = frameLoader;
   this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement;
+  let self = this;
   if (!this._frameElement) {
     debug("No frame element?");
     return;
   }
 
-  this._mm = frameLoader.messageManager;
-  let self = this;
-
-  // Messages we receive are handed to functions which take a (data) argument,
-  // where |data| is the message manager's data object.
-  // We use a single message and dispatch to various function based
-  // on data.msg_name
-  let mmCalls = {
-    "hello": this._recvHello,
-    "contextmenu": this._fireCtxMenuEvent,
-    "locationchange": this._fireEventFromMsg,
-    "loadstart": this._fireEventFromMsg,
-    "loadend": this._fireEventFromMsg,
-    "titlechange": this._fireEventFromMsg,
-    "iconchange": this._fireEventFromMsg,
-    "close": this._fireEventFromMsg,
-    "resize": this._fireEventFromMsg,
-    "activitydone": this._fireEventFromMsg,
-    "opensearch": this._fireEventFromMsg,
-    "securitychange": this._fireEventFromMsg,
-    "error": this._fireEventFromMsg,
-    "scroll": this._fireEventFromMsg,
-    "firstpaint": this._fireEventFromMsg,
-    "documentfirstpaint": this._fireEventFromMsg,
-    "nextpaint": this._recvNextPaint,
-    "keyevent": this._fireKeyEvent,
-    "showmodalprompt": this._handleShowModalPrompt,
-    "got-purge-history": this._gotDOMRequestResult,
-    "got-screenshot": this._gotDOMRequestResult,
-    "got-can-go-back": this._gotDOMRequestResult,
-    "got-can-go-forward": this._gotDOMRequestResult,
-    "fullscreen-origin-change": this._remoteFullscreenOriginChange,
-    "rollback-fullscreen": this._remoteFrameFullscreenReverted,
-    "exit-fullscreen": this._exitFullscreen,
-    "got-visible": this._gotDOMRequestResult,
-    "visibilitychange": this._childVisibilityChange,
-    "got-set-input-method-active": this._gotDOMRequestResult
-  }
-
-  this._mm.addMessageListener('browser-element-api:call', function(aMsg) {
-    if (self._isAlive() && (aMsg.data.msg_name in mmCalls)) {
-      return mmCalls[aMsg.data.msg_name].apply(self, arguments);
-    }
-  });
-
   Services.obs.addObserver(this, 'ask-children-to-exit-fullscreen', /* ownsWeak = */ true);
   Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true);
 
   let defineMethod = function(name, fn) {
     XPCNativeWrapper.unwrap(self._frameElement)[name] = function() {
       if (self._isAlive()) {
         return fn.apply(self, arguments);
       }
     };
   }
 
+  let defineNoReturnMethod = function(name, fn) {
+    XPCNativeWrapper.unwrap(self._frameElement)[name] = function method() {
+      if (!self._mm) {
+        // Remote browser haven't been created, we just queue the API call.
+        let args = Array.slice(arguments);
+        args.unshift(self);
+        self._pendingAPICalls.push(method.bind.apply(fn, args));
+        return;
+      }
+      if (self._isAlive()) {
+        fn.apply(self, arguments);
+      }
+    };
+  };
+
   let defineDOMRequestMethod = function(domName, msgName) {
     XPCNativeWrapper.unwrap(self._frameElement)[domName] = function() {
+      if (!self._mm) {
+        return self._queueDOMRequest;
+      }
       if (self._isAlive()) {
         return self._sendDOMRequest(msgName);
       }
     };
   }
 
   // Define methods on the frame element.
-  defineMethod('setVisible', this._setVisible);
+  defineNoReturnMethod('setVisible', this._setVisible);
   defineDOMRequestMethod('getVisible', 'get-visible');
-  defineMethod('sendMouseEvent', this._sendMouseEvent);
+  defineNoReturnMethod('sendMouseEvent', this._sendMouseEvent);
 
   // 0 = disabled, 1 = enabled, 2 - auto detect
   if (getIntPref(TOUCH_EVENTS_ENABLED_PREF, 0) != 0) {
-    defineMethod('sendTouchEvent', this._sendTouchEvent);
+    defineNoReturnMethod('sendTouchEvent', this._sendTouchEvent);
   }
-  defineMethod('goBack', this._goBack);
-  defineMethod('goForward', this._goForward);
-  defineMethod('reload', this._reload);
-  defineMethod('stop', this._stop);
-  defineMethod('purgeHistory', this._purgeHistory);
+  defineNoReturnMethod('goBack', this._goBack);
+  defineNoReturnMethod('goForward', this._goForward);
+  defineNoReturnMethod('reload', this._reload);
+  defineNoReturnMethod('stop', this._stop);
+  defineDOMRequestMethod('purgeHistory', 'purge-history');
   defineMethod('getScreenshot', this._getScreenshot);
   defineMethod('addNextPaintListener', this._addNextPaintListener);
   defineMethod('removeNextPaintListener', this._removeNextPaintListener);
   defineDOMRequestMethod('getCanGoBack', 'get-can-go-back');
   defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward');
 
   let principal = this._frameElement.ownerDocument.nodePrincipal;
   let perm = Services.perms
@@ -218,35 +192,108 @@ function BrowserElementParent(frameLoade
                                   /* useCapture = */ false,
                                   /* wantsUntrusted = */ false);
   }
 
   this._window._browserElementParents.set(this, null);
 
   // Insert ourself into the prompt service.
   BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this);
-
-  // If this browser represents an app then let the Webapps module register for
-  // any messages that it needs.
-  let appManifestURL =
-    this._frameElement.QueryInterface(Ci.nsIMozBrowserFrame).appManifestURL;
-  if (appManifestURL) {
-    let appId =
-      DOMApplicationRegistry.getAppLocalIdByManifestURL(appManifestURL);
-    if (appId != Ci.nsIScriptSecurityManager.NO_APP_ID) {
-      DOMApplicationRegistry.registerBrowserElementParentForApp(this, appId);
-    }
+  if (!isPendingFrame) {
+    this._setupMessageListener();
+    this._registerAppManifest();
+  } else {
+    // if we are a pending frame, we setup message manager after
+    // observing remote-browser-frame-shown
+    this._pendingAPICalls = [];
+    Services.obs.addObserver(this, 'remote-browser-frame-shown', /* ownsWeak = */ true);
   }
 }
 
 BrowserElementParent.prototype = {
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
+  _runPendingAPICall: function() {
+    if (!this._pendingAPICalls) {
+      return;
+    }
+    for (let i = 0; i < this._pendingAPICalls.length; i++) {
+      try {
+        this._pendingAPICalls[i]();
+      } catch (e) {
+        // throw the expections from pending functions.
+        debug('Exception when running pending API call: ' +  e);
+      }
+    }
+    delete this._pendingAPICalls;
+  },
+
+  _registerAppManifest: function() {
+    // If this browser represents an app then let the Webapps module register for
+    // any messages that it needs.
+    let appManifestURL =
+          this._frameElement.QueryInterface(Ci.nsIMozBrowserFrame).appManifestURL;
+    if (appManifestURL) {
+      let appId =
+            DOMApplicationRegistry.getAppLocalIdByManifestURL(appManifestURL);
+      if (appId != Ci.nsIScriptSecurityManager.NO_APP_ID) {
+        DOMApplicationRegistry.registerBrowserElementParentForApp(this, appId);
+      }
+    }
+  },
+
+  _setupMessageListener: function() {
+    this._mm = this._frameLoader.messageManager;
+    let self = this;
+
+    // Messages we receive are handed to functions which take a (data) argument,
+    // where |data| is the message manager's data object.
+    // We use a single message and dispatch to various function based
+    // on data.msg_name
+    let mmCalls = {
+      "hello": this._recvHello,
+      "contextmenu": this._fireCtxMenuEvent,
+      "locationchange": this._fireEventFromMsg,
+      "loadstart": this._fireEventFromMsg,
+      "loadend": this._fireEventFromMsg,
+      "titlechange": this._fireEventFromMsg,
+      "iconchange": this._fireEventFromMsg,
+      "close": this._fireEventFromMsg,
+      "resize": this._fireEventFromMsg,
+      "activitydone": this._fireEventFromMsg,
+      "opensearch": this._fireEventFromMsg,
+      "securitychange": this._fireEventFromMsg,
+      "error": this._fireEventFromMsg,
+      "scroll": this._fireEventFromMsg,
+      "firstpaint": this._fireEventFromMsg,
+      "documentfirstpaint": this._fireEventFromMsg,
+      "nextpaint": this._recvNextPaint,
+      "keyevent": this._fireKeyEvent,
+      "showmodalprompt": this._handleShowModalPrompt,
+      "got-purge-history": this._gotDOMRequestResult,
+      "got-screenshot": this._gotDOMRequestResult,
+      "got-can-go-back": this._gotDOMRequestResult,
+      "got-can-go-forward": this._gotDOMRequestResult,
+      "fullscreen-origin-change": this._remoteFullscreenOriginChange,
+      "rollback-fullscreen": this._remoteFrameFullscreenReverted,
+      "exit-fullscreen": this._exitFullscreen,
+      "got-visible": this._gotDOMRequestResult,
+      "visibilitychange": this._childVisibilityChange,
+      "got-set-input-method-active": this._gotDOMRequestResult
+    };
+
+    this._mm.addMessageListener('browser-element-api:call', function(aMsg) {
+      if (self._isAlive() && (aMsg.data.msg_name in mmCalls)) {
+        return mmCalls[aMsg.data.msg_name].apply(self, arguments);
+      }
+    });
+  },
+
   /**
    * You shouldn't touch this._frameElement or this._window if _isAlive is
    * false.  (You'll likely get an exception if you do.)
    */
   _isAlive: function() {
     return !Cu.isDeadWrapper(this._frameElement) &&
            !Cu.isDeadWrapper(this._frameElement.ownerDocument) &&
            !Cu.isDeadWrapper(this._frameElement.ownerDocument.defaultView);
@@ -440,16 +487,42 @@ BrowserElementParent.prototype = {
     }
 
     return new this._window.Event('mozbrowser' + evtName,
                                   { bubbles: true,
                                     cancelable: cancelable });
   },
 
   /**
+   * If remote frame haven't been set up, we enqueue a function that get a
+   * DOMRequest until the remote frame is ready and return another DOMRequest
+   * to caller. When we get the real DOMRequest, we will help forward the
+   * success/error callback to the DOMRequest that caller got.
+   */
+  _queueDOMRequest: function(msgName, args) {
+    if (!this._pendingAPICalls) {
+      return;
+    }
+
+    let req = Services.DOMRequest.createRequest(this._window);
+    let self = this;
+    let getRealDOMRequest = function() {
+      let realReq = self._sendDOMRequest(msgName, args);
+      realReq.onsuccess = function(v) {
+        Services.DOMRequest.fireSuccess(req, v);
+      };
+      realReq.onerror = function(v) {
+        Services.DOMRequest.fireError(req, v);
+      };
+    };
+    this._pendingAPICalls.push(getRealDOMRequest);
+    return req;
+  },
+
+  /**
    * Kick off a DOMRequest in the child process.
    *
    * We'll fire an event called |msgName| on the child process, passing along
    * an object with two fields:
    *
    *  - id:  the ID of this request.
    *  - arg: arguments to pass to the child along with this request.
    *
@@ -538,28 +611,31 @@ BrowserElementParent.prototype = {
   _reload: function(hardReload) {
     this._sendAsyncMsg('reload', {hardReload: hardReload});
   },
 
   _stop: function() {
     this._sendAsyncMsg('stop');
   },
 
-  _purgeHistory: function() {
-    return this._sendDOMRequest('purge-history');
-  },
-
   _getScreenshot: function(_width, _height) {
     let width = parseInt(_width);
     let height = parseInt(_height);
     if (isNaN(width) || isNaN(height) || width < 0 || height < 0) {
       throw Components.Exception("Invalid argument",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
 
+    if (!this._mm) {
+      // Child haven't been loaded.
+      return this._queueDOMRequest('get-screenshot',
+                                   {width: width, height: height,
+                                    mimeType: mimeType});
+    }
+
     return this._sendDOMRequest('get-screenshot',
                                 {width: width, height: height});
   },
 
   _recvNextPaint: function(data) {
     let listeners = this._nextPaintListeners;
     this._nextPaintListeners = [];
     for (let listener of listeners) {
@@ -570,33 +646,49 @@ BrowserElementParent.prototype = {
       }
     }
   },
 
   _addNextPaintListener: function(listener) {
     if (typeof listener != 'function')
       throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
 
-    if (this._nextPaintListeners.push(listener) == 1)
-      this._sendAsyncMsg('activate-next-paint-listener');
+    let self = this;
+    let run = function() {
+      if (self._nextPaintListeners.push(listener) == 1)
+        self._sendAsyncMsg('activate-next-paint-listener');
+    };
+    if (!this._mm) {
+      this._pendingAPICalls.push(run);
+    } else {
+      run();
+    }
   },
 
   _removeNextPaintListener: function(listener) {
     if (typeof listener != 'function')
       throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
 
-    for (let i = this._nextPaintListeners.length - 1; i >= 0; i--) {
-      if (this._nextPaintListeners[i] == listener) {
-        this._nextPaintListeners.splice(i, 1);
-        break;
+    let self = this;
+    let run = function() {
+      for (let i = self._nextPaintListeners.length - 1; i >= 0; i--) {
+        if (self._nextPaintListeners[i] == listener) {
+          self._nextPaintListeners.splice(i, 1);
+          break;
+        }
       }
+
+      if (self._nextPaintListeners.length == 0)
+        self._sendAsyncMsg('deactivate-next-paint-listener');
+    };
+    if (!this._mm) {
+      this._pendingAPICalls.push(run);
+    } else {
+      run();
     }
-
-    if (this._nextPaintListeners.length == 0)
-      this._sendAsyncMsg('deactivate-next-paint-listener');
   },
 
   _setInputMethodActive: function(isActive) {
     if (typeof isActive !== 'boolean') {
       throw Components.Exception("Invalid argument",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
 
@@ -696,14 +788,23 @@ BrowserElementParent.prototype = {
       break;
     case 'ask-children-to-exit-fullscreen':
       if (this._isAlive() &&
           this._frameElement.ownerDocument == subject &&
           this._hasRemoteFrame) {
         this._sendAsyncMsg('exit-fullscreen');
       }
       break;
+    case 'remote-browser-frame-shown':
+      if (this._frameLoader == subject) {
+        if (!this._mm) {
+          this._setupMessageListener();
+          this._registerAppManifest();
+          this._runPendingAPICall();
+        }
+        Services.obs.removeObserver(this, 'remote-browser-frame-shown');
+      }
     default:
       debug('Unknown topic: ' + topic);
       break;
     };
   },
 };