Skip to content

perf(wallet): stop the login spinner waiting on three network races - #1342

Merged
bmc08gt merged 1 commit into
code/cashfrom
perf/wallet-login-latency
Aug 27, 2026
Merged

perf(wallet): stop the login spinner waiting on three network races#1342
bmc08gt merged 1 commit into
code/cashfrom
perf/wallet-login-latency

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

On a fresh login or account create the wallet tab draws nothing while
isAwaitingTokens || isAwaitingActivity (WalletScreenContent.kt:86). That is an
AND over three independent network races, none of which start before
AuthState.Ready, and each was slower than it needed to be. iOS seeds its wallet
from the session synchronously and gates only the tutorial card, which is most of
why it feels quicker.

Three changes, no behaviour dropped.

The balance fetch no longer idles for two seconds

tokenUpdater.poll(startIn = 2.seconds) is the only thing that fetches balances
after login — TokenCoordinator.onUserLoggedIn just hydrates Room, which is empty
on a new account — so the head start was two seconds of spinner spent deliberately
idle.

It could also be worse than two seconds. onAppInForeground fires twice at login,
once from the transition into AuthState.Ready and once from ON_RESUME, and
NetworkUpdater.poll stopped and relaunched unconditionally: the second edge
cancelled the in-flight fetch and re-served startIn, pushing the first fetch of a
fresh session further out. poll now uses its previously unused key parameter as
an identity and leaves an equivalent running loop alone. stop still resets on
background and logout, and NetworkUpdaterTest covers both.

The tutorial waits on chat hydration; the rest of the tab does not

onboardingItems stayed null until hasEverTipped() could answer, and that answer
waits on the per-chat backfill behind ChatHydrationState — a wait that scales with
how many conversations the account has. The milestone combine now always emits,
carrying isTipMilestoneResolved to say whether the tip half can be believed. Only
isNewUserTutorialComplete reads that flag, so "Scan a Tip Card" is still never
drawn as outstanding to someone who has already tipped, while the balance, the card
deck and the action tiles draw off state the app already holds.

The login feed seed is 25 rows, not 100

The count only bites on a cold cache; with anything cached fetchSinceLatest pages
forward from the newest row and takes whatever has happened since. So it is really
how much history a fresh login waits for before the wallet can draw, and the wallet
previews three rows. 25 still covers the history screen's first page, which pages at
20, and leaves the rest to that screen's own paging. The catch-up is guarded against
the repeated foreground edge as well, so the second one joins the in-flight fetch
instead of duplicating it.

Out of scope

The sequential per-chat backfill behind CatchUpComplete is untouched, and it is
still the long pole for an account with many conversations. This takes the wallet off
it rather than fixing it.

The wallet tab blanks itself while `isAwaitingTokens || isAwaitingActivity`, and
on a fresh login neither side resolves promptly.

The balance fetch idled behind `tokenUpdater.poll(startIn = 2.seconds)`. Nothing
else fetches balances after login — `TokenCoordinator.onUserLoggedIn` only
hydrates Room, which is empty on a new account — so that head start was a second
of spinner spent deliberately idle. It could also be worse than a second:
`onAppInForeground` fires twice at login, once from the transition into
`AuthState.Ready` and once from `ON_RESUME`, and `NetworkUpdater.poll` restarted
unconditionally, cancelling the in-flight fetch and re-serving the delay. `poll`
now keys on its previously unused `key` parameter and leaves an equivalent
running loop alone; `stop` still resets on background and logout.

`onboardingItems` stayed null until `hasEverTipped()` could answer, which waits
on the sequential per-chat backfill behind `ChatHydrationState`. The milestone
combine now always emits, carrying `isTipMilestoneResolved` to say whether the
tip half can be believed. Only `isNewUserTutorialComplete` reads that flag, so
"Scan a Tip Card" is still never drawn as outstanding to someone who already
tipped, while the balance, the card deck and the action tiles draw off state the
app already has, as iOS does.

The feed gate waited on a 100-row page. The count only bites on a cold cache —
with anything cached `fetchSinceLatest` pages forward from the newest row — so
it is really how much history a login waits for before the wallet can draw, and
the wallet previews three rows. 25 still covers the history screen's first page,
which pages at 20, and leaves the rest to that screen's own paging. The catch-up
is guarded against the repeated foreground edge as well, so the second one joins
the fetch rather than duplicating it.

The per-chat backfill is untouched. It remains the long pole for an account with
many conversations; the wallet just no longer waits on it.
@bmc08gt bmc08gt self-assigned this Aug 27, 2026
@bmc08gt
bmc08gt merged commit 62f5e04 into code/cash Aug 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant