1

I work with creating timers / stopwatches often and I've always come upon a problem.

When using setInterval();, it is rather rarely ever accurate. I'm using Safari 8, and after 20 seconds, it can be off up to +-8 seconds. I'm trying to time every 10ms so requestFrameAnimation won't cut it because I'm not animating.

I've come up with the following which uses the Date object

var runs = 0,
    speed = 10,
    timeout = speed,
    time = Math.floor(new Date().getTime() / speed);

function timer () {

    runs += 1;

    console.log(runs, new Date().getTime() / speed);

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

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

    setTimeout(function () {
        timer(); //Repeat
    }, timeout);
}

timer();//Starts timer

Except that can still have 3 runs within 100 ms (.6, .64, .68) which is about a third of the speed.

I've seen many solutions on how this can be achieved with other languages such as Node.js, Java, C# and even with JavaScript libraries but I can't seem to solve this basic problem.

Is there something I'm clearly missing? What's the best way to do this?

Downgoat
  • 13,771
  • 5
  • 46
  • 69
  • Possible duplicate? Check this out! http://stackoverflow.com/questions/6233927/microsecond-timing-in-javascript – uotonyh Apr 10 '15 at 00:05
  • @uotonyh I've seen that post. It doesn't really go over accurate timers and instead more accurate *time*. Time is not the problem, it's accurate enough. It's that the timers sometime run slow or fast – Downgoat Apr 10 '15 at 00:07
  • After only 20 seconds you're off by 8??? Are you sure? What kind of precision (you cannot get perfect in JS with timers) you need? – Roko C. Buljan Apr 10 '15 at 00:18
  • @RokoC.Buljan I don't need it to be perfect but accurate. `setInterval` often runs slow and can be off by many a seconds. I'm hoping for at least accurate to the second or tenth of a second – Downgoat Apr 10 '15 at 00:20
  • 10th of a second (is really kind from you :) in terms of precision) and can be achieved. – Roko C. Buljan Apr 10 '15 at 00:22
  • Hint: You reference `setInterval()` several times, but you're code demonstrates a self-calling `setTimeout()`. Also, being off by eight seconds after only 20 seconds seems extreme. Javascript is [single-threaded](http://ejohn.org/blog/how-javascript-timers-work/), so it would seem you have something else blocking execution for "long periods" of time. `setTimeout()` is a better option in my opinion that `setInterval()` for this type of exercise (so you don't get execution stacking), but due to the low priority, it's never going to be exactly accurate. – Jared Farrish Apr 10 '15 at 00:30
  • BTW, Node.js is not a language – James Parsons Apr 10 '15 at 00:43
  • @James_Parsons yeah, I know that and was debating weather to state that I know it. But it's plenty different from JavaScript, enough to have a different answer. *(I barely know anything about Node.js besides that it is a server-side JavaScript solution using V8)* – Downgoat Apr 10 '15 at 00:44
  • What does the *3 runs within 10ms* part mean? – Jared Farrish Apr 10 '15 at 00:46
  • @JaredFarrish Ah, that's a typo, should be 100ms – Downgoat Apr 10 '15 at 00:46
  • @vihan1086 Ok, I'll give you that. – James Parsons Apr 10 '15 at 00:49
  • You're trying to run the timer more frequently so you're (more likely) to hit each 100ms segment of a second, correct? So you can make sure you're doing something each 100ms segment? – Jared Farrish Apr 10 '15 at 00:50
  • Something like this: http://jsfiddle.net/y3zL84ox/3/ – Jared Farrish Apr 10 '15 at 01:14
  • Little easier to follow: http://jsfiddle.net/y3zL84ox/4/ – Jared Farrish Apr 10 '15 at 01:21
  • With precision: http://jsfiddle.net/y3zL84ox/5/ (I did see some precision > 100, meaning it was more than 100ms behind, but this was on page load. This goes back to the old saw, when the browser's busy...) – Jared Farrish Apr 10 '15 at 01:29
  • @JaredFarrish Your system works fine too but after some timing. After **30.13** seconds, your timer was at **26.44** seconds, and `setInterval` was at **22.70**. I don't expect perfect times, just not non-accurate – Downgoat Apr 10 '15 at 19:01
  • How are you calculating 26.44? – Jared Farrish Apr 10 '15 at 22:48
  • @JaredFarrish I both of them for 30 seconds *(timed by my timer)*. Then I stop it. – Downgoat Apr 10 '15 at 22:51
  • That doesn't explain what calculation you're using. What does 26.44 mean? Is it the capture of the time when it started minus the time it ended? – Jared Farrish Apr 10 '15 at 22:55
  • @JaredFarrish That means after **30.13** seconds (real time), the timer showed **26.44** seconds. They were started at the sam time – Downgoat Apr 10 '15 at 22:56
  • So you're trying to increment the timer in seconds with a recurring function? Why don't you show the time according to the difference between when it started and the current time? It's impossible to do what you want with Javascript as well; the farther out you go, the more likely you are to have frame interruptions, leading to the loss of precision that's noticeable (as you're seeing). That's just a reality with Javascript. – Jared Farrish Apr 10 '15 at 22:58
  • It's possible you could use web workers (which run in a background thread). This looks suspiciously like your page linked below: http://frenticb.com/tricks/simple-timer.php (That tracked exactly with my phone's stopwatch.) – Jared Farrish Apr 10 '15 at 23:04

3 Answers3

3

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/

Jared Farrish
  • 48,585
  • 17
  • 95
  • 104
2

If you are trying to run it every 10th of a second, could your problem be that your initial speed is set to 10? 1000 ms = 1 second, so 10 / 1000 = 1 / 100th of a second. Also, the getTime function returns a Unix time stamp. Dividing that by speed doesn't make much sense unless you record the previous time stamp, subtract the current time stamp and adjust speed for any offset in execution time.

Joshua Dannemann
  • 2,003
  • 1
  • 14
  • 34
0

What you want to do is technically impossible for two reasons:

  1. The speed of the machine on which your code is running

  2. The synchronous nature of JavaScript

To explain, if a computer cannot execute the instructions given within 10 ms or 100 ms, it's not going to. The best you can do is try to adjust for the delay, but even then, any other processing that machine is doing and any other JavaScript that the browser is executing is going to block execution of your "threads" (a term I am using loosely).

I just wrote some code to test this out because I was curious and on average the most efficient function I can write is within 67% excess of accurate timing given a 10th of a second timeout.

I know it's bad news, but it's the truth. What you want to do cannot be accomplished.

Joshua Dannemann
  • 2,003
  • 1
  • 14
  • 34
  • I realize it can never be perfect. [this](http://vihan.ml/temp/timer.html) is an example. It can get very far off real-time which can be problem since it's point is to be a timer. – Downgoat Apr 10 '15 at 18:53
  • I think probably the best answer posted so far is Jared's. Web workers might be the best means of gaining higher accuracy, but like I said in my answer, there is technically still no probable way to get accuracy down to every 10th of a second. I ran another experiment and even with my fastest computer I could only get to 40% over a tenth of a second on average. Moreover, that's without anything else running that could block the timer. – Joshua Dannemann Apr 15 '15 at 01:02