perf(net): hold wallet scans at connect, in the background and on tab entry
- Connect gate: after the node starts answering, wallet scans wait until the lock state is known and the node knows its tip (or the wallet has fewer than 500 transactions), at most 20 s. A node already behind latches the sync profile from the connect's getinfo. Meanwhile a list-only pass keeps the address list and the cached history up, and the session's first balance and per-address balances are read once even while catching up, so the wallet shows its last-synced values instead of 0. - Background: minimized, hidden, or covered without focus (3 s debounce, any input cancels) polls only the chain state every 15 s. Wallet scans, the mining poll, price, nudges and auto-lock interaction tracking pause; auto-lock now fires while minimized. Leaving a held profile marks every timer due again. - Tab entry marks the timers due instead of forcing scans; a stale list or a never-loaded history still loads on entry. - The scan entry points own the hard gates, failed enumerations keep the previous lists behind a retry hold, and failed balance calls never overwrite known balances with 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+66
-21
@@ -801,11 +801,13 @@ void App::update()
|
||||
// If a wallet switch's daemon failed to start, revert to the previous wallet (main-thread-safe).
|
||||
processWalletSwitchRevert();
|
||||
|
||||
// Track user interaction for auto-lock
|
||||
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
|
||||
io.MouseClicked[0] || io.MouseClicked[1] ||
|
||||
io.InputQueueCharacters.Size > 0 ||
|
||||
ImGui::IsAnyItemActive()) {
|
||||
// Track user interaction for auto-lock. Not in the background: a minimized window runs update() without
|
||||
// new frames, so the IO state is the last frame's, and a stale mouse delta kept auto-lock from ever firing.
|
||||
if (!window_background_ &&
|
||||
(io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
|
||||
io.MouseClicked[0] || io.MouseClicked[1] ||
|
||||
io.InputQueueCharacters.Size > 0 ||
|
||||
ImGui::IsAnyItemActive())) {
|
||||
last_interaction_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
@@ -998,8 +1000,9 @@ void App::update()
|
||||
// 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).
|
||||
// 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_) {
|
||||
// per-tab cadence once that window elapses. Likewise when the window goes to or comes back from
|
||||
// the background.
|
||||
if (effectivelySyncing() != refresh_policy_syncing_ || window_background_ != refresh_policy_background_) {
|
||||
applyRefreshPolicy(current_page_);
|
||||
}
|
||||
|
||||
@@ -1043,7 +1046,7 @@ void App::update()
|
||||
// Skip the mining poll while the daemon is in warmup (e.g. during -rescan). Otherwise
|
||||
// getmininginfo is rejected with -28 ("Rescanning...") every second and floods the log.
|
||||
// The rescan progress poll below still runs — that's how we track the warmup/rescan.
|
||||
if (rpcConnected && !state_.warming_up && !runtime_rescan_active_ && !rpcBusy) {
|
||||
if (rpcConnected && !state_.warming_up && !runtime_rescan_active_ && !rpcBusy && !window_background_) {
|
||||
refreshMiningInfo();
|
||||
}
|
||||
|
||||
@@ -1401,6 +1404,9 @@ void App::update()
|
||||
};
|
||||
});
|
||||
}
|
||||
// While the address scan that normally ends the send progress can't run (the background, a catch-up, the
|
||||
// connect gate, a retry hold), end it once the operations are done: it keeps the window rendering.
|
||||
if (send_progress_active_ && !addressScanCanRunNow()) maybeFinishTransactionSendProgress();
|
||||
|
||||
// After a bootstrap the preserved wallet.dat has stale spent-state and must be reconciled
|
||||
// against the freshly-imported chain, or the first send tries to spend already-spent notes and
|
||||
@@ -1428,6 +1434,16 @@ void App::update()
|
||||
// nothing: the canary brings polling back (markImmediateRefresh) once the node answers
|
||||
} else if (rpcConnected && !state_.isLocked()) {
|
||||
const bool walletDataPage = currentPageNeedsWalletDataRefresh();
|
||||
const services::ScanContext scan = scanContext();
|
||||
// The connect gate just opened: what it held (the balance, the scans) is owed now. Under a held
|
||||
// profile the marks are lost, but leaving it marks everything due again (applyRefreshPolicy).
|
||||
if (scan_gate_.consumeOpenEdge(std::chrono::steady_clock::now(), state_.encryption_state_known,
|
||||
state_.wallet_tx_count)) {
|
||||
DEBUG_LOGF("[scan-gate] open\n");
|
||||
network_refresh_.markDue(RefreshTimer::Core);
|
||||
network_refresh_.markDue(RefreshTimer::Addresses);
|
||||
network_refresh_.markDue(RefreshTimer::Transactions);
|
||||
}
|
||||
if (!runtime_rescan_active_ && network_refresh_.consumeDue(RefreshTimer::Core)) {
|
||||
refreshCoreData();
|
||||
}
|
||||
@@ -1437,8 +1453,10 @@ void App::update()
|
||||
// History: refreshTransactionData() decides what this cycle may scan (services::decideTxCycle):
|
||||
// wallet-wide scans only as the scan budget allows, except explicit work (the first load, an
|
||||
// invalidation, a new address); sends and other mutations get their rows through Light cycles.
|
||||
// Held back by the budget, it asks again soon (a waiting stream must keep asking).
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Transactions) && shouldRunWalletTransactionRefresh()) {
|
||||
// Held back by the budget, it asks again soon (a waiting stream must keep asking). The hard
|
||||
// gates come first, so a held timer stays due instead of being eaten by a refused dispatch.
|
||||
if (services::wholeWalletScanAllowed(scan) && network_refresh_.consumeDue(RefreshTimer::Transactions) &&
|
||||
shouldRunWalletTransactionRefresh()) {
|
||||
if (refreshTransactionData()) network_refresh_.retrySoon(RefreshTimer::Transactions, kScanRetrySec);
|
||||
}
|
||||
// 0-conf chat: a DEDICATED ~2.5s mempool scan of the chat address, independent of the page's
|
||||
@@ -1451,15 +1469,32 @@ void App::update()
|
||||
chat_fast_scan_accum_ = 0.0f;
|
||||
fastScanChatMemos();
|
||||
}
|
||||
if (network_refresh_.isDue(RefreshTimer::Addresses)) {
|
||||
const services::ScanTier addressTier =
|
||||
services::planAddressScan(scan, addressListStale(), !address_balances_loaded_);
|
||||
// What a held profile's timer can't bring (kSyncProfile's and the background's Addresses interval
|
||||
// is 0): a stale list (list-only) and the session's first balances (while the node catches up).
|
||||
// refreshAddressData() refuses while a pass runs or the retry hold does, and a pass that lands
|
||||
// makes the list current and the balances loaded, so this stops by itself.
|
||||
// The first balances while the node catches up wait for the first balance read to land: sent
|
||||
// together they queue for the same cs_main and the second one times out.
|
||||
const bool addressPassOwed = addressTier == services::ScanTier::ListOnly ||
|
||||
(addressTier == services::ScanTier::Full && !services::wholeWalletScanAllowed(scan) &&
|
||||
!scan_governor_.inFlight(services::ScanStream::Balance));
|
||||
if (addressPassOwed) {
|
||||
const auto steadyNow = std::chrono::steady_clock::now();
|
||||
if (steadyNow >= address_owed_next_try_) {
|
||||
address_owed_next_try_ = steadyNow + std::chrono::seconds(2);
|
||||
refreshAddressData();
|
||||
}
|
||||
} else if (addressTier != services::ScanTier::None && network_refresh_.isDue(RefreshTimer::Addresses)) {
|
||||
// Explicit need (a changed address set, or an in-flight send tracking its change output)
|
||||
// refreshes now. The routine poll on a wallet page runs only when something it depends on
|
||||
// changed (tip, mempool, a wallet mutation) or its safety sweep is due, and only as the scan
|
||||
// budget allows; held back, it asks again soon.
|
||||
// refreshes now (list-only while the scans are held). The routine poll on a wallet page runs
|
||||
// only when something it depends on changed (tip, mempool, a wallet mutation) or its safety
|
||||
// sweep is due, and only as the scan budget allows; held back, it asks again soon.
|
||||
bool retry = false;
|
||||
if (addresses_dirty_ || hasTransactionSendProgress()) {
|
||||
refreshAddressData();
|
||||
} else if (walletDataPage && !effectivelySyncing()) {
|
||||
} else if (walletDataPage && addressTier == services::ScanTier::Full) {
|
||||
using services::ScanStream;
|
||||
const double scanNow = scanClockNow();
|
||||
const auto scanSig = currentScanSignals();
|
||||
@@ -1509,8 +1544,8 @@ void App::update()
|
||||
}
|
||||
}
|
||||
|
||||
// Price refresh every 60 seconds
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Price)) {
|
||||
// Price refresh every 60 seconds (not in the background: it is due again as soon as the window is back)
|
||||
if (!window_background_ && network_refresh_.consumeDue(RefreshTimer::Price)) {
|
||||
if (settings_->getFetchPrices()) {
|
||||
refreshPrice();
|
||||
refreshMarketChart(); // self-throttled to ~30 min; keeps sparkline history fresh
|
||||
@@ -5295,12 +5330,18 @@ void App::refreshNow()
|
||||
// A full receive re-scan (explicit, so it runs back to back), but keep the sent sweep's watermark:
|
||||
// every press would otherwise cost a whole-wallet z_listsentbyaddress.
|
||||
invalidateShieldedHistoryScanProgress(true, /*keepSentScan=*/true);
|
||||
address_retry_not_before_ = {}; // the user asked: an earlier failed enumeration's hold doesn't apply
|
||||
// While the node catches up the wallet scans wait (they would slow it down): say so. (Not in the short
|
||||
// settle window after it caught up: the status bar already says synced, and they run in a moment.)
|
||||
if (rpc_ && rpc_->isConnected() && state_.sync.syncing)
|
||||
ui::Notifications::instance().info(TR("refresh_waits_for_sync"));
|
||||
}
|
||||
|
||||
void App::skipDaemonOutputBacklog()
|
||||
{
|
||||
// While minimized, App::update() is paused, so daemon_output_offset_ is never advanced and a large
|
||||
// backlog of daemon output piles up. Parsing it all at once on restore would replay a background
|
||||
// update() keeps running while minimized, but its output parser (the Fast block) skips while the wallet
|
||||
// is locked, so daemon_output_offset_ stops advancing and a backlog of daemon output can pile up.
|
||||
// Parsing it all at once on restore would replay a background
|
||||
// witness rebuild's progress + completion in a single batch and fire a spurious "Blockchain rescan
|
||||
// complete" toast. Advance the offset to the current end so only NEW (post-restore) output is parsed.
|
||||
// A genuine user-initiated rescan still surfaces completion via the getrescaninfo monitor.
|
||||
@@ -7382,6 +7423,8 @@ std::string App::transactionRefreshProgressText() const
|
||||
// between them while addresses are still to scan.
|
||||
const bool initialLoad = !initial_history_scan_complete_ && work.total > 0;
|
||||
if (!running && !(canRefresh && (transactions_dirty_ || (initialLoad && work.pending > 0)))) return {};
|
||||
// The history scans wait while the node catches up.
|
||||
if (!running && state_.sync.syncing) return TR("tx_loading_waits_for_sync");
|
||||
|
||||
char buf[128];
|
||||
|
||||
@@ -7461,8 +7504,10 @@ void App::maybeFinishTransactionSendProgress()
|
||||
// Finish once the spend is reflected in the balance (addresses). Do NOT wait on the transaction
|
||||
// history scan: the sent tx is already shown via the optimistic pending insert, and the shielded
|
||||
// history scan can stay "dirty" for a long time on wallets with many z-addresses — gating on it
|
||||
// left the send-progress indicator stuck on indefinitely.
|
||||
if (addresses_dirty_ || network_refresh_.jobInProgress(Job::Addresses)) return;
|
||||
// left the send-progress indicator stuck on indefinitely. While the address scan can't run (the
|
||||
// background, a catch-up, the connect gate, a retry hold), finish once the operations are done:
|
||||
// addresses_dirty_ stays set, so the scan still runs when it can.
|
||||
if (addressScanCanRunNow() && (addresses_dirty_ || network_refresh_.jobInProgress(Job::Addresses))) return;
|
||||
send_progress_active_ = false;
|
||||
}
|
||||
std::string App::buildDiagnosticsReport()
|
||||
|
||||
Reference in New Issue
Block a user