BrowserNewTabPreloader.jsm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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 file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"];
  6. const Cu = Components.utils;
  7. const Cc = Components.classes;
  8. const Ci = Components.interfaces;
  9. Cu.import("resource://gre/modules/Services.jsm");
  10. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  11. Cu.import("resource://gre/modules/Promise.jsm");
  12. const HTML_NS = "http://www.w3.org/1999/xhtml";
  13. const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
  14. const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>";
  15. const NEWTAB_URL = "about:newtab";
  16. const PREF_BRANCH = "browser.newtab.";
  17. // The interval between swapping in a preload docShell and kicking off the
  18. // next preload in the background.
  19. const PRELOADER_INTERVAL_MS = 600;
  20. // The initial delay before we start preloading our first new tab page. The
  21. // timer is started after the first 'browser-delayed-startup' has been sent.
  22. const PRELOADER_INIT_DELAY_MS = 5000;
  23. // The number of miliseconds we'll wait after we received a notification that
  24. // causes us to update our list of browsers and tabbrowser sizes. This acts as
  25. // kind of a damper when too many events are occuring in quick succession.
  26. const PRELOADER_UPDATE_DELAY_MS = 3000;
  27. const TOPIC_TIMER_CALLBACK = "timer-callback";
  28. const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished";
  29. const TOPIC_XUL_WINDOW_CLOSED = "xul-window-destroyed";
  30. function createTimer(obj, delay) {
  31. let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  32. timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT);
  33. return timer;
  34. }
  35. function clearTimer(timer) {
  36. if (timer) {
  37. timer.cancel();
  38. }
  39. return null;
  40. }
  41. this.BrowserNewTabPreloader = {
  42. init: function() {
  43. Initializer.start();
  44. },
  45. uninit: function() {
  46. Initializer.stop();
  47. HostFrame.destroy();
  48. Preferences.uninit();
  49. HiddenBrowsers.uninit();
  50. },
  51. newTab: function(aTab) {
  52. let win = aTab.ownerDocument.defaultView;
  53. if (win.gBrowser) {
  54. let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
  55. .getInterface(Ci.nsIDOMWindowUtils);
  56. let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser);
  57. let hiddenBrowser = HiddenBrowsers.get(width, height)
  58. if (hiddenBrowser) {
  59. return hiddenBrowser.swapWithNewTab(aTab);
  60. }
  61. }
  62. return false;
  63. }
  64. };
  65. Object.freeze(BrowserNewTabPreloader);
  66. var Initializer = {
  67. _timer: null,
  68. _observing: false,
  69. start: function() {
  70. Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
  71. this._observing = true;
  72. },
  73. stop: function() {
  74. this._timer = clearTimer(this._timer);
  75. if (this._observing) {
  76. Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
  77. this._observing = false;
  78. }
  79. },
  80. observe: function(aSubject, aTopic, aData) {
  81. if (aTopic == TOPIC_DELAYED_STARTUP) {
  82. Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
  83. this._observing = false;
  84. this._startTimer();
  85. } else if (aTopic == TOPIC_TIMER_CALLBACK) {
  86. this._timer = null;
  87. this._startPreloader();
  88. }
  89. },
  90. _startTimer: function() {
  91. this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS);
  92. },
  93. _startPreloader: function() {
  94. Preferences.init();
  95. if (Preferences.enabled) {
  96. HiddenBrowsers.init();
  97. }
  98. }
  99. };
  100. var Preferences = {
  101. _enabled: null,
  102. _branch: null,
  103. get enabled() {
  104. if (this._enabled === null) {
  105. this._enabled = this._branch.getBoolPref("preload") &&
  106. !this._branch.prefHasUserValue("url");
  107. }
  108. return this._enabled;
  109. },
  110. init: function() {
  111. this._branch = Services.prefs.getBranch(PREF_BRANCH);
  112. this._branch.addObserver("", this, false);
  113. },
  114. uninit: function() {
  115. if (this._branch) {
  116. this._branch.removeObserver("", this);
  117. this._branch = null;
  118. }
  119. },
  120. observe: function() {
  121. let prevEnabled = this._enabled;
  122. this._enabled = null;
  123. if (prevEnabled && !this.enabled) {
  124. HiddenBrowsers.uninit();
  125. } else if (!prevEnabled && this.enabled) {
  126. HiddenBrowsers.init();
  127. }
  128. },
  129. };
  130. var HiddenBrowsers = {
  131. _browsers: null,
  132. _updateTimer: null,
  133. _topics: [
  134. TOPIC_DELAYED_STARTUP,
  135. TOPIC_XUL_WINDOW_CLOSED
  136. ],
  137. init: function() {
  138. this._browsers = new Map();
  139. this._updateBrowserSizes();
  140. this._topics.forEach(t => Services.obs.addObserver(this, t, false));
  141. },
  142. uninit: function() {
  143. if (this._browsers) {
  144. this._topics.forEach(t => Services.obs.removeObserver(this, t, false));
  145. this._updateTimer = clearTimer(this._updateTimer);
  146. for (let [key, browser] of this._browsers) {
  147. browser.destroy();
  148. }
  149. this._browsers = null;
  150. }
  151. },
  152. get: function(width, height) {
  153. // We haven't been initialized, yet.
  154. if (!this._browsers) {
  155. return null;
  156. }
  157. let key = width + "x" + height;
  158. if (!this._browsers.has(key)) {
  159. // Update all browsers' sizes if we can't find a matching one.
  160. this._updateBrowserSizes();
  161. }
  162. // We should now have a matching browser.
  163. if (this._browsers.has(key)) {
  164. return this._browsers.get(key);
  165. }
  166. // We should never be here. Return the first browser we find.
  167. Cu.reportError("NewTabPreloader: no matching browser found after updating");
  168. for (let [size, browser] of this._browsers) {
  169. return browser;
  170. }
  171. // We should really never be here.
  172. Cu.reportError("NewTabPreloader: not even a single browser was found?");
  173. return null;
  174. },
  175. observe: function(subject, topic, data) {
  176. if (topic === TOPIC_TIMER_CALLBACK) {
  177. this._updateTimer = null;
  178. this._updateBrowserSizes();
  179. } else {
  180. this._updateTimer = clearTimer(this._updateTimer);
  181. this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
  182. }
  183. },
  184. _updateBrowserSizes: function() {
  185. let sizes = this._collectTabBrowserSizes();
  186. let toRemove = [];
  187. // Iterate all browsers and check that they
  188. // each can be assigned to one of the sizes.
  189. for (let [key, browser] of this._browsers) {
  190. if (sizes.has(key)) {
  191. // We already have a browser for that size, great!
  192. sizes.delete(key);
  193. } else {
  194. // This browser is superfluous or needs to be resized.
  195. toRemove.push(browser);
  196. this._browsers.delete(key);
  197. }
  198. }
  199. // Iterate all sizes that we couldn't find a browser for.
  200. for (let [key, {width, height}] of sizes) {
  201. let browser;
  202. if (toRemove.length) {
  203. // Let's just resize one of the superfluous
  204. // browsers and put it back into the map.
  205. browser = toRemove.shift();
  206. browser.resize(width, height);
  207. } else {
  208. // No more browsers to reuse, create a new one.
  209. browser = new HiddenBrowser(width, height);
  210. }
  211. this._browsers.set(key, browser);
  212. }
  213. // Finally, remove all browsers we don't need anymore.
  214. toRemove.forEach(b => b.destroy());
  215. },
  216. _collectTabBrowserSizes: function() {
  217. let sizes = new Map();
  218. function tabBrowserBounds() {
  219. let wins = Services.ww.getWindowEnumerator("navigator:browser");
  220. while (wins.hasMoreElements()) {
  221. let win = wins.getNext();
  222. if (win.gBrowser) {
  223. let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
  224. .getInterface(Ci.nsIDOMWindowUtils);
  225. yield utils.getBoundsWithoutFlushing(win.gBrowser);
  226. }
  227. }
  228. }
  229. // Collect the sizes of all <tabbrowser>s out there.
  230. for (let {width, height} of tabBrowserBounds()) {
  231. if (width > 0 && height > 0) {
  232. let key = width + "x" + height;
  233. if (!sizes.has(key)) {
  234. sizes.set(key, {width: width, height: height});
  235. }
  236. }
  237. }
  238. return sizes;
  239. }
  240. };
  241. function HiddenBrowser(width, height) {
  242. this.resize(width, height);
  243. HostFrame.get().then(aFrame => {
  244. let doc = aFrame.document;
  245. this._browser = doc.createElementNS(XUL_NS, "browser");
  246. this._browser.setAttribute("type", "content");
  247. this._browser.setAttribute("src", NEWTAB_URL);
  248. this._applySize();
  249. doc.getElementById("win").appendChild(this._browser);
  250. });
  251. }
  252. HiddenBrowser.prototype = {
  253. _width: null,
  254. _height: null,
  255. _timer: null,
  256. _needsFrameScripts: true,
  257. get isPreloaded() {
  258. return this._browser &&
  259. this._browser.contentDocument &&
  260. this._browser.contentDocument.readyState === "complete" &&
  261. this._browser.currentURI.spec === NEWTAB_URL;
  262. },
  263. swapWithNewTab: function(aTab) {
  264. if (!this.isPreloaded || this._timer) {
  265. return false;
  266. }
  267. let win = aTab.ownerDocument.defaultView;
  268. let tabbrowser = win.gBrowser;
  269. if (!tabbrowser) {
  270. return false;
  271. }
  272. // Swap docShells.
  273. tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
  274. // Load all default frame scripts.
  275. if (this._needsFrameScripts) {
  276. this._needsFrameScripts = false;
  277. let mm = aTab.linkedBrowser.messageManager;
  278. mm.loadFrameScript("chrome://browser/content/content.js", true);
  279. mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
  280. if ("TabView" in win) {
  281. mm.loadFrameScript("chrome://browser/content/tabview-content.js", true);
  282. }
  283. }
  284. // Start a timer that will kick off preloading the next newtab page.
  285. this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
  286. // Signal that we swapped docShells.
  287. return true;
  288. },
  289. observe: function() {
  290. this._timer = null;
  291. // Start pre-loading the new tab page.
  292. this._browser.loadURI(NEWTAB_URL);
  293. },
  294. resize: function(width, height) {
  295. this._width = width;
  296. this._height = height;
  297. this._applySize();
  298. },
  299. _applySize: function() {
  300. if (this._browser) {
  301. this._browser.style.width = this._width + "px";
  302. this._browser.style.height = this._height + "px";
  303. }
  304. },
  305. destroy: function() {
  306. if (this._browser) {
  307. this._browser.remove();
  308. this._browser = null;
  309. }
  310. this._timer = clearTimer(this._timer);
  311. }
  312. };
  313. var HostFrame = {
  314. _frame: null,
  315. _deferred: null,
  316. get hiddenDOMDocument() {
  317. return Services.appShell.hiddenDOMWindow.document;
  318. },
  319. get isReady() {
  320. return this.hiddenDOMDocument.readyState === "complete";
  321. },
  322. get: function() {
  323. if (!this._deferred) {
  324. this._deferred = Promise.defer();
  325. this._create();
  326. }
  327. return this._deferred.promise;
  328. },
  329. destroy: function() {
  330. if (this._frame) {
  331. if (!Cu.isDeadWrapper(this._frame)) {
  332. this._frame.removeEventListener("load", this, true);
  333. this._frame.remove();
  334. }
  335. this._frame = null;
  336. this._deferred = null;
  337. }
  338. },
  339. handleEvent: function() {
  340. let contentWindow = this._frame.contentWindow;
  341. if (contentWindow.location.href === XUL_PAGE) {
  342. this._frame.removeEventListener("load", this, true);
  343. this._deferred.resolve(contentWindow);
  344. } else {
  345. contentWindow.location = XUL_PAGE;
  346. }
  347. },
  348. _create: function() {
  349. if (this.isReady) {
  350. let doc = this.hiddenDOMDocument;
  351. this._frame = doc.createElementNS(HTML_NS, "iframe");
  352. this._frame.addEventListener("load", this, true);
  353. doc.documentElement.appendChild(this._frame);
  354. } else {
  355. let flags = Ci.nsIThread.DISPATCH_NORMAL;
  356. Services.tm.currentThread.dispatch(() => this._create(), flags);
  357. }
  358. }
  359. };