Use a single flush thread for ComputerManager

Some QSettings backends don't like concurrent writes and using a
single thread improves write batching behavior.
This commit is contained in:
Cameron Gutman 2022-10-25 02:07:16 -05:00
parent 59cae610d2
commit 16a9ed682e
2 changed files with 67 additions and 21 deletions

View file

@ -148,7 +148,7 @@ ComputerManager::ComputerManager(QObject *parent)
m_PollingRef(0), m_PollingRef(0),
m_MdnsBrowser(nullptr), m_MdnsBrowser(nullptr),
m_CompatFetcher(nullptr), m_CompatFetcher(nullptr),
m_HostsListDirty(false) m_NeedsDelayedFlush(false)
{ {
QSettings settings; QSettings settings;
@ -164,6 +164,10 @@ ComputerManager::ComputerManager(QObject *parent)
// Fetch latest compatibility data asynchronously // Fetch latest compatibility data asynchronously
m_CompatFetcher.start(); m_CompatFetcher.start();
// Start the delayed flush thread to handle saveHosts() calls
m_DelayedFlushThread = new DelayedFlushThread(this);
m_DelayedFlushThread->start();
// To quit in a timely manner, we must block additional requests // To quit in a timely manner, we must block additional requests
// after we receive the aboutToQuit() signal. This is neccessary // after we receive the aboutToQuit() signal. This is neccessary
// because NvHTTP uses aboutToQuit() to abort requests in progres // because NvHTTP uses aboutToQuit() to abort requests in progres
@ -174,6 +178,18 @@ ComputerManager::ComputerManager(QObject *parent)
ComputerManager::~ComputerManager() ComputerManager::~ComputerManager()
{ {
// Stop the delayed flush thread before acquiring the lock in write mode
// to avoid deadlocking with a flush that needs the lock in read mode.
{
// Wake the delayed flush thread
m_DelayedFlushThread->requestInterruption();
m_DelayedFlushCondition.wakeOne();
// Wait for it to terminate (and finish any pending flush)
m_DelayedFlushThread->wait();
delete m_DelayedFlushThread;
}
QWriteLocker lock(&m_Lock); QWriteLocker lock(&m_Lock);
// Delete machines that haven't been resolved yet // Delete machines that haven't been resolved yet
@ -203,19 +219,27 @@ ComputerManager::~ComputerManager()
} }
} }
class DeferredHostSaveTask : public QRunnable void DelayedFlushThread::run() {
{ while (!QThread::currentThread()->isInterruptionRequested()) {
public: // Wait for a delayed flush request or an interruption
DeferredHostSaveTask(ComputerManager* cm) {
: m_ComputerManager(cm) {} QMutexLocker locker(&m_ComputerManager->m_DelayedFlushMutex);
void run() while (!QThread::currentThread()->isInterruptionRequested() && !m_ComputerManager->m_NeedsDelayedFlush) {
{ m_ComputerManager->m_DelayedFlushCondition.wait(&m_ComputerManager->m_DelayedFlushMutex);
// Only persist the host list if it was dirty. }
//
// NB: We can have multiple save tasks queued up and so another one could // Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
// have persisted the changes already. m_ComputerManager->m_NeedsDelayedFlush = false;
if (m_ComputerManager->m_HostsListDirty.testAndSetAcquire(true, false)) { }
if (QThread::currentThread()->isInterruptionRequested()) {
// Bail without flushing if we woke up for an interruption
break;
}
// Perform the flush
{
QReadLocker lock(&m_ComputerManager->m_Lock); QReadLocker lock(&m_ComputerManager->m_Lock);
QSettings settings; QSettings settings;
@ -229,17 +253,17 @@ public:
settings.endArray(); settings.endArray();
} }
} }
}
private:
ComputerManager* m_ComputerManager;
};
void ComputerManager::saveHosts() void ComputerManager::saveHosts()
{ {
Q_ASSERT(m_DelayedFlushThread != nullptr && m_DelayedFlushThread->isRunning());
// Punt to a worker thread because QSettings on macOS can take ages (> 500 ms) // Punt to a worker thread because QSettings on macOS can take ages (> 500 ms)
// to persist our host list to disk (especially when a host has a bunch of apps). // to persist our host list to disk (especially when a host has a bunch of apps).
m_HostsListDirty.storeRelease(true); QMutexLocker locker(&m_DelayedFlushMutex);
QThreadPool::globalInstance()->start(new DeferredHostSaveTask(this)); m_NeedsDelayedFlush = true;
m_DelayedFlushCondition.wakeOne();
} }
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses) QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)

View file

@ -15,6 +15,25 @@
#include <QSettings> #include <QSettings>
#include <QRunnable> #include <QRunnable>
#include <QTimer> #include <QTimer>
#include <QMutex>
#include <QWaitCondition>
class DelayedFlushThread : public QThread
{
Q_OBJECT
public:
DelayedFlushThread(ComputerManager* cm)
: m_ComputerManager(cm)
{
setObjectName("CM Delayed Flush Thread");
}
void run();
private:
ComputerManager* m_ComputerManager;
};
class MdnsPendingComputer : public QObject class MdnsPendingComputer : public QObject
{ {
@ -167,7 +186,7 @@ class ComputerManager : public QObject
friend class DeferredHostDeletionTask; friend class DeferredHostDeletionTask;
friend class PendingAddTask; friend class PendingAddTask;
friend class PendingPairingTask; friend class PendingPairingTask;
friend class DeferredHostSaveTask; friend class DelayedFlushThread;
public: public:
explicit ComputerManager(QObject *parent = nullptr); explicit ComputerManager(QObject *parent = nullptr);
@ -229,5 +248,8 @@ private:
QMdnsEngine::Cache m_MdnsCache; QMdnsEngine::Cache m_MdnsCache;
QVector<MdnsPendingComputer*> m_PendingResolution; QVector<MdnsPendingComputer*> m_PendingResolution;
CompatFetcher m_CompatFetcher; CompatFetcher m_CompatFetcher;
QAtomicInteger<bool> m_HostsListDirty; DelayedFlushThread* m_DelayedFlushThread;
QMutex m_DelayedFlushMutex;
QWaitCondition m_DelayedFlushCondition;
bool m_NeedsDelayedFlush;
}; };