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:
+54
-10
@@ -687,7 +687,9 @@ void App::tryConnect()
|
||||
|
||||
void App::onConnected()
|
||||
{
|
||||
maint_abort_.store(false); // maintenance calls queued before this connect have already failed (FIFO)
|
||||
state_.connected = true;
|
||||
ui::ShieldDialog::invalidateScope(); // inputs read before this connect may be another wallet's
|
||||
const bool completedSwitch = wallet_switch_pending_confirm_.exchange(false); // this connect confirms a switch
|
||||
// A successful connect completes a switch — close/clear the progress modal (kept open through the
|
||||
// stop → start → reconnect sequence, or already hidden via "continue in background").
|
||||
@@ -837,6 +839,9 @@ void App::onDisconnected(const std::string& reason)
|
||||
network_refresh_.resetJobs();
|
||||
rescan_status_poll_in_progress_ = false;
|
||||
opid_poll_in_progress_ = false;
|
||||
opid_filter_unsupported_ = false; // a reconnect may be to a different daemon
|
||||
last_total_balance_zat_.reset();
|
||||
ui::ShieldDialog::invalidateScope();
|
||||
address_validation_cache_dirty_ = true;
|
||||
resetTransactionHistoryCacheSession();
|
||||
|
||||
@@ -937,6 +942,7 @@ void App::handleLostConnection(const std::string& reason)
|
||||
DEBUG_LOGF("[Connection] %s — tearing down for reconnect\n", reason.c_str());
|
||||
// Flip the main client's connected_ flag so update()'s else-branch re-enters
|
||||
// tryConnect(). onDisconnected() alone only tears down the fast lane.
|
||||
abortMaintenanceCalls(); // a long call holds the curl lock disconnect() needs
|
||||
if (rpc_) rpc_->disconnect();
|
||||
onDisconnected(reason);
|
||||
}
|
||||
@@ -989,6 +995,24 @@ bool App::effectivelySyncing() const
|
||||
return std::time(nullptr) < sync_settle_until_;
|
||||
}
|
||||
|
||||
void App::abortMaintenanceCalls()
|
||||
{
|
||||
// Both stick until the next connect, which is fine here: every caller disconnects next.
|
||||
maint_abort_.store(true); // refuses maintenance calls still queued on the worker
|
||||
if (rpc_ && maint_calls_.load() > 0) rpc_->requestAbort(); // cuts off the one in flight
|
||||
}
|
||||
|
||||
bool App::walletScanReady() const
|
||||
{
|
||||
if (isLiteBuild() || capture_mode_ || shutting_down_.load()) return false;
|
||||
// state_.warming_up, not rpc_->isWarmingUp(): the client's flag is set when a connect lands in warmup
|
||||
// and stays set until the next reconnect, which kept this false for the whole session.
|
||||
if (!rpc_ || !rpc_->isConnected()) return false;
|
||||
if (!state_.connected || state_.warming_up || state_.isLocked()) return false;
|
||||
if (state_.sync.rescanning || runtime_rescan_active_ || effectivelySyncing()) return false;
|
||||
return !rpc_busy_gate_->busy();
|
||||
}
|
||||
|
||||
// Adaptive throttle: the next balance poll must wait at least (lastScanCost / kBalanceDutyCycle) since
|
||||
// the last one, so balance scanning can never occupy more than ~kBalanceDutyCycle of wall-clock. A
|
||||
// cheap wallet (sub-cadence cost) is unaffected — the tab's Core timer stays the real cadence; a ~20s
|
||||
@@ -1194,7 +1218,7 @@ void App::removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||
}
|
||||
|
||||
void App::applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
|
||||
bool includeAggregates)
|
||||
bool includeAggregates, bool includeAddress)
|
||||
{
|
||||
// For a debit (signedAmount < 0) this clamps at >=0 exactly as the debit sites did. For a
|
||||
// restore (signedAmount > 0) the clamp is a no-op — max(0, bal+amt) == bal+amt when bal,amt>=0 —
|
||||
@@ -1211,7 +1235,7 @@ void App::applyPendingSendDelta(const std::string& fromAddress, double signedAmo
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (!applyToAddress(state_.z_addresses)) applyToAddress(state_.t_addresses);
|
||||
if (includeAddress && !applyToAddress(state_.z_addresses)) applyToAddress(state_.t_addresses);
|
||||
if (includeAggregates) {
|
||||
if (!fromAddress.empty() && fromAddress[0] == 'z') {
|
||||
state_.spendablePrivateBalance = std::max(0.0, state_.spendablePrivateBalance + signedAmount);
|
||||
@@ -1244,11 +1268,11 @@ bool App::invokeSendResultCallback(const std::string& opid, bool ok,
|
||||
return true;
|
||||
}
|
||||
|
||||
void App::applyPendingSendBalanceDeltas(bool includeAggregateBalances)
|
||||
void App::applyPendingSendBalanceDeltas(bool includeAggregateBalances, bool includeAddressBalances)
|
||||
{
|
||||
for (const auto& [opid, pending] : pending_send_info_) {
|
||||
(void)opid;
|
||||
applyPendingSendDelta(pending.from, -pending.amount, includeAggregateBalances);
|
||||
applyPendingSendDelta(pending.from, -pending.amount, includeAggregateBalances, includeAddressBalances);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1458,6 +1482,7 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
|
||||
// start, so the daemon comes up on -wallet=<name>. WalletState clears on disconnect and the
|
||||
// per-wallet caches re-key off the new identity (P1), so no previous-wallet data leaks through.
|
||||
daemon_restarting_ = true;
|
||||
abortMaintenanceCalls(); // a long call holds the curl lock disconnect() needs
|
||||
if (rpc_ && rpc_->isConnected()) rpc_->disconnect();
|
||||
onDisconnected("Switching wallet");
|
||||
// The loaded wallet is changing — drop the old wallet's chat identity + decrypted store so it
|
||||
@@ -1885,15 +1910,31 @@ void App::refreshCoreData(bool allowBalance)
|
||||
// pending for after the unlock.
|
||||
const bool includeBalance = allowBalance && !effectivelySyncing() &&
|
||||
(force_balance_refresh_ || balanceRefreshDue());
|
||||
if (includeBalance) force_balance_refresh_ = false;
|
||||
// The forced request this poll carries. The result callback clears the flag only if the balance
|
||||
// call was really sent and no newer request came in; clearing it here lost it whenever the enqueue
|
||||
// was refused (a Core job already in flight) or the worker skipped the balance (behind, busy).
|
||||
const std::uint32_t forcedSeq = (includeBalance && force_balance_refresh_) ? force_balance_seq_ : 0;
|
||||
NetworkRefreshService::CoreRefreshPlan plan;
|
||||
plan.includeBalance = includeBalance;
|
||||
plan.needSpendable = NetworkRefreshService::spendableBalanceNeeded(
|
||||
state_, !pending_send_info_.empty() || !pending_opids_.empty() || !send_txids_.empty(),
|
||||
force_balance_refresh_);
|
||||
plan.prevTotalZat = last_total_balance_zat_;
|
||||
|
||||
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc, tracePage, includeBalance]() -> rpc::RPCWorker::MainCb {
|
||||
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc, tracePage, plan, forcedSeq]() -> rpc::RPCWorker::MainCb {
|
||||
AppRefreshRpcGateway refreshRpc(*rpc, traceSource(tracePage, "Core refresh"));
|
||||
auto result = NetworkRefreshService::collectCoreRefreshResult(refreshRpc, includeBalance);
|
||||
return [this, result]() {
|
||||
auto result = NetworkRefreshService::collectCoreRefreshResult(refreshRpc, plan);
|
||||
return [this, result, forcedSeq]() {
|
||||
try {
|
||||
NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr));
|
||||
applyPendingSendBalanceDeltas(true);
|
||||
if (result.balanceOk) {
|
||||
// Fresh aggregates: re-apply the pending sends' debits to them. The per-address
|
||||
// balances are the address scan's to refresh and re-debit.
|
||||
applyPendingSendBalanceDeltas(true, false);
|
||||
}
|
||||
last_total_balance_zat_ = NetworkRefreshService::nextTotalBaseline(result, last_total_balance_zat_);
|
||||
if (forcedSeq != 0 && result.balanceAttempted && forcedSeq == force_balance_seq_)
|
||||
force_balance_refresh_ = false;
|
||||
|
||||
// Feed the adaptive balance throttle + sync-settle hysteresis. Record the last scan's
|
||||
// cost (0 when balance was skipped), and arm the settle window only on the
|
||||
@@ -5182,6 +5223,7 @@ void App::beginAdoptSeedWallet()
|
||||
{ std::lock_guard<std::mutex> lk(seed_migration_mutex_); seed_migration_adopt_done_ = false; }
|
||||
// Gate the main-loop reconnect so tryConnect can't start the daemon while we swap wallet.dat.
|
||||
daemon_restarting_ = true;
|
||||
abortMaintenanceCalls(); // a long call holds the curl lock disconnect() needs
|
||||
if (rpc_ && rpc_->isConnected()) rpc_->disconnect();
|
||||
onDisconnected("Adopting seed wallet");
|
||||
// Adopting swaps in a brand-new seed wallet — drop the legacy wallet's chat identity + store so
|
||||
@@ -5302,6 +5344,7 @@ void App::restoreOriginalWallet()
|
||||
{ std::lock_guard<std::mutex> lk(wallet_restore_mutex_); wallet_restore_done_ = false; }
|
||||
daemon_restarting_ = true; // gate the reconnect loop while we swap files
|
||||
connection_status_ = TR("sb_restarting_daemon");
|
||||
abortMaintenanceCalls(); // a long call holds the curl lock disconnect() needs
|
||||
if (rpc_ && rpc_->isConnected()) rpc_->disconnect();
|
||||
onDisconnected("Restoring original wallet");
|
||||
ui::Notifications::instance().info(TR("wallet_restore_started"), 12.0f);
|
||||
@@ -5472,6 +5515,7 @@ void App::rebuildWalletDatabase()
|
||||
{ std::lock_guard<std::mutex> lk(wallet_restore_mutex_); wallet_restore_done_ = false; }
|
||||
daemon_restarting_ = true;
|
||||
connection_status_ = TR("sb_restarting_daemon");
|
||||
abortMaintenanceCalls(); // a long call holds the curl lock disconnect() needs
|
||||
if (rpc_ && rpc_->isConnected()) rpc_->disconnect();
|
||||
onDisconnected("Rebuilding wallet database");
|
||||
ui::Notifications::instance().info(TR("wallet_rebuild_started"), 15.0f);
|
||||
@@ -5813,7 +5857,7 @@ void App::submitZSendMany(const std::string& from, const std::string& to, double
|
||||
// Force transaction list refresh so the sent tx appears immediately
|
||||
transactions_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
force_balance_refresh_ = true; // the user's own send must update the balance now, past the throttle
|
||||
requestForcedBalanceRefresh(); // the user's own send must update the balance now, past the throttle
|
||||
network_refresh_.markWalletMutationRefresh();
|
||||
// z_sendmany only returned an opid: the transaction is built/signed/
|
||||
// broadcast asynchronously by the daemon. Defer the user-facing
|
||||
|
||||
Reference in New Issue
Block a user