Search code examples
c#winforms

Stopwatch elapsed time occasionally jumps by seconds and doesn't start immediately


Issue Description

I'm experiencing two main issues while using Stopwatch to track elapsed time in my application.

  1. Delayed Start: When I click the start button, the elapsed time takes about 3-5 seconds before it starts counting, even though the application process is already running.

  2. Time Jumps: Occasionally, the elapsed time "jumps" by a few seconds. For example, if the stopwatch reaches 00:00:25, it sometimes freezes momentarily and then jumps straight to 00:00:27, skipping a second.

What I've Tried

  • I attempted to force a UI update immediately by adding a delay, but that didn't fix the initial issues.

  • I tried different ways to ensure smooth UI updates, but the problems persist.

At the top of Form1, I have the following code:

private Stopwatch stopwatch = new Stopwatch();
System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();

in the constructor :

public Form1()
{
    InitializeComponent();

    LoadSettings();
    InitializeTooltips();
    lblWebpage.Select();
}

the button click event that start the operation abd the elapsed time:

private async void btnDownload_Click(object sender, EventArgs e)
{
    SaveSettings();

    InitElapsedTime(); // ✅ Start elapsed time immediately without using Task.Run

    await Task.Delay(10);  // ✅ Give the UI a chance to update before proceeding

    string url = txtUrl.Text.Trim();
    string startPattern = txtStartPattern.Text.Trim();
    string endPattern = txtEndPattern.Text.Trim();
    bool findAll = chkFindAll.Checked;
    bool useRegex = chkUseRegex.Checked;
    string regexPattern = cmbRegexPresets.SelectedIndex > 0 ? cmbRegexPresets.SelectedItem.ToString() : txtRegexPattern.Text.Trim();

    if (string.IsNullOrEmpty(url) || (string.IsNullOrEmpty(startPattern) && !useRegex))
    {
        LogMessage("Error: Please fill in all fields.", Color.Red);
        return;
    }

    LogMessage($"Downloading source from: {url}...", Color.Cyan);
    progressBar.Value = 0;

    try
    {
        using (HttpClient client = new HttpClient())
        {
            string pageSource = await client.GetStringAsync(url);
            LogMessage("Download successful!", Color.Green);
            progressBar.Value = 50;
            await ExtractAndSaveResultsAsync(pageSource, startPattern, endPattern, findAll, useRegex, regexPattern);
        }
    }
    catch (Exception ex)
    {
        LogMessage($"Error downloading source: {ex.Message}", Color.Red);
    }
    finally
    {
        stopwatch.Stop(); // ✅ Stop elapsed time when the process is finished
    }
}

handle extraction operation:

private async Task ExtractAndSaveResultsAsync(string source, string startPattern, string endPattern, bool findAll, bool useRegex, string regexPattern)
{
    List<string> extractedResults = await Task.Run(() => Extractor.Extract(source, startPattern, endPattern, findAll, useRegex, regexPattern));

    if (extractedResults.Count == 0)
    {
        LogMessage("No matches found!", Color.Red);
    }
    else
    {
        int count = extractedResults.Count;
        int processed = 0;

        foreach (var result in extractedResults)
        {
            LogMessage($"Extracted: {result}", Color.LightGreen);

            // ✅ Update progress live
            processed++;

            lblAmountExtractedCounter.Invoke(new Action(() =>
            {
                lblAmountExtractedCounter.Text = processed.ToString();
            }));

            progressBar.Invoke(new Action(() =>
            {
                progressBar.Value = (int)((processed / (float)count) * 100);
            }));

            await Task.Delay(1); // ✅ Prevents UI freezing
        }
    }

    if (chkSaveResults.Checked)
    {
        await Task.Run(() => Exporter.SaveResults(extractedResults, txtSavePath.Text, chkSaveAsTxt.Checked, chkSaveAsCsv.Checked));
        LogMessage("Results saved successfully!", Color.Green);
    }

    progressBar.Invoke(new Action(() =>
    {
        progressBar.Value = 100;
    }));

    // ✅ Final log message: extracted count and timestamp
    LogMessage($"Extraction Complete: {extractedResults.Count} items found | {DateTime.Now:yyyy-MM-dd HH:mm:ss}", Color.Cyan);
}

init the elapsed time:

private void InitElapsedTime()
{
    stopwatch.Reset();  // ✅ Reset stopwatch to start from zero
    stopwatch.Start();  // ✅ Start counting elapsed time immediately

    lblTime.Invoke(new Action(() => lblTime.Text = "00:00:00")); // ✅ Force UI update immediately
    timer.Interval = 1000;  // ✅ Set 1-second update interval
    timer.Tick -= timer_Tick;  // ✅ Remove previous event handlers (prevent multiple events)
    timer.Tick += timer_Tick;  // ✅ Attach new event
    timer.Start();
}

timer tick event:

void timer_Tick(object sender, EventArgs e)
{
    UpdateText();
}

update text method:

void UpdateText()
{
    if (stopwatch.IsRunning)
    {
        TimeSpan elapsed = stopwatch.Elapsed;

        lblTime.Invoke(new Action(() =>
        {
            lblTime.Text = string.Format("{0:D2}:{1:D2}:{2:D2}",
                                         elapsed.Hours, elapsed.Minutes, elapsed.Seconds);
        }));
    }
}

and last the log message method:

private void LogMessage(string message, Color color)
{
    if (this.IsDisposed || !this.IsHandleCreated) return; // ✅ Prevent error if form is closing
    try
    {
        rtbLogger.Invoke(new Action(() =>
        {
            if (this.IsDisposed || !this.IsHandleCreated) return; // ✅ Extra check

            rtbLogger.SelectionStart = rtbLogger.TextLength;
            rtbLogger.SelectionLength = 0;
            rtbLogger.SelectionColor = color;
            rtbLogger.AppendText($"{DateTime.Now:HH:mm:ss} - {message}\n");
            rtbLogger.SelectionColor = rtbLogger.ForeColor;
        }));
    }
    catch (ObjectDisposedException) { /* ✅ Ignore if form is already closed */ }
    catch (InvalidOperationException) { /* ✅ Ignore invalid UI access */ }
}

Solution

  • Take a look at the following section:

    processed++;
    lblAmountExtractedCounter.Invoke(new Action(() =>
    {
        lblAmountExtractedCounter.Text = processed.ToString();
    }));
    
    progressBar.Invoke(new Action(() =>
    {
        progressBar.Value = (int)((processed / (float)count) * 100);
    }));
    await Task.Delay(1); // ✅ Prevents UI freezing
    

    The point of await is that anything after it will continue to run in the same context, i.e. the UI thread, so all controls can be accessed directly, no need for .Invoke. The same with Windows.Forms.Timer, it also raises its events on the UI thread. So the .Invoke does not do anything helpful, and may very well cause issues. The only code runnin in the background is Extractor.Extract and that is not displayed. Given the problem description I would guess you are adding messages to the message queue faster than they can be processed, resulting in erratic behavior.

    The entire loop seem rather pointless since it will not start to do anything until Extractor.Extract has completed. So it will not report any meaningful progress. The correct way would be to create a Progress<T> object, attach an event handler to it that updates the progress bar, and update the progress object from inside the Extractor.Extract method.