coreaudiod persistent 10% CPU usage when running hex
- Dominant language
- Swift
- Stars
- 2.9k
- Forks
- 226
- PR merge metrics
- No merged PRs in 30d
Description
## Bug Report: coreaudiod persistent 10% CPU usage after transcriptions
### Description
After performing a transcription, `coreaudiod` persistently uses ~10% CPU even when Hex is idle. The CPU usage only drops to 0% when the Hex app is completely terminated.
### Environment
- **macOS Version**: Tahoe (macOS 26.2)
- **Hardware**: Apple Silicon (M-series)
- **Hex Version**: Debug build from main branch
- **Transcription Engine**: Parakeet TDT v3 (FluidAudio)
### Steps to Reproduce
1. Launch Hex Debug
2. Start a recording (press hotkey)
3. Complete transcription (or cancel with ESC)
4. Check `coreaudiod` CPU usage:
```bash
ps aux | grep "coreaudiod$" | grep -v grep
```
5. Observe `coreaudiod` at ~10-12% CPU
6. Kill Hex app:
```bash
pkill -f "Hex Debug"
sleep 3
ps aux | grep "coreaudiod$" | grep -v grep
```
7. Observe `coreaudiod` drops to 0.0% CPU
### Expected Behavior
After transcription completes and the app is idle, `coreaudiod` should return to 0% CPU without requiring app termination.
### Actual Behavior
`coreaudiod` maintains 10-12% CPU usage until Hex is killed, suggesting the app is holding audio sessions or resources that aren't being released.
### Investigation Summary
#### Attempted Fixes (Unsuccessful)
1. **Setting model references to `nil`**:
- `whisperKit = nil`
- `parakeet.asr = nil`, `parakeet.models = nil`
- Added 1-3 second wait times after cleanup
- **Result**: No change, CPU stays at 10%
2. **Recording cleanup**:
- Properly calling `recorder.stop()` and `recorder.deleteRecording()`
- Setting `recorder = nil`
- Adding cleanup delays
- **Result**: No change
3. **Preventing Parakeet load in main app**:
- Modified `downloadAndLoadModel()` to skip `ensureLoaded()` for Parakeet
- Only helper process loads models
- **Result**: No change
#### Successful Workarounds
1. **Helper Process Isolation (Partial Success)**:
- Created `ParakeetHelper` binary that loads Parakeet models in separate process
- Helper is killed after transcription completes
- **Result**: Prevents Parakeet-related CPU leak, but ~10% leak remains
- **Location**: `Hex/Resources/ParakeetHelper`, source at `/ParakeetHelper/`
2. **Watchdog Script (Temporary)**:
- Auto-restarts Hex when `coreaudiod` stays above 8% for 20+ seconds
- **Location**: `tools/coreaudiod-watchdog.sh`
- **Usage**: `nohup tools/coreaudiod-watchdog.sh &`
### Root Cause Analysis
**Key Observation**: Killing the Hex app immediately drops `coreaudiod` to 0%, proving the leak is within the main app process, not the system daemon itself.
**Suspected Components**:
1. **AVAudioRecorder** (Primary Suspect):
- Recording uses `AVAudioRecorder` in `RecordingClient.swift`
- Even after calling `.stop()`, `.deleteRecording()`, and setting reference to `nil`, audio sessions may persist
- Similar to known CoreML audio session bug
- macOS doesn't provide `AVAudioSession.setActive(false)` like iOS (API unavailable)
2. **Audio Queue Services**:
- `AVAudioRecorder` uses Audio Queue Services internally
- These may not be properly released when recorder is destroyed
- No explicit API to force-close audio queues on macOS
### Logs
Console logs show proper cleanup sequence:
```
[com.kitlangton.Hex:Recording] Recording started
[com.kitlangton.Hex:Recording] Recorder fully destroyed and cleaned up
[com.kitlangton.Hex:Transcription] Parakeet helper completed in 2.6s, exit code: 0
[com.kitlangton.Hex:Transcription] ✅ Parakeet helper SIGKILL sent (PID 76887)
```
No errors or warnings about audio sessions, yet `coreaudiod` remains at 10% CPU.
### Relevant Code Locations
**Recording Cleanup** (`Hex/Clients/RecordingClient.swift:820-850`):
```swift
func stopRecording() async -> URL {
// Stop and copy BEFORE cleanup/deletion
if let recorder = recorder {
recorder.stop()
recorder.isMeteringEnabled = false
}
// Copy recording
exportedURL = try duplicateCurrentRecording()
// Cleanup
if let recorder = recorder {
recorder.deleteRecording()
}
self.recorder = nil
stopMeterTask()
endRecordingSession()
try? await Task.sleep(for: .milliseconds(100))
}
```
**ParakeetHelper Integration** (`Hex/Clients/TranscriptionClient.swift:178-260`):
- Spawns helper process with `Process()`
- Passes `XDG_CACHE_HOME` environment variable
- Waits for completion, reads JSON output
- Forcefully kills helper with `SIGKILL`
### Questions for Investigation
1. **Audio Session Management**:
- Does `AVAudioRecorder` on macOS have a proper cleanup API?
- Is there a way to explicitly invalidate audio sessions?
- Should we move recording to a helper process like transcription?
2. **Alternative Recording Approaches**:
- Could we use `AVCaptureSession` instead of `AVAudioRecorder`?
- Would `Audio Queue Services` API give more control?
- Is there a lower-level API that properly releases resources?
3. **System-Level Debugging**:
- Can we use `sample` command to profile `coreaudiod` and see what's holding resources?
- Are there Instruments templates to trace audio session lifecycle?
- What does DTrace show about audio queue activity?
### Temporary Workaround
Users experiencing this issue can use the watchdog script:
```bash
# Start watchdog (checks every 10s, restarts after 20s high CPU)
nohup tools/coreaudiod-watchdog.sh > /dev/null 2>&1 &
# Monitor logs
tail -f ~/Library/Logs/hex-watchdog.log
# Stop watchdog
pkill -f coreaudiod-watchdog
```
### Potential Solutions (For Discussion)
1. **Move Recording to Helper Process**:
- Similar to `ParakeetHelper`, create `RecordingHelper`
- Isolate `AVAudioRecorder` in separate process
- **Pros**: Guaranteed resource cleanup on process exit
- **Cons**: Complex IPC, potential audio device conflicts
2. **Investigate Audio Unit API**:
- Use lower-level Audio Units instead of `AVAudioRecorder`
- More control over audio graph lifecycle
- **Pros**: Explicit cleanup, better resource management
- **Cons**: More complex code, harder to maintain
3. **Periodic App Restart**:
- Built-in auto-restart after N transcriptions
- Cleaner than watchdog script
- **Pros**: Simple, user-configurable
- **Cons**: Doesn't fix root cause, interrupts workflow
### Additional Context
This appears to be a macOS-level bug where audio resources aren't properly released when Swift objects are deallocated. Similar issues exist with:
- CoreML models keeping audio sessions active
- AVFoundation not cleaning up audio queues
- System audio daemon not detecting unused sessions
### References
- CoreML audio leak: https://developer.apple.com/forums/thread/699908
- AVAudioRecorder lifecycle: https://developer.apple.com/documentation/avfaudio/avaudiorecorder
- Audio Session management (iOS only): https://developer.apple.com/documentation/avfaudio/avaudiosession
---
**Labels**: bug, performance, audio, macos-specific, needs-investigation
**Priority**: Medium (workaround exists, but impacts battery life)
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the cleanup path in Hex/Clients/RecordingClient.swift:820-850 and reproduce the issue using the listed recording steps and ps command. Review the ParakeetHelper integration in Hex/Clients/TranscriptionClient.swift:178-260 to separate transcription cleanup from recording cleanup. Done means coreaudiod returns to 0% after transcription while Hex remains running, without relying on tools/coreaudiod-watchdog.sh.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, swift
- Domain
- audio-video-rtc, desktop, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100