First I want to thank Matthew Groves for hosting the 1st known C# Advent (English). I was honored to be able to grab the spot for Friday, December 22, 2017, which, happily, is the start of my Christmas holiday week, as well.
The crux of this post is that most visible performance issues in a Windows application come from the presentation layer. Specifically, anything that puts load or wait states on the main “UI Thread” will make the app/application appear to hang or become unresponsive for periods of time. This post talks about strategies for getting load off the UI as much as possible, beyond the async/await mechanism in C#. Most such load can be unloaded to a worker thread fairly easily. Other tasks can be awaited together. In some cases, a UI component is involved, and it becomes necessary to manage load that, for that reason, reason MUST stay on the UI thread.
I remember when I was a kid hearing of projects for stock traders that handled hundreds of data update events every second and being totally intimidated by the thought of it. I knew I’d “come of age” in technology when, in 2017, I worked with a focused team (known as “Blockheads”) to build such an app. This latest generation “stock blotter” ran stable, without memory leakage, and with no apparent lag at tens of Gigabytes per second! These general ideas stem back to the project I worked on in 2016-2017 with BlueMetal for Fidelity Investments’ Equity Trading team, called Artis OMT. Artis OMT has been on Fidelity’s Equity Trading floor for over a year now, and will soon reach a year of full deployment. While Artis OMT was WPF, this post looks at similar performance ideas in a similar but different platform: Windows 10 UWP (store apps).
Artis OMT didn’t start out able to handle 90Gigabytes of incoming data. We had to use JetBrains tools to identify code that was bogging down or hanging the main UI thread. That analysis, alone, is perhaps the subject of a different post, or more, some day.
When folks start thinking about UI Thread execution, the first thing most think of is Dispatcher.BeginInvoke(). This method is how you add workload to the UI thread. I’m trying to talk about how to UNLOAD the UI thread, and/or manage your load so that the user won’t observe UI freezes or lockups.
Here, however, are a few relatively easy ways to really make use of the extra cores in your CPU, and make your apps appear to perform much better:
Task.Run(() => { … });
|
Classic depiction of processes running in sequence vs in parallel |
The title of this says it all, really. Push a workload off the current thread. Use whenever you have long running processes that you don’t have to touch UI controls from. If you have timing dependencies, you can manage them with Task.When, Task,Wait, or even better, Task.ContinueWith(). Examples below cover this a little more.
Batch remote service calls using Tasks and WhenAll()
Service calls are low hanging fruit. So often I see code that makes calls in series, waiting on the results of one before making the next call, even though the two calls have no dependencies on each other… it’s just so much easier to write the sequence case that folks let it hang. await Task.WhenAll(…) is not as syntactically sweet, but still MUCH sweeter than having to set up an aggregate event.
///
/// Does one request at a time, holding up the entire process
/// at each step until it completes. Simpler code but….
/// Total time spent is the sum of all tasks’ time.
///
public async void GetContentinSequence(Session session)
{
var dbContent = awaitGetDatabaseContent(session);
var webContent = await GetWebContent(session);
var userProfile = await GetUserProfile(session);
var userContext = await GetUserContext(session);
}
///
/// Executes all requests simultaneously, letting the default task dispatcher do its thing.
/// total time spent is no more than the longest running individual task, all other things being equal.
///
public async void GetContentinParallel(Session session)
{
var contextTask = GetDatabaseContent(session);
var webContentTask = GetWebContent(session);
var userProfileTask = GetUserProfile(session);
var userContextTask = GetUserProfile(session);
var stuff = new Task[] { contextTask, webContentTask, userProfileTask, userContextTask };
await Task.WhenAll(stuff);
var dbContent = contextTask.Result;
var webContent = webContentTask.Result;
var userProfile = userProfileTask.Result;
var userContext = userContextTask.Result;
}
Here’s an example that makes this more clear:
var start = DateTimeOffset.Now;
var task1 = Task.Run(async () => { awaitTask.Delay(1000); });
var task2 = Task.Run(async () => { awaitTask.Delay(1500); }); //1.5 seconds
var task3 = Task.Run(async () => { await Task.Delay(1000); });
var task4 = Task.Run(async () => { awaitTask.Delay(1000); });
var tasks = new Task[] { task1, task2, task3, task4 };
Task.WhenAll(tasks).ContinueWith(t => { Debug.WriteLine(DateTimeOffset.Now – start); });
outputs something like:
00:00:01.5623681
As always, there’s some overhead with task switching. You’ll notice that the time was just a few ticks longer than 1.5 seconds.
What if you can’t unload the UI thread? what if your long running process must interact with controls like a huge grid that needs to calculate an aggregation of a data set that lives in it?
Here’s an option…
DoEvents() erhhh… ummm… await Task.Delay(…)
I once scrubbed references to Visual Basic from my CV and landed a job that had scrubbed VB from the job description. I didn’t want to work for a company that would hire a “VB-Weenie” and they didn’t want to hire a “VB-Weenie”, either… but there was VB6 work to do.
One thing that VB6 had going for it was a concept called DoEvents(). It enabled you to give up processing the current method to allow any pending events to execute. It would then return to finish the calling method.
In C#, the closest equivalent, nowadays, is “await Task.Yield()” or await.Task.Delay(…).
Most folks talk about using “await Task.Yield()” at the start of an awaitable method to make sure the whole method runs asynchronously. There’s some sense to that. More importantly, one can interrupt long running processes that must run on the UI in order to allow the UI to respond to user inputs. In testing, I’ve seen that Task.Yield() often doesn’t allow enough room for redraws of the UI. Likewise, setting a Task.Delay of a 1 tick timespan isn’t enough, either. 1 millisecond delay, however, does seem to suffice in my basic testing.
private async void LongRunningAggregatorOnUIThread(object sender, object e)
{
await Task.Yield();
timer.Stop();
var timeoutRate = TimeSpan.FromMilliseconds(100);
var timeout = DateTimeOffset.Now.Add(timeoutRate);
var value = 0L;
while (true)
{
value++;
if (DateTimeOffset.Now >= timeout)
{
textbox.Text = value.ToString();
await Task.Delay(1);
timeout = DateTimeOffset.Now.Add(timeoutRate);
}
};
}
As always, use this very carefully. This has overhead of its own, as well, that can cause performance issues…. including potential deadlocks.