From 851d2df88cc8ef7a8368f74e8b2e7254861ee00a Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:35:40 +0800 Subject: [PATCH] fix(audio): three 100% CPU busy loops on the _pa path (#16229) The Linux audio service in `--server` ignored the `Err` from `next_raw()`, so once the cm-side `_pa` peer closed, every iteration re-polled a dead socket: tokio-util's paused `Framed` issues one 0-byte read per poll and returns ready at once, never `Pending`. The thread never parked and burned a full core for the life of the process. Propagate instead, so `ServiceTmpl::run`'s existing backoff ends the inner loop and reconnects. Two sibling loops on the same audio path have the same shape: - `ipc::start_pa` (runs in `--cm`) ignored the `Err` from `psimple::Simple::read`, so a dead pulse handle spins there instead. - `start_voice_call`'s forwarding thread polls two channels with `try_recv` and has no blocking primitive at all: measured 99.8% of a core for the whole call, against 1.0% with a 1 ms pause (audio packets arrive every 10 ms). fix https://github.com/rustdesk/rustdesk/issues/16226 Co-authored-by: Claude Opus 5 (1M context) --- src/client/io_loop.rs | 3 +++ src/ipc.rs | 8 +++++++- src/server/audio_service.rs | 6 +++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 9de20588e..e893a4780 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -598,6 +598,9 @@ impl Remote { } else { log::debug!("Failed to record local audio channel: {}", err); } + // Both arms fall through with nothing else in this loop blocking, so + // without a pause the thread spun a core for the whole voice call. + std::thread::sleep(std::time::Duration::from_millis(1)); } } } diff --git a/src/ipc.rs b/src/ipc.rs index f4dc4f3f2..b750607be 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1497,7 +1497,13 @@ pub async fn start_pa() { None, // Use default buffering attributes ) { Ok(s) => loop { - if let Ok(_) = s.read(&mut buf) { + // A dead pulse handle fails every read at once, so ignoring the + // error left nothing pacing this loop and it burned a core. + if let Err(err) = s.read(&mut buf) { + log::error!("Failed to read audio data:{}", err); + break; + } + { let out = if buf.iter().filter(|x| **x != 0).next().is_none() { vec![] diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index 4f17115ea..cfa98b047 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -124,7 +124,11 @@ mod pa_impl { })?; #[cfg(target_os = "linux")] - if let Ok(data) = stream.next_raw().await { + { + // The `_pa` peer closing surfaces as `Err` here. Dropping it left the loop polling + // a dead socket -- one 0-byte read per poll, ready at once and never `Pending` -- + // which burned a full core for the rest of the process lifetime. + let data = stream.next_raw().await?; if data.len() == 0 { send_f32(&zero_audio_frame, &mut encoder, &sp); continue;