I have a WPF application that needs to process a lot of data and visualize it to the screen as an image. Of course if I do that from the UI thread, it will freeze so I offload it to the Thread Pool. Here when I launch it with debugger attached, it runs fine and no stutter. However, if I launch it without debugger attached, I don't understand why it stutter a lot.
Generally, programs launched without debugger attached should run faster right? so why in my application it just makes it worse? Setting configuration to either Debug or Release doesn't help, what's matter here is whether launching it with or without debugger.
Since the code is too complex to show it here and I have no idea on how to do minimal reproducible example code for my specific problem, here is a repository link to the project issue and you can see the GIF when launched with/without debugger, it is a public repo: https://github.com/binstarjs03/AerialOBJ/issues/2
My setup is I am on .NET 6.0.400, Visual Studio 2022 17.3.1, Windows 10 64bit
A little bit more context:
My UI thread offload its CPU intensive work to all of my CPU max thread, which is 12, and if you think the problem may be concurrency problem (such as contented lock etc.), I don't think that's the problem because it's a fire-and-forget, no locking involved at all. The only communication between thread is when the background worker finished its task, it tells the UI thread to update the image.
Another possible culprit may be the GC,. The program triggers the GC approximately 20 times per-second, but back to the main problem, it launch smoothly only with debugger attached and stutter without debugger.
Edit:
After investigated further, i figured out in the background worker threads method, i disabled one certain method that was exactly what the background worker thead supposed to execute, it eliminates the stuttering. At the same time i figured out that method was the culprit behind GC pressure.
Now we can narrow down the scope of the problem, it is a GC pressure problem. As i mentioned above, the GC kicks in about 20 times per second if the background worker threads invoke it (multiplies that by CPU cores count). What this particular method does, it deserializes array of bytes into primites, including string. I have optimized the parser by creating Span<byte>
instead of new byte[]
but despite that, i don't know where exactly my method generate a lot of heaps. And of course the deserialized data stored somewhere, e.g list, so the GC can't reclaim it, therefore the precise location of where the real culprit of GC pressure remains unknown.
Example what i mean by Span
instead of new byte[]
(deserializing array of bytes into length-prefixed string):
public string ReadStringLengthPrefixed(bool isBigEndian)
{
ushort length = ReadUShort(isBigEndian); // here endiannes matter
Span<byte> bytes = length < 1024 ? stackalloc byte[length] : new byte[length];
if (Read(bytes) != length)
throw new EndOfStreamException();
return Encoding.UTF8.GetString(bytes);
}
Relevant file and lines for the deserialization of the method that triggers GC a lot: https://github.com/binstarjs03/AerialOBJ/blob/main/src/binstarjs03.AerialOBJ.Core/BinaryReaderEndian.cs https://github.com/binstarjs03/AerialOBJ/blob/6a5bb01b09b45bd7b366e73022000c39b375989c/src/binstarjs03.AerialOBJ.Core/MinecraftWorld/Region.cs#L193
CodePudding user response:
Traditionally with high UI refresh cycles in applications we look to the game design for solutions.
The general solution in the gaming world is to use a timer to execute rendering logic at regular intervals, instead of while(true)
. Tune the frequency of the loop so that there is enough time for the UI to render the previous request before the next one starts.
- For real-time applications this would often look resemble two loops, one processing as fast as it needs to and a separate loop that runs a t a lower frequency to update the UI.
There is a practical limit to compute frequencies, when it takes longer to render the result than it does to compute the value you get race conditions that manifest visually like your example.
A while(true)
style loop is incredibly vulnerable to this. in WPF your visual updates are being queued, if you overfill the buffer quicker than the the results can be rendered the renderer ends up aborting some frames in the buffer to try and catch up.
- If you look carefully in the production version the canvas is more responsive and is loading more chunks than the debug version, it is actually processing faster.
With higher frequency processing you will see more GC activity, but that does not mean that GC is causing the issues, it is just doing it's thing to keep everything running smoothly. If you weren't expecting a high frequency process, then you might be concerned about GC, but otherwise this is expected behaviour.
- When running in debug, your compute is effectively throttled and results in taking longer to compute than in release mode, so it is harder to observe this phenomenon.
You might be tempted to add a deliberate delay in your processing loop instead of a timer... do not do that, it is an anti-pattern that can often result in additional blocking of the thread instead of releasing it to get the work done.
- Please use a timer, this is a classic timer implementation scenario.