How to Create an Accurate Timer in JavaScript

How to create an accurate timer in javascript?

Why is it not accurate?

Because you are using setTimeout() or setInterval(). They cannot be trusted, there are no accuracy guarantees for them. They are allowed to lag arbitrarily, and they do not keep a constant pace but tend to drift (as you have observed).

How can I create an accurate timer?

Use the Date object instead to get the (millisecond-)accurate, current time. Then base your logic on the current time value, instead of counting how often your callback has been executed.

For a simple timer or clock, keep track of the time difference explicitly:

var start = Date.now();
setInterval(function() {
var delta = Date.now() - start; // milliseconds elapsed since start

output(Math.floor(delta / 1000)); // in seconds
// alternatively just show wall clock time:
output(new Date().toUTCString());
}, 1000); // update about every second

Now, that has the problem of possibly jumping values. When the interval lags a bit and executes your callback after 990, 1993, 2996, 3999, 5002 milliseconds, you will see the second count 0, 1, 2, 3, 5 (!). So it would be advisable to update more often, like about every 100ms, to avoid such jumps.

However, sometimes you really need a steady interval executing your callbacks without drifting. This requires a bit more advanced strategy (and code), though it pays out well (and registers less timeouts). Those are known as self-adjusting timers. Here the exact delay for each of the repeated timeouts is adapted to the actually elapsed time, compared to the expected intervals:

var interval = 1000; // ms
var expected = Date.now() + interval;
setTimeout(step, interval);
function step() {
var dt = Date.now() - expected; // the drift (positive for overshooting)
if (dt > interval) {
// something really bad happened. Maybe the browser (tab) was inactive?
// possibly special handling to avoid futile "catch up" run
}
… // do what is to be done

expected += interval;
setTimeout(step, Math.max(0, interval - dt)); // take into account drift
}

Accurate Timers in Vanilla JavaScript

There are two methods to get what you want.

  1. Use a background process (web worker).
  2. Track the time against a timestamp at the beginning, and show the difference between now and then, instead of incrementing per interval frame.

Web Worker Version

If you can support web workers, then you could use those to run a dedicated background process that supports the type of time frame push you're wanting to do (increment a timer frame by frame in an interval, without a timestamp diff, and keep it accurate).

Here is an example, found on this webpage:

http://frenticb.com/tricks/simple-timer.php

<div class="header">A simple timer:</div>
<div class="timer" id="timer">00:00</div>
<div class="buttons">
<button onclick="startTimer()" id="button1">Start</button>
<button onclick="stopTimer()" id = "button2">Stop</button>
</div>
<script>
var w = null; // initialize variable

// function to start the timer
function startTimer(){
// First check whether Web Workers are supported
if (typeof(Worker)!=="undefined"){
// Check whether Web Worker has been created. If not, create a new Web Worker based on the Javascript file simple-timer.js
if (w==null){
w = new Worker("simple-timer.js");
}
// Update timer div with output from Web Worker
w.onmessage = function (event) {
document.getElementById("timer").innerHTML = event.data;
};
} else {
// Web workers are not supported by your browser
document.getElementById("timer").innerHTML = "Sorry, your browser does not support Web Workers ...";
}
}

// function to stop the timer
function stopTimer(){
w.terminate();
timerStart = true;
w = null;
}
</script>

And the simple.timer.js (note that web workers requires this to be a url):

var timerStart = true;

function myTimer(d0){
// get current time
var d=(new Date()).valueOf();
// calculate time difference between now and initial time
var diff = d-d0;
// calculate number of minutes
var minutes = Math.floor(diff/1000/60);
// calculate number of seconds
var seconds = Math.floor(diff/1000)-minutes*60;
var myVar = null;
// if number of minutes less than 10, add a leading "0"
minutes = minutes.toString();
if (minutes.length == 1){
minutes = "0"+minutes;
}
// if number of seconds less than 10, add a leading "0"
seconds = seconds.toString();
if (seconds.length == 1){
seconds = "0"+seconds;
}

// return output to Web Worker
postMessage(minutes+":"+seconds);
}

if (timerStart){
// get current time
var d0=(new Date()).valueOf();
// repeat myTimer(d0) every 100 ms
myVar=setInterval(function(){myTimer(d0)},100);
// timer should not start anymore since it has been started
timerStart = false;
}

Non-Web Worker Version

And the non-web worker version:

<p id='timer'>0.00</p>
<p id='starter-container'>
<button type='button' id='starter'>Start</button>
<button type='button' id='starter-reset'>Reset</button>
</p>
<script>
(function oab(){ // Keep it local.
var runs = 0,
max_runs = 10000,
speed = 10,
timeout = speed,
start_time = 0,
time = 0,
num_seconds = (30) * 1000,
mark_every = 100,
mark_next = time * speed,
timer_el = document.getElementById('timer'),
starter = document.getElementById('starter'),
reset = document.getElementById('starter-reset');

starter.addEventListener('click', function cl(){
reset_timer();
init_timer();
do_timer();
this.disabled = true;
});

reset.addEventListener('click', function cl(){
runs = max_runs++;
});

function init_timer() {
start_time = new Date().getTime();
time = Math.floor(start_time / speed);
}

function reset_timer() {
runs = 0;
starter.disabled = false;
timer_el.innerText = '0.00';
}

function do_timer(){
init_timer();

(function timer () {
var c_time = new Date().getTime(),
time_diff = c_time - start_time,
c_secs = 0;

runs += 1;

c_secs = (Math.round(time_diff / 10, 3) / 100).toString();

if (c_secs.indexOf('.') === -1) {
c_secs += '.00';
} else if (c_secs.split('.').pop().toString().length === 1 ) {
c_secs += '0';
}

timer_el.innerText = c_secs;

if (c_time >= mark_next) {
console.log(
'mark_next: ' + mark_next,
'mark time: ' + c_time,
'(' + (Math.floor(c_time * .01) * 100).toString().substring(10) + ')',
'precision: ' + (mark_next - c_time) + ')'
);

mark_next = Math.floor((c_time + mark_every) * .01) * 100;
}

if (Math.floor(c_time / speed) > time + 1) {
timeout = speed - ((c_time / speed) - time);
} else if (Math.floor(c_time / speed) < time + 1) {
timeout = speed + (time - Math.floor(c_time / speed));
} else {
timeout = speed;
}

time = Math.floor(new Date().getTime() / speed);

if (runs >= max_runs || time_diff > num_seconds) {
reset_timer();

return;
}

setTimeout(timer, timeout);
})();
}
})();
</script>

http://jsfiddle.net/y3zL84ox/9/

Create an accurate millisecond timer that loops in Javascript

You could use a .setTimeout() at the end of your code to restart your .setInterval(), after the desired time has passed

function runInterval (loopAfter) {

var startTime = Date.now();

var interval = setInterval(function() {

var elapsedTime = Date.now() - startTime;

var time = (elapsedTime / 1000).toFixed(3);

console.log(time);

}, 10);



setTimeout(() => {

clearInterval(interval);

runInterval(loopAfter);

}, loopAfter);

}

runInterval(5000);

Is there a more accurate way to create a Javascript timer than setTimeout?

Are there any tricks that can be done
to ensure that setTimeout() performs
accurately (without resorting to an
external API) or is this a lost cause?

No and no. You're not going to get anything close to a perfectly accurate timer with setTimeout() - browsers aren't set up for that. However, you don't need to rely on it for timing things either. Most animation libraries figured this out years ago: you set up a callback with setTimeout(), but determine what needs to be done based on the value of (new Date()).milliseconds (or equivalent). This allows you to take advantage of more reliable timer support in newer browsers, while still behaving appropriately on older browsers.

It also allows you to avoid using too many timers! This is important: each timer is a callback. Each callback executes JS code. While JS code is executing, browser events - including other callbacks - are delayed or dropped. When the callback finishes, additional callbacks must compete with other browser events for a chance to execute. Therefore, one timer that handles all pending tasks for that interval will perform better than two timers with coinciding intervals, and (for short timeouts) better than two timers with overlapping timeouts!

Summary: stop using setTimeout() to implement "one timer / one task" designs, and use the real-time clock to smooth out UI actions.

Accurate timer in Nodejs

an ideal approach would be to use absolute clock time to get time elapsed and timer to have that check.

Something like code below.

const startTime = new Date();

const maxTime = 5 * 1000; // 60 seconds
setInterval(() => {
checkExpired();
}, 300);

function checkExpired() {
const curTime = new Date();

timeDifference = Math.abs(curTime.getTime() - startTime.getTime());

if (timeDifference > maxTime) {
console.log("time up");
}
}

The browser will not run setInterval as required due to performance reason.
https://usefulangle.com/post/280/settimeout-setinterval-on-inactive-tab

If you need to run timer with required interval then it can be run in worker thread.

some info here.
How can I make setInterval also work when a tab is inactive in Chrome?

But my guess is increased granularity is fine for non active tab.

Creating an Accurate Timer in Javascript, but I sometimes get a negative Interval Time

There is no guarantee that the setTimeout callback function will be executed at exactly the desired time. If at that time other JavaScript code is running, that will first have to run to completion before the next event in the event queue gets processed, and a timer event is such an event.

So it might well be that you get the following scenario:

  • At 0ms: currTime is 0, progressBarNextAt is 100, and you call setTimeout(progressBarNextAt, 100). All is perfect.
  • At 1ms: The next event in the event queue is some callback for the real processing that is happening
  • At 220ms: that callback finishes, the next event in the event queue is the timer (which is late), and your function setPercentageBar gets called a second time
  • At 221ms: progressBarNextAt is increased from 100 to 200, which is not enough as we are already passed that moment. You print an interval time of -21.

There can even be some asynchronous code scheduled before the timer event gets processed, in the form of micro tasks. For instance, if the processing in the second bullet point has some promises that resolve immediately, the corresponding then callback functions will still execute as part of that task, and only when all of those have executed, will the timer event get its turn.

Note also that non-JS factors can influence how long a certain task takes. If the machine gets in a heavy load, and the operating system reallocates resources, this may impact an otherwise light JavaScript task. Browsers also tend to delay the processing of timer events when the window in question is not on the foreground. And there can be still other reasons.

How to create timer in javascript with fixed intervals? and option to start from a number?

Give this a try:

class Timer {
constructor () {
this.isRunning = false;
this.startTime = 0;
this.overallTime = 0;
}

_getTimeElapsedSinceLastStart () {
if (!this.startTime) {
return 0;
}

return Date.now() - this.startTime;
}

start () {
if (this.isRunning) {
return console.error('Timer is already running');
}

this.isRunning = true;

this.startTime = Date.now();
}

stop () {
if (!this.isRunning) {
return console.error('Timer is already stopped');
}

this.isRunning = false;

this.overallTime = this.overallTime + this._getTimeElapsedSinceLastStart();
}

reset () {
this.overallTime = 0;

if (this.isRunning) {
this.startTime = Date.now();
return;
}

this.startTime = 0;
}

getTime () {
if (!this.startTime) {
return 0;
}

if (this.isRunning) {
return this.overallTime + this._getTimeElapsedSinceLastStart();
}

return this.overallTime;
}
}

let the_interval;

function round_nearest_hundred(num){
return Math.floor(num / 100)*100;
}

function onUpdateTimer(start, duration){
const startTime = timer.getTime() + start;
const timeInSeconds = Math.floor(timer.getTime() / 1000);
document.getElementById('time').innerText = timeInSeconds;

document.getElementById('accTime').innerText = round_nearest_hundred(startTime);

console.log(round_nearest_hundred(startTime));

if(round_nearest_hundred(timer.getTime()) >= (duration - start)){
document.getElementById('status').innerText = 'complete';
timer.stop();
timer_manager(false);
}
}

function timer_manager(flag, updateFunction, time, start, duration){
if(flag){
the_interval = setInterval(function(){updateFunction(start, duration);}, time);
} else {
clearInterval(the_interval);
}
}

const timer = new Timer();
//timer.start();

timer_manager(true, onUpdateTimer, 50, 1000, 10000);

document.getElementById('start').addEventListener('click', function(){
timer.start();
});

document.getElementById('stop').addEventListener('click', function(){
timer.stop();
});

document.getElementById('restart').addEventListener('click', function(){
timer_manager(false);
timer_manager(true, onUpdateTimer, 100, 0, 10000);
timer.reset();
timer.start();
});
<p>Elapsed time: <span id="time">0</span>s</p>
<p id="accTime"></p>
<p id="status"></p>
<button id="start">start</button>
<button id="stop">pause</button>
<button id="restart">restart</button>


Related Topics



Leave a reply



Submit