From dfe54da962dad156d0f78285888c44987d295f74 Mon Sep 17 00:00:00 2001 From: TickTockBent Date: Wed, 18 Mar 2026 07:41:25 -0400 Subject: [PATCH] test: add unit tests for blocking pool filter, fix doc inconsistencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 9 unit tests for `is_blocking_pool_stack()` covering the blocking pool detection logic from issue #3. This function went through multiple release iterations (v0.4.2–v0.5.0) but had zero test coverage. Fix TROUBLESHOOTING.md listing 3 worker discovery steps when the actual implementation and ARCHITECTURE.md document 4. Fix README.md stating "x86_64 architecture" when ARCHITECTURE.md and DEVELOPMENT.md both document x86_64/aarch64 support. // ticktockbent --- README.md | 2 +- docs/TROUBLESHOOTING.md | 9 +- hud/src/profiling/event_processor.rs | 119 +++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e084f4d..274977b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Use hud to narrow down suspects, then dig deeper with instrumentation if needed. **System:** - Linux 5.8+ -- x86_64 architecture +- x86_64 or aarch64 architecture - Root privileges **Your application needs debug symbols** (so hud can show function names): diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index a49453c..072a301 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -65,11 +65,12 @@ Default Tokio threads are named `tokio-runtime-w` (Tokio ≤ 1.x) or `tokio-rt-w **Step 2: Understand the discovery chain** hud tries four methods in order: -1. Default prefixes `tokio-runtime-w` / `tokio-rt-worker` -2. Stack-based classification (samples stack traces for 500ms, looks for Tokio scheduler frames) -3. Largest thread group heuristic (`{name}-{N}` patterns) +1. **Explicit prefix** (`--workers `): match threads whose comm starts with the given prefix. No fallback. +2. **Default prefixes**: try `tokio-runtime-w` (Tokio ≤ 1.x) and `tokio-rt-worker` (Tokio 1.44+). +3. **Stack-based classification**: samples stack traces for 500ms, looks for Tokio scheduler frames. Catches custom-named runtimes automatically. +4. **Largest thread group heuristic**: picks the biggest group of threads sharing a `{name}-{N}` pattern. -If all three fail, you see `workers: 0`. Stack-based discovery handles most custom thread names automatically, but requires the target process to be actively running Tokio work during the 500ms sampling window. +If all four fail, you see `workers: 0`. Stack-based discovery handles most custom thread names automatically, but requires the target process to be actively running Tokio work during the 500ms sampling window. **Step 3: Override with --workers** ```bash diff --git a/hud/src/profiling/event_processor.rs b/hud/src/profiling/event_processor.rs index a719d61..41baa61 100644 --- a/hud/src/profiling/event_processor.rs +++ b/hud/src/profiling/event_processor.rs @@ -429,3 +429,122 @@ fn is_blocking_pool_stack(call_stack: &[StackFrame]) -> bool { // Genuine blocking pool: has Inner::run but NOT the worker scheduler has_blocking_pool && !has_worker_scheduler } + +#[cfg(test)] +mod tests { + use super::*; + use crate::classification::FrameOrigin; + + /// Build a `StackFrame` with just a function name. File, line, and origin + /// are irrelevant to `is_blocking_pool_stack` — it only inspects + /// `function`. + fn frame(function: &str) -> StackFrame { + StackFrame { + function: function.to_string(), + file: None, + line: None, + origin: FrameOrigin::Unknown, + is_user_code: false, + } + } + + // ── is_blocking_pool_stack unit tests ───────────────────────────── + + #[test] + fn blocking_pool_stack_is_detected() { + // A genuine spawn_blocking stack: Inner::run is present, but the + // multi-thread worker scheduler frame is NOT. + let stack = vec![ + frame("std::thread::sleep"), + frame("myapp::do_blocking_work"), + frame("tokio::runtime::blocking::pool::Inner::run"), + ]; + assert!(is_blocking_pool_stack(&stack)); + } + + #[test] + fn worker_thread_stack_is_not_filtered() { + // A genuine async worker thread: both Inner::run AND the + // multi-thread scheduler worker frame are present. This must NOT + // be classified as blocking pool. + let stack = vec![ + frame("myapp::handler::process_request"), + frame("tokio::runtime::scheduler::multi_thread::worker::Context::run"), + frame("tokio::runtime::blocking::pool::Inner::run"), + ]; + assert!(!is_blocking_pool_stack(&stack)); + } + + #[test] + fn unrelated_stack_is_not_filtered() { + // A stack with neither marker frame. User code, standard library, + // whatever — should not match. + let stack = vec![frame("myapp::main"), frame("std::io::Read::read")]; + assert!(!is_blocking_pool_stack(&stack)); + } + + #[test] + fn empty_stack_is_not_filtered() { + assert!(!is_blocking_pool_stack(&[])); + } + + #[test] + fn worker_scheduler_alone_is_not_filtered() { + // Has the worker scheduler frame but NOT Inner::run. + // Unusual in practice, but must not match. + let stack = vec![ + frame("myapp::handler"), + frame("tokio::runtime::scheduler::multi_thread::worker::Context::run"), + ]; + assert!(!is_blocking_pool_stack(&stack)); + } + + #[test] + fn inner_run_prefix_variants_match() { + // The filter uses starts_with, so any suffix of Inner::run should + // match (e.g. closure wrappers, monomorphized generics). + let stack = vec![frame("tokio::runtime::blocking::pool::Inner::run::{{closure}}")]; + assert!(is_blocking_pool_stack(&stack)); + } + + #[test] + fn worker_scheduler_substring_match() { + // The filter uses contains(), so the worker scheduler frame can + // appear anywhere in the function name (different Tokio versions + // may wrap it differently). + let stack = vec![ + frame("tokio::runtime::blocking::pool::Inner::run"), + frame("tokio::runtime::scheduler::multi_thread::worker::run"), + ]; + assert!(!is_blocking_pool_stack(&stack)); + } + + #[test] + fn deep_blocking_pool_stack() { + // Realistic deep stack from a spawn_blocking task doing sync I/O. + let stack = vec![ + frame("std::sys::pal::unix::fs::File::read"), + frame("std::fs::File::read"), + frame("myapp::data::load_config"), + frame("tokio::runtime::blocking::task::BlockingTask::poll"), + frame("tokio::runtime::blocking::pool::Inner::run"), + frame("std::thread::Builder::spawn::{{closure}}"), + ]; + assert!(is_blocking_pool_stack(&stack)); + } + + #[test] + fn deep_worker_stack_not_filtered() { + // Realistic deep stack from a Tokio worker running async code that + // happens to block. Both marker frames present. + let stack = vec![ + frame("bcrypt::bcrypt::hash_with_result"), + frame("myapp::handler::hash_password"), + frame("hyper::proto::h1::dispatch::Dispatcher::poll_inner"), + frame("tokio::runtime::scheduler::multi_thread::worker::Context::run_task"), + frame("tokio::runtime::scheduler::multi_thread::worker::Context::run"), + frame("tokio::runtime::blocking::pool::Inner::run"), + ]; + assert!(!is_blocking_pool_stack(&stack)); + } +}