uttrflow / uttrflow/uttrflow-swift

The drain of the in-flight early piece is a wait the user now sits through and no stage measures, so Diagnostics under-reports by up to a whole piece

Open
#216 1 comment 0 reactions 0 assignees View on GitHub
area:dictation bug claimed P2 performance
Dominant language
Swift
Stars
4
Forks
17
Avg merge
3h 32m
Merged PRs (30d)
277

Description

**P2 — latency.**

### What happens

An operator investigating a slow dictation opens Diagnostics and finds six stages that do not cover the wait: the block on the in-flight piece, the microphone opening and the situation read all sit outside every one of them, so a dictation that took four seconds can report two seconds of stages with no row saying where the rest went.

### Why

Timing is opt-in per call site rather than derived from the dictation's own timeline. `PipelineStage` is a fixed list of six things somebody chose to wrap, with no invariant tying the sum of the stages to the interval it is supposed to explain, so any await added later is free by default — and one was.

### What the code shows

Verified. `PipelineStage` has exactly six cases and none of them could hold the drain, the microphone opening or the context read (Metrics.swift:4-17). `process` transitions to `.transcribing` and then blocks on `await earlyWork?.value` with no `measuring` around it (DictationPipeline.swift:351-357). `.capture` wraps only `capture.stop()` (line 202-206). The early pieces are deliberately recorded into `NoOpMetricsRecorder()` (lines 304, 311), so the drain is invisible from both ends. Timing really is opt-in per call site — `measuring` is a decoration, and there is no invariant tying the sum of stages to any wall-clock interval, so an await added later is free by default. Two corrections. The citation is off: the sentence quoted from Docs/early-transcription.md — "the diagnostics page reports what the user waited for, and nobody waited for these" — is at line 106, not 118. And half of what the finding calls a defect is a decision the same document records: the early pieces' timings are not recorded on purpose. What is genuinely new is narrower and correct: since `process` began waiting for the in-flight piece, that sentence's justification no longer covers the drain, and the drain is now a wait nobody measures. The audience is whoever reads the Diagnostics page rather than the person dictating, which is what puts it at P2.

### Where

- `Sources/UttrflowPipeline/DictationPipeline.swift:351-357`
- `Sources/UttrflowPipeline/DictationPipeline.swift:304`
- `Sources/UttrflowPipeline/DictationPipeline.swift:311`
- `Sources/UttrflowPipeline/DictationPipeline.swift:202-206`
- `Sources/UttrflowCore/Support/Metrics.swift:4-17`
- `Sources/UttrflowCore/Support/Metrics.swift:148-156`
- `Docs/early-transcription.md:106`

### Fix direction

The per-dictation timeline is the right target, with one constraint the proposal leaves out: `PipelineStage`'s six cases are what the Diagnostics page and `StageLatency.summarise` iterate over, so the timeline has to land beside them and the page has to be moved onto it, not have cases bolted on underneath it. Open a timeline at key-down and close it at `.inserted`; spans are opened and closed as the pipeline moves, one span is always open, and the timeline exposes both the spans and the residue. Assert the invariant in a test — spans plus residue equal the interval — which is what makes a later unmeasured await show up as a growing "unaccounted" row instead of as nothing. Name the spans for what the user waits through: microphone open, situation read, waiting for the piece in flight, recognise, dictionary, tidy, snippets, insert, confirm. `StageTally` already exists to merge per-piece figures and should keep doing so, so the timeline must compose with it rather than replace it. Keep honouring the decision in Docs/early-transcription.md:106 that early pieces are not charged to the user — the point is that the *drain* now is, and the document should be corrected to say so. This is the prerequisite for landing any of the other latency changes with a number attached, which is the argument for doing it first.

### Principles

SRP: measuring becomes one type's job rather than a decoration scattered through the pipeline. KISS: "the parts add up to the whole" is a single rule a test can enforce, where "six enum cases someone wrapped" is not a rule at all. YAGNI: no spans beyond the awaits that already exist.

---

Found by a code-path audit of the dictation pipeline. Traced in the source and checked against `Docs/` by a second reader; **not reproduced at runtime**, which is why this carries no `confirmed` label. Any rule or prompt change proposed above is measured against the corpus with `make bakeoff` before it lands.

Contributor guide

Open the contributing guide

Research direction

Read Metrics.swift, DictationPipeline.swift, StageTally, and Docs/early-transcription.md:106, then trace the process interval from key-down through .inserted. Implement the per-dictation timeline alongside the existing stages, preserve StageTally and the early-piece decision, and add an invariant test proving spans plus residue equal the interval. Update the documentation to describe the newly measured drain.

Written by the indexing model from the issue text.

Assessment

Tech stack
macos, swift
Domain
desktop-dev, observability, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.