Skip to content

Commit b6dcccd

Browse files
Merge #7146: feat(qt): introduce framework for sourcing and applying data, use for {Masternode,Proposal}Lists
3d64056 move-only: move definitions to clientfeeds.{cpp,h} to deal with circulars (Kittywhiskers Van Gogh) 4f56a5e lint: update circular dependencies allowlist (Kittywhiskers Van Gogh) 77a99ee qt: register masternode information as a feed, replace per-wallet thread (Kittywhiskers Van Gogh) 722ec6a refactor(qt): pull out proposal data sourcing to `MasternodeFeed` (Kittywhiskers Van Gogh) a6c7ba3 qt: register proposal information as a feed, replace per-wallet thread (Kittywhiskers Van Gogh) 35eb3d2 feat(qt): introduce framework for sourcing and applying data (Kittywhiskers Van Gogh) 4e951df refactor(qt): pull out proposal data sourcing to `ProposalFeed` (Kittywhiskers Van Gogh) 7827ae1 move-only: src/qt/governancelist.{cpp,h} -> src/qt/proposallist.{cpp,h} (Kittywhiskers Van Gogh) 342f785 refactor: s/ProposalList/Proposals/g (Kittywhiskers Van Gogh) Pull request description: ## Additional Information * Dependency for #7118 * The redesigned "Masternode" and "Governance" tabs (see [dash#7110](#7110) and [dash#7116](#7116)) were migrated to a worker-based update mechanism with a773635 and others, this proved to deal with main thread contention and the debounce mechanism prevented frequent refreshes of infrequently updated data and was a step towards throttled event-driven updates. * This implementation though had a problematic side effect, a worker thread is spawned **per tab per wallet** and while per tab threads were by design, per wallet threads meant redundant work happening all at once fighting over the same locks, which cause noticeable performance degradation. * This pull request therefore introduces the concept of "feeds", that perform the expensive fetch operation that are either triggered by events (and debounced) or user feedback (and executed immediately) to ensure snappiness. The IBD throttling logic is respected. * The result is having a central worker thread fetching (`fetch()`) and dispatching refresh signals (`dataReady()`) which are then used by the consumer (e.g. `updateMasternodeList()`) to fetch wallet-specific information (which is relatively inexpensive) and then *apply* those changes (e.g. `setMasternodeList()`). * Note that wallet-specific fetching is done on the main thread, to prevent thread explosion (even if they are doing now non-redundant work) as they'd still be per tab per wallet. * Note that masternodes and proposal views use different refresh intervals, masternode data uses 3 seconds and proposal data uses 10 seconds, scaled up to 30s and 60s respectively during IBD. ## Breaking Changes None expected. ## Checklist - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests **(note: N/A)** - [x] I have made corresponding changes to the documentation **(note: N/A)** - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: UdjinM6: utACK 3d64056 Tree-SHA512: 2bd89418e9de80c10269fc723468141970a41e492baadf839824eb4c58cea43f65ab8b8cbeac1867e0dffd3c3bc5785ec59fab97bd1c3659fe46f7bf797eddcc
2 parents bb5dea2 + 3d64056 commit b6dcccd

25 files changed

Lines changed: 696 additions & 425 deletions

src/Makefile.qt.include

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ QT_FORMS_UI = \
2323
qt/forms/debugwindow.ui \
2424
qt/forms/descriptiondialog.ui \
2525
qt/forms/editaddressdialog.ui \
26-
qt/forms/governancelist.ui \
2726
qt/forms/helpmessagedialog.ui \
2827
qt/forms/intro.ui \
2928
qt/forms/masternodelist.ui \
@@ -33,6 +32,7 @@ QT_FORMS_UI = \
3332
qt/forms/optionsdialog.ui \
3433
qt/forms/overviewpage.ui \
3534
qt/forms/proposalcreate.ui \
35+
qt/forms/proposallist.ui \
3636
qt/forms/proposalresume.ui \
3737
qt/forms/psbtoperationsdialog.ui \
3838
qt/forms/qrdialog.ui \
@@ -53,14 +53,14 @@ QT_MOC_CPP = \
5353
qt/moc_bitcoinamountfield.cpp \
5454
qt/moc_bitcoingui.cpp \
5555
qt/moc_bitcoinunits.cpp \
56+
qt/moc_clientfeeds.cpp \
5657
qt/moc_clientmodel.cpp \
5758
qt/moc_coincontroldialog.cpp \
5859
qt/moc_coincontroltreewidget.cpp \
5960
qt/moc_createwalletdialog.cpp \
6061
qt/moc_csvmodelwriter.cpp \
6162
qt/moc_descriptiondialog.cpp \
6263
qt/moc_editaddressdialog.cpp \
63-
qt/moc_governancelist.cpp \
6464
qt/moc_guiutil.cpp \
6565
qt/moc_initexecutor.cpp \
6666
qt/moc_intro.cpp \
@@ -78,8 +78,9 @@ QT_MOC_CPP = \
7878
qt/moc_paymentserver.cpp \
7979
qt/moc_peertablemodel.cpp \
8080
qt/moc_peertablesortproxy.cpp \
81-
qt/moc_proposalmodel.cpp \
8281
qt/moc_proposalcreate.cpp \
82+
qt/moc_proposallist.cpp \
83+
qt/moc_proposalmodel.cpp \
8384
qt/moc_proposalresume.cpp \
8485
qt/moc_psbtoperationsdialog.cpp \
8586
qt/moc_qrdialog.cpp \
@@ -133,17 +134,17 @@ BITCOIN_QT_H = \
133134
qt/bitcoinamountfield.h \
134135
qt/bitcoingui.h \
135136
qt/bitcoinunits.h \
137+
qt/clientfeeds.h \
136138
qt/clientmodel.h \
137139
qt/coincontroldialog.h \
138140
qt/coincontroltreewidget.h \
139141
qt/createwalletdialog.h \
140142
qt/csvmodelwriter.h \
141143
qt/descriptiondialog.h \
142144
qt/editaddressdialog.h \
143-
qt/governancelist.h \
144145
qt/guiconstants.h \
145-
qt/guiutil.h \
146146
qt/guiutil_font.h \
147+
qt/guiutil.h \
147148
qt/initexecutor.h \
148149
qt/intro.h \
149150
qt/macdockiconhandler.h \
@@ -163,6 +164,7 @@ BITCOIN_QT_H = \
163164
qt/peertablemodel.h \
164165
qt/peertablesortproxy.h \
165166
qt/proposalcreate.h \
167+
qt/proposallist.h \
166168
qt/proposalmodel.h \
167169
qt/proposalresume.h \
168170
qt/psbtoperationsdialog.h \
@@ -242,9 +244,12 @@ BITCOIN_QT_BASE_CPP = \
242244
qt/bantablemodel.cpp \
243245
qt/bitcoin.cpp \
244246
qt/bitcoinaddressvalidator.cpp \
247+
qt/masternodemodel.cpp \
248+
qt/proposalmodel.cpp \
245249
qt/bitcoinamountfield.cpp \
246250
qt/bitcoingui.cpp \
247251
qt/bitcoinunits.cpp \
252+
qt/clientfeeds.cpp \
248253
qt/clientmodel.cpp \
249254
qt/csvmodelwriter.cpp \
250255
qt/guiutil.cpp \
@@ -277,15 +282,13 @@ BITCOIN_QT_WALLET_CPP = \
277282
qt/createwalletdialog.cpp \
278283
qt/descriptiondialog.cpp \
279284
qt/editaddressdialog.cpp \
280-
qt/governancelist.cpp \
281285
qt/masternodelist.cpp \
282-
qt/masternodemodel.cpp \
283286
qt/mnemonicverificationdialog.cpp \
284287
qt/openuridialog.cpp \
285288
qt/overviewpage.cpp \
286289
qt/paymentserver.cpp \
287290
qt/proposalcreate.cpp \
288-
qt/proposalmodel.cpp \
291+
qt/proposallist.cpp \
289292
qt/proposalresume.cpp \
290293
qt/psbtoperationsdialog.cpp \
291294
qt/qrdialog.cpp \

src/qt/bitcoingui.cpp

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,31 @@
55

66
#include <qt/bitcoingui.h>
77

8+
#include <chain.h>
9+
#include <chainparams.h>
10+
#include <interfaces/coinjoin.h>
11+
#include <interfaces/handler.h>
12+
#include <interfaces/node.h>
13+
#include <node/interface_ui.h>
14+
#include <util/system.h>
15+
#include <util/translation.h>
16+
#include <util/underlying.h>
17+
#include <validation.h>
18+
819
#include <qt/bitcoinunits.h>
920
#include <qt/clientmodel.h>
1021
#include <qt/createwalletdialog.h>
1122
#include <qt/guiconstants.h>
12-
#include <qt/guiutil.h>
1323
#include <qt/guiutil_font.h>
24+
#include <qt/guiutil.h>
25+
#include <qt/masternodelist.h>
1426
#include <qt/modaloverlay.h>
1527
#include <qt/networkstyle.h>
1628
#include <qt/notificator.h>
1729
#include <qt/openuridialog.h>
1830
#include <qt/optionsdialog.h>
1931
#include <qt/optionsmodel.h>
32+
#include <qt/proposallist.h>
2033
#include <qt/rpcconsole.h>
2134
#include <qt/utilitydialog.h>
2235

@@ -31,20 +44,6 @@
3144
#include <qt/macdockiconhandler.h>
3245
#endif
3346

34-
#include <functional>
35-
#include <chain.h>
36-
#include <chainparams.h>
37-
#include <interfaces/coinjoin.h>
38-
#include <interfaces/handler.h>
39-
#include <interfaces/node.h>
40-
#include <node/interface_ui.h>
41-
#include <qt/governancelist.h>
42-
#include <qt/masternodelist.h>
43-
#include <util/system.h>
44-
#include <util/translation.h>
45-
#include <util/underlying.h>
46-
#include <validation.h>
47-
4847
#include <QAction>
4948
#include <QActionGroup>
5049
#include <QApplication>
@@ -72,6 +71,8 @@
7271
#include <QVBoxLayout>
7372
#include <QWindow>
7473

74+
#include <functional>
75+
7576
namespace {
7677
// Total governance clock frames. Frame 0 is reserved for the superblock
7778
// maturity window; frames 1 through GOV_CYCLE_FRAME_COUNT-1 are used for the

src/qt/clientfeeds.cpp

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright (c) 2026 The Dash Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <qt/clientfeeds.h>
6+
7+
#include <coins.h>
8+
#include <key_io.h>
9+
#include <script/standard.h>
10+
#include <util/threadnames.h>
11+
#include <util/time.h>
12+
13+
#include <qt/clientmodel.h>
14+
#include <qt/masternodemodel.h>
15+
#include <qt/proposalmodel.h>
16+
17+
#include <QDebug>
18+
#include <QThread>
19+
20+
namespace {
21+
constexpr auto MASTERNODE_UPDATE_INTERVAL{3s};
22+
constexpr auto PROPOSAL_UPDATE_INTERVAL{10s};
23+
} // anonymous namespace
24+
25+
FeedBase::FeedBase(QObject* parent, const FeedBase::Config& config) :
26+
QObject{parent},
27+
m_config{config}
28+
{
29+
}
30+
31+
FeedBase::~FeedBase() = default;
32+
33+
void FeedBase::requestForceRefresh()
34+
{
35+
if (m_timer) {
36+
m_timer->start(0);
37+
}
38+
}
39+
40+
void FeedBase::requestRefresh()
41+
{
42+
if (m_timer && !m_timer->isActive()) {
43+
m_timer->start(m_syncing.load() ? m_config.m_throttle : m_config.m_baseline);
44+
}
45+
}
46+
47+
MasternodeFeed::MasternodeFeed(QObject* parent, ClientModel& client_model) :
48+
Feed<MasternodeData>{parent, {/*m_baseline=*/MASTERNODE_UPDATE_INTERVAL, /*m_throttle=*/MASTERNODE_UPDATE_INTERVAL*10}},
49+
m_client_model{client_model}
50+
{
51+
}
52+
53+
MasternodeFeed::~MasternodeFeed() = default;
54+
55+
void MasternodeFeed::fetch()
56+
{
57+
if (m_client_model.node().shutdownRequested()) {
58+
return;
59+
}
60+
61+
const auto [dmn, pindex] = m_client_model.getMasternodeList();
62+
if (!dmn || !pindex) {
63+
return;
64+
}
65+
66+
auto projectedPayees = dmn->getProjectedMNPayees(pindex);
67+
if (projectedPayees.empty() && dmn->getValidMNsCount() > 0) {
68+
// GetProjectedMNPayees failed to provide results for a list with valid mns.
69+
// Keep current list and let it try again later.
70+
return;
71+
}
72+
73+
auto ret = std::make_shared<Data>();
74+
ret->m_list_height = dmn->getHeight();
75+
76+
Uint256HashMap<int> nextPayments;
77+
for (size_t i = 0; i < projectedPayees.size(); i++) {
78+
const auto& dmn = projectedPayees[i];
79+
nextPayments.emplace(dmn->getProTxHash(), ret->m_list_height + (int)i + 1);
80+
}
81+
82+
dmn->forEachMN(/*only_valid=*/false, [&](const auto& dmn) {
83+
CTxDestination collateralDest;
84+
Coin coin;
85+
QString collateralStr = QObject::tr("UNKNOWN");
86+
if (m_client_model.node().getUnspentOutput(dmn.getCollateralOutpoint(), coin) &&
87+
ExtractDestination(coin.out.scriptPubKey, collateralDest)) {
88+
collateralStr = QString::fromStdString(EncodeDestination(collateralDest));
89+
}
90+
int nNextPayment{0};
91+
if (auto nextPaymentIt = nextPayments.find(dmn.getProTxHash()); nextPaymentIt != nextPayments.end()) {
92+
nNextPayment = nextPaymentIt->second;
93+
}
94+
ret->m_entries.push_back(std::make_unique<MasternodeEntry>(dmn, collateralStr, nNextPayment));
95+
});
96+
97+
ret->m_valid = true;
98+
setData(std::move(ret));
99+
}
100+
101+
ProposalFeed::ProposalFeed(QObject* parent, ClientModel& client_model) :
102+
Feed<ProposalData>{parent, {/*m_baseline=*/PROPOSAL_UPDATE_INTERVAL, /*m_throttle=*/PROPOSAL_UPDATE_INTERVAL*6}},
103+
m_client_model{client_model}
104+
{
105+
}
106+
107+
ProposalFeed::~ProposalFeed() = default;
108+
109+
void ProposalFeed::fetch()
110+
{
111+
if (m_client_model.node().shutdownRequested()) {
112+
return;
113+
}
114+
115+
const auto [dmn, pindex] = m_client_model.getMasternodeList();
116+
if (!dmn || !pindex) {
117+
return;
118+
}
119+
120+
auto ret = std::make_shared<Data>();
121+
// A proposal is considered passing if (YES votes - NO votes) >= (Total Weight of Masternodes / 10),
122+
// count total valid (ENABLED) masternodes to determine passing threshold.
123+
// Need to query number of masternodes here with access to client model.
124+
const int nWeightedMnCount = dmn->getValidWeightedMNsCount();
125+
ret->m_abs_vote_req = std::max(Params().GetConsensus().nGovernanceMinQuorum, nWeightedMnCount / 10);
126+
ret->m_gov_info = m_client_model.node().gov().getGovernanceInfo();
127+
std::vector<CGovernanceObject> govObjList;
128+
m_client_model.getAllGovernanceObjects(govObjList);
129+
for (const auto& govObj : govObjList) {
130+
if (govObj.GetObjectType() != GovernanceObject::PROPOSAL) {
131+
continue; // Skip triggers.
132+
}
133+
ret->m_proposals.emplace_back(std::make_shared<Proposal>(m_client_model, govObj, ret->m_gov_info, ret->m_gov_info.requiredConfs,
134+
/*is_broadcast=*/true));
135+
}
136+
137+
auto fundable{m_client_model.node().gov().getFundableProposalHashes()};
138+
ret->m_fundable_hashes = std::move(fundable.hashes);
139+
140+
setData(std::move(ret));
141+
}
142+
143+
ClientFeeds::ClientFeeds(QObject* parent) :
144+
QObject{parent},
145+
m_thread{new QThread(this)}
146+
{
147+
}
148+
149+
ClientFeeds::~ClientFeeds()
150+
{
151+
stop();
152+
}
153+
154+
void ClientFeeds::registerFeed(FeedBase* raw)
155+
{
156+
auto* timer = new QTimer(this);
157+
timer->setSingleShot(true);
158+
raw->m_timer = timer;
159+
160+
connect(timer, &QTimer::timeout, this, [this, raw] {
161+
if (raw->m_in_progress.exchange(true)) {
162+
raw->m_retry_pending.store(true);
163+
return;
164+
}
165+
QMetaObject::invokeMethod(m_worker, [this, raw] {
166+
try {
167+
raw->fetch();
168+
} catch (const std::exception& e) {
169+
qWarning() << "ClientFeeds::fetch() exception: " << e.what();
170+
} catch (...) {
171+
qWarning() << "ClientFeeds::fetch() unknown exception";
172+
}
173+
QTimer::singleShot(0, raw, [this, raw] {
174+
raw->m_in_progress.store(false);
175+
if (m_stopped) return;
176+
Q_EMIT raw->dataReady();
177+
if (raw->m_retry_pending.exchange(false)) {
178+
raw->requestRefresh();
179+
}
180+
});
181+
});
182+
});
183+
}
184+
185+
void ClientFeeds::start()
186+
{
187+
m_worker = new QObject();
188+
m_worker->moveToThread(m_thread);
189+
m_thread->start();
190+
QMetaObject::invokeMethod(m_worker, [] { util::ThreadRename("qt-clientfeed"); });
191+
192+
for (const auto& source : m_sources) {
193+
if (source->m_timer) {
194+
source->m_timer->start(0);
195+
}
196+
}
197+
}
198+
199+
void ClientFeeds::stop()
200+
{
201+
if (m_stopped) {
202+
return;
203+
}
204+
205+
m_stopped = true;
206+
for (const auto& source : m_sources) {
207+
if (source->m_timer) {
208+
source->m_timer->stop();
209+
source->m_timer = nullptr;
210+
}
211+
}
212+
if (m_thread->isRunning()) {
213+
m_thread->quit();
214+
m_thread->wait();
215+
}
216+
delete m_worker;
217+
m_worker = nullptr;
218+
}
219+
220+
void ClientFeeds::setSyncing(bool syncing)
221+
{
222+
if (m_stopped) {
223+
return;
224+
}
225+
226+
for (const auto& source : m_sources) {
227+
if (source->m_syncing.load() == syncing) {
228+
continue;
229+
}
230+
source->setSyncing(syncing);
231+
if (source->m_timer && source->m_timer->isActive()) {
232+
source->m_timer->start(syncing ? source->m_config.m_throttle : source->m_config.m_baseline);
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)