Smashinglabs

Sebastian Poręba's blog

Timing in JavaScript

In game engine good timing is essential. While there is a lot of good reading about time in C++, there is still not much said about timing in JS. There is a great article from John Resig – How JavaScript Timers Work, where he explains differences between setTimeout and setInterval. After reading that I thought “Ok, I will use setInterval for game loop. Thesis, chapter 1 – done!”. I couldn’t be more wrong…

 

Why do we use timeouts at all?

In C++ we have game loops. Typical loop works like that:

Attention! Rough generalization!

We don’t care too much about time passed, since we can always calculate required amount of physics steps. In JavaScript on the other hand, we can’t use while loop so easily. Long running scripts block browser UI and are eventually stopped by browser. You can read more about JS long executions on Opera Blog. I’m going to describe few techniques I have used to make JS timers accurate enough to be used in game engine as while loop replacement.

Test 1 – original functions

HTML markup for all tests is similar – there are divs for log.

I wrote a small test, to check how accurate timers are.


      var x = 100; // time step

      var log1 = document.getElementById('log1');
      var log2 = document.getElementById('log2');
      var d1,d2,t;
      var c1 = 0,c2 = 0;

      var timeout = function() {
        d1 = new Date();
        c1++;
        log1.innerHTML += "timeout " + c1 + " " + d1.getTime() + "";
        if(c1 !== 1000) setTimeout(timeout, x);
      };
      setTimeout(timeout, x);
      var interval = function() {
        d2 = new Date();
        c2++;
        log2.innerHTML += "interval " + c2 + " " + d2.getTime() + "";
        if(c2 == 1000) clearInterval(t);
      };
      t = setInterval(interval, x);

Both methods were used to loop 1000 times with 100ms step. Simulation lasted 100s (simple math) and I used browser as I normally would, to simulate some CPU usage.

Test 1 results: time/step number

Results were surprising. Looking at chart you may think that difference between expected timer and both methods are irrelevant, but interval method was 4,5s late and timeout was 9,8s late!  With “classic” JavaScript usage nobody would notice that, but in gaming that looks like a serious problem. Timer error is growing with time, when DOM is getting bigger and bigger. Notice, that simulation lasted less than 2 minutes, while usually game runs continuously for a long time and uses a lot of resources.

Test 2 – simple compensation with measured time

Basic methods fail and we need to fix it. The simplest solution I came up with is to measure time since last step and try to fix any timer errors.


     var x = 100; // time step

      var log1 = document.getElementById('log1');
      var log2 = document.getElementById('log2');
      var log3 = document.getElementById('log3');
      var d1,d2,d3, t, delta = x, time, newtime;
      var c1 = 0,c2 = 0,c3 = 0;

      /* previous code skipped */
      var compensation = function() {
        d3 = new Date();
        c3++;
        newtime = d3.getTime();
        delta += x - (newtime - time);
        time = newtime;
        log3.innerHTML += "compensation (" + delta + ") " + c3 + " " + time + "";
        if(c3 !== 1000) setTimeout(compensation, delta);
      };
      d3 = new Date();
      time = d3.getTime();
      setTimeout(compensation, x);

Test 2 results: measured time - expected time

Now we are getting somewhere. Time difference in every step is <70ms. However, notice how this values is growing constantly, as cost of DOM operations is increasing over time. This observation leads us to test 3.

Test 3 – additional compensations for high load

For my last test I created some high load in browser. Plain setInterval method was 60s late, and for setTimeout it was 71s. Method from test 2 was working quite well, until I generated some load. It quickly turned out, that timeout should be negative to keep up to script requirements.

How to break test 2 method

Now, how to fix that? This is time to use C++ similar method. If there is any kind of jamming involved, we should detect it and proceed amount of steps required to catch up.


      var x = 100; // time step
      var limit = 1000;

      var log1 = document.getElementById('log1');
      var log2 = document.getElementById('log2');
      var log3 = document.getElementById('log3');
      var log4 = document.getElementById('log4');

      var d1,d2,d3,d4, t, delta = x, time, newtime, delta2 = x, time2, newtime2;
      var c1 = 0,c2 = 0,c3 = 0,c4 = 0;
/* previous code skipped */
      var compcount = 0;
      var compensation2 = function() {
        d4 = new Date();
        c4++;
        newtime2 = d4.getTime();
        delta2 += x - (newtime2 - time2);
        while(delta2 < 0) {
          delta2+=x;
          compcount++;
          log4.innerHTML += "compensation (bonus) " + c4 + " " + time2 + "";
          c4++;
        };
        time2 = newtime2;
        log4.innerHTML += "compensation (" + delta2 + ") " + c4 + " " + time2 + "";
        if(c4 < limit) setTimeout(compensation2, delta2);
      };
      d4 = new Date();
      time2 = d4.getTime();
      setTimeout(compensation2, x);

On result chart you can see series of pikes and zeros – this is where script is calling additional loop.

High load resistant method

JS running in background tab

Method from test 3 is perfect for running scripts in background tab. New releases of browsers tend to clamp timeouts to improve performance. You can actually throttle your scripts using new page visibility API.

Bonus test – zero timeout as while loop simulation

Last question left to cover is – why not use zero timeout? I prepared test for that as well.

Quick note – there is no such thing as zero timeout.

        var timeout = function() {
        d1 = new Date();
        newtime = d1.getTime();
        delta += newtime - time;
        while(delta > 0) {
          c1++;
          delta-=x;
          time += x;
          log1.innerHTML += "loop " + c1 + " " + time + "<br />";
        };
        time = newtime;
        if(c1 < limit) setTimeout(timeout, 0);
      };
      d1 = new Date();
      time = d1.getTime();
      timeout();

While loop with low load

While loop with high load

You can see that we are always around 100ms late, which is correct, as we always call function when delay is greater than 100ms. This method behave very nicely for fixed step methods. However, if 100% CPU usage will be reached, while loop will quickly turn into what I described in first example – long running, blocking script.

Summary

After testing several methods I believe there is a small possibility of making timers very precise. However, with only few lines of code we can reduce errors to only few ms with every step, which should be sufficient for game engine.

Complete code for all the tests can be downloaded here: timing in js examples.

References

http://benalman.com/news/2009/07/the-mysterious-firefox-settime/ 
http://www.sitepoint.com/creating-accurate-timers-in-javascript/
http://code.google.com/chrome/whitepapers/pagevisibility.html 
http://testbed.nicon.nl/timeouttest/
http://www.elated.com/articles/javascript-timers-with-settimeout-and-setinterval/
http://ejohn.org/blog/how-javascript-timers-work/ 

  • RSS
  • Facebook
  • Twitter

FAQ about Wordpress

This came as a surprise for me but gMap is ...

gMap 3.3.3 released

It was a looong time since I last visited gMap. ...

Talks for Google Dev

Two new slide decks appeared in lectures tab. This time with ...

Talks and lectures w

Every now and then I spend a weekend watching various ...

3D Tetris with Three

In the fifth part of tutorial we add some final ...

FAQ about Wordpress

This came as a surprise for me but gMap is ...

gMap 3.3.0 released

Christmas came early! New version of gMap is ready!

Lecture for GTUG: Ja

Today I gave a lecture for GTUG Krakow about optimizations in ...

Unit testing for jQu

In part 1 I described basics of unit testing in ...

Unit testing for jQu

In part 1 I described some basic concepts behind unit ...