Merge lp:~lukas-kde/gsettings-qt/queued-processing into lp:gsettings-qt

Proposed by Lukáš Tinkl on 2015-05-22
Status: Merged
Approved by: Lars Karlitski on 2015-06-02
Approved revision: 76
Merged at revision: 72
Proposed branch: lp:~lukas-kde/gsettings-qt/queued-processing
Merge into: lp:gsettings-qt
Diff against target: 137 lines (+65/-6)
7 files modified
GSettings/gsettings-qml.cpp (+1/-1)
GSettings/gsettings-qml.h (+0/-2)
gsettings-qt.pro (+1/-1)
src/qgsettings.cpp (+2/-1)
tests/cpptest.cpp (+35/-0)
tests/cpptest.pro (+18/-0)
tests/tst_GSettings.qml (+8/-1)
To merge this branch: bzr merge lp:~lukas-kde/gsettings-qt/queued-processing
Reviewer Review Type Date Requested Status
Lars Karlitski (community) 2015-05-22 Approve on 2015-06-02
Nick Dedekind (community) Approve on 2015-06-02
Review via email: mp+259883@code.launchpad.net

Commit Message

Force handling deferred delete events to avoid memory leaks

Description of the Change

DeferredDelete events that are posted from a handler directly dispatched by glib are not run until the program exits, because Qt doesn't see them as being in the right main loop level. Force handling them here to avoid leaks until there's a fix to https://bugreports.qt.io/browse/QTBUG-32859

To post a comment you must log in.
Lars Karlitski (larsu) wrote :

Thanks for the patch.

Which two event loops are you talking about? Do we have a memory leak without this change? Please provide more detail.

> the latter doesn't work reliably with the in-memory backend

In what way is this unreliable? I added the changed signal because valueChanged is not emitted in all cases [1].

Also, please separate the unrelated changes into a separate commit.

[1] http://bazaar.launchpad.net/~system-settings-touch/gsettings-qt/trunk/revision/19

review: Needs Information
Lukáš Tinkl (lukas-kde) wrote :

> Thanks for the patch.
>
> Which two event loops are you talking about? Do we have a memory leak without
> this change? Please provide more detail.

There's a glib event loop handling integrated in Qt; we don't have a memory leak exactly but as
with mzanetti's previous work, the resulting QML Components are never freed when using a Loader.

> > the latter doesn't work reliably with the in-memory backend
>
> In what way is this unreliable? I added the changed signal because
> valueChanged is not emitted in all cases [1].

Try running the testcase, if you use the valueChanged() signal, it indeed does get emitted in all cases (unlike what the documentation says unfortunately)

> [1] http://bazaar.launchpad.net/~system-settings-touch/gsettings-
> qt/trunk/revision/19

I removed the updateKey() declaration because the implementation seems to be gone, was that an oversight?

Lukáš Tinkl (lukas-kde) wrote :

> In what way is this unreliable? I added the changed signal because

To clarify this a bit further, and this is just for the testcase, it seems that the changed signal is not emitted when using the in-memory backend gsettings backend.

Michael Zanetti (mzanetti) wrote :

Hi Lars, after some investigation we're quite sure that the changed events from gsettings are not posted to the normal Qt event loop but instead handled in a different context. This can cause all sorts of issues in Qt, one of them is the Loader issue we've been discussing in Belgium. Maybe you can think of a cleaner way to do it, but I really think we need this detaching of the signal emissions somehow.

Lars Karlitski (larsu) wrote :

I know this is on your plate right now and possibly blocking other stuff, but I can't in good conscience merge something based on conjecture.

There's a lot of integration points with glib. If there's some underlying issue with how qt uses the main loop, we ought to at least know what's going on.

Can you please provide that test case again and a more detailed explanation of what you've found so far? I might be able to have another look at this soon.

Sorry...

Michael Zanetti (mzanetti) wrote :

Here's the example: http://paste.ubuntu.com/11494214/

Run it and then use

gsettings set com.canonical.Unity8 usage-mode Windowed

and

gsettings set com.canonical.Unity8 usage-mode Staged

To switch between them. See how the loader leaks its item with every toggle. With everything else the Loader behaves correctly, it's really just an issue when it's hooked up to gsettings because the changed event happens in a different context than what Qt expects.

Lars Karlitski (larsu) wrote :

Thanks. I've investigated a bit and found that QEventLoop has a concept of "levels" and deferred delete events are only processed for when a given level finishes running. Calling deleteLater() from the gsettings changed signal (which runs in the level 0) makes the deferred event not run until the program exits.

There's a qt bug open about this, from ~nick-dedekind:

  https://bugreports.qt.io/browse/QTBUG-32859

Queuing the signal fixes the bug as that queues it onto the correct level. That's an ok workaround, but I'd like to be more explicit and call

  QCoreApplication::sendPostedEvents(NULL, QEvent::DeferredDelete);

in GSettingsQml::settingChanged, with a comment explaining why we need to do this and a link to the upstream bug.

Of course, I'd much prefer if someone fixed that upstream ;)

review: Needs Fixing
Lukáš Tinkl (lukas-kde) wrote :

OK, fixed using QCoreApplication::sendPostedEvents(), auto tests passing, and solves the original issue as well.

Lars Karlitski (larsu) wrote :

Thanks for the quick fix.

There's nothing to sync up with. Please amend the comment to something like this: "DeferredDelete events that are posted from a handler directly dispatched by glib are not run until the program exits, because Qt doesn't see them as being in the right main loop level. Force handling them here to avoid leaks until there's a fix to https://bugreports.qt.io/browse/QTBUG-32859"

And change the description and commit message of this merge request.

Lars Karlitski (larsu) wrote :

Thanks!

review: Approve
Nick Dedekind (nick-dedekind) wrote :

Hm. not entirely sure about this.

Doesn't sending posted DeleteLater events directly break the semantics of DeleteLater?
In unitymenumodel I fixed it by queuing the events from glib loop. I'm not sure if gsetting uses synchronous sets or not? (ie. a set synchronously causes an update which we could rely on?) maybe larsu can help here.

Maybe call QMetaObject::invoke(self, "settingChanged", Qt::QueuedConnection) in qgsettings.cpp?

review: Needs Information
Lars Karlitski (larsu) wrote :

> Hm. not entirely sure about this.
>
> Doesn't sending posted DeleteLater events directly break the semantics of
> DeleteLater?

You're right, it does. Thanks for pointing that out.

> Maybe call QMetaObject::invoke(self, "settingChanged", Qt::QueuedConnection)
> in qgsettings.cpp?

I didn't know you could force queued connections. Let's do that.

75. By Lukáš Tinkl on 2015-06-02

add a C++ test for the deferred deletion case

76. By Lukáš Tinkl on 2015-06-02

rename C++ test to cpptest (both target and the source)

Nick Dedekind (nick-dedekind) wrote :

Code looks good.
Tests pass.

All fine my me.

review: Approve
Lars Karlitski (larsu) wrote :

Very cool. Thanks for chiming in Nick.

review: Approve
77. By Lukáš Tinkl on 2015-07-28

fix building the Cpp test in CI

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'GSettings/gsettings-qml.cpp'
2--- GSettings/gsettings-qml.cpp 2014-06-23 14:55:54 +0000
3+++ GSettings/gsettings-qml.cpp 2015-07-28 14:56:19 +0000
4@@ -137,7 +137,7 @@
5
6 connect(priv->settings, SIGNAL(changed(const QString &)), this, SLOT(settingChanged(const QString &)));
7
8- Q_FOREACH(QString key, priv->settings->keys())
9+ Q_FOREACH(const QString &key, priv->settings->keys())
10 this->insert(key, priv->settings->get(key));
11
12 Q_EMIT(schemaChanged());
13
14=== modified file 'GSettings/gsettings-qml.h'
15--- GSettings/gsettings-qml.h 2014-04-08 09:28:52 +0000
16+++ GSettings/gsettings-qml.h 2015-07-28 14:56:19 +0000
17@@ -61,8 +61,6 @@
18
19 GSettingsSchemaQml * schema() const;
20
21- void updateKey(const char *gkey, bool emitChanged);
22-
23 void classBegin();
24 void componentComplete();
25
26
27=== modified file 'gsettings-qt.pro'
28--- gsettings-qt.pro 2013-07-19 14:22:57 +0000
29+++ gsettings-qt.pro 2015-07-28 14:56:19 +0000
30@@ -1,2 +1,2 @@
31 TEMPLATE = subdirs
32-SUBDIRS += src/gsettings-qt.pro GSettings/gsettings-qt.pro tests/tests.pro
33+SUBDIRS += src/gsettings-qt.pro GSettings/gsettings-qt.pro tests/tests.pro tests/cpptest.pro
34
35=== modified file 'src/qgsettings.cpp'
36--- src/qgsettings.cpp 2014-06-23 13:29:16 +0000
37+++ src/qgsettings.cpp 2015-07-28 14:56:19 +0000
38@@ -36,7 +36,8 @@
39 {
40 QGSettings *self = (QGSettings *)user_data;
41
42- Q_EMIT(self->changed(qtify_name(key)));
43+ // work around https://bugreports.qt.io/browse/QTBUG-32859 and http://pad.lv/1460970
44+ QMetaObject::invokeMethod(self, "changed", Qt::QueuedConnection, Q_ARG(QString, qtify_name(key)));
45 }
46
47 QGSettings::QGSettings(const QByteArray &schema_id, const QByteArray &path, QObject *parent):
48
49=== added file 'tests/cpptest.cpp'
50--- tests/cpptest.cpp 1970-01-01 00:00:00 +0000
51+++ tests/cpptest.cpp 2015-07-28 14:56:19 +0000
52@@ -0,0 +1,35 @@
53+#include <QtTest/QtTest>
54+
55+#include <QGSettings>
56+
57+class TestDeferredDelete: public QObject
58+{
59+ Q_OBJECT
60+private slots:
61+ void initTestCase();
62+ void test_deferredDelete();
63+private:
64+ QGSettings * settings;
65+ QPointer<QObject> dummy;
66+};
67+
68+void TestDeferredDelete::initTestCase()
69+{
70+ settings = new QGSettings("com.canonical.gsettings.Test", QByteArray(), this);
71+ dummy = new QObject;
72+ connect(settings, &QGSettings::changed, dummy.data(), &QObject::deleteLater); // delete the dummy object upon any gsettings change
73+}
74+
75+void TestDeferredDelete::test_deferredDelete()
76+{
77+ QSignalSpy spy(dummy.data(), &QObject::destroyed); // watch the dummy object get destroyed
78+ settings->set("testString", "bar");
79+
80+ QVERIFY(spy.wait(1));
81+ QVERIFY(dummy.isNull()); // verify dummy got destroyed for real
82+ QCOMPARE(settings->get("testString").toString(), QStringLiteral("bar")); // also verify the setting got written by reading it back
83+}
84+
85+QTEST_MAIN(TestDeferredDelete)
86+
87+#include "cpptest.moc"
88
89=== added file 'tests/cpptest.pro'
90--- tests/cpptest.pro 1970-01-01 00:00:00 +0000
91+++ tests/cpptest.pro 2015-07-28 14:56:19 +0000
92@@ -0,0 +1,18 @@
93+TEMPLATE = app
94+QT += testlib
95+QT -= gui
96+CONFIG += testcase link_pkgconfig
97+TARGET = cpptest
98+IMPORTPATH = $$PWD/..
99+SOURCES = cpptest.cpp
100+INCLUDEPATH += $$(PWD)/../src
101+LIBS += -L$$(PWD)/../src -lgsettings-qt
102+
103+schema.target = gschemas.compiled
104+schema.commands = glib-compile-schemas $$PWD
105+schema.depends = com.canonical.gsettings.test.gschema.xml
106+QMAKE_EXTRA_TARGETS += schema
107+PRE_TARGETDEPS = gschemas.compiled
108+
109+# qmake prepends this to the command line executed by `make check`
110+check.commands = QT_QPA_PLATFORM=minimal LD_LIBRARY_PATH=$$PWD/../src GSETTINGS_BACKEND=memory GSETTINGS_SCHEMA_DIR=$$PWD
111
112=== modified file 'tests/tst_GSettings.qml'
113--- tests/tst_GSettings.qml 2014-08-01 16:47:07 +0000
114+++ tests/tst_GSettings.qml 2015-07-28 14:56:19 +0000
115@@ -11,8 +11,14 @@
116 GSettings {
117 id: settings
118 schema.id: "com.canonical.gsettings.Test"
119+ // has to be "valueChanged" signal, not "changed"; the latter doesn't work reliably with the in-memory gsettings backend
120+ onValueChanged: changes.push([key, value]);
121+ }
122
123- onChanged: changes.push([key, value]);
124+ SignalSpy {
125+ id: changesSpy
126+ target: settings
127+ signalName: "changed"
128 }
129
130 GSettings {
131@@ -90,6 +96,7 @@
132 function test_reset() {
133 settings.testInteger = 4;
134 settings.schema.reset('testInteger');
135+ tryCompare(changesSpy, "count", 1); // this merely exists only to connect to the "changed" signal; resetting changes is async in gsettings
136 compare(settings.testInteger, 42);
137 }
138

Subscribers

People subscribed via source and target branches

to all changes: