Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. QML and Qt Quick
  4. Dynamic QML chart performance degenerates with large(ish) splineseries. How to fix?
Forum Updated to NodeBB v4.3 + New Features

Dynamic QML chart performance degenerates with large(ish) splineseries. How to fix?

Scheduled Pinned Locked Moved Solved QML and Qt Quick
3 Posts 2 Posters 1.9k Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • G Offline
    G Offline
    grk3010
    wrote on last edited by
    #1

    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()
    }
    
    JonBJ 1 Reply Last reply
    0
    • G Offline
      G Offline
      grk3010
      wrote on last edited by
      #3

      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...

      1 Reply Last reply
      2
      • G grk3010

        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()
        }
        
        JonBJ Online
        JonBJ Online
        JonB
        wrote on last edited by JonB
        #2

        @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....

        1 Reply Last reply
        1
        • G Offline
          G Offline
          grk3010
          wrote on last edited by
          #3

          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...

          1 Reply Last reply
          2

          • Login

          • Login or register to search.
          • First post
            Last post
          0
          • Categories
          • Recent
          • Tags
          • Popular
          • Users
          • Groups
          • Search
          • Get Qt Extensions
          • Unsolved