Bug 1470783: Migrate extensions framework to use sharedData for cross-process data. r?zombie draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 24 Jun 2018 16:34:44 -0700
changeset 810009 9122f07accb1d6cbad9a17b1dc7edd19e7cf50f1
parent 809956 009c8758774c879b2d7fc118cd50b0e438ea715b
push id113860
push usermaglione.k@gmail.com
push dateSun, 24 Jun 2018 23:37:44 +0000
reviewerszombie
bugs1470783
milestone62.0a1
Bug 1470783: Migrate extensions framework to use sharedData for cross-process data. r?zombie initialProcessData has the unfortunate side-effect of sending an entire copy of all of its data to all content processes, and eagerly decoding it. For the extension framework, this means that we wind up loading an entire copy of all of our schema data, and of every extension's manifest and locale data, into every process, even if we'll never need it. The sharedData helper allows us to store an encoded copy of that data in a shared memory region, and clone it into the current process only when we need it, which can be a significant savings. For screenshots alone, it saves about 15K on locale and manifest data per content process, plus the size we save on not copying schema data. MozReview-Commit-ID: KkIOoLsBd99
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/extension-process-script.js
toolkit/components/extensions/parent/ext-contentScripts.js
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -100,16 +100,18 @@ const {
   getUniqueId,
   promiseTimeout,
 } = ExtensionUtils;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
 
 XPCOMUtils.defineLazyGetter(this, "LocaleData", () => ExtensionCommon.LocaleData);
 
+const {sharedData} = Services.ppmm;
+
 // The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
 // storage used by the browser.storage.local API is not directly accessible from the extension code).
 XPCOMUtils.defineLazyGetter(this, "WEBEXT_STORAGE_USER_CONTEXT_ID", () => {
   return ContextualIdentityService.getDefaultPrivateIdentity(
     "userContextIdInternal.webextStorageLocal").userContextId;
 });
 
 // The maximum time to wait for extension child shutdown blockers to complete.
@@ -1237,25 +1239,29 @@ class LangpackBootstrapScope {
   }
 
   shutdown(data, reason) {
     this.langpack.shutdown();
     this.langpack = null;
   }
 }
 
+let activeExtensionIDs = new Set();
+
 /**
  * This class is the main representation of an active WebExtension
  * in the main process.
  * @extends ExtensionData
  */
 class Extension extends ExtensionData {
   constructor(addonData, startupReason) {
     super(addonData.resourceURI);
 
+    this.sharedDataKeys = new Set();
+
     this.uuid = UUIDMap.get(addonData.id);
     this.instanceId = getUniqueId();
 
     this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
     Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
 
     if (addonData.cleanupFile) {
       Services.obs.addObserver(this, "xpcom-shutdown");
@@ -1299,16 +1305,18 @@ class Extension extends ExtensionData {
     this.onShutdown = new Set();
 
     this.uninstallURL = null;
 
     this.whiteListedHosts = null;
     this._optionalOrigins = null;
     this.webAccessibleResources = null;
 
+    this.registeredContentScripts = new Map();
+
     this.emitter = new EventEmitter();
 
     /* eslint-disable mozilla/balanced-listeners */
     this.on("add-permissions", (ignoreEvent, permissions) => {
       for (let perm of permissions.permissions) {
         this.permissions.add(perm);
       }
 
@@ -1505,38 +1513,49 @@ class Extension extends ExtensionData {
 
     if (this.errors.length) {
       return Promise.reject({errors: this.errors});
     }
 
     return manifest;
   }
 
+  get contentSecurityPolicy() {
+    return this.manifest.content_security_policy;
+  }
+
+  get backgroundScripts() {
+    return (this.manifest.background &&
+            this.manifest.background.scripts);
+  }
+
   // Representation of the extension to send to content
   // processes. This should include anything the content process might
   // need.
   serialize() {
     return {
       id: this.id,
       uuid: this.uuid,
       name: this.name,
+      contentSecurityPolicy: this.contentSecurityPolicy,
       instanceId: this.instanceId,
-      manifest: this.manifest,
       resourceURL: this.resourceURL,
-      baseURL: this.baseURI.spec,
       contentScripts: this.contentScripts,
-      registeredContentScripts: new Map(),
       webAccessibleResources: this.webAccessibleResources.map(res => res.glob),
       whiteListedHosts: this.whiteListedHosts.patterns.map(pat => pat.pattern),
-      localeData: this.localeData.serialize(),
+      permissions: this.permissions,
+      optionalPermissions: this.manifest.optional_permissions,
+    };
+  }
+
+  serializeExtended() {
+    return {
+      backgroundScripts: this.backgroundScripts,
       childModules: this.modules && this.modules.child,
       dependencies: this.dependencies,
-      permissions: this.permissions,
-      principal: this.principal,
-      optionalPermissions: this.manifest.optional_permissions,
       schemaURLs: this.schemaURLs,
     };
   }
 
   get contentScripts() {
     return this.manifest.content_scripts || [];
   }
 
@@ -1569,42 +1588,55 @@ class Extension extends ExtensionData {
       ppmm.addMessageListener(msg + "Complete", listener, true);
       Services.obs.addObserver(observer, "message-manager-close");
       Services.obs.addObserver(observer, "message-manager-disconnect");
 
       ppmm.broadcastAsyncMessage(msg, data);
     });
   }
 
+  setSharedData(key, value) {
+    key = `extension/${this.id}/${key}`;
+    this.sharedDataKeys.add(key);
+
+    sharedData.set(key, value);
+  }
+
+  getSharedData(key, value) {
+    key = `extension/${this.id}/${key}`;
+    return sharedData.get(key);
+  }
+
+  initSharedData() {
+    this.setSharedData("", this.serialize());
+    this.setSharedData("extendedData", this.serializeExtended());
+    this.setSharedData("locales", this.localeData.serialize());
+    this.setSharedData("manifest", this.manifest);
+    this.updateContentScripts();
+  }
+
+  updateContentScripts() {
+    this.setSharedData("contentScripts", this.registeredContentScripts);
+  }
+
   runManifest(manifest) {
     let promises = [];
     for (let directive in manifest) {
       if (manifest[directive] !== null) {
         promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest));
 
         promises.push(Management.asyncEmitManifestEntry(this, directive));
       }
     }
 
-    let data = Services.ppmm.initialProcessData;
-    if (!data["Extension:Extensions"]) {
-      data["Extension:Extensions"] = [];
-    }
-
-    let serial = this.serialize();
+    activeExtensionIDs.add(this.id);
+    sharedData.set("extensions/activeIDs", activeExtensionIDs);
 
-    // Map of the programmatically registered content script definitions
-    // (by string scriptId), used in ext-contentScripts.js to propagate
-    // the registered content scripts to the child content processes
-    // (e.g. when a new content process starts after a content process crash).
-    this.registeredContentScripts = serial.registeredContentScripts;
-
-    data["Extension:Extensions"].push(serial);
-
-    return this.broadcast("Extension:Startup", serial).then(() => {
+    Services.ppmm.sharedData.flush();
+    return this.broadcast("Extension:Startup", this.id).then(() => {
       return Promise.all(promises);
     });
   }
 
   /**
    * Call the close() method on the given object when this extension
    * is shut down.  This can happen during browser shutdown, or when
    * an extension is manually disabled or uninstalled.
@@ -1719,16 +1751,18 @@ class Extension extends ExtensionData {
       }
 
       if (this.hasShutdown) {
         return;
       }
 
       GlobalManager.init(this);
 
+      this.initSharedData();
+
       this.policy.active = false;
       this.policy = processScript.initExtension(this);
       this.policy.extension = this;
 
       this.updatePermissions(this.startupReason);
 
       // The "startup" Management event sent on the extension instance itself
       // is emitted just before the Management "startup" event,
@@ -1790,18 +1824,22 @@ class Extension extends ExtensionData {
       Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
     }
 
     if (this.cleanupFile ||
         ["ADDON_INSTALL", "ADDON_UNINSTALL", "ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(reason)) {
       StartupCache.clearAddonData(this.id);
     }
 
-    let data = Services.ppmm.initialProcessData;
-    data["Extension:Extensions"] = data["Extension:Extensions"].filter(e => e.id !== this.id);
+    activeExtensionIDs.delete(this.id);
+    sharedData.set("extensions/activeIDs", activeExtensionIDs);
+
+    for (let key of this.sharedDataKeys) {
+      sharedData.delete(key);
+    }
 
     Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
 
     this.updatePermissions(this.shutdownReason);
 
     if (!this.manifest) {
       this.policy.active = false;
 
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -54,16 +54,18 @@ const {
 const {
   EventManager,
   LocalAPIImplementation,
   LocaleData,
   NoCloneSpreadArgs,
   SchemaAPIInterface,
 } = ExtensionCommon;
 
+const {sharedData} = Services.cpmm;
+
 const isContentProcess = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
 
 // Copy an API object from |source| into the scope |dest|.
 function injectAPI(source, dest) {
   for (let prop in source) {
     // Skip names prefixed with '_'.
     if (prop[0] == "_") {
       continue;
@@ -597,43 +599,45 @@ class BrowserExtensionContent extends Ev
   constructor(data) {
     super();
 
     this.data = data;
     this.id = data.id;
     this.uuid = data.uuid;
     this.instanceId = data.instanceId;
 
-    this.childModules = data.childModules;
-    this.dependencies = data.dependencies;
-    this.schemaURLs = data.schemaURLs;
+    if (WebExtensionPolicy.isExtensionProcess) {
+      Object.assign(this, this.getSharedData("extendedData"));
+    }
 
     this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
     Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
 
     defineLazyGetter(this, "scripts", () => {
       return data.contentScripts.map(scriptData => new ExtensionContent.Script(this, scriptData));
     });
 
     this.webAccessibleResources = data.webAccessibleResources.map(res => new MatchGlob(res));
     this.permissions = data.permissions;
     this.optionalPermissions = data.optionalPermissions;
-    this.principal = data.principal;
 
     let restrictSchemes = !this.hasPermission("mozillaAddons");
 
     this.whiteListedHosts = new MatchPatternSet(data.whiteListedHosts, {restrictSchemes, ignorePath: true});
 
     this.apiManager = this.getAPIManager();
 
-    this.localeData = new LocaleData(data.localeData);
+    this._manifest = null;
+    this._localeData = null;
 
-    this.manifest = data.manifest;
-    this.baseURL = data.baseURL;
-    this.baseURI = Services.io.newURI(data.baseURL);
+    this.baseURI = Services.io.newURI(`moz-extension://${this.uuid}/`);
+    this.baseURL = this.baseURI.spec;
+
+    this.principal = Services.scriptSecurityManager.createCodebasePrincipal(
+      this.baseURI, {});
 
     // Only used in addon processes.
     this.views = new Set();
 
     // Only used for devtools views.
     this.devtoolsViews = new Set();
 
     /* eslint-disable mozilla/balanced-listeners */
@@ -678,23 +682,43 @@ class BrowserExtensionContent extends Ev
         this.policy.allowedOrigins = this.whiteListedHosts;
       }
     });
     /* eslint-enable mozilla/balanced-listeners */
 
     ExtensionManager.extensions.set(this.id, this);
   }
 
+  getSharedData(key, value) {
+    return sharedData.get(`extension/${this.id}/${key}`);
+  }
+
+  get localeData() {
+    if (!this._localeData) {
+      this._localeData = new LocaleData(this.getSharedData("locales"));
+    }
+    return this._localeData;
+  }
+
+  get manifest() {
+    if (!this._manifest) {
+      this._manifest = this.getSharedData("manifest");
+    }
+    return this._manifest;
+  }
+
   getAPIManager() {
     let apiManagers = [ExtensionPageChild.apiManager];
 
-    for (let id of this.dependencies) {
-      let extension = processScript.getExtensionChild(id);
-      if (extension) {
-        apiManagers.push(extension.experimentAPIManager);
+    if (this.dependencies) {
+      for (let id of this.dependencies) {
+        let extension = processScript.getExtensionChild(id);
+        if (extension) {
+          apiManagers.push(extension.experimentAPIManager);
+        }
       }
     }
 
     if (this.childModules) {
       this.experimentAPIManager =
         new ExtensionCommon.LazyAPIManager("addon", this.childModules, this.schemaURLs);
 
       apiManagers.push(this.experimentAPIManager);
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -146,17 +146,19 @@ let apiManager = new class extends Schem
           promises.push(Schemas.load(url));
         }
         for (let [url, {content}] of this.schemaURLs) {
           promises.push(Schemas.load(url, content));
         }
         for (let url of schemaURLs) {
           promises.push(Schemas.load(url));
         }
-        return Promise.all(promises);
+        return Promise.all(promises).then(() => {
+          Schemas.broadcastSchemas();
+        })
       });
     })();
 
     /* eslint-disable mozilla/balanced-listeners */
     Services.mm.addMessageListener("Extension:GetTabAndWindowId", this);
     /* eslint-enable mozilla/balanced-listeners */
 
     this.initialized = promise;
@@ -744,29 +746,24 @@ class DevToolsExtensionPageContextParent
 
     super.shutdown();
   }
 }
 
 ParentAPIManager = {
   proxyContexts: new Map(),
 
-  parentMessageManagers: new Set(),
-
   init() {
     Services.obs.addObserver(this, "message-manager-close");
-    Services.obs.addObserver(this, "ipc:content-created");
 
     Services.mm.addMessageListener("API:CreateProxyContext", this);
     Services.mm.addMessageListener("API:CloseProxyContext", this, true);
     Services.mm.addMessageListener("API:Call", this);
     Services.mm.addMessageListener("API:AddListener", this);
     Services.mm.addMessageListener("API:RemoveListener", this);
-
-    this.schemaHook = this.schemaHook.bind(this);
   },
 
   attachMessageManager(extension, processMessageManager) {
     extension.parentMessageManager = processMessageManager;
   },
 
   async observe(subject, topic, data) {
     if (topic === "message-manager-close") {
@@ -778,33 +775,16 @@ ParentAPIManager = {
       }
 
       // Reset extension message managers when their child processes shut down.
       for (let extension of GlobalManager.extensionMap.values()) {
         if (extension.parentMessageManager === mm) {
           extension.parentMessageManager = null;
         }
       }
-
-      this.parentMessageManagers.delete(mm);
-    } else if (topic === "ipc:content-created") {
-      let mm = subject.QueryInterface(Ci.nsIInterfaceRequestor)
-                      .getInterface(Ci.nsIMessageSender);
-      if (mm.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE) {
-        this.parentMessageManagers.add(mm);
-        mm.sendAsyncMessage("Schema:Add", Schemas.schemaJSON);
-
-        Schemas.schemaHook = this.schemaHook;
-      }
-    }
-  },
-
-  schemaHook(schemas) {
-    for (let mm of this.parentMessageManagers) {
-      mm.sendAsyncMessage("Schema:Add", schemas);
     }
   },
 
   shutdownExtension(extensionId) {
     for (let [childId, context] of this.proxyContexts) {
       if (context.extension.id == extensionId) {
         if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(context.extension.shutdownReason)) {
           let modules = apiManager.eventModules.get("disable");
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -26,16 +26,19 @@ ChromeUtils.defineModuleGetter(this, "Ne
 XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
                                    "@mozilla.org/addons/content-policy;1",
                                    "nsIAddonContentPolicy");
 
 XPCOMUtils.defineLazyGetter(this, "StartupCache", () => ExtensionParent.StartupCache);
 
 var EXPORTED_SYMBOLS = ["SchemaRoot", "Schemas"];
 
+const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
+const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";
+
 const {DEBUG} = AppConstants;
 
 const isParentProcess = Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
 
 function readJSON(url) {
   return new Promise((resolve, reject) => {
     NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => {
       if (!Components.isSuccessCode(status)) {
@@ -2999,20 +3002,23 @@ this.Schemas = {
   initialized: false,
 
   REVOKE: Symbol("@@revoke"),
 
   // Maps a schema URL to the JSON contained in that schema file. This
   // is useful for sending the JSON across processes.
   schemaJSON: new Map(),
 
+
   // A separate map of schema JSON which should be available in all
   // content processes.
   contentSchemaJSON: new Map(),
 
+  privilegedSchemaJSON: new Map(),
+
   _rootSchema: null,
 
   get rootSchema() {
     if (!this.initialized) {
       this.init();
     }
     if (!this._rootSchema) {
       this._rootSchema = new SchemaRoot(null, this.schemaJSON);
@@ -3027,45 +3033,30 @@ this.Schemas = {
 
   init() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
 
     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-      let data = Services.cpmm.initialProcessData;
-      let schemas = data["Extension:Schemas"];
-      if (schemas) {
-        this.schemaJSON = schemas;
+      let addSchemas = schemas => {
+        for (let [key, value] of schemas.entries()) {
+          this.schemaJSON.set(key, value);
+        }
+      };
+
+      if (WebExtensionPolicy.isExtensionProcess) {
+        addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS));
       }
 
-      Services.cpmm.addMessageListener("Schema:Add", this);
-    }
-  },
-
-  receiveMessage(msg) {
-    let {data} = msg;
-    switch (msg.name) {
-      case "Schema:Add":
-        // If we're given a Map, the ordering of the initial items
-        // matters, so swap with our current data to make sure its
-        // entries appear first.
-        if (typeof data.get === "function") {
-          // Create a new Map so we're sure it's in the same compartment.
-          [this.schemaJSON, data] = [new Map(data), this.schemaJSON];
-        }
-
-        for (let [url, schema] of data) {
-          this.schemaJSON.set(url, schema);
-        }
-        if (this._rootSchema) {
-          throw new Error("Schema loaded after root schema populated");
-        }
-        break;
+      let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS);
+      if (schemas) {
+        addSchemas(schemas);
+      }
     }
   },
 
   _loadCachedSchemasPromise: null,
   loadCachedSchemas() {
     if (!this._loadCachedSchemasPromise) {
       this._loadCachedSchemasPromise = StartupCache.schemas.getAll().then(results => {
         return results;
@@ -3075,30 +3066,32 @@ this.Schemas = {
     return this._loadCachedSchemasPromise;
   },
 
   addSchema(url, schema, content = false) {
     this.schemaJSON.set(url, schema);
 
     if (content) {
       this.contentSchemaJSON.set(url, schema);
-
-      let data = Services.ppmm.initialProcessData;
-      data["Extension:Schemas"] = this.contentSchemaJSON;
-
-      Services.ppmm.broadcastAsyncMessage("Schema:Add", [[url, schema]]);
-    } else if (this.schemaHook) {
-      this.schemaHook([[url, schema]]);
+    } else {
+      this.privilegedSchemaJSON.set(url, schema);
     }
 
     if (this._rootSchema) {
       throw new Error("Schema loaded after root schema populated");
     }
   },
 
+  broadcastSchemas() {
+    let {sharedData} = Services.ppmm;
+
+    sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON);
+    sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON);
+  },
+
   fetch(url) {
     return readJSONAndBlobbify(url);
   },
 
   processSchema(json) {
     return blobbify(json);
   },
 
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -24,16 +24,22 @@ ChromeUtils.import("resource://gre/modul
 
 XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
 
 const {
   DefaultWeakMap,
   getInnerWindowID,
 } = ExtensionUtils;
 
+const {sharedData} = Services.cpmm;
+
+function getData(extension, key = "") {
+  return sharedData.get(`extension/${extension.id}/${key}`);
+}
+
 // We need to avoid touching Services.appinfo here in order to prevent
 // the wrong version from being cached during xpcshell test startup.
 // eslint-disable-next-line mozilla/use-services
 const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
 const isContentProcess = appinfo.processType == appinfo.PROCESS_TYPE_CONTENT;
 
 function tryMatchPatternSet(patterns, options) {
   try {
@@ -295,30 +301,18 @@ ExtensionManager = {
     MessageChannel.setupMessageManagers([Services.cpmm]);
 
     Services.cpmm.addMessageListener("Extension:Startup", this);
     Services.cpmm.addMessageListener("Extension:Shutdown", this);
     Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
     Services.cpmm.addMessageListener("Extension:RegisterContentScript", this);
     Services.cpmm.addMessageListener("Extension:UnregisterContentScripts", this);
 
-    let procData = Services.cpmm.initialProcessData || {};
-
-    for (let data of procData["Extension:Extensions"] || []) {
-      this.initExtension(data);
-    }
-
-    if (isContentProcess) {
-      // Make sure we handle new schema data until Schemas.jsm is loaded.
-      if (!procData["Extension:Schemas"]) {
-        procData["Extension:Schemas"] = new Map();
-      }
-      this.schemaJSON = procData["Extension:Schemas"];
-
-      Services.cpmm.addMessageListener("Schema:Add", this);
+    for (let id of sharedData.get("extensions/activeIDs") || []) {
+      this.initExtension(getData({id}));
     }
   },
 
   initExtensionPolicy(extension) {
     let policy = WebExtensionPolicy.getByID(extension.id);
     if (!policy) {
       let localizeCallback, allowedOrigins, webAccessibleResources;
       let restrictSchemes = !extension.permissions.has("mozillaAddons");
@@ -330,59 +324,64 @@ ExtensionManager = {
         webAccessibleResources = extension.webAccessibleResources;
       } else {
         // We have serialized extension data;
         localizeCallback = str => extensions.get(policy).localize(str);
         allowedOrigins = new MatchPatternSet(extension.whiteListedHosts, {restrictSchemes});
         webAccessibleResources = extension.webAccessibleResources.map(host => new MatchGlob(host));
       }
 
+      let {backgroundScripts} = extension;
+      if (!backgroundScripts && WebExtensionPolicy.isExtensionProcess) {
+        ({backgroundScripts} = getData(extension, "extendedData") || {});
+      }
+
       policy = new WebExtensionPolicy({
         id: extension.id,
         mozExtensionHostname: extension.uuid,
         name: extension.name,
         baseURL: extension.resourceURL,
 
         permissions: Array.from(extension.permissions),
         allowedOrigins,
         webAccessibleResources,
 
-        contentSecurityPolicy: extension.manifest.content_security_policy,
+        contentSecurityPolicy: extension.contentSecurityPolicy,
 
         localizeCallback,
 
-        backgroundScripts: (extension.manifest.background &&
-                            extension.manifest.background.scripts),
+        backgroundScripts,
 
         contentScripts: extension.contentScripts.map(script => parseScriptOptions(script, restrictSchemes)),
       });
 
       policy.debugName = `${JSON.stringify(policy.name)} (ID: ${policy.id}, ${policy.getURL()})`;
 
       // Register any existent dynamically registered content script for the extension
       // when a content process is started for the first time (which also cover
       // a content process that crashed and it has been recreated).
       const registeredContentScripts = this.registeredContentScripts.get(policy);
 
-      if (extension.registeredContentScripts) {
-        for (let [scriptId, options] of extension.registeredContentScripts) {
-          const parsedOptions = parseScriptOptions(options, restrictSchemes);
-          const script = new WebExtensionContentScript(policy, parsedOptions);
-          policy.registerContentScript(script);
-          registeredContentScripts.set(scriptId, script);
-        }
+      for (let [scriptId, options] of getData(extension, "contentScripts") || []) {
+        const parsedOptions = parseScriptOptions(options, restrictSchemes);
+        const script = new WebExtensionContentScript(policy, parsedOptions);
+        policy.registerContentScript(script);
+        registeredContentScripts.set(scriptId, script);
       }
 
       policy.active = true;
       policy.initData = extension;
     }
     return policy;
   },
 
   initExtension(data) {
+    if (typeof data === "string") {
+      data = getData({id: data});
+    }
     let policy = this.initExtensionPolicy(data);
 
     DocumentManager.initExtension(policy);
   },
 
   receiveMessage({name, data}) {
     switch (name) {
       case "Extension:Startup": {
@@ -409,33 +408,16 @@ ExtensionManager = {
       }
 
       case "Extension:FlushJarCache": {
         ExtensionUtils.flushJarCache(data.path);
         Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
         break;
       }
 
-      case "Schema:Add": {
-        // If we're given a Map, the ordering of the initial items
-        // matters, so swap with our current data to make sure its
-        // entries appear first.
-        if (typeof data.get === "function") {
-          [this.schemaJSON, data] = [data, this.schemaJSON];
-
-          Services.cpmm.initialProcessData["Extension:Schemas"] =
-            this.schemaJSON;
-        }
-
-        for (let [url, schema] of data) {
-          this.schemaJSON.set(url, schema);
-        }
-        break;
-      }
-
       case "Extension:RegisterContentScript": {
         let policy = WebExtensionPolicy.getByID(data.id);
 
         if (policy) {
           const registeredContentScripts = this.registeredContentScripts.get(policy);
 
           if (registeredContentScripts.has(data.scriptId)) {
             Cu.reportError(new Error(
--- a/toolkit/components/extensions/parent/ext-contentScripts.js
+++ b/toolkit/components/extensions/parent/ext-contentScripts.js
@@ -126,16 +126,17 @@ this.contentScripts = class extends Exte
           return;
         }
 
         const scriptIds = Array.from(parentScriptsMap.keys());
 
         for (let scriptId of scriptIds) {
           extension.registeredContentScripts.delete(scriptId);
         }
+        extension.updateContentScripts();
 
         extension.broadcast("Extension:UnregisterContentScripts", {
           id: extension.id,
           scriptIds,
         });
       },
     });
 
@@ -157,16 +158,17 @@ this.contentScripts = class extends Exte
 
           await extension.broadcast("Extension:RegisterContentScript", {
             id: extension.id,
             options: scriptOptions,
             scriptId,
           });
 
           extension.registeredContentScripts.set(scriptId, scriptOptions);
+          extension.updateContentScripts();
 
           return scriptId;
         },
 
         // This method is not available to the extension code, the extension code
         // doesn't have access to the internally used scriptId, on the contrary
         // the extension code will call script.unregister on the script API object
         // that is resolved from the register API method returned promise.
@@ -175,16 +177,17 @@ this.contentScripts = class extends Exte
           if (!contentScript) {
             Cu.reportError(new Error(`No such content script ID: ${scriptId}`));
 
             return;
           }
 
           parentScriptsMap.delete(scriptId);
           extension.registeredContentScripts.delete(scriptId);
+          extension.updateContentScripts();
 
           contentScript.destroy();
 
           await extension.broadcast("Extension:UnregisterContentScripts", {
             id: extension.id,
             scriptIds: [scriptId],
           });
         },