| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- /*
- * Pure Ecmascript eventloop example.
- * Original Version: https://github.com/svaarala/duktape/blob/master/examples/eventloop/ecma_eventloop.js
- *
- * Timer state handling is inefficient in this trivial example. Timers are
- * kept in an array sorted by their expiry time which works well for expiring
- * timers, but has O(n) insertion performance. A better implementation would
- * use a heap or some other efficient structure for managing timers so that
- * all operations (insert, remove, get nearest timer) have good performance.
- *
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Timers
- *
- * CHANGES
- * TSH - August 2015
- * - 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.
- * - Modified the way that setTimeout,clearTimeout,setInterval,clearInterval are loaded into global scope
- * - Implemented setImmediate/clearImmediate
- *
- * Jay Sistar - April 2016
- * - Added requestAnimationFrame and cancelAnimationFrame for per frame callbacks
- */
- /*
- * Event loop
- *
- * Timers are sorted by 'target' property which indicates expiry time of
- * the timer. The timer expiring next is last in the array, so that
- * removals happen at the end, and inserts for timers expiring in the
- * near future displace as few elements in the array as possible.
- */
- EventLoop = {
- currentTime: 0, // Current timer in the system. Need to make sure this doesn't overflow
- // timers
- timers: [], // active timers, sorted (nearest expiry last)
- immediateTimers: [],
- expiring: null, // set to timer being expired (needs special handling in clearTimeout/clearInterval/clearImmediate)
- nextTimerId: 1,
- minimumDelay: 1,
- maxExpirys: 10,
- // per frame events
- pfeCallbacks: [],
- pfeCurrentTime: 0.0,
- // misc
- exitRequested: false
- };
- EventLoop.dumpState = function () {
- print('TIMER STATE:');
- this.timers.forEach(function (t) {
- print(' ' + Duktape.enc('jx', t));
- });
- print('IMMEDIATE TIMER STATE:');
- this.immediateTimers.forEach(function (t) {
- print(' ' + Duktape.enc('jx', t));
- });
- if (this.expiring) {
- print(' EXPIRING: ' + Duktape.enc('jx', this.expiring));
- }
- }
- // Get timer with lowest expiry time. Since the active timers list is
- // sorted, it's always the last timer.
- EventLoop.getEarliestTimer = function () {
- var timers = this.timers;
- n = timers.length;
- return (n > 0 ? timers[n - 1] : null);
- }
- EventLoop.getEarliestWait = function () {
- var t = this.getEarliestTimer();
- return (t ? t.target - Date.now() : null);
- }
- EventLoop.insertTimer = function (timer) {
- var timers = this.timers;
- var i, n, t;
- /*
- * Find 'i' such that we want to insert *after* timers[i] at index i+1.
- * If no such timer, for-loop terminates with i-1, and we insert at -1+1=0.
- */
- n = timers.length;
- for (i = n - 1; i >= 0; i--) {
- t = timers[i];
- if (timer.target <= t.target) {
- // insert after 't', to index i+1
- break;
- }
- }
- timers.splice(i + 1 /*start*/ , 0 /*deleteCount*/ , timer);
- }
- EventLoop.insertImmediate = function (timer) {
- var timers = this.immediateTimers;
- // Just simply add to the end of the immediate timers array
- timers.push(timer);
- }
- // Remove timer/interval with a timer ID. The timer/interval can reside
- // either on the active list or it may be an expired timer (this.expiring)
- // whose user callback we're running when this function gets called.
- EventLoop.removeTimerById = function (timer_id, isImmediate) {
- var timers = isImmediate ? this.immediateTimers : this.timers;
- var i, n, t;
- t = this.expiring;
- if (t) {
- if (t.id === timer_id) {
- // Timer has expired and we're processing its callback. User
- // callback has requested timer deletion. Mark removed, so
- // that the timer is not reinserted back into the active list.
- // This is actually a common case because an interval may very
- // well cancel itself.
- t.removed = true;
- return;
- }
- }
- n = timers.length;
- for (i = 0; i < n; i++) {
- t = timers[i];
- if (t.id === timer_id) {
- // Timer on active list: mark removed (not really necessary, but
- // nice for dumping), and remove from active list.
- t.removed = true;
- timers.splice(i /*start*/ , 1 /*deleteCount*/ );
- return;
- }
- }
- // no such ID, ignore
- }
- EventLoop.processTimers = function () {
- var now = Date.now();
- var timers = this.timers;
- var sanity = this.maxExpirys;
- var n, t;
- /*
- * Here we must be careful with mutations: user callback may add and
- * delete an arbitrary number of timers.
- *
- * Current solution is simple: check whether the timer at the end of
- * the list has expired. If not, we're done. If it has expired,
- * remove it from the active list, record it in this.expiring, and call
- * the user callback. If user code deletes the this.expiring timer,
- * there is special handling which just marks the timer deleted so
- * it won't get inserted back into the active list.
- *
- * This process is repeated at most maxExpirys times to ensure we don't
- * get stuck forever; user code could in principle add more and more
- * already expired timers.
- */
- while (sanity-- > 0) {
- // If exit requested, don't call any more callbacks. This allows
- // a callback to do cleanups and request exit, and can be sure that
- // no more callbacks are processed.
- if (this.exitRequested) {
- //print('exit requested, exit');
- break;
- }
- // Timers to expire?
- n = timers.length;
- if (timers.length <= 0) {
- break;
- }
- t = timers[n - 1];
- if (now <= t.target) {
- // Timer has not expired, and no other timer could have expired
- // either because the list is sorted.
- break;
- }
- timers.pop();
- // Remove the timer from the active list and process it. The user
- // callback may add new timers which is not a problem. The callback
- // may also delete timers which is not a problem unless the timer
- // being deleted is the timer whose callback we're running; this is
- // why the timer is recorded in this.expiring so that clearTimeout()
- // and clearInterval() can detect this situation.
- if (t.oneshot) {
- t.removed = true; // flag for removal
- } else {
- t.target = now + t.delay;
- }
- this.expiring = t;
- try {
- t.cb();
- } catch (e) {
- console.log('timer callback failed, ignored: ' + e);
- }
- this.expiring = null;
- // If the timer was one-shot, it's marked 'removed'. If the user callback
- // requested deletion for the timer, it's also marked 'removed'. If the
- // timer is an interval (and is not marked removed), insert it back into
- // the timer list.
- if (!t.removed) {
- // Reinsert interval timer to correct sorted position. The timer
- // must be an interval timer because one-shot timers are marked
- // 'removed' above.
- this.insertTimer(t);
- }
- }
- // see if there are any events left over for this cycle
- n = timers.length;
- if (timers.length <= 0) {
- return false;
- }
- t = timers[n - 1];
- if (now > t.target) {
- // we have more timers that need to be handled. Let the driver know that we should pick them up in the next cycle
- return true;
- } else {
- return false;
- }
- }
- EventLoop.processImmediates = function () {
- var timers = this.immediateTimers;
- var sanity = timers.length;
- /*
- * Here we must be careful with mutations: user callback may add and
- * delete an arbitrary number of set immediates.
- *
- * The way we handle this is that if any setImmediates adds another setImmediate to the
- * list, it will wait until the end of the next cycle..not sure this is the best way to handle
- * this, but if not you could easily implement a DOS where the update handler never gets returned to
- */
- while (sanity-- > 0) {
- // If exit requested, don't call any more callbacks. This allows
- // a callback to do cleanups and request exit, and can be sure that
- // no more callbacks are processed.
- if (this.exitRequested) {
- //print('exit requested, exit');
- break;
- }
- if (timers.length <= 0) {
- break;
- }
- // we want to process setImmediate timers FIFO
- t = timers.shift();
- // Remove the timer from the active list and process it. The user
- // callback may add new timers which is not a problem. The callback
- // may also delete timers which is not a problem unless the timer
- // being deleted is the timer whose callback we're running; this is
- // why the timer is recorded in this.expiring so that clearTimeout()
- // and clearInterval() can detect this situation.
- t.removed = true; // flag for removal
- this.expiring = t;
- try {
- t.cb();
- } catch (e) {
- console.log('timer callback failed, ignored: ' + e);
- }
- this.expiring = null;
- }
- }
- EventLoop.processPerFrameEvents = function(eventData) {
- this.pfeCurrentTime += eventData.timeStep;
- var callbacks = this.pfeCallbacks;
- this.pfeCallbacks = [];
- callbacks.forEach(function(callback) {
- // The second parameter isn't standard, but it's useful in Atomic.
- callback(this.pfeCurrentTime, eventData.timeStep);
- }.bind(this));
- }
- EventLoop.requestExit = function () {
- this.exitRequested = true;
- }
- /*
- * Per Frame Event API
- *
- * These interface with the singleton EventLoop.
- */
- /**
- * schedules a function to be called on the next frame
- * @method
- * @param {function} func the Function to call
- * @returns {int} the id of the callback to be used in cancelAnimationFrame in order to cancel the call
- */
- function requestAnimationFrame(step) {
- if (typeof step !== "function") {
- throw '"requestAnimationFrame" only accepts a function.'
- }
- var call_id = EventLoop.pfeCallbacks.length;
- EventLoop.pfeCallbacks.push(step);
- return call_id;
- }
- /**
- * stops a previously queued call to requestAnimationFrame
- * @method
- * @param {int} call_id the id of the timer that was created via requestAnimationFrame
- */
- function cancelAnimationFrame(call_id) {
- EventLoop.pfeCallbacks[call_id] = function(){}; // NOP
- }
- /*
- * Timer API
- *
- * These interface with the singleton EventLoop.
- */
- /**
- * schedules a function to be called after a certain number of milliseconds
- * @method
- * @param {function} func the Function to call
- * @param {int} delay the number of milliseconds
- * @param {any} [parameters] A comma separated list of parameters to pass to func
- * @returns {int} the id of the timer to be used in clearTimeout in order to cancel the timeout
- */
- function setTimeout(func, delay) {
- var cb_func;
- var bind_args;
- var timer_id;
- var evloop = EventLoop;
- if (typeof delay !== 'number') {
- throw new TypeError('delay is not a number');
- }
- delay = Math.max(evloop.minimumDelay, delay);
- if (typeof func === 'string') {
- // Legacy case: callback is a string.
- cb_func = eval.bind(this, func);
- } else if (typeof func !== 'function') {
- throw new TypeError('callback is not a function/string');
- } else if (arguments.length > 2) {
- // Special case: callback arguments are provided.
- bind_args = Array.prototype.slice.call(arguments, 2); // [ arg1, arg2, ... ]
- bind_args.unshift(this); // [ global(this), arg1, arg2, ... ]
- cb_func = func.bind.apply(func, bind_args);
- } else {
- // Normal case: callback given as a function without arguments.
- cb_func = func;
- }
- timer_id = evloop.nextTimerId++;
- evloop.insertTimer({
- id: timer_id,
- oneshot: true,
- cb: cb_func,
- delay: delay,
- target: Date.now() + delay
- });
- return timer_id;
- }
- /**
- * stops a previously queued timeout.
- * @method
- * @param {int} timer_id the id of the timer that was created via setTimeout
- */
- function clearTimeout(timer_id) {
- var evloop = EventLoop;
- if (typeof timer_id !== 'number') {
- throw new TypeError('timer ID is not a number');
- }
- evloop.removeTimerById(timer_id, false);
- }
- /**
- * schedules a function to be called after the end of the current update cycle. SetImmediate timers are processed FIFO
- * @method
- * @param {function} func the Function to call
- * @param {any} [parameters] A comma separated list of parameters to pass to func
- * @returns {int} the id of the setImmediate function to be used in clearImmediate in order to cancel it.
- */
- function setImmediate(func) {
- var cb_func;
- var bind_args;
- var timer_id;
- var evloop = EventLoop;
- if (typeof func === 'string') {
- // Legacy case: callback is a string.
- cb_func = eval.bind(this, func);
- } else if (typeof func !== 'function') {
- throw new TypeError('callback is not a function/string');
- } else if (arguments.length > 1) {
- // Special case: callback arguments are provided.
- bind_args = Array.prototype.slice.call(arguments, 1); // [ arg1, arg2, ... ]
- bind_args.unshift(this); // [ global(this), arg1, arg2, ... ]
- cb_func = func.bind.apply(func, bind_args);
- } else {
- // Normal case: callback given as a function without arguments.
- cb_func = func;
- }
- timer_id = evloop.nextTimerId++;
- evloop.insertImmediate({
- id: timer_id,
- oneshot: true,
- cb: cb_func
- });
- return timer_id;
- }
- /**
- * stops a previously queued setImmediate callback.
- * @method
- * @param {int} timer_id the id of the timer that was created via setImmediate
- */
- function clearImmediate(timer_id) {
- var evloop = EventLoop;
- if (typeof timer_id !== 'number') {
- throw new TypeError('timer ID is not a number');
- }
- evloop.removeTimerById(timer_id, true);
- }
- /**
- * set up a timer that repeats every "delay" milliseconds.
- * @method
- * @param {function} func callback function to call every "delay" milliseconds
- * @param {int} delay number of milliseconds to delay
- * @returns {int} the id of the timer so that it can be stopped in the future.
- */
- function setInterval(func, delay) {
- var cb_func;
- var bind_args;
- var timer_id;
- var evloop = EventLoop;
- if (typeof delay !== 'number') {
- throw new TypeError('delay is not a number');
- }
- delay = Math.max(evloop.minimumDelay, delay);
- if (typeof func === 'string') {
- // Legacy case: callback is a string.
- cb_func = eval.bind(this, func);
- } else if (typeof func !== 'function') {
- throw new TypeError('callback is not a function/string');
- } else if (arguments.length > 2) {
- // Special case: callback arguments are provided.
- bind_args = Array.prototype.slice.call(arguments, 2); // [ arg1, arg2, ... ]
- bind_args.unshift(this); // [ global(this), arg1, arg2, ... ]
- cb_func = func.bind.apply(func, bind_args);
- } else {
- // Normal case: callback given as a function without arguments.
- cb_func = func;
- }
- timer_id = evloop.nextTimerId++;
- evloop.insertTimer({
- id: timer_id,
- oneshot: false,
- cb: cb_func,
- delay: delay,
- target: Date.now() + delay
- });
- return timer_id;
- }
- /**
- * stop a repeated timer that has been set.
- * @method
- * @param {int} timer_id the id of the timer that was created via setInterval
- */
- function clearInterval(timer_id) {
- var evloop = EventLoop;
- if (typeof timer_id !== 'number') {
- throw new TypeError('timer ID is not a number');
- }
- evloop.removeTimerById(timer_id);
- }
- /* custom call */
- /**
- * custom call to exit out of the current event loop.
- */
- function requestEventLoopExit() {
- EventLoop.requestExit();
- }
- // Create a closure that will throttle the EventLoop.processTimers function from being executed too often
- function throttleProcessTimers(ms) {
- var deltaTimer = 0;
- var pendingOps = false;
- function doThrottle(eventData) {
- EventLoop.processPerFrameEvents(eventData);
- deltaTimer += eventData.timeStep * 1000;
- if (deltaTimer > ms || pendingOps) {
- deltaTimer -= ms;
- // double check to see if we have more timers that expire during this sim
- // if we do, then we want to process them in the next update, and not
- // wait 100ms
- pendingOps = EventLoop.processTimers();
- }
- }
- return doThrottle;
- }
- // Hook into the update event of the engine and process all the timers. These
- // will be throttled to only run every 100ms
- Atomic.engine.subscribeToEvent('Update', throttleProcessTimers(100));
- // Hook into the postUpdate event of the engine and process all the setImmediate calls. These should
- // really process once the current update loop is completed.
- Atomic.engine.subscribeToEvent('PostUpdate', function (eventData) {
- EventLoop.processImmediates();
- });
- // Load up the global methods . This module doesn't export anything, it just sets up the global methods.
- (function (global) {
- global.setInterval = global.setInterval || setInterval;
- global.clearInterval = global.clearInterval || clearInterval;
- global.setTimeout = global.setTimeout || setTimeout;
- global.clearTimeout = global.clearTimeout || clearTimeout;
- global.setImmediate = global.setImmediate || setImmediate;
- global.clearImmediate = global.clearImmediate || clearImmediate;
- global.requestAnimationFrame = global.requestAnimationFrame || requestAnimationFrame;
- global.cancelAnimationFrame = global.cancelAnimationFrame || cancelAnimationFrame;
- global.requestEventLoopExit = global.requestEventLoopExit || requestEventLoopExit;
- })(new Function('return this')());
|