From d83169c963cbcd6a7345c7ec73b5735b4a464992 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 26 Oct 2026 14:22:53 -0500 Subject: [PATCH] feat(wallet): pause routine wallet scans during a user-run consolidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A manual consolidation's merges compete with the GUI's routine wallet scans for the daemon's cs_main: the run won't fire the next merge while any scan is in flight, and because each merge mutates the wallet it re-triggers the balance / history scans, which then delay the following merge. The WalletScanGovernor already serializes all of these one-at-a-time under a cs_main budget, so the fix is to pause the heavy routine streams — not every RPC — while a user run owns the budget. WalletScanGovernor gains per-stream routine suppression (tryAdmit returns empty for a paused stream, which keeps waiting and resumes the instant it is unpaused; explicit needs via beginExplicit are never paused), plus a convenience that pauses the balance / addresses / history / recent-tx group together. App::update pauses that group while consolidation_.active() AND the run is user-started — an auto (background) run is left gentle and untouched, and the cheap sync / peer / mempool / operation-status polls keep running so sync and disconnects stay visible. The end-of-run forced balance refresh repaints once the run finishes. Note: a manual run is bounded (≤ maxBatches per click, ~minutes), so the balance and recent-tx views are only briefly stale — an incoming payment shows once the run finishes or is stopped. Unit-tested (testWalletScanGovernor): a paused stream is not admitted but keeps waiting, explicit bypasses it, unpausing re-admits, and chat/Adhoc are never in the paused group. Co-Authored-By: Claude Opus 4.8 --- src/app.cpp | 5 +++++ src/services/wallet_scan_governor.cpp | 3 +++ src/services/wallet_scan_governor.h | 16 ++++++++++++++++ tests/test_phase4.cpp | 17 +++++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/src/app.cpp b/src/app.cpp index 05051c3..89e19c0 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1594,6 +1594,11 @@ void App::update() pumpConsolidation(); // Background auto-consolidation (settings toggle): gently trickle-merge notes when the wallet is idle. pumpAutoConsolidation(); + // While a USER-started consolidation is running, pause the heavy routine wallet scans (balance / + // addresses / history / recent-tx) so its merges get more of the node's cs_main budget and finish + // sooner — the end-of-run refresh repaints. An auto (background) run stays gentle and is left alone, and + // explicit needs (the user's own send, a never-loaded list) still go through. Idempotent per frame. + scan_governor_.suppressRoutineWalletScans(consolidation_.active() && !auto_consolidate_run_); // A Settings bootstrap that finished while its dialog wasn't drawn (clicked away, minimized): process it // here so the connect and reconcile gates release and the post-bootstrap rescan is armed. diff --git a/src/services/wallet_scan_governor.cpp b/src/services/wallet_scan_governor.cpp index 61f8265..fd087ab 100644 --- a/src/services/wallet_scan_governor.cpp +++ b/src/services/wallet_scan_governor.cpp @@ -171,6 +171,9 @@ ScanTicket WalletScanGovernor::tryAdmit(ScanStream stream, bool visible, const S if (st.waitingSince < 0.0 || now - st.lastAskAt > cfg_.waitLapseSec) st.waitingSince = now; // a new wait st.lastAskAt = now; st.askVisible = visible; + // Paused (e.g. a user-started consolidation owns the cs_main budget): keep the stream waiting, admit + // nothing. It resumes immediately once unpaused; explicit needs still go through beginExplicit. + if (suppressed_[static_cast(stream)]) return {}; pruneLeases(now); if (!live_.empty()) return {}; // one wallet scan in the daemon at a time if (bestWaiter(now) != stream) return {}; diff --git a/src/services/wallet_scan_governor.h b/src/services/wallet_scan_governor.h index f28742d..f642ae7 100644 --- a/src/services/wallet_scan_governor.h +++ b/src/services/wallet_scan_governor.h @@ -159,6 +159,21 @@ public: void invalidate(ScanStream stream); // forget its basis: the next routine scan is needed void reset(); // a new connection: forget everything; old tickets complete as no-ops + // Pause/resume a stream's ROUTINE scans (tryAdmit returns empty while paused; the stream keeps waiting + // and resumes the moment it is unpaused). Explicit needs (beginExplicit) are NEVER paused. + void suppressStream(ScanStream stream, bool paused) { suppressed_[static_cast(stream)] = paused; } + bool streamSuppressed(ScanStream stream) const { return suppressed_[static_cast(stream)]; } + // Convenience: pause/resume the heavy wallet-display scans (balance, addresses, history, recent-tx) as a + // group — used to hand a user-started consolidation more of the cs_main budget. Chat scans and Adhoc + // (the consolidation's own merge calls) are left running. + void suppressRoutineWalletScans(bool paused) + { + suppressStream(ScanStream::Balance, paused); + suppressStream(ScanStream::Addresses, paused); + suppressStream(ScanStream::History, paused); + suppressStream(ScanStream::RecentTx, paused); + } + bool inFlight(ScanStream stream) const; bool anyInFlight() const { return !live_.empty(); } double quietUntil() const { return quietUntil_; } @@ -208,6 +223,7 @@ private: Config cfg_; std::array(ScanStream::Count)> streams_{}; + std::array(ScanStream::Count)> suppressed_{}; // routine scans paused per stream std::vector live_; double quietUntil_ = 0.0; int tipLag_ = 0; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 9d4ea1c..e40a21c 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -18030,6 +18030,23 @@ void testWalletScanGovernor() EXPECT_TRUE(static_cast(gov.tryAdmit(ScanStream::Balance, true, sig, 372.0))); EXPECT_FALSE(gov.waiting(ScanStream::Balance, 373.0)); // admitted: no longer waiting } + { // Suppression: a paused routine stream is never admitted (but keeps waiting); explicit needs bypass + // it; unpausing admits it again. Used to give a user consolidation the cs_main budget. + G gov; + gov.suppressRoutineWalletScans(true); + EXPECT_TRUE(gov.streamSuppressed(ScanStream::Balance)); + EXPECT_FALSE(static_cast(gov.tryAdmit(ScanStream::Balance, true, sig, 0.0))); // paused: not admitted + EXPECT_TRUE(gov.waiting(ScanStream::Balance, 0.1)); // ...but still waiting + auto ex = gov.beginExplicit(ScanStream::Balance, sig, 0.2); // explicit bypasses it + EXPECT_TRUE(static_cast(ex)); + gov.complete(ex, 0.0, ScanOutcome::Skipped, 0.3); + gov.suppressRoutineWalletScans(false); + EXPECT_FALSE(gov.streamSuppressed(ScanStream::Balance)); + EXPECT_TRUE(static_cast(gov.tryAdmit(ScanStream::Balance, true, sig, 0.4))); // resumes once unpaused + // Chat scans and Adhoc (the merges themselves) are never in the suppressed group. + EXPECT_FALSE(gov.streamSuppressed(ScanStream::ChatNotes)); + EXPECT_FALSE(gov.streamSuppressed(ScanStream::Adhoc)); + } } // The between-blocks probe's rotation: every z-address once (oldest-scanned first) before it is done.