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:
2026-10-09 11:06:34 -05:00
co-authored by Claude Opus 5.5
parent 323143e111
commit f86cc4d6b7
26 changed files with 1083 additions and 159 deletions
+32 -5
View File
@@ -27,6 +27,8 @@
#include "daemon/daemon_busy_detect.h"
#include "rpc/rpc_busy_gate.h"
#include "services/history_scan_policy.h"
#include "services/connect_scan_gate.h"
#include "services/scan_policy.h"
#include "services/wallet_scan_governor.h"
#include "services/network_refresh_service.h"
#include "services/wallet_security_controller.h"
@@ -443,8 +445,11 @@ public:
// Something changed the wallet that the scans' change signals (tip, mempool) don't show: a send or other
// operation finished, an import, a Console command. The change-gated scans run again.
void noteWalletMutation() { ++wallet_mutation_epoch_; }
// Minimized, hidden or occluded: no page is visible, so no scan stream gets the visible head start.
void setWindowBackground(bool background) { window_background_ = background; }
// The window is in the background: minimized, hidden, or covered while the app has no focus (debounced
// by the main loop; any input ends it). The background refresh profile then holds the wallet scans and
// the peripheral polls (mining, price), and no page counts as visible to the scan budget.
void setWindowBackground(bool background);
bool isWindowBackground() const { return window_background_; }
// A balance scan is in flight (the staleness badge waits for it).
bool isBalanceRefreshInFlight() const { return scan_governor_.inFlight(services::ScanStream::Balance); }
// The balance is waiting for the scan budget and still within what it promises (its floor plus one scan):
@@ -486,9 +491,9 @@ public:
// Force refresh
void refreshNow();
// Called on window restore: drop the daemon-output backlog that accumulated while minimized (the
// per-frame update loop was paused), so a background witness rebuild that started+finished during
// the minimize isn't parsed in one batch and mistaken for a completed rescan (spurious toast).
// Called on window restore: drop any daemon-output backlog (update() runs while minimized, but its
// output parser skips while the wallet is locked), so a background witness rebuild that started and
// finished meanwhile isn't parsed in one batch and mistaken for a completed rescan (spurious toast).
void skipDaemonOutputBacklog();
void refreshMiningInfo();
void refreshPeerInfo();
@@ -1354,6 +1359,7 @@ private:
bool daemon_start_error_shown_ = false;
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_background_ = false; // whether the background 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
@@ -1386,6 +1392,27 @@ private:
services::ScanSignals currentScanSignals() const;
services::ScanVisibility currentScanVisibility() const;
static double scanClockNow(); // the governor's clock: steady-clock seconds
// C44: holds the wallet-wide scans for a moment after the node starts answering (connect, warmup end),
// until the lock state is known and the node knows its tip or the wallet is small (20 s at most).
services::ConnectScanGate scan_gate_;
// After an address enumeration failed: no address refresh before this (the previous lists stay).
std::chrono::steady_clock::time_point address_retry_not_before_{};
// A full address pass (the per-address balances) has run this connection. Until then the first one runs
// even while the node catches up.
bool address_balances_loaded_ = false;
// The session's first balance read was sent while the node was catching up (once per connection).
bool first_balance_read_tried_ = false;
// Paces the address passes the held profiles' timers can't bring (a stale list, the first balances).
std::chrono::steady_clock::time_point address_owed_next_try_{};
bool scanGateOpen() const;
// The address list was never loaded this session, or the address set may have changed (import, sweep,
// decrypt-import): it loads even while the wallet scans are held (list-only).
bool addressListStale() const;
// What the scan entry points check before sending a wallet-wide scan (services::scan_policy.h).
services::ScanContext scanContext() const;
// The full address pass could be sent now (the hard gates, and no retry hold).
bool addressScanCanRunNow() const;
void armScanGate(const services::NetworkRefreshService::ConnectionInfoResult& info);
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0;