AtomicEventLoop.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. /*
  2. * Pure Ecmascript eventloop example.
  3. * Original Version: https://github.com/svaarala/duktape/blob/master/examples/eventloop/ecma_eventloop.js
  4. *
  5. * Timer state handling is inefficient in this trivial example. Timers are
  6. * kept in an array sorted by their expiry time which works well for expiring
  7. * timers, but has O(n) insertion performance. A better implementation would
  8. * use a heap or some other efficient structure for managing timers so that
  9. * all operations (insert, remove, get nearest timer) have good performance.
  10. *
  11. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Timers
  12. *
  13. * CHANGES
  14. * TSH - August 2015
  15. * - Removed EventLoop.run as it was looking at Sockets which Atomic doesn't provide. Now, this subscribes to the Atomic.engine.update/afterUpdate events to process the timers.
  16. * - Modified the way that setTimeout,clearTimeout,setInterval,clearInterval are loaded into global scope
  17. * - Implemented setImmediate/clearImmediate
  18. *
  19. * Jay Sistar - April 2016
  20. * - Added requestAnimationFrame and cancelAnimationFrame for per frame callbacks
  21. */
  22. /*
  23. * Event loop
  24. *
  25. * Timers are sorted by 'target' property which indicates expiry time of
  26. * the timer. The timer expiring next is last in the array, so that
  27. * removals happen at the end, and inserts for timers expiring in the
  28. * near future displace as few elements in the array as possible.
  29. */
  30. EventLoop = {
  31. currentTime: 0, // Current timer in the system. Need to make sure this doesn't overflow
  32. // timers
  33. timers: [], // active timers, sorted (nearest expiry last)
  34. immediateTimers: [],
  35. expiring: null, // set to timer being expired (needs special handling in clearTimeout/clearInterval/clearImmediate)
  36. nextTimerId: 1,
  37. minimumDelay: 1,
  38. maxExpirys: 10,
  39. // per frame events
  40. pfeCallbacks: [],
  41. pfeCurrentTime: 0.0,
  42. // misc
  43. exitRequested: false
  44. };
  45. EventLoop.dumpState = function () {
  46. print('TIMER STATE:');
  47. this.timers.forEach(function (t) {
  48. print(' ' + Duktape.enc('jx', t));
  49. });
  50. print('IMMEDIATE TIMER STATE:');
  51. this.immediateTimers.forEach(function (t) {
  52. print(' ' + Duktape.enc('jx', t));
  53. });
  54. if (this.expiring) {
  55. print(' EXPIRING: ' + Duktape.enc('jx', this.expiring));
  56. }
  57. }
  58. // Get timer with lowest expiry time. Since the active timers list is
  59. // sorted, it's always the last timer.
  60. EventLoop.getEarliestTimer = function () {
  61. var timers = this.timers;
  62. n = timers.length;
  63. return (n > 0 ? timers[n - 1] : null);
  64. }
  65. EventLoop.getEarliestWait = function () {
  66. var t = this.getEarliestTimer();
  67. return (t ? t.target - Date.now() : null);
  68. }
  69. EventLoop.insertTimer = function (timer) {
  70. var timers = this.timers;
  71. var i, n, t;
  72. /*
  73. * Find 'i' such that we want to insert *after* timers[i] at index i+1.
  74. * If no such timer, for-loop terminates with i-1, and we insert at -1+1=0.
  75. */
  76. n = timers.length;
  77. for (i = n - 1; i >= 0; i--) {
  78. t = timers[i];
  79. if (timer.target <= t.target) {
  80. // insert after 't', to index i+1
  81. break;
  82. }
  83. }
  84. timers.splice(i + 1 /*start*/ , 0 /*deleteCount*/ , timer);
  85. }
  86. EventLoop.insertImmediate = function (timer) {
  87. var timers = this.immediateTimers;
  88. // Just simply add to the end of the immediate timers array
  89. timers.push(timer);
  90. }
  91. // Remove timer/interval with a timer ID. The timer/interval can reside
  92. // either on the active list or it may be an expired timer (this.expiring)
  93. // whose user callback we're running when this function gets called.
  94. EventLoop.removeTimerById = function (timer_id, isImmediate) {
  95. var timers = isImmediate ? this.immediateTimers : this.timers;
  96. var i, n, t;
  97. t = this.expiring;
  98. if (t) {
  99. if (t.id === timer_id) {
  100. // Timer has expired and we're processing its callback. User
  101. // callback has requested timer deletion. Mark removed, so
  102. // that the timer is not reinserted back into the active list.
  103. // This is actually a common case because an interval may very
  104. // well cancel itself.
  105. t.removed = true;
  106. return;
  107. }
  108. }
  109. n = timers.length;
  110. for (i = 0; i < n; i++) {
  111. t = timers[i];
  112. if (t.id === timer_id) {
  113. // Timer on active list: mark removed (not really necessary, but
  114. // nice for dumping), and remove from active list.
  115. t.removed = true;
  116. timers.splice(i /*start*/ , 1 /*deleteCount*/ );
  117. return;
  118. }
  119. }
  120. // no such ID, ignore
  121. }
  122. EventLoop.processTimers = function () {
  123. var now = Date.now();
  124. var timers = this.timers;
  125. var sanity = this.maxExpirys;
  126. var n, t;
  127. /*
  128. * Here we must be careful with mutations: user callback may add and
  129. * delete an arbitrary number of timers.
  130. *
  131. * Current solution is simple: check whether the timer at the end of
  132. * the list has expired. If not, we're done. If it has expired,
  133. * remove it from the active list, record it in this.expiring, and call
  134. * the user callback. If user code deletes the this.expiring timer,
  135. * there is special handling which just marks the timer deleted so
  136. * it won't get inserted back into the active list.
  137. *
  138. * This process is repeated at most maxExpirys times to ensure we don't
  139. * get stuck forever; user code could in principle add more and more
  140. * already expired timers.
  141. */
  142. while (sanity-- > 0) {
  143. // If exit requested, don't call any more callbacks. This allows
  144. // a callback to do cleanups and request exit, and can be sure that
  145. // no more callbacks are processed.
  146. if (this.exitRequested) {
  147. //print('exit requested, exit');
  148. break;
  149. }
  150. // Timers to expire?
  151. n = timers.length;
  152. if (timers.length <= 0) {
  153. break;
  154. }
  155. t = timers[n - 1];
  156. if (now <= t.target) {
  157. // Timer has not expired, and no other timer could have expired
  158. // either because the list is sorted.
  159. break;
  160. }
  161. timers.pop();
  162. // Remove the timer from the active list and process it. The user
  163. // callback may add new timers which is not a problem. The callback
  164. // may also delete timers which is not a problem unless the timer
  165. // being deleted is the timer whose callback we're running; this is
  166. // why the timer is recorded in this.expiring so that clearTimeout()
  167. // and clearInterval() can detect this situation.
  168. if (t.oneshot) {
  169. t.removed = true; // flag for removal
  170. } else {
  171. t.target = now + t.delay;
  172. }
  173. this.expiring = t;
  174. try {
  175. t.cb();
  176. } catch (e) {
  177. console.log('timer callback failed, ignored: ' + e);
  178. }
  179. this.expiring = null;
  180. // If the timer was one-shot, it's marked 'removed'. If the user callback
  181. // requested deletion for the timer, it's also marked 'removed'. If the
  182. // timer is an interval (and is not marked removed), insert it back into
  183. // the timer list.
  184. if (!t.removed) {
  185. // Reinsert interval timer to correct sorted position. The timer
  186. // must be an interval timer because one-shot timers are marked
  187. // 'removed' above.
  188. this.insertTimer(t);
  189. }
  190. }
  191. // see if there are any events left over for this cycle
  192. n = timers.length;
  193. if (timers.length <= 0) {
  194. return false;
  195. }
  196. t = timers[n - 1];
  197. if (now > t.target) {
  198. // we have more timers that need to be handled. Let the driver know that we should pick them up in the next cycle
  199. return true;
  200. } else {
  201. return false;
  202. }
  203. }
  204. EventLoop.processImmediates = function () {
  205. var timers = this.immediateTimers;
  206. var sanity = timers.length;
  207. /*
  208. * Here we must be careful with mutations: user callback may add and
  209. * delete an arbitrary number of set immediates.
  210. *
  211. * The way we handle this is that if any setImmediates adds another setImmediate to the
  212. * list, it will wait until the end of the next cycle..not sure this is the best way to handle
  213. * this, but if not you could easily implement a DOS where the update handler never gets returned to
  214. */
  215. while (sanity-- > 0) {
  216. // If exit requested, don't call any more callbacks. This allows
  217. // a callback to do cleanups and request exit, and can be sure that
  218. // no more callbacks are processed.
  219. if (this.exitRequested) {
  220. //print('exit requested, exit');
  221. break;
  222. }
  223. if (timers.length <= 0) {
  224. break;
  225. }
  226. // we want to process setImmediate timers FIFO
  227. t = timers.shift();
  228. // Remove the timer from the active list and process it. The user
  229. // callback may add new timers which is not a problem. The callback
  230. // may also delete timers which is not a problem unless the timer
  231. // being deleted is the timer whose callback we're running; this is
  232. // why the timer is recorded in this.expiring so that clearTimeout()
  233. // and clearInterval() can detect this situation.
  234. t.removed = true; // flag for removal
  235. this.expiring = t;
  236. try {
  237. t.cb();
  238. } catch (e) {
  239. console.log('timer callback failed, ignored: ' + e);
  240. }
  241. this.expiring = null;
  242. }
  243. }
  244. EventLoop.processPerFrameEvents = function(eventData) {
  245. this.pfeCurrentTime += eventData.timeStep;
  246. var callbacks = this.pfeCallbacks;
  247. this.pfeCallbacks = [];
  248. callbacks.forEach(function(callback) {
  249. // The second parameter isn't standard, but it's useful in Atomic.
  250. callback(this.pfeCurrentTime, eventData.timeStep);
  251. }.bind(this));
  252. }
  253. EventLoop.requestExit = function () {
  254. this.exitRequested = true;
  255. }
  256. /*
  257. * Per Frame Event API
  258. *
  259. * These interface with the singleton EventLoop.
  260. */
  261. /**
  262. * schedules a function to be called on the next frame
  263. * @method
  264. * @param {function} func the Function to call
  265. * @returns {int} the id of the callback to be used in cancelAnimationFrame in order to cancel the call
  266. */
  267. function requestAnimationFrame(step) {
  268. if (typeof step !== "function") {
  269. throw '"requestAnimationFrame" only accepts a function.'
  270. }
  271. var call_id = EventLoop.pfeCallbacks.length;
  272. EventLoop.pfeCallbacks.push(step);
  273. return call_id;
  274. }
  275. /**
  276. * stops a previously queued call to requestAnimationFrame
  277. * @method
  278. * @param {int} call_id the id of the timer that was created via requestAnimationFrame
  279. */
  280. function cancelAnimationFrame(call_id) {
  281. EventLoop.pfeCallbacks[call_id] = function(){}; // NOP
  282. }
  283. /*
  284. * Timer API
  285. *
  286. * These interface with the singleton EventLoop.
  287. */
  288. /**
  289. * schedules a function to be called after a certain number of milliseconds
  290. * @method
  291. * @param {function} func the Function to call
  292. * @param {int} delay the number of milliseconds
  293. * @param {any} [parameters] A comma separated list of parameters to pass to func
  294. * @returns {int} the id of the timer to be used in clearTimeout in order to cancel the timeout
  295. */
  296. function setTimeout(func, delay) {
  297. var cb_func;
  298. var bind_args;
  299. var timer_id;
  300. var evloop = EventLoop;
  301. if (typeof delay !== 'number') {
  302. throw new TypeError('delay is not a number');
  303. }
  304. delay = Math.max(evloop.minimumDelay, delay);
  305. if (typeof func === 'string') {
  306. // Legacy case: callback is a string.
  307. cb_func = eval.bind(this, func);
  308. } else if (typeof func !== 'function') {
  309. throw new TypeError('callback is not a function/string');
  310. } else if (arguments.length > 2) {
  311. // Special case: callback arguments are provided.
  312. bind_args = Array.prototype.slice.call(arguments, 2); // [ arg1, arg2, ... ]
  313. bind_args.unshift(this); // [ global(this), arg1, arg2, ... ]
  314. cb_func = func.bind.apply(func, bind_args);
  315. } else {
  316. // Normal case: callback given as a function without arguments.
  317. cb_func = func;
  318. }
  319. timer_id = evloop.nextTimerId++;
  320. evloop.insertTimer({
  321. id: timer_id,
  322. oneshot: true,
  323. cb: cb_func,
  324. delay: delay,
  325. target: Date.now() + delay
  326. });
  327. return timer_id;
  328. }
  329. /**
  330. * stops a previously queued timeout.
  331. * @method
  332. * @param {int} timer_id the id of the timer that was created via setTimeout
  333. */
  334. function clearTimeout(timer_id) {
  335. var evloop = EventLoop;
  336. if (typeof timer_id !== 'number') {
  337. throw new TypeError('timer ID is not a number');
  338. }
  339. evloop.removeTimerById(timer_id, false);
  340. }
  341. /**
  342. * schedules a function to be called after the end of the current update cycle. SetImmediate timers are processed FIFO
  343. * @method
  344. * @param {function} func the Function to call
  345. * @param {any} [parameters] A comma separated list of parameters to pass to func
  346. * @returns {int} the id of the setImmediate function to be used in clearImmediate in order to cancel it.
  347. */
  348. function setImmediate(func) {
  349. var cb_func;
  350. var bind_args;
  351. var timer_id;
  352. var evloop = EventLoop;
  353. if (typeof func === 'string') {
  354. // Legacy case: callback is a string.
  355. cb_func = eval.bind(this, func);
  356. } else if (typeof func !== 'function') {
  357. throw new TypeError('callback is not a function/string');
  358. } else if (arguments.length > 1) {
  359. // Special case: callback arguments are provided.
  360. bind_args = Array.prototype.slice.call(arguments, 1); // [ arg1, arg2, ... ]
  361. bind_args.unshift(this); // [ global(this), arg1, arg2, ... ]
  362. cb_func = func.bind.apply(func, bind_args);
  363. } else {
  364. // Normal case: callback given as a function without arguments.
  365. cb_func = func;
  366. }
  367. timer_id = evloop.nextTimerId++;
  368. evloop.insertImmediate({
  369. id: timer_id,
  370. oneshot: true,
  371. cb: cb_func
  372. });
  373. return timer_id;
  374. }
  375. /**
  376. * stops a previously queued setImmediate callback.
  377. * @method
  378. * @param {int} timer_id the id of the timer that was created via setImmediate
  379. */
  380. function clearImmediate(timer_id) {
  381. var evloop = EventLoop;
  382. if (typeof timer_id !== 'number') {
  383. throw new TypeError('timer ID is not a number');
  384. }
  385. evloop.removeTimerById(timer_id, true);
  386. }
  387. /**
  388. * set up a timer that repeats every "delay" milliseconds.
  389. * @method
  390. * @param {function} func callback function to call every "delay" milliseconds
  391. * @param {int} delay number of milliseconds to delay
  392. * @returns {int} the id of the timer so that it can be stopped in the future.
  393. */
  394. function setInterval(func, delay) {
  395. var cb_func;
  396. var bind_args;
  397. var timer_id;
  398. var evloop = EventLoop;
  399. if (typeof delay !== 'number') {
  400. throw new TypeError('delay is not a number');
  401. }
  402. delay = Math.max(evloop.minimumDelay, delay);
  403. if (typeof func === 'string') {
  404. // Legacy case: callback is a string.
  405. cb_func = eval.bind(this, func);
  406. } else if (typeof func !== 'function') {
  407. throw new TypeError('callback is not a function/string');
  408. } else if (arguments.length > 2) {
  409. // Special case: callback arguments are provided.
  410. bind_args = Array.prototype.slice.call(arguments, 2); // [ arg1, arg2, ... ]
  411. bind_args.unshift(this); // [ global(this), arg1, arg2, ... ]
  412. cb_func = func.bind.apply(func, bind_args);
  413. } else {
  414. // Normal case: callback given as a function without arguments.
  415. cb_func = func;
  416. }
  417. timer_id = evloop.nextTimerId++;
  418. evloop.insertTimer({
  419. id: timer_id,
  420. oneshot: false,
  421. cb: cb_func,
  422. delay: delay,
  423. target: Date.now() + delay
  424. });
  425. return timer_id;
  426. }
  427. /**
  428. * stop a repeated timer that has been set.
  429. * @method
  430. * @param {int} timer_id the id of the timer that was created via setInterval
  431. */
  432. function clearInterval(timer_id) {
  433. var evloop = EventLoop;
  434. if (typeof timer_id !== 'number') {
  435. throw new TypeError('timer ID is not a number');
  436. }
  437. evloop.removeTimerById(timer_id);
  438. }
  439. /* custom call */
  440. /**
  441. * custom call to exit out of the current event loop.
  442. */
  443. function requestEventLoopExit() {
  444. EventLoop.requestExit();
  445. }
  446. // Create a closure that will throttle the EventLoop.processTimers function from being executed too often
  447. function throttleProcessTimers(ms) {
  448. var deltaTimer = 0;
  449. var pendingOps = false;
  450. function doThrottle(eventData) {
  451. EventLoop.processPerFrameEvents(eventData);
  452. deltaTimer += eventData.timeStep * 1000;
  453. if (deltaTimer > ms || pendingOps) {
  454. deltaTimer -= ms;
  455. // double check to see if we have more timers that expire during this sim
  456. // if we do, then we want to process them in the next update, and not
  457. // wait 100ms
  458. pendingOps = EventLoop.processTimers();
  459. }
  460. }
  461. return doThrottle;
  462. }
  463. // Hook into the update event of the engine and process all the timers. These
  464. // will be throttled to only run every 100ms
  465. Atomic.engine.subscribeToEvent('Update', throttleProcessTimers(100));
  466. // Hook into the postUpdate event of the engine and process all the setImmediate calls. These should
  467. // really process once the current update loop is completed.
  468. Atomic.engine.subscribeToEvent('PostUpdate', function (eventData) {
  469. EventLoop.processImmediates();
  470. });
  471. // Load up the global methods . This module doesn't export anything, it just sets up the global methods.
  472. (function (global) {
  473. global.setInterval = global.setInterval || setInterval;
  474. global.clearInterval = global.clearInterval || clearInterval;
  475. global.setTimeout = global.setTimeout || setTimeout;
  476. global.clearTimeout = global.clearTimeout || clearTimeout;
  477. global.setImmediate = global.setImmediate || setImmediate;
  478. global.clearImmediate = global.clearImmediate || clearImmediate;
  479. global.requestAnimationFrame = global.requestAnimationFrame || requestAnimationFrame;
  480. global.cancelAnimationFrame = global.cancelAnimationFrame || cancelAnimationFrame;
  481. global.requestEventLoopExit = global.requestEventLoopExit || requestEventLoopExit;
  482. })(new Function('return this')());