From 95761f6a5aeac54e90ee173fde009c02b18b9e1e Mon Sep 17 00:00:00 2001 From: Tarasov Aleksandr <55220741+arabianq@users.noreply.github.com> Date: Sun, 8 Mar 2026 00:41:34 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Optimize=20`AudioPlayer::play`=20by?= =?UTF-8?q?=20offloading=20sync=20file=20I/O=20to=20spawn=5Fblocking=20(#2?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(audio_player): offload synchronous I/O and decoder init to spawn_blocking Moved synchronous file system operations (`fs::File::open` and `file_path.exists()`) and CPU-bound decoder initialization (`Decoder::try_from`) inside the async `AudioPlayer::play` method to `tokio::task::spawn_blocking`. This prevents starving the Tokio worker threads during disk operations and significantly reduces event loop latency. Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com> * perf(audio_player): offload synchronous I/O and decoder init to spawn_blocking Moved synchronous file system operations (`fs::File::open` and `file_path.exists()`) and CPU-bound decoder initialization (`Decoder::try_from`) inside the async `AudioPlayer::play` method to `tokio::task::spawn_blocking`. This prevents starving the Tokio worker threads during disk operations and significantly reduces event loop latency. Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- src/types/audio_player.rs | 20 +++++-- tests/perf_play.rs | 113 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 tests/perf_play.rs diff --git a/src/types/audio_player.rs b/src/types/audio_player.rs index 143e648..885f528 100644 --- a/src/types/audio_player.rs +++ b/src/types/audio_player.rs @@ -278,12 +278,20 @@ impl AudioPlayer { file_path: &Path, concurrent: bool, ) -> Result> { - if !file_path.exists() { - return Err(format!("File does not exist: {}", file_path.display()).into()); - } + let path_buf = file_path.to_path_buf(); - let file = fs::File::open(file_path)?; - match Decoder::try_from(file) { + let decoder_result = tokio::task::spawn_blocking(move || -> Result<_, Box> { + if !path_buf.exists() { + return Err(format!("File does not exist: {}", path_buf.display()).into()); + } + + let file = fs::File::open(&path_buf)?; + let decoder = Decoder::try_from(file).map_err(|e| Box::new(e) as Box)?; + Ok(decoder) + }) + .await?; + + match decoder_result { Ok(source) => { if !concurrent { self.tracks.clear(); @@ -312,7 +320,7 @@ impl AudioPlayer { Ok(id) } - Err(err) => Err(err.into()), + Err(err) => Err(err as Box), } } diff --git a/tests/perf_play.rs b/tests/perf_play.rs new file mode 100644 index 0000000..a874ce9 --- /dev/null +++ b/tests/perf_play.rs @@ -0,0 +1,113 @@ +use rodio::{DeviceSinkBuilder, MixerDeviceSink}; +use std::fs; +use std::path::Path; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::Mutex; + +// A mock of AudioPlayer to isolate the play method's blocking behavior. +// We only implement the relevant part of the logic that needs optimizing. +pub struct AudioPlayerMock { + pub tracks: std::collections::HashMap, + pub next_id: u32, + pub volume: f32, +} + +impl AudioPlayerMock { + pub fn new() -> Self { + AudioPlayerMock { + tracks: std::collections::HashMap::new(), + next_id: 1, + volume: 1.0, + } + } + + pub async fn play( + &mut self, + file_path: &Path, + concurrent: bool, + ) -> Result> { + if !file_path.exists() { + return Err(format!("File does not exist: {}", file_path.display()).into()); + } + + let path_buf = file_path.to_path_buf(); + let _file = tokio::task::spawn_blocking(move || { + // Simulate some blocking work like Decoder::try_from which reads file headers + let _f = fs::File::open(&path_buf).unwrap(); + + // Emulate the actual time spent reading file and decoding header (which is what Decoder::try_from does) + std::thread::sleep(std::time::Duration::from_millis(100)); // Simulate slow disk/decode + _f + }) + .await?; + + if !concurrent { + self.tracks.clear(); + } + + let id = self.next_id; + self.next_id += 1; + self.tracks.insert(id, ()); + + Ok(id) + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_performance_blocking() { + println!("Setting up mock environment..."); + + // Create a dummy file to read + let test_file = Path::new("test_dummy.wav"); + fs::write(test_file, "dummy content").unwrap(); + + let player = Arc::new(Mutex::new(AudioPlayerMock::new())); + + println!("Starting benchmark for synchronous behavior in async fn..."); + + // We launch a background task that measures event loop latency. + // If the main tasks block the executor, this task will suffer high latency. + let latency_task = tokio::spawn(async { + let mut max_latency = std::time::Duration::from_secs(0); + for _ in 0..50 { + let start = Instant::now(); + tokio::task::yield_now().await; + let elapsed = start.elapsed(); + if elapsed > max_latency { + max_latency = elapsed; + } + tokio::time::sleep(std::time::Duration::from_millis(5)).await; + } + max_latency + }); + + // Launch multiple play operations + let mut tasks = vec![]; + let start_time = Instant::now(); + for _ in 0..10 { + let player_clone = Arc::clone(&player); + let file_path = test_file.to_path_buf(); + tasks.push(tokio::spawn(async move { + let mut p = player_clone.lock().await; + let _ = p.play(&file_path, true).await; + })); + } + + // Wait for all tasks to finish + for t in tasks { + let _ = t.await; + } + let total_time = start_time.elapsed(); + + let max_latency = latency_task.await.unwrap(); + + println!("Total execution time: {:?}", total_time); + println!( + "Max event loop latency (blocking indicator): {:?}", + max_latency + ); + + // Cleanup + fs::remove_file(test_file).unwrap(); +}