perf(ui): incremental paged home timeline — reloads stop being O(wallet) - #974
Merged
QuantumExplorer merged 1 commit intoAug 10, 2026
Merged
Conversation
…llet) HomeViewModel's reload was the last O(wallet) hot path after the scoped- fetch work: every debounced sync/save tick re-materialized and re-wrapped the entire history (fetchAll -> walletTxRollup + full IN-fetch), measured 4.5-8.6s per tick on a 7,007-tx CoinJoin-heavy wallet. During a recovery sync that burned seconds of CPU per second, indefinitely. The feed now renders from a windowed row cache instead of the whole history: - First paint loads the newest ~100 rows via a firstSeen-index keyset page (fetchTimelineWindow). Pages are DAY-COMPLETED — the fetch always finishes its boundary calendar day, so a loaded day is never partially represented and the per-day CoinJoin mixing groups stay exact without a full-history pass. firstSeen doubles as the day key: the persister adopts the block timestamp once a tx is mined, so it matches the display date for settled history. - Scrolling to the feed's tail pages the next day-completed slice in (fetchOlderTimelinePage + a stamp-keyed sentinel row that re-fires while visible, so filters that match nothing keep paging). - Save/balance/sync ticks reconcile via fetchTimelineDelta: rows with lastUpdated past the reconcile stamp AND firstSeen inside the window, SQL-side. Restored history landing below the window costs nothing. Saves that DELETE feed rows (UnconfirmedTransactionRemover) and fiat- currency changes flag a window-sized refetch instead (deltas can't see deletions; cached wrappers hold currency-specific strings). - The Rewards/Masternode filter gates are answered by fetchLimit=1 existence probes on transactionTypeKind (coinbase=8, provider kinds 2-5) — never by scanning wrapped history. The cross-day "CoinJoin Withdrawals" group totals all tagged sweeps via point lookups (CoinJoinWithdrawalStore.allTxids) and renders once its day is paged in. Shielded/platform items clamp to the loaded day range; the "Date unknown" band renders only with full history loaded. - A recovery-sync growth cap trims an untouched full-history window back to ~one page past 400 rows (never after explicit paging). - Fixed in passing: UInt64 predicate bounds round-trip through SQLite's signed Int64, so a .max sentinel compared as -1 and matched nothing — scopedRows clamps bounds to Int64.max. TransactionSource grows the timeline API with fixture defaults (whole allTransactions as one complete window), so the onboarding stub and previews keep working unchanged. Measured on the synthetic 7,007-tx mainnet store (QA-iPhone16 sim, lldb in-process): first page 558ms cold / 173ms warm (100 rows), older page 310ms (104 rows), no-change delta 1ms, realistic 10-min delta 4ms (2 rows), filter-gate probes 12-30ms — vs 4.5-6.1s per full pass (idle; 8.6s under recovery-sync load). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
QuantumExplorer
merged commit Aug 10, 2026
80fabef
into
feat/marketplace-browse
2 of 3 checks passed
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue being fixed or feature implemented
After the scoped wallet-source fetches (93c370a), HomeViewModel's full reload was the last O(wallet) hot path: every debounced sync/save tick called
fetchAll()→fetchAndWrap, re-materializing and re-wrapping the entire history — measured 4.5–6.1s idle (8.6s under recovery-sync load) per tick on a 7,007-tx CoinJoin-heavy wallet. During a recovery sync the reload ticks burned seconds of CPU each, indefinitely.What was done?
The feed now renders from a windowed row cache instead of the whole history:
fetchTimelineWindowloads the newest ~100 rows via afirstSeen-index keyset scan and always finishes its boundary calendar day, so a loaded day is never partially represented and the per-day CoinJoin mixing groups stay exact without a full-history pass.firstSeendoubles as the day key: the persister adopts the block timestamp once a tx is mined, so it equals the display date for settled history.fetchOlderTimelinePage), auto-continuing while visible so a filter that matches nothing keeps paging.fetchTimelineDeltafetches only rows withlastUpdatedpast the reconcile stamp ANDfirstSeeninside the window, SQL-side. Restored history landing below the window costs nothing. Saves that delete feed rows (UnconfirmedTransactionRemover) and fiat-currency changes flag a window-sized refetch instead — deltas can't observe deletions, and cached wrappers hold currency-specific strings.fetchLimit=1existence probes ontransactionTypeKind(coinbase=8, provider kinds 2–5); the cross-day "CoinJoin Withdrawals" group totals every tagged sweep via point lookups (CoinJoinWithdrawalStore.allTxids) and renders once its day is paged in; shielded/platform items clamp to the loaded day range; the "Date unknown" band renders only with full history loaded.TransactionSourcegrows the timeline API with fixture defaults (wholeallTransactionsas one complete window), so the onboarding stub and previews work unchanged.#Predicateround-trip through SQLite's signed Int64 — a.max"no upper bound" sentinel compared as −1 and silently matched nothing.scopedRowsclamps bounds toInt64.max.How Has This Been Tested?
dashpaysimulator build (ARCHS=arm64).hasRewards/hasMasternodesboth true on the synthetic store).Reviewer notes: the delta stamp must never advance from older-page fetches (a paged-in row's
lastUpdatedcan postdate window updates the next delta still has to pick up); receipt matching for shielded items sees the loaded window — the only miss is a receipt up to 1h before an item across the window's bottom midnight, which self-resolves when that day pages in.Breaking Changes
None — UI behavior change only: history below the loaded window renders progressively as the user scrolls instead of all at once.
Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code