perf(net): one balance scan per poll, filtered opstatus, a guarded merge scope
The Core poll ran z_gettotalbalance twice (minconf 0 and 1), each a whole- wallet scan under dragonxd's cs_main. The spendable (minconf=1) call now runs only after the display call succeeded, and only when something is unconfirmed or pending or the exact total moved: about 95% of polls make one scan instead of two. A forced post-send balance can no longer be lost to a refused enqueue, and pending-send debits aren't re-applied each tick. z_getoperationstatus is filtered to our opids with its real argument form ([[id,...]]; the old ["id"] was rejected), falling back to the unfiltered call if a daemon refuses the filter. The Shield/Merge dialog's own polls are filtered and session-guarded too, and finally reach Done; the dead RPCClient wrappers are deleted. The Merge/Consolidate scope scan becomes a tested state machine: one scan at a time, failures latch behind a Retry button, only "node not ready" retries by itself (10/30/60 s, at most 3), Lite never scans. Its 120 s calls are maintenance calls: every main-thread disconnect aborts one in flight first, so a lifecycle action no longer freezes the UI behind it. An aborted merge that was already sent says it may still go through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
#include <unordered_set>
|
||||
#include <deque>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "data/address_book.h"
|
||||
@@ -432,6 +434,42 @@ public:
|
||||
// richer pending-send path internally; this is for operations with no optimistic
|
||||
// transaction row of their own.
|
||||
void trackOperation(const std::string& opid);
|
||||
// The connected daemon rejected the opid-filtered z_getoperationstatus, so polls use the unfiltered
|
||||
// form (every retained op) for the rest of this connection.
|
||||
bool opidFilterUnsupported() const { return opid_filter_unsupported_.load(std::memory_order_relaxed); }
|
||||
|
||||
// RAII around one long ("maintenance") rpc_ call made on a worker thread with a timeout above the
|
||||
// default 30 s, e.g. a merge or a whole-wallet scan given 120 s. rpc_ holds its curl lock for the
|
||||
// whole transfer and disconnect() takes the same lock, so these are counted for
|
||||
// abortMaintenanceCalls().
|
||||
class MaintenanceCallScope {
|
||||
public:
|
||||
explicit MaintenanceCallScope(App& app) : app_(app)
|
||||
{
|
||||
// Count first, then read the flag; abortMaintenanceCalls() sets the flag, then reads the count.
|
||||
// Either this sees the abort and never sends, or the abort sees this call and cuts it off. (A
|
||||
// call queued behind the one disconnect() waits on could otherwise take the curl lock next
|
||||
// and hold disconnect() for its full 120 s.)
|
||||
app_.maint_calls_.fetch_add(1);
|
||||
if (app_.maint_abort_.load()) {
|
||||
app_.maint_calls_.fetch_sub(1);
|
||||
throw rpc::RpcTransportError(rpc::RpcFailureKind::Aborted, "RPC aborted: disconnecting");
|
||||
}
|
||||
}
|
||||
~MaintenanceCallScope() { app_.maint_calls_.fetch_sub(1); }
|
||||
MaintenanceCallScope(const MaintenanceCallScope&) = delete;
|
||||
MaintenanceCallScope& operator=(const MaintenanceCallScope&) = delete;
|
||||
private:
|
||||
App& app_;
|
||||
};
|
||||
// Main thread, right before rpc_->disconnect(): abort rpc_'s in-flight call if it is a maintenance
|
||||
// call, so the disconnect doesn't freeze the UI for up to its 120 s, and refuse maintenance calls that
|
||||
// start before the next connect (onConnected() lifts that). Ordinary calls are left to finish (at most
|
||||
// 30 s, as before), so a user's own send is never cut off mid-call.
|
||||
void abortMaintenanceCalls();
|
||||
// Whether a whole-wallet scan the user didn't ask for may be sent now: a connected, unlocked, synced
|
||||
// full node that is past warmup, not rescanning and not busy.
|
||||
bool walletScanReady() const;
|
||||
|
||||
// Force refresh
|
||||
void refreshNow();
|
||||
@@ -738,14 +776,18 @@ private:
|
||||
// t_addresses for `fromAddress` and clamp its balance at >=0. A positive `signedAmount`
|
||||
// restores a debit; a negative one applies it. When `includeAggregates` is set, adjust the
|
||||
// private/transparent bucket (chosen by the address's leading 'z') and totalBalance with the
|
||||
// same clamp. Shared by the three pending-send delta sites so they can't drift.
|
||||
// same clamp. Shared by the three pending-send delta sites so they can't drift. `includeAddress`
|
||||
// off leaves the per-address balances alone (the Core poll refreshes only the aggregates).
|
||||
void applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
|
||||
bool includeAggregates);
|
||||
bool includeAggregates, bool includeAddress = true);
|
||||
// Deliver a deferred z_sendmany result to its waiting UI callback once the opid
|
||||
// reaches a terminal status. Returns true if a callback was registered (and fired).
|
||||
bool invokeSendResultCallback(const std::string& opid, bool ok,
|
||||
const std::string& result);
|
||||
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
|
||||
// Re-apply every pending send's debit on top of freshly fetched balances: the aggregates after a
|
||||
// Core poll that carried a balance, the per-address balances after an address scan. Only onto
|
||||
// fresh values, or a debit applied again on every tick would drain them.
|
||||
void applyPendingSendBalanceDeltas(bool includeAggregateBalances, bool includeAddressBalances = true);
|
||||
std::string transactionHistoryCacheWalletIdentity() const;
|
||||
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
||||
// Record the active wallet's cached metadata (balance, address count, identity, size) into the
|
||||
@@ -1302,6 +1344,16 @@ private:
|
||||
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
|
||||
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
|
||||
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
|
||||
// Bumped with each forced-refresh request, so the poll that carried the forced balance clears the
|
||||
// flag only if no newer request came in meanwhile (see requestForcedBalanceRefresh()).
|
||||
std::uint32_t force_balance_seq_ = 0;
|
||||
// The exact minconf=0 total of the last balance poll: the minconf=1 call is skipped while it holds
|
||||
// still and nothing is pending (NetworkRefreshService::CoreRefreshPlan).
|
||||
std::optional<std::int64_t> last_total_balance_zat_;
|
||||
// Force the next balance poll through the throttle (the user's own send). Cleared by the Core poll
|
||||
// that actually sent the forced balance call, not at dispatch, so a dispatch the job queue refuses
|
||||
// (a Core job already in flight) or one that skips the balance (node behind or busy) keeps it.
|
||||
void requestForcedBalanceRefresh() { force_balance_refresh_ = true; ++force_balance_seq_; }
|
||||
// Same adaptive back-off applied to the other two O(mapWallet) scans that hold the daemon's cs_main:
|
||||
// the address scan (z_listunspent) and the history scan (z_listreceivedbyaddress). Without this, a
|
||||
// large shielded wallet re-scans them every tab cadence (~seconds each), saturating cs_main and
|
||||
@@ -1490,6 +1542,9 @@ private:
|
||||
std::unordered_set<std::string> witness_seen_txids_;
|
||||
int witness_total_txs_ = 0;
|
||||
bool opid_poll_in_progress_ = false;
|
||||
std::atomic<bool> opid_filter_unsupported_{false}; // see opidFilterUnsupported(); set on the worker
|
||||
std::atomic<int> maint_calls_{0}; // maintenance calls in flight (MaintenanceCallScope)
|
||||
std::atomic<bool> maint_abort_{false}; // set by abortMaintenanceCalls(), cleared on connect
|
||||
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
|
||||
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
|
||||
int consecutive_core_failures_ = 0;
|
||||
|
||||
Reference in New Issue
Block a user