[audio_core] Fix capture overflow busy-spin race In BaseCapturer::Process(), when the CapturePacketQueue (CPQ) is empty during async capture, the mix thread waits for packets in WaitForPendingPacket(). If the FIDL thread shuts down async capture (via StopAsyncCapture or BeginShutdown), CPQ::Shutdown() unblocks WaitForPendingPacket(). Previously, WaitForPendingPacket() returned void, and Process() only checked `state_.load() == State::Shutdown`. If the capturer had not yet transitioned to State::Shutdown (e.g. it was still in State::AsyncOperating or State::AsyncStopping), Process() assumed a new packet arrived, reported an overflow, and continued looping. Because CPQ was shutdown, NextMixerJob() returned nullopt and WaitForPendingPacket() returned immediately with 0 delay, causing Process() to busy-spin and invoke ReportOverflow() dozens of times every millisecond. This CL fixes the race by: 1. Updating CapturePacketQueue::WaitForPendingPacket() to return bool (`!shutdown_ && !pending_.empty()`), indicating whether a packet is actually available to mix. 2. In BaseCapturer::Process(), checking WaitForPendingPacket()'s return value, as well as `state_ == State::AsyncOperating`. If the queue was shut down or the capturer is stopping, Process() exits immediately with ZX_OK rather than reporting false overflows. 3. Adding unit test coverage for CPQ shutdown/wait unblocking and capturer async stop during packet starvation.. New/updated unittest cases in audio_core_unittests: CapturePacketQueueTest: WaitForPendingPacketWhenAlreadyShutdown WaitForPendingPacketReturnsTrueWhenPacketAvailable WaitForPendingPacketReturnsTrueWhenPacketPushed WaitForPendingPacketReturnsTrueWhenPacketRecycled WaitForPendingPacketReturnsFalseOnShutdown AudioCapturerTest: StopAsyncCaptureCleanShutdown Bug: 537405213 Bug: 539867284 Fixed: 545709661 Test: audio_core_unittests CONV: dd9b809f-fe9e-46c8-b4da-dd8f8af98fcd TAG: agy Change-Id: I3eb48773f2acad6dbe72f57299101b4a44990a5b Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/1758453 Fuchsia-Auto-Submit: Martin Puryear <mpuryear@google.com> Commit-Queue: Martin Puryear <mpuryear@google.com> Reviewed-by: Andres Oportus <andresoportus@google.com>
diff --git a/src/media/audio/audio_core/audio_capturer_unittest.cc b/src/media/audio/audio_core/audio_capturer_unittest.cc index 030ac57..ff2dcb5 100644 --- a/src/media/audio/audio_core/audio_capturer_unittest.cc +++ b/src/media/audio/audio_core/audio_capturer_unittest.cc
@@ -378,5 +378,31 @@ EXPECT_TRUE(channel_dropped); } +// Verify that stopping async capture when all packets are in flight / waiting completes cleanly, +// invoking the stop callback and emitting OnEndOfStream without spinning or hanging. +TEST_F(AudioCapturerTest, StopAsyncCaptureCleanShutdown) { + zx::vmo duplicate; + ASSERT_EQ( + vmo_.duplicate(ZX_RIGHT_TRANSFER | ZX_RIGHT_WRITE | ZX_RIGHT_READ | ZX_RIGHT_MAP, &duplicate), + ZX_OK); + fidl_capturer_->AddPayloadBuffer(0, std::move(duplicate)); + RunLoopUntilIdle(); + + fidl_capturer_->StartAsyncCapture(480); + RunLoopUntilIdle(); + + bool stop_callback_received = false; + bool end_of_stream_received = false; + fidl_capturer_.events().OnEndOfStream = [&end_of_stream_received]() { + end_of_stream_received = true; + }; + + fidl_capturer_->StopAsyncCapture([&stop_callback_received]() { stop_callback_received = true; }); + RunLoopUntilIdle(); + + EXPECT_TRUE(stop_callback_received); + EXPECT_TRUE(end_of_stream_received); +} + } // namespace } // namespace media::audio
diff --git a/src/media/audio/audio_core/base_capturer.cc b/src/media/audio/audio_core/base_capturer.cc index 1770650..bf0eebf 100644 --- a/src/media/audio/audio_core/base_capturer.cc +++ b/src/media/audio/audio_core/base_capturer.cc
@@ -591,8 +591,12 @@ // Wait until we have another packet or have shut down. // This waits for the caller to ACK a packet, so it might block indefinitely. auto overflow_start = zx::clock::get_monotonic(); - pq->WaitForPendingPacket(); - if (state_.load() == State::Shutdown) { + bool has_packet = pq->WaitForPendingPacket(); + + // If the queue was shut down, or if the capturer is no longer in AsyncOperating mode + // (e.g. StopAsyncCapture, Shutdown, or Disconnect occurred while we were waiting), + // exit immediately without reporting a spurious overflow or looping. + if (!has_packet || state_.load() != State::AsyncOperating) { return ZX_OK; }
diff --git a/src/media/audio/audio_core/capture_packet_queue.cc b/src/media/audio/audio_core/capture_packet_queue.cc index 5e54f141..8736f61 100644 --- a/src/media/audio/audio_core/capture_packet_queue.cc +++ b/src/media/audio/audio_core/capture_packet_queue.cc
@@ -254,12 +254,13 @@ }; } // namespace -void CapturePacketQueue::WaitForPendingPacket() { +bool CapturePacketQueue::WaitForPendingPacket() { TRACE_DURATION("audio", "CapturePacketQueue::WaitForPendingPacket"); scoped_unique_lock lock(mutex_); while (!shutdown_ && pending_.empty()) { pending_signal_.wait(lock); } + return !shutdown_ && !pending_.empty(); } } // namespace media::audio
diff --git a/src/media/audio/audio_core/capture_packet_queue.h b/src/media/audio/audio_core/capture_packet_queue.h index 36d3dae..5dab5cb 100644 --- a/src/media/audio/audio_core/capture_packet_queue.h +++ b/src/media/audio/audio_core/capture_packet_queue.h
@@ -182,7 +182,8 @@ void Shutdown(); // Block until the pending queue is non-empty or the queue has been shut down. - void WaitForPendingPacket(); + // Returns true if a pending packet is available to mix, or false if the queue was shut down. + bool WaitForPendingPacket(); private: enum class Mode { Preallocated, DynamicallyAllocated };
diff --git a/src/media/audio/audio_core/capture_packet_queue_unittest.cc b/src/media/audio/audio_core/capture_packet_queue_unittest.cc index b71a47c..c38d860 100644 --- a/src/media/audio/audio_core/capture_packet_queue_unittest.cc +++ b/src/media/audio/audio_core/capture_packet_queue_unittest.cc
@@ -6,6 +6,9 @@ #include <lib/syslog/cpp/macros.h> +#include <atomic> +#include <thread> + #include <gmock/gmock.h> #include <gtest/gtest.h> @@ -390,13 +393,138 @@ ASSERT_TRUE(push_result.is_error()); } -// Verify that WaitForPendingPacket returns immediately on shutdown: no block or double-unlock. -TEST_F(CapturePacketQueueTest, WaitForPendingPacketShutdown) { +// Verify that WaitForPendingPacket returns immediately with false when the queue is already shut +// down (exercising the non-blocking shutdown path where NextMixerJob returns nullopt). +TEST_F(CapturePacketQueueTest, WaitForPendingPacketWhenAlreadyShutdown) { CreateMapper(50); auto pq = CapturePacketQueue::CreateDynamicallyAllocated(payload_buffer_, kFormat); pq->Shutdown(); - pq->WaitForPendingPacket(); + EXPECT_FALSE(pq->WaitForPendingPacket()); + EXPECT_EQ(pq->NextMixerJob(), std::nullopt); +} + +// Verify that WaitForPendingPacket returns true immediately without blocking if pending packets +// are already available in the queue. +TEST_F(CapturePacketQueueTest, WaitForPendingPacketReturnsTrueWhenPacketAvailable) { + CreateMapper(50); + auto pq = CapturePacketQueue::CreateDynamicallyAllocated(payload_buffer_, kFormat); + auto push_result = pq->PushPending(0, 10, nullptr); + ASSERT_TRUE(push_result.is_ok()); + + EXPECT_TRUE(pq->WaitForPendingPacket()); + auto mix_state = pq->NextMixerJob(); + ASSERT_TRUE(mix_state.has_value()); + EXPECT_EQ(mix_state->frames, 10u); +} + +// Verify that WaitForPendingPacket blocks across threads when the queue is empty, and returns true +// once a new packet is dynamically pushed via PushPending. +TEST_F(CapturePacketQueueTest, WaitForPendingPacketReturnsTrueWhenPacketPushed) { + CreateMapper(50); + auto pq = CapturePacketQueue::CreateDynamicallyAllocated(payload_buffer_, kFormat); + + std::atomic<bool> worker_started = false; + std::atomic<bool> wait_finished = false; + std::atomic<bool> wait_result = false; + std::thread worker([&pq, &worker_started, &wait_finished, &wait_result]() { + worker_started = true; + wait_result = pq->WaitForPendingPacket(); + wait_finished = true; + }); + + while (!worker_started.load()) { + std::this_thread::yield(); + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + EXPECT_FALSE(wait_finished.load()); + auto push_result = pq->PushPending(0, 10, nullptr); + ASSERT_TRUE(push_result.is_ok()); + + worker.join(); + EXPECT_TRUE(wait_finished.load()); + EXPECT_TRUE(wait_result.load()); + + auto mix_state = pq->NextMixerJob(); + ASSERT_TRUE(mix_state.has_value()); + EXPECT_EQ(mix_state->frames, 10u); +} + +// Verify that WaitForPendingPacket blocks in preallocated mode when all packets are in flight, +// and unblocks returning true once a packet is returned via Recycle (exercising async capture +// overflow recovery). +TEST_F(CapturePacketQueueTest, WaitForPendingPacketReturnsTrueWhenPacketRecycled) { + CreateMapper(20); + auto result = CapturePacketQueue::CreatePreallocated(payload_buffer_, kFormat, 10); + ASSERT_TRUE(result.is_ok()) << result.error(); + auto pq = result.take_value(); + + // Pop both packets so the pending queue is exhausted. + auto mix_state1 = pq->NextMixerJob().value(); + ASSERT_EQ(CapturePacketQueue::PacketMixStatus::Done, pq->FinishMixerJob(mix_state1)); + auto p1 = pq->PopReady(); + + auto mix_state2 = pq->NextMixerJob().value(); + ASSERT_EQ(CapturePacketQueue::PacketMixStatus::Done, pq->FinishMixerJob(mix_state2)); + auto p2 = pq->PopReady(); + + ASSERT_EQ(pq->PendingSize(), 0u); + + std::atomic<bool> worker_started = false; + std::atomic<bool> wait_finished = false; + std::atomic<bool> wait_result = false; + std::thread worker([&pq, &worker_started, &wait_finished, &wait_result]() { + worker_started = true; + wait_result = pq->WaitForPendingPacket(); + wait_finished = true; + }); + + while (!worker_started.load()) { + std::this_thread::yield(); + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + EXPECT_FALSE(wait_finished.load()); + auto recycle_result = pq->Recycle(p1->stream_packet()); + ASSERT_TRUE(recycle_result.is_ok()) << recycle_result.error(); + + worker.join(); + EXPECT_TRUE(wait_finished.load()); + EXPECT_TRUE(wait_result.load()); + + auto mix_state = pq->NextMixerJob(); + ASSERT_TRUE(mix_state.has_value()); + EXPECT_EQ(mix_state->frames, 10u); +} + +// Verify that a thread blocked in WaitForPendingPacket is unblocked and returns false when +// Shutdown is called concurrently (exercising cancellation/shutdown during packet starvation). +TEST_F(CapturePacketQueueTest, WaitForPendingPacketReturnsFalseOnShutdown) { + CreateMapper(50); + auto pq = CapturePacketQueue::CreateDynamicallyAllocated(payload_buffer_, kFormat); + + std::atomic<bool> worker_started = false; + std::atomic<bool> wait_finished = false; + std::atomic<bool> wait_result = true; + std::thread worker([&pq, &worker_started, &wait_finished, &wait_result]() { + worker_started = true; + wait_result = pq->WaitForPendingPacket(); + wait_finished = true; + }); + + while (!worker_started.load()) { + std::this_thread::yield(); + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + EXPECT_FALSE(wait_finished.load()); + + pq->Shutdown(); + worker.join(); + + EXPECT_TRUE(wait_finished.load()); + EXPECT_FALSE(wait_result.load()); + EXPECT_EQ(pq->NextMixerJob(), std::nullopt); } } // namespace media::audio