fix: large-wallet sync starvation + shutdown/console-flash UX (Windows full node) #2

Open
DanS wants to merge 12 commits from fix/balance-poll-sync-contention into dev
5 changed files with 76 additions and 7 deletions
Showing only changes of commit 870793433b - Show all commits

View File

@@ -919,7 +919,9 @@ void App::update()
// Re-apply the refresh cadence when sync starts/finishes: while syncing we throttle polling to // Re-apply the refresh cadence when sync starts/finishes: while syncing we throttle polling to
// a low-impact profile so RPC contention doesn't slow block download (see applyRefreshPolicy). // a low-impact profile so RPC contention doesn't slow block download (see applyRefreshPolicy).
if (state_.sync.syncing != refresh_policy_syncing_) { // effectivelySyncing() includes the post-sync settle window, so this also reverts to the normal
// per-tab cadence once that window elapses.
if (effectivelySyncing() != refresh_policy_syncing_) {
applyRefreshPolicy(current_page_); applyRefreshPolicy(current_page_);
} }

View File

@@ -1102,6 +1102,14 @@ private:
bool daemon_start_error_shown_ = false; bool daemon_start_error_shown_ = false;
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
// Sync-settle hysteresis + adaptive balance-poll throttle. Balance polling (z_gettotalbalance) is
// O(mapWallet) and holds the daemon's cs_main, which starves block connection on a large shielded
// wallet — so we keep the low-impact profile briefly after catching up, and back the balance poll
// off in proportion to its own measured cost. See effectivelySyncing() / balanceRefreshDue().
bool was_core_syncing_ = false; // previous Core-refresh sync state, to detect the caught-up edge
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept. // Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0; std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0; double clipboard_clear_deadline_ = 0.0;
@@ -1439,6 +1447,8 @@ private:
void refreshPrice(); void refreshPrice();
void refreshWalletEncryptionState(); void refreshWalletEncryptionState();
void applyRefreshPolicy(ui::NavPage page); void applyRefreshPolicy(ui::NavPage page);
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost?
bool currentPageNeedsWalletDataRefresh() const; bool currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const; bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const; bool shouldRefreshTransactions() const;

View File

@@ -837,13 +837,39 @@ void App::applyRefreshPolicy(ui::NavPage page)
// While the daemon is syncing, override the per-tab cadence with the low-impact sync profile so // While the daemon is syncing, override the per-tab cadence with the low-impact sync profile so
// the wallet stops contending for the daemon's cs_main lock (frequent getpeerinfo / per-block // the wallet stops contending for the daemon's cs_main lock (frequent getpeerinfo / per-block
// transaction scans / balance polls slow block connection). This makes every tab sync as fast // transaction scans / balance polls slow block connection). This makes every tab sync as fast
// as the Console tab does today. Reverts to the per-tab profile once sync finishes. // as the Console tab does today. effectivelySyncing() keeps this profile on briefly after catching
refresh_policy_syncing_ = state_.sync.syncing; // up (hysteresis) so a large-wallet scan can't immediately re-starve connection and bounce the
// node back into "syncing". Reverts to the per-tab profile once the settle window passes.
refresh_policy_syncing_ = effectivelySyncing();
network_refresh_.setIntervals(refresh_policy_syncing_ network_refresh_.setIntervals(refresh_policy_syncing_
? services::RefreshScheduler::kSyncProfile ? services::RefreshScheduler::kSyncProfile
: getIntervalsForPage(page)); : getIntervalsForPage(page));
} }
// True while the node is behind, and for a short settle window after it first catches up. The settle
// window is armed only on the syncing→caught-up edge (see the Core refresh callback), so a wallet that
// was synced from the start is never throttled at connect — only a node that just finished catching up.
bool App::effectivelySyncing() const
{
if (state_.sync.syncing) return true;
if (sync_settle_until_ == 0) return false; // no pending settle → genuinely caught up
return std::time(nullptr) < sync_settle_until_;
}
// Adaptive throttle: the next balance poll must wait at least (lastScanCost / kBalanceDutyCycle) since
// the last one, so balance scanning can never occupy more than ~kBalanceDutyCycle of wall-clock. A
// cheap wallet (sub-cadence cost) is unaffected — the tab's Core timer stays the real cadence; a ~20s
// scan on a large wallet backs off to roughly every ~200s instead of every 2s, freeing cs_main for
// block connection. A wallet mutation bypasses this via force_balance_refresh_.
bool App::balanceRefreshDue() const
{
constexpr double kBalanceDutyCycle = 0.10;
if (state_.last_balance_update == 0) return true; // never fetched
if (last_balance_scan_ms_ <= 0.0) return true; // no cost measured yet
const double minInterval = (last_balance_scan_ms_ / 1000.0) / kBalanceDutyCycle;
return std::difftime(std::time(nullptr), state_.last_balance_update) >= minInterval;
}
bool App::currentPageNeedsWalletDataRefresh() const bool App::currentPageNeedsWalletDataRefresh() const
{ {
using NP = ui::NavPage; using NP = ui::NavPage;
@@ -1666,9 +1692,15 @@ void App::refreshCoreData()
? fast_rpc_.get() : rpc_.get(); ? fast_rpc_.get() : rpc_.get();
if (!w || !rpc) return; if (!w || !rpc) return;
ui::NavPage tracePage = current_page_; ui::NavPage tracePage = current_page_;
// Skip the balance call while syncing (it's incomplete anyway and takes the wallet lock + // Decide whether to include the balance call (z_gettotalbalance — O(mapWallet), holds cs_main).
// cs_main). Captured on the main thread to avoid reading state_ off the worker thread. // Suppress it (a) while syncing or within the post-sync settle window, so it can't starve block
const bool includeBalance = !state_.sync.syncing; // connection, and (b) unless enough time has elapsed given the LAST scan's measured cost, so a
// large shielded wallet backs off automatically instead of re-scanning every couple of seconds.
// A wallet mutation (send/shield) forces the next poll through so the user's own action updates the
// balance immediately. Captured on the main thread to avoid reading state_ off the worker thread.
const bool includeBalance = !effectivelySyncing() &&
(force_balance_refresh_ || balanceRefreshDue());
if (includeBalance) force_balance_refresh_ = false;
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc, tracePage, includeBalance]() -> rpc::RPCWorker::MainCb { auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc, tracePage, includeBalance]() -> rpc::RPCWorker::MainCb {
AppRefreshRpcGateway refreshRpc(*rpc, traceSource(tracePage, "Core refresh")); AppRefreshRpcGateway refreshRpc(*rpc, traceSource(tracePage, "Core refresh"));
@@ -1678,6 +1710,19 @@ void App::refreshCoreData()
NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr)); NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr));
applyPendingSendBalanceDeltas(true); applyPendingSendBalanceDeltas(true);
// Feed the adaptive balance throttle + sync-settle hysteresis. Record the last scan's
// cost (0 when balance was skipped), and arm the settle window only on the
// syncing→caught-up edge so a wallet synced from the start is never throttled at connect.
if (result.balanceScanMs > 0.0) last_balance_scan_ms_ = result.balanceScanMs;
const bool nowSyncing = state_.sync.syncing;
if (nowSyncing) {
sync_settle_until_ = 0;
} else if (was_core_syncing_) {
constexpr double kSyncSettleSeconds = 8.0;
sync_settle_until_ = std::time(nullptr) + static_cast<std::time_t>(kSyncSettleSeconds);
}
was_core_syncing_ = nowSyncing;
// Mid-session connection-loss detection. During normal operation, both core // Mid-session connection-loss detection. During normal operation, both core
// RPCs failing together means the daemon connection is dead (a busy daemon // RPCs failing together means the daemon connection is dead (a busy daemon
// fails them individually, not both at once). Warmup is excluded — both fail // fails them individually, not both at once). Warmup is excluded — both fail
@@ -5300,6 +5345,7 @@ void App::submitZSendMany(const std::string& from, const std::string& to, double
// Force transaction list refresh so the sent tx appears immediately // Force transaction list refresh so the sent tx appears immediately
transactions_dirty_ = true; transactions_dirty_ = true;
last_tx_block_height_ = -1; last_tx_block_height_ = -1;
force_balance_refresh_ = true; // the user's own send must update the balance now, past the throttle
network_refresh_.markWalletMutationRefresh(); network_refresh_.markWalletMutationRefresh();
// z_sendmany only returned an opid: the transaction is built/signed/ // z_sendmany only returned an opid: the transaction is built/signed/
// broadcast asynchronously by the daemon. Defer the user-facing // broadcast asynchronously by the daemon. Defer the user-facing

View File

@@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <chrono>
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <map> #include <map>
@@ -294,8 +295,13 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
json blockInfo; json blockInfo;
bool balanceOk = false; bool balanceOk = false;
bool blockOk = false; bool blockOk = false;
double balanceScanMs = 0.0;
if (includeBalance) { if (includeBalance) {
// z_gettotalbalance is O(mapWallet) and holds the daemon's cs_main for its whole duration —
// seconds on a large shielded wallet. Time it so the caller can throttle how often it polls
// (balanceRefreshDue()), keeping balance scans from starving block connection.
const auto balanceStart = std::chrono::steady_clock::now();
try { // DISPLAY total: minconf=0 — includes the user's own pending change so it doesn't crater try { // DISPLAY total: minconf=0 — includes the user's own pending change so it doesn't crater
totalBalance = rpc.call("z_gettotalbalance", json::array({0})); totalBalance = rpc.call("z_gettotalbalance", json::array({0}));
balanceOk = true; balanceOk = true;
@@ -305,6 +311,8 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
try { // SPENDABLE total: minconf=1 (confirmed). If absent, spendable degrades to display in apply. try { // SPENDABLE total: minconf=1 (confirmed). If absent, spendable degrades to display in apply.
spendableBalance = rpc.call("z_gettotalbalance", json::array({1})); spendableBalance = rpc.call("z_gettotalbalance", json::array({1}));
} catch (...) {} } catch (...) {}
balanceScanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - balanceStart).count();
} }
try { try {
@@ -314,7 +322,9 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what()); DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
} }
return parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk); auto result = parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
result.balanceScanMs = balanceScanMs;
return result;
} }
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult( NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult(

View File

@@ -111,6 +111,7 @@ public:
std::optional<double> verificationProgress; std::optional<double> verificationProgress;
std::optional<int> longestChain; std::optional<int> longestChain;
std::optional<int> notarized; std::optional<int> notarized;
double balanceScanMs = 0.0; // wall-clock spent in z_gettotalbalance this refresh (0 if balance skipped)
}; };
struct MiningRefreshResult { struct MiningRefreshResult {