perf(net): one scan budget for all wallet scans, gated on what changed
The balance, address scan, history and probe were throttled one by one at 10% each, which added up to 30-40% of dragonxd's cs_main with all of them due. services::WalletScanGovernor runs one wallet scan at a time across both RPC lanes and keeps one shared quiet clock at 15% of wall-clock (cut when the node is behind). Waiters are served first in, first out, with a head start for what the page shows and a starvation floor of max(30 s visible / 900 s hidden, 15x the stream's cost). Explicit work (a dirty set, the user's send, a first load) is never refused but is charged. Routine scans run only when the tip, the mempool size (getmempoolinfo: no cs_main) or a wallet-mutation epoch changed since their last good scan, or a safety sweep is due. Pages that don't show the balance poll it at most every max(10 min, 50x its cost) (2 min with client-side auto-shield); coming back into view with an old value fetches it at once. The staleness badge counts from the last poll that confirmed nothing changed, and hides while the budget holds the balance within its guarantee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -343,6 +343,25 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
|
||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||
}
|
||||
|
||||
// The mempool's size: with the tip hash and the wallet's mutation epoch, what tells the caller whether a
|
||||
// wallet scan could see anything new. getmempoolinfo takes no cs_main. Failing leaves it unknown, which
|
||||
// counts as changed.
|
||||
bool mempoolFetched = false, mempoolOk = false;
|
||||
std::int64_t mempoolTxs = 0, mempoolBytes = 0;
|
||||
if (plan.fetchMempool && blockOk) {
|
||||
mempoolFetched = true;
|
||||
try {
|
||||
json pool = rpc.call("getmempoolinfo", json::array());
|
||||
if (pool.is_object()) {
|
||||
mempoolOk = true;
|
||||
if (pool.contains("size") && pool["size"].is_number_integer()) mempoolTxs = pool["size"].get<std::int64_t>();
|
||||
if (pool.contains("bytes") && pool["bytes"].is_number_integer()) mempoolBytes = pool["bytes"].get<std::int64_t>();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getmempoolinfo error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// If getblockchaininfo shows we're behind (same 2-block tolerance as applyCoreRefreshResult), skip
|
||||
// the balance scan this cycle regardless of includeBalance: the balance is incomplete mid-sync
|
||||
// anyway, and skipping it lets the sync-state update — and hence kSyncProfile — take effect at the
|
||||
@@ -400,6 +419,10 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
|
||||
result.balanceErrKind = balanceErrKind;
|
||||
result.balanceAttempted = balanceAttempted;
|
||||
result.spendableQueried = spendableQueried;
|
||||
result.mempoolFetched = mempoolFetched;
|
||||
result.mempoolOk = mempoolOk;
|
||||
result.mempoolTxs = mempoolTxs;
|
||||
result.mempoolBytes = mempoolBytes;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -773,6 +796,7 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
|
||||
result.addressListOk = false; // enumeration failed → the shielded list may be falsely short
|
||||
result.replyTimedOut |= rpc::rpcFailureKindOf(e) == rpc::RpcFailureKind::ReplyTimeout;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -799,11 +823,12 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
||||
result.replyTimedOut |= rpc::rpcFailureKindOf(e) == rpc::RpcFailureKind::ReplyTimeout;
|
||||
for (auto& info : result.shieldedAddresses) {
|
||||
try { // display total (minconf=0, includes pending change)
|
||||
json total = rpc.call("z_getbalance", json::array({info.address, 0}));
|
||||
if (!total.is_null()) info.balance = total.get<double>();
|
||||
} catch (...) {}
|
||||
} catch (...) { result.balancesOk = false; }
|
||||
try { // spendable (minconf=1); degrade to the display value on old daemons
|
||||
json conf = rpc.call("z_getbalance", json::array({info.address, 1}));
|
||||
info.spendableBalance = (!conf.is_null()) ? conf.get<double>() : info.balance;
|
||||
@@ -817,6 +842,7 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
|
||||
result.addressListOk = false; // enumeration failed → the transparent list may be falsely short
|
||||
result.replyTimedOut |= rpc::rpcFailureKindOf(e) == rpc::RpcFailureKind::ReplyTimeout;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -824,6 +850,8 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
applyTransparentBalancesFromUnspent(result.transparentAddresses, unspent);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
||||
result.balancesOk = false;
|
||||
result.replyTimedOut |= rpc::rpcFailureKindOf(e) == rpc::RpcFailureKind::ReplyTimeout;
|
||||
}
|
||||
|
||||
result.scanMs = std::chrono::duration<double, std::milli>(
|
||||
@@ -1114,6 +1142,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
result.transparentChainOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
transactionRpcError = true;
|
||||
result.replyTimedOut |= rpc::rpcFailureKindOf(e) == rpc::RpcFailureKind::ReplyTimeout;
|
||||
DEBUG_LOGF("listtransactions error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
@@ -1155,6 +1184,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight;
|
||||
} catch (const std::exception& e) {
|
||||
transactionRpcError = true;
|
||||
result.replyTimedOut |= rpc::rpcFailureKindOf(e) == rpc::RpcFailureKind::ReplyTimeout;
|
||||
DEBUG_LOGF("z_listreceivedbyaddress error: %s\n", e.what());
|
||||
}
|
||||
};
|
||||
@@ -1243,6 +1273,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
} catch (const std::exception& e) {
|
||||
transactionRpcError = true;
|
||||
result.sentSweepErrKind = rpc::rpcFailureKindOf(e);
|
||||
result.replyTimedOut |= result.sentSweepErrKind == rpc::RpcFailureKind::ReplyTimeout;
|
||||
DEBUG_LOGF("z_listsentbyaddress error: %s\n", e.what());
|
||||
}
|
||||
result.sentSweepMs = msSince(sweepStart);
|
||||
@@ -1447,6 +1478,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
|
||||
sortTransactionsNewestFirst(result.transactions);
|
||||
result.scanMs = msSince(scanStart);
|
||||
result.rpcError = transactionRpcError;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1499,6 +1531,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
result.rpcError = true;
|
||||
result.replyTimedOut |= rpc::rpcFailureKindOf(e) == rpc::RpcFailureKind::ReplyTimeout;
|
||||
DEBUG_LOGF("recent listtransactions error: %s\n", e.what());
|
||||
}
|
||||
|
||||
@@ -1540,6 +1574,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
||||
std::chrono::steady_clock::now() - callStart).count();
|
||||
} catch (const std::exception& e) {
|
||||
++result.shieldedAddressesScanned;
|
||||
result.rpcError = true;
|
||||
result.replyTimedOut |= rpc::rpcFailureKindOf(e) == rpc::RpcFailureKind::ReplyTimeout;
|
||||
DEBUG_LOGF("recent z_listreceivedbyaddress error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user