Stop invalidating ContentView on streaming partial transcription updates#293
Open
NathanDrake2406 wants to merge 1 commit intoaltic-dev:mainfrom
Open
Stop invalidating ContentView on streaming partial transcription updates#293NathanDrake2406 wants to merge 1 commit intoaltic-dev:mainfrom
NathanDrake2406 wants to merge 1 commit intoaltic-dev:mainfrom
Conversation
During streaming ASR, partialTranscription writes fire at 1-5 Hz. Because it was declared @published, each write triggered Combine's synthesized objectWillChange.send(). AppServices.setupASRForwarding forwards ASRService.objectWillChange into AppServices.objectWillChange, and ContentView observes AppServices via @EnvironmentObject. The result: ContentView's body re-evaluated at the streaming cadence for the entire duration of every dictation, even though ContentView never reads partialTranscription. The mistaken assumption was that @published is a free observability primitive. Under legacy ObservableObject it is not. Any objectWillChange.send() invalidates every subscriber regardless of which property they read. A hot-path value whose only Combine consumer is MenuBarManager belongs on a dedicated subject that does not route through objectWillChange. Demote partialTranscription from @published to a private CurrentValueSubject, expose an AnyPublisher for subscription, and keep the String getter for direct reads. MenuBarManager switches from the $partialTranscription projected publisher to partialTranscriptionPublisher. All other @published properties on ASRService stay intact because they are cold signals (isRunning, isAsrReady, micStatus, errorTitle, etc.) that ContentView legitimately reacts to.
There was a problem hiding this comment.
Pull request overview
Reduces SwiftUI invalidation during streaming ASR by taking partialTranscription off ObservableObject.objectWillChange and exposing it via a dedicated Combine publisher for the single consumer (MenuBarManager).
Changes:
- Replaced
@Published partialTranscriptionwith a privateCurrentValueSubjectplus a synchronous getter andAnyPublisheraccessor. - Updated all internal writers to
send(...)through the subject. - Updated
MenuBarManagerto subscribe viapartialTranscriptionPublisherinstead of$partialTranscription.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| Sources/Fluid/Services/ASRService.swift | Moves hot-path partial transcription updates to a CurrentValueSubject to avoid objectWillChange storms. |
| Sources/Fluid/Services/MenuBarManager.swift | Switches subscription from $partialTranscription to the new partialTranscriptionPublisher. |
Comments suppressed due to low confidence (1)
Sources/Fluid/Services/MenuBarManager.swift:90
- The sink closure doesn’t reference
self, so[weak self]plusguard self != nilis redundant. Consider removing the weak capture/guard (or, if you intend to useselflater, unwrap withguard let self = self else { return }and actually useself).
.sink { [weak self] newText in
guard self != nil else { return }
// CRITICAL FIX: Check if streaming preview is enabled before updating notch
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
ASRService.partialTranscriptionmoves from@Publishedto a privateCurrentValueSubject<String, Never>exposed to subscribers via anAnyPublisher. The value getter and the single Combine consumer (MenuBarManager) are updated accordingly. No other@Publishedproperties onASRServiceare affected.Why
During streaming ASR,
partialTranscriptionis written 1-5 times per second depending on the model (parakeetRealtimeruns at 0.2s intervals, default models at 0.6s, Cohere at 1.0s perSettingsStore.swift).Because it was
@Published, every write fired Combine's synthesizedobjectWillChange.send().AppServices.setupASRForwardingpipesASRService.objectWillChangeintoAppServices.objectWillChange, andContentViewobservesAppServicesvia@EnvironmentObject. SwiftUI's legacyObservableObjectprotocol has no per-property dependency tracking, so every tick invalidatedContentViewand forced its 3,061-line body to re-evaluate, even thoughContentViewnever readspartialTranscription.MenuBarManageris the only consumer of this value (it forwards to the notch overlay when streaming preview is enabled). Moving the hot path offobjectWillChangeeliminates the invalidation storm without touching any legitimate cold-signal forwarding (isRunning,isAsrReady,micStatus,errorTitle, etc. stay@PublishedandContentViewcontinues to react to them correctly).Approach
@Published var partialTranscription: String = ""withprivate let partialTranscriptionSubject = CurrentValueSubject<String, Never>("").var partialTranscription: String { subject.value }for synchronous reads andvar partialTranscriptionPublisher: AnyPublisher<String, Never>for subscription.ASRServiceto callpartialTranscriptionSubject.send(...)instead of assigning the property.MenuBarManager.configure(asrService:)to subscribe viaasrService.partialTranscriptionPublisherinstead ofasrService.$partialTranscription.Non-goals: no changes to any other
@Publishedproperty, no migration to@Observable, no changes toAppServicesforwarding orContentView. This is the minimal surgical fix to the documented hot path.Validation
xcodebuild build: clean with respect to this change. (The swift-sdk MCP transport has two pre-existing data-race errors onupstream/mainthat remain; confirmed identical on the unmodified branch.)partialTranscriptionSubject.send(x)updates the getter and the published publisher on the next subscription tick.MenuBarManager's.receive(on: DispatchQueue.main).sink { ... }still delivers on main as before.Risks / follow-ups
$partialTranscriptionvia the Combine projected value will break and must usepartialTranscriptionPublisherinstead. Currently there are no other consumers.objectWillChangeforwarding forAudioHardwareObserverstill exists, and migratingASRServiceto the@Observablemacro would give per-property tracking across all fields). Those are out of scope for this PR.