Why does decoupling from random signal fix QML binding loop?
-
I've been trying to address a defect that breaks my QML UI and the defect went away... why???
I have C++ code that monitors hardware readings that can change as often as every millisecond, but seldom changes more than 10 times in a second. The value is exposed as a QString property which is displayed in QML. After a while (usually hours) I get a message "binding loop detected" and my program becomes unstable.
I hypothesized that occasionally multiple signals come in very close together and are falsely detected as a "binding loop" but I have not read anything discussing this. I de-coupled the changes in the hardware reading from the property by storing the new value and polling the value 60-times-per-second and displaying changes. Now my code is stable.
Why does this work? Why is QML making a false "binding loop"? Is this a problem when reading hardware?
-
@stan.m The root cause was a thread-safety problem; data was read by the main thread at the same time that it was written by another thread. Specifically, the C++ getter for the QString "value" property was called at the same time that the value was written. Other data was written and read at the same time, but their types write as an atomic operation on the ARM processor. The data that caused the problem was of type: QString.
A QString object's member data is a heap pointer to the 16-bit unicode array. When written and read simultaneously, the C++ getter passes a corrupted pointer into the QML runtime. As I wrote before, the "binding loop" warning came after many of these "first-chance" exceptions and had nothing to do with a true binding loop.
The first-chance exception occurred when two value changes occurred back-to-back and the QML runtime called the C++ getter to retrieve the value at the same time that the second change was written.
My initial "fix" worked because polling the value every 100ms allowed the first change propagate to QML long before the next change comes in.
I since have revised the code to use a mutex to prevent writing and reading simultaneously.
-
@stan.m
you can't expect serious help without showing a single line of code where your binding loop might be. -
@raven-worx Here's the error:
WARNING QtUI qrc:////Screens/Status/TransducerSummary.qml:12:19: QML Text: Binding loop detected for property "text"
Here's minimum QML code that will show the first binding loop message after hours of continuous operation:
Column { Rectangle { width: 100; height: 40 color: ma3.pressed ? 'bluesteel' : 'darkgray' Text { // <- error message points to this line anchors.fill: parent font { bold: true; pixelSize: 24 } horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: 'white' text: PressureTransducer.value } MouseArea { id: ma3; anchors.fill: parent; onClicked: rate.decrement() } } }
-
@stan.m
and the value property ofPressureTransducer
is somehow dependent on the text property? -
@stan.m
I‘m unsure how exactly the binding loop detection works, but my guess would be, the changed signal of ‚‘PressureTransducer.value‘ es emitted more than once during one eventloop cycle and actually changes tooA workaround could be a single shot timer with an intervall of 0, that on timeout emit the changed signal. When the value changes, you simply reset the timer.
That would guarantee a single changed signal per eventloop cycle -
@J.Hilk I checked the Qt source code: two separate "changed" signals cannot trigger a "binding loop" message.
In Qt’s
qqmlbinding.cpp
source code, theQQmlBinding::update()
function checks an "updatingFlag" before printing the "binding loop" message and returning or continues, setting the "updatingFlag" before evaluating the binding then clears the "updatingFlag" before exiting the function.My solution works, but yours is better. My solution can result in multiple signals being added to the event queue if the UI thread pauses due to an operation taking too long. Your solution prevents that.
The only addition I would make is to use a std::atomic for the data to address the undefined behavior of data being “written to” and “read from” by two different threads.
I still do not know what causes the “binding loop” message, but I know better what it is not. It is not just a binding loop issue. Attaching a debugger, I see dozens of first chance exceptions before the “binding loop” message. I missed these before because they were not logged. Reviewing logs, this occurs after hours of continuous operation or never occurs even after weeks of operation.
It is not a thread-safety issue in regards to the data(It was a thread-safety issue -- see response below.) It is not a case of the Event queue overflowing (it has happened with 137-8000+ items on the queue and my solution works correctly with 2 million items on the Event queue).I can only reproduce it if the real time change is allowed to add signals to the UI thread’s event queue. I can add change signals 62 times per second indefinitely, 82 per second runs out of memory eventually as the Event queue fills up (avoided with the Timer solution).
Best practices:
- near real-time hardware changes should be polled instead of allowed to each add a signal on the UI thread’s event queue.
- a Timer (on the UI thread) prevents multiple change signals for the same QObject being processed in the same Eventloop cycle.
Where to learn this aside from ICS webinars? (and I’m sure KDAB teaches this too.)
-
@stan.m The root cause was a thread-safety problem; data was read by the main thread at the same time that it was written by another thread. Specifically, the C++ getter for the QString "value" property was called at the same time that the value was written. Other data was written and read at the same time, but their types write as an atomic operation on the ARM processor. The data that caused the problem was of type: QString.
A QString object's member data is a heap pointer to the 16-bit unicode array. When written and read simultaneously, the C++ getter passes a corrupted pointer into the QML runtime. As I wrote before, the "binding loop" warning came after many of these "first-chance" exceptions and had nothing to do with a true binding loop.
The first-chance exception occurred when two value changes occurred back-to-back and the QML runtime called the C++ getter to retrieve the value at the same time that the second change was written.
My initial "fix" worked because polling the value every 100ms allowed the first change propagate to QML long before the next change comes in.
I since have revised the code to use a mutex to prevent writing and reading simultaneously.