Skip to content

Commit 3a0530a

Browse files
committed
Added binding loop tracker
1 parent 11de75a commit 3a0530a

File tree

4 files changed

+157
-0
lines changed

4 files changed

+157
-0
lines changed

Diff for: binding-loops/LICENSE

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright 2023 GONICUS GmbH <[email protected]>
2+
Copyright 2016 David Edmundson <[email protected]>
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is furnished
9+
to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
17+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.

Diff for: binding-loops/README.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Traces for binding loops
2+
3+
Sometimes you manage to create *binding loops* in QML. That basically means
4+
that there's a change to a property *x*, which uses QML bindings in its
5+
calculation, loops back to itself after it has been modified.
6+
7+
While many of them are obvious and easy to fix, it can become tricky to
8+
understand the cause if there are various nested components involved. You'll
9+
just get some
10+
11+
```shell
12+
8:45:32.168 ?foo?|qt_message_output|QDebug::~QDebug \
13+
qrc:/de/gonicus/ui/components/controls/Spinner.qml:199:5: \
14+
QML TextField: Binding loop detected for property "text"
15+
```
16+
17+
and you've no idea what it comes from after some digging.
18+
19+
For me, this always ended up in the debugger, looking at the backtrace, and
20+
trying to understand what's going on manually.
21+
22+
## Automation with gdb
23+
24+
Doing it a couple of times in the past, I stumbled upon a 2016 blog entry of
25+
[David Edmundson]([email protected]:gonicus/qml-helper.git), who did a small
26+
python script to automate the tracing for Qt 5. Based on his work, I've adapted
27+
it to work with Qt 6.
28+
29+
### Preconditions
30+
31+
To make use of the script, you need Qt 6 and `gdb` packages installed.
32+
33+
### Usage
34+
35+
Just run `loop-tracker.py path/to/your/binary`. This will fire up **gdb** and
36+
start your app. Slowly. When it traps into a binding loop, it unwinds the stack
37+
and tries to find places where the update happens. It then prints the QML
38+
file / line number, and continues.
39+
40+
```
41+
$ ./loop-tracker.py ~/Qt/6.5.0/gcc_64/bin/qmlscene test.qml
42+
Starting gdb - please wait...
43+
[...]
44+
Thread 1 "qmlscene" hit Breakpoint 1, QQmlPropertyBinding::createBindingLoopErrorDescription (this=0x43bb20) at /home/qt/work/qt/qtdeclarative/src/qml/qml/qqmlpropertybinding.cpp:265
45+
265 /home/qt/work/qt/qtdeclarative/src/qml/qml/qqmlpropertybinding.cpp: No such file or directory.
46+
===== Binding loop detected =====
47+
48+
Backtrace:
49+
#0 - file:///var/home/prcs1076/Projekte/qml-helper/binding-loops/test.qml:6:9
50+
51+
file:///var/home/prcs1076/Projekte/qml-helper/binding-loops/test.qml:4:5: QML Rectangle: Binding loop detected for property "width"
52+
[...]
53+
```
54+
55+
That's not perfect, but helped to find some of the more hidden loops here.

Diff for: binding-loops/loop-tracker.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
import sys
3+
import subprocess
4+
5+
try:
6+
import gdb
7+
8+
except:
9+
from shutil import which
10+
from os.path import basename
11+
12+
args = sys.argv
13+
selfName = args.pop(0)
14+
15+
if not selfName.endswith(".py"):
16+
print("'%s' needs to end with '.py'" % basename(selfName))
17+
exit(1)
18+
19+
if not which("gdb"):
20+
print("'gdb' binary not found")
21+
exit(1)
22+
23+
print("Starting gdb - please wait...")
24+
subprocess.call(["gdb", "--silent", "--command", selfName, "--args"] + args)
25+
exit(0)
26+
27+
28+
def printq6string(val):
29+
d = val['d']
30+
data = d['ptr'].reinterpret_cast(gdb.lookup_type('char').pointer())
31+
data_len = d['size'] * gdb.lookup_type('unsigned short').sizeof
32+
return data.string('utf-16', 'replace', data_len)
33+
34+
35+
def breakpointHandler(event):
36+
frame = gdb.newest_frame()
37+
38+
if frame.name() == "QQmlPropertyBinding::createBindingLoopErrorDescription" or \
39+
frame.name() == "QQmlAbstractBinding::printBindingLoopError":
40+
41+
print("===== Binding loop detected =====")
42+
print("\nBacktrace:")
43+
44+
i = 0
45+
while True:
46+
frame = frame.older()
47+
48+
if frame == None or not frame.is_valid():
49+
break
50+
51+
if frame.name() == "QQmlBinding::update":
52+
currentBinding = frame.read_var("this")
53+
eval_string = "(*(QQmlBinding*)(%s)).expressionIdentifier()" % str(currentBinding)
54+
identifier = printq6string(gdb.parse_and_eval(eval_string))
55+
print("#" + str(i) + " - " + identifier)
56+
i+=1
57+
58+
print()
59+
60+
gdb.execute("continue")
61+
62+
63+
def main():
64+
gdb.events.stop.connect(breakpointHandler)
65+
66+
bp1 = gdb.Breakpoint("QQmlPropertyBinding::createBindingLoopErrorDescription")
67+
bp2 = gdb.Breakpoint("QQmlAbstractBinding::printBindingLoopError")
68+
69+
gdb.execute("run")
70+
71+
72+
if __name__ == "__main__":
73+
main()
74+

Diff for: binding-loops/test.qml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import QtQuick 2.0
2+
3+
Rectangle {
4+
width: childrenRect.width
5+
Text {
6+
text: parent.width > 10 ? "Hello World" : "Hi"
7+
}
8+
}

0 commit comments

Comments
 (0)