Compiling QML to C++: Avoiding duck typing

This is the fourth installment in the series of blog posts on how to adjust your QML application to make the most of qmlsc. In the first post we've set up the environment. You should read that post first in order to understand the others. In the second post I've shown how to add type annotations to JavaScript functions. In the third post I've shown how to navigate around various pitfalls you may find when making types visible at compile time.

This time around I'll write about a type category qmlsc cannot handle. If it walks like a duck and quacks like a duck, it is a duck. This concept refers to the practice of assuming the presence of properties and methods on objects without them being explicitly declared. From the place where you tell the duck to walk you don't really care how it walks. It should just do. Furthermore, you might not even care if quack() returns a string or a foozle. In JavaScript you can introspect the result and dispatch on its type, or just pass it on as an opaque piece of data. This, however, is where the compilation to C++ breaks. If we want to make the duck quack, we want to know what type to construct in C++. Otherwise we'd have to use QVariant or a similarly generic type for all such things. That would be more expensive than what the QML engine does when interpreting byte code. Where is the duck in our example project? The first warning generated from the CategoryLabel.qml file is as follows:

Warning: CategoryLabel.qml:36:41: Property "displayName" not found on type "QObject"
    property string text: model ? model.displayName : ""

We know that this particular QObject has a property displayName, because we know that it is in fact a TimelineModel as declared in src/libs/tracing/timelinemodel.h. However, qmlsc does not know. And it doesn't trust us. We could assign some object without a displayName to the "model" property tomorrow, after all.

Let's check how expensive this binding was. Profile Qt Creator again, and have it open our example trace. You can click the label on line 36 in CategoryLabel.qml to see how long it took. On my machine, the binding was evaluated 24 times, with a cumulative duration of 53.3µs. Click the label again a few times and notice that the table in the profiler jumps between a different lines. If you take a look at the two subtables at the bottom, you will see that one of the lines in the "Callee" table refers to the same file and line has the line in the main table:

binding and javascript

Here we have 40.4µs total time. How come? To clarify this, you can select the timeline view and zoom in using Ctrl and the mouse scroll wheel on the point you're interested in. As we are dealing with a very short time span you may need to scroll for while. Eventually you'll arrive at something like this:

zoomed timeline

You can see that there are two bindings and two JavaScript functions at play here. The binding on CategoryLabel.text is implemented by the JavaScript expression on its right hand side and those are measured separately. The binding measurement includes the overhead of finding the right function to call and writing the result to the property. qmlsc is only concerned with compiling the JavaScript expression to C++, though. So, the JavaScript events are the more interesting ones for us.

TimelineModel so far is an anonymous type. We can see that from the QML_ANONYMOUS macro in its declaration. In order to show the compiler that there is a displayName to the "model" property, we can give TimelineModel a QML name, and use that as type of the property. Replace the QML_ANONYMOUS with QML_ELEMENT to use the "TimelineModel" name also in QML, and declare the model property in CategoryLabel.qml as follows:

property TimelineModel model

This makes it find the property, but now we get another warning:

Warning: CategoryLabel.qml:36:41: Could not compile binding for text: Member displayName of Timeline::TimelineModel of ??::model with type Timeline::TimelineModel can be shadowed
    property string text: model ? model.displayName : ""

What is this? Shadowing of a property is the practice of declaring the same property, potentially with a different type in a derived class in C++. The QML engine will pick the most derived class when resolving the type of a property. We could therefore go and declare a type EvilTimelineModel that provides a displayName property, but not as a QString, but rather as an EvilFoozle. The compiler will not generate any code to deal with such things as that would slow down the very, very common case where this is not done. Instead, it forces you to declare that you won't do it. This is what the "FINAL" attribute to Q_PROPERTY is for. A FINAL property cannot be shadowed. The QML engine rejects types that shadow FINAL properties. So, add FINAL to the displayName property as follows:

Q_PROPERTY(QString displayName READ displayName WRITE setDisplayName NOTIFY displayNameChanged FINAL)

While, you're at it, you might do the same to all other properties in all other classes, too. This indeed silences the warning. Did this improve the performance? The QML Profiler tells me that now it takes a cumulative 50.1µs for the 24 bindings, 33.7µs of which are for the JavaScript execution. This is in contrast to 53.3µs and 40.4µs before. This time the speedup is not as clear cut. However, by allowing such bindings to be compiled to C++, you can prepare for further optimizations qmlsc might do here in future versions of Qt. For example, it might generate code that compresses the two lookups of "model" into one. When compiling ahead of time, we can see that the whole binding is free of side effects and therefore the "model" property cannot change between its two uses.

Compatibility

We depend on declarative QML type registration for the QML_ELEMENT macro. Declarative type registration has been around since Qt 5.15. The FINAL attribute to Q_PROPERTY has been around since Qt 4.6.


Blog Topics:

Comments