Bug 1102643 - [e10s] Prefetching for add-on shims (r=mconley)
authorBill McCloskey <wmccloskey@mozilla.com>
Mon, 05 Jan 2015 13:49:44 -0800
changeset 247957 78098039e9c13ac28b792d1ac3e086f278de09fa
parent 247881 72d7ae169b094ee14e59053a8ad1bdfc8e9e423e
child 247958 378c4ef307840d90f388ec286f7b2561f7980bc6
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1102643
milestone37.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 1102643 - [e10s] Prefetching for add-on shims (r=mconley)
browser/app/profile/firefox.js
toolkit/components/addoncompat/Prefetcher.jsm
toolkit/components/addoncompat/RemoteAddonsChild.jsm
toolkit/components/addoncompat/RemoteAddonsParent.jsm
toolkit/components/addoncompat/moz.build
toolkit/components/addoncompat/multiprocessShims.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1811,14 +1811,15 @@ pref("browser.tabs.remote.autostart.1", 
 pref("print.enable_e10s_testing", false);
 #else
 pref("print.enable_e10s_testing", true);
 #endif
 
 #ifdef NIGHTLY_BUILD
 // Enable e10s add-on interposition by default.
 pref("extensions.interposition.enabled", true);
+pref("extensions.interposition.prefetching", true);
 #endif
 
 pref("browser.defaultbrowser.notificationbar", false);
 
 // How many milliseconds to wait for a CPOW response from the child process.
 pref("dom.ipc.cpow.timeout", 0);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/addoncompat/Prefetcher.jsm
@@ -0,0 +1,549 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+this.EXPORTED_SYMBOLS = ["Prefetcher"];
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+
+// Rules are defined at the bottom of this file.
+let PrefetcherRules = {};
+
+/*
+ * When events that trigger in the content process are forwarded to
+ * add-ons in the chrome process, we expect the add-ons to send a lot
+ * of CPOWs to query content nodes while processing the events. To
+ * speed this up, the prefetching system anticipates which properties
+ * will be read and reads them ahead of time. The prefetched
+ * properties are passed to the chrome process along with each
+ * event. A typical scenario might work like this:
+ *
+ * 1. "load" event fires in content
+ * 2. Content process prefetches:
+ *      event.target.defaultView = <win 1>
+ *      <win 1>.location = <location obj>
+ *      event.target.getElementsByTagName("form") = [<elt 1>, <elt 2>]
+ *      <elt 1>.id = "login-form"
+ *      <elt 2>.id = "subscribe-form"
+ * 3. Content process forwards "load" event to add-on along with
+ *    prefetched data
+ * 4. Add-on reads:
+ *      event.target.defaultView (already prefetched)
+ *      event.target.getElementsByTagName("form") (already prefetched)
+ *      <elt 1>.id (already prefetched)
+ *      <elt 1>.className (not prefetched; CPOW must be sent)
+ *
+ * The amount of data to prefetch is determined based on the add-on ID
+ * and the event type. The specific data to select is determined using
+ * a set of Datalog-like rules (http://en.wikipedia.org/wiki/Datalog).
+ *
+ * Rules operate on a series of "tables" like in a database. Each
+ * table contains a set of content-process objects. When an event
+ * handler runs, it seeds some initial tables with objects of
+ * interest. For example, the Event table might start out containing
+ * the event that fired.
+ *
+ * Objects are added to tables using a set of rules of the form "if X
+ * is in table A, then add F(X) to table B", where F(X) is typically a
+ * property access or a method call. The most common functions F are:
+ *
+ * PropertyOp(destTable, sourceTable, property):
+ *   For each object X in sourceTable, add X.property to destTable.
+ * MethodOp(destTable, sourceTable, method, args):
+ *   For each object X in sourceTable, add X.method(args) to destTable.
+ * CollectionOp(destTable, sourceTable):
+ *   For each object X in sourceTable, add X[i] to destTable for
+ *   all i from 0 to X.length - 1.
+ *
+ * To generate the prefetching in the example above, the following
+ * rules would work:
+ *
+ * 1. PropertyOp("EventTarget", "Event", "target")
+ * 2. PropertyOp("Window", "EventTarget", "defaultView")
+ * 3. MethodOp("FormCollection", "EventTarget", "getElementsByTagName", "form")
+ * 4. CollectionOp("Form", "FormCollection")
+ * 5. PropertyOp(null, "Form", "id")
+ *
+ * Rules are written at the bottom of this file.
+ *
+ * When a rule runs, it will usually generate some cache entries that
+ * will be passed to the chrome process. For example, when PropertyOp
+ * prefetches obj.prop and gets the value X, it caches the value of
+ * obj and X. When the chrome process receives this data, it creates a
+ * two-level map [obj -> [prop -> X]]. When the add-on accesses a
+ * property on obj, the add-on shim code consults this map to see if
+ * the property has already been cached.
+ */
+
+const PREF_PREFETCHING_ENABLED = "extensions.interposition.prefetching";
+
+function objAddr(obj)
+{
+  if (obj && typeof(obj) == "object") {
+    return String(obj) + "[" + Cu.getJSTestingFunctions().objectAddress(obj) + "]";
+  }
+  return String(obj);
+}
+
+function log(...args)
+{
+  return;
+
+  for (let arg of args) {
+    dump(arg);
+    dump(" ");
+  }
+  dump("\n");
+}
+
+function logPrefetch(kind, value1, component, value2)
+{
+  return;
+  log("prefetching", kind, objAddr(value1) + "." + component, "=", objAddr(value2));
+}
+
+/*
+ * All the Op classes (representing Datalog rules) have the same interface:
+ *   outputTable: Table that objects generated by the rule are added to.
+ *     Note that this can be null.
+ *   inputTable: Table that the rule draws objects from.
+ *   addObject(database, obj): Called when an object is added to inputTable.
+ *     This code should take care of adding objects to outputTable.
+ *     Data to be cached should be stored by calling database.cache.
+ *   makeCacheEntry(item, cache):
+ *     Called by the chrome process to create the two-level map of
+ *     prefetched objects. |item| holds the cached data
+ *     generated by the content process. |cache| is the map to be
+ *     generated.
+ */
+
+function PropertyOp(outputTable, inputTable, prop)
+{
+  this.outputTable = outputTable;
+  this.inputTable = inputTable;
+  this.prop = prop;
+}
+
+PropertyOp.prototype.addObject = function(database, obj)
+{
+  let has = false, propValue;
+  try {
+    if (this.prop in obj) {
+      has = true;
+      propValue = obj[this.prop];
+    }
+  } catch (e) {
+    // Don't cache anything if an exception is thrown.
+    return;
+  }
+
+  logPrefetch("prop", obj, this.prop, propValue);
+  database.cache(this.index, obj, has, propValue);
+  if (has && typeof(propValue) == "object" && propValue && this.outputTable) {
+    database.add(this.outputTable, propValue);
+  }
+}
+
+PropertyOp.prototype.makeCacheEntry = function(item, cache)
+{
+  let [index, obj, has, propValue] = item;
+
+  let desc = { configurable: false, enumerable: true, writable: false, value: propValue };
+
+  if (!cache.has(obj)) {
+    cache.set(obj, new Map());
+  }
+  let propMap = cache.get(obj);
+  propMap.set(this.prop, desc);
+}
+
+function MethodOp(outputTable, inputTable, method, ...args)
+{
+  this.outputTable = outputTable;
+  this.inputTable = inputTable;
+  this.method = method;
+  this.args = args;
+}
+
+MethodOp.prototype.addObject = function(database, obj)
+{
+  let result;
+  try {
+    result = obj[this.method].apply(obj, this.args);
+  } catch (e) {
+    // Don't cache anything if an exception is thrown.
+    return;
+  }
+
+  logPrefetch("method", obj, this.method + "(" + this.args + ")", result);
+  database.cache(this.index, obj, result);
+  if (result && typeof(result) == "object" && this.outputTable) {
+    database.add(this.outputTable, result);
+  }
+}
+
+MethodOp.prototype.makeCacheEntry = function(item, cache)
+{
+  let [index, obj, result] = item;
+
+  if (!cache.has(obj)) {
+    cache.set(obj, new Map());
+  }
+  let propMap = cache.get(obj);
+  let fallback = propMap.get(this.method);
+
+  let method = this.method;
+  let selfArgs = this.args;
+  let methodImpl = function(...args) {
+    if (args.length == selfArgs.length && args.every((v, i) => v === selfArgs[i])) {
+      return result;
+    }
+
+    if (fallback) {
+      return fallback.value(...args);
+    } else {
+      return obj[method](...args);
+    }
+  };
+
+  let desc = { configurable: false, enumerable: true, writable: false, value: methodImpl };
+  propMap.set(this.method, desc);
+}
+
+function CollectionOp(outputTable, inputTable)
+{
+  this.outputTable = outputTable;
+  this.inputTable = inputTable;
+}
+
+CollectionOp.prototype.addObject = function(database, obj)
+{
+  let elements = [];
+  try {
+    let len = obj.length;
+    for (let i = 0; i < len; i++) {
+      logPrefetch("index", obj, i, obj[i]);
+      elements.push(obj[i]);
+    }
+  } catch (e) {
+    // Don't cache anything if an exception is thrown.
+    return;
+  }
+
+  database.cache(this.index, obj, ...elements);
+  for (let i = 0; i < elements.length; i++) {
+    if (elements[i] && typeof(elements[i]) == "object" && this.outputTable) {
+      database.add(this.outputTable, elements[i]);
+    }
+  }
+}
+
+CollectionOp.prototype.makeCacheEntry = function(item, cache)
+{
+  let [index, obj, ...elements] = item;
+
+  if (!cache.has(obj)) {
+    cache.set(obj, new Map());
+  }
+  let propMap = cache.get(obj);
+
+  let lenDesc = { configurable: false, enumerable: true, writable: false, value: elements.length };
+  propMap.set("length", lenDesc);
+
+  for (let i = 0; i < elements.length; i++) {
+    let desc = { configurable: false, enumerable: true, writable: false, value: elements[i] };
+    propMap.set(i, desc);
+  }
+}
+
+function CopyOp(outputTable, inputTable)
+{
+  this.outputTable = outputTable;
+  this.inputTable = inputTable;
+}
+
+CopyOp.prototype.addObject = function(database, obj)
+{
+  database.add(this.outputTable, obj);
+}
+
+function Database(trigger, addons)
+{
+  // Create a map of rules that apply to this specific trigger and set
+  // of add-ons. The rules are indexed based on their inputTable.
+  this.rules = new Map();
+  for (let addon of addons) {
+    let addonRules = PrefetcherRules[addon] || {};
+    let triggerRules = addonRules[trigger] || [];
+    for (let rule of triggerRules) {
+      let inTable = rule.inputTable;
+      if (!this.rules.has(inTable)) {
+        this.rules.set(inTable, new Set());
+      }
+      let set = this.rules.get(inTable);
+      set.add(rule);
+    }
+  }
+
+  // this.tables maps table names to sets of objects contained in them.
+  this.tables = new Map();
+
+  // todo is a worklist of items added to tables that have not had
+  // rules run on them yet.
+  this.todo = [];
+
+  // Cached data to be sent to the chrome process.
+  this.cached = [];
+}
+
+Database.prototype = {
+  // Add an object to a table.
+  add: function(table, obj) {
+    if (!this.tables.has(table)) {
+      this.tables.set(table, new Set());
+    }
+    let tableSet = this.tables.get(table);
+    if (tableSet.has(obj)) {
+      return;
+    }
+    tableSet.add(obj);
+
+    this.todo.push([table, obj]);
+  },
+
+  cache: function(...args) {
+    this.cached.push(args);
+  },
+
+  // Run a fixed-point iteration that adds objects to table based on
+  // this.rules until there are no more objects to add.
+  process: function() {
+    while (this.todo.length) {
+      let [table, obj] = this.todo.pop();
+      let rules = this.rules.get(table);
+      if (!rules) {
+        continue;
+      }
+      for (let rule of rules) {
+        rule.addObject(this, obj);
+      }
+    }
+  },
+};
+
+let Prefetcher = {
+  init: function() {
+    // Give an index to each rule and store it in this.ruleMap based
+    // on the index. The index is used to serialize and deserialize
+    // data from content to chrome.
+    let counter = 0;
+    this.ruleMap = new Map();
+    for (let addon in PrefetcherRules) {
+      for (let trigger in PrefetcherRules[addon]) {
+        for (let rule of PrefetcherRules[addon][trigger]) {
+          rule.index = counter++;
+          this.ruleMap.set(rule.index, rule);
+        }
+      }
+    }
+
+    this.prefetchingEnabled = Preferences.get(PREF_PREFETCHING_ENABLED, false);
+    Services.prefs.addObserver(PREF_PREFETCHING_ENABLED, this, false);
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+  },
+
+  observe: function(subject, topic, data) {
+    if (topic == "xpcom-shutdown") {
+      Services.prefs.removeObserver(PREF_PREFETCHING_ENABLED, this);
+      Services.obs.removeObserver(this, "xpcom-shutdown");
+    } else if (topic == PREF_PREFETCHING_ENABLED) {
+      this.prefetchingEnabled = Preferences.get(PREF_PREFETCHING_ENABLED, false);
+    }
+  },
+
+  // Called when an event occurs in the content process. The event is
+  // described by the trigger string. |addons| is a list of addons
+  // that have listeners installed for the event. |args| is
+  // event-specific data (such as the event object).
+  prefetch: function(trigger, addons, args) {
+    if (!this.prefetchingEnabled) {
+      return [[], []];
+    }
+
+    let db = new Database(trigger, addons);
+    for (let table in args) {
+      log("root", table, "=", objAddr(args[table]));
+      db.add(table, args[table]);
+    }
+
+    // Prefetch objects and add them to tables.
+    db.process();
+
+    // Data passed to sendAsyncMessage must be split into a JSON
+    // portion and a CPOW portion. This code splits apart db.cached
+    // into these two pieces. Any object in db.cache is added to an
+    // array of CPOWs and replaced with {cpow: <index in array>}.
+    let cpowIndexes = new Map();
+    let prefetched = [];
+    let cpows = [];
+    for (let item of db.cached) {
+      item = item.map((elt) => {
+        if (elt && typeof(elt) == "object") {
+          if (!cpowIndexes.has(elt)) {
+            let index = cpows.length;
+            cpows.push(elt);
+            cpowIndexes.set(elt, index);
+          }
+          return {cpow: cpowIndexes.get(elt)};
+        } else {
+          return elt;
+        }
+      });
+
+      prefetched.push(item);
+    }
+
+    return [prefetched, cpows];
+  },
+
+  cache: null,
+
+  // Generate a two-level mapping based on cached data received from
+  // the content process.
+  generateCache: function(prefetched, cpows) {
+    let cache = new Map();
+    for (let item of prefetched) {
+      // Replace anything of the form {cpow: <index>} with the actual
+      // object in |cpows|.
+      item = item.map((elt) => {
+        if (elt && typeof(elt) == "object") {
+          return cpows[elt.cpow];
+        }
+        return elt;
+      });
+
+      let index = item[0];
+      let op = this.ruleMap.get(index);
+      op.makeCacheEntry(item, cache);
+    }
+    return cache;
+  },
+
+  // Run |func|, using the prefetched data in |prefetched| and |cpows|
+  // as a cache.
+  withPrefetching: function(prefetched, cpows, func) {
+    if (!this.prefetchingEnabled) {
+      return func();
+    }
+
+    this.cache = this.generateCache(prefetched, cpows);
+
+    try {
+      log("Prefetching on");
+      return func();
+    } finally {
+      // After we return from this event handler, the content process
+      // is free to continue executing, so we invalidate our cache.
+      log("Prefetching off");
+      this.cache = null;
+    }
+  },
+
+  // Called by shim code in the chrome process to check if target.prop
+  // is cached.
+  lookupInCache: function(addon, target, prop) {
+    if (!this.cache || !Cu.isCrossProcessWrapper(target)) {
+      return null;
+    }
+
+    let propMap = this.cache.get(target);
+    if (!propMap) {
+      return null;
+    }
+
+    return propMap.get(prop);
+  },
+};
+
+let AdblockId = "{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}";
+let AdblockRules = {
+  "ContentPolicy.shouldLoad": [
+    new MethodOp("Node", "InitNode", "QueryInterface", Ci.nsISupports),
+    new PropertyOp("Document", "Node", "ownerDocument"),
+    new PropertyOp("Window", "Node", "defaultView"),
+    new PropertyOp("Window", "Document", "defaultView"),
+    new PropertyOp("TopWindow", "Window", "top"),
+    new PropertyOp("WindowLocation", "Window", "location"),
+    new PropertyOp(null, "WindowLocation", "href"),
+    new PropertyOp("Window", "Window", "parent"),
+    new PropertyOp(null, "Window", "name"),
+    new PropertyOp("Document", "Window", "document"),
+    new PropertyOp("TopDocumentElement", "Document", "documentElement"),
+    new MethodOp(null, "TopDocumentElement", "getAttribute", "data-adblockkey"),
+  ]
+};
+PrefetcherRules[AdblockId] = AdblockRules;
+
+let LastpassId = "support@lastpass.com";
+let LastpassRules = {
+  "EventTarget.handleEvent": [
+    new PropertyOp("EventTarget", "Event", "target"),
+    new PropertyOp("EventOriginalTarget", "Event", "originalTarget"),
+    new PropertyOp("Window", "EventOriginalTarget", "defaultView"),
+
+    new CopyOp("Frame", "Window"),
+    new PropertyOp("FrameCollection", "Window", "frames"),
+    new CollectionOp("Frame", "FrameCollection"),
+    new PropertyOp("FrameCollection", "Frame", "frames"),
+    new PropertyOp("FrameDocument", "Frame", "document"),
+    new PropertyOp(null, "Frame", "window"),
+    new PropertyOp(null, "FrameDocument", "defaultView"),
+
+    new PropertyOp("FrameDocumentLocation", "FrameDocument", "location"),
+    new PropertyOp(null, "FrameDocumentLocation", "href"),
+    new PropertyOp("FrameLocation", "Frame", "location"),
+    new PropertyOp(null, "FrameLocation", "href"),
+
+    new MethodOp("FormCollection", "FrameDocument", "getElementsByTagName", "form"),
+    new MethodOp("FormCollection", "FrameDocument", "getElementsByTagName", "FORM"),
+    new CollectionOp("Form", "FormCollection"),
+    new PropertyOp("FormElementCollection", "Form", "elements"),
+    new CollectionOp("FormElement", "FormElementCollection"),
+    new PropertyOp("Style", "Form", "style"),
+
+    new PropertyOp(null, "FormElement", "type"),
+    new PropertyOp(null, "FormElement", "name"),
+    new PropertyOp(null, "FormElement", "value"),
+    new PropertyOp(null, "FormElement", "tagName"),
+    new PropertyOp(null, "FormElement", "id"),
+    new PropertyOp("Style", "FormElement", "style"),
+
+    new PropertyOp(null, "Style", "visibility"),
+
+    new MethodOp("MetaElementsCollection", "EventOriginalTarget", "getElementsByTagName", "meta"),
+    new CollectionOp("MetaElement", "MetaElementsCollection"),
+    new PropertyOp(null, "MetaElement", "httpEquiv"),
+
+    new MethodOp("InputElementCollection", "FrameDocument", "getElementsByTagName", "input"),
+    new MethodOp("InputElementCollection", "FrameDocument", "getElementsByTagName", "INPUT"),
+    new CollectionOp("InputElement", "InputElementCollection"),
+    new PropertyOp(null, "InputElement", "type"),
+    new PropertyOp(null, "InputElement", "name"),
+    new PropertyOp(null, "InputElement", "tagName"),
+    new PropertyOp(null, "InputElement", "form"),
+
+    new PropertyOp("BodyElement", "FrameDocument", "body"),
+    new PropertyOp("BodyInnerText", "BodyElement", "innerText"),
+
+    new PropertyOp("DocumentFormCollection", "FrameDocument", "forms"),
+    new CollectionOp("DocumentForm", "DocumentFormCollection"),
+  ]
+};
+PrefetcherRules[LastpassId] = LastpassRules;
--- a/toolkit/components/addoncompat/RemoteAddonsChild.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsChild.jsm
@@ -9,16 +9,18 @@ const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
+                                  "resource://gre/modules/Prefetcher.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "SystemPrincipal",
                                    "@mozilla.org/systemprincipal;1", "nsIPrincipal");
 
 // Similar to Python. Returns dict[key] if it exists. Otherwise,
 // sets dict[key] to default_ and returns default_.
 function setDefault(dict, key, default_)
 {
@@ -160,27 +162,31 @@ let ContentPolicyChild = {
       catMan.addCategoryEntry("content-policy", this._contractID, this._contractID, false, true);
     } else {
       catMan.deleteCategoryEntry("content-policy", this._contractID, false);
     }
   },
 
   shouldLoad: function(contentType, contentLocation, requestOrigin,
                        node, mimeTypeGuess, extra, requestPrincipal) {
+    let addons = NotificationTracker.findSuffixes(["content-policy"]);
+    let [prefetched, cpows] = Prefetcher.prefetch("ContentPolicy.shouldLoad",
+                                                  addons, {InitNode: node});
+    cpows.node = node;
+
     let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
                .getService(Ci.nsISyncMessageSender);
     let rval = cpmm.sendRpcMessage("Addons:ContentPolicy:Run", {
       contentType: contentType,
       contentLocation: contentLocation.spec,
       requestOrigin: requestOrigin ? requestOrigin.spec : null,
       mimeTypeGuess: mimeTypeGuess,
       requestPrincipal: requestPrincipal,
-    }, {
-      node: node, // Sent as a CPOW.
-    });
+      prefetched: prefetched,
+    }, cpows);
     if (rval.length != 1) {
       return Ci.nsIContentPolicy.ACCEPT;
     }
 
     return rval[0];
   },
 
   shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra) {
@@ -411,21 +417,30 @@ EventTargetChild.prototype = {
     if (register) {
       this._childGlobal.addEventListener(eventType, listener, useCapture, true);
     } else {
       this._childGlobal.removeEventListener(eventType, listener, useCapture);
     }
   },
 
   handleEvent: function(capturing, event) {
+    let addons = NotificationTracker.findSuffixes(["event", event.type, capturing]);
+    let [prefetched, cpows] = Prefetcher.prefetch("EventTarget.handleEvent",
+                                                  addons,
+                                                  {Event: event,
+                                                   Window: this._childGlobal.content});
+    cpows.event = event;
+    cpows.eventTarget = event.target;
+
     this._childGlobal.sendRpcMessage("Addons:Event:Run",
                                      {type: event.type,
                                       capturing: capturing,
-                                      isTrusted: event.isTrusted},
-                                     {event: event});
+                                      isTrusted: event.isTrusted,
+                                      prefetched: prefetched},
+                                     cpows);
   }
 };
 
 // The parent can create a sandbox to run code in the child
 // process. We actually create the sandbox in the child so that the
 // code runs there. However, managing the lifetime of these sandboxes
 // can be tricky. The parent references these sandboxes using CPOWs,
 // which only keep weak references. So we need to create a strong
@@ -476,16 +491,17 @@ SandboxChild.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                          Ci.nsISupportsWeakReference])
 };
 
 let RemoteAddonsChild = {
   _ready: false,
 
   makeReady: function() {
+    Prefetcher.init();
     NotificationTracker.init();
     ContentPolicyChild.init();
     AboutProtocolChild.init();
     ObserverChild.init();
   },
 
   init: function(global) {
     if (!this._ready) {
--- a/toolkit/components/addoncompat/RemoteAddonsParent.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
@@ -10,16 +10,18 @@ const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import('resource://gre/modules/Services.jsm');
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
+                                  "resource://gre/modules/Prefetcher.jsm");
 
 // Similar to Python. Returns dict[key] if it exists. Otherwise,
 // sets dict[key] to default_ and returns default_.
 function setDefault(dict, key, default_)
 {
   if (key in dict) {
     return dict[key];
   }
@@ -138,23 +140,25 @@ let ContentPolicyParent = {
       } catch (e) {
         // Current Gecko behavior is to ignore entries that don't QI.
         continue;
       }
       try {
         let contentLocation = BrowserUtils.makeURI(aData.contentLocation);
         let requestOrigin = aData.requestOrigin ? BrowserUtils.makeURI(aData.requestOrigin) : null;
 
-        let result = policy.shouldLoad(aData.contentType,
-                                       contentLocation,
-                                       requestOrigin,
-                                       aObjects.node,
-                                       aData.mimeTypeGuess,
-                                       null,
-                                       aData.requestPrincipal);
+        let result = Prefetcher.withPrefetching(aData.prefetched, aObjects, () => {
+          return policy.shouldLoad(aData.contentType,
+                                   contentLocation,
+                                   requestOrigin,
+                                   aObjects.node,
+                                   aData.mimeTypeGuess,
+                                   null,
+                                   aData.requestPrincipal);
+        });
         if (result != Ci.nsIContentPolicy.ACCEPT && result != 0)
           return result;
       } catch (e) {
         Cu.reportError(e);
       }
     }
 
     return Ci.nsIContentPolicy.ACCEPT;
@@ -456,22 +460,24 @@ let EventTargetParent = {
       }
     }
   },
 
   receiveMessage: function(msg) {
     switch (msg.name) {
       case "Addons:Event:Run":
         this.dispatch(msg.target, msg.data.type, msg.data.capturing,
-                      msg.data.isTrusted, msg.objects.event);
+                      msg.data.isTrusted, msg.data.prefetched, msg.objects);
         break;
     }
   },
 
-  dispatch: function(browser, type, capturing, isTrusted, event) {
+  dispatch: function(browser, type, capturing, isTrusted, prefetched, cpows) {
+    let event = cpows.event;
+    let eventTarget = cpows.eventTarget;
     let targets = this.getTargets(browser);
     for (let target of targets) {
       let listeners = this._listeners.get(target);
       if (!listeners) {
         continue;
       }
       let forType = setDefault(listeners, type, []);
 
@@ -480,21 +486,23 @@ let EventTargetParent = {
       for (let {listener, wantsUntrusted, useCapture} of forType) {
         if ((wantsUntrusted || isTrusted) && useCapture == capturing) {
           handlers.push(listener);
         }
       }
 
       for (let handler of handlers) {
         try {
-          if ("handleEvent" in handler) {
-            handler.handleEvent(event);
-          } else {
-            handler.call(event.target, event);
-          }
+          Prefetcher.withPrefetching(prefetched, cpows, () => {
+            if ("handleEvent" in handler) {
+              handler.handleEvent(event);
+            } else {
+              handler.call(event.target, event);
+            }
+          });
         } catch (e) {
           Cu.reportError(e);
         }
       }
     }
   }
 };
 EventTargetParent.init();
@@ -737,42 +745,50 @@ RemoteBrowserElementInterposition.getter
   return target.contentWindowAsCPOW;
 };
 
 let DummyContentDocument = {
   readyState: "loading",
   location: { href: "about:blank" }
 };
 
-RemoteBrowserElementInterposition.getters.contentDocument = function(addon, target) {
-  // If we don't have a CPOW yet, just return something we can use to
-  // examine readyState. This is useful for tests that create a new
-  // tab and then immediately start polling readyState.
-  if (!target.contentDocumentAsCPOW) {
+function getContentDocument(addon, browser)
+{
+  let doc = Prefetcher.lookupInCache(addon, browser.contentWindowAsCPOW, "document");
+  if (doc) {
+    return doc;
+  }
+
+  doc = browser.contentDocumentAsCPOW;
+  if (!doc) {
+    // If we don't have a CPOW yet, just return something we can use to
+    // examine readyState. This is useful for tests that create a new
+    // tab and then immediately start polling readyState.
     return DummyContentDocument;
   }
-  return target.contentDocumentAsCPOW;
+  return doc;
+}
+
+RemoteBrowserElementInterposition.getters.contentDocument = function(addon, target) {
+  return getContentDocument(addon, target);
 };
 
 let TabBrowserElementInterposition = new Interposition("TabBrowserElementInterposition",
                                                        EventTargetInterposition);
 
 TabBrowserElementInterposition.getters.contentWindow = function(addon, target) {
   if (!target.selectedBrowser.contentWindowAsCPOW) {
     return makeDummyContentWindow(target.selectedBrowser);
   }
   return target.selectedBrowser.contentWindowAsCPOW;
 };
 
 TabBrowserElementInterposition.getters.contentDocument = function(addon, target) {
   let browser = target.selectedBrowser;
-  if (!browser.contentDocumentAsCPOW) {
-    return DummyContentDocument;
-  }
-  return browser.contentDocumentAsCPOW;
+  return getContentDocument(addon, browser);
 };
 
 let ChromeWindowInterposition = new Interposition("ChromeWindowInterposition",
                                                   EventTargetInterposition);
 
 ChromeWindowInterposition.getters.content = function(addon, target) {
   let browser = target.gBrowser.selectedBrowser;
   if (!browser.contentWindowAsCPOW) {
--- a/toolkit/components/addoncompat/moz.build
+++ b/toolkit/components/addoncompat/moz.build
@@ -8,11 +8,12 @@ TEST_DIRS += ['tests']
 
 EXTRA_COMPONENTS += [
     'addoncompat.manifest',
     'multiprocessShims.js',
     'remoteTagService.js',
 ]
 
 EXTRA_JS_MODULES += [
+    'Prefetcher.jsm',
     'RemoteAddonsChild.jsm',
     'RemoteAddonsParent.jsm',
 ]
--- a/toolkit/components/addoncompat/multiprocessShims.js
+++ b/toolkit/components/addoncompat/multiprocessShims.js
@@ -4,16 +4,18 @@
 
 "use strict";
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
+                                  "resource://gre/modules/Prefetcher.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RemoteAddonsParent",
                                   "resource://gre/modules/RemoteAddonsParent.jsm");
 
 /**
  * This service overlays the API that the browser exposes to
  * add-ons. The overlay tries to make a multiprocess browser appear as
  * much as possible like a single process browser. An overlay can
  * replace methods, getters, and setters of arbitrary browser objects.
@@ -56,16 +58,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
  * accessed. The functions in |getters| take two parameters: the
  * add-on ID and the original target object. The functions in
  * |setters| take those arguments plus the value that the property is
  * being set to.
  */
 
 function AddonInterpositionService()
 {
+  Prefetcher.init();
   RemoteAddonsParent.init();
 
   // These maps keep track of the interpositions for all different
   // kinds of objects.
   this._interfaceInterpositions = RemoteAddonsParent.getInterfaceInterpositions();
   this._taggedInterpositions = RemoteAddonsParent.getTaggedInterpositions();
 }
 
@@ -111,17 +114,17 @@ AddonInterpositionService.prototype = {
         interp = this._taggedInterpositions[this.getObjectTag(target)];
       }
       catch (e) {
         Cu.reportError(new Components.Exception("Failed to interpose object", e.result, Components.stack.caller));
       }
     }
 
     if (!interp) {
-      return null;
+      return Prefetcher.lookupInCache(addon, target, prop);
     }
 
     let desc = { configurable: false, enumerable: true };
 
     if ("methods" in interp && prop in interp.methods) {
       desc.writable = false;
       desc.value = function(...args) {
         return interp.methods[prop](addon, target, ...args);
@@ -133,13 +136,13 @@ AddonInterpositionService.prototype = {
 
       if ("setters" in interp && prop in interp.setters) {
         desc.set = function(v) { return interp.setters[prop](addon, target, v); };
       }
 
       return desc;
     }
 
-    return null;
+    return Prefetcher.lookupInCache(addon, target, prop);
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonInterpositionService]);