From 2e4bca949353a88536de6f9c0abc619dc08b2382 Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Thu, 26 Mar 2026 17:51:13 -0700 Subject: [PATCH] Batch transaction notifications, add RPC console filtering, macOS autostart Prevent UI freezes during sync by batching wallet transaction notifications with a 250ms debounce timer and full-refresh fallback for large batches. Disable dynamic sorting and view updates on overview/transaction pages while syncing. Add request/reply/error filter checkboxes to the RPC console with in-memory message store. Implement macOS LaunchAgents-based autostart. Co-Authored-By: Claude Opus 4.6 --- src/qt/forms/rpcconsole.ui | 57 +++++++++++++++ src/qt/guiutil.cpp | 90 +++++++++++++++++++++++ src/qt/overviewpage.cpp | 20 +++++ src/qt/overviewpage.h | 2 + src/qt/rpcconsole.cpp | 110 +++++++++++++++++++++++----- src/qt/rpcconsole.h | 16 ++++ src/qt/transactionview.cpp | 34 ++++++++- src/qt/transactionview.h | 6 +- src/qt/trianglesgui.cpp | 12 ++- src/qt/trianglesgui.h | 2 + src/qt/walletmodel.cpp | 146 +++++++++++++++++++++++++++++++++++-- src/qt/walletmodel.h | 19 +++++ 12 files changed, 488 insertions(+), 26 deletions(-) diff --git a/src/qt/forms/rpcconsole.ui b/src/qt/forms/rpcconsole.ui index c195494..f7a1c79 100644 --- a/src/qt/forms/rpcconsole.ui +++ b/src/qt/forms/rpcconsole.ui @@ -1085,6 +1085,63 @@ QPushButton:!enabled { + + + + 12 + + + + + Show: + + + + + + + Requests + + + true + + + + + + + Replies + + + true + + + + + + + Errors + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 752b745..4f4cf9a 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -405,6 +405,96 @@ bool SetStartOnSystemStartup(bool fAutoStart) } return true; } +#elif defined(Q_OS_MAC) || defined(MAC_OSX) || defined(__APPLE__) + +boost::filesystem::path static GetLaunchAgentsDir() +{ + const QString homeDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + if (homeDir.isEmpty()) + return boost::filesystem::path(); + return boost::filesystem::path(homeDir.toStdString()) / "Library" / "LaunchAgents"; +} + +boost::filesystem::path static GetAutostartFilePath() +{ + return GetLaunchAgentsDir() / "org.triangles.triangles-qt.plist"; +} + +static std::string PlistEscape(const std::string& value) +{ + std::string escaped; + escaped.reserve(value.size()); + for (std::string::const_iterator it = value.begin(); it != value.end(); ++it) + { + switch (*it) + { + case '&': escaped += "&"; break; + case '<': escaped += "<"; break; + case '>': escaped += ">"; break; + case '"': escaped += """; break; + case '\'': escaped += "'"; break; + default: escaped += *it; break; + } + } + return escaped; +} + +bool GetStartOnSystemStartup() +{ + boost::filesystem::ifstream optionFile(GetAutostartFilePath()); + if (!optionFile.good()) + return false; + + std::string contents; + std::string line; + while (getline(optionFile, line)) + contents += line; + optionFile.close(); + + return contents.find("RunAtLoad") != std::string::npos && + contents.find("") != std::string::npos && + contents.find("-min") != std::string::npos; +} + +bool SetStartOnSystemStartup(bool fAutoStart) +{ + if (!fAutoStart) + return !boost::filesystem::exists(GetAutostartFilePath()) || boost::filesystem::remove(GetAutostartFilePath()); + + const QString exePath = QApplication::applicationFilePath(); + if (exePath.isEmpty()) + return false; + + const QString workingDir = QFileInfo(exePath).absolutePath(); + boost::filesystem::create_directories(GetLaunchAgentsDir()); + + boost::filesystem::ofstream optionFile(GetAutostartFilePath(), std::ios_base::out|std::ios_base::trunc); + if (!optionFile.good()) + return false; + + optionFile + << "\n" + << "\n" + << "\n" + << "\n" + << " Label\n" + << " org.triangles.triangles-qt\n" + << " ProgramArguments\n" + << " \n" + << " " << PlistEscape(exePath.toStdString()) << "\n" + << " -min\n" + << " \n" + << " RunAtLoad\n" + << " \n" + << " WorkingDirectory\n" + << " " << PlistEscape(workingDir.toStdString()) << "\n" + << "\n" + << "\n"; + optionFile.close(); + + return optionFile.good(); +} #else // TODO: OSX startup stuff; see: diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 5815861..62e92fb 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -92,6 +92,7 @@ OverviewPage::OverviewPage(QWidget *parent) : currentStake(0), currentUnconfirmedBalance(-1), currentImmatureBalance(-1), + walletTransactionSyncing(false), txdelegate(new TxViewDelegate()), filter(0) { @@ -166,8 +167,10 @@ void OverviewPage::setModel(WalletModel *model) // Keep up to date with wallet setBalance(model->getBalance(), model->getStake(), model->getUnconfirmedBalance(), model->getImmatureBalance()); connect(model, SIGNAL(balanceChanged(qint64, qint64, qint64, qint64)), this, SLOT(setBalance(qint64, qint64, qint64, qint64))); + connect(model, SIGNAL(transactionSyncStateChanged(bool)), this, SLOT(setTransactionSyncState(bool))); connect(model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit())); + setTransactionSyncState(model->isTransactionSyncing()); } // update the display unit, to not use the default ("TRI") @@ -188,6 +191,23 @@ void OverviewPage::updateDisplayUnit() } } +void OverviewPage::setTransactionSyncState(bool syncing) +{ + walletTransactionSyncing = syncing; + if (!filter) + return; + + filter->setDynamicSortFilter(!syncing); + ui->listTransactions->setUpdatesEnabled(!syncing); + + if (!syncing) + { + filter->invalidate(); + filter->sort(TransactionTableModel::Status, Qt::DescendingOrder); + ui->listTransactions->viewport()->update(); + } +} + void OverviewPage::showOutOfSyncWarning(bool fShow) { ui->labelWalletStatus->setVisible(fShow); diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index 6d35830..0a6f038 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -30,6 +30,7 @@ public: public slots: void setBalance(qint64 balance, qint64 stake, qint64 unconfirmedBalance, qint64 immatureBalance); + void setTransactionSyncState(bool syncing); signals: void transactionClicked(const QModelIndex &index); @@ -42,6 +43,7 @@ private: qint64 currentStake; qint64 currentUnconfirmedBalance; qint64 currentImmatureBalance; + bool walletTransactionSyncing; TxViewDelegate *txdelegate; TransactionFilterProxy *filter; diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index ee53b04..880d301 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -17,9 +17,6 @@ #include -// TODO: make it possible to filter out categories (esp debug messages when implemented) -// TODO: receive errors and debug messages through ClientModel - const int CONSOLE_SCROLLBACK = 50; const int CONSOLE_HISTORY = 50; @@ -190,6 +187,7 @@ void RPCExecutor::request(const QString &command) RPCConsole::RPCConsole(QWidget *parent) : QDialog(parent), ui(new Ui::RPCConsole), + clientModel(0), historyPtr(0) { ui->setupUi(this); @@ -259,12 +257,16 @@ bool RPCConsole::eventFilter(QObject* obj, QEvent *event) void RPCConsole::setClientModel(ClientModel *model) { + if (clientModel) + disconnect(clientModel, 0, this, 0); + this->clientModel = model; if(model) { // Subscribe to information, replies, messages, errors connect(model, SIGNAL(numConnectionsChanged(int)), this, SLOT(setNumConnections(int))); connect(model, SIGNAL(numBlocksChanged(int,int)), this, SLOT(setNumBlocks(int,int))); + connect(model, SIGNAL(error(QString,QString,bool)), this, SLOT(showClientError(QString,QString,bool))); // Provide initial values ui->clientVersion->setText(model->formatFullVersion()); @@ -285,14 +287,16 @@ static QString categoryClass(int category) { case RPCConsole::CMD_REQUEST: return "cmd-request"; break; case RPCConsole::CMD_REPLY: return "cmd-reply"; break; - case RPCConsole::CMD_ERROR: return "cmd-error"; break; + case RPCConsole::CMD_ERROR: + case RPCConsole::MC_ERROR: return "cmd-error"; break; default: return "misc"; } } void RPCConsole::clear() { - ui->messagesWidget->clear(); + consoleEntries.clear(); + refreshMessages(); ui->lineEdit->clear(); ui->lineEdit->setFocus(); @@ -323,18 +327,21 @@ void RPCConsole::clear() void RPCConsole::message(int category, const QString &message, bool html) { - QTime time = QTime::currentTime(); - QString timeString = time.toString(); - QString out; - out += ""; - out += ""; - out += "
" + timeString + ""; - if(html) - out += message; - else - out += GUIUtil::HtmlEscape(message, true); - out += "
"; - ui->messagesWidget->append(out); + ConsoleEntry entry; + entry.category = category; + entry.text = message; + entry.time = QTime::currentTime().toString(); + entry.html = html; + + consoleEntries.append(entry); + while (consoleEntries.size() > CONSOLE_SCROLLBACK) + consoleEntries.removeFirst(); + + if (categoryVisible(category)) + { + ui->messagesWidget->append(formatEntry(entry)); + scrollToEnd(); + } } void RPCConsole::setNumConnections(int count) @@ -439,3 +446,72 @@ void RPCConsole::on_showCLOptionsButton_clicked() GUIUtil::HelpMessageBox help; help.exec(); } + +bool RPCConsole::categoryVisible(int category) const +{ + switch (category) + { + case CMD_REQUEST: + return ui->showRequestsCheckBox->isChecked(); + case CMD_ERROR: + case MC_ERROR: + return ui->showErrorsCheckBox->isChecked(); + case CMD_REPLY: + case MC_DEBUG: + default: + return ui->showRepliesCheckBox->isChecked(); + } +} + +QString RPCConsole::formatEntry(const ConsoleEntry &entry) const +{ + QString out; + out += ""; + out += ""; + out += "
" + entry.time + ""; + if (entry.html) + out += entry.text; + else + out += GUIUtil::HtmlEscape(entry.text, true); + out += "
"; + return out; +} + +void RPCConsole::refreshMessages() +{ + ui->messagesWidget->clear(); + for (QList::const_iterator it = consoleEntries.begin(); it != consoleEntries.end(); ++it) + { + if (categoryVisible(it->category)) + ui->messagesWidget->append(formatEntry(*it)); + } + scrollToEnd(); +} + +void RPCConsole::on_showRequestsCheckBox_toggled(bool checked) +{ + Q_UNUSED(checked); + refreshMessages(); +} + +void RPCConsole::on_showRepliesCheckBox_toggled(bool checked) +{ + Q_UNUSED(checked); + refreshMessages(); +} + +void RPCConsole::on_showErrorsCheckBox_toggled(bool checked) +{ + Q_UNUSED(checked); + refreshMessages(); +} + +void RPCConsole::showClientError(const QString &title, const QString &message, bool modal) +{ + Q_UNUSED(modal); + + if (title.isEmpty()) + this->message(MC_ERROR, message); + else + this->message(MC_ERROR, title + ": " + message); +} diff --git a/src/qt/rpcconsole.h b/src/qt/rpcconsole.h index 052f8fc..d938d03 100644 --- a/src/qt/rpcconsole.h +++ b/src/qt/rpcconsole.h @@ -37,6 +37,10 @@ private slots: void on_openDebugLogfileButton_clicked(); /** display messagebox with program parameters (same as triangles-qt --help) */ void on_showCLOptionsButton_clicked(); + void on_showRequestsCheckBox_toggled(bool checked); + void on_showRepliesCheckBox_toggled(bool checked); + void on_showErrorsCheckBox_toggled(bool checked); + void showClientError(const QString &title, const QString &message, bool modal); public slots: void clear(); @@ -55,11 +59,23 @@ signals: void cmdRequest(const QString &command); private: + struct ConsoleEntry + { + int category; + QString text; + QString time; + bool html; + }; + Ui::RPCConsole *ui; ClientModel *clientModel; QStringList history; + QList consoleEntries; int historyPtr; + bool categoryVisible(int category) const; + QString formatEntry(const ConsoleEntry &entry) const; + void refreshMessages(); void startExecutor(); }; diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 21081a5..f37d86c 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -34,7 +34,10 @@ TransactionView::TransactionView(QWidget *parent) : QWidget(parent), model(0), transactionProxyModel(0), ui(new Ui::TransactionsPage), transactionView(0), - transactionsSortOrderDown(true) + transactionsSortOrderDown(true), + walletTransactionSyncing(false), + transactionSortColumn(TransactionTableModel::Status), + transactionSortOrder(Qt::DescendingOrder) { ui->setupUi(this); setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Window); @@ -150,7 +153,7 @@ void TransactionView::setModel(WalletModel *model) transactionView->setSelectionBehavior(QAbstractItemView::SelectRows); transactionView->setSelectionMode(QAbstractItemView::ExtendedSelection); transactionView->setSortingEnabled(true); - transactionView->sortByColumn(TransactionTableModel::Status, Qt::DescendingOrder); + transactionView->sortByColumn(transactionSortColumn, transactionSortOrder); transactionView->verticalHeader()->hide(); transactionView->horizontalHeader()->resizeSection(TransactionTableModel::Status, 23); @@ -162,6 +165,9 @@ void TransactionView::setModel(WalletModel *model) transactionView->horizontalHeader()->setSectionResizeMode(TransactionTableModel::ToAddress, QHeaderView::Stretch); #endif transactionView->horizontalHeader()->resizeSection(TransactionTableModel::Amount, 100); + + connect(model, SIGNAL(transactionSyncStateChanged(bool)), this, SLOT(setTransactionSyncState(bool))); + setTransactionSyncState(model->isTransactionSyncing()); } } @@ -272,6 +278,30 @@ void TransactionView::exportClicked() } } +void TransactionView::setTransactionSyncState(bool syncing) +{ + walletTransactionSyncing = syncing; + if (!transactionProxyModel || !transactionView) + return; + + if (syncing) + { + transactionSortColumn = transactionView->horizontalHeader()->sortIndicatorSection(); + transactionSortOrder = transactionView->horizontalHeader()->sortIndicatorOrder(); + transactionProxyModel->setDynamicSortFilter(false); + transactionView->setSortingEnabled(false); + transactionView->setUpdatesEnabled(false); + return; + } + + transactionProxyModel->setDynamicSortFilter(true); + transactionView->setUpdatesEnabled(true); + transactionView->setSortingEnabled(true); + transactionProxyModel->invalidate(); + transactionView->sortByColumn(transactionSortColumn, transactionSortOrder); + transactionView->viewport()->update(); +} + void TransactionView::contextualMenu(const QPoint &point) { QModelIndex index = transactionView->indexAt(point); diff --git a/src/qt/transactionview.h b/src/qt/transactionview.h index 5c9e514..1c20120 100644 --- a/src/qt/transactionview.h +++ b/src/qt/transactionview.h @@ -48,9 +48,12 @@ public: private: WalletModel *model; TransactionFilterProxy *transactionProxyModel; - Ui::TransactionsPage *ui; + Ui::TransactionsPage *ui; QTableView *transactionView; bool transactionsSortOrderDown; + bool walletTransactionSyncing; + int transactionSortColumn; + Qt::SortOrder transactionSortOrder; QComboBox *dateWidget; QComboBox *typeWidget; @@ -74,6 +77,7 @@ private slots: void copyLabel(); void copyAmount(); void copyTxID(); + void setTransactionSyncState(bool syncing); signals: void doubleClicked(const QModelIndex&); diff --git a/src/qt/trianglesgui.cpp b/src/qt/trianglesgui.cpp index 065d2ec..ed7b6a2 100644 --- a/src/qt/trianglesgui.cpp +++ b/src/qt/trianglesgui.cpp @@ -106,7 +106,8 @@ TrianglesGUI::TrianglesGUI(bool fIsTestnet, QWidget *parent): trayIcon(0), notificator(0), rpcConsole(0), - prevBlocks(0) + prevBlocks(0), + walletTransactionSyncing(false) { ui->setupUi(this); @@ -592,6 +593,8 @@ void TrianglesGUI::setWalletModel(WalletModel *walletModel) setEncryptionStatus(walletModel->getEncryptionStatus()); connect(walletModel, SIGNAL(encryptionStatusChanged(int)), this, SLOT(setEncryptionStatus(int))); + connect(walletModel, SIGNAL(transactionSyncStateChanged(bool)), this, SLOT(setWalletTransactionSyncState(bool))); + setWalletTransactionSyncState(walletModel->isTransactionSyncing()); // Balloon pop-up for new transaction connect(walletModel->getTransactionTableModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), @@ -1039,6 +1042,8 @@ void TrianglesGUI::incomingTransaction(const QModelIndex & parent, int start, in { if(!walletModel || !clientModel) return; + if(walletTransactionSyncing) + return; TransactionTableModel *ttm = walletModel->getTransactionTableModel(); qint64 amount = ttm->index(start, TransactionTableModel::Amount, parent) .data(Qt::EditRole).toULongLong(); @@ -1070,6 +1075,11 @@ void TrianglesGUI::incomingTransaction(const QModelIndex & parent, int start, in } } +void TrianglesGUI::setWalletTransactionSyncState(bool syncing) +{ + walletTransactionSyncing = syncing; +} + void TrianglesGUI::incomingMessage(const QModelIndex & parent, int start, int end) { if(!messageModel) diff --git a/src/qt/trianglesgui.h b/src/qt/trianglesgui.h index 8512e64..dcdedea 100644 --- a/src/qt/trianglesgui.h +++ b/src/qt/trianglesgui.h @@ -143,6 +143,7 @@ private: QMovie *syncIconMovie; /** Keep track of previous number of blocks, to detect progress */ int prevBlocks; + bool walletTransactionSyncing; /** Create the main UI actions. */ void createActions(bool fIsTestnet); @@ -169,6 +170,7 @@ public slots: @see WalletModel::EncryptionStatus */ void setEncryptionStatus(int status); + void setWalletTransactionSyncState(bool syncing); /** Notify the user of an error in the network or transaction handling code. */ void error(const QString &title, const QString &message, bool modal); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 89afa3b..636c04e 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -8,9 +8,15 @@ #include "wallet.h" #include "walletdb.h" // for BackupWallet #include "base58.h" +#include "main.h" #include #include +#include + +static const int MODEL_UPDATE_BATCH_THRESHOLD = 128; +static const int MODEL_UPDATE_BATCH_DELAY_MS = 250; +static const int MODEL_FULL_REFRESH_MIN_INTERVAL_MS = 1500; WalletModel::WalletModel(CWallet *wallet, OptionsModel *optionsModel, QObject *parent) : QObject(parent), wallet(wallet), optionsModel(optionsModel), addressTableModel(0), @@ -18,7 +24,12 @@ WalletModel::WalletModel(CWallet *wallet, OptionsModel *optionsModel, QObject *p cachedBalance(0), cachedStake(0), cachedUnconfirmedBalance(0), cachedImmatureBalance(0), cachedNumTransactions(0), cachedEncryptionStatus(Unencrypted), - cachedNumBlocks(0) + cachedNumBlocks(0), + transactionNotificationFlushQueued(false), + fullTransactionRefreshQueued(false), + transactionSyncing(false), + transactionNotificationTimer(0), + lastFullTransactionRefreshTime(0) { addressTableModel = new AddressTableModel(wallet, this); transactionTableModel = new TransactionTableModel(wallet, this); @@ -28,6 +39,10 @@ WalletModel::WalletModel(CWallet *wallet, OptionsModel *optionsModel, QObject *p connect(pollTimer, SIGNAL(timeout()), this, SLOT(pollBalanceChanged())); pollTimer->start(MODEL_UPDATE_DELAY); + transactionNotificationTimer = new QTimer(this); + transactionNotificationTimer->setSingleShot(true); + connect(transactionNotificationTimer, SIGNAL(timeout()), this, SLOT(flushTransactionNotifications())); + subscribeToCoreSignals(); } @@ -66,6 +81,11 @@ int WalletModel::getNumTransactions() const return numTransactions; } +bool WalletModel::isTransactionSyncing() const +{ + return transactionSyncing; +} + void WalletModel::updateStatus() { EncryptionStatus newEncryptionStatus = getEncryptionStatus(); @@ -108,8 +128,126 @@ bool WalletModel::checkBalanceChanged() void WalletModel::updateTransaction(const QString &hash, int status) { + queueTransactionUpdate(hash, status); +} + +void WalletModel::queueTransactionUpdate(const QString &hash, int status) +{ + bool shouldScheduleFlush = false; + { + QMutexLocker locker(&transactionNotificationMutex); + + int mergedStatus = status; + QMap::iterator it = queuedTransactionNotifications.find(hash); + if (it != queuedTransactionNotifications.end()) + { + // Preserve insert/delete semantics when multiple updates arrive + // for the same transaction before the UI thread drains the queue. + if (it.value() == CT_DELETED || status == CT_DELETED) + mergedStatus = CT_DELETED; + else if (it.value() == CT_NEW || status == CT_NEW) + mergedStatus = CT_NEW; + else + mergedStatus = CT_UPDATED; + it.value() = mergedStatus; + } + else + { + queuedTransactionNotifications.insert(hash, mergedStatus); + } + + if (IsInitialBlockDownload() || queuedTransactionNotifications.size() >= MODEL_UPDATE_BATCH_THRESHOLD) + fullTransactionRefreshQueued = true; + + if (!transactionNotificationFlushQueued) + { + transactionNotificationFlushQueued = true; + shouldScheduleFlush = true; + } + } + + if (shouldScheduleFlush) + QMetaObject::invokeMethod(this, "startTransactionNotificationTimer", Qt::QueuedConnection); +} + +void WalletModel::startTransactionNotificationTimer() +{ + if (transactionNotificationTimer) + transactionNotificationTimer->start(MODEL_UPDATE_BATCH_DELAY_MS); +} + +void WalletModel::flushTransactionNotifications() +{ + QMap pendingNotifications; + bool refreshAll = false; + bool shouldRescheduleRefresh = false; + { + QMutexLocker locker(&transactionNotificationMutex); + pendingNotifications.swap(queuedTransactionNotifications); + refreshAll = fullTransactionRefreshQueued; + fullTransactionRefreshQueued = false; + transactionNotificationFlushQueued = false; + } + + const bool shouldSync = refreshAll || + IsInitialBlockDownload() || + pendingNotifications.size() >= MODEL_UPDATE_BATCH_THRESHOLD; + if (transactionSyncing != shouldSync) + { + transactionSyncing = shouldSync; + emit transactionSyncStateChanged(transactionSyncing); + } + if(transactionTableModel) - transactionTableModel->updateTransaction(hash, status); + { + if (refreshAll) + { + const qint64 now = GetTimeMillis(); + if (transactionSyncing && + lastFullTransactionRefreshTime != 0 && + now - lastFullTransactionRefreshTime < MODEL_FULL_REFRESH_MIN_INTERVAL_MS) + { + QMutexLocker locker(&transactionNotificationMutex); + fullTransactionRefreshQueued = true; + if (!transactionNotificationFlushQueued) + { + transactionNotificationFlushQueued = true; + shouldRescheduleRefresh = true; + } + } + else + { + transactionTableModel->refreshWallet(); + lastFullTransactionRefreshTime = now; + } + } + else + { + for (QMap::const_iterator it = pendingNotifications.begin(); it != pendingNotifications.end(); ++it) + transactionTableModel->updateTransaction(it.key(), it.value()); + } + } + + if (shouldRescheduleRefresh && transactionNotificationTimer) + transactionNotificationTimer->start(MODEL_FULL_REFRESH_MIN_INTERVAL_MS); + + bool stillPending = false; + { + QMutexLocker locker(&transactionNotificationMutex); + stillPending = transactionNotificationFlushQueued || !queuedTransactionNotifications.isEmpty(); + } + + if (!transactionSyncing && pendingNotifications.size() >= MODEL_UPDATE_BATCH_THRESHOLD) + { + transactionSyncing = true; + emit transactionSyncStateChanged(true); + } + + if (transactionSyncing && !IsInitialBlockDownload() && !stillPending && pendingNotifications.size() < MODEL_UPDATE_BATCH_THRESHOLD) + { + transactionSyncing = false; + emit transactionSyncStateChanged(false); + } // Don't call checkBalanceChanged() here - it does LOCK(cs_wallet) + iterates // all wallet transactions, blocking the UI thread. The pollBalanceChanged() @@ -392,9 +530,7 @@ static void NotifyAddressBookChanged(WalletModel *walletmodel, CWallet *wallet, static void NotifyTransactionChanged(WalletModel *walletmodel, CWallet *wallet, const uint256 &hash, ChangeType status) { OutputDebugStringF("NotifyTransactionChanged %s status=%i\n", hash.GetHex().c_str(), status); - QMetaObject::invokeMethod(walletmodel, "updateTransaction", Qt::QueuedConnection, - Q_ARG(QString, QString::fromStdString(hash.GetHex())), - Q_ARG(int, status)); + walletmodel->queueTransactionUpdate(QString::fromStdString(hash.GetHex()), status); } void WalletModel::subscribeToCoreSignals() diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 95e2197..17d233a 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "allocators.h" /* for SecureString */ @@ -72,6 +74,7 @@ public: qint64 getImmatureBalance() const; int getNumTransactions() const; EncryptionStatus getEncryptionStatus() const; + bool isTransactionSyncing() const; // Check address for validity bool validateAddress(const QString &address); @@ -160,10 +163,16 @@ public slots: void updateStatus(); /* New transaction, or transaction changed status */ void updateTransaction(const QString &hash, int status); + /* Queue a transaction update from a core thread without touching the UI directly */ + void queueTransactionUpdate(const QString &hash, int status); /* New, updated or removed address book entry */ void updateAddressBook(const QString &address, const QString &label, bool isMine, int status); /* Current, immature or unconfirmed balance might have changed - emit 'balanceChanged' if so */ void pollBalanceChanged(); + /* Start or restart the deferred transaction notification flush timer */ + void startTransactionNotificationTimer(); + /* Flush queued transaction notifications from core threads */ + void flushTransactionNotifications(); signals: // Signal that balance in wallet changed @@ -182,6 +191,16 @@ signals: // Asynchronous error notification void error(const QString &title, const QString &message, bool modal); + void transactionSyncStateChanged(bool syncing); + +private: + QMutex transactionNotificationMutex; + QMap queuedTransactionNotifications; + bool transactionNotificationFlushQueued; + bool fullTransactionRefreshQueued; + bool transactionSyncing; + QTimer *transactionNotificationTimer; + qint64 lastFullTransactionRefreshTime; };