From 63eea3472093d499376a6d0dbd11aa55fb7305bb Mon Sep 17 00:00:00 2001 From: ibbxx <134438301+ibbxx@users.noreply.github.com> Date: Sun, 17 May 2026 09:56:26 +0800 Subject: [PATCH 1/2] perf(recording): reduce recording memory pressure --- crates/recording/src/cursor.rs | 54 +++++++-- crates/recording/src/output_pipeline/macos.rs | 103 +++++++++++++----- 2 files changed, 121 insertions(+), 36 deletions(-) diff --git a/crates/recording/src/cursor.rs b/crates/recording/src/cursor.rs index c6ffaa63e47..bfa3c173527 100644 --- a/crates/recording/src/cursor.rs +++ b/crates/recording/src/cursor.rs @@ -1,12 +1,14 @@ use cap_cursor_capture::CursorCropBounds; use cap_cursor_info::CursorShape; use cap_project::{ - CursorClickEvent, CursorEvents, CursorMoveEvent, KeyPressEvent, KeyboardEvents, XY, + CursorClickEvent, CursorMoveEvent, KeyPressEvent, KeyboardEvents, XY, }; use cap_timestamp::Timestamps; use futures::{FutureExt, future::Shared}; use std::{ collections::HashMap, + fs::File, + io::{BufWriter, Write}, path::{Path, PathBuf}, time::Instant, }; @@ -51,15 +53,35 @@ impl CursorActor { const CURSOR_FLUSH_INTERVAL_SECS: u64 = 5; fn flush_cursor_data(output_path: &Path, moves: &[CursorMoveEvent], clicks: &[CursorClickEvent]) { - let events = CursorEvents { - clicks: clicks.to_vec(), - moves: moves.to_vec(), + #[derive(serde::Serialize)] + struct BorrowedCursorEvents<'a> { + clicks: &'a [CursorClickEvent], + moves: &'a [CursorMoveEvent], + } + + let file = match File::create(output_path) { + Ok(file) => file, + Err(e) => { + tracing::error!( + "Failed to create cursor data file {}: {}", + output_path.display(), + e + ); + return; + } }; - if let Ok(json) = serde_json::to_string_pretty(&events) - && let Err(e) = std::fs::write(output_path, json) - { + + let events = BorrowedCursorEvents { clicks, moves }; + let mut writer = BufWriter::new(file); + if let Err(e) = serde_json::to_writer(&mut writer, &events) { + tracing::error!( + "Failed to serialize cursor data to {}: {}", + output_path.display(), + e + ); + } else if let Err(e) = writer.write_all(b"\n").and_then(|_| writer.flush()) { tracing::error!( - "Failed to write cursor data to {}: {}", + "Failed to flush cursor data to {}: {}", output_path.display(), e ); @@ -226,6 +248,8 @@ pub fn spawn_cursor_recorder( let mut last_flush = Instant::now(); let flush_interval = Duration::from_secs(CURSOR_FLUSH_INTERVAL_SECS); + let mut last_flushed_cursor_moves = 0; + let mut last_flushed_cursor_clicks = 0; let mut last_cursor_id: Option = None; loop { @@ -362,7 +386,13 @@ pub fn spawn_cursor_recorder( if last_flush.elapsed() >= flush_interval { if let Some(ref path) = incremental_outputs.cursor { - flush_cursor_data(path, &response.moves, &response.clicks); + if response.moves.len() != last_flushed_cursor_moves + || response.clicks.len() != last_flushed_cursor_clicks + { + flush_cursor_data(path, &response.moves, &response.clicks); + last_flushed_cursor_moves = response.moves.len(); + last_flushed_cursor_clicks = response.clicks.len(); + } } if let Some(ref kb_path) = incremental_outputs.keyboard { flush_keyboard_data(kb_path, &response.keyboard_presses); @@ -374,7 +404,11 @@ pub fn spawn_cursor_recorder( info!("cursor recorder done"); if let Some(ref path) = incremental_outputs.cursor { - flush_cursor_data(path, &response.moves, &response.clicks); + if response.moves.len() != last_flushed_cursor_moves + || response.clicks.len() != last_flushed_cursor_clicks + { + flush_cursor_data(path, &response.moves, &response.clicks); + } } if let Some(ref kb_path) = incremental_outputs.keyboard { diff --git a/crates/recording/src/output_pipeline/macos.rs b/crates/recording/src/output_pipeline/macos.rs index 9eeb815002a..b3e45bac072 100644 --- a/crates/recording/src/output_pipeline/macos.rs +++ b/crates/recording/src/output_pipeline/macos.rs @@ -24,7 +24,7 @@ use std::{ use tracing::*; const DEFAULT_MP4_MUXER_BUFFER_SIZE: usize = 60; -const DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT: usize = 240; +const DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT: usize = 96; const DEFAULT_MP4_AUDIO_FINISH_TIMEOUT: Duration = Duration::from_secs(2); const DEFAULT_MP4_AUDIO_FINISH_TIMEOUT_INSTANT: Duration = Duration::from_secs(8); @@ -51,9 +51,17 @@ fn get_available_disk_space_mb(path: &std::path::Path) -> Option { } fn get_mp4_muxer_buffer_size(instant_mode: bool) -> usize { - std::env::var("CAP_MP4_MUXER_BUFFER_SIZE") - .ok() - .and_then(|s| s.parse().ok()) + let instant_override = instant_mode + .then(|| std::env::var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT").ok()) + .flatten() + .and_then(|s| s.parse().ok()); + + instant_override + .or_else(|| { + std::env::var("CAP_MP4_MUXER_BUFFER_SIZE") + .ok() + .and_then(|s| s.parse().ok()) + }) .unwrap_or(if instant_mode { DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT } else { @@ -1197,6 +1205,37 @@ mod tests { mod mp4_muxer_buffer_size { use super::*; + static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + fn with_muxer_env( + global: Option<&str>, + instant: Option<&str>, + test: impl FnOnce() -> T, + ) -> T { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { + match global { + Some(value) => std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", value), + None => std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE"), + } + match instant { + Some(value) => { + std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT", value) + } + None => std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT"), + } + } + + let result = test(); + + unsafe { + std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE"); + std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT"); + } + + result + } + #[test] fn instant_mode_buffer_is_larger_than_normal() { let instant = DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT; @@ -1210,8 +1249,8 @@ mod tests { } #[test] - fn instant_mode_default_is_240() { - assert_eq!(DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT, 240); + fn instant_mode_default_is_96() { + assert_eq!(DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT, 96); } #[test] @@ -1221,30 +1260,42 @@ mod tests { #[test] fn env_override_takes_precedence() { - unsafe { - std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", "500"); - } - let normal = get_mp4_muxer_buffer_size(false); - let instant = get_mp4_muxer_buffer_size(true); - unsafe { - std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE"); - } - assert_eq!(normal, 500); - assert_eq!(instant, 500); + with_muxer_env(Some("500"), None, || { + let normal = get_mp4_muxer_buffer_size(false); + let instant = get_mp4_muxer_buffer_size(true); + assert_eq!(normal, 500); + assert_eq!(instant, 500); + }); + } + + #[test] + fn instant_env_override_takes_precedence_over_global_override() { + with_muxer_env(Some("500"), Some("120"), || { + let normal = get_mp4_muxer_buffer_size(false); + let instant = get_mp4_muxer_buffer_size(true); + assert_eq!(normal, 500); + assert_eq!(instant, 120); + }); + } + + #[test] + fn invalid_instant_override_falls_back_to_global_override() { + with_muxer_env(Some("500"), Some("not_a_number"), || { + let normal = get_mp4_muxer_buffer_size(false); + let instant = get_mp4_muxer_buffer_size(true); + assert_eq!(normal, 500); + assert_eq!(instant, 500); + }); } #[test] fn invalid_env_falls_back_to_defaults() { - unsafe { - std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", "not_a_number"); - } - let normal = get_mp4_muxer_buffer_size(false); - let instant = get_mp4_muxer_buffer_size(true); - unsafe { - std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE"); - } - assert_eq!(normal, DEFAULT_MP4_MUXER_BUFFER_SIZE); - assert_eq!(instant, DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT); + with_muxer_env(Some("not_a_number"), None, || { + let normal = get_mp4_muxer_buffer_size(false); + let instant = get_mp4_muxer_buffer_size(true); + assert_eq!(normal, DEFAULT_MP4_MUXER_BUFFER_SIZE); + assert_eq!(instant, DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT); + }); } } From 4a34ab0b3f85002c42eac3c58999053a3a048a6c Mon Sep 17 00:00:00 2001 From: ibbxx <134438301+ibbxx@users.noreply.github.com> Date: Mon, 18 May 2026 07:23:25 +0800 Subject: [PATCH 2/2] test(recording): clean muxer env after panic --- crates/recording/src/output_pipeline/macos.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/recording/src/output_pipeline/macos.rs b/crates/recording/src/output_pipeline/macos.rs index b3e45bac072..ce8ec0d9113 100644 --- a/crates/recording/src/output_pipeline/macos.rs +++ b/crates/recording/src/output_pipeline/macos.rs @@ -1212,7 +1212,7 @@ mod tests { instant: Option<&str>, test: impl FnOnce() -> T, ) -> T { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); unsafe { match global { Some(value) => std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", value), @@ -1226,14 +1226,17 @@ mod tests { } } - let result = test(); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(test)); unsafe { std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE"); std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT"); } - result + match result { + Ok(value) => value, + Err(error) => std::panic::resume_unwind(error), + } } #[test]