beto-rodriguez / LiveCharts2

Simple, flexible, interactive & powerful charts, maps and gauges for .Net, LiveCharts2 can now practically run everywhere Maui, Uno Platform, Blazor-wasm, WPF, WinForms, Xamarin, Avalonia, WinUI, UWP.
https://livecharts.dev
MIT License
4.4k stars 576 forks source link

Inefficient performance due to GC pressure #1083

Closed daltzctr closed 1 year ago

daltzctr commented 1 year ago

In reviewing the code for another issue, I noticed a lot of object allocations that create heavy GC pressure (thus lowering performance at high rates/high quantities of data). Specifically, a ton of SolidColorPaint objects get created and then cleared.

I propose replacing the instantiation of SolidColorPaint with a ring-buffer implementation that will then feed it into the actual dataset that gets rendered on the chart. This will reduce/entirely eliminate GC pressure and in initial testing, greatly improve performance in real-time scenarios.

daltzctr commented 1 year ago

Expanding this. Real time performance of even a small amount of data (100pts/250ms) is poor and causes a ~20-50ms lockup of the UI due to GCs caused by Measure() and Invalidate

beto-rodriguez commented 1 year ago

Hi and thanks for the report.

Can you please provide an example with this case?

beto-rodriguez commented 1 year ago

After some research I was able to reproduce the issue, in my case I added and removed 1,000 points every 100ms and that caused the GC to run quite often.

Case 1 (code here)

I am using an object that implements IChartEntity, This interface is also implemented by all the default objects provided in the library (like ObservableValue or ObservablePoint), by default the Measurement class (in the example) holds a reference to the visual drawn in the UI, this is used internally to track and animate things in the library, that is the main reason that forces the GC to run that often, because when we are removing a point in the chart then the GC also needs to remove the visual in the UI.

gc1

Case 2 (code here)

In a case like this, we can re-use the same visual in the UI, a quick way of doing so, is to use a mapper and use the index of the element in the array to map the visual with the data, now we are just updating the old visual in the UI instead of delete/create a new one:

gc2

Conclusion

We can also help LiveCharts to improve things, this needs better docs, and I will consider this case when I build the examples/docs of the high-performance package, In general I am not disappointed with the default behavior of the library, because it allows us to create plots like this, that case is not possible using the Case 2 approach, then when we have cases where we need to change big amounts of data in short periods of time, we can help the library to manage that case.

I will close this for now because it seems that we can help the GC in special cases like this, I hope I made sense, but please feel free to reply or create a new issue if my alternative is not enough in your case (I will build an article about this when I create the high-performance tips).

daltzctr commented 1 year ago

@beto-rodriguez can you give a more complete implementation of case 2? I don't see any logic related to mappers or changing positions. Adding on to this, having the X axis represent useful data like (time) is useful, so an alternative method for reusing the same visual element is necessary.

daltzctr commented 1 year ago

Just wanted to give a bump on this. Manually applying a Labeler = (value) => {} somewhat works, but when there are multiple series, their values don't actually line up with the X axis.

Basically, how do we plot multiple rolling (updating from live data) series without incurring heavy GC penalties (which negatively effect application usage)?

beto-rodriguez commented 1 year ago

@daltzctr I will add an example to the repo/site soon.

beto-rodriguez commented 1 year ago

I think this is also fixed now with #1151, here is a chart that adds 50 points each millisecond, GC seems decent now!

v

The issues in #1151 were caused by tooltips and legends, the RelativePanel class was not disposing the background color (755f86c101a024d7f4f415811cab09be8655531d), also the legend was measured every time the chart was updated, legends use the RelativePanel class, now legends are not calculated always (036e10b77d033977dc4ccf5a133afa5cf20a15f1), it seems that now everything is smooth, thanks for the report!

daltzctr commented 1 year ago

Epic!

daltzctr commented 1 year ago

While this certainly improves the situation (nice!), the approach mentioned previously is still my preferred implementation.

Without reusing coordinate points, and with about ~5 line series (each with 50 point updates every 75ms) and a few seconds of updates the chart gets "behind" on renders. This causes a situation where the GC pressure increases until the application eventually deadlocks. Reusing the coordinate pretty much solves this situation but introduces a few behaviors that have to be worked around.

When reusing points, I have observed:

I have seen similar plot libraries implement an internal buffer (ring buffer) of points that the library will reuse. That was my suggested implementation in the original post in this thread. Reassigning the coordinate accomplishes a similar behavior, but skips lots of code that the library assumes (this is an assumption, not proven of mine).

To be clear, this project is absolutely amazing and you are an insane individual for maintaining a project whose scope is this large (and for free!). I, and I'm sure many others thank you for your work!

beto-rodriguez commented 1 year ago

Just for the record, how many points does your series have?

daltzctr commented 1 year ago

A maximum of 150 points per series. For context, I don't really expect amazing performance, just usable performance with up to ~6 series.

So my series has 6 * 150 points with old points being "removed" and "new ones" being added every 75ms.

beto-rodriguez commented 1 year ago

mm, that looks strange, I get a decent performance (on Avalonia), which is your platform?

daltzctr commented 1 year ago

MAUI/WinUI3.

beto-rodriguez commented 1 year ago

The delay issue could also be related to how the library works, this is just a hypothesis, I need to check this.

LiveCharts is not updated every time the data changes, it is updated once every interval (default is 50ms), this is controlled by the UpdaterTrottler property, reducing this property interval could inprove your case, there is also #1027, maybe we need a way to disable the throttler and just update on every call, but that would require more work from your side, since maybe you would require to remove the observable stuffs and just call a chart update every time you need it.

daltzctr commented 1 year ago

Update on every call would be nice to at least be able to trace the problem more deterministically.

daltzctr commented 1 year ago

After some further overnight testing I can produce these results

Thaaaaaaaaaaaaaaaaankkkkkkkkkkkkkkkk youuuuuuuuuuuuuuuuuuuuu :D