Skip to content

perf: cut app RPC load on open and while idle (Phase 16) - #56

Open
Romulus-Sol wants to merge 1 commit into
wp1-phase4-ondevice-fixesfrom
wp1-rpc-load-reduction
Open

perf: cut app RPC load on open and while idle (Phase 16)#56
Romulus-Sol wants to merge 1 commit into
wp1-phase4-ondevice-fixesfrom
wp1-rpc-load-reduction

Conversation

@Romulus-Sol

Copy link
Copy Markdown
Owner

Reported from the device: the app looks rate-limited and the heartbeat view takes a long time to load. Measured against the real endpoint, not estimated.

Stacks on wp1-phase4-ondevice-fixes (PR #55) since it touches the same files.

What was wrong

loadVaultState()                8 RPC calls, 558ms strictly sequential
  getAccountInfo(vault)            x2   (fetchVaultConfig, then again for rent)
  getParsedTokenAccountsByOwner    x4   (getVaultTokenBalances and PortfolioScanner
                                         each scan BOTH token programs)
  getMinimumBalanceForRentExemption, getBalance
refreshAuthoritativeDeadline()  4 RPC calls, 165ms

DashboardScreen called loadVaultState from both a mount useEffect and a useFocusEffect, with no single-flight guard → ~24 calls and ~1.45s serial on a VPS, which at mobile latency is 5–15s of apparent hang. Steady state: ~480 calls/hour per foregrounded Dashboard.

The deadline poll is new in Phase 4 — before it this cost was zero. The Phase 4 review flagged it as "worth measuring against the shared proxy before this ships widely" and didn't follow up. This is that prediction arriving.

Changes

Before After
loadVaultState on open twice, no guard once, single-flight
Its reads sequential, 558ms concurrent, 249ms
Deadline poll at Stage 0 30s → 480 calls/hr 300s → 48 calls/hr (−90%)

Cadence is adaptive: 30s within an hour of the deadline, 60s within a day, 300s beyond, with the freshness window scaled to match so a widened interval can't strand the countdown as stale.

Safety — unchanged, and asserted at the widest cadence

A projection still cannot enter Stage 4: stage4_refresh_required fires the moment a projection would cross finalDeadline, and executableByTime is pinned false on every projected snapshot. Two tests assert both at the 300s window.

Unknown or nonsensical margin falls back to the tightest cadence — backing off is an optimisation, never the fallback for missing information.

Concurrency preserves per-branch isolation: each read carries its own catch and resolves to a neutral value. A bare Promise.all would let one rejection discard every other result — the enrichment-drops-the-load failure this codebase has hit before.

One existing test updated, not deleted

A WP 4.5 guard asserted the literal 30_000 in source. Updated to assert the intent it protected — refreshes bounded, never free-running — via deadlineRefreshPlan( / scheduleRefresh / clearTimeout.

Validation

app 577/577 · tsc clean · preflight 27/27 · scans clean · 6 new tests, 2 of them safety invariants.

Not fixed here — ops change, owner's call

The app, notify-server and keeper-bot share one RPC key (verified by hashing: identical), and every keeper tick runs a whole-program getProgramAccounts scan regardless of whether anything is due. Separate keys are the single largest win and need no code change. Until then these client gains are shared with two server-side pollers on the same quota.

Reported from the device: the app looks rate-limited and the heartbeat view takes
a long time to load. Measured against the real endpoint rather than estimated.

loadVaultState issued 8 RPC calls in strict sequence (558ms), and DashboardScreen
called it from BOTH a mount useEffect and a useFocusEffect with no single-flight
guard, so app open cost ~24 calls and ~1.45s of serial latency from a VPS -- 5-15s
at mobile latency. Steady state then cost ~480 calls/hour per foregrounded
Dashboard for a deadline poll that is new in Phase 4; before it this was zero. The
Phase 4 review flagged it as worth measuring before shipping widely and did not
follow up. This is that prediction arriving.

Removes the duplicate mount effect (useFocusEffect already covers first focus and
connected/publicKey changes) and adds a single-flight guard, so open loads once.

Runs the four independent reads concurrently: 558ms -> 249ms. Each branch keeps its
own catch and resolves to a neutral value, because a bare Promise.all would let one
rejection discard every other result -- the enrichment-drops-the-load failure this
codebase has hit before.

Makes the deadline cadence adaptive: 30s within an hour of the deadline, 60s within
a day, 300s beyond, with the freshness window scaled to match so a widened interval
cannot strand the countdown as stale. At Stage 0 with a fortnight of margin that is
480 -> 48 calls/hour.

The execution boundary is untouched and is asserted at the widest cadence: a
projection still cannot enter Stage 4 (stage4_refresh_required on crossing
finalDeadline, executableByTime pinned false), and unknown or nonsensical margin
falls back to the TIGHTEST cadence -- backing off is an optimisation, never the
fallback for missing information.

A WP 4.5 guard test asserted the literal 30_000 in source. Updated to assert the
intent it protected -- refreshes bounded, never free-running -- rather than deleted.

Not addressed here: the app, notify-server and keeper-bot share one RPC key
(verified by hash), and each keeper tick runs a whole-program getProgramAccounts
scan. Separate keys are the larger win and are an ops change, logged as Phase 16.

app 577/577, tsc clean.
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fdd4665e-6587-4c19-bcdd-68861f368ae5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.

Warning

Your free Security trial is over. An organization admin can activate billing to continue.


Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant