'UI thread slow to respond to Progress updaters on async Task method using VS2022 & Net6.0

I’ve run into a performance obstacle and I’m uncertain of the cause, all of this is running under VS2022 & Net6.0. As this is my 1st time using this combination of a modal windows form, and progress bar, with the work running on a background thread and two Progress objects updating the UI, the progress bar, and a text label, I don’t know where to attack the problem. Prior to placing the workload on a background thread, everything was snappy, searching a thousand files with about 600 lines of text in each, in about a minute. Naturally, the windows form was frozen during this, which is why the workload was placed on a background thread.

After doing so, the workload will be 25-50% complete before the UI starts displaying the values from the Progress objects, and overall, the entire process now takes 10x as long to complete. Progress objects aren’t skipping over any values sent to them, the UI thread just seems slow in getting the information. Likewise, if I try to drag the modal form to a new spot on the desktop it’s unresponsive for 20—30 seconds before it finally moves. One more thing, I can step through the code on the background thread and see it calling the Progress updaters, but the UI thread is just very slow in responding to them.

I could use some suggestions on how to uncover the problem or if clearly evident, point out where the likely problem could be. Here are the essential controls and methods used.

public class SearchProgressForm : Form
{
    private System.Windows.Forms.Button btnSearch = new Button();
    private System.Windows.Forms.TextBox txtTextSearch = new TextBox();
    private System.Windows.Forms.Label lblSearchFile = new Label();
    private System.Windows.Forms.ProgressBar SearchProgressBar = new ProgressBar();

    public event LogSearchEventHandler SearchSucceededEvent;
    protected void OnSearchSucceeded(LogSearchEventArguments p_eventArguments)
    {
        LogSearchEventHandler handler = SearchSucceededEvent;
        if (handler != null)
        {
            handler(this, p_eventArguments);
        }
    }

    private void InitializeComponent()
    {
        this.btnSearch.Name = "btnSearch";
        this.btnSearch.Text = "Search";
        this.btnSearch.Click += new System.EventHandler(this.btnSearch_Click);

        this.lblSearchFile.Text = "Searching File: ";
        this.txtTextSearch.Text = "search string";
    }

    public SearchProgressForm() { }

    private void btnSearch_Click(object sender, EventArgs e)
    {
        this.SearchByText(this.txtTextSearch.Text);
    }

    private void SearchByText(string p_searchParameter)
    {
        // Setup a progress report for thr ProgressBar
        var _progressBarUpdate = new Progress<int>(value =>
        {
            this.SearchProgressBar.Value = value;
            this.SearchProgressBar.Refresh();
        });

        var _progressFileNameUpdate = new Progress<string>(value =>
        {
            this.lblSearchFile.Text = "Searching File For : " + value;
            this.lblSearchFile.Refresh();
        });

        // Start search on a backgroud thread and report progress as it occurs
        Task.Run(async () => await this.SearchByStringAsync(p_searchParameter, _progressBarUpdate, _progressFileNameUpdate));
    }

    private async Task SearchByStringAsync(string p_searchParameter, IProgress<int> p_progressBar, IProgress<string> p_progressFileName)
    {
        await Task.Delay(1);
        TextFileReader textFileReader = new TextFileReader();
        LogSearchEventArguments logSearchEventArguments = null;
        long _sessionloopCount = 0;
        long _totalTextLinesCount = this.GetTotalSearchCount(p_searchParameter, SearchType.TextString);


        // Get file names from SQL table
        var _logFiles = DataOperations.LogFileSortableList(null);
        foreach (var log in _logFiles)
        {
            // Format a file name to be read from the file system
            string _fileName = log.Directory + "\\" + log.FileName;

            p_progressFileName.Report(log.FileName);

            // If we've raised an event for this file, then stop iterating over remaning text
            if (logSearchEventArguments != null)
            {
                logSearchEventArguments = null;
                break;
            }

            // Read in file contents from file system
            List<string> _fileContents = textFileReader.ReadAndReturnStringList(_fileName);
            long _fileTotalRecordCount = _fileContents.Count;
            long _fileRecordCount = 0;

            foreach (var _line in _fileContents)
            {
                if (_line.ToUpper().Contains(p_searchParameter.ToUpper()))
                {
                    // Raise an event so search parameter and file name can be captured in another form
                    logSearchEventArguments =
                        new LogSearchEventArguments
                        (
                            "TextSearch", p_searchParameter, SearchType.TextString, true, log,
                            new DateTime(
                                Convert.ToInt32("20" + log.FileName.Substring(14, 2)),
                                Convert.ToInt32(log.FileName.Substring(16, 2)),
                                Convert.ToInt32(log.FileName.Substring(18, 2)))
                        );

                    // We found a match, so no further searching is needed in this log file,
                    // and it's been flagged in the DB, so raise the event to save search parameter and file name
                    // then break out of this loop to get the next file to search in.
                    this.OnSearchSucceeded(logSearchEventArguments);
                    break;
                }

                // These calcs are based on actual searches performed
                _fileRecordCount++;
                _sessionloopCount++;

                p_progressBar.Report(Convert.ToInt32((_sessionloopCount * 100) / _totalTextLinesCount));
            }

            // Because we exit a search as soon as the 1st match is made, need to resynch all counts
            // and update the progress bar accordingly
            if (_fileRecordCount < _fileTotalRecordCount)
            {
                long _countDifference = _fileTotalRecordCount - _fileRecordCount;

                // Add count difference to sessionLoopCount and update progress bar
                _sessionloopCount += _countDifference;
                p_progressBar.Report(Convert.ToInt32((_sessionloopCount * 100) / _totalTextLinesCount));
            }
        }

        //Search is complete set Progress to 100% and report before exiting
        p_progressBar.Report(100);

        // Close the modal SearchForm and exit
        this.Close();
    }
}


Solution 1:[1]

I solved this problem but I'm still not certain of what caused it. I eliminated the method "private void SearchByText(string p_searchParameter)" and moved the code there into the btnSearch_Click event handler so I could call my background worker "SearchByStringAsync" directly from the button click event handler.

I also updated the EFCore NuGet Packages, which were version Net6.0 to version 6.0.4, because of single line of code in my Async background method, "var _logFiles = DataOperations.LogFileSortableList(null)".

That call returned a Sortable BindingList, using BindingList <T>. Between the NuGet updates and a minor change on a custom comparer method in my BindingList <T> class, the windows modal form now updates the ProgressBar and Label text as expected, and the form now responds immediately to user interaction.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 DooHickey