Why is the same code much slower in my BackGroundWorker stream than in my graphics stream?

I am trying to create a C # WinForms application that searches and selects text in a RichTextBox. I created two search methods: one that runs in the GUI thread, and one that runs in BackGroundWorker. The logic of both methods is essentially identical. However, code in BGW is much slower.

Please review the results below:

0.25MB Text file, keyword search: GUI: 2.9s - BGW: 7.0s
1MB Text File, Keyword Search: GUI: 14.1s - BGW: 71.4s
5MB Text file looking for a common keyword: GUI: 172s - BGW: 1545s

It seems strange to me that the ratio between the time spent on the two methods is not a liner relative to the size of the search.

The application will be used to search for files up to 10 MB in size, so it is important that this is fast. I wanted to use a background worker so that the user could see the progress and continue reading the file while doing a search.

See the code for the two methods below:

// background search thread private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { // Get the BackgroundWorker that raised this event. BackgroundWorker worker = sender as BackgroundWorker; RichTextBox rtb = new RichTextBox(); RichTextBox results = new RichTextBox(); rtb.Rtf = e.Argument as string; //recive text to be searched int hits = 0; // track number of hits int pos = 0; // track position in rtb int i = 0; // trach current line number for progress report string lowerT = searchTerm.ToLowerInvariant(); string lowerl = ""; int n = 0; int len = searchTerm.Length; foreach (string l in rtb.Lines) { lowerl = l.ToLowerInvariant(); n = lowerl.IndexOf(lowerT); if (n > -1) { while (n > -1) //if found sterm highlight instances { hits++; //incriment hits //hilight term rtb.SelectionStart = pos + n; rtb.SelectionLength = len; rtb.SelectionBackColor = Color.Yellow; rtb.SelectionColor = Color.Black; //find next n = lowerl.IndexOf(lowerT, n + len); } searchRes.Add(pos); // add positon of hit to results list //add rtb formatted text to results rtb rtb.SelectionStart = pos; rtb.SelectionLength = l.Length; results.SelectedRtf = rtb.SelectedRtf; results.AppendText(Environment.NewLine); } pos += l.Length + 1; //incriment position //worker.ReportProgress(++i); } string[] res = {rtb.Rtf,results.Rtf,hits.ToString()}; e.Result = res; } // old non threaded search method public void OldSearch(string sTerm) { int hits = 0; // track number of hits int pos = 0; // track position in rtb int oldPos = richTextBox1.SelectionStart; //save current positin in rtb int oldLen = richTextBox1.SelectionLength; string lowerT = sTerm.ToLowerInvariant(); sTime = 0; System.Threading.Timer tmr = new System.Threading.Timer(new TimerCallback(TimerTask), null, 0, 100); if (sTerm.Length > 0) { //clear old search ReloadFile(); richTextBox4.Clear(); searchRes = new List<int>(); //open results pane label1.Text = "Searching for \"" + sTerm + "\"..."; splitContainer1.Panel2Collapsed = false; frmFind.Focus(); frmFind.ShowProgress(true); foreach (string l in richTextBox1.Lines) { string lowerl = l.ToLowerInvariant(); int n = lowerl.IndexOf(lowerT); if (n > -1) { while (n > -1) //if found sterm highlight instances { hits++; //incriment hits //hilight term richTextBox1.SelectionStart = pos + n; richTextBox1.SelectionLength = sTerm.Length; richTextBox1.SelectionBackColor = Color.Yellow; richTextBox1.SelectionColor = Color.Black; //find next n = lowerl.IndexOf(lowerT, n + sTerm.Length); } searchRes.Add(pos); richTextBox1.SelectionStart = pos; richTextBox1.SelectionLength = l.Length; richTextBox4.SelectedRtf = richTextBox1.SelectedRtf; richTextBox4.AppendText(Environment.NewLine); } pos += l.Length + 1; //incriment position } tmr.Dispose(); float time = (float)sTime / 10; label1.Text = "Search for \"" + sTerm + "\": Found " + hits + " instances in " + time + " seconds."; richTextBox4.SelectionStart = 0; richTextBox1.SelectionStart = oldPos; richTextBox1.SelectionLength = oldLen; richTextBox1.Focus(); frmFind.ShowProgress(false); } } 

NOTES:

  • I know that the RTB class has its own search method, but found that it is much slower than my own method.
  • I read a few topics regarding BGW performance, and most of them seem to use Invoke methods as a reason, but I don't use them.
  • I understand that using multiple threads will make it work slower, but did not expect such a big difference.
  • The problem is not ReportProgress , I commented on this line. The reason I do it this way, and not as a percentage, is the calculation to calculate the percentage that made a big contribution. This is actually faster.
  • This link, provided by another user, describes how I use my RTB in a stream without a GUI. This does not seem to be a problem, but will require more overhead, as this will cause the creation of a message queue. I am not sure if this will affect code performance inside my foreach loop. Any comments on this would be greatly appreciated.
+6
source share
3 answers

One thing that usually slows down Winforms is synchronizing with the UI thread. If ReportProgress does this (I don’t know, but I assume it is necessary), and you call it too often (say, 100-1000 times per second or more), it will slow everything down to a stop, due to various locking problems it will happen.

Try to remove any interaction between the user interface and the background thread that you have, and if that helps, restore the interaction, but let it happen much less often, for example, 1-100 times per second.

In addition, I'm not sure, but if you pass a link to a control object, it can still belong to the user interface thread, and each interaction with it from another thread can also cause synchronization problems (and interaction with the control of actual forms will lead to an exception).

0
source

Not sure ... but every time you call setter on SelectedRtf, a lot of things happen, including getting Unicode encoding, writing to the buffer, and then sending a lot of Windows messages.

So, firstly, if you can reverse engineer the algorithm to do as much as possible without accessing the RTF search box, and then release the selection, you are likely to improve performance.

As for why it is slower ... RTF boxes are created in the background thread. Maybe when they send messsages and there is no message loop to process them, there is a delay. Or maybe some sorting back to the correct SynchronizationContext occurs, which takes time. Not sure.

A profiler who looks at your own code and .NET Framework code should tell you.

 public string SelectedRtf { get { this.ForceHandleCreate(); return this.StreamOut(32770); } set { this.ForceHandleCreate(); if (value == null) value = ""; this.StreamIn(value, 32770); } } private void StreamIn(string str, int flags) { if (str.Length == 0) { if ((32768 & flags) != 0) { this.SendMessage(771, 0, 0); this.ProtectedError = false; } else this.SendMessage(12, 0, ""); } else { int length = str.IndexOf(char.MinValue); if (length != -1) str = str.Substring(0, length); byte[] buffer = (flags & 16) == 0 ? Encoding.Default.GetBytes(str) : Encoding.Unicode.GetBytes(str); this.editStream = (Stream) new MemoryStream(buffer.Length); this.editStream.Write(buffer, 0, buffer.Length); this.editStream.Position = 0L; this.StreamIn(this.editStream, flags); } } private void StreamIn(Stream data, int flags) { if ((flags & 32768) == 0) System.Windows.Forms.UnsafeNativeMethods.SendMessage(new HandleRef((object) this, this.Handle), 1079, 0, new System.Windows.Forms.NativeMethods.CHARRANGE()); try { this.editStream = data; if ((flags & 2) != 0) { long position = this.editStream.Position; byte[] numArray = new byte[RichTextBox.SZ_RTF_TAG.Length]; this.editStream.Read(numArray, (int) position, RichTextBox.SZ_RTF_TAG.Length); string @string = Encoding.Default.GetString(numArray); if (!RichTextBox.SZ_RTF_TAG.Equals(@string)) throw new ArgumentException(System.Windows.Forms.SR.GetString("InvalidFileFormat")); this.editStream.Position = position; } System.Windows.Forms.NativeMethods.EDITSTREAM editstream = new System.Windows.Forms.NativeMethods.EDITSTREAM(); int num1 = (flags & 16) == 0 ? 5 : 9; int num2 = (flags & 2) == 0 ? num1 | 16 : num1 | 64; editstream.dwCookie = (IntPtr) num2; editstream.pfnCallback = new System.Windows.Forms.NativeMethods.EditStreamCallback(this.EditStreamProc); this.SendMessage(1077, 0, int.MaxValue); if (IntPtr.Size == 8) { System.Windows.Forms.NativeMethods.EDITSTREAM64 editstreaM64 = this.ConvertToEDITSTREAM64(editstream); System.Windows.Forms.UnsafeNativeMethods.SendMessage(new HandleRef((object) this, this.Handle), 1097, flags, editstreaM64); editstream.dwError = this.GetErrorValue64(editstreaM64); } else System.Windows.Forms.UnsafeNativeMethods.SendMessage(new HandleRef((object) this, this.Handle), 1097, flags, editstream); this.UpdateMaxLength(); if (this.GetProtectedError()) return; if (editstream.dwError != 0) throw new InvalidOperationException(System.Windows.Forms.SR.GetString("LoadTextError")); this.SendMessage(185, -1, 0); this.SendMessage(186, 0, 0); } finally { this.editStream = (Stream) null; } } 
0
source

Does not fit in comments, so I will post a response.

I have not used WinForms for ages, but should WinForms not throw an error for accessing a user interface element from code other than the UI? I remember that I had to do a ton of this.Invoke , but maybe the background work handles things differently.

In any case, I assume that the main piece of extra time will be synchronized with the user interface stream to access the RichTextBox. Bring out a good old stopwatch and measure your code to find out where the botken is.

I wonder if it will be faster to split the text into pieces and use several threads - work with four cores;) - find all matches, and then go to the user interface thread at the end, scroll through all the matches and select the text.

It should also be possible to select only the text in the visible area of ​​the screen and when the user scrolls through the brighter text ...

0
source

Source: https://habr.com/ru/post/903535/


All Articles