reflow.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  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. "use strict";
  5. /**
  6. * About the types of objects in this file:
  7. *
  8. * - ReflowActor: the actor class used for protocol purposes.
  9. * Mostly empty, just gets an instance of LayoutChangesObserver and forwards
  10. * its "reflows" events to clients.
  11. *
  12. * - LayoutChangesObserver: extends Observable and uses the ReflowObserver, to
  13. * track reflows on the page.
  14. * Used by the LayoutActor, but is also exported on the module, so can be used
  15. * by any other actor that needs it.
  16. *
  17. * - Observable: A utility parent class, meant at being extended by classes that
  18. * need a to observe something on the tabActor's windows.
  19. *
  20. * - Dedicated observers: There's only one of them for now: ReflowObserver which
  21. * listens to reflow events via the docshell,
  22. * These dedicated classes are used by the LayoutChangesObserver.
  23. */
  24. const {Ci} = require("chrome");
  25. const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
  26. const protocol = require("devtools/shared/protocol");
  27. const {method, Arg} = protocol;
  28. const events = require("sdk/event/core");
  29. const Heritage = require("sdk/core/heritage");
  30. const EventEmitter = require("devtools/shared/event-emitter");
  31. const {reflowSpec} = require("devtools/shared/specs/reflow");
  32. /**
  33. * The reflow actor tracks reflows and emits events about them.
  34. */
  35. var ReflowActor = exports.ReflowActor = protocol.ActorClassWithSpec(reflowSpec, {
  36. initialize: function (conn, tabActor) {
  37. protocol.Actor.prototype.initialize.call(this, conn);
  38. this.tabActor = tabActor;
  39. this._onReflow = this._onReflow.bind(this);
  40. this.observer = getLayoutChangesObserver(tabActor);
  41. this._isStarted = false;
  42. },
  43. /**
  44. * The reflow actor is the first (and last) in its hierarchy to use
  45. * protocol.js so it doesn't have a parent protocol actor that takes care of
  46. * its lifetime. So it needs a disconnect method to cleanup.
  47. */
  48. disconnect: function () {
  49. this.destroy();
  50. },
  51. destroy: function () {
  52. this.stop();
  53. releaseLayoutChangesObserver(this.tabActor);
  54. this.observer = null;
  55. this.tabActor = null;
  56. protocol.Actor.prototype.destroy.call(this);
  57. },
  58. /**
  59. * Start tracking reflows and sending events to clients about them.
  60. * This is a oneway method, do not expect a response and it won't return a
  61. * promise.
  62. */
  63. start: function () {
  64. if (!this._isStarted) {
  65. this.observer.on("reflows", this._onReflow);
  66. this._isStarted = true;
  67. }
  68. },
  69. /**
  70. * Stop tracking reflows and sending events to clients about them.
  71. * This is a oneway method, do not expect a response and it won't return a
  72. * promise.
  73. */
  74. stop: function () {
  75. if (this._isStarted) {
  76. this.observer.off("reflows", this._onReflow);
  77. this._isStarted = false;
  78. }
  79. },
  80. _onReflow: function (event, reflows) {
  81. if (this._isStarted) {
  82. events.emit(this, "reflows", reflows);
  83. }
  84. }
  85. });
  86. /**
  87. * Base class for all sorts of observers that need to listen to events on the
  88. * tabActor's windows.
  89. * @param {TabActor} tabActor
  90. * @param {Function} callback Executed everytime the observer observes something
  91. */
  92. function Observable(tabActor, callback) {
  93. this.tabActor = tabActor;
  94. this.callback = callback;
  95. this._onWindowReady = this._onWindowReady.bind(this);
  96. this._onWindowDestroyed = this._onWindowDestroyed.bind(this);
  97. events.on(this.tabActor, "window-ready", this._onWindowReady);
  98. events.on(this.tabActor, "window-destroyed", this._onWindowDestroyed);
  99. }
  100. Observable.prototype = {
  101. /**
  102. * Is the observer currently observing
  103. */
  104. isObserving: false,
  105. /**
  106. * Stop observing and detroy this observer instance
  107. */
  108. destroy: function () {
  109. if (this.isDestroyed) {
  110. return;
  111. }
  112. this.isDestroyed = true;
  113. this.stop();
  114. events.off(this.tabActor, "window-ready", this._onWindowReady);
  115. events.off(this.tabActor, "window-destroyed", this._onWindowDestroyed);
  116. this.callback = null;
  117. this.tabActor = null;
  118. },
  119. /**
  120. * Start observing whatever it is this observer is supposed to observe
  121. */
  122. start: function () {
  123. if (this.isObserving) {
  124. return;
  125. }
  126. this.isObserving = true;
  127. this._startListeners(this.tabActor.windows);
  128. },
  129. /**
  130. * Stop observing
  131. */
  132. stop: function () {
  133. if (!this.isObserving) {
  134. return;
  135. }
  136. this.isObserving = false;
  137. if (this.tabActor.attached && this.tabActor.docShell) {
  138. // It's only worth stopping if the tabActor is still attached
  139. this._stopListeners(this.tabActor.windows);
  140. }
  141. },
  142. _onWindowReady: function ({window}) {
  143. if (this.isObserving) {
  144. this._startListeners([window]);
  145. }
  146. },
  147. _onWindowDestroyed: function ({window}) {
  148. if (this.isObserving) {
  149. this._stopListeners([window]);
  150. }
  151. },
  152. _startListeners: function (windows) {
  153. // To be implemented by sub-classes.
  154. },
  155. _stopListeners: function (windows) {
  156. // To be implemented by sub-classes.
  157. },
  158. /**
  159. * To be called by sub-classes when something has been observed
  160. */
  161. notifyCallback: function (...args) {
  162. this.isObserving && this.callback && this.callback.apply(null, args);
  163. }
  164. };
  165. /**
  166. * The LayouChangesObserver will observe reflows as soon as it is started.
  167. * Some devtools actors may cause reflows and it may be wanted to "hide" these
  168. * reflows from the LayouChangesObserver consumers.
  169. * If this is the case, such actors should require this module and use this
  170. * global function to turn the ignore mode on and off temporarily.
  171. *
  172. * Note that if a node is provided, it will be used to force a sync reflow to
  173. * make sure all reflows which occurred before switching the mode on or off are
  174. * either observed or ignored depending on the current mode.
  175. *
  176. * @param {Boolean} ignore
  177. * @param {DOMNode} syncReflowNode The node to use to force a sync reflow
  178. */
  179. var gIgnoreLayoutChanges = false;
  180. exports.setIgnoreLayoutChanges = function (ignore, syncReflowNode) {
  181. if (syncReflowNode) {
  182. let forceSyncReflow = syncReflowNode.offsetWidth;
  183. }
  184. gIgnoreLayoutChanges = ignore;
  185. };
  186. /**
  187. * The LayoutChangesObserver class is instantiated only once per given tab
  188. * and is used to track reflows and dom and style changes in that tab.
  189. * The LayoutActor uses this class to send reflow events to its clients.
  190. *
  191. * This class isn't exported on the module because it shouldn't be instantiated
  192. * to avoid creating several instances per tabs.
  193. * Use `getLayoutChangesObserver(tabActor)`
  194. * and `releaseLayoutChangesObserver(tabActor)`
  195. * which are exported to get and release instances.
  196. *
  197. * The observer loops every EVENT_BATCHING_DELAY ms and checks if layout changes
  198. * have happened since the last loop iteration. If there are, it sends the
  199. * corresponding events:
  200. *
  201. * - "reflows", with an array of all the reflows that occured,
  202. * - "resizes", with an array of all the resizes that occured,
  203. *
  204. * @param {TabActor} tabActor
  205. */
  206. function LayoutChangesObserver(tabActor) {
  207. this.tabActor = tabActor;
  208. this._startEventLoop = this._startEventLoop.bind(this);
  209. this._onReflow = this._onReflow.bind(this);
  210. this._onResize = this._onResize.bind(this);
  211. // Creating the various observers we're going to need
  212. // For now, just the reflow observer, but later we can add markupMutation,
  213. // styleSheetChanges and styleRuleChanges
  214. this.reflowObserver = new ReflowObserver(this.tabActor, this._onReflow);
  215. this.resizeObserver = new WindowResizeObserver(this.tabActor, this._onResize);
  216. EventEmitter.decorate(this);
  217. }
  218. exports.LayoutChangesObserver = LayoutChangesObserver;
  219. LayoutChangesObserver.prototype = {
  220. /**
  221. * How long does this observer waits before emitting batched events.
  222. * The lower the value, the more event packets will be sent to clients,
  223. * potentially impacting performance.
  224. * The higher the value, the more time we'll wait, this is better for
  225. * performance but has an effect on how soon changes are shown in the toolbox.
  226. */
  227. EVENT_BATCHING_DELAY: 300,
  228. /**
  229. * Destroying this instance of LayoutChangesObserver will stop the batched
  230. * events from being sent.
  231. */
  232. destroy: function () {
  233. this.isObserving = false;
  234. this.reflowObserver.destroy();
  235. this.reflows = null;
  236. this.resizeObserver.destroy();
  237. this.hasResized = false;
  238. this.tabActor = null;
  239. },
  240. start: function () {
  241. if (this.isObserving) {
  242. return;
  243. }
  244. this.isObserving = true;
  245. this.reflows = [];
  246. this.hasResized = false;
  247. this._startEventLoop();
  248. this.reflowObserver.start();
  249. this.resizeObserver.start();
  250. },
  251. stop: function () {
  252. if (!this.isObserving) {
  253. return;
  254. }
  255. this.isObserving = false;
  256. this._stopEventLoop();
  257. this.reflows = [];
  258. this.hasResized = false;
  259. this.reflowObserver.stop();
  260. this.resizeObserver.stop();
  261. },
  262. /**
  263. * Start the event loop, which regularly checks if there are any observer
  264. * events to be sent as batched events
  265. * Calls itself in a loop.
  266. */
  267. _startEventLoop: function () {
  268. // Avoid emitting events if the tabActor has been detached (may happen
  269. // during shutdown)
  270. if (!this.tabActor || !this.tabActor.attached) {
  271. return;
  272. }
  273. // Send any reflows we have
  274. if (this.reflows && this.reflows.length) {
  275. this.emit("reflows", this.reflows);
  276. this.reflows = [];
  277. }
  278. // Send any resizes we have
  279. if (this.hasResized) {
  280. this.emit("resize");
  281. this.hasResized = false;
  282. }
  283. this.eventLoopTimer = this._setTimeout(this._startEventLoop,
  284. this.EVENT_BATCHING_DELAY);
  285. },
  286. _stopEventLoop: function () {
  287. this._clearTimeout(this.eventLoopTimer);
  288. },
  289. // Exposing set/clearTimeout here to let tests override them if needed
  290. _setTimeout: function (cb, ms) {
  291. return setTimeout(cb, ms);
  292. },
  293. _clearTimeout: function (t) {
  294. return clearTimeout(t);
  295. },
  296. /**
  297. * Executed whenever a reflow is observed. Only stacks the reflow in the
  298. * reflows array.
  299. * The EVENT_BATCHING_DELAY loop will take care of it later.
  300. * @param {Number} start When the reflow started
  301. * @param {Number} end When the reflow ended
  302. * @param {Boolean} isInterruptible
  303. */
  304. _onReflow: function (start, end, isInterruptible) {
  305. if (gIgnoreLayoutChanges) {
  306. return;
  307. }
  308. // XXX: when/if bug 997092 gets fixed, we will be able to know which
  309. // elements have been reflowed, which would be a nice thing to add here.
  310. this.reflows.push({
  311. start: start,
  312. end: end,
  313. isInterruptible: isInterruptible
  314. });
  315. },
  316. /**
  317. * Executed whenever a resize is observed. Only store a flag saying that a
  318. * resize occured.
  319. * The EVENT_BATCHING_DELAY loop will take care of it later.
  320. */
  321. _onResize: function () {
  322. if (gIgnoreLayoutChanges) {
  323. return;
  324. }
  325. this.hasResized = true;
  326. }
  327. };
  328. /**
  329. * Get a LayoutChangesObserver instance for a given window. This function makes
  330. * sure there is only one instance per window.
  331. * @param {TabActor} tabActor
  332. * @return {LayoutChangesObserver}
  333. */
  334. var observedWindows = new Map();
  335. function getLayoutChangesObserver(tabActor) {
  336. let observerData = observedWindows.get(tabActor);
  337. if (observerData) {
  338. observerData.refCounting ++;
  339. return observerData.observer;
  340. }
  341. let obs = new LayoutChangesObserver(tabActor);
  342. observedWindows.set(tabActor, {
  343. observer: obs,
  344. // counting references allows to stop the observer when no tabActor owns an
  345. // instance.
  346. refCounting: 1
  347. });
  348. obs.start();
  349. return obs;
  350. }
  351. exports.getLayoutChangesObserver = getLayoutChangesObserver;
  352. /**
  353. * Release a LayoutChangesObserver instance that was retrieved by
  354. * getLayoutChangesObserver. This is required to ensure the tabActor reference
  355. * is removed and the observer is eventually stopped and destroyed.
  356. * @param {TabActor} tabActor
  357. */
  358. function releaseLayoutChangesObserver(tabActor) {
  359. let observerData = observedWindows.get(tabActor);
  360. if (!observerData) {
  361. return;
  362. }
  363. observerData.refCounting --;
  364. if (!observerData.refCounting) {
  365. observerData.observer.destroy();
  366. observedWindows.delete(tabActor);
  367. }
  368. }
  369. exports.releaseLayoutChangesObserver = releaseLayoutChangesObserver;
  370. /**
  371. * Reports any reflow that occurs in the tabActor's docshells.
  372. * @extends Observable
  373. * @param {TabActor} tabActor
  374. * @param {Function} callback Executed everytime a reflow occurs
  375. */
  376. function ReflowObserver(tabActor, callback) {
  377. Observable.call(this, tabActor, callback);
  378. }
  379. ReflowObserver.prototype = Heritage.extend(Observable.prototype, {
  380. QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
  381. Ci.nsISupportsWeakReference]),
  382. _startListeners: function (windows) {
  383. for (let window of windows) {
  384. let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
  385. .getInterface(Ci.nsIWebNavigation)
  386. .QueryInterface(Ci.nsIDocShell);
  387. docshell.addWeakReflowObserver(this);
  388. }
  389. },
  390. _stopListeners: function (windows) {
  391. for (let window of windows) {
  392. try {
  393. let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
  394. .getInterface(Ci.nsIWebNavigation)
  395. .QueryInterface(Ci.nsIDocShell);
  396. docshell.removeWeakReflowObserver(this);
  397. } catch (e) {
  398. // Corner cases where a global has already been freed may happen, in
  399. // which case, no need to remove the observer.
  400. }
  401. }
  402. },
  403. reflow: function (start, end) {
  404. this.notifyCallback(start, end, false);
  405. },
  406. reflowInterruptible: function (start, end) {
  407. this.notifyCallback(start, end, true);
  408. }
  409. });
  410. /**
  411. * Reports window resize events on the tabActor's windows.
  412. * @extends Observable
  413. * @param {TabActor} tabActor
  414. * @param {Function} callback Executed everytime a resize occurs
  415. */
  416. function WindowResizeObserver(tabActor, callback) {
  417. Observable.call(this, tabActor, callback);
  418. this.onResize = this.onResize.bind(this);
  419. }
  420. WindowResizeObserver.prototype = Heritage.extend(Observable.prototype, {
  421. _startListeners: function () {
  422. this.listenerTarget.addEventListener("resize", this.onResize);
  423. },
  424. _stopListeners: function () {
  425. this.listenerTarget.removeEventListener("resize", this.onResize);
  426. },
  427. onResize: function () {
  428. this.notifyCallback();
  429. },
  430. get listenerTarget() {
  431. // For the rootActor, return its window.
  432. if (this.tabActor.isRootActor) {
  433. return this.tabActor.window;
  434. }
  435. // Otherwise, get the tabActor's chromeEventHandler.
  436. return this.tabActor.window.QueryInterface(Ci.nsIInterfaceRequestor)
  437. .getInterface(Ci.nsIWebNavigation)
  438. .QueryInterface(Ci.nsIDocShell)
  439. .chromeEventHandler;
  440. }
  441. });