perf(net): hold wallet scans at connect, in the background and on tab entry
- Connect gate: after the node starts answering, wallet scans wait until the lock state is known and the node knows its tip (or the wallet has fewer than 500 transactions), at most 20 s. A node already behind latches the sync profile from the connect's getinfo. Meanwhile a list-only pass keeps the address list and the cached history up, and the session's first balance and per-address balances are read once even while catching up, so the wallet shows its last-synced values instead of 0. - Background: minimized, hidden, or covered without focus (3 s debounce, any input cancels) polls only the chain state every 15 s. Wallet scans, the mining poll, price, nudges and auto-lock interaction tracking pause; auto-lock now fires while minimized. Leaving a held profile marks every timer due again. - Tab entry marks the timers due instead of forcing scans; a stale list or a never-loaded history still loads on entry. - The scan entry points own the hard gates, failed enumerations keep the previous lists behind a retry hold, and failed balance calls never overwrite known balances with 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
// Holds the wallet-wide scans (the balance, the address scan, the history) for a moment after the node
|
||||
// starts answering: until the lock state is known and either the node knows the chain tip or the wallet is
|
||||
// small. Right after warmup a node that is behind doesn't know it yet (longestchain stays 0 until its first
|
||||
// UpdateTip), so the first Core poll would read "synced" and a large wallet's scans would start, holding
|
||||
// cs_main ahead of the catch-up. Opens after kGraceSeconds whatever happens.
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
class ConnectScanGate {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static constexpr int kGraceSeconds = 20;
|
||||
static constexpr int kMinWalletTxs = 500; // below this the scans are cheap: don't hold them for the tip
|
||||
|
||||
void arm(Clock::time_point now, bool tipKnown)
|
||||
{
|
||||
armed_ = true;
|
||||
edgePending_ = true;
|
||||
tipKnown_ = tipKnown;
|
||||
armedAt_ = now;
|
||||
}
|
||||
void noteTipKnown() { tipKnown_ = true; }
|
||||
void disarm()
|
||||
{
|
||||
armed_ = false;
|
||||
edgePending_ = false;
|
||||
}
|
||||
bool armed() const { return armed_; }
|
||||
|
||||
// walletTxCount: getwalletinfo's txcount, -1 when unknown.
|
||||
bool open(Clock::time_point now, bool lockStateKnown, int walletTxCount) const
|
||||
{
|
||||
if (!armed_) return true;
|
||||
const bool smallWallet = walletTxCount >= 0 && walletTxCount < kMinWalletTxs;
|
||||
if (lockStateKnown && (tipKnown_ || smallWallet)) return true;
|
||||
return now - armedAt_ >= std::chrono::seconds(kGraceSeconds);
|
||||
}
|
||||
// True once per arm(), the first time open() is: what the gate held is owed now.
|
||||
bool consumeOpenEdge(Clock::time_point now, bool lockStateKnown, int walletTxCount)
|
||||
{
|
||||
if (!armed_ || !edgePending_ || !open(now, lockStateKnown, walletTxCount)) return false;
|
||||
edgePending_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool armed_ = false;
|
||||
bool edgePending_ = false;
|
||||
bool tipKnown_ = false;
|
||||
Clock::time_point armedAt_{};
|
||||
};
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
@@ -247,9 +247,21 @@ NetworkRefreshService::WalletEncryptionResult NetworkRefreshService::parseWallet
|
||||
result.ok = false;
|
||||
}
|
||||
}
|
||||
if (walletInfo.contains("txcount") && walletInfo["txcount"].is_number_integer())
|
||||
result.txCount = walletInfo["txcount"].get<int>();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool NetworkRefreshService::connectionInfoTipKnown(const ConnectionInfoResult& info)
|
||||
{
|
||||
return info.ok && info.longestChain && *info.longestChain > 0;
|
||||
}
|
||||
|
||||
bool NetworkRefreshService::connectionInfoShowsBehind(const ConnectionInfoResult& info)
|
||||
{
|
||||
return connectionInfoTipKnown(info) && info.blocks && *info.blocks < *info.longestChain - 2;
|
||||
}
|
||||
|
||||
NetworkRefreshService::WarmupPollResult NetworkRefreshService::collectWarmupPollResult(RefreshRpcGateway& rpc)
|
||||
{
|
||||
WarmupPollResult result;
|
||||
@@ -378,7 +390,7 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
|
||||
// balance scans: they need the same cs_main, and each would only take another of its few RPC slots.
|
||||
bool balanceAttempted = false;
|
||||
bool spendableQueried = false;
|
||||
if (plan.includeBalance && !behind && !rpc::rpcKindMeansBusy(blockErrKind)) {
|
||||
if (plan.includeBalance && (!behind || plan.evenIfBehind) && !rpc::rpcKindMeansBusy(blockErrKind)) {
|
||||
// z_gettotalbalance is O(mapWallet) and holds the daemon's cs_main for its whole duration —
|
||||
// seconds on a large shielded wallet. Time it so the caller can throttle how often it polls
|
||||
// (balanceRefreshDue()), keeping balance scans from starving block connection.
|
||||
@@ -402,7 +414,9 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
|
||||
const auto total = totalBalance.is_object() ? totalBalance.find("total") : totalBalance.end();
|
||||
if (total != totalBalance.end() && total->is_string()) totalZat = parseMoneyZat(total->get<std::string>());
|
||||
const bool totalMoved = !totalZat || !plan.prevTotalZat || *totalZat != *plan.prevTotalZat;
|
||||
if (plan.needSpendable || totalMoved) {
|
||||
// Not on the session's first read while the node is behind: nothing can be sent until it caught
|
||||
// up anyway, and it is a second whole-wallet scan into the catch-up.
|
||||
if ((plan.needSpendable || totalMoved) && !(behind && plan.evenIfBehind)) {
|
||||
spendableQueried = true;
|
||||
try { // If it fails, spendable degrades to display in apply.
|
||||
spendableBalance = rpc.call("z_gettotalbalance", json::array({1}));
|
||||
@@ -753,9 +767,12 @@ NetworkRefreshService::AddressRefreshSnapshot NetworkRefreshService::buildAddres
|
||||
|
||||
NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddressRefreshResult(
|
||||
RefreshRpcGateway& rpc,
|
||||
const AddressRefreshSnapshot& snapshot)
|
||||
const AddressRefreshSnapshot& snapshot,
|
||||
bool listOnly)
|
||||
{
|
||||
AddressRefreshResult result;
|
||||
result.listOnly = listOnly;
|
||||
|
||||
// Time the whole scan — z_listunspent (and per-address z_getbalance fallback) hold the daemon's
|
||||
// cs_main for the duration, seconds on a large shielded wallet. The measured cost feeds the
|
||||
// caller's adaptive throttle so the address poll can't starve block connection.
|
||||
@@ -797,9 +814,23 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
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;
|
||||
result.nodeBusy |= rpc::rpcKindMeansBusy(rpc::rpcFailureKindOf(e));
|
||||
}
|
||||
|
||||
try {
|
||||
json tList = rpc.call("getaddressesbyaccount", json::array({""}));
|
||||
result.transparentAddresses = parseTransparentAddressList(tList);
|
||||
} 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;
|
||||
result.nodeBusy |= rpc::rpcKindMeansBusy(rpc::rpcFailureKindOf(e));
|
||||
}
|
||||
|
||||
// The balances need the lists they belong to: none in a list-only pass, and none when an enumeration
|
||||
// failed (the caller discards the whole pass and keeps the previous lists).
|
||||
const bool scanBalances = !listOnly && result.addressListOk;
|
||||
if (scanBalances) try {
|
||||
json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change
|
||||
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
|
||||
// Retain a minimal view so a downstream consumer (chat note-budget) can reuse this scan instead
|
||||
@@ -822,8 +853,15 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
const auto kind = rpc::rpcFailureKindOf(e);
|
||||
result.replyTimedOut |= kind == rpc::RpcFailureKind::ReplyTimeout;
|
||||
if (rpc::rpcKindMeansBusy(kind)) {
|
||||
// The node is busy: every z_getbalance would be held back and read 0. Nothing more this pass.
|
||||
DEBUG_LOGF("z_listunspent: node busy (%s)\n", e.what());
|
||||
result.balancesOk = false;
|
||||
result.nodeBusy = true;
|
||||
} else {
|
||||
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}));
|
||||
@@ -834,24 +872,23 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
info.spendableBalance = (!conf.is_null()) ? conf.get<double>() : info.balance;
|
||||
} catch (...) { info.spendableBalance = info.balance; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
json tList = rpc.call("getaddressesbyaccount", json::array({""}));
|
||||
result.transparentAddresses = parseTransparentAddressList(tList);
|
||||
} 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 {
|
||||
if (scanBalances && !result.nodeBusy) try {
|
||||
json unspent = rpc.call("listunspent", json::array({0})); // minconf=0 → include 0-conf change
|
||||
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.nodeBusy |= rpc::rpcKindMeansBusy(rpc::rpcFailureKindOf(e));
|
||||
}
|
||||
// A balance that wasn't read reads 0: never overwrite known balances with that. The pass degrades to
|
||||
// list-only (apply carries the previous balances), and its notes aren't a full view either.
|
||||
if (scanBalances && !result.balancesOk) {
|
||||
result.listOnly = true;
|
||||
result.unspentNotes.clear();
|
||||
}
|
||||
|
||||
result.scanMs = std::chrono::duration<double, std::milli>(
|
||||
@@ -1694,6 +1731,7 @@ void NetworkRefreshService::applyWalletEncryptionResult(WalletState& state, cons
|
||||
state.unlocked_until = 0;
|
||||
}
|
||||
state.encryption_state_known = true;
|
||||
if (result.txCount) state.wallet_tx_count = *result.txCount;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result)
|
||||
@@ -1810,6 +1848,22 @@ void NetworkRefreshService::applyPriceRefreshFailure(WalletState& state,
|
||||
void NetworkRefreshService::applyAddressRefreshResult(WalletState& state,
|
||||
AddressRefreshResult&& result)
|
||||
{
|
||||
if (result.listOnly) {
|
||||
// No balance scan ran: keep each address's last known balances (a new address shows 0 until the
|
||||
// next full pass).
|
||||
auto carry = [](std::vector<AddressInfo>& fresh, const std::vector<AddressInfo>& previous) {
|
||||
std::unordered_map<std::string, const AddressInfo*> known;
|
||||
for (const auto& info : previous) known[info.address] = &info;
|
||||
for (auto& info : fresh) {
|
||||
const auto it = known.find(info.address);
|
||||
if (it == known.end()) continue;
|
||||
info.balance = it->second->balance;
|
||||
info.spendableBalance = it->second->spendableBalance;
|
||||
}
|
||||
};
|
||||
carry(result.shieldedAddresses, state.z_addresses);
|
||||
carry(result.transparentAddresses, state.t_addresses);
|
||||
}
|
||||
state.z_addresses = std::move(result.shieldedAddresses);
|
||||
state.t_addresses = std::move(result.transparentAddresses);
|
||||
// Keep the combined `addresses` view in sync with the authoritative z/t lists.
|
||||
|
||||
@@ -91,6 +91,7 @@ public:
|
||||
bool ok = false;
|
||||
bool encrypted = false;
|
||||
std::int64_t unlockedUntil = 0;
|
||||
std::optional<int> txCount; // getwalletinfo's txcount (the wallet's transactions)
|
||||
};
|
||||
|
||||
struct WarmupPollResult {
|
||||
@@ -147,6 +148,7 @@ public:
|
||||
bool needSpendable = true;
|
||||
std::optional<std::int64_t> prevTotalZat;
|
||||
bool fetchMempool = false; // add getmempoolinfo after a successful getblockchaininfo
|
||||
bool evenIfBehind = false; // the session's first balance read: also while the node is behind
|
||||
};
|
||||
|
||||
struct MiningRefreshResult {
|
||||
@@ -208,6 +210,10 @@ public:
|
||||
std::vector<UnspentNoteLite> unspentNotes;
|
||||
bool balancesOk = true; // false: a balance call failed, the per-address balances may be stale
|
||||
bool replyTimedOut = false; // a call hit the reply timeout (the daemon kept running it)
|
||||
// The lists only: no z_listunspent / listunspent, or a balance call failed (the pass then degrades to
|
||||
// list-only). Applying it carries the previous balances over.
|
||||
bool listOnly = false;
|
||||
bool nodeBusy = false; // a call failed because the node was busy (timeout, full queue, held back)
|
||||
};
|
||||
|
||||
struct AddressRefreshSnapshot {
|
||||
@@ -317,6 +323,10 @@ public:
|
||||
static Intervals intervalsForPage(ui::NavPage page) { return RefreshScheduler::intervalsForPage(page); }
|
||||
|
||||
static ConnectionInfoResult parseConnectionInfoResult(const nlohmann::json& info);
|
||||
// The node knows the chain tip: longestchain stays 0 after a start until its first UpdateTip.
|
||||
static bool connectionInfoTipKnown(const ConnectionInfoResult& info);
|
||||
// The tip is known and the node is more than 2 blocks behind it (the sync tolerance).
|
||||
static bool connectionInfoShowsBehind(const ConnectionInfoResult& info);
|
||||
static WalletEncryptionResult parseWalletEncryptionResult(const nlohmann::json& walletInfo);
|
||||
static WarmupPollResult collectWarmupPollResult(RefreshRpcGateway& rpc);
|
||||
static ConnectionInitResult collectConnectionInitResult(
|
||||
@@ -378,9 +388,13 @@ public:
|
||||
static void applyTransparentBalancesFromUnspent(std::vector<AddressInfo>& addresses,
|
||||
const nlohmann::json& unspent);
|
||||
static AddressRefreshSnapshot buildAddressRefreshSnapshot(const WalletState& state);
|
||||
// listOnly: enumerate the addresses only (cheap), without the wallet-wide balance scans; for while the
|
||||
// scans are held (the node catching up, the connect gate). Both lists are enumerated first, and the
|
||||
// balances are scanned only when both enumerations succeeded (a failed pass is discarded whole).
|
||||
static AddressRefreshResult collectAddressRefreshResult(
|
||||
RefreshRpcGateway& rpc,
|
||||
const AddressRefreshSnapshot& snapshot = {});
|
||||
const AddressRefreshSnapshot& snapshot = {},
|
||||
bool listOnly = false);
|
||||
static TransactionRefreshSnapshot buildTransactionRefreshSnapshot(const WalletState& state,
|
||||
const TransactionViewCache& viewTxCache,
|
||||
const std::unordered_set<std::string>& sendTxids);
|
||||
|
||||
@@ -26,6 +26,18 @@ RefreshScheduler::Intervals RefreshScheduler::intervalsForPage(ui::NavPage page)
|
||||
}
|
||||
}
|
||||
|
||||
RefreshScheduler::Intervals RefreshScheduler::selectIntervals(ui::NavPage page, bool syncing, bool background)
|
||||
{
|
||||
if (background) return kBackgroundProfile;
|
||||
if (syncing) return kSyncProfile;
|
||||
return intervalsForPage(page);
|
||||
}
|
||||
|
||||
static_assert(RefreshScheduler::kSyncProfile.core * 2.0f <= RefreshScheduler::kSyncSettleSeconds,
|
||||
"the sync settle window must outlast two Core polls");
|
||||
static_assert(RefreshScheduler::kBackgroundProfile.core * 2.0f <= RefreshScheduler::kSyncSettleSeconds,
|
||||
"the sync settle window must outlast two Core polls");
|
||||
|
||||
void RefreshScheduler::applyPage(ui::NavPage page)
|
||||
{
|
||||
setIntervals(intervalsForPage(page));
|
||||
|
||||
@@ -43,8 +43,17 @@ public:
|
||||
// the lightweight Console tab syncs faster than the Peers tab. Reverts to the per-tab profile
|
||||
// once sync completes. (Tx/address/balance data is incomplete mid-sync anyway.)
|
||||
static constexpr Intervals kSyncProfile{10.0f, 0.0f, 0.0f, 0.0f};
|
||||
// While the window is in the background (minimized, hidden, or covered without focus): only the chain
|
||||
// state, slower. It keeps sync detection, the lost-connection check and the balance's own rare poll
|
||||
// (auto-shield) alive; the wallet scans and peers wait for the window to come back.
|
||||
static constexpr Intervals kBackgroundProfile{15.0f, 0.0f, 0.0f, 0.0f};
|
||||
// How long the sync profile holds after the node last read "behind" (see the Core callback). Must
|
||||
// outlast two Core polls in every held profile, or it could lapse between two "behind" readings.
|
||||
static constexpr float kSyncSettleSeconds = 30.0f;
|
||||
|
||||
static Intervals intervalsForPage(ui::NavPage page);
|
||||
// The profile in force: background, else syncing, else the page's.
|
||||
static Intervals selectIntervals(ui::NavPage page, bool syncing, bool background);
|
||||
|
||||
void applyPage(ui::NavPage page);
|
||||
void setIntervals(Intervals intervals);
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
// The hard gates in front of the wallet-wide scans: whether the node can take one at all right now. The
|
||||
// WalletScanGovernor decides on top of them how often. The scan entry points (refreshAddressData,
|
||||
// refreshTransactionData) check these themselves, so no caller (tab entry, refreshData, an import, the
|
||||
// history's re-route through the address scan) can send a scan into a node that is syncing, warming up,
|
||||
// rescanning, busy or locked, or while the window is in the background. The cheap address listing (no
|
||||
// wallet scan) still runs for a stale list, and the session's first balances are read once even while the
|
||||
// node catches up (otherwise every balance would read 0 for the whole catch-up).
|
||||
//
|
||||
// Pure, header-only.
|
||||
|
||||
#include "ui/sidebar.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
enum class ScanTier {
|
||||
None, // nothing
|
||||
ListOnly, // the address lists only (z_listaddresses, getaddressesbyaccount): cheap, no wallet scan
|
||||
Full, // the lists and the per-address balances (z_listunspent, listunspent)
|
||||
};
|
||||
|
||||
struct ScanContext {
|
||||
bool connected = false;
|
||||
bool locked = false;
|
||||
bool warmingUp = false;
|
||||
bool runtimeRescan = false;
|
||||
bool rpcBusy = false;
|
||||
bool syncing = false; // effectivelySyncing(): behind, or settling after catching up
|
||||
bool gateOpen = true; // the ConnectScanGate
|
||||
bool background = false; // the window is minimized, hidden, or covered without focus
|
||||
};
|
||||
|
||||
// The node answers RPCs that take cs_main without queueing behind something long.
|
||||
inline bool nodeCanAnswerScans(const ScanContext& c)
|
||||
{
|
||||
return c.connected && !c.warmingUp && !c.runtimeRescan && !c.rpcBusy;
|
||||
}
|
||||
|
||||
// A wallet-wide scan (O(wallet), holds cs_main) may be sent.
|
||||
inline bool wholeWalletScanAllowed(const ScanContext& c)
|
||||
{
|
||||
return nodeCanAnswerScans(c) && !c.background && !c.locked && !c.syncing && c.gateOpen;
|
||||
}
|
||||
|
||||
// The session's first read of a wallet value (the balance, the per-address balances): like
|
||||
// wholeWalletScanAllowed, but also while the node catches up. Once, so the catch-up shows the wallet's last
|
||||
// known values instead of 0.
|
||||
inline bool firstReadAllowed(const ScanContext& c)
|
||||
{
|
||||
return nodeCanAnswerScans(c) && !c.background && !c.locked && c.gateOpen;
|
||||
}
|
||||
|
||||
// listStale: never loaded this connection, or the address set may have changed (import, sweep,
|
||||
// decrypt-import). A stale list still loads while the scans are held, without the balances.
|
||||
// balancesOwed: no full pass has read the per-address balances yet this session.
|
||||
inline ScanTier planAddressScan(const ScanContext& c, bool listStale, bool balancesOwed = false)
|
||||
{
|
||||
if (wholeWalletScanAllowed(c)) return ScanTier::Full;
|
||||
if (balancesOwed && firstReadAllowed(c)) return ScanTier::Full;
|
||||
return (nodeCanAnswerScans(c) && listStale) ? ScanTier::ListOnly : ScanTier::None;
|
||||
}
|
||||
|
||||
// What entering a page does. Wallet data isn't fetched on entry any more (the periodic refresh and the
|
||||
// scan governor decide), except what the page can't show without: a stale address list, a never-loaded
|
||||
// history.
|
||||
struct PageEntryPlan {
|
||||
bool core = false;
|
||||
bool peers = false;
|
||||
bool mining = false;
|
||||
ScanTier addressesNow = ScanTier::None;
|
||||
bool transactionsNow = false;
|
||||
bool markAddresses = false;
|
||||
bool markTransactions = false;
|
||||
};
|
||||
|
||||
inline PageEntryPlan planPageEntry(ui::NavPage page, const ScanContext& c, bool addressListStale,
|
||||
bool transactionsNeverLoaded)
|
||||
{
|
||||
using NP = ui::NavPage;
|
||||
PageEntryPlan p;
|
||||
if (!c.connected) return p;
|
||||
switch (page) {
|
||||
case NP::Overview:
|
||||
p.core = !c.runtimeRescan && !c.rpcBusy;
|
||||
break;
|
||||
case NP::Peers:
|
||||
p.peers = !c.rpcBusy;
|
||||
break;
|
||||
case NP::Mining:
|
||||
p.mining = !c.warmingUp && !c.runtimeRescan && !c.rpcBusy;
|
||||
break;
|
||||
case NP::Send:
|
||||
case NP::Receive:
|
||||
case NP::History: {
|
||||
const ScanTier tier = planAddressScan(c, addressListStale);
|
||||
const bool loadNow = addressListStale && tier != ScanTier::None;
|
||||
if (loadNow) p.addressesNow = tier;
|
||||
else if (page != NP::History) p.markAddresses = true; // History needs the addresses only when stale
|
||||
if (page == NP::History) {
|
||||
if (!loadNow && transactionsNeverLoaded && wholeWalletScanAllowed(c)) p.transactionsNow = true;
|
||||
else p.markTransactions = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// The window counts as in the background when it is minimized or hidden, or covered while none of the
|
||||
// app's windows has focus (SDL reports occlusion on X11, Wayland and macOS, never on Windows).
|
||||
inline bool windowLooksBackground(bool minimized, bool hidden, bool occluded, bool appHasFocus)
|
||||
{
|
||||
return minimized || hidden || (occluded && !appHasFocus);
|
||||
}
|
||||
|
||||
constexpr double kBackgroundDebounceSeconds = 3.0;
|
||||
constexpr double kBackgroundInputGraceSeconds = 2.0;
|
||||
|
||||
// Background once it has looked so for a few seconds with no input meanwhile (a restore is immediate).
|
||||
inline bool isBackgroundWindow(bool flagged, double flaggedForSeconds, double secondsSinceInput)
|
||||
{
|
||||
return flagged && flaggedForSeconds >= kBackgroundDebounceSeconds &&
|
||||
secondsSinceInput >= kBackgroundInputGraceSeconds;
|
||||
}
|
||||
|
||||
// After a failed address enumeration, hold address refreshes for max(10 s, 10x what that pass cost). A pass
|
||||
// that failed because the node was busy (a reply timeout, a full queue, held back by the busy gate) gets the
|
||||
// floor: its wall time was the wait for cs_main, not its cost, and the busy gate already paces a busy node.
|
||||
inline double addressRetryDelaySeconds(double failedScanMs, bool nodeBusy = false)
|
||||
{
|
||||
if (nodeBusy) return 10.0;
|
||||
return std::max(10.0, (std::max(0.0, failedScanMs) / 1000.0) / 0.10);
|
||||
}
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
@@ -46,8 +46,13 @@ double hiddenBalanceIntervalSec(double emaCostSec, bool autoShieldArmed)
|
||||
|
||||
BalancePollDecision decideBalancePoll(const BalancePollInputs& in)
|
||||
{
|
||||
if (!in.allowBalance || in.syncing) return BalancePollDecision::Skip;
|
||||
if (in.forced || in.neverFetched) return BalancePollDecision::Explicit;
|
||||
if (!in.allowBalance || !in.gateOpen) return BalancePollDecision::Skip;
|
||||
// The session's first read runs even while the node catches up, once per connection and only when
|
||||
// someone can see it: otherwise the total reads 0 for the whole catch-up. After that (or if that one
|
||||
// attempt failed) the balance waits for the sync to finish, a forced one too.
|
||||
if (in.neverFetched && !(in.syncing && (in.background || in.firstReadTried))) return BalancePollDecision::Explicit;
|
||||
if (in.syncing) return BalancePollDecision::Skip;
|
||||
if (in.forced) return BalancePollDecision::Explicit;
|
||||
if (!in.changed) return BalancePollDecision::Skip;
|
||||
if (in.visible) {
|
||||
// Just came into view with an old value: fetch it now rather than queue behind the other streams.
|
||||
|
||||
@@ -78,7 +78,10 @@ ScanVisibility scanVisibilityForPage(ui::NavPage page, bool windowBackground);
|
||||
enum class BalancePollDecision { Skip, AskGovernor, Explicit };
|
||||
struct BalancePollInputs {
|
||||
bool allowBalance = true; // false for the locked wallet's chain-state poll
|
||||
bool syncing = false; // the node is catching up (or settling after it)
|
||||
bool syncing = false; // the node is catching up (or settling after it): only a first read runs
|
||||
bool background = false; // the window is in the background: no first read while syncing
|
||||
bool firstReadTried = false; // the first read while syncing was already sent this connection
|
||||
bool gateOpen = true; // the connect gate (C44): closed right after a connect, at most ~20 s
|
||||
bool forced = false; // the user's own send
|
||||
bool neverFetched = false; // no balance yet this session
|
||||
bool changed = true; // the governor's change gate
|
||||
|
||||
Reference in New Issue
Block a user