Of course, even if I have tried everything to minimize the negative effects on the the performance of the mock data generator, GUI is GUI, it will drag it down anyway, but by how much? I decide to dig a little further.
Thanks to the nature of MVVM, it is easy to swap out the GUI V-VM with ... a CLI! Yes that's the capability MVVM is supposed to have, V-VM are dumb layers and replace them with another UI, business as usual!
I wrapped up Model, Common and BusinessLogic (refer to the source code I published on GitHub, link is at the bottom of part II) into a DLL, and wrote a console program:
using HighThroughputDataGrid.BusinessLogic;
using HighThroughputDataGrid.Common;
using System;
namespace HighOutputDataCli
{
public class DataObserver : IObserver<double>
{
public PerformanceMonitor Performance { get; set; }
public void OnCompleted() { }
public void OnError(Exception error) { }
public void OnNext(double value)
{
if(Performance != null)
{
Performance.Add(value);
Console.WriteLine("Max: {0:0.00}, Min: {1:0.00}, Avg: {2:0.00}", Performance.Maximum, Performance.Minimum, Performance.Average);
}
}
}
public class Program
{
static void Main(string[] args)
{
HighOutputDataSource source = new HighOutputDataSource
{
FreshRateObserver = new Observable<double>(),
NumberOfRandomGenerators = 200
};
DataObserver observer = new DataObserver { Performance = new PerformanceMonitor() };
source.FreshRateObserver.Subscribe(observer);
source.Start();
Console.ReadKey();
}
}
}
That's it, gives us the highest number the mock data generator could do (on my m5 tablet):
Max: 1079.91, Min: 968.99, Avg: 1032.01
Max: 1079.91, Min: 968.99, Avg: 1033.67
Max: 1079.91, Min: 968.99, Avg: 1031.02
Max: 1079.91, Min: 948.77, Avg: 1027.59
Max: 1079.91, Min: 948.77, Avg: 1025.02
Max: 1079.91, Min: 948.77, Avg: 1022.16
Max: 1079.91, Min: 948.77, Avg: 1019.44
Max: 1079.91, Min: 948.77, Avg: 1018.53
Max: 1079.91, Min: 948.77, Avg: 1016.13
Max: 1079.91, Min: 948.77, Avg: 1014.00
Max: 1079.91, Min: 948.77, Avg: 1014.55
It's about 8% - 15% higher than our super efficient WPF DataGrid application could possibly do, that shows the penalty of the modern fancy UI costs.
Wednesday, July 6, 2016
Wednesday, June 29, 2016
A High Performance WPF DataGrid Part II
The Suppressible is straightforward:
public class Suppressible : Notifier
{
private bool _isSuppressed = true;
public bool IsSuppressed
{
get { return _isSuppressed; }
set
{
if (_isSuppressed != value)
{
_isSuppressed = value;
RaisePropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName]string propertyName = "")
{
if(!_isSuppressed)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
I don't need to change anything else in Stock but the base class. Now is the fun part:
public class HighThroughputBehavior : Behavior<ItemsControl>
{
protected override void OnAttached() { AssociatedObject.Loaded += OnItemsControlLoaded; }
protected override void OnDetaching() { AssociatedObject.Loaded -= OnItemsControlLoaded; }
private void OnItemsControlLoaded(object sender, System.Windows.RoutedEventArgs args)
{
ScrollViewer scrollViewer = ((ItemsControl)AssociatedObject).FindVisualChild<ScrollViewer>();
OnScrollChanged(scrollViewer);
scrollViewer.ScrollChanged += OnScrollChanged;
}
private void OnScrollChanged(object sender, RoutedEvent e = null) { ... }
}
public class Suppressible : Notifier
{
private bool _isSuppressed = true;
public bool IsSuppressed
{
get { return _isSuppressed; }
set
{
if (_isSuppressed != value)
{
_isSuppressed = value;
RaisePropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName]string propertyName = "")
{
if(!_isSuppressed)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
I don't need to change anything else in Stock but the base class. Now is the fun part:
public class HighThroughputBehavior : Behavior<ItemsControl>
{
protected override void OnAttached() { AssociatedObject.Loaded += OnItemsControlLoaded; }
protected override void OnDetaching() { AssociatedObject.Loaded -= OnItemsControlLoaded; }
private void OnItemsControlLoaded(object sender, System.Windows.RoutedEventArgs args)
{
ScrollViewer scrollViewer = ((ItemsControl)AssociatedObject).FindVisualChild<ScrollViewer>();
OnScrollChanged(scrollViewer);
scrollViewer.ScrollChanged += OnScrollChanged;
}
private void OnScrollChanged(object sender, RoutedEvent e = null) { ... }
}
When OnScrollChanged is invoked, how do I find and iterate through the items going off the screen and on the screen? ScrollViewer has a pair of properties seems to be handy:
ScrollViewer.VerticalOffset, it contains the index of the first Stock item in the view port;
ScrollViewer.ViewPortHeight, it contains the number of the Stock items in the view port, can I just say:
foreach (int i in Enumerable.Range(scrollViewer.VerticalOffset, scrollViewer.ViewPortHeight) stocks[i].IsSuppressed = false;
and call it done? I ran into an issue soon after that. The value ScrollViewer.ViewPortHeight contains only the items are entirely in the view port. If the bottom item only shows half of it, it may not be included so in this implementation the data won't get refreshed on the screen! (why did I say "may not be included"? because sometimes it is included depends on how you scrolled).
I can choose to be lazy and just update whatever included in ViewPortHeight + 1, but I hate workaround like this, soon or later it will become annoying when adding new features. So, I go back to the old school stuffs from the Win32 time: do the geometry and calculate it by your self. ScrollViewer doesn't have the numbers I need directly, I have to go with VirtualizingStackPanel:
private IList<Stock> _onScreenItems = new List<Stock>();
private void OnScrollChanged(object sender, RoutedEvent e = null)
{
VirtualizingStackPanel panel = ((ScrollViewer)sender).FindVisualChild<VirtualizingStackPanel>();
Rect layout = LayoutInformation.GetLayoutSlot(panel);
IEnumerable<ListBoxItem> children = panel.Children.Cast<ListBoxItem>();
var onScreenItems = children
.Select(item => new { bound = LayoutInformation.GetLayoutSlot(item), item })
.Where(x => (x.bound.Bottom < layout.Bottom && x.bound.Bottom > layout.Top)
|| (x.bound.Top > 0 && x.bound.Top < layout.Bottom))
.Select(x => x.item.DataContext).Cast<Suppressible>().ToList();
foreach (var suppressible in _onScreenItems.Except(onScreenItems))
suppressible.IsSuppressed = true;
foreach (var suppressible in onScreenItems)
suppressible.IsSuppressed = false;
_onScreenItems = onScreenItems;
}
I calculate how many times the mock data generator refreshes the 3226 stocks in a second and bound it to the screen, so I can clearly see how each step of the improvements did, the DataGrid (I called a ItemsControl a DataGrid? yes it looks like a DataGrid and scrolls like a DataGrid so it is a DataGrid) holds up really well against the huge amount of data pumping in.
I won't stop here. Refresh the data of each stock hundreds of times in a second on the UI doesn' make sense, even da Vinci won't be able to track them (does a man can track thousands items in a ListBox? the use case is not in discussion at this moment), and it obviously hurts the performance, so I need to reduce the refresh rate of the data on the screen - throttling the data source is not an option because Stock objects are the best place to store the latest data, even if the data is not shown on the screen.
I added a QueuePropertyChanged(string propertyName) method alongside with the old RaisePropertyChanged in Suppressible, all of the high volume properties like Bid, Ask, Last, Change, Percent, Volume will redirect their notification calls this way:
private HashSet<string> _eventCache = new HashSet<string>();
([CallerMemberName]string propertyName = "")
{
if (!_isSuppressed)
{
lock (_eventCache)
{
_eventCache.Add(propertyName);
}
}
}
public void OnTimerTick()
{
if (!_isSuppressed)
{
lock (_eventCache)
{
foreach(string propertyName in _eventCache)
RaisePropertyChanged(propertyName);
_eventCache.Clear();
}
}
}
In the MainViewModel, I created a DispatchTimer, and refresh the Suppressibles every second, the performance of final DataGrid is way above my expectation, the mock data engine can pump in the data for 3226 stocks over 1000 times per second on Dell tablet running m5-6Y54@1.51 GHz processor with 8GB memory!
PERFORMANCE COMPARISON
m5-6Y54@1.51 GHz | |||
High
|
Average
|
Improvement
|
|
Refresh Reduction
|
1018
|
740 | 1805% |
On Screen Detection
|
685
|
470 | 1146% |
UI Virtualization
|
657
|
450 | 1098% |
Out of Box
|
48*
|
41 | 100% |
* The data refresh
of the entire collection per second Source code available on GitHub: https://github.com/SX-GitHub/HighThroughputDataGrid |
Previous:
To be continued:
To be continued:
Tuesday, June 28, 2016
A High Performance WPF DataGrid Part I
Disclamation: The research shown here were done on my personal time and equipment, and it is not related to the work I'm doing at J.P. Morgan Chase.
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:
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:
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
Subscribe to:
Posts (Atom)