perf(ui): paint the home feed from a window instead of the whole history - #929
perf(ui): paint the home feed from a window instead of the whole history#929QuantumExplorer wants to merge 1 commit into
Conversation
The feed showed nothing until every transaction had been read and classified. On a wallet with ~1.7k transactions and ~1.7k UTXOs that is seconds of work before the first row: activeWalletTxids scans every PersistentTxo and faults two relationships apiece to build a txid set, fetchAndWrap then wraps every matching row, and only then does the per-transaction pass (rate refresh, metadata across four providers, classification) run. LazyVStack renders lazily, but by then the cost is already paid — nothing was ever fetched as the user scrolled. Adds TransactionSource.recentTransactions(limit:) and publishes a first paint from the newest 60 before the full reload runs. The windowed fetch skips the txid prescan entirely: it walks transactions newest-first in pages and tests wallet membership per row — the same union fetchOne uses — so first paint reads about a screenful instead of the table. The first paint is deliberately partial and only ever adds rows. It omits aggregate rows (CrowdNode, the per-day CoinJoin mixing sets, the CoinJoin withdrawal set) along with the transactions that belong to them, because their membership is only correct once the whole history has been seen; rendering them from a window would show groups with the wrong contents, and correcting them afterwards would churn rows the user is already looking at. Shielded and Platform activity are likewise left to the full pass, which owns their whole-history reads. It touches neither txByHash nor the grouping sets — that state belongs to the full reload and the incremental-update path reads it. It runs only when no load has completed, so an incremental refresh never flashes a partial list over rows already on screen, and a full reload that lands first wins: the main-thread publish bails if hasLoadedInitialTxItems is already set. This is windowed first paint, not scroll-driven pagination — see the PR for why the aggregate rows make true paging a semantic change rather than a tuning knob. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughChangesRecent transaction feed
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant HomeViewModel
participant TransactionSource
participant HomeFeed
HomeViewModel->>TransactionSource: request recent transactions
TransactionSource-->>HomeViewModel: bounded transaction set
HomeViewModel->>HomeViewModel: filter and group provisional transactions
HomeViewModel->>HomeFeed: publish provisional feed
HomeViewModel->>TransactionSource: load full wallet history
TransactionSource-->>HomeViewModel: complete transaction history
HomeViewModel->>HomeFeed: replace provisional feed
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
Closing: measured, and this doesn't fix the symptom. Device log from the built branch: First paint works as designed, but lands only ~1s before the full reload it was meant to pre-empt — the full pass over all 1,756 transactions is ~1-2s, not the multi-second cost I inferred from reading the code. I never measured before proposing this, and the The real latency is upstream of the feed (the reload doesn't start until +3s), and the log also shows 7 full reloads in 36s, which is a better explanation for a sluggish feed than the initial load. Chasing those instead. |
Answering the question behind this: no, nothing was fetched as you scrolled
LazyVStackrenders lazily, but the data was fully materialised first. On every load, for a wallet with ~1.7k transactions and ~1.7k UTXOs:activeWalletTxidsfetches everyPersistentTxoand faultstransaction+spendingTransactionon each, purely to build a txid set.fetchAndWrapfetches every matchingPersistentTransactionand wraps all of them, each touchingisCoinJoinMixingTx(another relationship walk).txItemspublished, in one shot.No
fetchLimit, no window, no cursor anywhere in that path. Load time scales with total history, and it is paid on every full reload — not just launch.What this does
Adds
TransactionSource.recentTransactions(limit:)and publishes a first paint from the newest 60 before the full reload runs.The windowed fetch skips the txid prescan entirely: it walks transactions newest-first in pages and tests wallet membership per row (the same union
fetchOnealready uses), so first paint reads about a screenful rather than the whole table. Pages, because the store can hold other wallets' rows; bounded bymaxPagesso a store dominated by another wallet degrades to a short prefix instead of scanning to the end.The full reload is unchanged and still produces the authoritative result.
Why this is windowed first paint, not scroll-driven pagination
True paging would be a semantic change here, not a tuning knob. The feed contains aggregate rows whose membership is only correct once the entire history has been seen:
Build those from a 50-row window and they render with the wrong contents; extend the window on scroll and they mutate rows the user is already looking at. Shielded and Platform activity come from separate whole-history sources and interleave by date, so a Core-only page isn't a valid prefix of the timeline either.
So the first paint omits aggregate rows and the transactions belonging to them, rather than approximating them. Rows are only ever added, never corrected away underneath the user. Genuine pagination would need those aggregates reworked into something incrementally computable — a real piece of design work, and a much bigger change than this.
Safety
hasLoadedInitialTxItemsis already set — a full reload that finishes first wins, since its result is complete.txByHashnor the grouping sets: that state belongs to the full reload, and the incremental-update path reads it.hasRewardsHistory/hasMasternodeHistorystill come from the full pass over real history.Verification
dashpayscheme builds clean (arm64 simulator).Not measured on-device: the simulator is PIN-gated, so I have no before/after timing on the real wallet — the reasoning above is from the code path, not a profile. Worth confirming the first paint actually lands quickly on your ~1.7k-transaction wallet, and that the transition to the full list is not visibly jarring.
Summary by CodeRabbit