EventLoop.js 16 KB

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