qt: Move transaction notification to transaction table model

Move transaction new/update notification to TransactionTableModel.
This moves the concerns to where they're actually handled.
No need to bounce this through wallet model.

- Do wallet transaction preprocessing on signal handler side;
  avoids locking cs_main/cs_wallet on notification in GUI thread
  (except for new transactions)
This commit is contained in:
Wladimir J. van der Laan 2014-10-28 19:52:21 +01:00
parent cd9114e513
commit 023e63df78
5 changed files with 160 additions and 111 deletions

View file

@ -91,87 +91,80 @@ public:
Call with transaction that was added, removed or changed. Call with transaction that was added, removed or changed.
*/ */
void updateWallet(const uint256 &hash, int status) void updateWallet(const uint256 &hash, int status, bool showTransaction)
{ {
qDebug() << "TransactionTablePriv::updateWallet : " + QString::fromStdString(hash.ToString()) + " " + QString::number(status); qDebug() << "TransactionTablePriv::updateWallet : " + QString::fromStdString(hash.ToString()) + " " + QString::number(status);
// Find bounds of this transaction in model
QList<TransactionRecord>::iterator lower = qLowerBound(
cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
QList<TransactionRecord>::iterator upper = qUpperBound(
cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
int lowerIndex = (lower - cachedWallet.begin());
int upperIndex = (upper - cachedWallet.begin());
bool inModel = (lower != upper);
if(status == CT_UPDATED)
{ {
LOCK2(cs_main, wallet->cs_wallet); if(showTransaction && !inModel)
status = CT_NEW; /* Not in model, but want to show, treat as new */
if(!showTransaction && inModel)
status = CT_DELETED; /* In model, but want to hide, treat as deleted */
}
// Find transaction in wallet qDebug() << " inModel=" + QString::number(inModel) +
std::map<uint256, CWalletTx>::iterator mi = wallet->mapWallet.find(hash); " Index=" + QString::number(lowerIndex) + "-" + QString::number(upperIndex) +
bool inWallet = mi != wallet->mapWallet.end(); " showTransaction=" + QString::number(showTransaction) + " derivedStatus=" + QString::number(status);
// Find bounds of this transaction in model switch(status)
QList<TransactionRecord>::iterator lower = qLowerBound( {
cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan()); case CT_NEW:
QList<TransactionRecord>::iterator upper = qUpperBound( if(inModel)
cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
int lowerIndex = (lower - cachedWallet.begin());
int upperIndex = (upper - cachedWallet.begin());
bool inModel = (lower != upper);
// Determine whether to show transaction or not
bool showTransaction = (inWallet && TransactionRecord::showTransaction(mi->second));
if(status == CT_UPDATED)
{ {
if(showTransaction && !inModel) qWarning() << "TransactionTablePriv::updateWallet : Warning: Got CT_NEW, but transaction is already in model";
status = CT_NEW; /* Not in model, but want to show, treat as new */ break;
if(!showTransaction && inModel)
status = CT_DELETED; /* In model, but want to hide, treat as deleted */
} }
if(showTransaction)
qDebug() << " inWallet=" + QString::number(inWallet) + " inModel=" + QString::number(inModel) +
" Index=" + QString::number(lowerIndex) + "-" + QString::number(upperIndex) +
" showTransaction=" + QString::number(showTransaction) + " derivedStatus=" + QString::number(status);
switch(status)
{ {
case CT_NEW: LOCK2(cs_main, wallet->cs_wallet);
if(inModel) // Find transaction in wallet
{ std::map<uint256, CWalletTx>::iterator mi = wallet->mapWallet.find(hash);
qWarning() << "TransactionTablePriv::updateWallet : Warning: Got CT_NEW, but transaction is already in model"; if(mi == wallet->mapWallet.end())
break;
}
if(!inWallet)
{ {
qWarning() << "TransactionTablePriv::updateWallet : Warning: Got CT_NEW, but transaction is not in wallet"; qWarning() << "TransactionTablePriv::updateWallet : Warning: Got CT_NEW, but transaction is not in wallet";
break; break;
} }
if(showTransaction) // Added -- insert at the right position
QList<TransactionRecord> toInsert =
TransactionRecord::decomposeTransaction(wallet, mi->second);
if(!toInsert.isEmpty()) /* only if something to insert */
{ {
// Added -- insert at the right position parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex+toInsert.size()-1);
QList<TransactionRecord> toInsert = int insert_idx = lowerIndex;
TransactionRecord::decomposeTransaction(wallet, mi->second); foreach(const TransactionRecord &rec, toInsert)
if(!toInsert.isEmpty()) /* only if something to insert */
{ {
parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex+toInsert.size()-1); cachedWallet.insert(insert_idx, rec);
int insert_idx = lowerIndex; insert_idx += 1;
foreach(const TransactionRecord &rec, toInsert)
{
cachedWallet.insert(insert_idx, rec);
insert_idx += 1;
}
parent->endInsertRows();
} }
parent->endInsertRows();
} }
break; }
case CT_DELETED: break;
if(!inModel) case CT_DELETED:
{ if(!inModel)
qWarning() << "TransactionTablePriv::updateWallet : Warning: Got CT_DELETED, but transaction is not in model"; {
break; qWarning() << "TransactionTablePriv::updateWallet : Warning: Got CT_DELETED, but transaction is not in model";
}
// Removed -- remove entire transaction from table
parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex-1);
cachedWallet.erase(lower, upper);
parent->endRemoveRows();
break;
case CT_UPDATED:
// Miscellaneous updates -- nothing to do, status update will take care of this, and is only computed for
// visible transactions.
break; break;
} }
// Removed -- remove entire transaction from table
parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex-1);
cachedWallet.erase(lower, upper);
parent->endRemoveRows();
break;
case CT_UPDATED:
// Miscellaneous updates -- nothing to do, status update will take care of this, and is only computed for
// visible transactions.
break;
} }
} }
@ -230,16 +223,20 @@ TransactionTableModel::TransactionTableModel(CWallet* wallet, WalletModel *paren
QAbstractTableModel(parent), QAbstractTableModel(parent),
wallet(wallet), wallet(wallet),
walletModel(parent), walletModel(parent),
priv(new TransactionTablePriv(wallet, this)) priv(new TransactionTablePriv(wallet, this)),
fProcessingQueuedTransactions(false)
{ {
columns << QString() << QString() << tr("Date") << tr("Type") << tr("Address") << BitcoinUnits::getAmountColumnTitle(walletModel->getOptionsModel()->getDisplayUnit()); columns << QString() << QString() << tr("Date") << tr("Type") << tr("Address") << BitcoinUnits::getAmountColumnTitle(walletModel->getOptionsModel()->getDisplayUnit());
priv->refreshWallet(); priv->refreshWallet();
connect(walletModel->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit())); connect(walletModel->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit()));
subscribeToCoreSignals();
} }
TransactionTableModel::~TransactionTableModel() TransactionTableModel::~TransactionTableModel()
{ {
unsubscribeFromCoreSignals();
delete priv; delete priv;
} }
@ -250,12 +247,12 @@ void TransactionTableModel::updateAmountColumnTitle()
emit headerDataChanged(Qt::Horizontal,Amount,Amount); emit headerDataChanged(Qt::Horizontal,Amount,Amount);
} }
void TransactionTableModel::updateTransaction(const QString &hash, int status) void TransactionTableModel::updateTransaction(const QString &hash, int status, bool showTransaction)
{ {
uint256 updated; uint256 updated;
updated.SetHex(hash.toStdString()); updated.SetHex(hash.toStdString());
priv->updateWallet(updated, status); priv->updateWallet(updated, status, showTransaction);
} }
void TransactionTableModel::updateConfirmations() void TransactionTableModel::updateConfirmations()
@ -649,3 +646,82 @@ void TransactionTableModel::updateDisplayUnit()
updateAmountColumnTitle(); updateAmountColumnTitle();
emit dataChanged(index(0, Amount), index(priv->size()-1, Amount)); emit dataChanged(index(0, Amount), index(priv->size()-1, Amount));
} }
// queue notifications to show a non freezing progress dialog e.g. for rescan
struct TransactionNotification
{
public:
TransactionNotification() {}
TransactionNotification(uint256 hash, ChangeType status, bool showTransaction):
hash(hash), status(status), showTransaction(showTransaction) {}
void invoke(QObject *ttm)
{
QString strHash = QString::fromStdString(hash.GetHex());
qDebug() << "NotifyTransactionChanged : " + strHash + " status= " + QString::number(status);
QMetaObject::invokeMethod(ttm, "updateTransaction", Qt::QueuedConnection,
Q_ARG(QString, strHash),
Q_ARG(int, status),
Q_ARG(bool, showTransaction));
}
private:
uint256 hash;
ChangeType status;
bool showTransaction;
};
static bool fQueueNotifications = false;
static std::vector< TransactionNotification > vQueueNotifications;
static void NotifyTransactionChanged(TransactionTableModel *ttm, CWallet *wallet, const uint256 &hash, ChangeType status)
{
// Find transaction in wallet
std::map<uint256, CWalletTx>::iterator mi = wallet->mapWallet.find(hash);
// Determine whether to show transaction or not (determine this here so that no relocking is needed in GUI thread)
bool inWallet = mi != wallet->mapWallet.end();
bool showTransaction = (inWallet && TransactionRecord::showTransaction(mi->second));
TransactionNotification notification(hash, status, showTransaction);
if (fQueueNotifications)
{
vQueueNotifications.push_back(notification);
return;
}
notification.invoke(ttm);
}
static void ShowProgress(TransactionTableModel *ttm, const std::string &title, int nProgress)
{
if (nProgress == 0)
fQueueNotifications = true;
if (nProgress == 100)
{
fQueueNotifications = false;
if (vQueueNotifications.size() > 10) // prevent balloon spam, show maximum 10 balloons
QMetaObject::invokeMethod(ttm, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, true));
for (unsigned int i = 0; i < vQueueNotifications.size(); ++i)
{
if (vQueueNotifications.size() - i <= 10)
QMetaObject::invokeMethod(ttm, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, false));
vQueueNotifications[i].invoke(ttm);
}
std::vector<TransactionNotification >().swap(vQueueNotifications); // clear
}
}
void TransactionTableModel::subscribeToCoreSignals()
{
// Connect signals to wallet
wallet->NotifyTransactionChanged.connect(boost::bind(NotifyTransactionChanged, this, _1, _2, _3));
wallet->ShowProgress.connect(boost::bind(ShowProgress, this, _1, _2));
}
void TransactionTableModel::unsubscribeFromCoreSignals()
{
// Disconnect signals from wallet
wallet->NotifyTransactionChanged.disconnect(boost::bind(NotifyTransactionChanged, this, _1, _2, _3));
wallet->ShowProgress.disconnect(boost::bind(ShowProgress, this, _1, _2));
}

View file

@ -72,12 +72,17 @@ public:
QVariant data(const QModelIndex &index, int role) const; QVariant data(const QModelIndex &index, int role) const;
QVariant headerData(int section, Qt::Orientation orientation, int role) const; QVariant headerData(int section, Qt::Orientation orientation, int role) const;
QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const; QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const;
bool processingQueuedTransactions() { return fProcessingQueuedTransactions; }
private: private:
CWallet* wallet; CWallet* wallet;
WalletModel *walletModel; WalletModel *walletModel;
QStringList columns; QStringList columns;
TransactionTablePriv *priv; TransactionTablePriv *priv;
bool fProcessingQueuedTransactions;
void subscribeToCoreSignals();
void unsubscribeFromCoreSignals();
QString lookupAddress(const std::string &address, bool tooltip) const; QString lookupAddress(const std::string &address, bool tooltip) const;
QVariant addressColor(const TransactionRecord *wtx) const; QVariant addressColor(const TransactionRecord *wtx) const;
@ -92,11 +97,14 @@ private:
QVariant txAddressDecoration(const TransactionRecord *wtx) const; QVariant txAddressDecoration(const TransactionRecord *wtx) const;
public slots: public slots:
void updateTransaction(const QString &hash, int status); /* New transaction, or transaction changed status */
void updateTransaction(const QString &hash, int status, bool showTransaction);
void updateConfirmations(); void updateConfirmations();
void updateDisplayUnit(); void updateDisplayUnit();
/** Updates the column title to "Amount (DisplayUnit)" and emits headerDataChanged() signal for table headers to react. */ /** Updates the column title to "Amount (DisplayUnit)" and emits headerDataChanged() signal for table headers to react. */
void updateAmountColumnTitle(); void updateAmountColumnTitle();
/* Needed to update fProcessingQueuedTransactions through a QueuedConnection */
void setProcessingQueuedTransactions(bool value) { fProcessingQueuedTransactions = value; }
friend class TransactionTablePriv; friend class TransactionTablePriv;
}; };

View file

@ -34,7 +34,6 @@ WalletModel::WalletModel(CWallet *wallet, OptionsModel *optionsModel, QObject *p
cachedEncryptionStatus(Unencrypted), cachedEncryptionStatus(Unencrypted),
cachedNumBlocks(0) cachedNumBlocks(0)
{ {
fProcessingQueuedTransactions = false;
fHaveWatchOnly = wallet->HaveWatchOnly(); fHaveWatchOnly = wallet->HaveWatchOnly();
fForceCheckBalanceChanged = false; fForceCheckBalanceChanged = false;
@ -164,11 +163,8 @@ void WalletModel::checkBalanceChanged()
} }
} }
void WalletModel::updateTransaction(const QString &hash, int status) void WalletModel::updateTransaction()
{ {
if(transactionTableModel)
transactionTableModel->updateTransaction(hash, status);
// Balance and number of transactions might have changed // Balance and number of transactions might have changed
fForceCheckBalanceChanged = true; fForceCheckBalanceChanged = true;
} }
@ -455,45 +451,16 @@ static void NotifyAddressBookChanged(WalletModel *walletmodel, CWallet *wallet,
Q_ARG(int, status)); Q_ARG(int, status));
} }
// queue notifications to show a non freezing progress dialog e.g. for rescan
static bool fQueueNotifications = false;
static std::vector<std::pair<uint256, ChangeType> > vQueueNotifications;
static void NotifyTransactionChanged(WalletModel *walletmodel, CWallet *wallet, const uint256 &hash, ChangeType status) static void NotifyTransactionChanged(WalletModel *walletmodel, CWallet *wallet, const uint256 &hash, ChangeType status)
{ {
if (fQueueNotifications) Q_UNUSED(wallet);
{ Q_UNUSED(hash);
vQueueNotifications.push_back(make_pair(hash, status)); Q_UNUSED(status);
return; QMetaObject::invokeMethod(walletmodel, "updateTransaction", Qt::QueuedConnection);
}
QString strHash = QString::fromStdString(hash.GetHex());
qDebug() << "NotifyTransactionChanged : " + strHash + " status= " + QString::number(status);
QMetaObject::invokeMethod(walletmodel, "updateTransaction", Qt::QueuedConnection,
Q_ARG(QString, strHash),
Q_ARG(int, status));
} }
static void ShowProgress(WalletModel *walletmodel, const std::string &title, int nProgress) static void ShowProgress(WalletModel *walletmodel, const std::string &title, int nProgress)
{ {
if (nProgress == 0)
fQueueNotifications = true;
if (nProgress == 100)
{
fQueueNotifications = false;
if (vQueueNotifications.size() > 10) // prevent balloon spam, show maximum 10 balloons
QMetaObject::invokeMethod(walletmodel, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, true));
for (unsigned int i = 0; i < vQueueNotifications.size(); ++i)
{
if (vQueueNotifications.size() - i <= 10)
QMetaObject::invokeMethod(walletmodel, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, false));
NotifyTransactionChanged(walletmodel, NULL, vQueueNotifications[i].first, vQueueNotifications[i].second);
}
std::vector<std::pair<uint256, ChangeType> >().swap(vQueueNotifications); // clear
}
// emits signal "showProgress" // emits signal "showProgress"
QMetaObject::invokeMethod(walletmodel, "showProgress", Qt::QueuedConnection, QMetaObject::invokeMethod(walletmodel, "showProgress", Qt::QueuedConnection,
Q_ARG(QString, QString::fromStdString(title)), Q_ARG(QString, QString::fromStdString(title)),

View file

@ -133,7 +133,6 @@ public:
CAmount getWatchUnconfirmedBalance() const; CAmount getWatchUnconfirmedBalance() const;
CAmount getWatchImmatureBalance() const; CAmount getWatchImmatureBalance() const;
EncryptionStatus getEncryptionStatus() const; EncryptionStatus getEncryptionStatus() const;
bool processingQueuedTransactions() { return fProcessingQueuedTransactions; }
// Check address for validity // Check address for validity
bool validateAddress(const QString &address); bool validateAddress(const QString &address);
@ -197,7 +196,6 @@ public:
private: private:
CWallet *wallet; CWallet *wallet;
bool fProcessingQueuedTransactions;
bool fHaveWatchOnly; bool fHaveWatchOnly;
bool fForceCheckBalanceChanged; bool fForceCheckBalanceChanged;
@ -254,15 +252,13 @@ public slots:
/* Wallet status might have changed */ /* Wallet status might have changed */
void updateStatus(); void updateStatus();
/* New transaction, or transaction changed status */ /* New transaction, or transaction changed status */
void updateTransaction(const QString &hash, int status); void updateTransaction();
/* New, updated or removed address book entry */ /* New, updated or removed address book entry */
void updateAddressBook(const QString &address, const QString &label, bool isMine, const QString &purpose, int status); void updateAddressBook(const QString &address, const QString &label, bool isMine, const QString &purpose, int status);
/* Watch-only added */ /* Watch-only added */
void updateWatchOnlyFlag(bool fHaveWatchonly); void updateWatchOnlyFlag(bool fHaveWatchonly);
/* Current, immature or unconfirmed balance might have changed - emit 'balanceChanged' if so */ /* Current, immature or unconfirmed balance might have changed - emit 'balanceChanged' if so */
void pollBalanceChanged(); void pollBalanceChanged();
/* Needed to update fProcessingQueuedTransactions through a QueuedConnection */
void setProcessingQueuedTransactions(bool value) { fProcessingQueuedTransactions = value; }
}; };
#endif // WALLETMODEL_H #endif // WALLETMODEL_H

View file

@ -137,10 +137,12 @@ void WalletView::setWalletModel(WalletModel *walletModel)
void WalletView::processNewTransaction(const QModelIndex& parent, int start, int /*end*/) void WalletView::processNewTransaction(const QModelIndex& parent, int start, int /*end*/)
{ {
// Prevent balloon-spam when initial block download is in progress // Prevent balloon-spam when initial block download is in progress
if (!walletModel || walletModel->processingQueuedTransactions() || !clientModel || clientModel->inInitialBlockDownload()) if (!walletModel || !clientModel || clientModel->inInitialBlockDownload())
return; return;
TransactionTableModel *ttm = walletModel->getTransactionTableModel(); TransactionTableModel *ttm = walletModel->getTransactionTableModel();
if (!ttm || ttm->processingQueuedTransactions())
return;
QString date = ttm->index(start, TransactionTableModel::Date, parent).data().toString(); QString date = ttm->index(start, TransactionTableModel::Date, parent).data().toString();
qint64 amount = ttm->index(start, TransactionTableModel::Amount, parent).data(Qt::EditRole).toULongLong(); qint64 amount = ttm->index(start, TransactionTableModel::Amount, parent).data(Qt::EditRole).toULongLong();