Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

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 alter axisX.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 upping axisX.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...


Log in to reply