1use crate::OxirsError;
44use crossbeam_deque::{Injector, Stealer, Worker};
45use scirs2_core::metrics::{Counter, Timer};
46use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
47use std::sync::Arc;
48use std::thread::{self, JoinHandle};
49use std::time::Duration;
50
51pub type Result<T> = std::result::Result<T, OxirsError>;
53
54pub struct ThreadPerCore {
56 workers: Vec<CoreWorker>,
58 global_queue: Arc<Injector<Task>>,
60 running: Arc<AtomicBool>,
62 config: ThreadPerCoreConfig,
64 submitted_counter: Counter,
66 #[allow(dead_code)]
67 completed_counter: Counter,
68 #[allow(dead_code)]
69 stolen_counter: Counter,
70 #[allow(dead_code)]
71 execution_timer: Timer,
72}
73
74#[derive(Debug, Clone)]
76pub struct ThreadPerCoreConfig {
77 pub num_workers: usize,
79 pub enable_affinity: bool,
81 pub queue_capacity: usize,
83 pub enable_work_stealing: bool,
85 pub steal_batch_size: usize,
87}
88
89impl Default for ThreadPerCoreConfig {
90 fn default() -> Self {
91 Self {
92 num_workers: std::thread::available_parallelism()
93 .map(|n| n.get())
94 .unwrap_or(1),
95 enable_affinity: true,
96 queue_capacity: 1024,
97 enable_work_stealing: true,
98 steal_batch_size: 16,
99 }
100 }
101}
102
103pub struct Task {
105 func: Box<dyn FnOnce() + Send + 'static>,
107 id: usize,
109}
110
111impl Task {
112 pub fn new<F>(f: F) -> Self
114 where
115 F: FnOnce() + Send + 'static,
116 {
117 static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
118 Self {
119 func: Box::new(f),
120 id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
121 }
122 }
123
124 fn execute(self) {
126 (self.func)();
127 }
128
129 pub fn id(&self) -> usize {
131 self.id
132 }
133}
134
135struct CoreWorker {
137 #[allow(dead_code)]
139 id: usize,
140 handle: Option<JoinHandle<()>>,
142 local_queue: Worker<Task>,
144 #[allow(dead_code)]
146 stealer: Stealer<Task>,
147 stats: Arc<WorkerStats>,
149}
150
151#[derive(Default)]
153struct WorkerStats {
154 executed: AtomicUsize,
156 #[allow(dead_code)]
158 stolen_from: AtomicUsize,
159 stolen_by: AtomicUsize,
161 idle_time_us: AtomicUsize,
163}
164
165impl ThreadPerCore {
166 pub fn new() -> Result<Self> {
168 Self::with_config(ThreadPerCoreConfig::default())
169 }
170
171 pub fn with_config(config: ThreadPerCoreConfig) -> Result<Self> {
173 tracing::info!(
174 "Initializing thread-per-core executor with {} workers",
175 config.num_workers
176 );
177
178 let global_queue = Arc::new(Injector::new());
179 let running = Arc::new(AtomicBool::new(true));
180
181 let mut workers = Vec::with_capacity(config.num_workers);
183 let mut stealers = Vec::new();
184 let mut worker_stats = Vec::new();
185
186 for worker_id in 0..config.num_workers {
188 let local_queue = Worker::new_fifo();
189 let stealer = local_queue.stealer();
190 stealers.push(stealer.clone());
191
192 let stats = Arc::new(WorkerStats::default());
193 worker_stats.push(stats.clone());
194
195 let worker = CoreWorker {
196 id: worker_id,
197 handle: None,
198 local_queue,
199 stealer,
200 stats,
201 };
202
203 workers.push(worker);
204 }
205
206 let stealers_arc = Arc::new(stealers);
208
209 for (worker_id, worker) in workers.iter_mut().enumerate() {
210 let local_queue = std::mem::replace(&mut worker.local_queue, Worker::new_fifo());
212 let global_queue = global_queue.clone();
213 let running = running.clone();
214 let stealers = stealers_arc.clone();
215 let stats = worker_stats[worker_id].clone();
216 let enable_affinity = config.enable_affinity;
217 let enable_work_stealing = config.enable_work_stealing;
218
219 let handle = thread::Builder::new()
220 .name(format!("rdf-worker-{}", worker_id))
221 .spawn(move || {
222 Self::worker_loop(
223 worker_id,
224 local_queue,
225 global_queue,
226 stealers,
227 running,
228 stats,
229 enable_affinity,
230 enable_work_stealing,
231 )
232 })
233 .map_err(|e| {
234 OxirsError::ConcurrencyError(format!("Failed to spawn worker: {}", e))
235 })?;
236
237 worker.handle = Some(handle);
238 }
239
240 Ok(Self {
241 workers,
242 global_queue,
243 running,
244 config,
245 submitted_counter: Counter::new("threadpool.submitted".to_string()),
246 completed_counter: Counter::new("threadpool.completed".to_string()),
247 stolen_counter: Counter::new("threadpool.stolen".to_string()),
248 execution_timer: Timer::new("threadpool.execution".to_string()),
249 })
250 }
251
252 pub fn submit(&self, task: Task) -> Result<()> {
254 if !self.running.load(Ordering::Relaxed) {
255 return Err(OxirsError::ConcurrencyError(
256 "Thread pool is shutting down".to_string(),
257 ));
258 }
259
260 self.global_queue.push(task);
262 self.submitted_counter.add(1);
263
264 Ok(())
265 }
266
267 pub fn submit_batch(&self, tasks: Vec<Task>) -> Result<()> {
269 if !self.running.load(Ordering::Relaxed) {
270 return Err(OxirsError::ConcurrencyError(
271 "Thread pool is shutting down".to_string(),
272 ));
273 }
274
275 for task in tasks {
276 self.global_queue.push(task);
277 }
278
279 self.submitted_counter.add(1);
280
281 Ok(())
282 }
283
284 pub fn stats(&self) -> ThreadPerCoreStats {
286 let total_executed: usize = self
287 .workers
288 .iter()
289 .map(|w| w.stats.executed.load(Ordering::Relaxed))
290 .sum();
291
292 let total_stolen: usize = self
293 .workers
294 .iter()
295 .map(|w| w.stats.stolen_by.load(Ordering::Relaxed))
296 .sum();
297
298 let total_idle_us: usize = self
299 .workers
300 .iter()
301 .map(|w| w.stats.idle_time_us.load(Ordering::Relaxed))
302 .sum();
303
304 ThreadPerCoreStats {
305 num_workers: self.config.num_workers,
306 submitted: self.submitted_counter.get(),
307 completed: total_executed as u64,
308 stolen: total_stolen as u64,
309 avg_idle_time_us: total_idle_us as f64 / self.config.num_workers as f64,
310 }
311 }
312
313 #[allow(clippy::too_many_arguments)]
315 fn worker_loop(
316 worker_id: usize,
317 local_queue: Worker<Task>,
318 global_queue: Arc<Injector<Task>>,
319 stealers: Arc<Vec<Stealer<Task>>>,
320 running: Arc<AtomicBool>,
321 stats: Arc<WorkerStats>,
322 enable_affinity: bool,
323 enable_work_stealing: bool,
324 ) {
325 if enable_affinity {
327 if let Err(e) = Self::set_cpu_affinity(worker_id) {
328 tracing::warn!("Failed to set CPU affinity for worker {}: {}", worker_id, e);
329 } else {
330 tracing::debug!("Worker {} pinned to core {}", worker_id, worker_id);
331 }
332 }
333
334 while running.load(Ordering::Relaxed) {
335 if let Some(task) = local_queue.pop() {
337 task.execute();
338 stats.executed.fetch_add(1, Ordering::Relaxed);
339 continue;
340 }
341
342 match global_queue.steal() {
344 crossbeam_deque::Steal::Success(task) => {
345 task.execute();
346 stats.executed.fetch_add(1, Ordering::Relaxed);
347 continue;
348 }
349 crossbeam_deque::Steal::Empty => {}
350 crossbeam_deque::Steal::Retry => continue,
351 }
352
353 if enable_work_stealing {
355 let mut found = false;
356 for (i, stealer) in stealers.iter().enumerate() {
357 if i == worker_id {
358 continue; }
360
361 match stealer.steal() {
362 crossbeam_deque::Steal::Success(task) => {
363 task.execute();
364 stats.executed.fetch_add(1, Ordering::Relaxed);
365 stats.stolen_by.fetch_add(1, Ordering::Relaxed);
366 found = true;
367 break;
368 }
369 crossbeam_deque::Steal::Empty => {}
370 crossbeam_deque::Steal::Retry => continue,
371 }
372 }
373
374 if found {
375 continue;
376 }
377 }
378
379 let idle_start = std::time::Instant::now();
381 thread::sleep(Duration::from_micros(10));
382 let idle_us = idle_start.elapsed().as_micros() as usize;
383 stats.idle_time_us.fetch_add(idle_us, Ordering::Relaxed);
384 }
385
386 tracing::info!("Worker {} shutting down", worker_id);
387 }
388
389 #[cfg(target_os = "linux")]
391 fn set_cpu_affinity(core_id: usize) -> Result<()> {
392 use std::mem;
393
394 unsafe {
396 let mut cpu_set: libc::cpu_set_t = mem::zeroed();
397 libc::CPU_SET(core_id, &mut cpu_set);
398
399 if libc::sched_setaffinity(0, mem::size_of::<libc::cpu_set_t>(), &cpu_set) != 0 {
400 return Err(OxirsError::ConcurrencyError(format!(
401 "Failed to set CPU affinity: {}",
402 std::io::Error::last_os_error()
403 )));
404 }
405 }
406
407 Ok(())
408 }
409
410 #[cfg(not(target_os = "linux"))]
412 fn set_cpu_affinity(_core_id: usize) -> Result<()> {
413 Ok(())
415 }
416
417 pub fn shutdown(self) -> Result<()> {
419 tracing::info!("Shutting down thread-per-core executor");
420
421 self.running.store(false, Ordering::Relaxed);
423
424 for mut worker in self.workers {
426 if let Some(handle) = worker.handle.take() {
427 handle.join().map_err(|_| {
428 OxirsError::ConcurrencyError("Worker thread panicked".to_string())
429 })?;
430 }
431 }
432
433 tracing::info!("Thread-per-core executor shut down successfully");
434 Ok(())
435 }
436}
437
438impl Default for ThreadPerCore {
439 fn default() -> Self {
440 Self::new().expect("Failed to create ThreadPerCore executor")
441 }
442}
443
444#[derive(Debug, Clone)]
446pub struct ThreadPerCoreStats {
447 pub num_workers: usize,
449 pub submitted: u64,
451 pub completed: u64,
453 pub stolen: u64,
455 pub avg_idle_time_us: f64,
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use std::sync::atomic::AtomicUsize;
463 use std::sync::Arc;
464
465 #[test]
466 fn test_thread_per_core_creation() -> Result<()> {
467 let config = ThreadPerCoreConfig {
468 num_workers: 4,
469 ..Default::default()
470 };
471
472 let executor = ThreadPerCore::with_config(config)?;
473 executor.shutdown()?;
474
475 Ok(())
476 }
477
478 #[test]
479 fn test_task_submission() -> Result<()> {
480 let executor = ThreadPerCore::new()?;
481
482 let counter = Arc::new(AtomicUsize::new(0));
483 let counter_clone = counter.clone();
484
485 let task = Task::new(move || {
486 counter_clone.fetch_add(1, Ordering::Relaxed);
487 });
488
489 executor.submit(task)?;
490
491 thread::sleep(Duration::from_millis(100));
493
494 assert_eq!(counter.load(Ordering::Relaxed), 1);
495
496 executor.shutdown()?;
497 Ok(())
498 }
499
500 #[test]
501 fn test_batch_submission() -> Result<()> {
502 let executor = ThreadPerCore::new()?;
503
504 let counter = Arc::new(AtomicUsize::new(0));
505
506 let tasks: Vec<_> = (0..100)
507 .map(|_| {
508 let counter = counter.clone();
509 Task::new(move || {
510 counter.fetch_add(1, Ordering::Relaxed);
511 })
512 })
513 .collect();
514
515 executor.submit_batch(tasks)?;
516
517 thread::sleep(Duration::from_millis(500));
519
520 assert_eq!(counter.load(Ordering::Relaxed), 100);
521
522 executor.shutdown()?;
523 Ok(())
524 }
525
526 #[test]
527 fn test_stats() -> Result<()> {
528 let executor = ThreadPerCore::new()?;
529
530 for _ in 0..10 {
532 let task = Task::new(|| {
533 thread::sleep(Duration::from_millis(1));
534 });
535 executor.submit(task)?;
536 }
537
538 thread::sleep(Duration::from_millis(100));
540
541 let stats = executor.stats();
542 assert_eq!(stats.submitted, 10);
543 assert!(stats.completed <= 10);
544
545 executor.shutdown()?;
546 Ok(())
547 }
548}