perf(history): scan shielded history in bounded passes, not every block
Every block re-ran z_listreceivedbyaddress for every z-address (O(wallet) each, under cs_main) and a whole-wallet z_listsentbyaddress. On a 40-address wallet that was ~80% of wall-clock. The history now advances in passes: - an address is rescanned when it is stale by a number of blocks scaled to its measured cost (8 to 500), never-scanned and hinted addresses first (a hint: z_listunspent shows a note the history doesn't); - z_listsentbyaddress covers only the blocks since the last good sweep (+100 reorg margin); the whole-wallet sweep runs once per connect, with a 10 min / 1 h / 6 h backoff after real failures; - listtransactions pages are sized from last time, and an unchanged tip reuses the transparent rows; - found-but-unenriched txids carry over as a backlog; only -5 is a definitive "not ours" for z_viewtransaction; - between blocks, a cheap recent poll plus a one-address probe. Duty model for a 40-address wallet at 10 s per scan: ~9% instead of ~80%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+30
-36
@@ -1373,7 +1373,7 @@ void App::update()
|
||||
removePendingSendTransactions(successOpids, false);
|
||||
removePendingSendTransactions(failedOrStaleOpids, true);
|
||||
// Transaction confirmed by daemon — force immediate data refresh
|
||||
transactions_dirty_ = true;
|
||||
markTransactionsDirty();
|
||||
addresses_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
network_refresh_.markWalletMutationRefresh();
|
||||
@@ -1417,21 +1417,11 @@ void App::update()
|
||||
// Skip balance/tx/address refresh during warmup — RPC calls fail with -28 — and during a
|
||||
// runtime rescan, when the daemon holds cs_main and these calls would block.
|
||||
if (!state_.warming_up && !runtime_rescan_active_) {
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
|
||||
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
|
||||
// Throttle the routine new-block full history rescan (z_listreceivedbyaddress —
|
||||
// O(mapWallet), holds cs_main) by its measured cost, so a large wallet doesn't
|
||||
// re-scan every block and starve connection near the tip. Bypass when there's an
|
||||
// explicit need: a dirty set, an in-progress multi-cycle shielded scan (which must
|
||||
// continue to completion), or an in-flight send — all need fresh data immediately.
|
||||
const bool txExplicitNeed = transactions_dirty_ || shielded_history_scan_pending_ ||
|
||||
hasTransactionSendProgress() || !send_txids_.empty();
|
||||
if (txExplicitNeed || txRefreshDue()) {
|
||||
refreshTransactionData();
|
||||
}
|
||||
} else if (walletDataPage && shouldRefreshRecentTransactions()) {
|
||||
refreshRecentTransactionData();
|
||||
}
|
||||
// History: refreshTransactionData() decides what this cycle may scan (services::decideTxCycle):
|
||||
// wallet-wide scans only as their measured cost allows, except explicit work (the first load, an
|
||||
// invalidation, a new address); sends and other mutations get their rows through Light cycles.
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Transactions) && shouldRunWalletTransactionRefresh()) {
|
||||
refreshTransactionData();
|
||||
}
|
||||
// 0-conf chat: a DEDICATED ~2.5s mempool scan of the chat address, independent of the page's
|
||||
// (slower, 10–15s) Transactions cadence — so incoming messages surface in a few seconds. The
|
||||
@@ -5263,10 +5253,12 @@ void App::refreshNow()
|
||||
{
|
||||
// Trigger immediate refresh on all categories
|
||||
network_refresh_.markImmediateRefresh();
|
||||
transactions_dirty_ = true; // Force transaction list update
|
||||
markTransactionsDirty(); // Force transaction list update
|
||||
addresses_dirty_ = true; // Force address/balance update
|
||||
last_tx_block_height_ = -1; // Reset tx cache
|
||||
invalidateShieldedHistoryScanProgress(true);
|
||||
// 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);
|
||||
}
|
||||
|
||||
void App::skipDaemonOutputBacklog()
|
||||
@@ -5828,7 +5820,7 @@ void App::rescanBlockchain()
|
||||
user_initiated_rescan_ = true; // user-triggered rescan → its completion should toast
|
||||
state_.sync.rescan_progress = 0.0f;
|
||||
state_.sync.rescan_status = decision.status;
|
||||
transactions_dirty_ = true;
|
||||
markTransactionsDirty();
|
||||
last_tx_block_height_ = -1;
|
||||
invalidateShieldedHistoryScanProgress(true);
|
||||
|
||||
@@ -5882,7 +5874,7 @@ void App::repairWallet()
|
||||
user_initiated_rescan_ = true; // user-triggered repair (implies rescan) → completion should toast
|
||||
state_.sync.rescan_progress = 0.0f;
|
||||
state_.sync.rescan_status = decision.status;
|
||||
transactions_dirty_ = true;
|
||||
markTransactionsDirty();
|
||||
last_tx_block_height_ = -1;
|
||||
invalidateShieldedHistoryScanProgress(true);
|
||||
|
||||
@@ -7346,9 +7338,14 @@ std::string App::transactionSendProgressText() const
|
||||
std::string App::transactionRefreshProgressText() const
|
||||
{
|
||||
using Job = services::NetworkRefreshService::Job;
|
||||
if (!rpc_ || !rpc_->isConnected()) return {}; // Lite, or no node: there's no full-node history scan
|
||||
bool running = network_refresh_.jobInProgress(Job::Transactions);
|
||||
bool canRefresh = state_.connected && !state_.warming_up && !state_.isLocked();
|
||||
if (!running && !(canRefresh && transactions_dirty_)) return {};
|
||||
const auto work = historyScanWork();
|
||||
// The first load spans several cycles, and the dirty flag now clears after each: keep the banner up
|
||||
// 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 {};
|
||||
|
||||
char buf[128];
|
||||
|
||||
@@ -7358,26 +7355,23 @@ std::string App::transactionRefreshProgressText() const
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Initial bulk history load: the incremental shielded scan streams older transactions in over
|
||||
// many refresh cycles, so the first batch can appear long before the history is complete. Keep
|
||||
// the user informed (with a %) even after rows are on screen, so they know it's still loading.
|
||||
// Progress = fraction of z-addresses scanned at least once (monotonic). Goes quiet for good once
|
||||
// the first full scan completes; routine per-block re-scans don't re-trigger it.
|
||||
if (!initial_history_scan_complete_ && !state_.z_addresses.empty()) {
|
||||
std::size_t total = state_.z_addresses.size();
|
||||
std::size_t scanned = std::min(shielded_history_scan_heights_.size(), total);
|
||||
int pct = total > 0 ? static_cast<int>((scanned * 100) / total) : 0;
|
||||
// Initial bulk history load: the shielded receive scans stream older transactions in over several
|
||||
// refresh cycles, so the first batch can appear long before the history is complete. Keep the user
|
||||
// informed (with a %) even after rows are on screen, so they know it's still loading. Progress is the
|
||||
// share of z-addresses this pass has covered. Goes quiet for good once the first pass completes;
|
||||
// routine re-scans afterwards don't re-trigger it.
|
||||
if (initialLoad) {
|
||||
const std::size_t total = work.total;
|
||||
const std::size_t done = total - std::min(work.pending, total);
|
||||
int pct = static_cast<int>((done * 100) / total);
|
||||
if (pct > 99) pct = 99; // 100% is the "complete" state, which suppresses the banner
|
||||
snprintf(buf, sizeof(buf), TR("tx_loading_history_progress"), pct);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Once history is on screen, stay quiet for routine background refreshes. The incremental
|
||||
// shielded-receive scan re-dirties transactions_dirty_ on EVERY new block (each block moves the
|
||||
// "scanned at tip" bar, invalidating prior per-address scans), so for a wallet with more
|
||||
// z-addresses than the per-cycle scan budget the flag never clears — which left this banner lit
|
||||
// indefinitely. New receives still appear live as they're scanned; only the INITIAL load
|
||||
// (nothing displayed yet) and send enrichment (above) warrant a loading banner.
|
||||
// Once history is on screen, stay quiet for routine background refreshes: new receives appear live
|
||||
// as they're scanned; only the INITIAL load (nothing displayed yet) and send enrichment (above)
|
||||
// warrant a loading banner.
|
||||
if (!state_.transactions.empty()) return {};
|
||||
|
||||
if (!running && transactions_dirty_) return TR("tx_loading_queued");
|
||||
|
||||
Reference in New Issue
Block a user