Browse Source

Merge pull request #259 from shaddockh/TSH-EVENTLOOP

TSH-EVENTLOOP
JoshEngebretson 10 years ago
parent
commit
1a62cd977e
2 changed files with 503 additions and 0 deletions
  1. 1 0
      AUTHORS.md
  2. 502 0
      Resources/CoreData/AtomicModules/AtomicEventLoop.js

+ 1 - 0
AUTHORS.md

@@ -11,6 +11,7 @@
 
 
 - rsredsq (https://github.com/rsredsq)
 - rsredsq (https://github.com/rsredsq)
 
 
+- Shaddock Heath (https://github.com/shaddockh)
 
 
 ### Contribution Copyright and Licensing
 ### Contribution Copyright and Licensing
 
 

+ 502 - 0
Resources/CoreData/AtomicModules/AtomicEventLoop.js

@@ -0,0 +1,502 @@
+/*
+ *  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
+ *
+ */
+
+/*
+ *  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,
+
+    // 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.requestExit = function () {
+    this.exitRequested = true;
+}
+
+/*
+ *  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) {
+        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.requestEventLoopExit = global.requestEventLoopExit || requestEventLoopExit;
+})(new Function('return this')());