Bug 1333990: Part 3a - Use async loading and in-memory caching for WebExtension content scripts. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Tue, 14 Mar 2017 11:14:22 -0700
changeset 499661 eeb143a3afed297131c3fc4fab49a37a54076959
parent 499660 8d96d55d4506959f0e3cceae4786f1b4a314fd54
child 499662 e49b2ce255f8f030d952a644cdecf8108c2a8806
push id49469
push usermaglione.k@gmail.com
push dateThu, 16 Mar 2017 02:25:47 +0000
reviewersaswan
bugs1333990
milestone54.0a1
Bug 1333990: Part 3a - Use async loading and in-memory caching for WebExtension content scripts. r?aswan MozReview-Commit-ID: GcdKDbWcUtu
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionTabs.jsm
toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -44,16 +44,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
 Cu.import("resource://gre/modules/ExtensionChild.jsm");
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
+  DefaultMap,
   EventEmitter,
   LocaleData,
   defineLazyGetter,
   flushJarCache,
   getInnerWindowID,
   promiseDocumentReady,
   runSafeSyncWithoutClone,
 } = ExtensionUtils;
@@ -97,41 +98,57 @@ var apiManager = new class extends Schem
 
   registerSchemaAPI(namespace, envType, getAPI) {
     if (envType == "content_child") {
       super.registerSchemaAPI(namespace, envType, getAPI);
     }
   }
 }();
 
+class ScriptCache extends DefaultMap {
+  constructor(options) {
+    super(url => ChromeUtils.compileScript(url, options));
+  }
+}
+
 // Represents a content script.
 function Script(extension, options, deferred = PromiseUtils.defer()) {
   this.extension = extension;
   this.options = options;
   this.run_at = this.options.run_at;
   this.js = this.options.js || [];
   this.css = this.options.css || [];
   this.remove_css = this.options.remove_css;
   this.match_about_blank = this.options.match_about_blank;
   this.css_origin = this.options.css_origin;
 
   this.deferred = deferred;
 
+  this.scriptCache = extension[options.wantReturnValue ? "dynamicScripts"
+                                                       : "staticScripts"];
+  if (options.wantReturnValue) {
+    this.compileScripts();
+  }
+
   this.matches_ = new MatchPattern(this.options.matches);
   this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
   // TODO: MatchPattern should pre-mangle host-only patterns so that we
   // don't need to call a separate match function.
   this.matches_host_ = new MatchPattern(this.options.matchesHost || null);
   this.include_globs_ = new MatchGlobs(this.options.include_globs);
   this.exclude_globs_ = new MatchGlobs(this.options.exclude_globs);
 
   this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
 }
 
 Script.prototype = {
+  compileScripts() {
+    return this.js.map(url => this.scriptCache.get(url));
+  },
+
   get cssURLs() {
     // We can handle CSS urls (css) and CSS code (cssCode).
     let urls = [];
     for (let url of this.css) {
       urls.push(this.extension.baseURI.resolve(url));
     }
 
     if (this.options.cssCode) {
@@ -252,47 +269,42 @@ Script.prototype = {
         for (let url of cssURLs) {
           runSafeSyncWithoutClone(method, url, type);
         }
 
         this.deferred.resolve();
       }
     }
 
-    let result;
     let scheduled = this.run_at || "document_idle";
     if (shouldRun(scheduled)) {
-      for (let url of this.js) {
-        url = this.extension.baseURI.resolve(url);
+      let scriptsPromise = Promise.all(this.compileScripts());
 
-        let options = {
-          target: sandbox,
-          charset: "UTF-8",
-          // Inject asynchronously unless we're expected to inject before any
-          // page scripts have run, and we haven't already missed that boat.
-          async: this.run_at !== "document_start" || when !== "document_start",
-        };
-        try {
-          result = Services.scriptloader.loadSubScriptWithOptions(url, options);
-        } catch (e) {
-          Cu.reportError(e);
-          this.deferred.reject(e);
-        }
+      // If we're supposed to inject at the start of the document load,
+      // and we haven't already missed that point, block further parsing
+      // until the scripts have been loaded.
+      if (this.run_at === "document_start" && when === "document_start") {
+        window.document.blockParsing(scriptsPromise);
       }
 
-      if (this.options.jsCode) {
-        try {
+      this.deferred.resolve(scriptsPromise.then(scripts => {
+        let result;
+
+        // The evaluations below may throw, in which case the promise will be
+        // automatically rejected.
+        for (let script of scripts) {
+          result = script.executeInGlobal(sandbox);
+        }
+
+        if (this.options.jsCode) {
           result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
-        } catch (e) {
-          Cu.reportError(e);
-          this.deferred.reject(e);
         }
-      }
 
-      this.deferred.resolve(result);
+        return result;
+      }));
     }
   },
 };
 
 function getWindowMessageManager(contentWindow) {
   let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDocShell)
                         .QueryInterface(Ci.nsIInterfaceRequestor);
@@ -790,17 +802,20 @@ class BrowserExtensionContent extends Ev
     this.id = data.id;
     this.uuid = data.uuid;
     this.data = data;
     this.instanceId = data.instanceId;
 
     this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
     Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
 
-    this.scripts = data.content_scripts.map(scriptData => new Script(this, scriptData));
+    defineLazyGetter(this, "scripts", () => {
+      return data.content_scripts.map(scriptData => new Script(this, scriptData));
+    });
+
     this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
     this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
     this.permissions = data.permissions;
     this.principal = data.principal;
 
     this.localeData = new LocaleData(data.localeData);
 
     this.manifest = data.manifest;
@@ -852,16 +867,24 @@ class BrowserExtensionContent extends Ev
     let match = /^manifest:(.*)/.exec(perm);
     if (match) {
       return this.manifest[match[1]] != null;
     }
     return this.permissions.has(perm);
   }
 }
 
+defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", () => {
+  return new ScriptCache({returnValue: false});
+});
+
+defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", () => {
+  return new ScriptCache({returnValue: true});
+});
+
 ExtensionManager = {
   // Map[extensionId, BrowserExtensionContent]
   extensions: new Map(),
 
   init() {
     Schemas.init();
     ExtensionChild.initOnce();
 
--- a/toolkit/components/extensions/ExtensionTabs.jsm
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -558,16 +558,18 @@ class TabBase {
       options.run_at = "document_idle";
     }
     if (details.cssOrigin !== null) {
       options.css_origin = details.cssOrigin;
     } else {
       options.css_origin = "author";
     }
 
+    options.wantReturnValue = true;
+
     return this.sendMessage(context, "Extension:Execute", {options});
   }
 
   /**
    * Executes a script in the tab's content window, and returns a Promise which
    * resolves to the result of the evaluation, or rejects to the value of any
    * error the injection generates.
    *
--- a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
@@ -213,17 +213,17 @@ add_task(function* test_web_accessible_r
   function testScript() {
     window.postMessage("test-script-loaded", "*");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "content_scripts": [{
         "matches": ["http://example.com/*/file_csp.html"],
-        "run_at": "document_start",
+        "run_at": "document_end",
         "js": ["content_script_helper.js", "content_script.js"],
       }],
       "web_accessible_resources": [
         "image.png",
         "test_script.js",
       ],
     },
     background,
@@ -308,17 +308,17 @@ add_task(function* test_web_accessible_r
   function testScript() {
     window.postMessage("accessible-script-loaded", "*");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "content_scripts": [{
         "matches": ["https://example.com/*/file_mixed.html"],
-        "run_at": "document_start",
+        "run_at": "document_end",
         "js": ["content_script_helper.js", "content_script.js"],
       }],
       "web_accessible_resources": [
         "image.png",
         "test_script.js",
       ],
     },
     background,