Closed daltzctr closed 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
Hi and thanks for the report.
Can you please provide an example with this case?
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.
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.
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:
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).
@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.
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)?
@daltzctr I will add an example to the repo/site soon.
I think this is also fixed now with #1151, here is a chart that adds 50 points each millisecond, GC seems decent now!
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!
Epic!
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!
Just for the record, how many points does your series have?
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.
mm, that looks strange, I get a decent performance (on Avalonia), which is your platform?
MAUI/WinUI3.
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.
Update on every call would be nice to at least be able to trace the problem more deterministically.
After some further overnight testing I can produce these results
Thaaaaaaaaaaaaaaaaankkkkkkkkkkkkkkkk youuuuuuuuuuuuuuuuuuuuu :D
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.