Summary
The debug-flag guards in the viscous solver's inner loops call std::env::var(...) on every iteration. On macOS (and glibc Linux under some conditions), getenv takes a process-global lock (an os_unfair_lock on Darwin), so when a batch polar sweep fans out across the rayon pool, the workers spend a large share of their time contending on that lock instead of solving. Observed from flexfoil 1.1.6 in a Python API process: a batch sweep saturating ~12 cores (~1240% CPU) delivered nowhere near 12x single-thread throughput; profiling the host process showed heavy contention in getenv under solve_body_oper_point.
Where
std::env::var("RUSTFOIL_*_DEBUG").is_ok() guards inside per-station / per-Newton-iteration code, e.g.:
crates/rustfoil-xfoil/src/march.rs:713 (RUSTFOIL_WAKE_MARCH_DEBUG), :932 / :1275 (RUSTFOIL_TRCHEK_DEBUG), :1110 (RUSTFOIL_WAKE_ITER_DEBUG), :1127 (RUSTFOIL_TAIL_ITER_DEBUG)
crates/rustfoil-xfoil/src/update.rs:120 (RUSTFOIL_UPDATE_DEBUG)
- similar guards in
solve.rs, oper.rs, assembly.rs, state_ops.rs, and rustfoil-solver/src/viscous/{viscal,forces}.rs
These run per BL station per Newton iteration per operating point - easily 10^5-10^6 getenv calls per polar, times N rayon workers hammering the same lock.
Suggested fix
Read each flag once into a std::sync::LazyLock<bool> (or OnceLock) at first use and branch on the cached bool:
static UPDATE_DEBUG: LazyLock<bool> =
LazyLock::new(|| std::env::var("RUSTFOIL_UPDATE_DEBUG").is_ok());
Debug flags don't need to be re-read mid-run; process-start semantics are fine (and are already the effective semantics for anyone using them). A small debug_flags module holding all the flags would keep it tidy.
Impact
This is why capping RAYON_NUM_THREADS barely hurts sweep throughput today - the extra workers were mostly spinning on the getenv lock. Fixing this should make batch sweeps actually scale with the pool size.
Summary
The debug-flag guards in the viscous solver's inner loops call
std::env::var(...)on every iteration. On macOS (and glibc Linux under some conditions),getenvtakes a process-global lock (an os_unfair_lock on Darwin), so when a batch polar sweep fans out across the rayon pool, the workers spend a large share of their time contending on that lock instead of solving. Observed fromflexfoil1.1.6 in a Python API process: a batch sweep saturating ~12 cores (~1240% CPU) delivered nowhere near 12x single-thread throughput; profiling the host process showed heavy contention ingetenvundersolve_body_oper_point.Where
std::env::var("RUSTFOIL_*_DEBUG").is_ok()guards inside per-station / per-Newton-iteration code, e.g.:crates/rustfoil-xfoil/src/march.rs:713(RUSTFOIL_WAKE_MARCH_DEBUG),:932/:1275(RUSTFOIL_TRCHEK_DEBUG),:1110(RUSTFOIL_WAKE_ITER_DEBUG),:1127(RUSTFOIL_TAIL_ITER_DEBUG)crates/rustfoil-xfoil/src/update.rs:120(RUSTFOIL_UPDATE_DEBUG)solve.rs,oper.rs,assembly.rs,state_ops.rs, andrustfoil-solver/src/viscous/{viscal,forces}.rsThese run per BL station per Newton iteration per operating point - easily 10^5-10^6
getenvcalls per polar, times N rayon workers hammering the same lock.Suggested fix
Read each flag once into a
std::sync::LazyLock<bool>(orOnceLock) at first use and branch on the cached bool:Debug flags don't need to be re-read mid-run; process-start semantics are fine (and are already the effective semantics for anyone using them). A small
debug_flagsmodule holding all the flags would keep it tidy.Impact
This is why capping
RAYON_NUM_THREADSbarely hurts sweep throughput today - the extra workers were mostly spinning on the getenv lock. Fixing this should make batch sweeps actually scale with the pool size.