Solved Dynamic QML chart performance degenerates with large(ish) splineseries. How to fix?
-
I am writing an android app that reads an integer value at 1s intervals and uses a swipeview to display statistics and current status etc on one page, then plots the values over time as a splineseries on a chart on the next page. The app works great for the first 10 minutes or so, but then the UI starts to get sluggish and unresponsive. When I run the qml profiler, the Animations framerate starts around 60FPS, but after 10 minutes drops to 5FPS. Is this just do to appending values to a large splineseries? I need the app to remain responsive for at least 60 minutes of operation (so the splineseries will have about 3600 points by the end).
I initially implemented the chart update logic in QML to append values to the splineseries. My first attempt to improve performance was to moving that to the C++ backend by creating a C++ class and functions to update the splineseries. This improved performance some, but I am still seeing sluggish UI performance after running the application for 10-30minutes. Running the QML profiler shows a normal 60FPS Animation framerate, but then large (500ms) gap with no Animations after the Javascript function call to C++ to update the chart. Do I need to move the chart update function to a separate thread? what is the recommended method for calling a function on a new thread from QML?
My initial QML logic is shown below. I can provide my attempts at moving the chart update function to C++ as well if that is helpful.
in main.qml:
SwipeView { id: swipeView anchors.fill: parent currentIndex: titleBar.currentIndex Connect { } Measure { } Stats { } }
in Stats.qml:
import QtQuick 2.9 import QtQuick.Controls 2.2 import QtCharts 2.3 import "." PageLayout { id: statsPage Connections{ target: app onResetChart: resetChart() } Connections{ target: scaleHandler onStatsChanged: updateChart() } function resetChart() { values.clear() axisY.max = 50 axisY.min = 0 } property int maxSeconds: 120 //number of seconds before chart switches to minutes function updateChart() { var i=scaleHandler.measurements.length; axisY.max = (scaleHandler.maxWT > 0) ? scaleHandler.maxWT*1.2 : 50 axisY.min = (scaleHandler.average < 0) ? scaleHandler.average - 50 : 0 if (i < maxSeconds) //plot values on chart with duration in seconds up to maxSeconds { values.append(i,scaleHandler.measurements[i-1]) axisX.titleText = "seconds" axisX.labelFormat = "%.0f" if (i === 1){ axisX.max = 4 } else if (i > 4) { axisX.max = i*1.05 } } if (i === maxSeconds) //switch units to minutes and redraw chart after maxSeconds { print("switching to minutes") values.clear() for (var j=0; j<i;j++) values.append(j/60,scaleHandler.measurements[j]) axisX.max = i*1.05/60 axisX.titleText = "minutes" axisX.labelFormat = "%.1f" } if (i > maxSeconds) //continue adding values with x scaled to minutes { values.append(i/60,scaleHandler.measurements[i-1]) if (i/60 + 1 > axisX.max) { axisX.max = i*1.05/60 } } if (i === maxSeconds*4) //after 4*maxSeconds, remove decimal { axisX.labelFormat = "%.0f" } } ... ChartView { id: chart theme: ChartView.ChartThemeQt anchors.centerIn: parent anchors.verticalCenterOffset: ViewSettings.fieldHeight*.5 antialiasing: true width: parent.width - ViewSettings.fieldMargin*2 height: (2 * parent.height < parent.width) ? parent.height - ViewSettings.fieldHeight*4 : parent.height - ViewSettings.fieldHeight *6 legend.visible: false animationOptions: ChartView.SeriesAnimations SplineSeries { id: values axisX: axisX axisY: axisY } ValueAxis{ id: axisX min: 0 max: 4 titleText: "seconds" labelFormat: "%.0f" } ValueAxis{ id: axisY min: 0 max: 50 labelFormat: "%.0f" } } Button { id: resetButton width: ViewSettings.fieldHeight * 4 height: ViewSettings.fieldHeight anchors.bottom: parent.bottom anchors.bottomMargin: ViewSettings.fieldHeight anchors.horizontalCenter: parent.horizontalCenter visible: !scaleHandler.measuring && !paused onClicked:{ app.reset() resetChart() swipeView.currentIndex = 1 } Text { anchors.centerIn: parent font.pixelSize: ViewSettings.tinyFontSize text: qsTr("RESET") color: resetButton.enabled ? ViewSettings.textColor : ViewSettings.disabledTextColor } } Row { spacing: 15 anchors.bottom: parent.bottom anchors.bottomMargin: ViewSettings.fieldHeight anchors.horizontalCenter: parent.horizontalCenter visible: scaleHandler.measuring || paused Button { id: startButton width: ViewSettings.fieldHeight * 2.5 height: ViewSettings.fieldHeight enabled: __timeCounter ===0 || scaleHandler.measuring || paused onClicked: __timeCounter === 0 ? app.start() : app.stop() border.color:startButton.enabled ? ViewSettings.textColor : ViewSettings.disabledTextColor Text { anchors.centerIn: parent font.pixelSize: ViewSettings.tinyFontSize text: __timeCounter === 0 ? qsTr("START") : qsTr("STOP") color: startButton.enabled ? ViewSettings.textColor : ViewSettings.disabledTextColor } } Button { id: resumeButton width: ViewSettings.fieldHeight * 2.5 height: ViewSettings.fieldHeight enabled: __timeCounter != 0 onClicked: scaleHandler.measuring ? app.pause() : app.resume() border.color:resumeButton.enabled ? ViewSettings.textColor : ViewSettings.disabledTextColor Text { anchors.centerIn: parent font.pixelSize: ViewSettings.tinyFontSize text: scaleHandler.measuring ? qsTr("PAUSE") : qsTr("RESUME") color: resumeButton.enabled ? ViewSettings.textColor : ViewSettings.disabledTextColor } } } Component.onCompleted: resetChart() }
-
Eliminating the axisX.max changes didnt solve the problem, but I think I've figured it out.
It seems like the culprit is the use of splineseries. From the Qt documentation: "The control points are automatically calculated when the data changes. " I guess that means that ALL the control points are re-calculated when data is appended to the splineseries.
Changing the series from a splineseries to a lineseries was all that I had to do to fix the problem. OpenGL is also not supported for splineseries, so using a lineseries with OpenGL enabled gives even better performance.
I'll probably still use splineseries for the initial data as is looks a lot better, and then re-draw the data as a lineseries when it switches from seconds to minutes after a 120seconds. By then the chart looks about the same whether its using a splineseries or lineseries...
-
@grk3010
I've never used QML or any chart series. Sounds promising? Read on....Whenever a point arrives, in all 3 or your
i
cases you alteraxisX.max
. That would imply to me that you force a complete redraw of all points, because of the whole chart scale change?Now I don't know how long drawing 3,600 points is supposed to take, but maybe it's not good. Presumably just appending a new point should not redraw all existing ones?
If that's so: temporarily fix the
axisX.max
to some constant. Does that make difference to your performance? If so, look at uppingaxisX.max
dynamically in "steps", so it only happens once a minute or something? Otherwise, if it's the sheer number of points, maybe you could "sample" the old ones so that you can "thin them out" so as to keep the total number of points low?But really I'm thinking you should be able to append a new point without some big redraw. One point per second surely cannot be that taxing....
-
Eliminating the axisX.max changes didnt solve the problem, but I think I've figured it out.
It seems like the culprit is the use of splineseries. From the Qt documentation: "The control points are automatically calculated when the data changes. " I guess that means that ALL the control points are re-calculated when data is appended to the splineseries.
Changing the series from a splineseries to a lineseries was all that I had to do to fix the problem. OpenGL is also not supported for splineseries, so using a lineseries with OpenGL enabled gives even better performance.
I'll probably still use splineseries for the initial data as is looks a lot better, and then re-draw the data as a lineseries when it switches from seconds to minutes after a 120seconds. By then the chart looks about the same whether its using a splineseries or lineseries...