1use std::sync::atomic::{AtomicUsize, Ordering};
2use std::sync::{Arc, LazyLock};
3use std::thread;
4use std::time::Instant;
5
6use crossbeam_channel::{unbounded, Receiver, Sender};
7
8use super::job::{InspectCategory, InspectJob, InspectResult};
9
10pub type InspectWorker = Arc<dyn Fn(InspectJob) -> InspectResult + Send + Sync + 'static>;
11
12#[derive(Clone)]
13pub struct DispatchHandles {
14 pub request_tx: Sender<InspectJob>,
15 pub result_rx: Receiver<InspectResult>,
16 pub pool: Arc<rayon::ThreadPool>,
17}
18
19static INSPECT_THREAD_COUNT: AtomicUsize = AtomicUsize::new(0);
22
23static INSPECT_POOL: LazyLock<Arc<rayon::ThreadPool>> = LazyLock::new(|| {
34 Arc::new(
35 rayon::ThreadPoolBuilder::new()
36 .num_threads(default_pool_size())
37 .thread_name(|index| format!("aft-inspect-{index}"))
38 .stack_size(8 * 1024 * 1024)
45 .start_handler(|_| {
46 INSPECT_THREAD_COUNT.fetch_add(1, Ordering::SeqCst);
47 })
48 .exit_handler(|_| {
49 INSPECT_THREAD_COUNT.fetch_sub(1, Ordering::SeqCst);
50 })
51 .build()
52 .expect("inspect worker pool must build"),
53 )
54});
55
56pub fn start_dispatch_loop(worker: InspectWorker) -> DispatchHandles {
57 let (request_tx, request_rx) = unbounded::<InspectJob>();
58 let (result_tx, result_rx) = unbounded::<InspectResult>();
59 let pool = inspect_pool();
60
61 let loop_pool = Arc::clone(&pool);
62 thread::spawn(move || dispatch_loop(request_rx, result_tx, loop_pool, worker));
63
64 DispatchHandles {
65 request_tx,
66 result_rx,
67 pool,
68 }
69}
70
71fn inspect_pool() -> Arc<rayon::ThreadPool> {
72 Arc::clone(&INSPECT_POOL)
73}
74
75#[doc(hidden)]
76pub fn inspect_pool_size_for_test() -> usize {
77 inspect_pool().current_num_threads()
78}
79
80#[doc(hidden)]
81pub fn inspect_pool_thread_count_for_test() -> usize {
82 INSPECT_THREAD_COUNT.load(Ordering::SeqCst)
83}
84
85pub fn default_worker() -> InspectWorker {
86 Arc::new(dispatch_category)
87}
88
89fn dispatch_loop(
90 request_rx: Receiver<InspectJob>,
91 result_tx: Sender<InspectResult>,
92 pool: Arc<rayon::ThreadPool>,
93 worker: InspectWorker,
94) {
95 while let Ok(job) = request_rx.recv() {
96 let tx = result_tx.clone();
97 let worker = Arc::clone(&worker);
98 pool.spawn_fifo(move || {
99 let result = worker(job);
100 let _ = tx.send(result);
101 });
102 }
103}
104
105fn dispatch_category(job: InspectJob) -> InspectResult {
106 use crate::inspect::scanners;
107
108 match job.category {
109 InspectCategory::Todos => scanners::todos::run_todos_scan(&job),
110 InspectCategory::Metrics => scanners::metrics::run_metrics_scan(&job),
111 InspectCategory::DeadCode => scanners::dead_code::run_dead_code_scan(&job),
112 InspectCategory::UnusedExports => scanners::unused_exports::run_unused_exports_scan(&job),
113 InspectCategory::Duplicates => scanners::duplicates::run_duplicates_scan(&job),
114 InspectCategory::Cycles => scanners::cycles::run_cycles_scan(&job),
115 InspectCategory::Diagnostics => {
116 let started = Instant::now();
122 InspectResult::failed(
123 &job,
124 "diagnostics must run on the main thread (run_diagnostics_category), \
125 not the rayon inspect worker pool",
126 started.elapsed(),
127 )
128 }
129 other => {
130 let started = Instant::now();
131 InspectResult::failed(
132 &job,
133 format!("inspect category '{other}' is not active in v0.33"),
134 started.elapsed(),
135 )
136 }
137 }
138}
139
140fn default_pool_size() -> usize {
141 resolve_pool_size(std::env::var("AFT_INSPECT_POOL_THREADS").ok().as_deref())
145}
146
147fn resolve_pool_size(override_value: Option<&str>) -> usize {
152 if let Some(threads) = override_value.and_then(|value| value.parse::<usize>().ok()) {
153 return threads.clamp(1, 512);
154 }
155
156 std::thread::available_parallelism()
157 .map(|parallelism| parallelism.get())
158 .unwrap_or(1)
159 .min(8)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::resolve_pool_size;
165
166 #[test]
167 fn pool_thread_override_wins_and_clamps() {
168 assert_eq!(resolve_pool_size(Some("17")), 17);
169 assert_eq!(resolve_pool_size(Some("0")), 1);
170 assert_eq!(resolve_pool_size(Some("100000")), 512);
171 }
172
173 #[test]
174 fn pool_thread_override_ignores_absent_and_unparseable_values() {
175 let derived = resolve_pool_size(None);
176
177 assert_eq!(resolve_pool_size(Some("wide")), derived);
178 assert_eq!(resolve_pool_size(Some("-4")), derived);
179 assert_eq!(resolve_pool_size(Some("")), derived);
180 }
181}