1

So it turns out the while loop blocks the event loop in JavaScript, which prevents it from being a viable solution to creating a run loop if you want your application to respond to external / async events.

My question is, what techniques are used to have a "run loop" be infinite / constant but still have high performance and not block the event loop.

I ran a quick test to see the performance of while, setTimeout(fn, 0) (browser), and setImmediate(fn) (node.js).

var x = 1000
var isNode = typeof window == 'undefined'

function a() {
  var start = (new Date()).getTime()
  var q = 0

  function next() {
    q++
    if (q < x) {
      if (isNode) {
        setImmediate(next)
      } else {
        setTimeout(next, 0)
      }
    } else {
      var end = (new Date).getTime()
      console.log('a', end - start)
    }
  }

  next()
}

function b() {
  var start = (new Date()).getTime()
  var q = 0

  while (q < x) {
    q++
  }

  var end = (new Date).getTime()
  console.log('b', end - start)
}

a()
b()

node.js:

a = 20
b = 0

browser:

a = 5039
b = 0

Is there not a way to do an "infinite while loop" that is as performant as doing a while loop in JavaScript. setImmediate is much faster than setTimeout, but it's still a lot slower than the basic while loop.

Lance
  • 75,200
  • 93
  • 289
  • 503
  • 2
    Why do you *need* an infinite loop? The whole point of the browser event loop is to do just that. What kind of external/async events do you want to handle that the event loop can't? (genuinely asking) – Máté Safranka Apr 24 '18 at 21:37
  • 1
    The event loop *is* the application run loop. Unless you want some constant-paced interval, e.g. for a rendering loop or physics engine. – Bergi Apr 24 '18 at 21:40
  • @LancePollard Just put the VM in its own thread (webworker on the client side, subprocess on node). – Bergi Apr 24 '18 at 21:41
  • Browser-based JS and I think JS, in general, is optimized to respond to events as opposed to do one thing for a lot of time. The Event Loop is the infinite loop that is watching for Events. So if you have a loop running, then there is no time to respond to Events. – Nishant Apr 24 '18 at 21:46
  • @Nishant correct that was the first link I posted. The question is what is the workaround. – Lance Apr 24 '18 at 21:47
  • @LancePollard Yes, that's the perfect use case for a web worker. (Please specify: are you trying to run this on the server or client side?) – Bergi Apr 24 '18 at 21:51
  • @LancePollard What's the problem with async syscalls? I suppose you don't want to introduce asynchrony to brainfuck, just have some instructions do async work before continuing the evaluation. – Bergi Apr 24 '18 at 21:52
  • @LancePollard The web worker only responds to events that you are sending (like `evaluate expression`). The main thread (DOM) is still responsive. – Bergi Apr 24 '18 at 21:53

3 Answers3

1

You can yield to the event loop inside a while loop with async/await:

// This can only be logged when the event loop
// is yielded to in the infinite loop below.
// So, it can only ever be logged between `#1` and `#2` being logged;
// it can sometimes occur more than once, or not at all between `#1` and `#2`
const interval = setInterval( _ => console.log( '#1.5' ), 0 );

(async _ => {

  // Using a for loop so this demo doesn't run forever.
  // Can use an infinite loop instead
  for ( let i = 0; i < 150; i++ ) { // while( true ) {

    console.log( '#1 About to yield, ' );
    await new Promise( resolve => setTimeout( _ => resolve(), 0 ) ); // yield
    console.log( '#2 Done yielding (other callbacks may have run)' );

  }

  clearInterval( interval );

})();

In node.js use setImmediate instead of setTimeout. Do not use process.nextTick.

To demo with your timer:

var start = (new Date()).getTime()
var q = 0, x = 1000;

;(async _ => {
  while (q < x) {
    await new Promise( resolve => setTimeout( _ => resolve(), 0 ) );
    q++
  }

  var end = (new Date).getTime()
  console.log('a', end - start)
})();
Marcos Casagrande
  • 37,983
  • 8
  • 84
  • 98
Paul
  • 139,544
  • 27
  • 275
  • 264
  • I tried `setImmediate` and `setTimeout` in the tests but they have too much a performance impact to be viable. – Lance Apr 24 '18 at 21:57
  • @LancePollard To be viable for what? The event loop polling takes time, and running all the messages in the event loop it takes an indeterminate amount of time. You can't get around that. You're comparing the time to increment a number 1000 times (0ms), to the time to poll the event loop 1000 times and run any arbitrary code that may be queued up in the event-loop. – Paul Apr 24 '18 at 22:05
  • @Paulpro the demo is wrong, the `console.log` with the time is not waiting until the `while` loop ends... That's why you're getting 0ms – Marcos Casagrande Apr 25 '18 at 01:51
  • 1
    @MarcosCasagrande You're right, thanks for that! If you don't mind editing you can move those two lines into the async arrow function; otherwise I'll do it next time I'm at my computer – Paul Apr 25 '18 at 02:48
0

Something like this will work:

var THRESHOLDCOUNT = 10000
var THRESHOLDTIME = 15

function loop(fn) {
  var start = Date.now()
  var changed = false
  var x = 0

  while (!changed) {
    while (x < THRESHOLDCOUNT) {
      x++
      fn()
    }

    var end = Date.now()
    changed = end - start > THRESHOLDTIME
  }

  // make room for external events.
  setImmediate(loop)
}
Lance
  • 75,200
  • 93
  • 289
  • 503
-1

If you really need a performant loop, I'd say go with requestAnimationFrame(), it's highly optimized and pretty easy to use.

https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame

Máté Safranka
  • 4,081
  • 1
  • 10
  • 22
  • 1
    This is incorrect. Running the test with `requestAnimationFrame` lead to `c = 16637ms`, which is the slowest. – Lance Apr 24 '18 at 21:43
  • `requestAnimationFrame` is for rendering loops, not for high performance stuff. – Bergi Apr 24 '18 at 21:49