After #14193 ClientModel::updateTimer can take some time, as such the GUI hangs, like #17112.
Fixes this by polling in a background thread and updating the GUI asynchronously.
Elegant Solution.
Concept ACK.
In the long run, we should probably also improve the 250ms polling on WalletModel::pollBalanceChanged()
Will test.
also improve the 250ms polling
You mean remove polling? :trollface:
89 | @@ -90,7 +90,9 @@ class ClientModel : public QObject 90 | PeerTableModel *peerTableModel; 91 | BanTableModel *banTableModel; 92 | 93 | - QTimer *pollTimer; 94 | + //! A thread to interact with m_node asynchronously 95 | + QThread* const thread;
QThread* const m_thread;
:eyes: ?
Sorry, done.
Concept ACK
Put in a gitian build, but I no longer have a windows box for testing
108 | -{ 109 | - // no locking required at this point 110 | - // the following calls will acquire the required lock 111 | - Q_EMIT mempoolSizeChanged(m_node.getMempoolSize(), m_node.getMempoolDynamicUsage()); 112 | - Q_EMIT bytesChanged(m_node.getTotalBytesRecv(), m_node.getTotalBytesSent()); 113 | -}
moving this code into a lambda is stylistic only, right?
I could keep this here but the connection from the timer needs to be direct (not auto) so that this would run in the timer's thread.
Also, this is now unreachable, and it's a small step towards removing polling.
ACK 8f0d8bd17a575a4235296d82cda80ba268a01dba
Didn't test on windows to confirm this actually reduces the "not responding" pop ups, nor that the mempool size is still updated
<!--a722867cd34abeea1fadc8d60700f111-->
Gitian builds for commit ec3ed5a4487886f1c2a35fda0a3289be7b280248 (master):
f64e5b9561bbfd24763ca216dfb72e6c... bitcoin-0.19.99-aarch64-linux-gnu-debug.tar.gz3450cd39da524d4259d8971fa07f7cd3... bitcoin-0.19.99-aarch64-linux-gnu.tar.gzf9c08aecb54c510abc1c49a0f615cc47... bitcoin-0.19.99-arm-linux-gnueabihf-debug.tar.gz48858aa869f72b05f6fd77a174cec72d... bitcoin-0.19.99-arm-linux-gnueabihf.tar.gz2f3d551fa3a0744ced807e575f1e4643... bitcoin-0.19.99-i686-pc-linux-gnu-debug.tar.gz61eef7a7416e41bd49f11481339df776... bitcoin-0.19.99-i686-pc-linux-gnu.tar.gzbefb99d795c38a2f320e99e677e437fb... bitcoin-0.19.99-osx-unsigned.dmgaadf3d3811d300b416a2e3a36e5d9636... bitcoin-0.19.99-osx64.tar.gz75ad9c5b79ef283885def690f4210e28... bitcoin-0.19.99-riscv64-linux-gnu-debug.tar.gz0bdc56205b7a686505b49d9694dfbb7d... bitcoin-0.19.99-riscv64-linux-gnu.tar.gz0ed997185c1a65c6ab29882306a5c4ed... bitcoin-0.19.99-win64-debug.zipe642bb08c080af778aad7ab1bc889769... bitcoin-0.19.99-win64-setup-unsigned.exe471525f13e4ee98129b3e140175776a5... bitcoin-0.19.99-win64.zipa2d38074e8a186c6251fbb30948caf6f... bitcoin-0.19.99-x86_64-linux-gnu-debug.tar.gz544d75a68b31657850b22296599a3d3f... bitcoin-0.19.99-x86_64-linux-gnu.tar.gzc780c4b108f18250dfaa13a06d7ded80... bitcoin-0.19.99.tar.gz42923b2fe3a4f72ea620650c3be0a040... bitcoin-core-linux-0.20-res.yml15e9a3c42739179b937c4bb927c02731... bitcoin-core-osx-0.20-res.yml4f45465e66f847f97cfd6af3c2bd3514... bitcoin-core-win-0.20-res.ymla34a94c2dee07af19dfd80a405f39c1d... linux-build.loge0a214ba922d8febd8566d7102414cc0... osx-build.logbfb37291238fa4be8727e7c882d422e0... win-build.logGitian builds for commit 6661358a0c082fd50378fe6fc88dddbd0566ebf0 (master and this pull):
bd75e5285ba3454089eea66851951a3e... bitcoin-0.19.99-aarch64-linux-gnu-debug.tar.gz847ad3dbf8e9f1c9b9d3826d534258a6... bitcoin-0.19.99-aarch64-linux-gnu.tar.gzabc3a5164207550aeaf56721f32edd2c... bitcoin-0.19.99-arm-linux-gnueabihf-debug.tar.gz45bb13636a019270312cba6197ffd32c... bitcoin-0.19.99-arm-linux-gnueabihf.tar.gzfb0a6b56c92ac730da300429a600b9db... bitcoin-0.19.99-i686-pc-linux-gnu-debug.tar.gz0bd7258f275d4d34ad07d4ec905b1b3f... bitcoin-0.19.99-i686-pc-linux-gnu.tar.gz4d9ca8b60fc979544cb99ffc4e232484... bitcoin-0.19.99-osx-unsigned.dmgbdfe572383c66989ca8a463ed1ee48af... bitcoin-0.19.99-osx64.tar.gz7b986c3b3cf8a6b3c7cbbe5d442a27dc... bitcoin-0.19.99-riscv64-linux-gnu-debug.tar.gzf5bb3c29e722f8e092d4660dc168d777... bitcoin-0.19.99-riscv64-linux-gnu.tar.gzfd1b3e6297f2e86cd2df663c3f6d6f92... bitcoin-0.19.99-win64-debug.zip773470fe260bc71e17da117b37c5e9ff... bitcoin-0.19.99-win64-setup-unsigned.exe7945027d5a1e6ffb14e07cb2b5816dab... bitcoin-0.19.99-win64.zip00c4bc060177f9a8969740538cb9c9b2... bitcoin-0.19.99-x86_64-linux-gnu-debug.tar.gz3945984613ad6cdd1d229cc1e63af38f... bitcoin-0.19.99-x86_64-linux-gnu.tar.gz8cc01845714d8ad70ff969ca8d518d02... bitcoin-0.19.99.tar.gzffee7001f20c1c3ff9c3502c2e00f692... bitcoin-core-linux-0.20-res.yml37d12c597b73b9f31a3aa1ff1c9b4c4f... bitcoin-core-linux-0.20-res.yml.difff0a73b15111d52d814864e7dbb376e9a... bitcoin-core-osx-0.20-res.yml3381d609d590467fa206e35d2cf70081... bitcoin-core-osx-0.20-res.yml.diffeb691454dfca0bc139a483c4a51bd49a... bitcoin-core-win-0.20-res.ymle56f284e2044226f0d5e96a2c2ac2f3b... bitcoin-core-win-0.20-res.yml.diffb9636440f209ed05e6cc33613d049c82... linux-build.log3de26d2981536c352e0e25d6a7d6a066... linux-build.log.diff4c64ce4e05da72a2d5c6bf6a737d76b5... osx-build.logf761b312fafeaa275c9c53073d68cce3... osx-build.log.diff695226feb3f04e98b30265d3f29f1dc9... win-build.log41ce19eae944a02dac666d0c41f5d6e3... win-build.log.diff55 | @@ -46,6 +56,10 @@ ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QO 56 | ClientModel::~ClientModel() 57 | { 58 | unsubscribeFromCoreSignals(); 59 | + 60 | + delete pollTimer;
why is the timer deleted before the thread stops and not the other way around?
What is the issue?
I don't know if there is an issue or not, I am just wondering if there is any specific reason for this ordering. Mostly because I saw this comment.
Thanks for pointing it, there's indeed an issue here - must be deleteLater of move after joining the other thread.
Fixed.
I can confirm that 8f0d8bd makes the unresponsive behavior go away on macOS 10.15 Catalina (e.g. the file menu now works at launch).
@MarcoFalke @Sjors force push with
--- a/src/qt/clientmodel.cpp
+++ b/src/qt/clientmodel.cpp
@@ -57,9 +57,9 @@ ClientModel::~ClientModel()
{
unsubscribeFromCoreSignals();
- delete pollTimer;
m_thread->quit();
m_thread->wait();
+ delete pollTimer;
}
int ClientModel::getNumConnections(unsigned int flags) const
"Moved to another thread" isn't really "asynchronous", but Concept ACK
43 | + 44 | + pollTimer = new QTimer; 45 | + // move timer to thread so that polling doesn't disturb event loop 46 | + pollTimer->moveToThread(m_thread); 47 | + m_thread->start(); 48 | + connect(pollTimer, &QTimer::timeout, [this] {
Can't you leave ClientModel::updateTimer as-is for now? Or at least move the code movement to another commit?
Yes but I'd have to connect with Qt::DirectConnection, otherwise the slot would be called in the GUI event loop.
Even after moveToThread? Because of the slot being on a GUI object?
Yes, with auto connection (the default) the signal checks if the target's thread is the same as the sender or not, if not then it's the same as queued connection.
I think you should do this connect before moving the timer to the thread? (or maybe not, I'm not sure here…)
QObject::connect is thread safe.
39 | peerTableModel = new PeerTableModel(m_node, this); 40 | banTableModel = new BanTableModel(m_node, this); 41 | - pollTimer = new QTimer(this); 42 | - connect(pollTimer, &QTimer::timeout, this, &ClientModel::updateTimer); 43 | + 44 | + pollTimer = new QTimer;
Why can't this use QTimer(m_thread)?
Because Qt threading model :trollface:
Being a child of QThread does nothing other than be owned by it. To have it's events processed in the thread then moveToThread must be used. This also works only if the object being moved has no parent.
why can't this be a unique_ptr?
I thought it deleted children when being deleted itself?
You can't have a tree of objects with different thread affinity.
why can't this be a unique_ptr?
It can, but it requires more changes, which are unnecessary in the scope of this PR.
tested ACK 8f0d8bd
Built, ran tests, some manual testing while running for 24h on macOS.
Please add a description. I got the context from the core dev logs but there should be some information here as well.
55 | @@ -46,6 +56,10 @@ ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QO 56 | ClientModel::~ClientModel() 57 | { 58 | unsubscribeFromCoreSignals(); 59 | + 60 | + m_thread->quit(); 61 | + m_thread->wait(); 62 | + delete pollTimer;
As discussed on IRC: I don't think deleting the timer in the main thread, after having moved it to another thread is safe. Qt objects need to be deleted in the thread that owns them. I think that that thread doesn't exist anymore here doesn't change that. You're not even supposed to hold on to the pointer.
I think you can do this before moving it to the thread to make sure it's cleaned up:
connect(&thread, &QThread::finished, pollTimer, &QTimer::deleteLater);
(like in RPCConsole::startExecutor())
Done.
Updated after IRC discussion.
49 | + // the following calls will acquire the required lock 50 | + Q_EMIT mempoolSizeChanged(m_node.getMempoolSize(), m_node.getMempoolDynamicUsage()); 51 | + Q_EMIT bytesChanged(m_node.getTotalBytesRecv(), m_node.getTotalBytesSent()); 52 | + }); 53 | + connect(m_thread, &QThread::finished, timer, &QObject::deleteLater); 54 | + connect(m_thread, &QThread::started, timer, static_cast<void (QTimer::*)()>(&QTimer::start));
The cast is needed because QTimer::start is overloaded.
^ can you put that in a source code comment?
could this work instead of the scary cast ?
connect(m_thread, &QThread::started, timer, [timer] {
timer->start();
});
Yes that works, and the following also works:
connect(m_thread, &QThread::started, [timer] {
timer->start();
});
because started is emitted from the associated thread.
Much better!
ACK 092d91f6196af497b9618418f84527f552ef1154. Tested without a wallet (bitcoin-qt -nowallet) by calling invalidateblock and later reconsiderblock. Without this commit QT is very unresponsive during sync. The original issue seems less pronounced with a wallet.
Would it make sense to create a NodePoller object, which handles the timer life cycle? Or is that overkill if the plan is to get rid of polling asap anyway?
Would it make sense to create a NodePoller object, which handles the timer life cycle? Or is that overkill if the plan is to get rid of polling asap anyway?
I think that as long as polling is necessary (and I don't believe we will completely get rid of it any time soon) it would make sense to have one thread to do all polling, and not, say, create a thread per WalletModel too.
But it's out of scope for this PR.
With this change polling runs in a different thread to prevent
disturbing the event loop.
48 | + // no locking required at this point 49 | + // the following calls will acquire the required lock 50 | + Q_EMIT mempoolSizeChanged(m_node.getMempoolSize(), m_node.getMempoolDynamicUsage()); 51 | + Q_EMIT bytesChanged(m_node.getTotalBytesRecv(), m_node.getTotalBytesSent()); 52 | + }); 53 | + connect(m_thread, &QThread::finished, timer, &QObject::deleteLater);
Ops, I think this is wrong - deleteLater isn't called because the thread's event loop is not running. I'll check this.
Nope, all good! From https://doc.qt.io/qt-5/qthread.html#finished:
When this signal is emitted, the event loop has already stopped running. No more events will be processed in the thread, except for deferred deletion events. This signal can be connected to QObject::deleteLater(), to free objects in that thread.
Phew, good to know (I've read that once but forgot)
ACK 6b6be41c36e4fe9a74bed50e7f0a06532ab1260b
Code review re-ACK 6b6be41; only replaced the scary cast with { timer->start(); }
Milestone
0.19.0