Skip to content

Commit ea8a508

Browse files
committed
fix(ui): bound desktop runtime worker allocation
Replace the desktop UI background runtime's implicit Tokio defaults with an explicit multi-thread runtime builder. The UI now sizes its async worker pool from available_parallelism(), clamps long-lived worker threads to a small 2..=4 range, names worker threads as mhrv-ui-worker, and bounds the blocking pool separately at 32 threads. This keeps proxy lifecycle tasks, stats polling, certificate operations, tests, scans, and update/download work on the same command paths while making the scheduler shape predictable on both low-core machines and high-core desktops. Small devices avoid a single-worker UI runtime, and large desktops avoid creating an oversized worker pool for a UI-owned background coordinator. Log the selected runtime shape at UI startup so support reports can distinguish runtime sizing from proxy transport behavior. Add focused unit coverage for the worker-count policy across low-core, midrange, and high-core inputs.
1 parent 40b5386 commit ea8a508

1 file changed

Lines changed: 59 additions & 9 deletions

File tree

src/bin/ui.rs

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1899,8 +1899,7 @@ impl App {
18991899
let custom_label = ui.add_sized(
19001900
[0.0, 0.0],
19011901
egui::Label::new(
1902-
egui::RichText::new("Custom SNI")
1903-
.color(egui::Color32::TRANSPARENT),
1902+
egui::RichText::new("Custom SNI").color(egui::Color32::TRANSPARENT),
19041903
),
19051904
);
19061905
ui.add(
@@ -1966,8 +1965,38 @@ fn fmt_bytes(b: u64) -> String {
19661965

19671966
// ---------- Background thread: owns the tokio runtime + proxy lifecycle ----------
19681967

1968+
const DESKTOP_RUNTIME_MIN_WORKERS: usize = 2;
1969+
const DESKTOP_RUNTIME_MAX_WORKERS: usize = 4;
1970+
const DESKTOP_RUNTIME_MAX_BLOCKING_THREADS: usize = 32;
1971+
1972+
fn desktop_runtime_worker_count(available_parallelism: usize) -> usize {
1973+
available_parallelism.clamp(DESKTOP_RUNTIME_MIN_WORKERS, DESKTOP_RUNTIME_MAX_WORKERS)
1974+
}
1975+
1976+
fn build_desktop_runtime() -> std::io::Result<(Runtime, usize)> {
1977+
let available = std::thread::available_parallelism()
1978+
.map(|n| n.get())
1979+
.unwrap_or(DESKTOP_RUNTIME_MIN_WORKERS);
1980+
let workers = desktop_runtime_worker_count(available);
1981+
let runtime = tokio::runtime::Builder::new_multi_thread()
1982+
.thread_name("mhrv-ui-worker")
1983+
.worker_threads(workers)
1984+
.max_blocking_threads(DESKTOP_RUNTIME_MAX_BLOCKING_THREADS)
1985+
.thread_keep_alive(Duration::from_secs(30))
1986+
.enable_all()
1987+
.build()?;
1988+
Ok((runtime, workers))
1989+
}
1990+
19691991
fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
1970-
let rt = Runtime::new().expect("failed to create tokio runtime");
1992+
let (rt, runtime_workers) = build_desktop_runtime().expect("failed to create tokio runtime");
1993+
push_log(
1994+
&shared,
1995+
&format!(
1996+
"[ui] tokio runtime ready: {} worker threads, {} max blocking threads",
1997+
runtime_workers, DESKTOP_RUNTIME_MAX_BLOCKING_THREADS
1998+
),
1999+
);
19712000

19722001
let mut active: Option<(
19732002
JoinHandle<()>,
@@ -2113,14 +2142,14 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
21132142
https://whatismyipaddress.com in your browser \
21142143
via 127.0.0.1:8085. The IP shown should be your \
21152144
tunnel-node's VPS IP. Tracking a real Full-mode \
2116-
test in #160."
2145+
test in #160.",
21172146
),
21182147
Some(mhrv_rs::config::Mode::Direct) => Some(
21192148
"Test Relay is wired only for apps_script mode. \
21202149
In direct mode there is no Apps Script relay — \
21212150
every request goes through the SNI-rewrite tunnel \
21222151
straight to Google's edge. Verify by loading \
2123-
https://www.google.com via the proxy."
2152+
https://www.google.com via the proxy.",
21242153
),
21252154
_ => None,
21262155
};
@@ -2492,10 +2521,7 @@ fn install_ui_tracing(shared: Arc<Shared>, config_level: &str) {
24922521
/// by `install_ui_tracing`. `apply_log_level` uses it to swap in a new
24932522
/// filter when the user clicks Save with a different log level (#401).
24942523
static LOG_RELOAD: std::sync::OnceLock<
2495-
tracing_subscriber::reload::Handle<
2496-
tracing_subscriber::EnvFilter,
2497-
tracing_subscriber::Registry,
2498-
>,
2524+
tracing_subscriber::reload::Handle<tracing_subscriber::EnvFilter, tracing_subscriber::Registry>,
24992525
> = std::sync::OnceLock::new();
25002526

25012527
/// Reinstall the tracing filter at runtime. Called from the Save handler
@@ -2568,3 +2594,27 @@ fn push_log(shared: &Shared, msg: &str) {
25682594
s.log.pop_front();
25692595
}
25702596
}
2597+
2598+
#[cfg(test)]
2599+
mod tests {
2600+
use super::*;
2601+
2602+
#[test]
2603+
fn desktop_runtime_worker_count_clamps_small_devices_to_two_workers() {
2604+
assert_eq!(desktop_runtime_worker_count(0), 2);
2605+
assert_eq!(desktop_runtime_worker_count(1), 2);
2606+
assert_eq!(desktop_runtime_worker_count(2), 2);
2607+
}
2608+
2609+
#[test]
2610+
fn desktop_runtime_worker_count_uses_midrange_core_counts_directly() {
2611+
assert_eq!(desktop_runtime_worker_count(3), 3);
2612+
assert_eq!(desktop_runtime_worker_count(4), 4);
2613+
}
2614+
2615+
#[test]
2616+
fn desktop_runtime_worker_count_caps_large_desktops_at_four_workers() {
2617+
assert_eq!(desktop_runtime_worker_count(8), 4);
2618+
assert_eq!(desktop_runtime_worker_count(32), 4);
2619+
}
2620+
}

0 commit comments

Comments
 (0)