It's June 2016, WPF is still alive and has become the choice of tool to build full-blown applications on Windows (aka the line of business applications).
I recently come across an interesting topic: if the UI based on WPF can handle high volume and high speed data, e.g. the data of 5,000 stocks refreshing dozens of times per second. I learned how to optimize the UI pixel by pixel from the pre-MFC time, but how well would WPF perform if I try to squeeze it as hard as I can? I built a stock market mocking application as the test bed.
If I have enough time I always choose my building blocks from the basic. When it comes to DataGrid, I would pick ItemsControl first. If I need certain feature it doesn't support, I go one step up to ListBox, then ListVIew, and then DataGrid. Third party controls are always my last choice - they are built for flexibility not for performance. So as always, I started my experiments with ItemsControl.
By default, ItemsControl, including it's derivations ListBox and ListView uses StackPanel as the ItemsPanel, and they usually work fine. However, when the data source has hundreds even thousands of items, they are very laggy because they are busy to generate the FrameworkElements for each of the item, the UI will become unresponsive even before I pump data to them. Once we start pouring data to them, the Stock objects in the ObservableCollection raise thousands PropertyChanged events a second, the controls will immediately render unusable.
Microsoft included UI virtualization features in WPF but it has to be turned on. After I swap out the StackPanel with VirtualizingStackPanel from the ItemsControl, it will only generate the FrameworkElements for those items visible on the screen plus a few more spares:
<ItemsControl ItemsSource="{Binding Stocks}"
VirtualizingStackPanel.IsVirtualizing="true"
VirtualizingStackPanel.VirtualizationMode="Standard">
<ItemsControl.ItemsPanel>
</ItemsControl>VirtualizingStackPanel.VirtualizationMode="Standard">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<VirtualizingStackPanel Orientation="Vertical" IsItemsHost="True"/>
</ItemsPanelTemplate>The Stock objects still raise PropertyChanged events, but only the instantiated ListBoxItems will receive them through the data bindings. The UI will become highly usable by just turning on UI virtualization.
How many ListBoxItems will be created? It depends on the size of the ItemsControl on the screen and how you have scrolled the items. In a ItemsControl with 30 rows shown on the screen, it usually holds no more than 200 items after you scroll it up and down, and will drop down to 50 or so as soon as it got a chance. In case you are dragging the scrollbar like crazy, I have never seen it holds more than 500 items at the peak, and will immediately drop to a much lower number of items once you stopped.
To test the effectiveness of different tricks of tuning the performance, I created a mocked data generator. It uses the ThreadPool and Randoms to generate about 10 different numbers for each of the 3226 stocks I downloaded from NYSE. The algorithm is so efficient it will immediately grab any computing resources the UI leftover.
DateTime marketOpen = DateTime.Now;
Random[] randoms = new Random[200];
foreach (int i in Enumerable.Range(0, 200))
randoms[i] = new Random(Guid.NewGuid().GetHashCode());
CountdownEvent countdown = new CountdownEvent(Stocks.Count);
while (true)
{
TimeSpan sinceOpen = DateTime.Now - marketOpen;
double tick = sinceOpen.TotalSeconds / 28800;
for (int i = 0; i < Stocks.Count; i++)
{
ThreadPool.QueueUserWorkItem((o) =>
{
int index = (int)o;
Stock stock = Stocks[index];
stock.Volumn = (int)(tick * stock.LastVolumn);
Random random = randoms[(sinceOpen.Milliseconds + index) % 200];
double factor = random.NextDouble();
if (stock.Volumn > 0)
{
if (stock.Last < 0.00001)
stock.Last = (factor + 9999.5) / 10000.0 * stock.Close;
else stock.Last *= (factor + 9999.5) / 10000.0;
double high = stock.High;
double low = stock.Low;
if (high < 0.00001)
high = stock.Last;
if (low < 0.00001)
low = stock.Last;
stock.High = Math.Max(stock.Last, high);
stock.Low = Math.Min(stock.Last, low);
stock.Bid = stock.Last * (factor + 99.0) / 100.0;
stock.Ask = stock.Last * (factor + 100.0) / 100.0;
stock.Change = stock.Last - stock.Close;
stock.ChangeInPercentage = stock.Change / stock.Close;
}
countdown.Signal();
}, i);
}
countdown.Wait();
countdown.Reset();
}
I created an array of 200 Randoms. I found anything less than that the Randoms will get jammed and start producing 0s, which causes the mocked stock market crash down.
To further improve the performance, I need to eliminate the PropertyChanged events send to anything not on the screen:
To achieve this, we need to find a way to detect which ListBoxItem is on the screen precisely, and notify the Stock objects don't have the visual represents on the screen to suppress the events. The most common approach to do this in WPF is to create an Attached Behavior and tap into the ItemsControl, to watch the actual objects in the cache of the VirtualizingStackPanel. Because I need to preserve the cache before ScrollChanged occurs, in order to find out who was removed from the cache and reset them, I choose to implement Blend Behavior so that I can have the instance properties as the storage. I also need to implement a base class for the Stock objects that can track their status and suppress the events, so I created these 2 classes:
- HighThroughputBehavior
- Suppressible
No comments:
Post a Comment