10

I've encountered this problem on several occasions, with objects created dynamically, regardless of whether they were created in QML or C++. The objects are deleted while still in use, causing hard crashes for no apparent reason. The objects are still referenced and parented to other objects all the way down to the root object, so I find it strange for QML to delete those objects while their refcount is still above zero.

So far the only solution I found was to create the objects in C++ and set the ownership to CPP explicitly, making it impossible to delete the objects from QML.

At first I assumed it may be an issue with parenting, since I was using QObject derived classes, and the QML method of dynamic instantiation passes an Item for a parent, whereas QtObject doesn't even come with a parent property - it is not exposed from QObject.

But then I tried with a Qobject derived which exposes and uses parenting and finally even tried using Item just for the sake of being sure that the objects are properly parented, and yet this behavior still persists.

Here is an example that produces this behavior, unfortunately I could not flatten it down to a single source because the deep nesting of Components breaks it:

// ObjMain.qml
Item {
    property ListModel list : ListModel { }
    Component.onCompleted: console.log("created " + this + " with parent " + parent)
    Component.onDestruction: console.log("deleted " + this)
}

// Uimain.qml
Item {
    id: main
    width: childrenRect.width
    height: childrenRect.height
    property Item object
    property bool expanded : true
    Loader {
        id: li
        x: 50
        y: 50
        active: expanded && object && object.list.count
        width: childrenRect.width
        height: childrenRect.height
        sourceComponent: listView
    }
    Component {
        id: listView
        ListView {
            width: contentItem.childrenRect.width
            height: contentItem.childrenRect.height
            model: object.list
            delegate: Item {
                id: p
                width: childrenRect.width
                height: childrenRect.height
                Component.onCompleted: Qt.createComponent("Uimain.qml").createObject(p, {"object" : o})
            }
        }
    }
    Rectangle {
        width: 50
        height: 50
        color: "red"

        MouseArea {
            anchors.fill: parent
            acceptedButtons: Qt.RightButton | Qt.LeftButton
            onClicked: {
                if (mouse.button == Qt.RightButton) {
                    expanded = !expanded
                } else {
                    object.list.append({ "o" : Qt.createComponent("ObjMain.qml").createObject(object) })
                }
            }
        }
    }
}

// main.qml

Window {
    visible: true
    width: 1280
    height: 720

    ObjMain {
        id: obj
    }

    Uimain {
        object: obj
    }
}

The example is a trivial object tree builder, with the left button adding a leaf to the node and the right button collapsing the node. All it takes to reproduce the bug is to create a node with depth of 3 and then collapse and expand the root node, upon which the console output shows:

qml: created ObjMain_QMLTYPE_0(0x1e15bb8) with parent QQuickRootItem(0x1e15ca8)
qml: created ObjMain_QMLTYPE_0(0x1e5afc8) with parent ObjMain_QMLTYPE_0(0x1e15bb8)
qml: created ObjMain_QMLTYPE_0(0x1e30f58) with parent ObjMain_QMLTYPE_0(0x1e5afc8)
qml: deleted ObjMain_QMLTYPE_0(0x1e30f58)

The object of the deepest node is deleted for no reason, even though it is parented to the parent node Item and referenced in the JS object in the list model. Attempting to add a new node to the deepest node crashes the program.

The behavior is consistent, regardless of the structure of the tree, only the second level of nodes survives, all deeper nodes are lost when the tree is collapsed.

The fault does not lie in the list model being used as storage, I've tested with a JS array and a QList and the objects are still lost. This example uses a list model merely to save the extra implementation of a C++ model. The sole remedy I found so far was to deny QML ownership of the objects altogether. Although this example produces rather consistent behavior, in production code the spontaneous deletions are often completely arbitrary.

In regard to the garbage collector - I've tested it before, and noticed it is quite liberal - creating and deleting objects a 100 MB of ram worth did not trigger the garbage collection to release that memory, and yet in this case only a few objects, worth a few hundred bytes are being hastily deleted.

According to the documentation, objects which have a parent or are referenced by JS should not be deleted, and in my case, both are valid:

The object is owned by JavaScript. When the object is returned to QML as the return value of a method call, QML will track it and delete it if there are no remaining JavaScript references to it and it has no QObject::parent()

As mentioned in Filip's answer, this does not happen if the objects are created by a function which is not in an object that gets deleted, so it may have something to do with the vaguely mentioned JS state associated with QML objects, but I am essentially still in the dark as of why the deletion happens, so the question is effectively still unanswered.

Any ideas what causes this?

UPDATE: Nine months later still zero development on this critical bug. Meanwhile I discovered several additional scenarios where objects still in use are deleted, scenarios in which it doesn't matter where the object was created and the workaround to simply create the objects in the main qml file doesn't apply. The strangest part is the objects are not being destroyed when they are being "un-referenced" but as they are being "re-referenced". That is, they are not being destroyed when the visual objects referencing them are getting destroyed, but when they are being re-created.

The good news is that it is still possible to set the ownership to C++ even for objects, which are created in QML, so the flexibility of object creation in QML is not lost. There is the minor inconvenience to call a function to protect and delete every object, but at least you avoid the buggy lifetime management of QtQuick. Gotta love the "convenience" of QML though - being forced back to manual object lifetime management.

dtech
  • 47,916
  • 17
  • 112
  • 190
  • Should it be `property ObjMain object` instead of `property Item object` in the `Uimain.qml`? – skypjack Nov 19 '15 at 07:38
  • Moreover, where is the object named `o` the reference of which you use in `Component.onCompleted: Qt.createComponent("Uimain.qml").createObject(p, {"object" : o}) }`? Should it be `object` instead of `o`? – skypjack Nov 19 '15 at 07:40
  • @skypjack - 1 doesn't make any difference, 2 no, in that context `object` would give the child node the same object as the current node, `o` uses the model data to give it the corresponding child node. – dtech Nov 19 '15 at 08:16
  • Maybe it's related to `Component` 's [creation context](http://doc.qt.io/qt-5/qtqml-javascript-dynamicobjectcreation.html#maintaining-dynamically-created-objects) . When `Uimain` is deleted, the creation context, which contains the property `object` that binds to `ObjMain` 's parent, is destroyed and makes `ObjMain` gets deleted, too. This also explains why Filip's answer, that the creation context is `main.qml`, works. – mcchu Nov 22 '15 at 15:03
  • @mcchu - yes, that is pretty much what I mean by the "JS state associated with QML objects", it sure looks that way, but so far I have not found anything in the documentation about it, the doc says if an object is referenced and parented, it should not be collected. – dtech Nov 22 '15 at 15:05
  • I believe @mcchu is right. The linked documentation explicitly states that the creation context of the object *must* outlive the object itself. In the documentation, it says that for `Qt.createComponent()`, the creation context is the current context. In your code, that would be the context related to the ListView or Loader, I believe. As those are being dynamically created at levels above one, the context will be destroyed once the root node is collapsed. Anything created by that context no longer has working bindings, and all bets are off. – arhzu Dec 09 '16 at 20:27
  • @arhzu - the bug persists when objects are created in `main.qml` in the root object, which is the last one to go out. – dtech Dec 09 '16 at 20:44
  • I was able to reproduce the issue and a crash with your example. However, with changes here http://hastebin.com/uviwibanat.cs it seems to work no matter what I try. – arhzu Dec 09 '16 at 22:09
  • As the object tree increases in size and dynamism the object collection fails regardless of the particular method of instantiation. I have found several scenarios where this occurs, and the only solution is to deny qml ownership of the objects. This is just one trivial and minimal example that reproduces the problem. In the actual production code `Component`s are stored in a component cache that persist for the entire duration of the application runtime. – dtech Dec 09 '16 at 22:43
  • The bug you mentioned is fixed for 5.12.0 alpha ;-) – FourtyTwo Sep 26 '18 at 06:03

3 Answers3

2

I've encountered this problem on several occasions, with objects created dynamically, regardless of whether they were created in QML or C++

Objects are only considered for garbage collection if they have JavaScriptOwnership set, which is the case if they are

  1. Directly created by JavaScript/QML
  2. Ownership is explicitly set to JavaScriptOwnership
  3. The object is returned from a Q_INVOKABLE method and didn't have setObjectOwnership() called on it previously.

In all other cases objects are assumed to be owned by C++ and not considered for garbage collection.

At first I assumed it may be an issue with parenting, since I was using QObject derived classes, and the QML method of dynamic instantiation passes an Item for a parent, whereas QtObject doesn't even come with a parent property - it is not exposed from QObject.

The Qt object tree is completely different from the Qml object tree. QML only cares about its own object tree.

    delegate: Item {
        id: p
        width: childrenRect.width
        height: childrenRect.height
        Component.onCompleted: Qt.createComponent("Uimain.qml").createObject(p, {"object" : o})
    }

The combination of dynamically created objects in the onCompleted handler of a delegate is bound to lead to bugs.

When you collapse the tree, the delegates get destroyed, and with them all of their children, which includes your dynamically created objects. It doesn't matter if there are still live references to the children.

Essentially you've provided no stable backing store for the tree - it consists of a bunch of nested delegates which can go away at any time.

Now, there are some situations where QML owned objects are unexpectedly deleted: any C++ references don't count as a ref for the garbage collector; this includes Q_PROPERTYs. In this case, you can:

  1. Set CppOwnership explicitly
  2. Use QPointer<> to hold the reference to deal with objects going away.
  3. Hold an explicit reference to the object in QML.
0

QML is not C++ in a way of managing memory. QML is intended to take care about allocating memory and releasing it. I think the problem you found is just the result of this.

If dynamic object creation goes too deep everything seems to be deleted. So it does not matter that your created objects were a part of the data - they are destroyed too.

Unfortunately my knowledge ends here.

One of the work arounds to the problem (proving my previous statement) is moving the creation of data structure out from the dynamic UI qml files:

  1. Place object creating function for example in main.qml

function createNewObject(parentObject) {
    parentObject.list.append({ "o" : Qt.createComponent("ObjMain.qml").createObject(parentObject) })
}
  1. Use this function instead in your code:

// fragment of the Uimain.qml file
    MouseArea {
        anchors.fill: parent
        acceptedButtons: Qt.RightButton | Qt.LeftButton
        onClicked: {
            if (mouse.button == Qt.RightButton) {
                expanded = !expanded
            } else {
                createNewObject(object)
            }
        }
    }
Filip Hazubski
  • 1,668
  • 1
  • 17
  • 33
  • It says that it doesn't affect `sourceComponent`. I excluded it because of that. Can you explain better why it seems correct? – skypjack Nov 19 '15 at 08:20
  • You misunderstand the code, what is released when the loader is set to inactive is the UI part, not the data part, the UI part merely references the data part, but even after it is released the data is still parented in a object tree and referenced by the model. – dtech Nov 19 '15 at 08:22
  • Furthermore, if what you say had anything to do with it, the second level data nodes would have been lost too, yet it is only 3 level and deeper nodes which are affected. – dtech Nov 19 '15 at 08:26
  • @ddriver For me only first level stays unchanged and from the second everything is destroyed. – Filip Hazubski Nov 19 '15 at 08:36
  • @FilipHazubski - which Qt version are you using? For me second level nodes always survive. – dtech Nov 19 '15 at 08:41
  • @ddriver I use Qt 5.5.0. If what stays is different for me and you I think it is the sign of garbage collector. Maybe you have more RAM or something like this. – Filip Hazubski Nov 19 '15 at 08:43
  • @ddriver Also you said that data is not lost. Maybe I don't understand the code, but it is not only `ObjMain` object that is destroyed. `ListView` is destroyed as well. – Filip Hazubski Nov 19 '15 at 08:46
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/95532/discussion-between-filip-hazubski-and-ddriver). – Filip Hazubski Nov 19 '15 at 08:46
  • @skypjack `sourceComponent` has nothing to do with the `Loader.item`. `sourceComponent` as well as `source` are just a template from which `Loader.item` will be created. – Filip Hazubski Nov 19 '15 at 09:42
  • @ddriver In your code every object dynamically created by the `Loader.item` has parent in scope of the `Loader.item`. That is why I don't think there is anything wrong if these objects are deleted along with the `Loader.item`. – Filip Hazubski Nov 19 '15 at 09:45
  • @FilipHazubski - the objects you speak of are UI objects, none of the `ObjMain` objects are parented to the loader's item. – dtech Nov 19 '15 at 09:51
  • The `ObjMain` tree exists completely independently of the UI tree and is only referenced by the UI elements. – dtech Nov 19 '15 at 09:56
  • @ddriver `ObjMain` objects are not parented to `Loader.item` but they are parented to `Loader.item` custom property (named `object`). – Filip Hazubski Nov 19 '15 at 10:04
  • No, they are not parented, they are simply referenced. Removing a UI object should only subtract 1 from the object's reference count. Even then, the object is still referenced by the model and its parent, so it shouldn't be deleted. If that was the case, the second level nodes would be lost too, since their corresponding UI elements are deleted, but they "survive". – dtech Nov 19 '15 at 10:10
  • If it was `property Item object: ObjMain {}` then the property would "own" the object, but as you can see, there is nothing bound to the property, the object reference comes from the outside, when the UI element is being created. So it shouldn't be deleted with the UI, as it is still parented to another object, and referenced by the JS engine. – dtech Nov 19 '15 at 10:21
  • @ddriver Thanks for the patience. I see it now. – Filip Hazubski Nov 19 '15 at 10:31
  • @ddriver Improved my answer. Hope it helps you :). – Filip Hazubski Nov 19 '15 at 11:33
  • I will check this out, however based on the documentation, this shouldn't be the case: "The object is owned by JavaScript. When the object is returned to QML as the return value of a method call, QML will track it and delete it if there are no remaining JavaScript references to it and it has no QObject::parent()" - The objects have parent and are referenced and are still deleted. – dtech Nov 19 '15 at 11:38
  • @ddriver I agree. Probably you have found a bug. – Filip Hazubski Nov 19 '15 at 11:44
  • 1
    This works, so it another approach to deal with that issue besides using C++ and explicit ownership, unfortunately it doesn't answer the question why are the objects deleted, might have something to do with the JS state associated with QML objects, the documentation is rather vague on the subject. – dtech Nov 20 '15 at 13:11
-1

Create an array inside of a .js file and then create an instance of that array with var myArray = []; on the top-level of that .js. file.

Now you can reference any object that you append to myArray including ones that are created dynamically.

Javascript vars are not deleted by garbage collection as long as they remain defined, so if you define one as a global object then include that Javascript file in your qml document, it will remain as long as the main QML is in scope.


In a file called: backend.js

var tiles = [];

function create_square(new_square) {
    var component = Qt.createComponent("qrc:///src_qml/src_game/Square.qml");
    var sq = component.createObject(background, { "backend" : new_square });
    sq.x = new_square.tile.x
    sq.y = new_square.tile.y
    sq.width = new_square.tile.width;
    sq.height = new_square.tile.height;
    tiles[game.board.getIndex(new_square.tile.row, new_square.tile.col)] = sq;
    sq.visible = true;
}

EDIT :

Let me explain a little more clearly how this could apply to your particular tree example.

By using the line property Item object you are inadvertently forcing it to be a property of Item, which is treated differently in QML. Specifically, properties fall under a unique set of rules in terms of garbage collections, since the QML engine can simply start removing properties of any object to decrease the memory required to run.

Instead, at the top of your QML document, include this line:

import "./object_file.js" as object_file

Then in the file object_file.js , include this line:

 var object_hash = []; 

Now you can use object_hash any time to save your dynamically created components and prevent them from getting wiped out by referencing the

object_file.object_hash

object.

No need to go crazy changing ownership etc

mike510a
  • 2,102
  • 1
  • 11
  • 27