1/* 2 * Pure Ecmascript eventloop example. 3 * 4 * Timer state handling is inefficient in this trivial example. Timers are 5 * kept in an array sorted by their expiry time which works well for expiring 6 * timers, but has O(n) insertion performance. A better implementation would 7 * use a heap or some other efficient structure for managing timers so that 8 * all operations (insert, remove, get nearest timer) have good performance. 9 * 10 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Timers 11 */ 12 13/* 14 * Event loop 15 * 16 * Timers are sorted by 'target' property which indicates expiry time of 17 * the timer. The timer expiring next is last in the array, so that 18 * removals happen at the end, and inserts for timers expiring in the 19 * near future displace as few elements in the array as possible. 20 */ 21 22EventLoop = { 23 // timers 24 timers: [], // active timers, sorted (nearest expiry last) 25 expiring: null, // set to timer being expired (needs special handling in clearTimeout/clearInterval) 26 nextTimerId: 1, 27 minimumDelay: 1, 28 minimumWait: 1, 29 maximumWait: 60000, 30 maxExpirys: 10, 31 32 // sockets 33 socketListening: {}, // fd -> callback 34 socketReading: {}, // fd -> callback 35 socketConnecting: {}, // fd -> callback 36 37 // misc 38 exitRequested: false 39}; 40 41EventLoop.dumpState = function() { 42 print('TIMER STATE:'); 43 this.timers.forEach(function(t) { 44 print(' ' + Duktape.enc('jx', t)); 45 }); 46 if (this.expiring) { 47 print(' EXPIRING: ' + Duktape.enc('jx', this.expiring)); 48 } 49} 50 51// Get timer with lowest expiry time. Since the active timers list is 52// sorted, it's always the last timer. 53EventLoop.getEarliestTimer = function() { 54 var timers = this.timers; 55 n = timers.length; 56 return (n > 0 ? timers[n - 1] : null); 57} 58 59EventLoop.getEarliestWait = function() { 60 var t = this.getEarliestTimer(); 61 return (t ? t.target - Date.now() : null); 62} 63 64EventLoop.insertTimer = function(timer) { 65 var timers = this.timers; 66 var i, n, t; 67 68 /* 69 * Find 'i' such that we want to insert *after* timers[i] at index i+1. 70 * If no such timer, for-loop terminates with i-1, and we insert at -1+1=0. 71 */ 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 82 timers.splice(i + 1 /*start*/, 0 /*deleteCount*/, timer); 83} 84 85// Remove timer/interval with a timer ID. The timer/interval can reside 86// either on the active list or it may be an expired timer (this.expiring) 87// whose user callback we're running when this function gets called. 88EventLoop.removeTimerById = function(timer_id) { 89 var timers = this.timers; 90 var i, n, t; 91 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 105 n = timers.length; 106 for (i = 0; i < n; i++) { 107 t = timers[i]; 108 if (t.id === timer_id) { 109 // Timer on active list: mark removed (not really necessary, but 110 // nice for dumping), and remove from active list. 111 t.removed = true; 112 this.timers.splice(i /*start*/, 1 /*deleteCount*/); 113 return; 114 } 115 } 116 117 // no such ID, ignore 118} 119 120EventLoop.processTimers = function() { 121 var now = Date.now(); 122 var timers = this.timers; 123 var sanity = this.maxExpirys; 124 var n, t; 125 126 /* 127 * Here we must be careful with mutations: user callback may add and 128 * delete an arbitrary number of timers. 129 * 130 * Current solution is simple: check whether the timer at the end of 131 * the list has expired. If not, we're done. If it has expired, 132 * remove it from the active list, record it in this.expiring, and call 133 * the user callback. If user code deletes the this.expiring timer, 134 * there is special handling which just marks the timer deleted so 135 * it won't get inserted back into the active list. 136 * 137 * This process is repeated at most maxExpirys times to ensure we don't 138 * get stuck forever; user code could in principle add more and more 139 * already expired timers. 140 */ 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 147 if (this.exitRequested) { 148 //print('exit requested, exit'); 149 break; 150 } 151 152 // Timers to expire? 153 154 n = timers.length; 155 if (n <= 0) { 156 break; 157 } 158 t = timers[n - 1]; 159 if (now <= t.target) { 160 // Timer has not expired, and no other timer could have expired 161 // either because the list is sorted. 162 break; 163 } 164 timers.pop(); 165 166 // Remove the timer from the active list and process it. The user 167 // callback may add new timers which is not a problem. The callback 168 // may also delete timers which is not a problem unless the timer 169 // being deleted is the timer whose callback we're running; this is 170 // why the timer is recorded in this.expiring so that clearTimeout() 171 // and clearInterval() can detect this situation. 172 173 if (t.oneshot) { 174 t.removed = true; // flag for removal 175 } else { 176 t.target = now + t.delay; 177 } 178 this.expiring = t; 179 try { 180 t.cb(); 181 } catch (e) { 182 print('timer callback failed, ignored: ' + e); 183 } 184 this.expiring = null; 185 186 // If the timer was one-shot, it's marked 'removed'. If the user callback 187 // requested deletion for the timer, it's also marked 'removed'. If the 188 // timer is an interval (and is not marked removed), insert it back into 189 // the timer list. 190 191 if (!t.removed) { 192 // Reinsert interval timer to correct sorted position. The timer 193 // must be an interval timer because one-shot timers are marked 194 // 'removed' above. 195 this.insertTimer(t); 196 } 197 } 198} 199 200EventLoop.run = function() { 201 var wait; 202 var POLLIN = Poll.POLLIN; 203 var POLLOUT = Poll.POLLOUT; 204 var poll_set; 205 var poll_count; 206 var fd; 207 var t, rev; 208 var rc; 209 var acc_res; 210 211 for (;;) { 212 /* 213 * Process expired timers. 214 */ 215 216 this.processTimers(); 217 //this.dumpState(); 218 219 /* 220 * Exit check (may be requested by a user callback) 221 */ 222 223 if (this.exitRequested) { 224 //print('exit requested, exit'); 225 break; 226 } 227 228 /* 229 * Create poll socket list. This is a very naive approach. 230 * On Linux, one could use e.g. epoll() and manage socket lists 231 * incrementally. 232 */ 233 234 poll_set = {}; 235 poll_count = 0; 236 for (fd in this.socketListening) { 237 poll_set[fd] = { events: POLLIN, revents: 0 }; 238 poll_count++; 239 } 240 for (fd in this.socketReading) { 241 poll_set[fd] = { events: POLLIN, revents: 0 }; 242 poll_count++; 243 } 244 for (fd in this.socketConnecting) { 245 poll_set[fd] = { events: POLLOUT, revents: 0 }; 246 poll_count++; 247 } 248 //print(new Date(), 'poll_set IN:', Duktape.enc('jx', poll_set)); 249 250 /* 251 * Wait timeout for timer closest to expiry. Since the poll 252 * timeout is relative, get this as close to poll() as possible. 253 */ 254 255 wait = this.getEarliestWait(); 256 if (wait === null) { 257 if (poll_count === 0) { 258 print('no active timers and no sockets to poll, exit'); 259 break; 260 } else { 261 wait = this.maximumWait; 262 } 263 } else { 264 wait = Math.min(this.maximumWait, Math.max(this.minimumWait, wait)); 265 } 266 267 /* 268 * Do the actual poll. 269 */ 270 271 try { 272 Poll.poll(poll_set, wait); 273 } catch (e) { 274 // Eat errors silently. When resizing curses window an EINTR 275 // happens now. 276 } 277 278 /* 279 * Process all sockets so that nothing is left unhandled for the 280 * next round. 281 */ 282 283 //print(new Date(), 'poll_set OUT:', Duktape.enc('jx', poll_set)); 284 for (fd in poll_set) { 285 t = poll_set[fd]; 286 rev = t.revents; 287 288 if (rev & POLLIN) { 289 cb = this.socketReading[fd]; 290 if (cb) { 291 data = Socket.read(fd); // no size control now 292 //print('READ', Duktape.enc('jx', data)); 293 if (data.length === 0) { 294 //print('zero read for fd ' + fd + ', closing forcibly'); 295 rc = Socket.close(fd); // ignore result 296 delete this.socketListening[fd]; 297 delete this.socketReading[fd]; 298 } else { 299 cb(fd, data); 300 } 301 } else { 302 cb = this.socketListening[fd]; 303 if (cb) { 304 acc_res = Socket.accept(fd); 305 //print('ACCEPT:', Duktape.enc('jx', acc_res)); 306 cb(acc_res.fd, acc_res.addr, acc_res.port); 307 } else { 308 //print('UNKNOWN'); 309 } 310 } 311 } 312 313 if (rev & POLLOUT) { 314 cb = this.socketConnecting[fd]; 315 if (cb) { 316 delete this.socketConnecting[fd]; 317 cb(fd); 318 } else { 319 //print('UNKNOWN POLLOUT'); 320 } 321 } 322 323 if ((rev & ~(POLLIN | POLLOUT)) !== 0) { 324 //print('revents ' + t.revents + ' for fd ' + fd + ', closing forcibly'); 325 rc = Socket.close(fd); // ignore result 326 delete this.socketListening[fd]; 327 delete this.socketReading[fd]; 328 } 329 } 330 } 331} 332 333EventLoop.requestExit = function() { 334 this.exitRequested = true; 335} 336 337EventLoop.server = function(address, port, cb_accepted) { 338 var fd = Socket.createServerSocket(address, port); 339 this.socketListening[fd] = cb_accepted; 340} 341 342EventLoop.connect = function(address, port, cb_connected) { 343 var fd = Socket.connect(address, port); 344 this.socketConnecting[fd] = cb_connected; 345} 346 347EventLoop.close = function(fd) { 348 delete this.socketReading[fd]; 349 delete this.socketListening[fd]; 350} 351 352EventLoop.setReader = function(fd, cb_read) { 353 this.socketReading[fd] = cb_read; 354} 355 356EventLoop.write = function(fd, data) { 357 // This simple example doesn't have support for write blocking / draining 358 var rc = Socket.write(fd, Duktape.Buffer(data)); 359} 360 361/* 362 * Timer API 363 * 364 * These interface with the singleton EventLoop. 365 */ 366 367function setTimeout(func, delay) { 368 var cb_func; 369 var bind_args; 370 var timer_id; 371 var evloop = EventLoop; 372 373 if (typeof delay !== 'number') { 374 throw new TypeError('delay is not a number'); 375 } 376 delay = Math.max(evloop.minimumDelay, delay); 377 378 if (typeof func === 'string') { 379 // Legacy case: callback is a string. 380 cb_func = eval.bind(this, func); 381 } else if (typeof func !== 'function') { 382 throw new TypeError('callback is not a function/string'); 383 } else if (arguments.length > 2) { 384 // Special case: callback arguments are provided. 385 bind_args = Array.prototype.slice.call(arguments, 2); // [ arg1, arg2, ... ] 386 bind_args.unshift(this); // [ global(this), arg1, arg2, ... ] 387 cb_func = func.bind.apply(func, bind_args); 388 } else { 389 // Normal case: callback given as a function without arguments. 390 cb_func = func; 391 } 392 393 timer_id = evloop.nextTimerId++; 394 395 evloop.insertTimer({ 396 id: timer_id, 397 oneshot: true, 398 cb: cb_func, 399 delay: delay, 400 target: Date.now() + delay 401 }); 402 403 return timer_id; 404} 405 406function clearTimeout(timer_id) { 407 var evloop = EventLoop; 408 409 if (typeof timer_id !== 'number') { 410 throw new TypeError('timer ID is not a number'); 411 } 412 evloop.removeTimerById(timer_id); 413} 414 415function setInterval(func, delay) { 416 var cb_func; 417 var bind_args; 418 var timer_id; 419 var evloop = EventLoop; 420 421 if (typeof delay !== 'number') { 422 throw new TypeError('delay is not a number'); 423 } 424 delay = Math.max(evloop.minimumDelay, delay); 425 426 if (typeof func === 'string') { 427 // Legacy case: callback is a string. 428 cb_func = eval.bind(this, func); 429 } else if (typeof func !== 'function') { 430 throw new TypeError('callback is not a function/string'); 431 } else if (arguments.length > 2) { 432 // Special case: callback arguments are provided. 433 bind_args = Array.prototype.slice.call(arguments, 2); // [ arg1, arg2, ... ] 434 bind_args.unshift(this); // [ global(this), arg1, arg2, ... ] 435 cb_func = func.bind.apply(func, bind_args); 436 } else { 437 // Normal case: callback given as a function without arguments. 438 cb_func = func; 439 } 440 441 timer_id = evloop.nextTimerId++; 442 443 evloop.insertTimer({ 444 id: timer_id, 445 oneshot: false, 446 cb: cb_func, 447 delay: delay, 448 target: Date.now() + delay 449 }); 450 451 return timer_id; 452} 453 454function clearInterval(timer_id) { 455 var evloop = EventLoop; 456 457 if (typeof timer_id !== 'number') { 458 throw new TypeError('timer ID is not a number'); 459 } 460 evloop.removeTimerById(timer_id); 461} 462 463/* custom call */ 464function requestEventLoopExit() { 465 EventLoop.requestExit(); 466} 467