perf(chat): sends re-read by origin, chat scans under the scan budget
A z_sendmany submit set off a burst of wallet scans before anything had changed. Ops now re-read the wallet when they finish, by who started them: the user's sends and dialog ops get a re-read plus one forced balance once none of their ops is pending; client auto-shield re-reads unforced; chat messages and splits refresh only the chat note view. A pending send no longer forces scans, and chat txids no longer enter the tracked-send set. Chat ingest skips stored messages before decrypting and caches undecryptable ones per identity. The inbox scan runs when getwalletinfo's txcount changed (a cheap probe, at most 2% duty) or a cost-scaled safety net is due; the note-buffer scan runs on need. Both are admitted by the scan budget, on a steady clock. The chat payer comes from a per-address view of verified notes, with debits re-applied across in-flight scans; splits are sized to one payer and only when they can refill the buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+46
-25
@@ -832,6 +832,10 @@ void App::update()
|
||||
const double scanNow = scanClockNow();
|
||||
if (!network_refresh_.jobInProgress(Job::Core)) scan_governor_.dropOrphans(ScanStream::Balance, scanNow);
|
||||
if (!network_refresh_.jobInProgress(Job::Addresses)) scan_governor_.dropOrphans(ScanStream::Addresses, scanNow);
|
||||
// The chat scans aren't refresh jobs: their in-flight flag is set with the ticket and cleared by the reply
|
||||
// that completes it.
|
||||
if (!chat_fast_scan_in_flight_) scan_governor_.dropOrphans(ScanStream::ChatFast, scanNow);
|
||||
if (!chat_note_scan_in_flight_) scan_governor_.dropOrphans(ScanStream::ChatNotes, scanNow);
|
||||
if (!network_refresh_.jobInProgress(Job::Transactions)) {
|
||||
scan_governor_.dropOrphans(ScanStream::History, scanNow);
|
||||
scan_governor_.dropOrphans(ScanStream::RecentTx, scanNow);
|
||||
@@ -945,12 +949,14 @@ void App::update()
|
||||
// it can act; compiled away when the feature is off (constexpr gate inside).
|
||||
maybeProvisionChatIdentity();
|
||||
|
||||
// Chat note-buffer coordinator (BOTH variants). Full-node only: refresh the per-note counts via a
|
||||
// rate-limited z_listunspent worker scan (lite gets its counts from the refresh model, above). Then
|
||||
// pump: drain queued chat sends against verified notes and build/refill the buffer during idle. Both
|
||||
// self-gate to no-ops unless chat is engaged; the pump serializes to one send outstanding at a time.
|
||||
refreshChatNoteBudgetNode();
|
||||
// Chat note-buffer coordinator (BOTH variants). Pump: drain queued chat sends against verified notes and
|
||||
// build/refill the buffer during idle. Then, full-node only, refresh the per-note counts via the governed
|
||||
// z_listunspent scan (lite gets its counts from the refresh model, above). The pump goes first: a scan
|
||||
// posted in the same frame then queues on worker_ behind the new z_sendmany, which locks its notes, so
|
||||
// the scan's reply already reflects the spend. Both self-gate to no-ops unless chat is engaged; the pump
|
||||
// serializes to one send outstanding at a time.
|
||||
pumpChatNoteBuffer();
|
||||
refreshChatNoteBudgetNode();
|
||||
|
||||
// One-time reminder to back up the wallet's seed phrase (mnemonic wallets only).
|
||||
maybeRemindSeedBackup();
|
||||
@@ -1337,14 +1343,25 @@ void App::update()
|
||||
}
|
||||
return [this, parsed = std::move(parsed)]() mutable {
|
||||
opid_poll_in_progress_ = false;
|
||||
using Origin = services::NetworkRefreshService::OperationOrigin;
|
||||
// Classify first: the moves below consume the parse.
|
||||
const auto batch = services::NetworkRefreshService::planOperationBatch(parsed, op_origin_);
|
||||
auto originOf = [this](const std::string& id) {
|
||||
const auto it = op_origin_.find(id);
|
||||
return it == op_origin_.end() ? Origin::User : it->second;
|
||||
};
|
||||
|
||||
// Successes: hand the real txid to any waiting send UI callback.
|
||||
std::unordered_set<std::string> successfulOpids;
|
||||
for (const auto& [opid, txid] : parsed.successTxidsByOpid) {
|
||||
successfulOpids.insert(opid);
|
||||
markPendingSendTransactionSucceeded(opid, txid);
|
||||
send_txids_.insert(txid);
|
||||
send_txid_heights_.emplace(txid, state_.sync.blocks);
|
||||
// A chat op's record is its echo: tracking it would refresh the history every block and keep
|
||||
// the balance poll reading minconf=1 until it settles.
|
||||
if (originOf(opid) != Origin::Chat) {
|
||||
send_txids_.insert(txid);
|
||||
send_txid_heights_.emplace(txid, state_.sync.blocks);
|
||||
}
|
||||
invokeSendResultCallback(opid, true, txid);
|
||||
}
|
||||
|
||||
@@ -1373,9 +1390,6 @@ void App::update()
|
||||
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
|
||||
pending_opids_.end());
|
||||
}
|
||||
// An op finished (or vanished): whatever it did to the wallet, the scans gated on change
|
||||
// signals must look again.
|
||||
if (!terminalOpids.empty()) noteWalletMutation();
|
||||
|
||||
// Stale opids (no longer reported by the daemon): let any waiting send UI
|
||||
// know the outcome couldn't be confirmed rather than spinning forever.
|
||||
@@ -1392,15 +1406,21 @@ void App::update()
|
||||
}
|
||||
removePendingSendTransactions(successOpids, false);
|
||||
removePendingSendTransactions(failedOrStaleOpids, true);
|
||||
// Transaction confirmed by daemon — force immediate data refresh
|
||||
markTransactionsDirty();
|
||||
addresses_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
network_refresh_.markWalletMutationRefresh();
|
||||
} else {
|
||||
removePendingSendTransactions(terminalOpids, true);
|
||||
maybeFinishTransactionSendProgress();
|
||||
}
|
||||
// After the failure loop: maybeRetrySendForFeeGap reads the fee-gap marker there.
|
||||
for (const auto& id : terminalOpids) {
|
||||
op_origin_.erase(id);
|
||||
send_feegap_retried_opids_.erase(id);
|
||||
}
|
||||
// A user op re-reads the wallet and forces the headline balance (once no user op is pending);
|
||||
// the client auto-shield re-reads it unforced. A chat op only refreshes the chat note view: its
|
||||
// tx moves the mempool signal, and its record is the echo.
|
||||
if (batch.userSucceeded || batch.autoSucceeded) markWalletMutated(/*forceBalance=*/batch.userSucceeded);
|
||||
else if (batch.walletTerminal) noteWalletMutation(); // failed or vanished: look again (conservative)
|
||||
if (batch.chatTerminal) chat_note_rescan_requested_ = true; // the chat note view (payer, budget) changed
|
||||
maybeFinishTransactionSendProgress();
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1459,13 +1479,14 @@ void App::update()
|
||||
shouldRunWalletTransactionRefresh()) {
|
||||
if (refreshTransactionData()) network_refresh_.retrySoon(RefreshTimer::Transactions, kScanRetrySec);
|
||||
}
|
||||
// 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
|
||||
// normal harvest only re-scans on a new block. Self-gated (no-op without a chat identity /
|
||||
// connection / on lite); the in-flight guard keeps overlapping RPCs from stacking.
|
||||
// 0-conf chat on the Chat page: a cheap txcount probe each tick (the user's chat poll rate), and the
|
||||
// inbox scan when the wallet gained a transaction, as the scan budget admits it. A scan the budget held
|
||||
// back asks again within kScanRetrySec (a waiting stream must keep asking). The normal harvest only
|
||||
// re-scans on a new block. Self-gated (no-op without a chat identity / connection / on lite).
|
||||
chat_fast_scan_accum_ += logic_dt_;
|
||||
const float chatPoll = settings_ ? settings_->getChatPollRateSec() : 2.5f; // user-configurable
|
||||
if (chat_fast_scan_accum_ >= chatPoll) {
|
||||
const float chatTick = chat_inbox_scan_wanted_ ? std::min(chatPoll, kScanRetrySec) : chatPoll;
|
||||
if (chat_fast_scan_accum_ >= chatTick) {
|
||||
chat_fast_scan_accum_ = 0.0f;
|
||||
fastScanChatMemos();
|
||||
}
|
||||
@@ -1487,12 +1508,12 @@ void App::update()
|
||||
refreshAddressData();
|
||||
}
|
||||
} else if (addressTier != services::ScanTier::None && network_refresh_.isDue(RefreshTimer::Addresses)) {
|
||||
// Explicit need (a changed address set, or an in-flight send tracking its change output)
|
||||
// refreshes now (list-only while the scans are held). The routine poll on a wallet page runs
|
||||
// Explicit need (a changed address set, a committed send or other mutation) refreshes now
|
||||
// (list-only while the scans are held). The routine poll on a wallet page runs
|
||||
// only when something it depends on changed (tip, mempool, a wallet mutation) or its safety
|
||||
// sweep is due, and only as the scan budget allows; held back, it asks again soon.
|
||||
bool retry = false;
|
||||
if (addresses_dirty_ || hasTransactionSendProgress()) {
|
||||
if (addresses_dirty_) {
|
||||
refreshAddressData();
|
||||
} else if (walletDataPage && addressTier == services::ScanTier::Full) {
|
||||
using services::ScanStream;
|
||||
@@ -5325,7 +5346,7 @@ void App::refreshNow()
|
||||
noteWalletMutation(); // the user asked: the change-gated scans run, the balance too
|
||||
requestForcedBalanceRefresh();
|
||||
markTransactionsDirty(); // Force transaction list update
|
||||
addresses_dirty_ = true; // Force address/balance update
|
||||
markAddressesDirty(); // Force address/balance update
|
||||
last_tx_block_height_ = -1; // Reset tx cache
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user