BackburnerJS / backburner.js

A rewrite of the Ember.js run loop as a generic microlibrary
MIT License
392 stars 80 forks source link

Allow _later() to recover when time travelling #354

Closed kanongil closed 5 years ago

kanongil commented 5 years ago

So, given that the timer queue is scheduled based on times calculated from Date.now(), it is possible that it will "time travel" back and forth. The current code, unfortunately doesn't handle this.

Example:

bb.setTimeout(() => console.log('slow'), 60 * 60 * 1000); // at = "now" + 3600000

// ... system time is adjusted forward 1 hour ...
// wait time is still ~3600000 (which I think is correct, but "at" is wrong)

bb.setTimeout(() => console.log('fast'), 10); // at = "now + 3600000" + 10

Since the "fast" timeout at is after the at of the "slow" timeout, the wait time won't be recalculated, and the "fast" timeout has to wait until the hour has passed to fire.

When exactly the "slow" timer should fire, is debatable, and secondary. The main issue, is that the "fast" timeout is delayed.

rwjblue commented 5 years ago

I'm not terribly sure what we can do about this. We are essentially just using the platform to queue setTimeout's, and I don't see a way for us to detect the bad state here (that the platform failed to deliver the timer that it should have) until the subsequent timer is scheduled. We could check if the first timer's executeAt < platform.now(), but I'm not sure what the right correction should be (reinstall the first timer with a setTimeout(..., 0), splice the first timer out and pretend its gone, just add an extra timer for the subsequent timer)...

kanongil commented 5 years ago

One fix for this, is to always recompute the timer (though the "slow" timer would arguably fire too soon).

Another, is to use a better time source, like performance.now(). Unfortunately this has some issues as well, since afaik, it's platform dependent if the timer is advanced while in the "frozen" state.

kanongil commented 5 years ago

A bit of elaboration on performance.now() on what makes it better than Date.now():

Remaining issues:

  1. Platform support (notably doesn't work with iOS < 9, and Android < 4.4).
  2. Potential platform discrepancies in how the time is advanced during sleep / frozen state.

(2) Can possibly be detected and worked around using the page life-cycle api, and Date.now() checkpoints. Eg. suspend all timers when frozen, and store the Date.now() and performance.now() values. Then on resume, obtain the current Date.now() and performance.now(), and if the performance.now() has not advanced meaningfully, while Date.now() has, add the Date.now() delta to the computation.