From shortcut
to delivered text.
A complete map of the normal desktop dictation path: what runs, what overlaps, what blocks the first chunk, and what survives into PostHog.
0Read the trace
The time origin is session open
T0 = openSessionTrace() during the IDLE → STARTING snapshot. Input handling, start admission, and the synchronous prefix of transcription.open() happen before this anchor.
T0 is not physical key-down, mic acquisition start, or recording-live. UI buttons and hands-free toggles can also start a session.
PostHog receives a summary
The client collects named spans, synthetic durations, points, and chunk statistics. At flush it builds one flattened event. Raw startedAt, endedAt, span IDs, and the span tree are not included in that event.
A 261 ms mute_duration_ms says how long the phase took. It does not locate its start or end relative to the shortcut.
Scope: a successful live dictation with sounds enabled and automatic paste. The native audio detail is the current macOS AVAudioPlayer implementation. The AVAudioEngine experiment is not part of this flow. Other outcomes and draft mode are covered in section 6.
1Inspect the trace waterfall
Select a bar or diamond to see its exact boundaries, PostHog mapping, and limitations. Gray and dashed bars show supporting work; they are not all real named spans.
Physical shortcut → native key event → Electron shortcut manager → input grammar / verb queue → admission checks (including selected model) → STARTING snapshot → stream open / warmup launch → trace opens. The admission lookup has a 3 s bound; it is not a fixed startup delay.
recording_live_offset_ms; the first PCM arriving in main ends recorder_spinup_duration_ms; the first frame admitted past readiness and the startup audio gate produces first_chunk_offset_ms. That last timestamp is set before VAD, provider work, and transcription-lock processing.2The actual desktop sequence
Startup branches overlap. On a normal stop, recorder closure permits transcript resolution while WAV closure and audio restoration can continue. Storage finishes before automatic delivery is staged.
Shortcut / UI → admission → STARTING → trace T0
Mic + worklet ∥ start sound + mute ∥ provider warmup
PCM → readiness + startup audio gate → VAD → provider, with WAV writes alongside
Final frame → recorder closed → drain → flush → format; close WAV and restore audio alongside
Commit → dispatch paste → IDLE → native reply → telemetry flush
Loading the detailed sequence diagram. The flow summary above remains available if the diagram CDN cannot load.
2.1 · What the startup audio gate actually waits for
Recorder.start() immediately sets beepPending = true. Main reads preferences and sends startRecording. On macOS the helper serializes ambiance operations, waits for rec-start playback completion, calls the system-mute setter if enabled, and replies. The promise then releases the gate.
Non-final frames are dropped while the recorder is still starting or the startup audio gate is closed. First-PCM liveness is measured before these checks. A final frame bypasses the startup audio gate, but not the recorder-starting check. If dictation sounds are muted, the gate releases after reading preferences without waiting for the native RPC.
The mute setter returning successfully does not prove physical silence. The implementation does not wait for a CoreAudio property-change notification.
2.2 · Resolution and backend time
resolve.drain waits for already admitted chunk work. resolve.flush then obtains the transcription lock and flushes the provider. For cloud dictation, this can include waiting for a backend final result and cloud formatting. resolve.format prepares the returned text and may perform additional configured formatting.
resolve_duration_ms covers this whole chain plus other work in the resolve effect. It is not a pure server-latency metric. Provider warmup and server-internal spans are not separately exported by this desktop event.
3What reaches PostHog
Event: transcription_completed_v2. Ten named phase durations, a separate session duration, and four offset fields can be emitted. Fields are conditional on which work ran and finished in time; a missing field is not zero.
3.1 · Durations and offsets
| PostHog field | Source span / point | What is actually measured |
|---|
recorder.start(). Native-start timing begins inside ambiance.begin(). Only exported offsets use the session-open anchor. Do not add parallel phases, or subtract population percentiles to estimate a per-session gap.3.2 · Chunk aggregates and descriptive fields
| Fields | Meaning / caveat |
|---|---|
chunk_count | Count of calls admitted into streaming-chunk processing. Frames dropped by readiness or beep gating are not included. This is not a count of server requests. |
vad_duration_sum_msvad_duration_max_ms | Sum and maximum of per-chunk VAD timings. The timer starts before the VAD lock, so waiting for that lock is included. These are aggregates, not a single contiguous span. |
transcribe_duration_sum_mstranscribe_duration_max_ms | Sum and maximum of per-chunk provider transcribe() calls. Starts inside the transcription lock after materialization; excludes the prior lock wait. Meaning depends on the provider, and may include local buffering or enqueueing rather than a complete remote transcription. |
materialize_duration_ms | One-time setup on the first processed chunk: load dictation context and configuration, select model, open and attach the provider session. Timing starts inside the transcription lock. Zero can mean no materialization was recorded. |
model_id, provider | Recorded model/provider when the session materializes. No warmup duration or connection-ready offset accompanies these fields. |
audio_input_channel_countaudio_track_channel_countaudio_stereo_downmix_enabled | Copied from the lifecycle.audio-capture point. Format metadata only; that point's timestamp is not exported. |
session_id, disposition, flush_reasonfailed_stage, error_code, error_tag, defect | Identity, outcome, and failure attribution. The terminal-latch point can supply failure details, but no latch offset is exported. App/version/machine/system metadata is added through the telemetry envelope. |
3.3 · The flattened event for this mock session
All numbers below are illustrative and match the waterfall. Routine identity/system properties are omitted. Notice that pasted_offset_ms can exceed session_duration_ms because native confirmation may arrive after the lifecycle reaches idle.
Expand example PostHog payload
Derived values that are safe to interpret
first_chunk_offset_ms − recording_live_offset_ms measures the elapsed time from recording-live to first chunk admission within the same session. It can include frame buffering, IPC/scheduling, and beep gating; it does not isolate any one of them.
pasted_offset_ms − last_chunk_offset_ms measures last admitted chunk → native paste confirmation. It is not exact shortcut-release → paste latency: the stop input can precede the final admitted frame.
A native paste acknowledgement means the helper reported success posting the paste command. It does not verify insertion or timestamp when another app renders the text.
4The local delay we measured
This is separate evidence from the illustrative waterfall: a recording in the installed desktop app's local log on this M4 Max, 23 September 2026 at 13:21:41 IST. Its time origin here is the native RPC dispatch, not T0 or shortcut press. The installed app was reporting 1.12.0.
| Position from native RPC dispatch | Observed event |
|---|---|
| 0 ms | Main sends startRecording. |
| 3 ms | Renderer begins capture startup. |
| 5 ms | Native sound request begins. |
| 45 ms | play() has returned successfully; the code logs “Playing sound”. This is not a measured acoustic onset. |
| 128 ms | Renderer graph setup has completed. Its local total-startup timer reports 125.8 ms. |
| 156 ms | Renderer sees its first PCM frame; the native playback wait is still in progress. |
| 248 ms | Playback completion has released the native wait; mute setter succeeds and helper replies. The mute call is below the log's 1 ms resolution, not a measurement of physical silence. |
In that capture startup: getUserMedia 83.9 ms, AudioContext resume 36.2 ms, first-frame wait 28.7 ms. Those renderer operations overlap the native sound path.
4.1 · Standalone playback experiments
The sound contains 99.68 ms of audio. A standalone AVAudioPlayer probe reached the end of its playback timeline about 121–123 ms after play() returned, but the completion callback arrived at about 201–214 ms. A WAV version and prepareToPlay() did not remove the callback wait. play() itself took roughly 16–19 ms warm and 92 ms in one cold run.
A separate AVAudioEngine prototype using the same sound and .dataPlayedBack completed in 142–150 ms while warm and 157–168 ms after pausing between plays. These are isolated local probes, not an integrated desktop change or a population result. The current source still uses AVAudioPlayer.
5What the current event cannot tell us
Before and during capture
- Physical key-down → accepted start / trace open.
- Renderer scheduling, capture-mutex wait, and config IPC individually.
- Device enumeration,
getUserMedia, AudioContext resume, worklet load, and graph setup as separate PostHog timings. Several are already measured in local console logs. - First PCM arrival as an offset from T0; only recorder-spinup duration is exported.
Sound, mute, and ending
- Native request start/end offsets or sound-enabled setting per trace.
- Sound setup / audible start / playback completion / mute setter as separate timings.
- Beep-gate release, frames dropped by the gate, and confirmed device mute.
- Stop input / recording-stop offset and exact backend-internal stages.
5.1 · Proposed additions — not currently implemented
The detailed, source-audited proposal is now in section 8: each interval has its boundary, a concrete reason it could exceed 20 ms, a local measurement or explicit gap, and its current v2 coverage.
Section 9 defines the shared names and clock contract. It preserves session-open T0 and uses shared epoch timestamps for offsets with local duration measurements. Native timing is limited to caller-observed RPCs. The event names and proposed field pairs there supersede the earlier sketch.
6Branches and trace-flush rules
The lifecycle ends before every tail must end
The root duration stops when the public state returns to idle. Telemetry waits for registered obligations, including recorder closure, storage, native audio restoration, and a dispatched paste response.
Flush occurs once: when the root is closed and all expected obligations settle, or after a 15 s grace period from root close. Late records after flush are dropped. Shutdown force-flushes remaining traces.
Presence does not always prove success
An Effect span may end with failure or interruption and still supply a duration. Some best-effort spans catch their own errors. By contrast, delivery.pasted is recorded only for a confirmed successful native paste.
The flat payload takes the first mapped duration/offset for each key. Individual span status and timestamps remain in internal/debug records; failure attribution is summarized separately.
6.1 · Quick release, cancel, empty, and failure
A quick release or interrupted start can seal a discard before usable audio exists. Empty, dismissed, failed, and discarded sessions still go through settlement and emit the session summary if the app gets to flush it. An admission refusal before session creation does not create this trace.
Capture failures and provider terminal failures can seal the result during STARTING or RECORDING. The happy-path finalization and paste spans may therefore be absent. Cancellation can retire chunk work before late timings finish.
6.2 · Draft mode and post-session actions
On stop, draft mode may wait for a bounded clipboard-copy capture barrier of up to 2.5 s before entering the measured transcription.resolve effect. That wait is not a dedicated exported timing.
Successful draft sessions stage text for review instead of immediately pasting. The later “confirm draft” paste and “paste latest transcription” action are outside this live session's delivery.pasted timing.
6.3 · Two different drains
The renderer/recorder drain flushes the worklet, accepts final in-flight PCM, then emits recorderClosed. There is no dedicated PostHog duration for that stop-to-close-decision interval.
resolve.drain begins after that decision and waits for chunk-processing tasks already admitted to transcription. recorder_close_duration_ms is another distinct measurement: the asynchronous WAV write/finalize/enrichment tail, which may overlap resolve.
7Source map
Paths are relative to /Users/z/exa9/@amical/amical. This document describes revision 097ffe4; local experimental results were collected on 23 September 2026.
apps/desktop/src/main/telemetry/dictation-trace.ts:58apps/desktop/src/main/telemetry/dictation-trace.ts:129apps/desktop/src/main/telemetry/dictation-trace.ts:407apps/desktop/src/main/lifecycle/runtime.ts:354apps/desktop/src/main/lifecycle/runtime.ts:445apps/desktop/src/main/lifecycle/runtime.ts:481apps/desktop/src/main/lifecycle/runtime.ts:524apps/desktop/src/main/lifecycle/live.ts:99apps/desktop/src/main/lifecycle/live.ts:158apps/desktop/src/main/lifecycle/live.ts:205apps/desktop/src/hooks/useAudioCapture.ts:212apps/desktop/src/hooks/audioCaptureDevice.ts:24apps/desktop/src/hooks/audioCaptureContext.ts:6apps/desktop/src/hooks/audioCaptureWorklet.ts:17apps/desktop/src/assets/audio-recorder-processor.js:4apps/desktop/src/main/lifecycle/adapters/recorder.ts:182apps/desktop/src/main/lifecycle/adapters/recorder.ts:253apps/desktop/src/main/lifecycle/adapters/recorder.ts:336apps/desktop/src/main/lifecycle/adapters/transcription.ts:119apps/desktop/src/services/transcription-service.ts:567apps/desktop/src/services/transcription-service.ts:941apps/desktop/src/main/lifecycle/adapters/storage.ts:172apps/desktop/src/main/lifecycle/adapters/host.ts:110apps/desktop/src/main/lifecycle/adapters/host.ts:157packages/native-helpers/swift-helper/Sources/SwiftHelper/RecordingAmbianceController.swift:22packages/native-helpers/swift-helper/Sources/SwiftHelper/AudioService.swift:142packages/native-helpers/swift-helper/Sources/SwiftHelper/AccessibilityService.swift:146Apple: asynchronous property-set semantics/Users/z/Library/Logs/Amical/amical.logRecording beginning 2026-09-23 13:21:41 IST. Native-helper and standalone Swift playback probes were separate diagnostic runs.8Proposed spans: evidence before implementation
PROPOSAL ONLY · NO TELEMETRY ADDED
The earlier list was too broad to treat every item as an established >20 ms bottleneck. Below, measured slow work, concrete waits, and lower-priority diagnostic candidates are distinguished explicitly. Existing span names stay unchanged; new names follow the same dotted namespace and hyphenated operation convention.
“Not measured separately” means unknown, not zero. The log contains no “Dictation trace” debug records, so missing v2 phase values cannot be recovered from it. No number from the illustrative waterfall is used in these tables. A faster CPU does not shorten the audio file, remove the 32 ms frame size, or determine backend latency.
8.1 · Input and renderer capture
| Span and exact boundary | Why it could exceed 20 ms | This machine | Current v2 coverage |
|---|---|---|---|
lifecycle.recording-start-queue-waitStart request enqueued → admission begins. | Can wait behind earlier asynchronous lifecycle verbs. Contention candidate; not inherently slow. | Not measured separately. | Not exported |
lifecycle.recording-start-checksAdmission begins → start accepted/refused. | Selected-model settings read and possible migration/write; 3 s guard. Ordinary local reads are expected to be short. Lower priority. | Not measured separately. | Not exported |
capture.dispatchMain publishes starting → renderer enters startCapture(). | Process scheduling and React work can delay handling. This is a cross-process interval, not pure wire/IPC latency. | Not measured separately. | Not exported |
capture.lock-waitstartCapture() requests mutex → its body enters. | An earlier stop, context close, or cleanup can still hold the capture mutex. | Not measured separately. | Not exported |
capture.config-read.rpcRenderer starts cached-config query → query settles. | Cached memory read itself is small; only queueing, IPC scheduling, or an event-loop stall justifies >20 ms. Lower priority. | Not measured separately. | Not exported |
capture.enumerate-devicesenumerateDevices() call → result. | Actual slow samples exist on this machine: 5/31 exceeded 20 ms. Device discovery crosses browser/OS boundaries. | 2.1 ms median; 0.3–63.9 ms; n=31. | Not exported |
capture.get-user-mediagetUserMedia() call → stream returned or error. | Microphone permission/device opening and browser/OS audio setup. 31/31 exceeded 20 ms. | 83.9 ms median; 68.9–137.7 ms; n=31. | Not exported |
capture.audio-context-createAudioContext constructor entry → return. | Creating the audio engine opens/initializes browser and OS audio resources. 12/12 exceeded 20 ms. | 35.0 ms median; 32.8–37.2 ms; n=12 cold contexts. Excludes worklet loading. | Not exported |
capture.audio-context-resumeAudioContext.resume() call → promise settles. | Audio engine/device restart and scheduling. 19/19 exceeded 20 ms. | 36.2 ms median; 34.6–40.9 ms; n=19 warm contexts. | Not exported |
capture.worklet-loadaudioWorklet.addModule() call → promise settles. | Cold module loading, evaluation, and audio-thread scheduling can be slower on other machines. No >20 ms sample here. Conditional candidate. | 2.45 ms median; 1.8–16.7 ms; n=12; 0/12 >20 ms. | Not exported |
capture.graph-setupSource/worklet construction → graph and analyser connected. | No evidence of intrinsic >20 ms cost. Keep inside capture-startup coverage unless an actual slow sample appears; not a priority standalone span. | Full interval not measured. Node construction alone: 0.1 ms median; 0.0–0.7 ms; n=31. | Not exported |
capture.first-frame-waitFrame handler attached → first nonempty renderer PCM frame observed. | Regular frames contain 512 samples at 16 kHz: 32 ms of audio. Includes audio scheduling and the final graph-connection work after handler attachment. | 34.0 ms median; 27.3–46.7 ms; n=31; all >20 ms. | Not exported |
capture.first-frame-ipcRenderer sends first PCM → main receives it. | Ordinary local IPC should be short. >20 ms would mainly indicate process scheduling/backlog. Cross-process interval; lower priority. | Not measured separately. First renderer PCM was 156 ms after native-start RPC dispatch in the 13:21 sample; that is not IPC duration. | Not exported |
lifecycle.recorder-spinuprecorder.start() → first nonempty PCM observed in main. | Existing parent includes renderer scheduling, acquisition, context work, buffering, and IPC. | Not measured separately. Renderer startup diagnostic is a different interval: 126.4 ms median, 110.2–221.2 ms, n=31. | Duration: recorder_spinup_duration_ms |
8.2 · Native sound, mute, and gate — internal candidates deferred
Current scope: main measures RPC duration and start offset. Internal native spans below are candidates only; they are not implemented.
| Span and exact boundary | Why it could exceed 20 ms | This machine | Current v2 coverage |
|---|---|---|---|
lifecycle.ambiance-preferencesPreference read begins → sound/mute settings available. | Local settings read; no evidence of normal >20 ms work. Lower priority, useful only to explain a parent-span gap. | Not measured separately. | Not exported |
native.start-recording.rpcMain calls startRecording → helper response/error. | Includes dispatch, helper scheduling, start sound, and mute. Every measured round trip exceeded 20 ms. | 254 ms median; 218–285 ms; n=31. | Contained in mute_duration_ms; exact RPC not separately exported |
native.start-recording.queue-waitHelper start operation requests ambiance mutex → lock acquired. | Can wait behind a prior stop sound/restoration operation. Actual queue wait is not separately logged. | Not measured separately. | Not exported |
native.start-recording.sound-setupNative sound work starts → play() returns. | Per-play AVAudioPlayer creation and playback setup; 29/31 observed setup intervals exceeded 20 ms. | ≈47 ms median; 9–64 ms; n=31. Wall-log proxy, includes work before player creation. | Not exported |
native.start-recording.sound-waitplay() returns → playback completion is processed. | The sound itself contains ≈99.68 ms of audio; completion also includes output/callback scheduling. Faster CPU cannot remove the sound duration. | ≈203 ms median; 199–203 ms; n=31. Proxy endpoint is the subsequent mute request, not a direct callback timestamp. | Not exported |
native.start-recording.system-muteMute setter call → returned result. | No >20 ms evidence here. Device/driver stalls are possible; retain as a small diagnostic child to distinguish actual mute cost from playback. | ≈1 ms median; 0–2 ms; n=31; 0/31 >20 ms. Physical silence is not measured. | Not exported |
lifecycle.recorder-start-gate-waitRecorder start sets beepPending → gate clears. | Normally waits for the start operation when sounds are enabled. Regular frames are dropped while closed; sounds-disabled path opens earlier. | Not measured separately. Do not substitute native RPC duration. | Not exported |
lifecycle.mute-ambianceExisting ambiance.begin timer → start operation resolves. | Parent includes remaining preference read, native request, sound completion, and optional mute. | Not measured separately. Sound-request → mute-request proxy alone: ≈250 ms median, 211–264 ms, n=31. | Duration: mute_duration_ms |
lifecycle.unmute-ambianceEnd ambiance effect begins → native stop settles/error handled. | Can first join a pending start, then restore audio and await the stop sound. | Not measured separately. The narrower native stop RPC is measured below. | Duration: unmute_duration_ms |
native.stop-recording.rpcMain calls stopRecording → helper response/error. | Restoration plus stop sound and helper scheduling. 31/31 exceeded 20 ms. Excludes any main-side wait for pending startup. | 261 ms median; 218–270 ms; n=31. | Contained in unmute_duration_ms; exact RPC not separately exported |
native.stop-recording.queue-waitHelper stop operation requests ambiance mutex → lock acquired. | May wait for an earlier start/stop operation that is still playing a sound. | Not measured separately. | Not exported |
native.stop-recording.system-restoreRestore original output properties → API result. | No measured >20 ms case isolated here. OS/device calls can stall; diagnostic child, not a known bottleneck. | Not measured separately. | Not exported |
native.stop-recording.sound-setupStop-sound work starts → play() returns. | Same player setup as start; 26/31 observed setup intervals exceeded 20 ms. | ≈56 ms median; 10–65 ms; n=31. | Not exported |
native.stop-recording.sound-waitplay() returns → stop-sound completion processed. | Waits for audio playback and the callback. The >200 ms stop RPC establishes a slow parent, but does not isolate this child. | Not measured separately. Do not subtract medians to estimate it. | Not exported |
8.3 · Context and streaming transcription
| Span and exact boundary | Why it could exceed 20 ms | This machine | Current v2 coverage |
|---|---|---|---|
context.accessibility-refreshRefresh requested → accessibility snapshot propagated into session memory. | Helper queue and accessibility calls into another app, tree walks and selection fallbacks. Foreground-app responsiveness matters. | Whole interval not measured. Native extraction only: 16.80 ms median; 8.03–72.01 ms; n=31; 10/31 >20 ms. | Not exported |
transcription.provider-warmupProvider warmup requested → settles. | Cloud can refresh an expired token over the network; local Whisper can load a model/worker. Warm calls may be short. | Not measured separately. | Not exported |
transcription.vad-resetReset requested → VAD lock acquired and state reset. | Reset itself is small synchronous work. >20 ms would usually be waiting behind earlier inference/retry work. | Not measured separately. | Not exported |
transcription.session-setupInside transcription lock: context/config loading begins → provider session attached. | SQLite context, vocabulary, snippet and settings reads plus provider-session setup. Useful setup parent; model loading is not necessarily inside this interval. | Not measured separately. | Duration: materialize_duration_ms, in chunk aggregate; no offset |
transcription.vad-lock-waitChunk requests VAD lock → lock acquired. | Prior inference, reset, or history retry can hold the lock and create a backlog. | Not measured separately. | Included in VAD sum/max only |
transcription.vad-processVAD frame-processing call → result. | ONNX inference and its scheduling can cross 20 ms under CPU load or on slower devices. No isolated local timing in this log. | Not measured separately. | Included in VAD sum/max only |
transcription.chunk-lock-waitChunk requests transcription lock → lock acquired. | Earlier materialization, inference, provider work or flush can hold the shared lock. | Not measured separately. | Not exported separately |
transcription.provider-transcribeProvider transcribe() called → result/error. | Local model loading/inference, or cloud auth/transport/fallback work. Some provider calls only buffer data and return quickly. | Not measured separately. | Transcribe sum/max only; these exclude lock wait and materialization |
8.4 · Stop, resolution, storage, and paste
| Span and exact boundary | Why it could exceed 20 ms | This machine | Current v2 coverage |
|---|---|---|---|
lifecycle.recorder-final-frame-waitRecorder.stop() → recorder-close decision. | Waits for renderer drain/final-frame IPC. A missing final frame can reach the 3 s bound. | Not measured separately. | Not exported; not recorder_close_duration_ms |
capture.worklet-flushFlush request posted → final frame handed off or timeout. | Worklet/audio-thread scheduling; timeout is 1 s. Main processing of the final frame is outside this renderer wait. | Not measured separately. | Not exported |
capture.teardownPost-flush teardown begins → context suspended and mic/nodes released. | AudioContext suspension and audio-device/resource cleanup can wait on browser/OS work. | Not measured separately. Stopping → stopped log gap was ≈1 ms in the 13:21 sample; final-frame handoff was logged 1 ms later. Neither isolates this teardown span. | Not exported |
lifecycle.recorder-closeClose-tail execution → queued writes, WAV finalization and duration enrichment finish. | File write queue, stream completion, header rewrite/file close, then metadata update can wait for I/O. | Not measured separately. | Duration: recorder_close_duration_ms |
context.draft-selection-copyDraft fallback begins → copied selection merged into session. | Native Cmd+C and clipboard polling every 25 ms, up to 300 ms; RPC is bounded at 2 s. | Not measured separately. | Not exported |
resolve.draft-selection-waitResolve adapter reaches copy barrier → barrier releases/times out. | Waits for the preceding copy fallback, bounded at 2.5 s. May overlap most of the copy operation. | Not measured separately. | Not exported; outside transcription.resolve |
transcription.resolveResolve effect entry → resolved result assembled. | Parent covers admitted-work drain, provider flush, formatting and remaining resolve work. | Not measured separately. Stop RPC dispatch → resolved log was 1,224 ms in one sample; this broader gap is not resolve duration. | Duration: resolve_duration_ms |
resolve.drainBegin waiting for admitted chunk work → that work settles. | Outstanding inference, network/provider work and lock backlogs can hold finalization. | Not measured separately. | Duration: resolve_drain_duration_ms |
resolve.flushAttempt transcription lock → provider flush and final-result merge finish. | Includes lock wait, final local inference or cloud result/formatting. Network/backend cost is not determined by the CPU in this machine. | Not measured separately. | Duration: resolve_flush_duration_ms |
resolve.formatprepareTranscriptText() → completion. | Can call a separate remote formatter or Ollama when configured; otherwise often only settings/text processing. Cloud formatting may already be in resolve.flush. | Not measured separately. | Duration: resolve_format_duration_ms |
storage.transcription-metadata-writePost-resolve enrichment requested → metadata update finishes. | Local SQLite read/update; only I/O contention/stalls make >20 ms compelling. Lower priority. | Not measured separately. | Not exported |
storage.commitFirst disposition-commit attempt begins → result/error. | Can await WAV custody (configured 5 s bound), then perform transaction/file cleanup. Later repair retries are outside this first attempt. | Not measured separately. | Duration: storage_duration_ms |
delivery.pasteDelivery effect begins → native paste dispatched. | Preserve-clipboard preference read and dispatch. Expected short unless local DB/event-loop work stalls. Lower priority. | Not measured separately. | Internal span only |
delivery.pastedDelivery effect begins → successful native paste acknowledgement. | Helper queue, clipboard format snapshot, clipboard writes and key-event dispatch can take time, especially with large clipboard data. | Not measured separately. | Duration: paste_duration_ms; end offset: pasted_offset_ms |
delivery.pasted name and pasted_offset_ms key for compatibility, but label their meaning correctly.The existing session root is retained as the lifecycle envelope. It starts at T0 and ends at idle; outstanding native responses and other obligations can end after it. Parent and child intervals overlap, so their durations must not be added.
9Names and clocks — simplified contract
Measure existing operations first. Native RPCs are measured entirely in main. Native code, response contracts, and generators are unchanged. Internal playback/mute subphases are deferred until later analysis justifies them.
9.1 · Interval attributes
name dotted namespace + hyphenated operation span_id unique within the event parent_span_id logical parent process main | renderer start_offset_ms epoch start timestamp − session-open epoch timestamp duration_ms elapsed milliseconds status ok | failed | interrupted chunk_index present for repeated chunk work
9.2 · Clock sources
Cross-process starts use Date.now() Unix-epoch milliseconds. Main and renderer share the machine's wall clock, so their offsets need no clock mapping. Local performance.now() and existing Effect timers supply elapsed durations where already used. Existing synthetic main spans can use Date differences.
start_offset_ms = startedAtMs − sessionStartMs
Renderer timing batches contain only phase name, epoch start, and elapsed duration for completed operations. The original cached-config query is retained. There is no clock-sync endpoint, uncertainty metadata, clock-change correction, or native timing payload.
9.3 · Measurement scope
- The session anchor remains session open, not physical shortcut press. Early provider warmup can have a negative offset.
- Native RPC duration includes transport, scheduling, playback and mute work inside that RPC. It does not isolate those internal parts.
- A helper reply does not establish acoustic silence or visible text insertion in another application.
- The local measurements in sections 4 and 8 remain historical evidence, not results from this change. Unmeasured internal work remains unmeasured.
10Implemented v2 attributes
The event remains transcription_completed_v2. It contains the selected main/renderer span durations and start offsets, with existing field meanings preserved. For native operations, it contains caller-observed RPC timing only.
10.1 · Payload contract
trace_spans holds the interval attributes in section 9. trace_points holds name, offset_ms, and process. The implicit root, identified by trace_root_span_id, starts at offset 0 and ends at session_duration_ms. Logical children can finish after the lifecycle returns to idle.
Example added properties — illustrative values
{
"trace_schema_version": 1,
"trace_offset_anchor": "session_open",
"trace_root_span_id": "root-example",
"trace_spans": [
{
"span_id": "phase-example",
"parent_span_id": "root-example",
"name": "native.start-recording.rpc",
"process": "main",
"start_offset_ms": 5,
"duration_ms": 248.1,
"status": "ok"
}
],
"trace_points": [
{
"name": "lifecycle.recording-stopping",
"offset_ms": 3000,
"process": "main"
}
],
"native_start_recording_rpc_start_offset_ms": 5,
"native_start_recording_rpc_duration_ms": 248
}10.2 · Flat duration / start-offset pairs
24 September trim: retained 12 additional spans for microphone/device startup, audio readiness, native start/stop RPCs, the recorder start gate, provider warmup, and context capture. Removed admission checks/queue, capture lock/config read, worklet load/flush, teardown/context close, VAD reset, recorder final-frame wait, WAV write/finalize, and metadata-write timers. Existing aggregate timings remain. The candidate audit in section 8 is historical; this table is the current field list.
These selected spans also have top-level properties for percentile queries. The first recorded instance supplies the flat pair; individual instances remain in trace_spans.
Expand implemented field pairs (22 spans)
| Span name | Event attributes |
|---|---|
lifecycle.mute-ambiance | mute_duration_msmute_start_offset_ms |
lifecycle.recorder-spinup | recorder_spinup_duration_msrecorder_spinup_start_offset_ms |
lifecycle.recorder-close | recorder_close_duration_msrecorder_close_start_offset_ms |
delivery.pasted | paste_duration_mspaste_start_offset_ms |
storage.commit | storage_duration_msstorage_start_offset_ms |
lifecycle.unmute-ambiance | unmute_duration_msunmute_start_offset_ms |
transcription.resolve | resolve_duration_msresolve_start_offset_ms |
resolve.drain | resolve_drain_duration_msresolve_drain_start_offset_ms |
resolve.flush | resolve_flush_duration_msresolve_flush_start_offset_ms |
resolve.format | resolve_format_duration_msresolve_format_start_offset_ms |
lifecycle.recorder-start-gate-wait | recorder_start_gate_wait_duration_msrecorder_start_gate_wait_start_offset_ms |
capture.enumerate-devices | capture_enumerate_devices_duration_mscapture_enumerate_devices_start_offset_ms |
capture.get-user-media | capture_get_user_media_duration_mscapture_get_user_media_start_offset_ms |
capture.audio-context-create | capture_audio_context_create_duration_mscapture_audio_context_create_start_offset_ms |
capture.audio-context-resume | capture_audio_context_resume_duration_mscapture_audio_context_resume_start_offset_ms |
capture.first-frame-wait | capture_first_frame_wait_duration_mscapture_first_frame_wait_start_offset_ms |
native.start-recording.rpc | native_start_recording_rpc_duration_msnative_start_recording_rpc_start_offset_ms |
native.stop-recording.rpc | native_stop_recording_rpc_duration_msnative_stop_recording_rpc_start_offset_ms |
context.accessibility-refresh | accessibility_refresh_duration_msaccessibility_refresh_start_offset_ms |
context.draft-selection-copy | draft_selection_copy_duration_msdraft_selection_copy_start_offset_ms |
resolve.draft-selection-wait | draft_selection_wait_duration_msdraft_selection_wait_start_offset_ms |
transcription.provider-warmup | provider_warmup_duration_msprovider_warmup_start_offset_ms |
Additional intervals in trace_spans include transcription.session-setup, transcription.provider-transcribe, and completed VAD work and lock waits. Existing VAD/provider aggregates and materialize_duration_ms retain their previous names and scope.
New point properties: recording_stopping_offset_ms (lifecycle entering stopping) and first_accepted_frame_offset_ms (main admits first PCM). Settings: dictation_sounds_enabled and system_audio_mute_enabled.
10.3 · Coverage and limits
- Native
startRecordingandstopRecordinguse their original requests and responses. Main records call start and elapsed time. No native subphase timestamps are returned. - The 12 selected additional spans are kept even when fast. Added operation timers record successful completions; a failed operation has no duration for that operation. Existing failure reporting and lifecycle spans remain. Completed VAD/provider work and lock waits are retained above 20 ms, capped at 64 per session with a dropped count. Full-session aggregates remain available.
- Completed startup timings piggyback existing renderer reports, including completed steps before a later capture failure. First-frame reports and the final completion batch are nonblocking. Capture cleanup is not timed. Format metadata sent with audio is retained as its latest value, not expanded into per-frame trace points.
- The existing 15 s telemetry grace bounds late reports after lifecycle idle. Incomplete obligations are listed without fabricated completion times.
- Paste confirmation retains its existing meaning: native acknowledgement, not observed insertion.
- No transcript, audio, clipboard/window text, device identifier, email or credential is added to these timing properties.