DispatcherTimer and UI refresh limits in C# silverlight
Asked Answered
J

2

9

Again I apologize for a question that might be simple to all of you. I have a limited understanding of what goes behind the scenes in Silverlight.

I have a charting app (Visiblox) that I use as a rolling scope updated every 20ms, adding and removing a point. In pseudocode:

List<Point> datapoints= new List<Point>();
Series series = new Series(datapoints);
void timer_tick(){
  datapoints.Add(new Point);
  datapoints.RemoveAt(0);
  // no need to refresh chart, it does refresh automatically
}

When running 6 series in this charting tool, it started to show a bit sluggish. Changing the tick to 10ms made no difference whatsoever, chart was updated at the same speed, so it seems that 20ms is the speed limit (UI or chart?).

I tried with CompositionTarget.Rendering and got the same results: below 20ms there was no difference in speed.

Then I accidentally enabled both and speed doubled. So I tested with multiple threads (2, 3, 4) and speed doubled, tripled and quadrupled. This has no locks yet, as I don't even know what process I need to generate a lock on, but got no data corruption nor memory leaks.

The question I have is why a sluggish chart at 20ms can not run at 10ms but is ridiculously fast when multithreaded? Is the UI refresh process being run faster? Is the chart computation doubled? Or is there a limit to how fast a single DispatcherTimer can be executed?

Thanks!


Edit: I have a background of embedded coding, so when I think of threads and timings, I immediately think of toggling a pin in hardware and hook up a scope to measure process lengths. I am new to threads in C# and there are no pins to hook up scopes. Is there a way to see thread timings graphically?

Justinajustine answered 12/1, 2011 at 1:25 Comment(0)
E
5

The key here I think is to realise that Silverlight renders at a maximum frame rate of 60fps by default (customisable through your MaxFrameRate property). That means that the DispatcherTimer ticks will fire at most 60 times per second. Additionally, all the rendering work happens on the UI thread as well so the DispatcherTimer fires at the rate that the drawing is happening at best, as pointed out by the previous poster.

The result of what you're doing by adding three timers is just to fire the "add data" method 3 times per event loop rather than once, so it will look like your charts are going much faster but in fact the frame rate is roughly the same. You could get the same effect with a single DispatcherTimer and just add 3 times as much data on each Tick. You can verify this by hooking into the CompositionTarget.Rendering event and counting the frame rate there in parallel.

The ObservableCollection point made previously is a good one but in Visiblox there is a bit of magic to try and mitigate the effects of that so if you're adding data at a very fast rate the chart updates will be batched up at the rate of the render loop and unnecessary re-renders will be dropped.

Also regarding your point about being tied to the ObservableCollection implementation of IDataSeries, you are entirely free to implement the IDataSeries interface yourself, for example by backing it with a simple List. Just be aware that obviously if you do that the chart will no longer automatically update when data changes. You can force a chart update by calling Chart.Invalidate() or by changing a manually set axis range.

Ermelindaermengarde answered 14/1, 2011 at 9:46 Comment(1)
You are right. It is an illusion. At slow speeds the 'rolling' of the chart is easier to track with your eyes and the sluggishness is easily detected, but updating more than one point at a time , the sluggishness is hard to see. Thanks!Justinajustine
P
8

A DispatcherTimer, which fires its Tick event on the UI thread, is what's considered a low-resolution or low-accuracy timer because its Interval effectively means "tick no sooner than x since the last tick". If the UI thread is busy doing anything (processing input, refreshing the chart, etc.) then it will delay the timer's events. Furthermore, having a bunch of DispatcherTimer's ticking away on the UI thread at very low intervals will also slow down the responsiveness of your application because while the Tick event is being raised, the application can't respond to input.

So as you noted, in order to process data frequently, you should move to a background thread. But there are caveats. The fact that you aren't currently observing corruption or other bugs could be purely coincidental. If the list is being modified on a background thread at the same time the foreground thread is trying to read from it, you will eventually crash (if you're lucky) or see corrupt data.

In your example, you have a comment that says "no need to refresh chart, it does refresh automatically." This makes me wonder how does the chart know that you have changed the datapoints collection? List<T> does not raise events when it is modified. If you were using an ObservableCollection<T> I would point out that each time you remove/add a point you are potentially refreshing the chart, which could be slowing things down.

But if you are in fact using List<T> then there must be something else (perhaps another timer?) that is refreshing the chart. Maybe the chart control itself has a built-in auto-refresh mechanism?

In any event, the problem is a little bit tricky but not completely new. There are ways that you could maintain a collection on a background thread and bind to it from the UI thread. But the faster your UI refreshes, the more likely you'll be waiting for a background thread to release a lock.

One way to minimize this would be to use a LinkedList<T> instead of List<T>. Adding to the end of a LinkedList is O(1), so is removing an item. A List<T> needs to shift everything down by one when you remove an item from the beginning. By using LinkedList you can lock on it in the background thread(s) and you'll minimize the amount of time that you're holding the lock. On the UI thread you would also need to obtain the same lock and either copy the list out to an array or refresh the chart while the lock is held.

Another possible solution would be to buffer "chunks" of points on the background thread and post a batch of them to the UI thread with Dispatcher.BeginInvoke, where you could then safely update a collection.

Preterite answered 12/1, 2011 at 1:59 Comment(6)
Thanks for the great insight. To answer a few of your concerns, I am not exactly using a List<>, but an implementation of a collection that has INotifyCollectionChanged with it. Since this is a third party app, there is not much I can do about it. My first approach at this was exactly as you suggested, I was rendering a bitmap in the background while I was scrolling a bitmap in the foreground. But due to time constraints and quality of my 'home made' charting tool, I opted for a 3rd party one. So LinkedList and others are out, I need to use their specific implementation...Justinajustine
(continued, ran out of chars) Another thing, I just realized that System.Windows.Threading creates threads that run in the same thread as the UI, otherwise it would be not possible to update the UI. Right? So the fact that I was thinking I am multithreading should be incorrect. What is going on then? Does silverlight assign time slices to share between UI and DispatchTimer and the fact that I created several of them increases the priority of the timer tasks?Justinajustine
(continued III) This is also why I think I am not seeing crashes or bugs, because I am actually not running multiple threads but just one after another. My application is a simulation tool that processes thousands of matrix calculations per timer period, resulting in millions of calculations per second. I should have crashed by now. Please correct me if I am wrong. These are all assumptions.Justinajustine
The Thread class does not create "threads that run in the same thread as the UI". That doesn't even make sense - there is just one UI thread; any new threads you create are new. If accessing UI from another thread works, it is so because whetver you are accessing does appropriate cross-thread marshaling for the call, so the UI is still actually updated on the UI thread.Pedaiah
Furthermore, data binding is "slow" by design - most controls load data only when the application is not busy. Even if you are doing lots of data processing in a background thread, parts of the UI might not be updated until the background thread is finished with its work. ItemsControl will, for example, not update itself if you are constantly adding items in a background thread.Pedaiah
I don't have an answer/solution to you, unfortunately, but I just thought I'd clear up some misconceptions. Perhaps wjbeau's answer is useful to you.Pedaiah
E
5

The key here I think is to realise that Silverlight renders at a maximum frame rate of 60fps by default (customisable through your MaxFrameRate property). That means that the DispatcherTimer ticks will fire at most 60 times per second. Additionally, all the rendering work happens on the UI thread as well so the DispatcherTimer fires at the rate that the drawing is happening at best, as pointed out by the previous poster.

The result of what you're doing by adding three timers is just to fire the "add data" method 3 times per event loop rather than once, so it will look like your charts are going much faster but in fact the frame rate is roughly the same. You could get the same effect with a single DispatcherTimer and just add 3 times as much data on each Tick. You can verify this by hooking into the CompositionTarget.Rendering event and counting the frame rate there in parallel.

The ObservableCollection point made previously is a good one but in Visiblox there is a bit of magic to try and mitigate the effects of that so if you're adding data at a very fast rate the chart updates will be batched up at the rate of the render loop and unnecessary re-renders will be dropped.

Also regarding your point about being tied to the ObservableCollection implementation of IDataSeries, you are entirely free to implement the IDataSeries interface yourself, for example by backing it with a simple List. Just be aware that obviously if you do that the chart will no longer automatically update when data changes. You can force a chart update by calling Chart.Invalidate() or by changing a manually set axis range.

Ermelindaermengarde answered 14/1, 2011 at 9:46 Comment(1)
You are right. It is an illusion. At slow speeds the 'rolling' of the chart is easier to track with your eyes and the sluggishness is easily detected, but updating more than one point at a time , the sluggishness is hard to see. Thanks!Justinajustine

© 2022 - 2024 — McMap. All rights reserved.