2

I've been trying to manage a timer via recursive setTimeout in react-native.

But i'm facing the problem that in some devices the timer is taking some time more in process(like 1-4 seconds in around 100-150 seconds timer).

I've already removed setInterval as it was worse than recursive setTimeout. any ideas that how can i make this timer perfect?

Edit: the main problem is that i ran application(In release mode) in 2 or more devices. the timer starts perfectly but devices seem to have very small delay in them, which is quite increasing by time.

The api calls in app are done parrallely.

Code:

AnotherTimerHandler = () => {
    this.time = setTimeout(() => {
        if (this.state.gameState == timesup) {
            console.log(timesup)
            this.setState({ timer: this.state.timer - 1 });
            if (this.state.timer <= 0) {
                if (this.state.questionIndex < numberOfQuestions - 1) {
                    this.setState({ gameState: splash, timer: splashTime, QAndA: {}, correctAnswer: '', questionIndex: this.state.questionIndex + 1, answered: false })
                } else {
                    // console.log('123')
                    clearInterval(this.time)
                    console.log(this.state.playerMode)

                    if (this.state.playerMode) {
                        const { username, firstName, lastName } = this.props.navigation.state.params.userData;
                        firebase.database().ref(`tblGame/${gameIdToLoad}/gameWinners`).push({ Email: firebase.auth().currentUser.email, Name: firstName + ' ' + lastName })
                            .then(() => this.props.navigation.navigate('Winner', { gameId: gameIdToLoad, prizeAmount: this.props.navigation.state.params.QuizData.prizeAmount }))
                            .catch(err => alert(err))
                    } else { this.props.navigation.navigate('Winner', { gameId: gameIdToLoad, prizeAmount: this.props.navigation.state.params.QuizData.prizeAmount }); }
                }
            }
        }
        else if (this.state.gameState == playing) {
            console.log('playing')
            if (this.state.timer == questionTimer) {
                // console.log('playing1', this.state.timer)
                // this.setState({ answerLoaded: false })
                // this.QAndAHandler(Question)
                this.refs.circularProgress.performLinearAnimation(0, (questionTimer - 1) * 1000)
            }
            this.setState({ timer: this.state.timer - 1 })
            // if (this.state.timer == -1) {
            if (this.state.timer <= 0) {
                this.setState({ gameState: timesup, timer: answerTimer }); this.QAndAHandler(Ans);
                // console.log('playing2', this.state.timer)
            }
        }
        else if (this.state.gameState == splash) {
            console.log(splash)
            console.log(this.state.timer)
            this.setState({ timer: this.state.timer - 1 })
            if (this.state.timer == splashTime - 1) {
                this.QAndAHandler(Question)
            } else if (this.state.timer <= 0) this.setState({ timer: questionTimer, gameState: playing, answerLoaded: false })
        }
        // Dont call again if scren is being changed 
    return this.state.gameState == timesup && this.state.timer<=0 && !(this.state.questionIndex < numberOfQuestions - 1) ? null : this.AnotherTimerHandler()    
    }, 1000)
}
  • 1
    What comes to mind is using a short interval and checking `Date.now()` –  Apr 17 '18 at 10:18
  • 2
    Timing is never guaranteed. What exactly are you trying to achieve? – deceze Apr 17 '18 at 10:18
  • What are you calling after the interval? Could the functions be so complex that they themselves are taking 1-4 seconds to execute, rather than the setTimeout / setInterval being out of sync? – Panomosh Apr 17 '18 at 10:19
  • I'm doing api calls but i was clear that it can take quite seconds so i am doing database calls(in my case firebase) parallelly so even after calls fail or take time, it isn't affecting timer. @Panomosh – Prashant Dhameja Apr 17 '18 at 10:21
  • @deceze the timings are varying in different devices. anyway that can make timer go in all devices the same way. may be delayed but same delay? – Prashant Dhameja Apr 17 '18 at 10:26
  • 2
    Timing. Is. Never. Guaranteed. Not between devices, not within one device. Your code needs to expect that fact and work with it. What exactly is your code and what are you trying to do? – deceze Apr 17 '18 at 10:27
  • @deceze im trying to create a simple quiz game which shows splash screen for some n seconds, lets users to play for some n seconds and shows answer to them for some seconds. you can check code in question.! – Prashant Dhameja Apr 17 '18 at 10:30
  • @ChrisG can't relay on Date.now() as if person changes time manually, date.now() will make things worst. – Prashant Dhameja Apr 17 '18 at 10:40
  • And if someone pauses your script execution and fiddles around with the debugger all bets are off… – deceze Apr 17 '18 at 10:45
  • umm nope. im using babel-plugin-transform-remove-console . suggested at https://facebook.github.io/react-native/docs/performance.html so console.logs would be removed so one wouldn't get anything. anyways would be great if you'd suggest a way. @deceze – Prashant Dhameja Apr 17 '18 at 10:50
  • @Kaiido the only function used in code is this.QAndAHandler. and please check code, i said 100-150 'seconds' timer. its 4 to 5 seconds in 100-150 seconds. and the QAndAHandler is used for api calls, the problem still stays even after its removed, all other things done in code are changing states and timer state to show question, answers and splash screen. – Prashant Dhameja Apr 17 '18 at 11:06
  • Ah my bad, misread ms for these 100-150. How precise do you need it to be? 4s on 150s seems quite big indeed, but for ±100ms precision that can be easily worked around by setting smaller and dynamic timeouts based on current time. If you need <30ms precision, then you might be interested in [the WebAudioAPI clock](https://stackoverflow.com/questions/40687010/canvascapturemediastream-mediarecorder-frame-synchronization/40691112#40691112). – Kaiido Apr 17 '18 at 11:15

2 Answers2

0

"Timing is never guaranteed",
but 4 seconds difference on 150ms interval can indeed be seen as big.

One way to circumvent this is splitting your timings in smaller dynamics intervals, self-correcting its own delay.

Here is a dumb implementation that will reschedule the next tick to the next second, correcting itself its delay at every tick:

// Self-correcting setInterval
// intended for long intervals
// returns an object which "_id" property is the inner timeout id, so it can be canceled by clearInterval
function selfCorrectingInterval(cb, ms) {

  var innerTimeout = ms < 1000 ? 100 : 1000, // fire every ?s
    begin = performance.now(), // what time is it?
    last = begin + ms, // when should all this end?
    next = Math.min(innerTimeout, ms),
    prev = begin,
    result = {
      _id: setTimeout(inner, next)
    },
    passed = true; // a flag to avoid try-catch the callback
  return result;

  function inner() {
    if (!passed) return;
    passed = false; // set up the callback trap

    var shouldCall = false;
    var now = performance.now(),
      delay = (now - prev) - innerTimeout;
    prev += innerTimeout; // fixed increment

    if (last - now < 6) {
      shouldCall = true;
      begin = last; // start a new interval
      last += ms;
    }

    next = Math.min(innerTimeout - delay, last - now);
    result._id = setTimeout(inner, next);
    // call it at the end so we can cancel inside the callback
    if (shouldCall) {
      cb();
    }
    passed = true; // didn't throw we can continue
  }
}

// snippet-only tests
function test(ms) {

  function setTimeoutLoop(cb, ms) {
    function loop() {
      cb();
      setTimeout(loop, ms);
    }
    setTimeout(loop, ms);
  }

  var now = performance.now(),
    built_in_prev = now,
    timeout_prev = now,
    sCI_prev = now,
    built_in_elem = document.querySelector('#test_' + ms + ' .delay.built_in'),
    timeout_elem = document.querySelector('#test_' + ms + ' .delay.timeout'),
    sCI_elem = document.querySelector('#test_' + ms + ' .delay.sCI');

  setInterval(() =>  {
    var now = performance.now(),
      delay = (now - built_in_prev) - ms;
    built_in_prev += ms;
    built_in_elem.textContent = Math.round(delay);
  }, ms);

  setTimeoutLoop(() => {
    var now = performance.now(),
      delay = (now - timeout_prev) - ms;
    timeout_prev += ms;
    timeout_elem.textContent = Math.round(delay);
  }, ms);

  selfCorrectingInterval(() =>  {
    var now = performance.now(),
      delay = (now - sCI_prev) - ms;
    sCI_prev += ms;
    sCI_elem.textContent = Math.round(delay);
  }, ms);

}

test(1000);
test(5000);
test(60000);
test(150000);
[id^='test'] {
  border: 1px solid;
  padding: 0 12px
}
<div id="test_1000">
  <p>built in setInterval delay for 1000ms interval: <span class="delay built_in">0</span>ms</p>
  <p>built in setTimeout loop delay for 1000ms interval: <span class="delay timeout">0</span>ms</p>
  <p>selfCorrectingInterval delay for 1000ms interval: <span class="delay sCI">0</span>ms</p>
</div>
<div id="test_5000">
  <p>built in setInterval delay for 5000ms interval: <span class="delay built_in">0</span>ms</p>
  <p>built in setTimeout loop delay for 5000ms interval: <span class="delay timeout">0</span>ms</p>
  <p>selfCorrectingInterval delay for 5000ms interval: <span class="delay sCI">0</span>ms</p>
</div>
<div id="test_60000">
  <p>built in setInterval delay for 1 minute interval: <span class="delay built_in">0</span>ms</p>
  <p>built in setTimeout loop delay for 1 minute interval: <span class="delay timeout">0</span>ms</p>
  <p>selfCorrectingInterval delay for 1 minute interval: <span class="delay sCI">0</span>ms</p>
</div>
<div id="test_150000">
  <p>built in setInterval delay for 150s interval: <span class="delay built_in">0</span>ms</p>
  <p>built in setTimeout loop delay for 150s interval: <span class="delay timeout">0</span>ms</p>
  <p>selfCorrectingInterval delay for 150s interval: <span class="delay sCI">0</span>ms</p>
</div>

And that's how I discover Chrome's implementation of setInterval already does correct itself...

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Hey, thanks for the answer, but in my particular case i cant relay on performance.now because the requirement is that it shouldn't relay on device time so if user changes devTime this won't work. Thanks for answer though :) – Prashant Dhameja Apr 18 '18 at 10:10
  • @PrashantDhameja not sure what you mean by devTime, but if you are talking about the os local time, then performance.now is not tied to it, it's based on the CPU clock. It might get stuck if the device is in sleeping mode though. – Kaiido Apr 18 '18 at 10:16
-1

If you have very small time interval to fire your callback then javascript based timer would not be suitable, but if you have longer time interval to fire your callback then that will work. Try using this, https://github.com/ocetnik/react-native-background-timer Hope this would give better result, precession will be still questionable as it is relative term.

Kunal
  • 564
  • 5
  • 12
  • Hey, i've checked this library already but seems like its not being managed anymore. i tried to install it but was facing issues in iOS. – Prashant Dhameja Apr 18 '18 at 10:09