Bug 1467223 - Part 5: Perform parent-process interception for HTTP loads, r=qdot,valentin
authorNika Layzell <nika@thelayzells.com>
Wed, 23 Jan 2019 21:07:10 +0000
changeset 515205 ae82e6f22e29d37c2e28280bb5192b200c997688
parent 515204 47630659e925fe8ecca4e153e0a2cb3b13d292cf
child 515206 909797e624a8551662268623ef60b11531f1a4d7
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersqdot, valentin
bugs1467223
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1467223 - Part 5: Perform parent-process interception for HTTP loads, r=qdot,valentin This will only happen if the pref is enabled, and works through the existing mechanism for process switching loads. It should enable POST data to be preserved when performing a process switch, for example when submitting a form on a file:// or moz-extension:// URI to a http:// URI. Depends on D15611 Differential Revision: https://phabricator.services.mozilla.com/D15612
browser/components/sessionstore/ContentRestore.jsm
browser/components/sessionstore/SessionStore.jsm
toolkit/modules/E10SUtils.jsm
--- a/browser/components/sessionstore/ContentRestore.jsm
+++ b/browser/components/sessionstore/ContentRestore.jsm
@@ -181,16 +181,23 @@ ContentRestoreInternal.prototype = {
     // load happens. Don't bother doing this if we're restoring immediately
     // due to a process switch.
     if (!isRemotenessUpdate) {
       webNavigation.setCurrentURI(Services.io.newURI("about:blank"));
     }
 
     try {
       if (loadArguments) {
+        // If the load was started in another process, and the in-flight channel
+        // was redirected into this process, resume that load within our process.
+        if (loadArguments.redirectLoadSwitchId) {
+          webNavigation.resumeRedirectedLoad(loadArguments.redirectLoadSwitchId);
+          return true;
+        }
+
         // A load has been redirected to a new process so get history into the
         // same state it was before the load started then trigger the load.
         let referrer = loadArguments.referrer ?
                        Services.io.newURI(loadArguments.referrer) : null;
         let referrerPolicy = ("referrerPolicy" in loadArguments
             ? loadArguments.referrerPolicy
             : Ci.nsIHttpChannel.REFERRER_POLICY_UNSET);
         let postData = loadArguments.postData ?
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -43,16 +43,19 @@ const SCREEN_EDGE_SLOP = 8;
 
 // global notifications observed
 const OBSERVING = [
   "browser-window-before-show", "domwindowclosed",
   "quit-application-granted", "browser-lastwindow-close-granted",
   "quit-application", "browser:purge-session-history",
   "browser:purge-session-history-for-domain",
   "idle-daily", "clear-origin-attributes-data",
+  "http-on-examine-response",
+  "http-on-examine-merged-response",
+  "http-on-examine-cached-response",
 ];
 
 // XUL Window properties to (re)store
 // Restored in restoreDimensions()
 const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
 
 // Hideable window features to (re)store
 // Restored in restoreWindowFeatures()
@@ -166,16 +169,17 @@ XPCOMUtils.defineLazyServiceGetters(this
   Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"],
 });
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
+  E10SUtils: "resource://gre/modules/E10SUtils.jsm",
   GlobalState: "resource:///modules/sessionstore/GlobalState.jsm",
   HomePage: "resource:///modules/HomePage.jsm",
   PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
   PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
   RunState: "resource:///modules/sessionstore/RunState.jsm",
   SessionCookies: "resource:///modules/sessionstore/SessionCookies.jsm",
   SessionFile: "resource:///modules/sessionstore/SessionFile.jsm",
   SessionSaver: "resource:///modules/sessionstore/SessionSaver.jsm",
@@ -443,16 +447,19 @@ var SessionStoreInternal = {
     Ci.nsISupportsWeakReference,
   ]),
 
   _globalState: new GlobalState(),
 
   // A counter to be used to generate a unique ID for each closed tab or window.
   _nextClosedId: 0,
 
+  // A monotonic value used to generate a unique ID for each process switch.
+  _switchIdMonotonic: 0,
+
   // During the initial restore and setBrowserState calls tracks the number of
   // windows yet to be restored
   _restoreCount: -1,
 
   // For each <browser> element, records the current epoch.
   _browserEpochs: new WeakMap(),
 
   // Any browsers that fires the oop-browser-crashed event gets stored in
@@ -798,18 +805,25 @@ var SessionStoreInternal = {
         this.onIdleDaily();
         this._notifyOfClosedObjectsChange();
         break;
       case "clear-origin-attributes-data":
         let userContextId = 0;
         try {
           userContextId = JSON.parse(aData).userContextId;
         } catch (e) {}
-        if (userContextId)
+        if (userContextId) {
           this._forgetTabsWithUserContextId(userContextId);
+        }
+        break;
+      case "http-on-examine-response":
+      case "http-on-examine-cached-response":
+      case "http-on-examine-merged-response":
+        this.onExamineResponse(aSubject);
+        break;
     }
   },
 
   /**
    * This method handles incoming messages sent by the session store content
    * script via the Frame Message Manager or Parent Process Message Manager,
    * and thus enables communication with OOP tabs.
    */
@@ -2261,16 +2275,114 @@ var SessionStoreInternal = {
         if (now - data.closedAt > TIME_TO_LIVE) {
           array.splice(i, 1);
           this._closedObjectsChanged = true;
         }
       }
     }
   },
 
+  /**
+   * Perform a destructive process switch into a distinct process.
+   * This method is asynchronous, as it requires multiple calls into content
+   * processes.
+   */
+  async _doProcessSwitch(aBrowser, aRemoteType, aChannel, aSwitchId) {
+    // Don't try to switch tabs before delayed startup is completed.
+    await aBrowser.ownerGlobal.delayedStartupPromise;
+
+    // Perform a navigateAndRestore to trigger the process switch.
+    let tab = aBrowser.ownerGlobal.gBrowser.getTabForBrowser(aBrowser);
+    let loadArguments = {
+      newFrameloader: true,  // Switch even if remoteType hasn't changed.
+      remoteType: aRemoteType,  // Don't derive remoteType to switch to.
+
+      // Information about which channel should be performing the load.
+      redirectLoadSwitchId: aSwitchId,
+    };
+
+    await SessionStore.navigateAndRestore(tab, loadArguments, -1);
+
+    // If the process switch seems to have failed, send an error over to our
+    // caller, to give it a chance to kill our channel.
+    if (aBrowser.remoteType != aRemoteType ||
+        !aBrowser.frameLoader || !aBrowser.frameLoader.tabParent) {
+      throw Cr.NS_ERROR_FAILURE;
+    }
+
+    // Tell our caller to redirect the load into this newly created process.
+    return aBrowser.frameLoader.tabParent;
+  },
+
+  // Examine the channel response to see if we should change the process
+  // performing the given load.
+  onExamineResponse(aChannel) {
+    if (!E10SUtils.useHttpResponseProcessSelection()) {
+      return;
+    }
+
+    if (!aChannel.isDocument || !aChannel.loadInfo) {
+      return; // Not a document load.
+    }
+
+    let browsingContext = aChannel.loadInfo.browsingContext;
+    if (!browsingContext) {
+      return; // Not loading in a browsing context.
+    }
+
+    if (browsingContext.parent) {
+      return; // Not a toplevel load, can't flip procs.
+    }
+
+    // Get principal for a document already loaded in the BrowsingContext.
+    let currentPrincipal = null;
+    if (browsingContext.currentWindowGlobal) {
+      currentPrincipal = browsingContext.currentWindowGlobal.documentPrincipal;
+    }
+
+    let parentChannel = aChannel.notificationCallbacks
+                                .getInterface(Ci.nsIParentChannel);
+    if (!parentChannel) {
+      return; // Not an actor channel
+    }
+
+    let tabParent = parentChannel.QueryInterface(Ci.nsIInterfaceRequestor)
+                                 .getInterface(Ci.nsITabParent);
+    if (!tabParent || !tabParent.ownerElement) {
+      console.warn("warning: Missing tabParent");
+      return; // Not an embedded browsing context
+    }
+
+    let browser = tabParent.ownerElement;
+    if (browser.tagName !== "browser") {
+      console.warn("warning: Not a xul:browser element:", browser.tagName);
+      return; // Not a vanilla xul:browser element performing embedding.
+    }
+
+    let resultPrincipal =
+      Services.scriptSecurityManager.getChannelResultPrincipal(aChannel);
+    let useRemoteTabs = browser.ownerGlobal.gMultiProcessBrowser;
+    let remoteType = E10SUtils.getRemoteTypeForPrincipal(resultPrincipal,
+                                                         useRemoteTabs,
+                                                         browser.remoteType,
+                                                         currentPrincipal);
+    if (browser.remoteType == remoteType) {
+      return; // Already in compatible process.
+    }
+
+    // ------------------------------------------------------------------------
+    // DANGER ZONE: Perform a process switch into the new process. This is
+    // destructive.
+    // ------------------------------------------------------------------------
+    let identifier = ++this._switchIdMonotonic;
+    let tabPromise = this._doProcessSwitch(browser, remoteType,
+                                           aChannel, identifier);
+    aChannel.switchProcessTo(tabPromise, identifier);
+  },
+
   /* ........ nsISessionStore API .............. */
 
   getBrowserState: function ssi_getBrowserState() {
     let state = this.getCurrentState();
 
     // Don't include the last session state in getBrowserState().
     delete state.lastSessionState;
 
@@ -2998,42 +3110,51 @@ var SessionStoreInternal = {
    * or restoring the exact same state again and passing the new URL to load
    * in |loadArguments|. Use this method to seamlessly switch between pages
    * loaded in the parent and pages loaded in the child process.
    *
    * This method might be called multiple times before it has finished
    * flushing the browser tab. If that occurs, the loadArguments from
    * the most recent call to navigateAndRestore will be used once the
    * flush has finished.
+   *
+   * This method returns a promise which will be resolved when the browser
+   * element's process has been swapped. The load is not guaranteed to have
+   * been completed at this point.
    */
   navigateAndRestore(tab, loadArguments, historyIndex) {
     let window = tab.ownerGlobal;
 
     if (!window.__SSi) {
       Cu.reportError("Tab's window must be tracked.");
-      return;
+      return Promise.reject();
     }
 
     let browser = tab.linkedBrowser;
 
-    // Were we already waiting for a flush from a previous call to
-    // navigateAndRestore on this tab?
-    let alreadyRestoring =
-      this._remotenessChangingBrowsers.has(browser.permanentKey);
-
-    // Stash the most recent loadArguments in this WeakMap so that
-    // we know to use it when the TabStateFlusher.flush resolves.
-    this._remotenessChangingBrowsers.set(browser.permanentKey, loadArguments);
-
-    if (alreadyRestoring) {
-      // This tab was already being restored to run in the
-      // correct process. We're done here.
-      return;
-    }
-
+    // If we were alerady waiting for a flush from a previous call to
+    // navigateAndRestore on this tab, update the loadArguments stored, and
+    // asynchronously wait on the flush's promise.
+    if (this._remotenessChangingBrowsers.has(browser.permanentKey)) {
+      let opts = this._remotenessChangingBrowsers.get(browser.permanentKey);
+      // XXX(nika): In the existing logic, we always use the initial
+      // historyIndex value, and don't update it if multiple navigateAndRestore
+      // calls are made. Should we update it here?
+      opts.loadArguments = loadArguments;
+      return opts.promise;
+    }
+
+    // Begin the asynchronous NavigateAndRestore process, and store the current
+    // load arguments and promise in our _remotenessChangingBrowsers weakmap.
+    let promise = this._asyncNavigateAndRestore(tab);
+    this._remotenessChangingBrowsers.set(
+      browser.permanentKey, {loadArguments, historyIndex, promise});
+
+    // Set up the browser UI to look like we're doing something while waiting
+    // for a TabStateFlush from our frame scripts.
     let uriObj;
     try {
       uriObj = Services.io.newURI(loadArguments.uri);
     } catch (e) {}
 
     // Start the throbber to pretend we're doing something while actually
     // waiting for data from the frame script. This throbber is disabled
     // if the URI is a local about: URI.
@@ -3041,70 +3162,84 @@ var SessionStoreInternal = {
       tab.setAttribute("busy", "true");
     }
 
     // Hack to ensure that the about:home, about:newtab, and about:welcome
     // favicon is loaded instantaneously, to avoid flickering and improve
     // perceived performance.
     window.gBrowser.setDefaultIcon(tab, uriObj);
 
-    // Flush to get the latest tab state.
-    TabStateFlusher.flush(browser).then(() => {
-      // loadArguments might have been overwritten by multiple calls
-      // to navigateAndRestore while we waited for the tab to flush,
-      // so we use the most recently stored one.
-      let recentLoadArguments =
-        this._remotenessChangingBrowsers.get(browser.permanentKey);
-      this._remotenessChangingBrowsers.delete(browser.permanentKey);
-
-      // The tab might have been closed/gone in the meantime.
-      if (tab.closing || !tab.linkedBrowser) {
-        return;
-      }
-
-      let refreshedWindow = tab.ownerGlobal;
-
-      // The tab or its window might be gone.
-      if (!refreshedWindow || !refreshedWindow.__SSi || refreshedWindow.closed) {
-        return;
-      }
-
-      let tabState = TabState.clone(tab, TAB_CUSTOM_VALUES.get(tab));
-      let options = {
-        restoreImmediately: true,
-        // We want to make sure that this information is passed to restoreTab
-        // whether or not a historyIndex is passed in. Thus, we extract it from
-        // the loadArguments.
-        newFrameloader: recentLoadArguments.newFrameloader,
-        remoteType: recentLoadArguments.remoteType,
-        // Make sure that SessionStore knows that this restoration is due
-        // to a navigation, as opposed to us restoring a closed window or tab.
-        restoreContentReason: RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE,
-      };
-
-      if (historyIndex >= 0) {
-        tabState.index = historyIndex + 1;
-        tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
-      } else {
-        options.loadArguments = recentLoadArguments;
-      }
-
-      // Need to reset restoring tabs.
-      if (TAB_STATE_FOR_BROWSER.has(tab.linkedBrowser)) {
-        this._resetLocalTabRestoringState(tab);
-      }
-
-      // Restore the state into the tab.
-      this.restoreTab(tab, tabState, options);
-    });
-
     TAB_STATE_FOR_BROWSER.set(tab.linkedBrowser, TAB_STATE_WILL_RESTORE);
 
     // Notify of changes to closed objects.
     this._notifyOfClosedObjectsChange();
+
+    return promise;
+  },
+
+  /**
+   * Internal logic called by navigateAndRestore to flush tab state, and
+   * trigger a remoteness changing load with the most recent load arguments.
+   *
+   * This method's promise will resolve when the process for the given
+   * xul:browser element has successfully been swapped.
+   *
+   * @param tab to navigate and restore.
+   */
+  async _asyncNavigateAndRestore(tab) {
+    let initialBrowser = tab.linkedBrowser;
+    // NOTE: This is currently the only async operation used, but this is likely
+    // to change in the future.
+    await TabStateFlusher.flush(initialBrowser);
+
+    // Now that we have flushed state, our loadArguments, etc. may have been
+    // overwritten by multiple calls to navigateAndRestore. Load the most
+    // recently stored one.
+    let {loadArguments, historyIndex} =
+      this._remotenessChangingBrowsers.get(initialBrowser.permanentKey);
+    this._remotenessChangingBrowsers.delete(initialBrowser.permanentKey);
+
+    // The tab might have been closed/gone in the meantime.
+    if (tab.closing || !tab.linkedBrowser) {
+      return;
+    }
+
+    // The tab or its window might be gone.
+    let window = tab.ownerGlobal;
+    if (!window || !window.__SSi || window.closed) {
+      return;
+    }
+
+    let tabState = TabState.clone(tab, TAB_CUSTOM_VALUES.get(tab));
+    let options = {
+      restoreImmediately: true,
+      // We want to make sure that this information is passed to restoreTab
+      // whether or not a historyIndex is passed in. Thus, we extract it from
+      // the loadArguments.
+      newFrameloader: loadArguments.newFrameloader,
+      remoteType: loadArguments.remoteType,
+      // Make sure that SessionStore knows that this restoration is due
+      // to a navigation, as opposed to us restoring a closed window or tab.
+      restoreContentReason: RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE,
+    };
+
+    if (historyIndex >= 0) {
+      tabState.index = historyIndex + 1;
+      tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
+    } else {
+      options.loadArguments = loadArguments;
+    }
+
+    // Need to reset restoring tabs.
+    if (TAB_STATE_FOR_BROWSER.has(tab.linkedBrowser)) {
+      this._resetLocalTabRestoringState(tab);
+    }
+
+    // Restore the state into the tab.
+    this.restoreTab(tab, tabState, options);
   },
 
   /**
    * Retrieves the latest session history information for a tab. The cached data
    * is returned immediately, but a callback may be provided that supplies
    * up-to-date data when or if it is available. The callback is passed a single
    * argument with data in the same format as the return value.
    *
--- a/toolkit/modules/E10SUtils.jsm
+++ b/toolkit/modules/E10SUtils.jsm
@@ -10,16 +10,18 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "useSeparateFileUriProcess",
                                       "browser.tabs.remote.separateFileUriProcess", false);
 XPCOMUtils.defineLazyPreferenceGetter(this, "allowLinkedWebInFileUriProcess",
                                       "browser.tabs.remote.allowLinkedWebInFileUriProcess", false);
 XPCOMUtils.defineLazyPreferenceGetter(this, "useSeparatePrivilegedContentProcess",
                                       "browser.tabs.remote.separatePrivilegedContentProcess", false);
+XPCOMUtils.defineLazyPreferenceGetter(this, "useHttpResponseProcessSelection",
+                                      "browser.tabs.remote.useHTTPResponseProcessSelection", false);
 ChromeUtils.defineModuleGetter(this, "Utils",
                                "resource://gre/modules/sessionstore/Utils.jsm");
 
 function getAboutModule(aURL) {
   // Needs to match NS_GetAboutModuleName
   let moduleName = aURL.pathQueryRef.replace(/[#?].*/, "").toLowerCase();
   let contract = "@mozilla.org/network/protocol/about;1?what=" + moduleName;
   try {
@@ -85,16 +87,20 @@ var E10SUtils = {
   DEFAULT_REMOTE_TYPE,
   NOT_REMOTE,
   WEB_REMOTE_TYPE,
   FILE_REMOTE_TYPE,
   EXTENSION_REMOTE_TYPE,
   PRIVILEGED_REMOTE_TYPE,
   LARGE_ALLOCATION_REMOTE_TYPE,
 
+  useHttpResponseProcessSelection() {
+    return useHttpResponseProcessSelection;
+  },
+
   canLoadURIInRemoteType(aURL, aRemoteType = DEFAULT_REMOTE_TYPE) {
     // We need a strict equality here because the value of `NOT_REMOTE` is
     // `null`, and there is a possibility that `undefined` is passed as the
     // second argument, which might result a load in the parent process.
     let preferredRemoteType = aRemoteType === NOT_REMOTE
       ? NOT_REMOTE
       : DEFAULT_REMOTE_TYPE;
     return aRemoteType == this.getRemoteTypeForURI(aURL, true, preferredRemoteType);
@@ -221,16 +227,46 @@ var E10SUtils = {
                                                 aPreferredRemoteType,
                                                 aCurrentUri);
         }
 
         return validatedWebRemoteType(aPreferredRemoteType, aURI, aCurrentUri);
     }
   },
 
+  getRemoteTypeForPrincipal(aPrincipal, aMultiProcess,
+                            aPreferredRemoteType = DEFAULT_REMOTE_TYPE,
+                            aCurrentPrincipal) {
+    if (!aMultiProcess) {
+      return NOT_REMOTE;
+    }
+
+    // We can't pick a process based on a system principal or expanded
+    // principal. In fact, we should never end up with one here!
+    if (aPrincipal.isSystemPrincipal || aPrincipal.isExpandedPrincipal) {
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+
+    // Null principals can be loaded in any remote process.
+    if (aPrincipal.isNullPrincipal) {
+      return aPreferredRemoteType == NOT_REMOTE ? DEFAULT_REMOTE_TYPE
+                                                : aPreferredRemoteType;
+    }
+
+    // We might care about the currently loaded URI. Pull it out of our current
+    // principal. We never care about the current URI when working with a
+    // non-codebase principal.
+    let currentURI = (aCurrentPrincipal && aCurrentPrincipal.isCodebasePrincipal)
+                     ? aCurrentPrincipal.URI : null;
+    return E10SUtils.getRemoteTypeForURIObject(aPrincipal.URI,
+                                               aMultiProcess,
+                                               aPreferredRemoteType,
+                                               currentURI);
+  },
+
   shouldLoadURIInBrowser(browser, uri, multiProcess = true,
                          flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE) {
     let currentRemoteType = browser.remoteType;
     let requiredRemoteType;
     let uriObject;
     try {
       let fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE;
       if (flags & Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) {
@@ -271,18 +307,31 @@ var E10SUtils = {
 
   shouldLoadURIInThisProcess(aURI) {
     let remoteType = Services.appinfo.remoteType;
     return remoteType == this.getRemoteTypeForURIObject(aURI, true, remoteType);
   },
 
   shouldLoadURI(aDocShell, aURI, aReferrer, aHasPostData) {
     // Inner frames should always load in the current process
-    if (aDocShell.sameTypeParent)
+    if (aDocShell.sameTypeParent) {
       return true;
+    }
+
+    // If we are performing HTTP response process selection, and are loading an
+    // HTTP URI, we can start the load in the current process, and then perform
+    // the switch later-on using the RedirectProcessChooser mechanism.
+    //
+    // We should never be sending a POST request from the parent process to a
+    // http(s) uri, so make sure we switch if we're currently in that process.
+    if (useHttpResponseProcessSelection &&
+        (aURI.scheme == "http" || aURI.scheme == "https") &&
+        Services.appinfo.remoteType != NOT_REMOTE) {
+      return true;
+    }
 
     // If we are in a Large-Allocation process, and it wouldn't be content visible
     // to change processes, we want to load into a new process so that we can throw
     // this one out. We don't want to move into a new process if we have post data,
     // because we would accidentally throw out that data.
     if (!aHasPostData &&
         Services.appinfo.remoteType == LARGE_ALLOCATION_REMOTE_TYPE &&
         !aDocShell.awaitingLargeAlloc &&