Why is this timer / stopwatch off eventually after about 15 minutes, 5 second diff

112 Views Asked by At

I found [this article] that shows how to build a stopwatch / timer and after about 15 minutes there's 5 second gap between my physical timer (good battery; physical timer goes ahead 5 seconds)

async Task StartTimer()
{
    SetButtonState();        

    isStopWatchRunning = true;
    while (isStopWatchRunning)
    {
        await Task.Delay(1000);
        if (isStopWatchRunning)
        {
            stopWatchValue = stopWatchValue.Add(new TimeSpan(0, 0, 1));
            StateHasChanged();
        }
    }
}
3

There are 3 best solutions below

0
LoneSpawn On

You are not measuring and accounting for how long your other code takes to run such as the call to StateHasChanged() and it is called ~900 times in that 15 minutes. Also, Task.Delay is not guaranteed to be accurate as stated in the responses to the StackOverflow question about its accuracy in the link below; which also has some possible solutions.

Accuracy of Task.Delay

0
Dmitry Bychenko On

The reason is that Task.Delay is not precise; the actual interval resolution is about 15 ms.

https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay?view=net-7.0&redirectedfrom=MSDN#System_Threading_Tasks_Task_Delay_System_Int32_

This method depends on the system clock. This means that the time delay will approximately equal the resolution of the system clock if the milliseconds Delay argument is less than the resolution of the system clock, which is approximately 15 milliseconds on Windows systems.

(bold is mine, Dmitrii)

In your case you can expect up to 15 ms * 900 ~ 13.5 s gap.

0
Kurt Hamilton On

The existing answers do a good job of explaining why Task.Delay introduces inaccuracies when it comes to timekeeping.

An alternative approach would be to track the actual start and stop times, and then derive the elapsed time from there.

<div>
    <button @onclick="@OnStart">Start</button>
</div>
<div>
    <button @onclick="@OnStop">Stop</button>
</div>
<div>
    Updating timespan: <span>@StopwatchValue</span>
</div>
<div>
    Comparing elapsed time: <span>@Elapsed</span>
</div>
@code {
    TimeSpan StopwatchValue { get; set; } = new();
    TimeSpan Elapsed { get; set; } = new();

    List<DateTime> StartTimes { get; } = new();
    List<DateTime> StopTimes { get; } = new();

    bool IsRunning { get; set; }

    private async Task OnStart()
    {
        if (IsRunning)
        {
            return;
        }

        IsRunning = true;

        StartTimes.Add(DateTime.Now);

        while (IsRunning)
        {
            await Task.Delay(1000);
            if (IsRunning)
            {
                StopwatchValue = StopwatchValue.Add(new TimeSpan(0, 0, 1));
                Elapsed = GetElapsedTime();
                StateHasChanged();
            }
        }
    }

    private void OnStop()
    {
        if (!IsRunning)
        {
            return;
        }

        StopTimes.Add(DateTime.Now);
        IsRunning = false;
    }

    private TimeSpan GetElapsedTime()
    {
        TimeSpan elapsed = new TimeSpan();

        for (int i = 0; i < StartTimes.Count - 1; i++)
        {
            DateTime start = StartTimes[i];
            DateTime stop = StopTimes[i];
            elapsed += stop - start;
        }

        elapsed += DateTime.Now - StartTimes.Last();

        return elapsed;
    }
}

Example fiddle: https://blazorfiddle.com/s/ko5jseqd