QML is very easy to learn and use. Depending on the complexity of the UI application, it often takes a few hours to have the application wireframe fully implemented and a few more to add some JavaScript logic and actually perform some real actions, like for example listen to the radio from the internet. Now that said, it is really important to not underestimate the risks of fast prototyping or most importantly writing your logic in JavaScript – both can be ‘powerful’ enough to make your UI unresponsive, slow, to freeze, to perform bad or even crash! That said, we’re not referring only to adding some standalone js files into your project and call methods that are implemented there but to every place and in any way JavaScript is used in the entire UI.
For example, imagine a list menu where if pressing the “more” button in the list’s delegate, another component is loaded that contains another smaller list of items as options. It is not much saying that this may block the UI for a few seconds, until all operations started from when that button was pressed are finished, we’ll also have a look at a similar example later in this article. This and many other problematic cases we can examine and optimize using the QML profiler. However, before going into more details about that, it is useful to explain roughly what is the Scene Graph in Qt applications.
For performance optimization, Qt/QML applications make use of a dedicated Scene Graph that it’s rendered via a graphics API, like for example OpenGL. This scene graph takes care about organizing all items (and their state changes) to be rendered in frames, reducing this way the draw calls and hence achieving better performance. Now when it comes to rendering, there are 3 loop variants available, the basic, the windows and the threaded. In summary, the basic and windows loops are single threaded and the threaded runs the scene graph rendering on a dedicated thread. You can dive into more details about the Qt Quick Scene Graph here.
The below graphics are representing both single and multi-thread rendering loops respectively.
Now that we know about the scene graph and we have an idea of what it does under the hood, we can better understand the data we get in the QML profiler when profiling our applications. That said, when we stop the profiler, we’ll get the analysis results divided in categories with the first one of them being the Scene Graph.1.1 QML Profiler basic render loop, Scene Graph frame view
Above we can see the Scene Graph steps, as they occur when the applications is running.
Zooming out to take the whole picture, it looks like below with more frames showing up:1.2 QML Profiler basic render loop, scene graph multiple frames view
Below is the code used in the above profiling example:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
id: window
width: 640
height: 480
visible: true
SequentialAnimation {
running: true
loops: Animation.Infinite
NumberAnimation {
target: window
property: "animatedHeight"
to: window.height
duration: 600
}
NumberAnimation {
target: window
property: "animatedHeight"
to: 0
duration: 600
}
}
Rectangle {
width: parent.width
height: animatedHeight
color: "pink"
}
}
It is worth mentioning here that we will actually see data in the Scene Graph section only when something is requesting to paint. In the above example the rectangle’s height has a binding to the “animatedHeight” property that’s being animated continuously hence we have paint calls since the rectangle resizes following the value of that property. That said, if we remove this binding and profile again, we’ll have a completely different view:
1.3 QML Profiler basic render loop, scene graph multiple frames view, no further painting
Now let’s decrease our example’s performance a bit by adding the following code (part is borrowed from the Qt Quick Demo – RSS News) after the pink rectangle:
Button {
anchors.centerIn: parent
text: "Press me!"
onClicked: {
newsLoader.active = true;
}
}
Loader {
id: newsLoader
width: parent.width
height: 200
anchors.bottom: parent.bottom
active: false
sourceComponent: Component {
id: newsFeedList
Item {
anchors.fill: parent
XmlListModel {
id: feedModel
source: "http://news.yahoo.com/rss/science"
query: "/rss/channel/item[child::media:content]"
namespaceDeclarations: "declare namespace media = 'http://search.yahoo.com/mrss/';"
XmlRole { name: "title"; query: "title/string()" }
// Remove any links from the description
XmlRole { name: "description"; query: "fn:replace(description/string(), '\<a href=.*\/a\>', '')" }
XmlRole { name: "image"; query: "media:content/@url/string()" }
XmlRole { name: "link"; query: "link/string()" }
XmlRole { name: "pubDate"; query: "pubDate/string()" }
}
Rectangle {
anchors.fill: parent
color: "lightblue"
opacity: 0.3
}
ListView {
anchors.fill: parent
model: feedModel
delegate: Item {
width: parent.width
height: 10
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "black"
anchors.bottom: parent.bottom
}
Text {
id: titleText
text: title
width: parent.width
wrapMode: Text.WordWrap
font.pixelSize: 8
font.bold: true
}
}
}
}
}
}
This time something looks wrong in QML profiler, as soon as I press the “Press me!” button.
If you ran the example yourself, you should have noticed that the pink rectangle stopped animating for a while, for as long as the news feed was loading and was feeding the list view with data. Nevertheless this performance drop can be improved dramatically if we pull XmlModel out the Loader component.
Having explained the above, it’s clear now that it’s all matter of how you implement your code, and that every detail that comes along with it can have a big impact on your application’s performance.
Generally speaking, in the QML profiler’s timeline we get to know what is that we’re doing wrong and causes our UI to be slow or even freeze for a few seconds. The most regular parts to look at when this happens, is Animations and JavaScript.
Compiling and creating are also very important for the startup time of the application. If we have a look at the below graphics of a different example we see that main.qml(MainStack.qml etc.) and MainSocket.qml are consuming a lot of time in compilation and creation respectively.
Now, the total startup time can be visualized in the stats. Taking as input the second example we can basically see that the application took 2.89 seconds to start and the most costly components were exactly the ones above, MainSocket.qml and MainStack.qml.
Other than this, we also have a flame graph available where we can get an overview of the QML and JavaScript execution in percentage values. From the previous example we once again see the same components being the ones consuming more resources.
In flame graph view we can also choose to switch between total, memory and allocations modes to see memory amount allocated or memory allocations performed by the components/functions, or the total of both.
Looking at the second example stats, we immediately know where to look for improvements, in order to increase performance – 2.89 seconds and on a mac book pro is not what you want to have 🙂 That would be even worse on a less powerful hardware. Once again, as stated above: It all depends on how you implement your application.
We generally recommend the following:
- Try learning QML before implementing anything, 1000 lines of bad code and zero application architecture and structure, is only understood by you and can easily create dependencies and reduce quallity and productivity. That said, don’t let C++ developers implement UI in QML if they have not been educated first. This might sound tough, but it’s true. In reality it’s not only about C++ developers, but all. QML is so easy, allowing you to put stuff together so quickly that you forget about performance and optimizations. Oftenly we don’t even notice when we right bad code in QML because we’re not aware that we’re doing it bad at all!
- Always take into account the hardware you’re targeting. If desktop, of course you have more flexibility but still we have seen Qt/QML desktop applications performing bad (like the example above), because of bad software architecture and design. Qt is offering pretty much everything you need in order to have fast and efficient apps so treat them like native apps implementing the logic in C++ instead of introducing tones of JavaScript like if it was a web app.
- Don’t become a victim of the JavaScript trap, it is not necessary after all. Sure, using JavaScript to do things is pretty easy and convenient, however it doesn’t turn out to be so convenient at the end, as it might result in wasted CPU usage-less time while your UI thread remains blocked as well as might cause memory problems. You have to always have in mind that the more the JavaScript, the most you put you UI into danger to freeze or be slow while the heavy JavaScript operations are being executed.
- Have in mind that you could help lower costs by developing with best practices and not whatever just ‘works’.
Concluding, never misunderstand the simplicity of QML – it requires as much attention as any other software application. We saw earlier the negative impact that minor details like in the first example can have in an application, so learning best practices and profiling your application is your key to success!
Have you profiled your application yet?