AtomicEventLoop.js 16 KB

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