0

I saw there are a lot of posts about this function, but none really tackles this issue: how can I ensure that a setTimeout is run exactly and precise after those milliseconds? I am writing an application where precision is critical, and I read in many places, for example here, which considers my same issue, that

The problem with setInterval() and setTimeout() is that there is no guarantee your code will run in the specified time.

so, is there any way to force it to run to the millisecond precision? My application is actually of the same type as the one in the topic I linked.

I already run many timeouts in a loop (I first create an array which run the timeouts one after the other) and after a while the system gets out of sync.

A second question, maybe: if not javascript, is there any other thing that might help with this? Maybe going for WebGL? Or is there some js library already considering/solving this?

EDIT: I considered using requestAnimationFrame, but the intervals might be very different in timing and I read that this function was made for constant steps.

EDIT2: I found a good algorithm on this page, which is the best I could find until now.

LowFieldTheory
  • 1,722
  • 1
  • 26
  • 39

3 Answers3

1

Here's a thread that explains it very good - it has more to do with Javascript and Browsers in general: What is the reason JavaScript setTimeout is so inaccurate?

You might get better results by creating a self adjusting timer that relies on the system time but even then there might be very small latency (that self adjusts again)

function doTimer(length, resolution, oninstance, oncomplete)
{
var steps = (length / 100) * (resolution / 10),
    speed = length / steps,
    count = 0,
    start = new Date().getTime();

function instance()
{
    if(count++ == steps)
    {
        oncomplete(steps, count);
    }
    else
    {
        oninstance(steps, count);

        var diff = (new Date().getTime() - start) - (count * speed);
        window.setTimeout(instance, (speed - diff));
    }
}

window.setTimeout(instance, speed);
}

Source: https://www.sitepoint.com/creating-accurate-timers-in-javascript/

mxmtsk
  • 4,285
  • 6
  • 22
  • 47
  • thanks, it's a starting point, but I think that this function rather implements a setInterval, which indeed can adjust easily at each iteration. But in fact I need a solution for a series of timeouts with different timings. I will fiddle with the code in the article, maybe I will carve a solution out for me from there. – LowFieldTheory Aug 19 '18 at 10:31
  • Actually browsers do the same today, so intervals will be quite accurate in average. – Jonas Wilms Aug 19 '18 at 10:37
  • @JonasWilms there is a demo in that page which demonstrates that it's actually a good workaround. It calculates the accumulated delay. – LowFieldTheory Aug 19 '18 at 14:20
  • 2
    @lowFieldTheory my point is that browsers already do this with `setInterval`: https://github.com/whatwg/html/issues/3151#issuecomment-340120400 – Jonas Wilms Aug 19 '18 at 14:46
1

Js has 3 microtask queues, these are(were) setTimeout/Interval/Immediate (some people call these macrotask, etc whatever), requestAnimationFrame (rAF) and the new child Promises. Promises resolve asap, setTimeouts have 4ms min difference between successive invocations if they are nested (& more than 5 layers deep), rAF will execute around 60 frames per second.

Amongst these rAF is aware of document.hidden state and roughly executes every ~17ms (16.67 theoretically). If your desired intervals are larger than this value, settle with rAF.

The problem with rAF is, since it executes every ~17ms, if I would want to execute something with 100 ms intervals,then after 5 ticks I would be at ~85ms, at the sixth tick I'd be at 102ms. I can execute at 102ms, but then I need to drop down 2ms from the next invocation time. This will prevent accidental 'phasing out' of the callback with respect to the frames you specify. You can roughly design a function that accepts an options object:

function wait(t,options){
    if(!options){
        options = t;
        window.requestAnimationFrame(function(t){
            wait(t,options);
        });
        return options;
    }
    if(!options._set) {
        options.startTime = options.startTime || t;
        options.relativeStartTime = options.startTime;
        options.interval = options.interval || 50;
        options.frame = options.frame || 0;
        options.callback = options.callback || function(){};
        options.surplus = options.surplus || 0;
        options._set = true;
    }
    options.cancelFrame = window.requestAnimationFrame(function(t){
        wait(t,options);
    });
    options.elapsed = t - options.relativeStartTime + options.surplus;
    if (options.elapsed >= options.interval) {
        options.surplus = options.elapsed % options.interval;
        options.lastInvoked = t;
        options.callback.call(options);
        options.frame++;
        options.relativeStartTime = t;
    }
    return options;
}

The object gets recycled and updated at every invocation. Copy paste to your console the above and try:

var x = wait({interval:190,callback:function(){console.log(this.lastInvoked - this.relativeStartTime)}})

The callback executes with this pointing to the options object. The returned x is the options object itself. To cancel it from running:

window.cancelAnimationFrame(x.cancelFrame);

This doesn't always have to act like interval, you can also use it like setTimeout. Let's say you have variable frames with multiples of 32 as you said,in that case extend the options object:

var x = wait({frameList:[32,32,64,128,256,512,1024,2048,32,128],interval:96,callback:function(){
    console.log(this.lastInvoked - this.relativeStartTime);
    window.cancelAnimationFrame(this.cancelFrame);
    this.interval = this.frameList[this.frame];
    if(this.interval){
        wait(this);
    }
}})

I added a frameList key to the options object. Here are some time values that we want to execute the callback. We start with 96, then go inside the frameList array, 32,32, 64 etc. If you run the above you'll get:

99.9660000000149
33.32199999992736
33.32199999992736
66.64400000008754
133.28799999994226
249.91499999980442
517.7960000000894
1016.5649999999441
2049.7950000001583
33.330000000074506
133.31999999983236

So these are my thoughts about what I'd do in your situation.

it runs as close as possible to the specified interval. If you put very close intervals such as 28,30,32 you will not be able to inspect the difference by eye. perhaps try console logging the 'surplus' values like this:

var x = wait({interval:28,callback:function(){
    console.log(this.surplus);
}})

You will see slightly different numbers for different intervals, and these numbers will shift in time, because we are preventing 'phasing out'. The ultimate test would be to look at the average time takes for certain amount of 'frames':

var x = wait({interval:28,callback:function(){
    if(this.frame === 99){
        console.log(this.lastInvoked - this.startTime);
        window.cancelAnimationFrame(this.cancelFrame);
    }
}}) //logs something around 2800

if you change the interval to 32 for instance, it will log something around 3200ms etc. In conclusion, the function we design should not depend on what the real time is, it should get from the js engine which frame we are currently at, and should base its pace on that.

ibrahim tanyalcin
  • 5,643
  • 3
  • 16
  • 22
  • Actually I don't get it. The code starting with "var x = wait({interval:190,...", when changing interval to 32, it returns only 16,.. and 33,.. as values, which is good because they're actually very precise, but does the function actually runs every 32 ms? I can't really understand that. How can I use your code to run a function at a given interval? even putting 28, 30, or 32, it never changes the returned values. – LowFieldTheory Aug 19 '18 at 18:43
  • Yes it runs as close as possible to the specified interval. If you put small numbers, you won't be able to inspect it by eye. Logging surplus might help but that can shift also. We are concerned about not accidentally causing phase difference. Read through my updated answer, that might explain a bit more. – ibrahim tanyalcin Aug 19 '18 at 19:02
  • I will accept this as an answer, which I think is the closest to a real one. – LowFieldTheory Aug 24 '18 at 05:30
0

No, there is really no way. Javascript runtime environments (e.g. v8) have no realtime guarantees nor you are not running them on real time operating systems.

There is basically no guarantee that javascript runtime gets scheduled on time by the host operating system's scheduler nor there is no guarantee that the Javascript runtime schedules the timer callback precisely at the specified tick since it is handling numerous other tasks besides that callback.

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
teroi
  • 1,087
  • 10
  • 19
  • I just found that the Performance API and the Web Audio API give what I need, I just needed to search more between html specs. moreover, the above answer gives already a partial solution. This answer is just a repetition of other answers in other threads, and not so useful either. – LowFieldTheory Aug 19 '18 at 11:45
  • Even with the mentioned APIs that alleviate the problems, one must remember that there is really no way around the development platform limitations. – teroi Sep 11 '18 at 14:19