uttrflow / uttrflow/uttrflow-swift
Every dictation state change rebuilds all main-window pages from disk on the main actor, even with the window closed: about 54–86 ms each with a full history
- Dominant language
- Swift
- Stars
- 4
- Forks
- 17
- Avg merge
- 3h 32m
- Merged PRs (30d)
- 277
Description
## What happens
`AppDelegate.render(_:)` runs for every pipeline state (recording, transcribing, tidying, inserting, inserted or failed, idle) and calls `refreshMainWindow()` (`Sources/Uttrflow/AppDelegate.swift:1131-1192`). `refreshMainWindow()` (`:1360-1386`) re-reads the diagnostics, the whole history file, the kept recordings, every dictionary entry and snippet, the cached profile twice (each `ProfileCache.load()` verifies the entitlement signature, `Sources/UttrflowAccount/ProfileCache.swift:54-59, 69-71`) and both permissions, then builds `mainContent` on the main actor: all eleven pages (home, sidebar, dictation, history, dictionary, corrections, insights, snippets, style, diagnostics, account, `:1388-1463`), whichever page is showing.
The only guard is `mainWindow != nil`, and `mainWindow` is created at launch (`:222`) and never set back to `nil` (no `mainWindow = nil` in the file), so this happens for every dictation for as long as the app runs, window open or not.
## Measured
A throwaway test in `Tests/UttrflowUXTests` (release build, `swift test -c release -Xswiftc -enable-testing`, M5 Pro, best of 5) building five of the pages from 1,000 history entries (the store's cap, `DictationHistoryStore.defaultCapacity`), each with 20 words and 3 recorded corrections:
| | 1,000 entries, 100 of them today | 1,000 entries, all today |
|---|---|---|
| decode `history.v1.json` (920 KB), on the history actor | 11 ms | 11 ms |
| `HomePresenter.page` | 9.8 ms | 16.7 ms |
| `DictationPresenter.page` | 13.6 ms | 55.4 ms |
| `HistoryPresenter.page` | 4.2 ms | 3.5 ms |
| `CorrectionsPresenter.page` | 3.4 ms | 5.5 ms |
| `InsightsPresenter.page` | 21.1 ms | 4.3 ms |
| those five together, main actor | **54 ms** | **86 ms** |
The sidebar, dictionary, snippets, style, diagnostics and account pages were not included, so the real main-actor cost is higher. `Docs/performance.md` scales this machine to roughly 55–60% on an M1, so 100–150 ms of main-actor work per state change there, about five times per dictation, including the moment the key goes down.
`DictationPresenter.row(for:in:locale:)` filters the whole corrections list once per row (`Sources/UttrflowUX/MainDictationPresentation.swift:221-224`), which is why the all-today column grows fastest: rows × corrections.
## Why it matters
The main actor is where the dock, the menu bar and the shortcut handling live. A 100 ms stall at key-down or at insertion is visible as a late meter or a late tick on an 8 GB Air, and the work is done for a window nobody is looking at. It also scales with exactly the users who dictate most.
## How to reproduce
Build the pages from a 1,000-entry `[DictationRecord]` in a test and time them as above (a timing harness is easy to write against `HomeSnapshot`, `DictationSnapshot`, `HistorySnapshot`, `CorrectionsSnapshot`, `InsightsSnapshot`). Or, in the app, use Instruments' Time Profiler on the main thread while dictating with a full history and the main window closed.
## Acceptance criteria
- While recording, transcribing and tidying, nothing rebuilds the main window's content; the refresh happens once when a dictation ends.
- When the window is not visible, no page is built; the next `show` builds the page it shows.
- Only the visible page is built on a redraw (the others lazily on navigation), or the build moves off the main actor.
- `DictationPresenter` counts corrections per dictation with one pass (a dictionary keyed by dictation id), with a test that the counts are unchanged.
- A measurement before and after, with a 1,000-entry history, is recorded on this issue.
Contributor guide
Research direction
Start with AppDelegate.render(_:) and refreshMainWindow() in Sources/Uttrflow/AppDelegate.swift, then inspect DictationPresenter.row(for:in:locale:) in Sources/UttrflowUX/MainDictationPresentation.swift. Run the release timing harness against the presenter snapshots with 1,000 history entries. Done means refreshes are deferred or limited to the visible page, correction counts use one pass, and before/after measurements are recorded.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, swift
- Domain
- desktop, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100