FlowFuse / node-red-dashboard

https://dashboard.flowfuse.com
Apache License 2.0
181 stars 45 forks source link

Performance issues with charts #676

Open postlund opened 5 months ago

postlund commented 5 months ago

Current Behavior

I'm trying to plot a single integer value from an MQTT node in a chart with these settings:

Plotting works, but it is unbearably slow/intense on the rendering engine. I added a delay node to rate limit incoming data to 5 points/seconds (would prefer to go up to at least 10) but I'm still almost hitting 100% CPU usage. Rate limiting is set to drop intermediate messages.

Expected Behavior

CPU usage should not be that high. To me it looks like adding new points and adjusting time labels take an awful lot of time. Rendering looks to be pretty ok:

slow_render

Steps To Reproduce

A version of my flow:

[{"id":"a0c4ab0bbd5622e1","type":"mqtt in","z":"c2e9349180cc114d","name":"Incoming value","topic":"signals/my_value","qos":"2","datatype":"auto-detect","broker":"a32ce2ad7015ac8d","nl":false,"rap":true,"rh":0,"inputs":0,"x":320,"y":580,"wires":[["d6196e04e9c97295"]]},{"id":"d6196e04e9c97295","type":"delay","z":"c2e9349180cc114d","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"milliseconds","rate":"5","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"allowrate":false,"outputs":1,"x":600,"y":580,"wires":[["dac43467a8dc873a"]]},{"id":"dac43467a8dc873a","type":"ui-chart","z":"c2e9349180cc114d","group":"dddb49f16fb95d13","name":"","label":"Value chart","order":3,"chartType":"line","category":"topic","categoryType":"msg","xAxisProperty":"","xAxisPropertyType":"msg","xAxisType":"time","yAxisProperty":"","ymin":"-180","ymax":"180","action":"append","pointShape":"false","pointRadius":4,"showLegend":true,"removeOlder":"15","removeOlderUnit":"1","removeOlderPoints":"200","colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"width":"27","height":8,"className":"","x":830,"y":580,"wires":[[]]},{"id":"a32ce2ad7015ac8d","type":"mqtt-broker","name":"mqtt","broker":"mqtt","port":"1883","clientid":"nodered","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":false,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"dddb49f16fb95d13","type":"ui-group","name":"Information","page":"341daee243fdb851","width":"27","height":"1","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"341daee243fdb851","type":"ui-page","name":"Sensors","ui":"ea6622fbd539b85d","path":"/sensors","icon":"monitor","layout":"grid","theme":"67f28c895c273935","order":4,"className":"","visible":"true","disabled":"false"},{"id":"ea6622fbd539b85d","type":"ui-base","name":"Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"icon"},{"id":"67f28c895c273935","type":"ui-theme","name":"Autoliv","colors":{"surface":"#141991","primary":"#00427a","bgPage":"#9d9d9d","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

Environment

I'm running Node-RED in a container with an official image (https://hub.docker.com/layers/nodered/node-red/latest/images/sha256-867a225c65ed6364e7e88a788b9ac54711544cd1af94c488f7954f880104d61a?context=explore),

Have you provided an initial effort estimate for this issue?

I am not a FlowFuse team member

joepavitt commented 5 months ago

Thanks for raising this @postlund - will investigate, have definitely seen higher throughput going into our charts than this, without issue, so will need to dig deeper as to what's going on here.

postlund commented 5 months ago

Thank you @joepavitt! Just let me know if you need any additional profiling data or want me to try something.

postlund commented 5 months ago

Can I support this in any way? Or would it be possible to integrate an external Vue component as a temporary workaround somehow? I don't want to be pushy in any way, just need to find another way forward as my client is waiting for a fix and I need to know how to prioritize my work.

joepavitt commented 5 months ago

Other than diving into source and investigating the specifics, not at the moment @postlund. We've a big backlog of work at the moment, and I'm only on this 2 days a week, so any help is apprecated.

I can try and look this week, but I can't guarantee it.

postlund commented 5 months ago

That sounds good @joepavitt. My initial conclusion is that it mainly has to do with date handling. A lot of time is spent just dealing with calculating time for some reason. It's quite apparent when diving deeper, like in this chart:

timecalc

I tried to changing the chart to linear instead of time and insert x by just appending current second and millisecond as string (to get a unique value for x for each value) and that is order or magnitudes faster (or at least 15-20x). This is of course not ideal as what I am plotting is real time date (e.g. steering wheel angles) which only makes sense to look at with a time based scale. But I can use that as a workaround for now at least. The big question is why date formatting is so slow for me (and if that is the general case?).

joepavitt commented 5 months ago

The big question is why date formatting is so slow for me (and if that is the general case?).

It's a great question, and my concern (which I'll need to dive deeper on) is if this is something in our control, or in the underlying ChartJS library (out of our control)

thebaldgeek commented 4 months ago

Can confirm the high CPU use for graphs here as well.
At the moment I have had to remove all graphs from my dashboard to keep it functional. Node-RED is running on a high end gaming class PC and its brought to its knees by use of 4 graphs with one pen on each. 15 minute or 750 points max each.

joepavitt commented 4 months ago

So for each update I push, I'm seeing similar to @postlund:

We already do all of the things recommended by ChartJS to improve performance, e.g. animation: false

Really not sure where to go from here, other than considering ripping out ChartJS entirely and switching to something like Apache Charts

joepavitt commented 4 months ago

Sending 10k data points in one hit performs fine, sending 0k points individually is a problem:

All at once:

Screenshot 2024-04-18 at 14 36 38

One at a time - with only 1k data points:

Screenshot 2024-04-18 at 14 38 36

note big jump on the JS memory heap...

thebaldgeek commented 4 months ago

We have similar issues with graphs in Dash v1.0. A lot of industrial processes require trends/graphs and its a bit painful to tell our customers to try and not use any graphs on their dashboards. In other words, some testing of the Apache option in Dash 2.0 might be of great value.

joepavitt commented 4 months ago

okay, so this is not just an us issue with ChartsJS: https://github.com/chartjs/Chart.js/issues/10073

In particular:

With 2.x.x, it takes 180ms : With 4.x.x, it takes 855ms :

joepavitt commented 4 months ago

In other words, some testing of the Apache option in Dash 2.0 might be of great value.

This will make @Steve-Mcl very happy who's been asking me to switch to Apache for some time 😁

Steve-Mcl commented 4 months ago

In other words, some testing of the Apache option in Dash 2.0 might be of great value.

This will make @Steve-Mcl very happy who's been asking me to switch to Apache for some time 😁

😃

postlund commented 4 months ago

This sounds like good news to me! 👍

arturv2000 commented 4 months ago

One of the issues seems to be when the chart is configured in TimeScale.

The flame graph on the browser indicates a lot of time spent on buildticks and _convertTicksToLabels.

image

In a simple test, having the chart with TimeScale it takes 200 ~ 250ms to update, using a Linear Scale it takes 3 ~ 5ms. The time reduces to 30~50ms when the chart is fully populated with all the points and it just needs to "slide" the labels and create a new one.

Don't know if when using TimeScale we don't have too many ticks, in my case for a a chart with 20 minutes of data (1200 points) there are ticks labels every 9 seconds, that have to be recalculated and repositioned every chart update.

Maybe a quick potential fix, would be to limit the number of ticks interval to be displayed.

arturv2000 commented 4 months ago

Today did a little test to check my hypothesis regarding the ticks.

Implemented my own chart using template node and did some comparison with the default chart node.

Booth charts present the same "data", data is updated every second and the charts are configured to display 20minutes of data.

The chart using the default node, apparently takes around 50~60ms to perform each update

image

image

The chart implemented on template node (the main difference is that the configuration options.scales.x.ticks.stepSize = 30, in this case would every 30seconds) takes around 7~10ms for each update

image

image

joepavitt commented 4 months ago

@arturv2000 thanks for investigating this further - what would you suggest moving forward? Difficult for us to hard-code this into source as then all charts, independent of the timeframe would try to render at 30 seconds intervals? Alternatively, I'm not convinced that the option should be exposed in the Node-RED Editor, as it feels a little over-the-top for a low-code charting solution?

arturv2000 commented 4 months ago

@joepavitt Regarding the ticks, what I implemented on the template was a dynamic tick value depending on the "chart" data duration.

//If lower than one minute, every second, if lower than 2min every 10 seconds, if lower than 5 minutes every 20s, else 30 seconds
let _dif = this.mychart.data.labels[this.mychart.data.labels.length - 1] - this.mychart.data.labels[0];
if(_dif < (60 * 1000)){
    if(this.mychart.options.scales.x.ticks.stepSize != 1){
        this.mychart.options.scales.x.ticks.stepSize = 1;
    }
}else if(_dif < (2 * 60 * 1000)){
    if(this.mychart.options.scales.x.ticks.stepSize != 10){
        this.mychart.options.scales.x.ticks.stepSize = 10;
    }
}else if(_dif < (5 * 60 * 1000)){
    if(this.mychart.options.scales.x.ticks.stepSize != 20){
        this.mychart.options.scales.x.ticks.stepSize = 20;
    }
}else{
    if(this.mychart.options.scales.x.ticks.stepSize != 30){
        this.mychart.options.scales.x.ticks.stepSize = 30;
    }
}

A very crude solution, but if we have the ticks hardcoded to 30s the chart is a little weird until there is data for 1~2 minutes.

Honestly, I get it that is tricky to pass this kind of options to a GUI for "LowCode / NoCode" solution without overcomplicating stuff, but having a strong resilience that the user should be able to code stuff in a template node i think that it kind of defeats the ideia of "LowCode / NoCode" solution, especially with the lack of error / auto-format in the template node editor, at least I am constantly copying the template node code to the vuetify play (deleting some stuff like <script src ...) to check where the error is since the console log on the browser is almost useless.

Don't know if we could have for the ticks as an option that by default would be 'auto' (current behaviour) and allow the user to insert up to X parameters (duration vs tick interval) in a table to implement something like i did.

Regarding ChartJs, another option, don't know if it would be a good ideia (or easy to implement) to let mode advanced users to simply pass the option parameter for ChartJs since most of this stuff is configured in the options section or just allow passing some sub-parameters like scales, although some nice stuff is under plugins, this would also solve issues like axis title or changing the default color of axis labels or title for example. Obviously, this kind of defeats the ideia of LowCode / NoCode but "easier" than coding the entire stuff in a template node, this would be optionally passed in the message to the node no requirement to be present in the Node Config Window. This would allow the Config Window to be kept simple and allow more advanced users to customize the chart as they like. Eventually we would need some kind of event (message) from the node indicating that it was mounted in order to pass the option at the start since this configuration could most likely be static for the lifetime of the chart and not necessary to always pass with each message.

NOTE: I am not against to change the chart to echarts or apexCharts for example, any of these have more options regarding charts types and don't rely so much in plugins (that i am having a bit of trouble using inside the template node), recently i did some tests regarding this, I needed to show the FFT result of vibration sensors, up to 3 sensors and each sensor should show 3 axis values on the chart with up to 2048 points and i need to have a vertical line annotation to indicate where several important points where visually, as such i tried also echarts and apexcharts, for this kind of approach at least ChartJs was the fastest, don't recall if it was echarts or apexcharts that with more than 1000 points being plotted for each series the browser window becomes irresponsive (FFT data updating every 500ms~1000ms), obviously (most likely) that could be because of the way I implemented it. The final result look like this at the moment: image

Even with 2048 points for each series, it "only" takes about 60~70ms to render and the page is still responsive.

Steve-Mcl commented 2 months ago

For the memory banks:

The state trail ui node on DB1 is quite popular. Here is an example I knocked up using echarts that does pretty much what that does:

https://codepen.io/Steve-Mcl/pen/dyEBeZy

Related forum thread: https://discourse.nodered.org/t/how-to-submit-html-form-using-template-table/89100/6