1

I'm making a simple countdown timer that displays the seconds and setseconds left. When an user clicks a button the countdown should stop and some text should be displayed instead of the timer. Everything works fine on pc but if I use my phone with a worse cpu I sometimes get this unexpected behaviour: when the user clicks the button the text shows for a fraction of a second and then the timer keeps going on. I thought it was caused by the clearTimeout() method being called when the function was running and right before a new timeout would be called but this post proved me wrong. Do you have any idea what's causing this behaviour and how should I go about fixing it?

const finished = () => { // finished() is also called when user clicks a button
 // show some thext on the screen
    clearTimeout(timeout);
}

const start = document.timeline.currentTime! // Think of it as Date.now(); also returns a ms value; just slightly faster.
const end = start + 30000 // the countodwn takes 30 seconds
let timeout:any;

const frame = () => {
    const elapsed = Math.floor(end - document.timeline.currentTime!)

    if (elapsed <= 0) { 
        finished()
        return
    };

    let secs: number | string = Math.floor(elapsed / 1000)
    let setsecs: number | string = Math.floor((elapsed % 1000) * 0.1)

    secs = secs < 10 ? "0" + secs : secs
    setsecs = setsecs < 10 ? "0" + setsecs : setsecs

    result.innerText = `Time left: ${secs}.${setsecs}`

    timeout = setTimeout(() => requestAnimationFrame(frame), 15 - elapsed % 15) // raf is for optimisation and I'm substracting the timeout by the time that has already passed for drift minimisation
}

frame()

EDIT: I managed to recreate the slow behaviour on my phone with promises: Here's the code

Confuze
  • 37
  • 1
  • 7
  • Could maybe this thread be a solution to you? https://stackoverflow.com/questions/31106189/create-a-simple-10-second-countdown/50846458#50846458 – treckstar Apr 27 '22 at 13:16
  • I copied your code to TypeScript Playground and it works. It might be an issue with your script related to the user button. – Edwin Apr 27 '22 at 13:27
  • @Edwin it works totally fine on a pc. The issue only occurs on slower cpus. The button script itself is very simple. Basically just a `finished()` callback in the onclick attribute. – Confuze Apr 27 '22 at 13:39
  • Please have a look on this: [TS Playground](shorturl.at/fqrJ3). I set finish to be fired after 2000 to act like a user event, end after +3000, nextTimeOut after 4000. However, it still works. – Edwin Apr 27 '22 at 13:47
  • 1
    @Edwin Thanks for replying! The code works because the time between the function being called and the timeout being registered is very little on pc. I managed to recreate the behaviour on my slow phone by using a promise that slows down the function. Please take a look: (the link is too long so I'll edit the post and put it there) – Confuze Apr 27 '22 at 14:00

1 Answers1

1

TL;DR

Problem

The time is too short between setTimeout and clearTimeout. A rescheduling is fired behind clearTimeout.

Example

Use a boolean flag to check if that finished function had been called.


Solution

TS Playground

const start = document.timeline.currentTime! // Think of it as Date.now(); also returns a ms value; just slightly faster.
const end = start + 3000 // the countodwn takes 3 seconds
let timeout:any;

let finishedCalled = false
const finished = () => { // finished() is also called when user clicks a button
 // show some thext on the screen
    finishedCalled  = true
    clearTimeout(timeout)

    console.log(`Finished!`)
}

setTimeout(finished, 2000)

const frame = async () => {
    if(finishedCalled) return console.log(`Finished called found. Have to be end here.`)
    const elapsed = Math.floor(end - document.timeline.currentTime!)
    console.log(`Elapsed:`, elapsed)

    if (elapsed <= 0) { 
        finished()
        return
    };

    let secs: number | string = Math.floor(elapsed / 1000)
    let setsecs: number | string = Math.floor((elapsed % 1000) * 0.1)

    secs = secs < 10 ? "0" + secs : secs
    setsecs = setsecs < 10 ? "0" + setsecs : setsecs

    console.log(`Time left: ${secs}.${setsecs}`)

    await new Promise((resolve) => setTimeout(resolve, 500));

    timeout = setTimeout(() => {
        console.log(`Timer fired!`)
        requestAnimationFrame(frame)
    }, 10) // raf is for optimisation and I'm substracting the timeout by the time that has already passed for drift minimisation
}

frame()

Dig Deeper

Let's use Node.js as an example and trace down the source.

Until this point, we can know it is an async call and may be rescheduled before execute. That's why the finished function cannot stop the timer.

Disclaimer: I am not an expert of C++. Please feel free to improve the answer.

Edwin
  • 395
  • 2
  • 12