From dd44bb4f7a2235b036fce5f35949dc78f50df39c Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:47:23 +0200 Subject: [PATCH] inspect: allow overriding the shared pool thread count The inspect pool is process-wide and its width derives from available_parallelism, so there is no way to reproduce a different thread regime on one machine. glibc allocates arenas per contending thread, so pool width changes how allocations interleave -- which is the variable under investigation in #205. AFT_INSPECT_POOL_THREADS overrides the derivation at pool init: parsed as usize, clamped to 1..=512, with anything absent or unparseable falling through to the existing derivation unchanged. Deliberately undocumented and not a config field -- a dev knob in the same class as AFT_STORM_SCALE and AFT_SEMANTIC_QUIET_WINDOW_MS, per the standing rule against low-value config surface. --- crates/aft/src/inspect/dispatch.rs | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/aft/src/inspect/dispatch.rs b/crates/aft/src/inspect/dispatch.rs index 6c5cf22b..66c1182e 100644 --- a/crates/aft/src/inspect/dispatch.rs +++ b/crates/aft/src/inspect/dispatch.rs @@ -138,8 +138,44 @@ fn dispatch_category(job: InspectJob) -> InspectResult { } fn default_pool_size() -> usize { + // Dev-only override for reproducing thread-regime-dependent behaviour + // (glibc allocates arenas per contending thread, so pool width changes + // fragmentation). Not a tuning surface and deliberately undocumented. + resolve_pool_size(std::env::var("AFT_INSPECT_POOL_THREADS").ok().as_deref()) +} + +/// Split from the env read so the parsing and clamping are testable without +/// mutating process-global state: `INSPECT_POOL` is a `LazyLock`, and any +/// concurrent test that builds an `InspectManager` would otherwise capture +/// whatever width the env happened to hold. +fn resolve_pool_size(override_value: Option<&str>) -> usize { + if let Some(threads) = override_value.and_then(|value| value.parse::().ok()) { + return threads.clamp(1, 512); + } + std::thread::available_parallelism() .map(|parallelism| parallelism.get()) .unwrap_or(1) .min(8) } + +#[cfg(test)] +mod tests { + use super::resolve_pool_size; + + #[test] + fn pool_thread_override_wins_and_clamps() { + assert_eq!(resolve_pool_size(Some("17")), 17); + assert_eq!(resolve_pool_size(Some("0")), 1); + assert_eq!(resolve_pool_size(Some("100000")), 512); + } + + #[test] + fn pool_thread_override_ignores_absent_and_unparseable_values() { + let derived = resolve_pool_size(None); + + assert_eq!(resolve_pool_size(Some("wide")), derived); + assert_eq!(resolve_pool_size(Some("-4")), derived); + assert_eq!(resolve_pool_size(Some("")), derived); + } +}