Skip to content

Commit 9f924f6

Browse files
committed
refactor(linter): always explicitly initialize Rayon thread pool (#13122)
Previously we only explicitly initialized the global Rayon thread pool if `--threads` option is specified. Instead, do it unconditionally, and always specify thread count. The purpose is to obtain a guarantee that `rayon::current_num_threads()` will always return the same number during the rest of the linter's work. This invariant is not relied upon currently, but will become critical for soundness once we run JS plugins on multiple threads. Also, if `std::thread::available_parallelism()` is not able to obtain CPU core count, log a warning that the linter will run single-threaded. Previously Rayon would use only a single thread, but would make that decision silently. I'm not sure in what circumstances `available_parallelism()` can return `Err`, but if it does, the user will probably want to know about it.
1 parent 83d25ce commit 9f924f6

File tree

2 files changed

+57
-5
lines changed

2 files changed

+57
-5
lines changed

apps/oxlint/src/command/lint.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,45 @@ pub struct LintCommand {
6060

6161
impl LintCommand {
6262
pub fn handle_threads(&self) {
63-
Self::set_rayon_threads(self.misc_options.threads);
63+
Self::init_rayon_thread_pool(self.misc_options.threads);
6464
}
6565

66-
fn set_rayon_threads(threads: Option<usize>) {
67-
if let Some(threads) = threads {
68-
rayon::ThreadPoolBuilder::new().num_threads(threads).build_global().unwrap();
69-
}
66+
/// Initialize Rayon global thread pool with specified number of threads.
67+
///
68+
/// If `--threads` option is not used, or `--threads 0` is given,
69+
/// default to the number of available CPU cores.
70+
#[expect(clippy::print_stderr)]
71+
fn init_rayon_thread_pool(threads: Option<usize>) {
72+
// Always initialize thread pool, even if using default thread count,
73+
// to ensure thread pool's thread count is locked after this point.
74+
// `rayon::current_num_threads()` will always return the same number after this point.
75+
//
76+
// If you don't initialize the global thread pool explicitly, or don't specify `num_threads`,
77+
// Rayon will initialize the thread pool when it's first used, with a thread count of
78+
// `std::thread::available_parallelism()`, and that thread count won't change thereafter.
79+
// So we don't *need* to initialize the thread pool here if we just want the default thread count.
80+
//
81+
// However, Rayon's docs state that:
82+
// > In the future, the default behavior may change to dynamically add or remove threads as needed.
83+
// https://docs.rs/rayon/1.11.0/rayon/struct.ThreadPoolBuilder.html#method.num_threads
84+
//
85+
// To ensure we continue to have a "locked" thread count, even after future Rayon upgrades,
86+
// we always initialize the thread pool and explicitly specify thread count here.
87+
88+
let thread_count = if let Some(thread_count) = threads
89+
&& thread_count > 0
90+
{
91+
thread_count
92+
} else if let Ok(thread_count) = std::thread::available_parallelism() {
93+
thread_count.get()
94+
} else {
95+
eprintln!(
96+
"Unable to determine available thread count. Defaulting to 1.\nConsider specifying the number of threads explicitly with `--threads` option."
97+
);
98+
1
99+
};
100+
101+
rayon::ThreadPoolBuilder::new().num_threads(thread_count).build_global().unwrap();
70102
}
71103
}
72104

crates/oxc_linter/src/service/runtime.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,26 @@ use message_cloner::MessageCloner;
241241

242242
impl Runtime {
243243
pub(super) fn new(linter: Linter, options: LintServiceOptions) -> Self {
244+
// If global thread pool wasn't already initialized, do it now.
245+
// This "locks" config for the thread pool, which ensures `rayon::current_num_threads()`
246+
// cannot change from now on.
247+
//
248+
// Initializing the thread pool without specifying `num_threads` produces a threadpool size
249+
// based on `std::thread::available_parallelism`. However, Rayon's docs state that:
250+
// > In the future, the default behavior may change to dynamically add or remove threads as needed.
251+
// https://docs.rs/rayon/1.11.0/rayon/struct.ThreadPoolBuilder.html#method.num_threads
252+
//
253+
// However, I (@overlookmotel) assume that would be considered a breaking change,
254+
// so we don't have to worry about it until Rayon v2.
255+
// When Rayon v2 is released and we upgrade to it, we'll need to revisit this and make sure
256+
// we still guarantee that thread count is locked.
257+
//
258+
// If thread pool was already initialized, this won't do anything.
259+
// `build_global` will return `Err` in that case, but we can ignore it.
260+
// That just means the config (and so number of threads) is already locked.
261+
// https://docs.rs/rayon/1.11.0/rayon/struct.ThreadPoolBuilder.html#method.build_global
262+
let _ = rayon::ThreadPoolBuilder::new().build_global();
263+
244264
let thread_count = rayon::current_num_threads();
245265
let allocator_pool = AllocatorPool::new(thread_count);
246266

0 commit comments

Comments
 (0)