Compiling QML to C++: Untangling dependencies

This is the fifth installment in the series of blog posts on how to adjust your QML application to take the maximum advantage of qmlsc. In the first post we've set up the environment. You should read that post first in order to understand the others. After fixing various other problems in the previous posts we're going to learn how to straighten out cyclic dependencies between QML documents.

Looking at CategoryLabel.qml again, we can see that its "draggerParent" property has the duck typing problem described in the previous post. The problem surfaces when trying to compile the binding on "contentBottom":

Error: CategoryLabel.qml:44:47: Could not compile binding for contentBottom: Cannot load property contentY from QQuickItem of (component in .../src/libs/tracing/CategoryLabel.qml)::draggerParent with type QQuickItem.

Luckily, here the solution looks simpler. "draggerParent" is a TimelineLabels as we can see from the one place where it's initialized, in TimelineLabels.qml:

draggerParent: categories

Now we could just type the property as TimelineLabels, but that would trigger an error at run time. TimelineLabels instantiates and therefore depends on CategoryLabel. Due to limitations in the QML engine we cannot create cyclic dependencies. Using the TimelineLabels type in CategoryLabel.qml will therefore not work. However, the properties we need are actually from TimelineLabels' base type, Flickable. We can easily state that without creating a cyclic dependency:

property Flickable draggerParent

Unfortunately, this doesn't help:

Error: CategoryLabel.qml:44:47: Could not compile binding for contentBottom: Member contentY of QQuickFlickable of (component in .../src/libs/tracing/CategoryLabel.qml)::draggerParent with type QQuickFlickable can be shadowed
    property int contentBottom: draggerParent.contentY + draggerParent.height - dragOffset

QQuickFlickable and it's non-FINAL properties are outside our control (although the fact that this one isn't FINAL is a bug in QtQuick). Let's take a step back here. When we identified the cyclic dependency, we might have gotten suspicious already. Cyclic dependencies between QML components usually tell us that the components are coupled too tightly. Rather than passing the Flickable into each CategoryLabel, and therefore tying each CategoryLabel to the specifics of a Flickable, we might just tell the CategoryLabel whatever it needs to know to position itself. Then it can also be instantiated in a context where we have a different parent element.

What we need to achieve is to allow the CategoryLabel to be dragged around in the currently visible area of the parent element, while being positioned in its "content" area. Therefore we need to pass a contentY and a visibleHeight. With this in place, we can keep the draggerParent as Item in order to remain flexible in the choice of parent elements, while still allowing CategoryLabel to implement its dragging behavior. Adding a few defaults doesn't hurt here:

property Item draggerParent
property real contentY: 0
property real visibleHeight: contentHeight

The instantiation then looks as follows:

CategoryLabel {
    id: label
    // [...]
    draggerParent: categories
    contentY: categories.contentY
    visibleHeight: categories.height
    // [...]
}

What does it do? Let's look at the binding on "contentBottom", which is then changed to:

property int contentBottom: contentY + visibleHeight - dragOffset

We can profile Qt creator loading an example trace file again, like we did in the previous posts. The binding is executed 82 times here. Without the optimization, I get 153µs for he whole binding and 108µs for only the JavaScript execution. With the optimization, it's 66µs and 34µs.

This speedup is in large part due to the fact that we've reduced the number of lookups needed in this particular binding. Previously we had to get draggerParent.contentY and draggerParent.height instead of contentY and visibleHeight. A lookup of draggerParent.contentY results in first a lookup of draggerParent in the scope, and then a lookup of contentY in the resulting object. As the interpreter cannot prove that this operation doesn't change draggerParent itself, it will repeat the lookup for draggerParent when getting draggerParent.height. Therefore, previously we had five lookup operations, and now we have three. However, due to the way we initialize CategoryLabel, we have shifted some of this effort to a different place. A more detailed analysis would be necessary to put an exact number on the cumulative effect.

You may be tempted to inline the whole "contentBottom" binding into the one place where it's used. However, this is not generally a good idea. If we do so, the calculation will be repeated for each of the sub-labels when the category is expanded. As we have seen, the lookups needed to retrieve the values are not free. Looking up only one value (contentBottom) is substantially cheaper than looking up three of them (contentY, visibleHeight, dragOffset).

Compatibility

All of this has been possible since the earliest days of QML. It's a good idea to decouple your components this way. The compilation to C++ is an added benefit you can then get when switching to Qt 6.


Blog Topics:

Comments