Making a persistent, accurate timer without a real time operating system (ie nearly everything!)

One way to make a timer is:


let seconds = 0;
setInterval(() => {
  console.log(seconds);
  seconds++;
}, 1000);
          

and, for a rough idea of elapsed time, over a short-ish period that would be fine…ish. The problem is that running code like this in anything other than a Real Time Operating System (RTOS) is going to lead to inaccuracies.

  1. Without an RTOS there is no guarantee that 1000ms is actually 1000ms seconds, if another process caused the CPU to run your code late at 1010ms you could have no idea, let that happen a few dozen times and now your timer is several seconds out.
  2. Clock-drift, less of a problem over short periods but cumulatively over a long period the difference between the frequency of a CPUs clock (due to environmental reasons or workload) and what you think it is can cause a large offsets in time. The best example is merging and syncing long recordings from multiple sources – ask anyone who tries to combine multi track podcasts, not all clocks are the same and the audio drift can be large.
  3. The timer above only exists in memory, refresh the page, close your app and it’s gone – very inaccurate!

So, how do we solve this?

The solution is straightforward, switch from thinking about a timer as an active thing whirring away, and instead think of it as simply a point marked in time. A logical timer if you like.

All platforms have the ability to tell you what time it is right now to within a few milliseconds. So when we want to start a timer we simply record the start datetime, and when we want it to end it, we record that datetime as well, we then compare the difference between the two points in time.

Running timer

In JavaScript you can do this very simply with the MomentJS library:


let start = 0;
let end = 0;

onStart() {
  start = moment();
}

onStop() {
  stop = moment();
  console.log(stop.diff(start, 'milliseconds'));
}
          

This separates your UI code from you timer code as this is now basically a headless timer, taking up very little resource and not subject to whims of the CPU as to exactly when it gets run.

You can leave this running for days if needs be and so long as start still exists in memory you will get an accurate measurement of the elapsed time.

What about a count down timer? In this instance we will need to either do some polling or rely on a single interrupt. I prefer the polling approach as I can then use the same loop to update my UI with the timer status. So, how would we do that?

Count down timer


const duration = 100000; // 10 seconds in ms
let timerExpiration = null;
let interval = null;
let displayedTime = 0;

onStart() {
  timerExpiration = moment().add(duration, 'milliseconds');
  startPolling();
}

startPolling() {
  interval = setInterval(() => {
    if (timerExpiration.isBefore(moment())) {
      console.log('Time Up!');
      timerExpiration = null;
      clearInterval(interval);
    }
    // update UI
    displayedTime = moment().diff(timerExpiration, 'milliseconds')
  }, 10); // this will poll every 10 ms, increase for less accuracy, decrease for more
}
          

This will only be as accurate as your interval polling period. However, as you are simply comparing two points in time (now and the expiration) your error scope resets each loop, rather than accumulating like when decrementing a duration by 1 each time. The timer will always expire at the same time, how often you poll it just determines how soon after it expired you notice and can update your UI.

Persistance

We now have an approach that is much better than a counter however, it still suffers from the problem of living in memory only. Refreshing the page, closing the app or turning off the device will loose the timer.

Since we are just looking at fixed points in time and comparing them to now() this is easy to solve.

We can use whatever local storage is available to save the relevant data required to “rebuild” the timer.

So using the running timer example:


// timer component code
let start = 0;
let end = 0;

onStart() {
  start = moment();
  LocalStorage.write({ 'activeTimer': start.toJSON() });
}

onStop() {
  stop = moment();
  LocalStorage.remove('activeTimer');
  console.log(stop.diff(start, 'milliseconds');
}

...
...
...

// app launch code
if (LocalStorage.get('activeTimer') !== null) {
  start = JSON.parse(LocalStorage.get('activeTimer'));
}
          

LocalStorage in this case could be a database, local plist or even a text file.

Now we have a timer that:

  1. can run indefinitely,
  2. isn’t subject to clock drift,
  3. doesn’t get lost when we refresh a page or restart a device.