Prefetcher.jsm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. // This Source Code Form is subject to the terms of the Mozilla Public
  2. // License, v. 2.0. If a copy of the MPL was not distributed with this
  3. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
  4. this.EXPORTED_SYMBOLS = ["Prefetcher"];
  5. const Ci = Components.interfaces;
  6. const Cu = Components.utils;
  7. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  8. Cu.import("resource://gre/modules/Services.jsm");
  9. XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
  10. "resource://gre/modules/Preferences.jsm");
  11. // Rules are defined at the bottom of this file.
  12. var PrefetcherRules = {};
  13. /*
  14. * When events that trigger in the content process are forwarded to
  15. * add-ons in the chrome process, we expect the add-ons to send a lot
  16. * of CPOWs to query content nodes while processing the events. To
  17. * speed this up, the prefetching system anticipates which properties
  18. * will be read and reads them ahead of time. The prefetched
  19. * properties are passed to the chrome process along with each
  20. * event. A typical scenario might work like this:
  21. *
  22. * 1. "load" event fires in content
  23. * 2. Content process prefetches:
  24. * event.target.defaultView = <win 1>
  25. * <win 1>.location = <location obj>
  26. * event.target.getElementsByTagName("form") = [<elt 1>, <elt 2>]
  27. * <elt 1>.id = "login-form"
  28. * <elt 2>.id = "subscribe-form"
  29. * 3. Content process forwards "load" event to add-on along with
  30. * prefetched data
  31. * 4. Add-on reads:
  32. * event.target.defaultView (already prefetched)
  33. * event.target.getElementsByTagName("form") (already prefetched)
  34. * <elt 1>.id (already prefetched)
  35. * <elt 1>.className (not prefetched; CPOW must be sent)
  36. *
  37. * The amount of data to prefetch is determined based on the add-on ID
  38. * and the event type. The specific data to select is determined using
  39. * a set of Datalog-like rules (http://en.wikipedia.org/wiki/Datalog).
  40. *
  41. * Rules operate on a series of "tables" like in a database. Each
  42. * table contains a set of content-process objects. When an event
  43. * handler runs, it seeds some initial tables with objects of
  44. * interest. For example, the Event table might start out containing
  45. * the event that fired.
  46. *
  47. * Objects are added to tables using a set of rules of the form "if X
  48. * is in table A, then add F(X) to table B", where F(X) is typically a
  49. * property access or a method call. The most common functions F are:
  50. *
  51. * PropertyOp(destTable, sourceTable, property):
  52. * For each object X in sourceTable, add X.property to destTable.
  53. * MethodOp(destTable, sourceTable, method, args):
  54. * For each object X in sourceTable, add X.method(args) to destTable.
  55. * CollectionOp(destTable, sourceTable):
  56. * For each object X in sourceTable, add X[i] to destTable for
  57. * all i from 0 to X.length - 1.
  58. *
  59. * To generate the prefetching in the example above, the following
  60. * rules would work:
  61. *
  62. * 1. PropertyOp("EventTarget", "Event", "target")
  63. * 2. PropertyOp("Window", "EventTarget", "defaultView")
  64. * 3. MethodOp("FormCollection", "EventTarget", "getElementsByTagName", "form")
  65. * 4. CollectionOp("Form", "FormCollection")
  66. * 5. PropertyOp(null, "Form", "id")
  67. *
  68. * Rules are written at the bottom of this file.
  69. *
  70. * When a rule runs, it will usually generate some cache entries that
  71. * will be passed to the chrome process. For example, when PropertyOp
  72. * prefetches obj.prop and gets the value X, it caches the value of
  73. * obj and X. When the chrome process receives this data, it creates a
  74. * two-level map [obj -> [prop -> X]]. When the add-on accesses a
  75. * property on obj, the add-on shim code consults this map to see if
  76. * the property has already been cached.
  77. */
  78. const PREF_PREFETCHING_ENABLED = "extensions.interposition.prefetching";
  79. function isPrimitive(v) {
  80. if (!v)
  81. return true;
  82. let type = typeof(v);
  83. return type !== "object" && type !== "function";
  84. }
  85. function objAddr(obj)
  86. {
  87. /*
  88. if (!isPrimitive(obj)) {
  89. return String(obj) + "[" + Cu.getJSTestingFunctions().objectAddress(obj) + "]";
  90. }
  91. return String(obj);
  92. */
  93. }
  94. function log(/* ...args*/)
  95. {
  96. /*
  97. for (let arg of args) {
  98. dump(arg);
  99. dump(" ");
  100. }
  101. dump("\n");
  102. */
  103. }
  104. function logPrefetch(/* kind, value1, component, value2*/)
  105. {
  106. /*
  107. log("prefetching", kind, objAddr(value1) + "." + component, "=", objAddr(value2));
  108. */
  109. }
  110. /*
  111. * All the Op classes (representing Datalog rules) have the same interface:
  112. * outputTable: Table that objects generated by the rule are added to.
  113. * Note that this can be null.
  114. * inputTable: Table that the rule draws objects from.
  115. * addObject(database, obj): Called when an object is added to inputTable.
  116. * This code should take care of adding objects to outputTable.
  117. * Data to be cached should be stored by calling database.cache.
  118. * makeCacheEntry(item, cache):
  119. * Called by the chrome process to create the two-level map of
  120. * prefetched objects. |item| holds the cached data
  121. * generated by the content process. |cache| is the map to be
  122. * generated.
  123. */
  124. function PropertyOp(outputTable, inputTable, prop)
  125. {
  126. this.outputTable = outputTable;
  127. this.inputTable = inputTable;
  128. this.prop = prop;
  129. }
  130. PropertyOp.prototype.addObject = function(database, obj)
  131. {
  132. let has = false, propValue;
  133. try {
  134. if (this.prop in obj) {
  135. has = true;
  136. propValue = obj[this.prop];
  137. }
  138. } catch (e) {
  139. // Don't cache anything if an exception is thrown.
  140. return;
  141. }
  142. logPrefetch("prop", obj, this.prop, propValue);
  143. database.cache(this.index, obj, has, propValue);
  144. if (has && !isPrimitive(propValue) && this.outputTable) {
  145. database.add(this.outputTable, propValue);
  146. }
  147. }
  148. PropertyOp.prototype.makeCacheEntry = function(item, cache)
  149. {
  150. let [, obj, , propValue] = item;
  151. let desc = { configurable: false, enumerable: true, writable: false, value: propValue };
  152. if (!cache.has(obj)) {
  153. cache.set(obj, new Map());
  154. }
  155. let propMap = cache.get(obj);
  156. propMap.set(this.prop, desc);
  157. }
  158. function MethodOp(outputTable, inputTable, method, ...args)
  159. {
  160. this.outputTable = outputTable;
  161. this.inputTable = inputTable;
  162. this.method = method;
  163. this.args = args;
  164. }
  165. MethodOp.prototype.addObject = function(database, obj)
  166. {
  167. let result;
  168. try {
  169. result = obj[this.method].apply(obj, this.args);
  170. } catch (e) {
  171. // Don't cache anything if an exception is thrown.
  172. return;
  173. }
  174. logPrefetch("method", obj, this.method + "(" + this.args + ")", result);
  175. database.cache(this.index, obj, result);
  176. if (!isPrimitive(result) && this.outputTable) {
  177. database.add(this.outputTable, result);
  178. }
  179. }
  180. MethodOp.prototype.makeCacheEntry = function(item, cache)
  181. {
  182. let [, obj, result] = item;
  183. if (!cache.has(obj)) {
  184. cache.set(obj, new Map());
  185. }
  186. let propMap = cache.get(obj);
  187. let fallback = propMap.get(this.method);
  188. let method = this.method;
  189. let selfArgs = this.args;
  190. let methodImpl = function(...args) {
  191. if (args.length == selfArgs.length && args.every((v, i) => v === selfArgs[i])) {
  192. return result;
  193. }
  194. if (fallback) {
  195. return fallback.value(...args);
  196. }
  197. return obj[method](...args);
  198. };
  199. let desc = { configurable: false, enumerable: true, writable: false, value: methodImpl };
  200. propMap.set(this.method, desc);
  201. }
  202. function CollectionOp(outputTable, inputTable)
  203. {
  204. this.outputTable = outputTable;
  205. this.inputTable = inputTable;
  206. }
  207. CollectionOp.prototype.addObject = function(database, obj)
  208. {
  209. let elements = [];
  210. try {
  211. let len = obj.length;
  212. for (let i = 0; i < len; i++) {
  213. logPrefetch("index", obj, i, obj[i]);
  214. elements.push(obj[i]);
  215. }
  216. } catch (e) {
  217. // Don't cache anything if an exception is thrown.
  218. return;
  219. }
  220. database.cache(this.index, obj, ...elements);
  221. for (let i = 0; i < elements.length; i++) {
  222. if (!isPrimitive(elements[i]) && this.outputTable) {
  223. database.add(this.outputTable, elements[i]);
  224. }
  225. }
  226. }
  227. CollectionOp.prototype.makeCacheEntry = function(item, cache)
  228. {
  229. let [, obj, ...elements] = item;
  230. if (!cache.has(obj)) {
  231. cache.set(obj, new Map());
  232. }
  233. let propMap = cache.get(obj);
  234. let lenDesc = { configurable: false, enumerable: true, writable: false, value: elements.length };
  235. propMap.set("length", lenDesc);
  236. for (let i = 0; i < elements.length; i++) {
  237. let desc = { configurable: false, enumerable: true, writable: false, value: elements[i] };
  238. propMap.set(i, desc);
  239. }
  240. }
  241. function CopyOp(outputTable, inputTable)
  242. {
  243. this.outputTable = outputTable;
  244. this.inputTable = inputTable;
  245. }
  246. CopyOp.prototype.addObject = function(database, obj)
  247. {
  248. database.add(this.outputTable, obj);
  249. }
  250. function Database(trigger, addons)
  251. {
  252. // Create a map of rules that apply to this specific trigger and set
  253. // of add-ons. The rules are indexed based on their inputTable.
  254. this.rules = new Map();
  255. for (let addon of addons) {
  256. let addonRules = PrefetcherRules[addon] || {};
  257. let triggerRules = addonRules[trigger] || [];
  258. for (let rule of triggerRules) {
  259. let inTable = rule.inputTable;
  260. if (!this.rules.has(inTable)) {
  261. this.rules.set(inTable, new Set());
  262. }
  263. let set = this.rules.get(inTable);
  264. set.add(rule);
  265. }
  266. }
  267. // this.tables maps table names to sets of objects contained in them.
  268. this.tables = new Map();
  269. // todo is a worklist of items added to tables that have not had
  270. // rules run on them yet.
  271. this.todo = [];
  272. // Cached data to be sent to the chrome process.
  273. this.cached = [];
  274. }
  275. Database.prototype = {
  276. // Add an object to a table.
  277. add: function(table, obj) {
  278. if (!this.tables.has(table)) {
  279. this.tables.set(table, new Set());
  280. }
  281. let tableSet = this.tables.get(table);
  282. if (tableSet.has(obj)) {
  283. return;
  284. }
  285. tableSet.add(obj);
  286. this.todo.push([table, obj]);
  287. },
  288. cache: function(...args) {
  289. this.cached.push(args);
  290. },
  291. // Run a fixed-point iteration that adds objects to table based on
  292. // this.rules until there are no more objects to add.
  293. process: function() {
  294. while (this.todo.length) {
  295. let [table, obj] = this.todo.pop();
  296. let rules = this.rules.get(table);
  297. if (!rules) {
  298. continue;
  299. }
  300. for (let rule of rules) {
  301. rule.addObject(this, obj);
  302. }
  303. }
  304. },
  305. };
  306. var Prefetcher = {
  307. init: function() {
  308. // Give an index to each rule and store it in this.ruleMap based
  309. // on the index. The index is used to serialize and deserialize
  310. // data from content to chrome.
  311. let counter = 0;
  312. this.ruleMap = new Map();
  313. for (let addon in PrefetcherRules) {
  314. for (let trigger in PrefetcherRules[addon]) {
  315. for (let rule of PrefetcherRules[addon][trigger]) {
  316. rule.index = counter++;
  317. this.ruleMap.set(rule.index, rule);
  318. }
  319. }
  320. }
  321. this.prefetchingEnabled = Preferences.get(PREF_PREFETCHING_ENABLED, false);
  322. Services.prefs.addObserver(PREF_PREFETCHING_ENABLED, this, false);
  323. Services.obs.addObserver(this, "xpcom-shutdown", false);
  324. },
  325. observe: function(subject, topic, data) {
  326. if (topic == "xpcom-shutdown") {
  327. Services.prefs.removeObserver(PREF_PREFETCHING_ENABLED, this);
  328. Services.obs.removeObserver(this, "xpcom-shutdown");
  329. } else if (topic == PREF_PREFETCHING_ENABLED) {
  330. this.prefetchingEnabled = Preferences.get(PREF_PREFETCHING_ENABLED, false);
  331. }
  332. },
  333. // Called when an event occurs in the content process. The event is
  334. // described by the trigger string. |addons| is a list of addons
  335. // that have listeners installed for the event. |args| is
  336. // event-specific data (such as the event object).
  337. prefetch: function(trigger, addons, args) {
  338. if (!this.prefetchingEnabled) {
  339. return [[], []];
  340. }
  341. let db = new Database(trigger, addons);
  342. for (let table in args) {
  343. log("root", table, "=", objAddr(args[table]));
  344. db.add(table, args[table]);
  345. }
  346. // Prefetch objects and add them to tables.
  347. db.process();
  348. // Data passed to sendAsyncMessage must be split into a JSON
  349. // portion and a CPOW portion. This code splits apart db.cached
  350. // into these two pieces. Any object in db.cache is added to an
  351. // array of CPOWs and replaced with {cpow: <index in array>}.
  352. let cpowIndexes = new Map();
  353. let prefetched = [];
  354. let cpows = [];
  355. for (let item of db.cached) {
  356. item = item.map((elt) => {
  357. if (!isPrimitive(elt)) {
  358. if (!cpowIndexes.has(elt)) {
  359. let index = cpows.length;
  360. cpows.push(elt);
  361. cpowIndexes.set(elt, index);
  362. }
  363. return {cpow: cpowIndexes.get(elt)};
  364. }
  365. return elt;
  366. });
  367. prefetched.push(item);
  368. }
  369. return [prefetched, cpows];
  370. },
  371. cache: null,
  372. // Generate a two-level mapping based on cached data received from
  373. // the content process.
  374. generateCache: function(prefetched, cpows) {
  375. let cache = new Map();
  376. for (let item of prefetched) {
  377. // Replace anything of the form {cpow: <index>} with the actual
  378. // object in |cpows|.
  379. item = item.map((elt) => {
  380. if (!isPrimitive(elt)) {
  381. return cpows[elt.cpow];
  382. }
  383. return elt;
  384. });
  385. let index = item[0];
  386. let op = this.ruleMap.get(index);
  387. op.makeCacheEntry(item, cache);
  388. }
  389. return cache;
  390. },
  391. // Run |func|, using the prefetched data in |prefetched| and |cpows|
  392. // as a cache.
  393. withPrefetching: function(prefetched, cpows, func) {
  394. if (!this.prefetchingEnabled) {
  395. return func();
  396. }
  397. this.cache = this.generateCache(prefetched, cpows);
  398. try {
  399. log("Prefetching on");
  400. return func();
  401. } finally {
  402. // After we return from this event handler, the content process
  403. // is free to continue executing, so we invalidate our cache.
  404. log("Prefetching off");
  405. this.cache = null;
  406. }
  407. },
  408. // Called by shim code in the chrome process to check if target.prop
  409. // is cached.
  410. lookupInCache: function(addon, target, prop) {
  411. if (!this.cache || !Cu.isCrossProcessWrapper(target)) {
  412. return null;
  413. }
  414. let propMap = this.cache.get(target);
  415. if (!propMap) {
  416. return null;
  417. }
  418. return propMap.get(prop);
  419. },
  420. };
  421. var AdblockId = "{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}";
  422. var AdblockRules = {
  423. "ContentPolicy.shouldLoad": [
  424. new MethodOp("Node", "InitNode", "QueryInterface", Ci.nsISupports),
  425. new PropertyOp("Document", "Node", "ownerDocument"),
  426. new PropertyOp("Window", "Node", "defaultView"),
  427. new PropertyOp("Window", "Document", "defaultView"),
  428. new PropertyOp("TopWindow", "Window", "top"),
  429. new PropertyOp("WindowLocation", "Window", "location"),
  430. new PropertyOp(null, "WindowLocation", "href"),
  431. new PropertyOp("Window", "Window", "parent"),
  432. new PropertyOp(null, "Window", "name"),
  433. new PropertyOp("Document", "Window", "document"),
  434. new PropertyOp("TopDocumentElement", "Document", "documentElement"),
  435. new MethodOp(null, "TopDocumentElement", "getAttribute", "data-adblockkey"),
  436. ]
  437. };
  438. PrefetcherRules[AdblockId] = AdblockRules;
  439. var LastpassId = "support@lastpass.com";
  440. var LastpassRules = {
  441. "EventTarget.handleEvent": [
  442. new PropertyOp("EventTarget", "Event", "target"),
  443. new PropertyOp("EventOriginalTarget", "Event", "originalTarget"),
  444. new PropertyOp("Window", "EventOriginalTarget", "defaultView"),
  445. new CopyOp("Frame", "Window"),
  446. new PropertyOp("FrameCollection", "Window", "frames"),
  447. new CollectionOp("Frame", "FrameCollection"),
  448. new PropertyOp("FrameCollection", "Frame", "frames"),
  449. new PropertyOp("FrameDocument", "Frame", "document"),
  450. new PropertyOp(null, "Frame", "window"),
  451. new PropertyOp(null, "FrameDocument", "defaultView"),
  452. new PropertyOp("FrameDocumentLocation", "FrameDocument", "location"),
  453. new PropertyOp(null, "FrameDocumentLocation", "href"),
  454. new PropertyOp("FrameLocation", "Frame", "location"),
  455. new PropertyOp(null, "FrameLocation", "href"),
  456. new MethodOp("FormCollection", "FrameDocument", "getElementsByTagName", "form"),
  457. new MethodOp("FormCollection", "FrameDocument", "getElementsByTagName", "FORM"),
  458. new CollectionOp("Form", "FormCollection"),
  459. new PropertyOp("FormElementCollection", "Form", "elements"),
  460. new CollectionOp("FormElement", "FormElementCollection"),
  461. new PropertyOp("Style", "Form", "style"),
  462. new PropertyOp(null, "FormElement", "type"),
  463. new PropertyOp(null, "FormElement", "name"),
  464. new PropertyOp(null, "FormElement", "value"),
  465. new PropertyOp(null, "FormElement", "tagName"),
  466. new PropertyOp(null, "FormElement", "id"),
  467. new PropertyOp("Style", "FormElement", "style"),
  468. new PropertyOp(null, "Style", "visibility"),
  469. new MethodOp("MetaElementsCollection", "EventOriginalTarget", "getElementsByTagName", "meta"),
  470. new CollectionOp("MetaElement", "MetaElementsCollection"),
  471. new PropertyOp(null, "MetaElement", "httpEquiv"),
  472. new MethodOp("InputElementCollection", "FrameDocument", "getElementsByTagName", "input"),
  473. new MethodOp("InputElementCollection", "FrameDocument", "getElementsByTagName", "INPUT"),
  474. new CollectionOp("InputElement", "InputElementCollection"),
  475. new PropertyOp(null, "InputElement", "type"),
  476. new PropertyOp(null, "InputElement", "name"),
  477. new PropertyOp(null, "InputElement", "tagName"),
  478. new PropertyOp(null, "InputElement", "form"),
  479. new PropertyOp("BodyElement", "FrameDocument", "body"),
  480. new PropertyOp("BodyInnerText", "BodyElement", "innerText"),
  481. new PropertyOp("DocumentFormCollection", "FrameDocument", "forms"),
  482. new CollectionOp("DocumentForm", "DocumentFormCollection"),
  483. ]
  484. };
  485. PrefetcherRules[LastpassId] = LastpassRules;