You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Non-blocking follow-up from the review of #4233 / #4251 (both merged). Flagged by @aethersdr-agent and confirmed independently.
Problem
AudioEngine::audioEndpointDiagnostics() (src/core/AudioEngine.cpp) is const and invoked from AutomationServer (get audio) and DeviceDiagnostics on the main/bridge thread, but AudioEngine lives on m_audioThread (MainWindow.cppm_audio->moveToThread(m_audioThread)). The method reads state that is owned and mutated on the audio thread:
m_txCaptureHealth.snapshot() — the TxCaptureHealthTracker's non-atomic members (m_tciSuppressedCallbacks, m_lastMicReadMs, m_currentlySaturated, …), written in onTxAudioReady, recordMicRead, and the queued stateChanged lambda;
txCaptureBufferCapacityBytes() → m_audioSource->bufferSize(); and (macOS) m_micBuffer->size().
So the bridge thread reads while the audio thread writes → a data race on non-atomic members. Torn reads are diagnostic-only (benign counter values, and m_micDevice is a QPointer so it degrades to null rather than dangling), which is why it wasn't blocking.
Why it's worth fixing
It's consistent with a pre-existing pattern in the same method (it already reads m_audioSource->state() / error(), m_micDevice->isOpen() cross-thread), but the tell is the asymmetry the author deliberately created in #4233/#4251: recordLocalTxAttempt is marshaled onto the owning thread "so diagnostics cannot race readyRead/stateChanged" — yet the read side of the very same data is not marshaled. bytesAvailable() racing readAll() on one QIODevice is also a genuinely unsafe concurrent Qt-object call, not just a torn scalar.
Options
Marshal the whole snapshot onto the audio thread (QMetaObject::invokeMethod(..., Qt::BlockingQueuedConnection)) and return the assembled values — cleanest, matches the write-side discipline.
Or expose the diagnostic-relevant fields as std::atomic and copy the buffer byte counts under a small mutex.
Ideally fix the whole method, not just the TX-capture-health additions, so the pre-existing state()/isOpen() reads are covered too.
Non-blocking follow-up from the review of #4233 / #4251 (both merged). Flagged by @aethersdr-agent and confirmed independently.
Problem
AudioEngine::audioEndpointDiagnostics()(src/core/AudioEngine.cpp) isconstand invoked fromAutomationServer(get audio) andDeviceDiagnosticson the main/bridge thread, butAudioEnginelives onm_audioThread(MainWindow.cppm_audio->moveToThread(m_audioThread)). The method reads state that is owned and mutated on the audio thread:m_txCaptureHealth.snapshot()— theTxCaptureHealthTracker's non-atomic members (m_tciSuppressedCallbacks,m_lastMicReadMs,m_currentlySaturated, …), written inonTxAudioReady,recordMicRead, and the queuedstateChangedlambda;txCaptureBufferedBytes()→m_micDevice->bytesAvailable(), which races the Linux drain'sm_micDevice->readAll()([audio] Fix Linux PC Audio capture stall after TCI TX #4251) mutating the sameQIODevice;txCaptureBufferCapacityBytes()→m_audioSource->bufferSize(); and (macOS)m_micBuffer->size().So the bridge thread reads while the audio thread writes → a data race on non-atomic members. Torn reads are diagnostic-only (benign counter values, and
m_micDeviceis aQPointerso it degrades to null rather than dangling), which is why it wasn't blocking.Why it's worth fixing
It's consistent with a pre-existing pattern in the same method (it already reads
m_audioSource->state()/error(),m_micDevice->isOpen()cross-thread), but the tell is the asymmetry the author deliberately created in #4233/#4251:recordLocalTxAttemptis marshaled onto the owning thread "so diagnostics cannot race readyRead/stateChanged" — yet the read side of the very same data is not marshaled.bytesAvailable()racingreadAll()on oneQIODeviceis also a genuinely unsafe concurrent Qt-object call, not just a torn scalar.Options
QMetaObject::invokeMethod(..., Qt::BlockingQueuedConnection)) and return the assembled values — cleanest, matches the write-side discipline.std::atomicand copy the buffer byte counts under a small mutex.state()/isOpen()reads are covered too.Low priority / diagnostic-only. Ref: #4233, #4251.