Home Callback Scheduling in JavaScript: Fire Callbacks at the Stroke of Every Second
Post
Cancel

Callback Scheduling in JavaScript: Fire Callbacks at the Stroke of Every Second

Sometimes you would like to execute a callback exactly on the stroke of each second. There are many cases when you would like to achieve such behavior, here are just a few examples:

  • New Year’s Countdown
  • Birthday Countdown
  • Precise alarm clock
  • Automating HTTP requests
  • Precise animation triggers

You may think it’s not a big deal if you miss the exact stroke of a second, but it would be really nice to achieve precision when it comes to triggering animation at the exact time (like New Year’s Countdown).

Logging time

First, let’s define our simple callback function logTime for testing:

1
const logTime = () => console.log(+new Date());

Converting Date to number will give us time since January 1. 1970 UTC, including milliseconds.

That is important, as we’ll see how much deviation we get from the stroke of a second.

First attempt: setInterval

Whenever you need to achieve some periodic execution in Javascript, what first comes to mind is the setInterval Web API.

In short, the setInterval function takes a callback as the first parameter and delay as the second.

1
setInterval(logTime, 1000);

This will schedule the execution of the logTime function each second, but the first execution will not have to be invocated at the stroke of a second (and it’s a rare chance it is). Here is the result of the setInterval call:

1
2
3
4
1711139891173
1711139892173
1711139893173
1711139894173

Fixing the first invocation

You don’t want to start interval on some arbitrary point in time, but rather on the stroke of a second.

So, essentially what you want to do is to delay setting up an interval until the next stroke of the second.

What is used for delaying invocations in Javascript? That’s right, the setTimeout Web API.

And how much time would you need to delay?

That should be easy: as much time as we’re left with until the next stroke of the second.

We can get milliseconds of the current time using:

new Date().getMilliseconds()

Since there are 1000 milliseconds in the second, we need to subtract current milliseconds from 1000 to get to the next stroke of a second.

Let’s try it out.

Similar to setInterval, the setTimeout will take the callback as the first parameter and delay as the second.

1
setTimeout(logTime, 1000 - new Date().getMilliseconds());

The result is probably what you expected, the method was executed at the stroke of the second:

1
1711144065000

Tying up the first invocation with interval

Now that we fixed the first invocation, it should be straightforward to tie it up with an interval:

1
2
3
4
setTimeout(
  () => setInterval(logTime, 1000),
  1000 - new Date().getMilliseconds()
);

Finally, we got an interval that is executing a callback on each stroke of a second:

1
2
3
4
1711144386000
1711144387000
1711144388000
1711144389000

Drifting away

Javascript setInterval and setTimeout Web APIs don’t guarantee that a callback will be executed at exactly the requested time.

Furthermore, setInterval is susceptible to drifting away, especially if run for a long time.

In order to fix this, we need to re-synchronize time intervals, which should keep us safe from drifting away phenomena.

Fixing drift-away phenomena

Instead of using setInterval, we can use setTimeout in a recursive function. After every invocation, we’ll calculate the next invocation time and schedule another execution using setTimeout:

1
2
3
4
5
6
7
8
9
10
const clockTick = (callback) =>
  setTimeout(() => 
    {
      callback();
      clockTick(callback);
    }
    ,1000 - new Date().getMilliseconds()
  );

clockTick(logTime);

This should give us a similar result as before, but with a stronger guarantee of the precise execution:

1
2
3
4
1711146244000
1711146245000
1711146246000
1711146247000

Long execution times

In general, you should avoid running expensive computations in callbacks that are scheduled. Depending on the desired behavior you can choose between two options:

  • Block scheduling new execution until the current one is finished (this approach will skip over some invocations if they are not able to be scheduled on time)
  • Allow multiple executions simultaneously

Our solution that we implemented so far allows for multiple executions, given that callback is an async function.

To convert it to block scheduling all we need to change is make the clockTick function async and await the callback to finish:

1
2
3
4
5
6
7
8
const clockTick = (callback) =>
  setTimeout(async () => 
    {
      await callback();
      clockTick(callback);
    }
    ,1000 - new Date().getMilliseconds()
  );
This post is licensed under CC BY 4.0 by the author.

IT Crossword

-