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