Bug 1333990: Part 3b - Preload matching content scripts when opening document channels. r?aswan,billm draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 10 Mar 2017 21:01:52 -0800
changeset 499662 e49b2ce255f8f030d952a644cdecf8108c2a8806
parent 499661 eeb143a3afed297131c3fc4fab49a37a54076959
child 499663 e860bd3996c8e8573df10895064d65df712a2605
push id49469
push usermaglione.k@gmail.com
push dateThu, 16 Mar 2017 02:25:47 +0000
reviewersaswan, billm
bugs1333990
milestone54.0a1
Bug 1333990: Part 3b - Preload matching content scripts when opening document channels. r?aswan,billm This uses the http-on-opening-request observer that's dispatched in the child process to begin preloading matching content scripts as early in the load cycle as possible. Ideally we would use the network predictor for this, but most of its prediction work happens in the parent process, and there are no simple ways for us to hook into it. This currently does not do any pre-loading in the parent process, mainly because there isn't a good way to distinguish top-level document loads that are happening directly in the parent verses those that are being proxied from the child. MozReview-Commit-ID: dIQW68HtxZ
toolkit/components/extensions/ExtensionContent.jsm
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -154,16 +154,50 @@ Script.prototype = {
     if (this.options.cssCode) {
       let url = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
       urls.push(url);
     }
 
     return urls;
   },
 
+  matchesLoadInfo(uri, loadInfo) {
+    if (!this.matchesURI(uri)) {
+      return false;
+    }
+
+    if (!this.options.all_frames && !loadInfo.isTopLevelLoad) {
+      return false;
+    }
+
+    return true;
+  },
+
+  matchesURI(uri) {
+    if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) {
+      return false;
+    }
+
+    if (this.exclude_matches_.matches(uri)) {
+      return false;
+    }
+
+    if (this.options.include_globs != null) {
+      if (!this.include_globs_.matches(uri.spec)) {
+        return false;
+      }
+    }
+
+    if (this.exclude_globs_.matches(uri.spec)) {
+      return false;
+    }
+
+    return true;
+  },
+
   matches(window) {
     let uri = window.document.documentURIObject;
     let principal = window.document.nodePrincipal;
 
     // If mozAddonManager is present on this page, don't allow
     // content scripts.
     if (window.navigator.mozAddonManager !== undefined) {
       return false;
@@ -186,31 +220,17 @@ Script.prototype = {
     // Documents from data: URIs also inherit the principal.
     if (Services.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) {
       if (!this.match_about_blank) {
         return false;
       }
       uri = principal.URI;
     }
 
-    if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) {
-      return false;
-    }
-
-    if (this.exclude_matches_.matches(uri)) {
-      return false;
-    }
-
-    if (this.options.include_globs != null) {
-      if (!this.include_globs_.matches(uri.spec)) {
-        return false;
-      }
-    }
-
-    if (this.exclude_globs_.matches(uri.spec)) {
+    if (!this.matchesURI(uri)) {
       return false;
     }
 
     if (this.options.frame_id != null) {
       if (WebNavigationFrames.getFrameId(window) != this.options.frame_id) {
         return false;
       }
     } else if (!this.options.all_frames && window.top != window) {
@@ -506,22 +526,28 @@ DocumentManager = {
 
   // Map[windowId -> Map[extensionId -> ContentScriptContextChild]]
   contentScriptWindows: new Map(),
 
   // Map[windowId -> ContentScriptContextChild]
   extensionPageWindows: new Map(),
 
   init() {
+    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+      Services.obs.addObserver(this, "http-on-opening-request", false);
+    }
     Services.obs.addObserver(this, "content-document-global-created", false);
     Services.obs.addObserver(this, "document-element-inserted", false);
     Services.obs.addObserver(this, "inner-window-destroyed", false);
   },
 
   uninit() {
+    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+      Services.obs.removeObserver(this, "http-on-opening-request");
+    }
     Services.obs.removeObserver(this, "content-document-global-created");
     Services.obs.removeObserver(this, "document-element-inserted");
     Services.obs.removeObserver(this, "inner-window-destroyed");
   },
 
   getWindowState(contentWindow) {
     let readyState = contentWindow.document.readyState;
     if (readyState == "complete") {
@@ -551,17 +577,17 @@ DocumentManager = {
           DocumentManager.getExtensionPageContext(extension, window);
         } else if (apiLevel == FULL_PRIVILEGES) {
           ExtensionChild.createExtensionContext(extension, window);
         }
       }
     }
   },
 
-  observe: function(subject, topic, data) {
+  observe(subject, topic, data) {
     // For some types of documents (about:blank), we only see the first
     // notification, for others (data: URIs) we only observe the second.
     if (topic == "content-document-global-created" || topic == "document-element-inserted") {
       let document = subject;
       let window = document && document.defaultView;
 
       if (topic == "content-document-global-created") {
         window = subject;
@@ -608,20 +634,29 @@ DocumentManager = {
       // Close any existent iframe extension page context for the destroyed window.
       if (this.extensionPageWindows.has(windowId)) {
         let context = this.extensionPageWindows.get(windowId);
         context.close();
         this.extensionPageWindows.delete(windowId);
       }
 
       ExtensionChild.destroyExtensionContext(windowId);
+    } else if (topic === "http-on-opening-request") {
+      let {loadInfo} = subject.QueryInterface(Ci.nsIChannel);
+      if (loadInfo) {
+        let {externalContentPolicyType: type} = loadInfo;
+        if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT ||
+            type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) {
+          this.preloadScripts(subject.URI, loadInfo);
+        }
+      }
     }
   },
 
-  handleEvent: function(event) {
+  handleEvent(event) {
     let window = event.currentTarget;
     if (event.target != window.document) {
       // We use capturing listeners so we have precedence over content script
       // listeners, but only care about events targeted to the element we're
       // listening on.
       return;
     }
     window.removeEventListener(event.type, this, true);
@@ -770,16 +805,26 @@ DocumentManager = {
     MessageChannel.abortResponses({extensionId});
 
     this.extensionCount--;
     if (this.extensionCount == 0) {
       this.uninit();
     }
   },
 
+  preloadScripts(uri, loadInfo) {
+    for (let extension of ExtensionManager.extensions.values()) {
+      for (let script of extension.scripts) {
+        if (script.matchesLoadInfo(uri, loadInfo)) {
+          script.compileScripts();
+        }
+      }
+    }
+  },
+
   trigger(when, window) {
     if (when === "document_start") {
       for (let extension of ExtensionManager.extensions.values()) {
         for (let script of extension.scripts) {
           if (script.matches(window)) {
             let context = this.getContentScriptContext(extension, window);
             context.addScript(script, when);
           }