diff --git a/src/bin/ui.rs b/src/bin/ui.rs index c5f9ed63..01d85a65 100644 --- a/src/bin/ui.rs +++ b/src/bin/ui.rs @@ -1899,8 +1899,7 @@ impl App { let custom_label = ui.add_sized( [0.0, 0.0], egui::Label::new( - egui::RichText::new("Custom SNI") - .color(egui::Color32::TRANSPARENT), + egui::RichText::new("Custom SNI").color(egui::Color32::TRANSPARENT), ), ); ui.add( @@ -1966,8 +1965,38 @@ fn fmt_bytes(b: u64) -> String { // ---------- Background thread: owns the tokio runtime + proxy lifecycle ---------- +const DESKTOP_RUNTIME_MIN_WORKERS: usize = 2; +const DESKTOP_RUNTIME_MAX_WORKERS: usize = 4; +const DESKTOP_RUNTIME_MAX_BLOCKING_THREADS: usize = 32; + +fn desktop_runtime_worker_count(available_parallelism: usize) -> usize { + available_parallelism.clamp(DESKTOP_RUNTIME_MIN_WORKERS, DESKTOP_RUNTIME_MAX_WORKERS) +} + +fn build_desktop_runtime() -> std::io::Result<(Runtime, usize)> { + let available = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(DESKTOP_RUNTIME_MIN_WORKERS); + let workers = desktop_runtime_worker_count(available); + let runtime = tokio::runtime::Builder::new_multi_thread() + .thread_name("mhrv-ui-worker") + .worker_threads(workers) + .max_blocking_threads(DESKTOP_RUNTIME_MAX_BLOCKING_THREADS) + .thread_keep_alive(Duration::from_secs(30)) + .enable_all() + .build()?; + Ok((runtime, workers)) +} + fn background_thread(shared: Arc, rx: Receiver) { - let rt = Runtime::new().expect("failed to create tokio runtime"); + let (rt, runtime_workers) = build_desktop_runtime().expect("failed to create tokio runtime"); + push_log( + &shared, + &format!( + "[ui] tokio runtime ready: {} worker threads, {} max blocking threads", + runtime_workers, DESKTOP_RUNTIME_MAX_BLOCKING_THREADS + ), + ); let mut active: Option<( JoinHandle<()>, @@ -2113,14 +2142,14 @@ fn background_thread(shared: Arc, rx: Receiver) { https://whatismyipaddress.com in your browser \ via 127.0.0.1:8085. The IP shown should be your \ tunnel-node's VPS IP. Tracking a real Full-mode \ - test in #160." + test in #160.", ), Some(mhrv_rs::config::Mode::Direct) => Some( "Test Relay is wired only for apps_script mode. \ In direct mode there is no Apps Script relay — \ every request goes through the SNI-rewrite tunnel \ straight to Google's edge. Verify by loading \ - https://www.google.com via the proxy." + https://www.google.com via the proxy.", ), _ => None, }; @@ -2492,10 +2521,7 @@ fn install_ui_tracing(shared: Arc, config_level: &str) { /// by `install_ui_tracing`. `apply_log_level` uses it to swap in a new /// filter when the user clicks Save with a different log level (#401). static LOG_RELOAD: std::sync::OnceLock< - tracing_subscriber::reload::Handle< - tracing_subscriber::EnvFilter, - tracing_subscriber::Registry, - >, + tracing_subscriber::reload::Handle, > = std::sync::OnceLock::new(); /// Reinstall the tracing filter at runtime. Called from the Save handler @@ -2568,3 +2594,27 @@ fn push_log(shared: &Shared, msg: &str) { s.log.pop_front(); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn desktop_runtime_worker_count_clamps_small_devices_to_two_workers() { + assert_eq!(desktop_runtime_worker_count(0), 2); + assert_eq!(desktop_runtime_worker_count(1), 2); + assert_eq!(desktop_runtime_worker_count(2), 2); + } + + #[test] + fn desktop_runtime_worker_count_uses_midrange_core_counts_directly() { + assert_eq!(desktop_runtime_worker_count(3), 3); + assert_eq!(desktop_runtime_worker_count(4), 4); + } + + #[test] + fn desktop_runtime_worker_count_caps_large_desktops_at_four_workers() { + assert_eq!(desktop_runtime_worker_count(8), 4); + assert_eq!(desktop_runtime_worker_count(32), 4); + } +}