Crazy memory usage with TableView :(
-
Working on porting an app from QWidget to QML. It was all going great until I started measuring the memory usage. This is across OSX and Windows
The memory usage of a "do nothing" QML app is about 30-50MB. That's OK, understandable even if I'm not too happy about it.
But comparing the QWidget version of a QTableView and a QML TableView is depressing. A TableView with 11 visible columns (with about 10 hidden) with plain text in each cell and about 450 rows uses more than 120MB whereas the QWidget version uses 40MB altogether.
I've tried a number of things, but the only thing that seems to help is literally displaying less data which is a non-starter for this app. Are there any suggestions that can be made on how to debug and reduce the memory usage?
I really don't want to go back to QWidget...
Here is an example I just whipped up to demonstrate:
@
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Window 2.1
import QtQuick.Layouts 1.0Window {
visible: true
visibility: Window.MaximizedTableView { anchors.fill: parent model: 500 TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } TableViewColumn { role: "modelData" } }
}
@!https://i.imgur.com/bsJTjjA.png(qmlscene using 115 MB)!
-
I am afraid a TableView in QML can never directly compare with the raw performance of a widget item view. The main reason for this is that the technologies are vastly different. Unlike a widget item view, where the delegate is actually the same instance for every single painted cell, every one of the visible table cells you create in QML will create its own unique delegate instance with a unique state, live bindings, signals and connections being made and updated in real time. (though rows of items are re-cycled when scrolled in and out of view)
This is what makes it possible to do complex animations and other things you could never do with a widget item view but this power unfortunately comes at a price. That is probably not the answer you were hoping for but I think realistically we will never see the exact same memory use for a complex table view in QML even though performance has already been significantly improved from the earliest versions and there is lots of room for future improvements.
Nothing really indicate that a leak or that something particularly wrong is happening as the memory use scales with the complexity of the data set. Make sure you test everything in a release build though as both performance and memory use can be very different in the case of a debug build. Also you can try to create a raw ListView yourself with very simple delegates to see if memory use is significantly less than for a TableView. I suspect it won't be though.
-
Looking at a heap profiler on your example:
- 200 x 56kb allocations (11mb total) which look to be caused by javascript expressions (QQmlJavaScriptExpression::GuardCapture::captureProperty)
- 12.33mb of QQmlBinding instances (89,807 of them!)
- ~9mb of QQuickTextNode instances (3,090 of them)
- ~4mb of v4 heap allocations
These numbers seem abnormally high. I'm not sure what TableView is doing internally, but those don't look at all healthy.
I wouldn't expect numbers anywhere near this high, either, even taking into account what Jens says -- it is not incorrect that there are additional costs in some ways, but the costs for typical Quick applications are perfectly manageable by comparison to this.
-
Here's my testcase for a ListView equivilent:
@import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Window 2.1
import QtQuick.Layouts 1.0Window {
id: root
visible: true
visibility: Window.MaximizedRow { Repeater { model: 30 ListView { height: root.height width: root.width / 30 model: 500 delegate: Text { text: modelData } } } }
}@
Memory use, as according to Activity Monitor on OS X:
TableView: 116mb
Repeater of ListView: 54mb -
Interesting findings:
- ListView includes no QQmlJavaScriptExpression allocations (expected; my test doesn't really contain any javascript. TableView is rather full of it.)
- QQmlBinding instances are way down: 89,800 down to 3,600 (506kb, much more manageable)
- Highest memory use comes from QQuickTextNode, which is unsurprising given how many items of text are visible on my screen (and QQuickTextNode is a bit of a pig, that is a known issue)
- There's a few allocations from WTF::OSAllocator::reserveAndCommit that are at the top of the list this time (6.25mb total). I think this is used for the JS garbage collection heap, but I forget. Seems high.
Any other users don't really stand out, as things start to get "lost" in the crowd a bit...
-
To me, it looks like the TableView implementation is suboptimal. I don't really know how to suggest fixing it, because I don't really do much work on or with controls - most of my work is focused on QtQuick itself. You could try opening a bug on it with the findings from here, but I'm not sure how much priority it will get (again, because I'm not involved with it, I don't know what the people working on it are busy with and what their workloads look like).
It could well also be that it isn't a simple fix: generally speaking there's a reason that the base components like ListView in Quick are implemented in C++ - developer efficiency on the base component set goes down (because writing good components is harder), but performance tends to be better because one can take advantage of a less declarative implementation in favor of better performance. This is pure speculation, however: I haven't really looked at the TableView implementation beyond a trivial glance.
Seperately from that, the text node stuff should be improved on. That's already a known issue, but I don't think there's a bug for that yet. I'll ask the person whom I think would know best.
-
Regarding the text node, the most comprehensive bug is https://bugreports.qt.io/browse/QTBUG-42853. The overhead for each element is easily in the 1k range on the render thread, that comes in addition to whatever the QQuickText object has on the GUI thread (which is a QQuickItem which is 300+bytes and sits on a QTextDocument with its own datastructures).
-
Robin,
It's funny that you used the "orthogonal" of TableView in your example. TableView is ListView of Repeaters, not the other way around. That also means that we may look even worse, but it's worth checking.
There are a few things that can be optimized in TableView, but the most flagrant is related to styling. Our abuse of Loaders is to blame for a lot of the performance issues we've been having in controls as a whole, and having one Loader per table cell doesn't help. I can't say where all those QQmlBinding instances come from, though.
-
Certainly some pretty good ideas coming out of this thread. Setting custom delegates to simple placeholders should remove most of the overhead of style itself. I don't think using a Loader is really the main issue though.
The excessive binding use is most likely caused by the construction of lots of simple styleData objects passed to the delegate containing style information. At the moment these are QtObjects with a lot of bound properties. Sounds like they might benefit significantly from being replaced with simple C++ objects. This would at least reduce the amount of created javascript bindings to a fraction of what it is at now.
-
I've been working on doing things a bit more lazily on TableView. "Here's a WIP patch":https://codereview.qt-project.org/105966 that instantiates columns only as they get close the the actual visible portion of the table view (applicable only on the very latest dev branch). Unfortunately, we can't recycle per-column delegates because each column can have their own distinct delegate, at least not in a straightforward way. But I'm sure something can be done. So, the tradeoff is less memory consumption, and faster creation and vertical scrolling, against slower horizontal scrolling. And, for obvious reasons, smaller windows perform better than larger windows.
@Jens, it has been proved (and confirmed by Simon) that the way we inject the styleData object in the loaded component is one of the causes things are so slow. The main reason being the "styleData" name resolution going through the slowest path possible. Making it a plain object is not going to change the creation time by much. Also, since we're showing model data (therefore, potentially dynamic), we need to bind to something, so moving away from QObject seems a step backwards to me.
-
sohail. You would have to hack the source but that is actually pretty simple as it's just a matter of copying the contents of TableView.qml to a local project and directly fork it. You might get some warnings from the style construction, but that is all.
For instance, you can go through all of the "styleData" properties and comment out all bindings except the "value" one just to experiment on it's impact. I did a quick test and I'm afraid it wont make all that much difference regardless. They really don't seem like the bottleneck.
@gabriel Thats indeed interesting. Did Simon suggest any other ways of injecting the data or is the concept itself not fixable? Given how much we rely on these, it might make sense to modify the engine to be more accommodating for this scenario.
Regarding columns, I was actually just playing around with the same concept by simply setting columns !visible based on scrolling but seems like you are way ahead on that already. That should at least prevent the loaders from running. If we could also find a way to prevent setting up the hidden styleData objects too, there would be a pretty significant improvement on memory use. Did you test the patch on the example above already?