Reputation: 4493
I have a problem with reporting progress within the method Process2() in my code below. I want to increment the progress bar after every line that is read, but doing so blocks the UI and it becomes unresponsive. If I comment out the progress.Report() line, it no longer blocks UI thread. Does anyone know why this happens and how I can fix it?
Here is fully working code that can be pasted into a starter WPF application.
Click the Run button (there might be a small pause at the beginning for generating the file, wait until after Generating file is Done) and try to move the window around, it remains frozen.
WARNING: this code will generate a text file in your bin\Debug folder (or whatever your configuration is pointing to) It may not be able to write this file if you run this from a network path, thus recommended to run from local disk.
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace WpfApplication2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
Queue<string> queue = new Queue<string>();
List<string> stringsCollection = new List<string>() { "1_abc123_A_AA_zzz", "2_abc123_AAAA_zzz", "3_abc123_AAAAAA_zzz" };
int linesCount = 0;
int totalLines = 0;
string ASSEMBLY_PATH;
string file;
public MainWindow()
{
InitializeComponent();
ASSEMBLY_PATH = ReturnThisAssemblyPath();
file = ASSEMBLY_PATH + @"\test.txt";
generateFile();
}
private async void Button_Click2(object sender, RoutedEventArgs e)
{
linesCount = 0;
Progress<int> process2_progress;
this.progress.Value = 0;
this.status.Text = "";
process2_progress = new Progress<int>();
process2_progress.ProgressChanged += Process2_progress_ProgressChanged;
this.status.Text += "Searching..." + Environment.NewLine;
await Task.Run(() =>
{
totalLines = System.IO.File.ReadLines(file).Count();
foreach (string s in stringsCollection)
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)(() =>
{
this.status.Text += "Searching " + s + Environment.NewLine;
}));
List<string> strCollection = Process2(s, process2_progress);
foreach (string str in strCollection)
queue.Enqueue(str);
}
});
this.status.Text += "DONE!!" + Environment.NewLine;
}
private void Process2_progress_ProgressChanged(object sender, int e)
{
linesCount += e;
this.progress.Value = linesCount * 100 / totalLines;
}
List<string> Process2(string inputString, IProgress<int> progress)
{
List<string> result = new List<string>();
foreach (string line in System.IO.File.ReadLines(file, new UTF8Encoding()))
{
progress.Report(1);
}
return result;
}
void generateFile()
{
this.status.Text += "Generating FIle..." + Environment.NewLine;
int count = 0;
using (StreamWriter sw = new StreamWriter(file, true))
{
do
{
sw.WriteLine(Guid.NewGuid().ToString());
count++;
} while (count < 51000);
}
this.status.Text += "Done Generating FIle!" + Environment.NewLine;
}
public string ReturnThisAssemblyPath()
{
string codeBase = Assembly.GetAssembly(typeof(MainWindow)).CodeBase;
UriBuilder uri = new UriBuilder(codeBase);
string path = Uri.UnescapeDataString(uri.Path);
return System.IO.Path.GetDirectoryName(path);
}
}
}
MainWindow.xaml
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication2"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox x:Name="status" Grid.Row="0"></TextBox>
<Button Grid.Row="2" Height="50" Click="Button_Click2">Run2</Button>
<ProgressBar x:Name="progress" Grid.Row="3" Height="20" ></ProgressBar>
</Grid>
</Window>
Upvotes: 4
Views: 823
Reputation: 54907
I suspect your issue is that you're reporting progress too frequently. If the work that you're doing between Report
calls is trivial (such as reading just one line from a file), then the dispatching of operations to the UI thread will become your bottleneck. Your UI dispatcher queue becomes flooded, and it won't keep up with new events such as responding to mouse clicks or moves.
To mitigate this, you should reduce the frequency of your Report
calls to a sensible level -- for example, only calling it when a batch of 1,000 lines are processed.
int i = 0;
foreach (string line in System.IO.File.ReadLines(file, Encoding.UTF8))
{
if (++i % 1000 == 0)
progress.Report(1000);
}
In response to the comments: The file size doesn't matter when picking the batch size. Rather: Find a sensible target for the update frequency -- say, 100 ms. Measure or estimate the time it takes to read and process one line -- for example, 100 μs. Divide the former by the latter, and you get your answer. We picked 1,000 because we estimate that 1,000 lines would take 100 ms to process. The optimal update frequency lies around 10–100 ms, which is the limit of human perception; anything more frequent than that will not be noticed by the user.
Per the above, your 10- and 500-line files don't need to issue any updates to the UI, because they would have been processed completely in mere milliseconds, before the user has the chance to observe any progress. The 1,000,000-line file would take around 100 seconds in total, and it will update the UI 1,000 times during that period (once every 100 ms).
Upvotes: 7