From b1ee49c4f87d177da38c366680494723ab6707cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=A3=E5=85=83=E5=BE=81?= Date: Sun, 17 May 2026 01:07:38 +0800 Subject: [PATCH 1/2] perf(recording): reduce cursor flush overhead and instant buffer --- crates/recording/src/cursor.rs | 57 ++++++++++++++----- crates/recording/src/output_pipeline/macos.rs | 55 ++++++++++++++---- 2 files changed, 86 insertions(+), 26 deletions(-) diff --git a/crates/recording/src/cursor.rs b/crates/recording/src/cursor.rs index c6ffaa63e47..769e21f13ae 100644 --- a/crates/recording/src/cursor.rs +++ b/crates/recording/src/cursor.rs @@ -1,12 +1,15 @@ 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 serde::Serialize; use std::{ collections::HashMap, + fs::File, + io::BufWriter, path::{Path, PathBuf}, time::Instant, }; @@ -50,19 +53,32 @@ impl CursorActor { const CURSOR_FLUSH_INTERVAL_SECS: u64 = 5; +#[derive(Serialize)] +struct CursorEventsSnapshot<'a> { + clicks: &'a [CursorClickEvent], + moves: &'a [CursorMoveEvent], +} + fn flush_cursor_data(output_path: &Path, moves: &[CursorMoveEvent], clicks: &[CursorClickEvent]) { - let events = CursorEvents { - clicks: clicks.to_vec(), - moves: moves.to_vec(), - }; - if let Ok(json) = serde_json::to_string_pretty(&events) - && let Err(e) = std::fs::write(output_path, json) - { - tracing::error!( - "Failed to write cursor data to {}: {}", - output_path.display(), - e - ); + let events = CursorEventsSnapshot { clicks, moves }; + match File::create(output_path) { + Ok(file) => { + 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 + ); + } + } + Err(e) => { + tracing::error!( + "Failed to write cursor data to {}: {}", + output_path.display(), + e + ); + } } } @@ -227,6 +243,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_cursor_id: Option = None; + let mut last_flushed_cursor_counts: Option<(usize, usize)> = None; + let mut last_flushed_keyboard_count: Option = None; loop { let sleep = tokio::time::sleep(Duration::from_millis(16)); @@ -361,11 +379,20 @@ pub fn spawn_cursor_recorder( last_keys = current_keys; if last_flush.elapsed() >= flush_interval { + let cursor_counts = (response.moves.len(), response.clicks.len()); + let keyboard_count = response.keyboard_presses.len(); + if let Some(ref path) = incremental_outputs.cursor { - flush_cursor_data(path, &response.moves, &response.clicks); + if last_flushed_cursor_counts != Some(cursor_counts) { + flush_cursor_data(path, &response.moves, &response.clicks); + last_flushed_cursor_counts = Some(cursor_counts); + } } if let Some(ref kb_path) = incremental_outputs.keyboard { - flush_keyboard_data(kb_path, &response.keyboard_presses); + if last_flushed_keyboard_count != Some(keyboard_count) { + flush_keyboard_data(kb_path, &response.keyboard_presses); + last_flushed_keyboard_count = Some(keyboard_count); + } } last_flush = Instant::now(); } diff --git a/crates/recording/src/output_pipeline/macos.rs b/crates/recording/src/output_pipeline/macos.rs index 9eeb815002a..6a812a7bb95 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,14 +51,15 @@ 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()) - .unwrap_or(if instant_mode { - DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT - } else { - DEFAULT_MP4_MUXER_BUFFER_SIZE - }) + let parse_env = |name: &str| std::env::var(name).ok().and_then(|s| s.parse().ok()); + + if instant_mode { + parse_env("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT") + .or_else(|| parse_env("CAP_MP4_MUXER_BUFFER_SIZE")) + .unwrap_or(DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT) + } else { + parse_env("CAP_MP4_MUXER_BUFFER_SIZE").unwrap_or(DEFAULT_MP4_MUXER_BUFFER_SIZE) + } } fn get_mp4_audio_finish_timeout(instant_mode: bool) -> Duration { @@ -1210,8 +1211,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] @@ -1233,6 +1234,22 @@ mod tests { assert_eq!(instant, 500); } + #[test] + fn instant_env_override_takes_precedence_over_global_override() { + unsafe { + std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", "500"); + std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT", "120"); + } + 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"); + std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT"); + } + assert_eq!(normal, 500); + assert_eq!(instant, 120); + } + #[test] fn invalid_env_falls_back_to_defaults() { unsafe { @@ -1246,6 +1263,22 @@ mod tests { assert_eq!(normal, DEFAULT_MP4_MUXER_BUFFER_SIZE); assert_eq!(instant, DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT); } + + #[test] + fn invalid_instant_override_falls_back_to_global_override() { + unsafe { + std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", "80"); + std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT", "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"); + std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT"); + } + assert_eq!(normal, 80); + assert_eq!(instant, 80); + } } mod mp4_audio_finish_timeout { From 97f8b7c1c1e7e9fa964df0e9cd27bafbe31ffb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=A3=E5=85=83=E5=BE=81?= Date: Sun, 17 May 2026 04:59:16 +0800 Subject: [PATCH 2/2] fix(recording): address review notes for cursor flush and env tests --- crates/recording/src/cursor.rs | 8 +- crates/recording/src/output_pipeline/macos.rs | 106 ++++++++++-------- 2 files changed, 69 insertions(+), 45 deletions(-) diff --git a/crates/recording/src/cursor.rs b/crates/recording/src/cursor.rs index 769e21f13ae..4cdcba4000d 100644 --- a/crates/recording/src/cursor.rs +++ b/crates/recording/src/cursor.rs @@ -9,7 +9,7 @@ use serde::Serialize; use std::{ collections::HashMap, fs::File, - io::BufWriter, + io::{BufWriter, Write}, path::{Path, PathBuf}, time::Instant, }; @@ -70,6 +70,12 @@ fn flush_cursor_data(output_path: &Path, moves: &[CursorMoveEvent], clicks: &[Cu output_path.display(), e ); + } else if let Err(e) = writer.flush() { + tracing::error!( + "Failed to flush cursor data to {}: {}", + output_path.display(), + e + ); } } Err(e) => { diff --git a/crates/recording/src/output_pipeline/macos.rs b/crates/recording/src/output_pipeline/macos.rs index 6a812a7bb95..6e10682177a 100644 --- a/crates/recording/src/output_pipeline/macos.rs +++ b/crates/recording/src/output_pipeline/macos.rs @@ -1198,6 +1198,22 @@ mod tests { mod mp4_muxer_buffer_size { use super::*; + static MP4_MUXER_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + fn with_muxer_env_lock(f: impl FnOnce()) { + let _guard = MP4_MUXER_ENV_LOCK + .lock() + .expect("mp4 muxer env lock should not be poisoned"); + f(); + } + + fn clear_muxer_env_overrides() { + unsafe { + std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE"); + std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT"); + } + } + #[test] fn instant_mode_buffer_is_larger_than_normal() { let instant = DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT; @@ -1222,62 +1238,64 @@ 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_lock(|| { + clear_muxer_env_overrides(); + 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); + clear_muxer_env_overrides(); + assert_eq!(normal, 500); + assert_eq!(instant, 500); + }); } #[test] fn instant_env_override_takes_precedence_over_global_override() { - unsafe { - std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", "500"); - std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT", "120"); - } - 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"); - std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT"); - } - assert_eq!(normal, 500); - assert_eq!(instant, 120); + with_muxer_env_lock(|| { + clear_muxer_env_overrides(); + unsafe { + std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", "500"); + std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT", "120"); + } + let normal = get_mp4_muxer_buffer_size(false); + let instant = get_mp4_muxer_buffer_size(true); + clear_muxer_env_overrides(); + assert_eq!(normal, 500); + assert_eq!(instant, 120); + }); } #[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_lock(|| { + clear_muxer_env_overrides(); + 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); + clear_muxer_env_overrides(); + assert_eq!(normal, DEFAULT_MP4_MUXER_BUFFER_SIZE); + assert_eq!(instant, DEFAULT_MP4_MUXER_BUFFER_SIZE_INSTANT); + }); } #[test] fn invalid_instant_override_falls_back_to_global_override() { - unsafe { - std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", "80"); - std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT", "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"); - std::env::remove_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT"); - } - assert_eq!(normal, 80); - assert_eq!(instant, 80); + with_muxer_env_lock(|| { + clear_muxer_env_overrides(); + unsafe { + std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE", "80"); + std::env::set_var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT", "not_a_number"); + } + let normal = get_mp4_muxer_buffer_size(false); + let instant = get_mp4_muxer_buffer_size(true); + clear_muxer_env_overrides(); + assert_eq!(normal, 80); + assert_eq!(instant, 80); + }); } }