Skip to content

Coordinator: the feed and warm loops are a RecordingEngineSession - #80

Open
dinooo13 wants to merge 1 commit into
core/finish-pipelinefrom
core/recording-engine-session
Open

dinooo13 wants to merge 1 commit into
core/finish-pipelinefrom
core/recording-engine-session

Conversation

@dinooo13

Copy link
Copy Markdown
Owner

Part of #73, point 2 (second half: the warm and live loops). Stacked on #79 (core/finish-pipeline).

What

The engine's side of a recording leaves DictationCoordinator and becomes Sources/PladderCore/RecordingEngineSession.swift, a @MainActor class on the coordinator's clock. It holds the utterance, the feed loop with its live passes, the warm loop and warmupSamples; the coordinator's Stream, warmupTask, startEngineWork, startStreamingFeed and startWarmupLoop go.

  • begin() async: begins an utterance when the engine streams. A streaming engine that cannot begin is used like a batch engine, as before.
  • startLoops(): the feed loop (with live passes in Live Transcript) and the warm loop, or only the warm loop for a batch engine.
  • end() -> Ending: synchronous, it only cancels. It returns what the session ended with: the DictationFinish.Source to transcribe and the feed task to wait for.
  • Ending.abandon() async: waits for the feed and only then drops the utterance, for a cancel's cleanup.

The coordinator keeps the recording number, isCurrent, and the race check after await session.begin(). endRecording() calls end() where it used to cancel the feed and the warm task, still before the state flip. hotkeyReleased waits for ending.feed and hands ending.source to the finish. beginCancel's cleanup calls ending.abandon() after the microphone stops.

Deviations from the plan's sketch. None changes behaviour.

  • No isRecording closure. The loops check the session's own isEnded. Every path that ends a recording (release, cancel, drop) goes through endRecording(), which ends the session before the state flips, so isEnded is set exactly when the old state.isRecording check turned false.
  • A begin overtaken by end() is handled by the session. begin() returns Void; if the session ended while the utterance was beginning, begin() abandons that utterance itself, where the coordinator used to. startLoops() after end() does nothing.
  • abandon() is on Ending, the value end() returned, so the cleanup needs no reference to the session. It drops the utterance only if the session had one when it ended, exactly as the cleanup did with stream before.

DictationCoordinator.swift goes from 644 to 571 lines (676 on main); the session is 137.

Tests

The existing CoordinatorHandOverTests, theWarmLoopRunsOnTheClockAndStopsAtRelease, noLivePassStartsAfterTheRelease and keyDownWarmsTheEngine are unchanged and pass. New Tests/PladderCoreTests/RecordingEngineSessionTests.swift, 7 tests on ManualClock:

  • theFeedLoopHandsOverEveryDrainedChunkOnce
  • endCancelsTheWarmLoopBeforeReturning: the clock has no sleeper left the moment end() returns
  • aLivePassFinishingAfterEndPublishesNothing
  • aBatchEngineGetsOnlyTheWarmLoop
  • aStreamingEngineThatCannotBeginIsWarmedAndTranscribedWhole
  • abandonWaitsForTheFeedBeforeDroppingTheUtterance
  • aBeginOvertakenByEndDropsItsUtteranceAndStartsNoLoop

swift build (no warnings) and swift test (619) are green; the session, hand-over, regression and finish suites passed 20 runs in a row.

Benchmark (critical path)

swift run -c release pladder-cli bench-coordinator --runs 101, Apple M1, 16 GB, macOS 27.0. The builds ran back to back from copies of their release products.

Before, core/finish-pipeline (5a5a80f), load 1.72 → 1.46:

Hold Engine Total (median) Share (median / p90 / max) Engine excess (median)
1500 ms 150 ms 160 ms 0.10 ms / 0.11 ms / 0.13 ms 0.25 ms

After, this branch (621023c), load 1.34 → 1.40:

Hold Engine Total (median) Share (median / p90 / max) Engine excess (median)
1500 ms 150 ms 160 ms 0.10 ms / 0.11 ms / 0.13 ms 0.25 ms

The same run on main plus the harness (#77, d43924b) gave 160 ms / 0.08 / 0.10 / 0.14 ms / 0.25 ms, so the two splits together cost nothing measurable either. Engine excess, which holds the feed hand-over that moved here, is unchanged.

For the developer

  • Paced bench before (Coordinator: the finish pipeline is DictationFinish, which returns an outcome #79's branch) and after (this branch): swift run -c release pladder-cli bench bench/fixtures --paced --runs 2 --pause 2, and the same with --live, since the live loop moved
  • About 20 live dictations on an installed build of each, comparing the release-to-paste log lines: /usr/bin/log show --last 1h --style compact --predicate 'subsystem == "de.dinooo13.pladder"'

🤖 Generated with Claude Code

The utterance, the feed loop with its live passes, and the warm loop move into
a session on the coordinator's clock. It hands back what it ended with: the
source to transcribe and the feed to wait for. The coordinator keeps the
recording number and the checks after every wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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