Skip to content

perf(mobile): gate dictation setup progress polling on foreground + single-flight - #9892

Merged
nwparker merged 2 commits into
mainfrom
nwparker/perf-mobile-dictation-poll-gate
Jul 22, 2026
Merged

nwparker merged 2 commits into
mainfrom
nwparker/perf-mobile-dictation-poll-gate

Conversation

@nwparker

@nwparker nwparker commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

What

The mobile dictation setup progress polls (voice-settings.tsx screen and MobileDictationSetupSheet.tsx) fired a setup-state RPC every 1.5s during model download/extraction, with no AppState/focus gate and no single-flight guard — so they kept polling while backgrounded/covered, and a slow relay could overlap successive ticks (up to ~40 RPC/min per poller).

Fix: a shared DictationSetupPollController (recursive-setTimeout, single in-flight request, immediate reconciliation on resume, stops when setup leaves download/extraction) plus a useDictationSetupPoller React/AppState adapter. The screen requires route focus + foreground; the sheet requires visible + foreground; both immediately reconcile on resume.

ELI5

While your phone was downloading a dictation model, it pinged the computer for progress every 1.5 seconds even if the app was in your pocket — and could stack up pings on a slow connection. Now it only polls while you're looking, never overlaps requests, and instantly refreshes when you return.

Proof of zero regression

  • Progress is decorative/structural; the final state is reconciled immediately on resume (foreground/visible/focus). Polling still starts/stops on the same download/extraction lifecycle.
  • Single-flight: no overlapping requests; a resume/manual refresh requested during a slow in-flight read is coalesced and runs once the read completes (covered by a fake-timer test).
  • Timers + AppState listeners are cleaned up on hide/unmount.
  • Verified in a deps-installed checkout: dictation-setup-poll-controller.test.ts (hidden/unfocused/background gating, slow-response single-flight, resume coalescing, immediate foreground reconciliation, completion stop), full mobile suite 307 files / 2232 pass, typecheck / oxlint / oxfmt --check clean.

Proof of perf improvement

Eliminates the 1.5s progress poll while backgrounded/covered and prevents overlapping in-flight requests during model installation (up to ~40 RPC/min per poller saved when not foregrounded); zero cost when no setup is running.

Made with Orca 🐋

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cf9d54de-4785-4f94-ac1d-4464b2f480e4

📥 Commits

Reviewing files that changed from the base of the PR and between b7fd229 and 790a730.

📒 Files selected for processing (5)
  • mobile/app/voice-settings.tsx
  • mobile/src/components/MobileDictationSetupSheet.tsx
  • mobile/src/dictation/dictation-setup-poll-controller.test.ts
  • mobile/src/dictation/dictation-setup-poll-controller.ts
  • mobile/src/dictation/use-dictation-setup-poller.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • mobile/src/dictation/dictation-setup-poll-controller.test.ts
  • mobile/src/dictation/use-dictation-setup-poller.ts
  • mobile/src/components/MobileDictationSetupSheet.tsx

📝 Walkthrough

Walkthrough

Adds a shared DictationSetupPollController and useDictationSetupPoller hook for visibility- and foreground-aware polling, single-flight refreshes, timer scheduling, and disposal. Replaces manual polling in voice settings and the dictation setup sheet. Post-action refreshes now use the hook’s refresh callback. Adds fake-timer tests covering eligibility, concurrency, resume behavior, polling delays, and lifecycle completion.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description doesn't follow the required template and omits Screenshots, Testing, AI Review Report, Security Audit, and Notes. Rewrite the description using the repo template and add the missing sections: Summary, Screenshots, Testing, AI Review Report, Security Audit, and Notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: foreground-gated, single-flight polling for mobile dictation setup.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
mobile/src/dictation/dictation-setup-poll-controller.ts (1)

64-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add "why" comments to the non-obvious eligibility/coalescing branches.

The eligibility-resume branch (Line 64-67) and the immediate-refresh coalescing branch (Line 109-113) encode important lifecycle decisions (eager reconcile on resume; defer resolving waiters until the coalesced refresh actually runs) but have no rationale comment, unlike the well-explained catch block at Line 96.

As per coding guidelines, "For non-obvious TypeScript code, add a concise comment explaining why the constraint exists, not how the code works."

Also applies to: 109-113

Source: Coding guidelines

mobile/src/dictation/use-dictation-setup-poller.ts (1)

18-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the rationale for the ref indirection and the refresh-change effect.

refreshRef (Line 18-19) exists so the memoized controller (kept stable across renders) always calls the latest refresh without being recreated — worth a one-line "why" comment. Similarly, the effect at Line 27-29 only has an observable effect on re-renders where refresh changes while already eligible (it's a no-op on initial mount, since it runs before the visibility/foreground effect establishes eligibility) — that ordering dependency is easy to break unknowingly during a future refactor.

As per coding guidelines, "For non-obvious TypeScript code, add a concise comment explaining why the constraint exists, not how the code works."

Source: Coding guidelines

mobile/app/voice-settings.tsx (2)

71-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated refresh/polling wiring; early return bypasses finally.

This refresh implementation, the polling derivation, and the useDictationSetupPoller call are duplicated verbatim in MobileDictationSetupSheet.tsx (Line 41-62 there). Separately, the if (!client) return false at Line 72-74 sits outside the try/finally, so it skips setLoading(false) — currently hard to hit given the visible eligibility gate, but the controller's state.visible can lag one render behind client becoming null, so a manually-triggered refreshSetup() (Line 114, Line 131) could still land here and leave the spinner stuck.

See the consolidated comment for a suggested extraction that also closes this gap in one place.


1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated fetch/polling/poller-wiring block into a shared hook. Both integration sites copy-paste the same refresh/polling/useDictationSetupPoller wiring, including the same !client early return that sits outside any completion signaling.

  • mobile/app/voice-settings.tsx#L71-94: replace this block with a shared useDictationSetupState(client)-style hook that returns { setup, error, polling, refreshSetup }; ensure the shared implementation sets setLoading(false)/resolves completion even on the !client path.
  • mobile/src/components/MobileDictationSetupSheet.tsx#L41-62: adopt the same shared hook so both sites stay in sync and the !client path can't leave setup (and its spinner) stuck.
mobile/src/components/MobileDictationSetupSheet.tsx (1)

41-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated refresh/polling wiring shared with voice-settings.tsx.

This block is structurally identical to voice-settings.tsx Line 71-94 (fetch, derive polling, wire useDictationSetupPoller). Here the risk of the !client early return (Line 42-44) is that setup never gets its first value if that branch is ever reached before a successful fetch, leaving the Line 137-140 spinner stuck indefinitely.

See the consolidated comment for a suggested shared extraction.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cdee10ea-f421-4d1f-85e9-1a313e17fe6e

📥 Commits

Reviewing files that changed from the base of the PR and between e9e7d17 and b7fd229.

📒 Files selected for processing (5)
  • mobile/app/voice-settings.tsx
  • mobile/src/components/MobileDictationSetupSheet.tsx
  • mobile/src/dictation/dictation-setup-poll-controller.test.ts
  • mobile/src/dictation/dictation-setup-poll-controller.ts
  • mobile/src/dictation/use-dictation-setup-poller.ts

nwparker and others added 2 commits July 22, 2026 15:55
Co-authored-by: Orca <help@stably.ai>
…ng intent

An in-flight setup read resolving 'keep polling' after an explicit setPolling(false)
wrote polling=true and rescheduled, resurrecting a poll the caller had just stopped.
Snapshot a pollingRevision when each read starts and only apply its result if no
explicit setPolling superseded it mid-flight — so a late true can't restart a stopped
poll (nor a late false cancel a restart).

Co-authored-by: Orca <help@stably.ai>
@nwparker
nwparker force-pushed the nwparker/perf-mobile-dictation-poll-gate branch from b7fd229 to 790a730 Compare July 22, 2026 22:57
@nwparker
nwparker merged commit 01bcc57 into main Jul 22, 2026
2 checks passed
@nwparker
nwparker deleted the nwparker/perf-mobile-dictation-poll-gate branch July 22, 2026 23:22
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…ingle-flight (stablyai#9892)

* fix(mobile): gate dictation setup polling

Co-authored-by: Orca <help@stably.ai>

* fix(mobile): fence a stale dictation refresh against a newer setPolling intent

An in-flight setup read resolving 'keep polling' after an explicit setPolling(false)
wrote polling=true and rescheduled, resurrecting a poll the caller had just stopped.
Snapshot a pollingRevision when each read starts and only apply its result if no
explicit setPolling superseded it mid-flight — so a late true can't restart a stopped
poll (nor a late false cancel a restart).

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
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