From aac8af9c26ceae772dd2c12b9daaf8873c786a87 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 16 Mar 2026 14:03:07 -0700 Subject: [PATCH] Honor cpuprofile when pprof is disabled --- seaweed-volume/src/config.rs | 4 + seaweed-volume/src/main.rs | 19 ++- seaweed-volume/src/server/grpc_client.rs | 2 + seaweed-volume/src/server/mod.rs | 1 + seaweed-volume/src/server/profiling.rs | 187 +++++++++++++++++++++++ 5 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 seaweed-volume/src/server/profiling.rs diff --git a/seaweed-volume/src/config.rs b/seaweed-volume/src/config.rs index 34e06c0d3..9beee7f50 100644 --- a/seaweed-volume/src/config.rs +++ b/seaweed-volume/src/config.rs @@ -217,6 +217,8 @@ pub struct VolumeServerConfig { pub white_list: Vec, pub fix_jpg_orientation: bool, pub read_mode: ReadMode, + pub cpu_profile: String, + pub mem_profile: String, pub compaction_byte_per_second: i64, pub maintenance_byte_per_second: i64, pub file_size_limit_bytes: i64, @@ -759,6 +761,8 @@ fn resolve_config(cli: Cli) -> VolumeServerConfig { white_list, fix_jpg_orientation: cli.fix_jpg_orientation, read_mode, + cpu_profile: cli.cpu_profile, + mem_profile: cli.mem_profile, compaction_byte_per_second: cli.compaction_mb_per_second as i64 * 1024 * 1024, maintenance_byte_per_second: cli.maintenance_mb_per_second as i64 * 1024 * 1024, file_size_limit_bytes: cli.file_size_limit_mb as i64 * 1024 * 1024, diff --git a/seaweed-volume/src/main.rs b/seaweed-volume/src/main.rs index f03d5de32..9cac9d46f 100644 --- a/seaweed-volume/src/main.rs +++ b/seaweed-volume/src/main.rs @@ -13,6 +13,7 @@ use seaweed_volume::security::{Guard, SigningKey}; use seaweed_volume::server::debug::build_debug_router; use seaweed_volume::server::grpc_client::load_outgoing_grpc_tls; use seaweed_volume::server::grpc_server::VolumeGrpcService; +use seaweed_volume::server::profiling::CpuProfileSession; use seaweed_volume::server::volume_server::{ build_metrics_router, RuntimeMetricsConfig, VolumeServerState, }; @@ -39,6 +40,13 @@ fn main() { .init(); let config = config::parse_cli(); + let cpu_profile = match CpuProfileSession::start(&config) { + Ok(session) => session, + Err(e) => { + error!("{}", e); + std::process::exit(1); + } + }; info!( "SeaweedFS Volume Server (Rust) v{}", seaweed_volume::version::full_version() @@ -53,7 +61,7 @@ fn main() { .build() .expect("Failed to build tokio runtime"); - if let Err(e) = rt.block_on(run(config)) { + if let Err(e) = rt.block_on(run(config, cpu_profile)) { error!("Volume server failed: {}", e); std::process::exit(1); } @@ -245,7 +253,10 @@ where Box::pin(stream) } -async fn run(config: VolumeServerConfig) -> Result<(), Box> { +async fn run( + config: VolumeServerConfig, + cpu_profile: Option, +) -> Result<(), Box> { // Initialize the store let mut store = Store::new(config.index_type); store.id = config.id.clone(); @@ -714,6 +725,10 @@ async fn run(config: VolumeServerConfig) -> Result<(), Box, +} + +impl CpuProfileSession { + pub fn start(config: &VolumeServerConfig) -> Result, String> { + if config.cpu_profile.is_empty() { + if !config.mem_profile.is_empty() && !config.pprof { + tracing::warn!( + "--memprofile is not yet supported in the Rust volume server; ignoring '{}'", + config.mem_profile + ); + } + return Ok(None); + } + + if config.pprof { + tracing::info!( + "--pprof is enabled; ignoring --cpuprofile '{}' and --memprofile '{}'", + config.cpu_profile, + config.mem_profile + ); + return Ok(None); + } + + if !config.mem_profile.is_empty() { + tracing::warn!( + "--memprofile is not yet supported in the Rust volume server; only --cpuprofile '{}' will be written", + config.cpu_profile + ); + } + + let guard = pprof::ProfilerGuardBuilder::default() + .frequency(GO_CPU_PROFILE_FREQUENCY) + .blocklist(&GO_PPROF_BLOCKLIST) + .build() + .map_err(|e| { + format!( + "Failed to start CPU profiler '{}': {}", + config.cpu_profile, e + ) + })?; + + Ok(Some(Self { + output_path: PathBuf::from(&config.cpu_profile), + guard, + })) + } + + pub fn finish(self) -> Result<(), String> { + let report = self + .guard + .report() + .build() + .map_err(|e| format!("Failed to build CPU profile report: {}", e))?; + let profile = report + .pprof() + .map_err(|e| format!("Failed to encode CPU profile report: {}", e))?; + + let mut bytes = Vec::new(); + profile + .encode(&mut bytes) + .map_err(|e| format!("Failed to serialize CPU profile report: {}", e))?; + + let mut file = File::create(&self.output_path).map_err(|e| { + format!( + "Failed to create CPU profile '{}': {}", + self.output_path.display(), + e + ) + })?; + file.write_all(&bytes).map_err(|e| { + format!( + "Failed to write CPU profile '{}': {}", + self.output_path.display(), + e + ) + })?; + file.flush().map_err(|e| { + format!( + "Failed to flush CPU profile '{}': {}", + self.output_path.display(), + e + ) + })?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::CpuProfileSession; + use crate::config::{NeedleMapKind, ReadMode, VolumeServerConfig}; + use crate::security::tls::TlsPolicy; + + fn sample_config() -> VolumeServerConfig { + VolumeServerConfig { + port: 8080, + grpc_port: 18080, + public_port: 8080, + ip: "127.0.0.1".to_string(), + bind_ip: "127.0.0.1".to_string(), + public_url: "127.0.0.1:8080".to_string(), + id: "127.0.0.1:8080".to_string(), + masters: vec![], + pre_stop_seconds: 0, + idle_timeout: 0, + data_center: String::new(), + rack: String::new(), + index_type: NeedleMapKind::InMemory, + disk_type: String::new(), + folders: vec!["/tmp".to_string()], + folder_max_limits: vec![8], + folder_tags: vec![vec![]], + min_free_spaces: vec![], + disk_types: vec![String::new()], + idx_folder: String::new(), + white_list: vec![], + fix_jpg_orientation: false, + read_mode: ReadMode::Local, + cpu_profile: String::new(), + mem_profile: String::new(), + compaction_byte_per_second: 0, + maintenance_byte_per_second: 0, + file_size_limit_bytes: 0, + concurrent_upload_limit: 0, + concurrent_download_limit: 0, + inflight_upload_data_timeout: std::time::Duration::from_secs(0), + inflight_download_data_timeout: std::time::Duration::from_secs(0), + has_slow_read: false, + read_buffer_size_mb: 4, + ldb_timeout: 0, + pprof: false, + metrics_port: 0, + metrics_ip: String::new(), + debug: false, + debug_port: 0, + ui_enabled: false, + jwt_signing_key: vec![], + jwt_signing_expires_seconds: 0, + jwt_read_signing_key: vec![], + jwt_read_signing_expires_seconds: 0, + https_cert_file: String::new(), + https_key_file: String::new(), + https_ca_file: String::new(), + https_client_enabled: false, + https_client_cert_file: String::new(), + https_client_key_file: String::new(), + https_client_ca_file: String::new(), + grpc_cert_file: String::new(), + grpc_key_file: String::new(), + grpc_ca_file: String::new(), + grpc_allowed_wildcard_domain: String::new(), + grpc_volume_allowed_common_names: vec![], + tls_policy: TlsPolicy::default(), + enable_write_queue: false, + security_file: String::new(), + } + } + + #[test] + fn test_cpu_profile_session_skips_when_disabled() { + let config = sample_config(); + assert!(CpuProfileSession::start(&config).unwrap().is_none()); + } + + #[test] + fn test_cpu_profile_session_skips_when_pprof_enabled() { + let mut config = sample_config(); + config.cpu_profile = "/tmp/cpu.pb".to_string(); + config.pprof = true; + assert!(CpuProfileSession::start(&config).unwrap().is_none()); + } +}