perf: cut app RPC load on open and while idle (Phase 16) - #56
Conversation
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.
|
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 Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
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
DashboardScreencalledloadVaultStatefrom both a mountuseEffectand auseFocusEffect, 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
loadVaultStateon openCadence 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_requiredfires the moment a projection would crossfinalDeadline, andexecutableByTimeis pinnedfalseon 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.allwould 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_000in source. Updated to assert the intent it protected — refreshes bounded, never free-running — viadeadlineRefreshPlan(/scheduleRefresh/clearTimeout.Validation
app 577/577 ·
tscclean · 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
getProgramAccountsscan 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.