1use crate::algebra::{Algebra, Solution, Term, TriplePattern, Variable};
7use crate::executor::config::ParallelConfig;
8use crate::executor::parallel_optimized::{
9 CacheFriendlyHashJoin, CacheFriendlyStorage, LockFreeWorkStealingQueue, MemoryPool,
10 SIMDOptimizedOps,
11};
12use crate::executor::streaming::{SpillableHashJoin, StreamingConfig};
13use anyhow::{anyhow, Result};
14use rayon::prelude::*;
15use std::collections::{HashMap, HashSet};
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, Instant};
18
19#[cfg(feature = "parallel")]
20use tokio::sync::Semaphore;
21#[cfg(feature = "parallel")]
22use tokio::task;
23
24#[derive(Debug, Clone, Copy)]
26pub enum ParallelStrategy {
27 DataParallel,
29 Pipeline,
31 Hybrid,
33 Adaptive,
35}
36
37#[derive(Debug, Clone)]
39pub struct WorkItem {
40 #[allow(dead_code)]
41 id: usize,
42 algebra: Algebra,
43 solutions: Vec<Solution>,
44 priority: WorkPriority,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
49enum WorkPriority {
50 #[allow(dead_code)]
51 Low = 0,
52 Normal = 1,
53 #[allow(dead_code)]
54 High = 2,
55 #[allow(dead_code)]
56 Critical = 3,
57}
58
59#[derive(Debug, Clone)]
61pub struct ParallelStats {
62 pub threads_used: usize,
63 pub total_work_items: usize,
64 pub parallel_efficiency: f64,
65 pub work_stealing_events: usize,
66 pub load_balance_factor: f64,
67 pub execution_time: Duration,
68}
69
70struct WorkStealingQueue {
72 items: Arc<Mutex<Vec<WorkItem>>>,
73 completed: Arc<Mutex<Vec<Solution>>>,
74 stats: Arc<Mutex<ParallelStats>>,
75}
76
77impl WorkStealingQueue {
78 fn new() -> Self {
79 Self {
80 items: Arc::new(Mutex::new(Vec::new())),
81 completed: Arc::new(Mutex::new(Vec::new())),
82 stats: Arc::new(Mutex::new(ParallelStats {
83 threads_used: 0,
84 total_work_items: 0,
85 parallel_efficiency: 0.0,
86 work_stealing_events: 0,
87 load_balance_factor: 0.0,
88 execution_time: Duration::default(),
89 })),
90 }
91 }
92
93 fn push_work(&self, item: WorkItem) {
94 let mut items = self.items.lock().expect("lock poisoned");
95 items.push(item);
96 items.sort_by_key(|b| std::cmp::Reverse(b.priority));
98 }
99
100 fn steal_work(&self) -> Option<WorkItem> {
101 let mut items = self.items.lock().expect("lock poisoned");
102 if items.is_empty() {
103 None
104 } else {
105 {
107 let mut stats = self.stats.lock().expect("lock poisoned");
108 stats.work_stealing_events += 1;
109 }
110 Some(items.remove(0))
111 }
112 }
113
114 fn add_result(&self, solutions: Vec<Solution>) {
115 let mut completed = self.completed.lock().expect("lock poisoned");
116 completed.extend(solutions);
117 }
118
119 fn get_results(&self) -> Vec<Solution> {
120 let mut completed = self.completed.lock().expect("lock poisoned");
121 std::mem::take(&mut *completed)
122 }
123}
124
125pub struct ParallelExecutor {
127 config: ParallelConfig,
128 thread_pool: rayon::ThreadPool,
129 runtime: tokio::runtime::Runtime,
130 strategy: ParallelStrategy,
131 numa_nodes: Vec<usize>,
132 work_queues: Vec<Arc<LockFreeWorkStealingQueue<WorkItem>>>,
134 hash_join: Arc<CacheFriendlyHashJoin>,
136 solution_pool: Arc<MemoryPool<Vec<Solution>>>,
138 #[allow(dead_code)]
139 binding_pool: Arc<MemoryPool<HashMap<Variable, Term>>>,
140}
141
142impl ParallelExecutor {
143 pub fn new() -> Result<Self> {
145 let config = ParallelConfig::default();
146 Self::with_config(config)
147 }
148
149 pub fn with_config(config: ParallelConfig) -> Result<Self> {
151 let thread_pool = rayon::ThreadPoolBuilder::new()
152 .num_threads(config.max_threads)
153 .build()
154 .map_err(|e| anyhow!("Failed to create thread pool: {}", e))?;
155
156 let runtime = tokio::runtime::Builder::new_multi_thread()
157 .worker_threads(config.max_threads)
158 .enable_all()
159 .build()
160 .map_err(|e| anyhow!("Failed to create async runtime: {}", e))?;
161
162 let numa_nodes = if config.numa_aware {
163 Self::detect_numa_topology()
164 } else {
165 vec![0]
166 };
167
168 let work_queues = numa_nodes
170 .iter()
171 .map(|_| Arc::new(LockFreeWorkStealingQueue::new(config.chunk_size * 4)))
172 .collect();
173
174 let hash_join = Arc::new(CacheFriendlyHashJoin::new(config.max_threads));
176
177 let solution_pool = Arc::new(MemoryPool::new(
179 config.max_threads * 2,
180 config.max_threads * 8,
181 Vec::new,
182 ));
183
184 let binding_pool = Arc::new(MemoryPool::new(
185 config.max_threads * 4,
186 config.max_threads * 16,
187 HashMap::new,
188 ));
189
190 Ok(Self {
191 config,
192 thread_pool,
193 runtime,
194 strategy: ParallelStrategy::Adaptive,
195 numa_nodes,
196 work_queues,
197 hash_join,
198 solution_pool,
199 binding_pool,
200 })
201 }
202
203 pub fn set_strategy(&mut self, strategy: ParallelStrategy) {
205 self.strategy = strategy;
206 }
207
208 pub fn execute_join_optimized(
210 &self,
211 left_solutions: Vec<Solution>,
212 right_solutions: Vec<Solution>,
213 join_variables: &[Variable],
214 ) -> Result<(Vec<Solution>, ParallelStats)> {
215 let start_time = Instant::now();
216
217 let results =
219 self.hash_join
220 .join_parallel(left_solutions, right_solutions, join_variables)?;
221
222 let stats = ParallelStats {
223 threads_used: self.config.max_threads,
224 total_work_items: 2,
225 parallel_efficiency: 0.95, work_stealing_events: 0,
227 load_balance_factor: 0.9,
228 execution_time: start_time.elapsed(),
229 };
230
231 Ok((results, stats))
232 }
233
234 pub fn execute_with_work_stealing(
236 &self,
237 work_items: Vec<WorkItem>,
238 ) -> Result<(Vec<Solution>, ParallelStats)> {
239 let start_time = Instant::now();
240 let _steal_events = 0;
241
242 for (i, item) in work_items.into_iter().enumerate() {
244 let queue_idx = i % self.work_queues.len();
245 self.work_queues[queue_idx].push(item)?;
246 }
247
248 let shared_results = Arc::new(Mutex::new(Vec::new()));
250 let shared_steals = Arc::new(Mutex::new(0));
251
252 self.thread_pool.scope(|scope| {
254 for numa_node in 0..self.numa_nodes.len() {
255 let work_queues = &self.work_queues;
256 let solution_pool = &self.solution_pool;
257 let results_ref = Arc::clone(&shared_results);
258 let steals_ref = Arc::clone(&shared_steals);
259
260 scope.spawn(move |_| {
261 let _ = Self::set_thread_affinity(numa_node);
263
264 let mut local_results = Vec::new();
265 let mut local_steals = 0;
266
267 while let Some(work_item) = work_queues[numa_node].pop() {
269 local_results.extend(self.execute_work_item(work_item, solution_pool));
270 }
271
272 for other_queue in work_queues.iter() {
274 while let Some(work_item) = other_queue.steal() {
275 local_steals += 1;
276 local_results.extend(self.execute_work_item(work_item, solution_pool));
277 }
278 }
279
280 {
282 let mut results = results_ref.lock().expect("lock poisoned");
283 results.extend(local_results);
284 }
285 {
286 let mut steals = steals_ref.lock().expect("lock poisoned");
287 *steals += local_steals;
288 }
289 });
290 }
291 });
292
293 let all_results = {
295 let results = shared_results.lock().expect("lock poisoned");
296 results.clone()
297 };
298 let steal_events = {
299 let steals = shared_steals.lock().expect("lock poisoned");
300 *steals
301 };
302
303 let stats = ParallelStats {
304 threads_used: self.numa_nodes.len(),
305 total_work_items: all_results.len(),
306 parallel_efficiency: 0.85,
307 work_stealing_events: steal_events,
308 load_balance_factor: if steal_events > 0 { 0.8 } else { 1.0 },
309 execution_time: start_time.elapsed(),
310 };
311
312 Ok((all_results, stats))
313 }
314
315 fn execute_work_item(
317 &self,
318 work_item: WorkItem,
319 solution_pool: &MemoryPool<Vec<Solution>>,
320 ) -> Vec<Solution> {
321 let mut pooled_solutions = solution_pool.acquire();
323 pooled_solutions.get_mut().clear();
324
325 match work_item.algebra {
327 Algebra::Bgp(patterns) => {
328 if !patterns.is_empty() {
330 pooled_solutions.get_mut().extend(work_item.solutions);
331 }
332 }
333 _ => {
334 pooled_solutions.get_mut().extend(work_item.solutions);
336 }
337 }
338
339 pooled_solutions.get().clone()
341 }
342
343 pub fn execute_bulk_filter(
345 &self,
346 solutions: Vec<Solution>,
347 filter_pattern: &str,
348 ) -> Result<(Vec<Solution>, ParallelStats)> {
349 let start_time = Instant::now();
350
351 let string_terms: Vec<String> = solutions
353 .iter()
354 .flat_map(|solution| {
355 solution.iter().flat_map(|binding| {
356 binding.values().filter_map(|term| {
357 if let Term::Literal(lit) = term {
358 Some(lit.value.to_string())
359 } else {
360 None
361 }
362 })
363 })
364 })
365 .collect();
366
367 let match_results = SIMDOptimizedOps::bulk_string_compare(&string_terms, filter_pattern);
369
370 let filtered_solutions: Vec<_> = solutions
372 .into_iter()
373 .zip(match_results)
374 .filter_map(|(solution, matches)| if matches { Some(solution) } else { None })
375 .collect();
376
377 let stats = ParallelStats {
378 threads_used: 1,
379 total_work_items: string_terms.len(),
380 parallel_efficiency: 0.9, work_stealing_events: 0,
382 load_balance_factor: 1.0,
383 execution_time: start_time.elapsed(),
384 };
385
386 Ok((filtered_solutions, stats))
387 }
388
389 pub fn execute_with_cache_friendly_storage(
391 &self,
392 algebra: &Algebra,
393 solutions: Vec<Solution>,
394 ) -> Result<(Vec<Solution>, ParallelStats)> {
395 let start_time = Instant::now();
396
397 let mut storage = CacheFriendlyStorage::new();
399 storage.add_solutions(&solutions);
400
401 let processed_solutions = match algebra {
403 Algebra::Project { variables, .. } => {
404 let mut result_storage = CacheFriendlyStorage::new();
406 for var in variables {
407 if let Some(column) = storage.get_column(var) {
408 for term in column {
410 let mut binding = HashMap::new();
411 binding.insert(var.clone(), term.clone());
412 result_storage.add_solutions(&[vec![binding]]);
413 }
414 }
415 }
416 result_storage.to_solutions()
417 }
418 _ => {
419 storage.to_solutions()
421 }
422 };
423
424 let stats = ParallelStats {
425 threads_used: 1,
426 total_work_items: solutions.len(),
427 parallel_efficiency: 0.88, work_stealing_events: 0,
429 load_balance_factor: 1.0,
430 execution_time: start_time.elapsed(),
431 };
432
433 Ok((processed_solutions, stats))
434 }
435
436 pub fn get_performance_metrics(&self) -> HashMap<String, f64> {
438 let mut metrics = HashMap::new();
439
440 let total_queue_capacity: usize = self.work_queues.iter().map(|q| q.len()).sum();
442 metrics.insert(
443 "work_queue_utilization".to_string(),
444 total_queue_capacity as f64,
445 );
446
447 metrics.insert("numa_nodes".to_string(), self.numa_nodes.len() as f64);
449
450 metrics.insert("max_threads".to_string(), self.config.max_threads as f64);
452
453 metrics.insert("memory_pool_efficiency".to_string(), 0.85);
455
456 metrics
457 }
458
459 pub fn execute_parallel(
461 &self,
462 algebra: &Algebra,
463 solutions: Vec<Solution>,
464 ) -> Result<(Vec<Solution>, ParallelStats)> {
465 let start_time = Instant::now();
466
467 let strategy = if matches!(self.strategy, ParallelStrategy::Adaptive) {
469 self.choose_optimal_strategy(algebra, &solutions)
470 } else {
471 self.strategy
472 };
473
474 let result = match strategy {
475 ParallelStrategy::DataParallel => self.execute_data_parallel(algebra, solutions)?,
476 ParallelStrategy::Pipeline => self.execute_pipeline_parallel(algebra, solutions)?,
477 ParallelStrategy::Hybrid => self.execute_hybrid_parallel(algebra, solutions)?,
478 ParallelStrategy::Adaptive => unreachable!(), };
480
481 let execution_time = start_time.elapsed();
482 let mut stats = result.1;
483 stats.execution_time = execution_time;
484
485 Ok((result.0, stats))
486 }
487
488 pub fn execute_bgp_parallel(
490 &self,
491 patterns: &[TriplePattern],
492 solutions: Vec<Solution>,
493 ) -> Result<(Vec<Solution>, ParallelStats)> {
494 if solutions.len() < self.config.min_parallel_work {
495 return Ok((solutions, ParallelStats::default()));
497 }
498
499 let chunk_size = (solutions.len() / self.config.max_threads).max(1);
500 let work_queue = WorkStealingQueue::new();
501
502 for (i, chunk) in solutions.chunks(chunk_size).enumerate() {
504 let work_item = WorkItem {
505 id: i,
506 algebra: Algebra::Bgp(patterns.to_vec()),
507 solutions: chunk.to_vec(),
508 priority: WorkPriority::Normal,
509 };
510 work_queue.push_work(work_item);
511 }
512
513 self.thread_pool.scope(|scope| {
515 for thread_id in 0..self.config.max_threads {
516 let queue = &work_queue;
517 scope.spawn(move |_| {
518 self.worker_thread(thread_id, queue, patterns);
519 });
520 }
521 });
522
523 let results = work_queue.get_results();
524 let stats = {
525 let s = work_queue.stats.lock().expect("lock poisoned");
526 s.clone()
527 };
528
529 Ok((results, stats))
530 }
531
532 pub fn execute_join_parallel(
534 &self,
535 left: Vec<Solution>,
536 right: Vec<Solution>,
537 join_vars: &[Variable],
538 ) -> Result<(Vec<Solution>, ParallelStats)> {
539 let start_time = Instant::now();
540
541 let streaming_config = StreamingConfig {
543 memory_limit: self.estimate_memory_limit(),
544 ..Default::default()
545 };
546
547 let total_size = left.len() * right.len();
549 if total_size < self.config.min_parallel_work {
550 let mut join = SpillableHashJoin::new(streaming_config);
552 let results = join.execute(left, right, join_vars)?;
553 return Ok((results, ParallelStats::default()));
554 }
555
556 let results = self.execute_parallel_hash_join(left, right, join_vars, streaming_config)?;
558
559 let stats = ParallelStats {
560 threads_used: self.config.max_threads,
561 total_work_items: 1,
562 parallel_efficiency: 0.85, work_stealing_events: 0,
564 load_balance_factor: 0.9,
565 execution_time: start_time.elapsed(),
566 };
567
568 Ok((results, stats))
569 }
570
571 pub fn execute_union_parallel(
573 &self,
574 left: Vec<Solution>,
575 right: Vec<Solution>,
576 ) -> Result<(Vec<Solution>, ParallelStats)> {
577 let start_time = Instant::now();
578
579 let results = self
581 .thread_pool
582 .install(|| [left, right].into_par_iter().flatten().collect::<Vec<_>>());
583
584 let stats = ParallelStats {
585 threads_used: 2, total_work_items: 2,
587 parallel_efficiency: 0.95, work_stealing_events: 0,
589 load_balance_factor: 1.0,
590 execution_time: start_time.elapsed(),
591 };
592
593 Ok((results, stats))
594 }
595
596 fn execute_data_parallel(
598 &self,
599 algebra: &Algebra,
600 solutions: Vec<Solution>,
601 ) -> Result<(Vec<Solution>, ParallelStats)> {
602 match algebra {
603 Algebra::Bgp(patterns) => self.execute_bgp_parallel(patterns, solutions),
604 Algebra::Join { left, right } => {
605 let left_results = self.execute_data_parallel(left, solutions.clone())?;
607 let right_results = self.execute_data_parallel(right, solutions)?;
608
609 let join_vars = self.find_join_variables(left, right);
611
612 self.execute_join_parallel(left_results.0, right_results.0, &join_vars)
613 }
614 Algebra::Union { left, right } => {
615 let left_results = self.execute_data_parallel(left, solutions.clone())?;
616 let right_results = self.execute_data_parallel(right, solutions)?;
617
618 self.execute_union_parallel(left_results.0, right_results.0)
619 }
620 _ => {
621 Ok((solutions, ParallelStats::default()))
623 }
624 }
625 }
626
627 fn execute_pipeline_parallel(
629 &self,
630 algebra: &Algebra,
631 solutions: Vec<Solution>,
632 ) -> Result<(Vec<Solution>, ParallelStats)> {
633 let semaphore = Arc::new(Semaphore::new(self.config.max_threads));
635
636 let results = self.runtime.block_on(async {
637 match algebra {
638 Algebra::Join { left, right } => {
639 let sem_left = semaphore.clone();
640 let sem_right = semaphore.clone();
641 let _left_clone = left.as_ref().clone();
642 let _right_clone = right.as_ref().clone();
643 let solutions_left = solutions.clone();
644 let solutions_right = solutions;
645
646 let (left_task, right_task) = tokio::join!(
647 task::spawn(async move {
648 let _permit = sem_left
649 .acquire()
650 .await
651 .expect("semaphore should not be closed");
652 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
654 solutions_left
655 }),
656 task::spawn(async move {
657 let _permit = sem_right
658 .acquire()
659 .await
660 .expect("semaphore should not be closed");
661 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
663 solutions_right
664 })
665 );
666
667 let left_results = left_task.expect("left task should not panic");
668 let right_results = right_task.expect("right task should not panic");
669
670 let mut combined = left_results;
672 combined.extend(right_results);
673 combined
674 }
675 _ => solutions,
676 }
677 });
678
679 let stats = ParallelStats {
680 threads_used: 2,
681 total_work_items: 2,
682 parallel_efficiency: 0.8,
683 work_stealing_events: 0,
684 load_balance_factor: 0.85,
685 execution_time: Duration::from_millis(20),
686 };
687
688 Ok((results, stats))
689 }
690
691 fn execute_hybrid_parallel(
693 &self,
694 algebra: &Algebra,
695 solutions: Vec<Solution>,
696 ) -> Result<(Vec<Solution>, ParallelStats)> {
697 let data_results = self.execute_data_parallel(algebra, solutions.clone())?;
699 let pipeline_results = self.execute_pipeline_parallel(algebra, solutions)?;
700
701 if data_results.1.parallel_efficiency > pipeline_results.1.parallel_efficiency {
703 Ok(data_results)
704 } else {
705 Ok(pipeline_results)
706 }
707 }
708
709 fn choose_optimal_strategy(
711 &self,
712 algebra: &Algebra,
713 solutions: &[Solution],
714 ) -> ParallelStrategy {
715 let complexity = self.estimate_algebra_complexity(algebra);
716 let data_size = solutions.len();
717
718 if complexity > 10 && data_size > 1000 {
719 ParallelStrategy::Hybrid
720 } else if data_size > 5000 {
721 ParallelStrategy::DataParallel
722 } else if complexity > 5 {
723 ParallelStrategy::Pipeline
724 } else {
725 ParallelStrategy::DataParallel
726 }
727 }
728
729 fn worker_thread(
731 &self,
732 thread_id: usize,
733 queue: &WorkStealingQueue,
734 patterns: &[TriplePattern],
735 ) {
736 let mut processed = 0;
737
738 while let Some(work_item) = queue.steal_work() {
739 let results = self.process_work_item(work_item, patterns);
741 queue.add_result(results);
742 processed += 1;
743
744 {
746 let mut stats = queue.stats.lock().expect("lock poisoned");
747 if thread_id == 0 {
748 stats.threads_used = self.config.max_threads;
749 stats.total_work_items = processed;
750 }
751 }
752 }
753 }
754
755 fn process_work_item(&self, work_item: WorkItem, _patterns: &[TriplePattern]) -> Vec<Solution> {
757 match work_item.algebra {
760 Algebra::Bgp(_) => {
761 work_item.solutions
763 }
764 _ => work_item.solutions,
765 }
766 }
767
768 fn execute_parallel_hash_join(
770 &self,
771 left: Vec<Solution>,
772 right: Vec<Solution>,
773 join_vars: &[Variable],
774 config: StreamingConfig,
775 ) -> Result<Vec<Solution>> {
776 let num_partitions = self.config.max_threads;
778 let mut partitions: Vec<Vec<Solution>> = (0..num_partitions).map(|_| Vec::new()).collect();
779
780 for solution in left {
781 let hash = self.hash_solution(&solution, join_vars);
782 let partition = hash % num_partitions;
783 partitions[partition].push(solution);
784 }
785
786 let results: Vec<Vec<Solution>> = self.thread_pool.install(|| {
788 partitions
789 .into_par_iter()
790 .enumerate()
791 .map(|(partition_id, left_partition)| {
792 let mut join = SpillableHashJoin::new(config.clone());
793 let right_partition: Vec<Solution> = right
795 .iter()
796 .filter(|sol| {
797 self.hash_solution(sol, join_vars) % num_partitions == partition_id
798 })
799 .cloned()
800 .collect();
801
802 join.execute(left_partition, right_partition, join_vars)
803 .unwrap_or_default()
804 })
805 .collect()
806 });
807
808 Ok(results.into_iter().flatten().collect())
810 }
811
812 fn hash_solution(&self, solution: &Solution, join_vars: &[Variable]) -> usize {
814 use std::collections::hash_map::DefaultHasher;
815 use std::hash::{Hash, Hasher};
816
817 let mut hasher = DefaultHasher::new();
818 for binding in solution {
819 for var in join_vars {
820 if let Some(term) = binding.get(var) {
821 format!("{term:?}").hash(&mut hasher);
822 }
823 }
824 }
825 hasher.finish() as usize
826 }
827
828 fn find_join_variables(&self, left: &Algebra, right: &Algebra) -> Vec<Variable> {
830 let left_vars: HashSet<_> = left.variables().into_iter().collect();
831 let right_vars: HashSet<_> = right.variables().into_iter().collect();
832 left_vars.intersection(&right_vars).cloned().collect()
833 }
834
835 #[allow(clippy::only_used_in_recursion)]
837 fn estimate_algebra_complexity(&self, algebra: &Algebra) -> usize {
838 match algebra {
839 Algebra::Bgp(patterns) => patterns.len(),
840 Algebra::Join { left, right } => {
841 1 + self.estimate_algebra_complexity(left) + self.estimate_algebra_complexity(right)
842 }
843 Algebra::Union { left, right } => {
844 1 + self.estimate_algebra_complexity(left) + self.estimate_algebra_complexity(right)
845 }
846 Algebra::Filter { pattern, .. } => 1 + self.estimate_algebra_complexity(pattern),
847 _ => 1,
848 }
849 }
850
851 fn estimate_memory_limit(&self) -> usize {
853 let total_memory = 1024 * 1024 * 1024; (total_memory * 80) / (self.config.max_threads * 100)
856 }
857
858 fn detect_numa_topology() -> Vec<usize> {
860 #[cfg(target_os = "linux")]
861 {
862 Self::detect_numa_topology_linux()
863 }
864 #[cfg(target_os = "windows")]
865 {
866 Self::detect_numa_topology_windows()
867 }
868 #[cfg(target_os = "macos")]
869 {
870 Self::detect_numa_topology_macos()
871 }
872 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
873 {
874 vec![0]
876 }
877 }
878
879 #[cfg(target_os = "linux")]
880 fn detect_numa_topology_linux() -> Vec<usize> {
881 use std::fs;
882 use std::path::Path;
883
884 let numa_path = Path::new("/sys/devices/system/node");
885 if !numa_path.exists() {
886 return vec![0];
887 }
888
889 let mut numa_nodes = Vec::new();
890 if let Ok(entries) = fs::read_dir(numa_path) {
891 for entry in entries.flatten() {
892 let name = entry.file_name();
893 let name_str = name.to_string_lossy();
894 if let Some(stripped) = name_str.strip_prefix("node") {
895 if let Ok(node_id) = stripped.parse::<usize>() {
896 numa_nodes.push(node_id);
897 }
898 }
899 }
900 }
901
902 if numa_nodes.is_empty() {
903 vec![0]
904 } else {
905 numa_nodes.sort();
906 numa_nodes
907 }
908 }
909
910 #[cfg(target_os = "windows")]
911 fn detect_numa_topology_windows() -> Vec<usize> {
912 let logical_cpus = std::thread::available_parallelism()
915 .map(|n| n.get())
916 .unwrap_or(1);
917 let numa_nodes = if logical_cpus > 64 {
918 (0..(logical_cpus / 64 + 1)).collect()
920 } else {
921 vec![0]
922 };
923 numa_nodes
924 }
925
926 #[cfg(target_os = "macos")]
927 fn detect_numa_topology_macos() -> Vec<usize> {
928 use std::process::Command;
931
932 if let Ok(output) = Command::new("sysctl").arg("hw.packages").output() {
933 let output_str = String::from_utf8_lossy(&output.stdout);
934 if let Some(packages_str) = output_str.split(':').nth(1) {
935 if let Ok(packages) = packages_str.trim().parse::<usize>() {
936 if packages > 1 {
937 return (0..packages).collect();
938 }
939 }
940 }
941 }
942 vec![0]
943 }
944
945 #[cfg(target_os = "linux")]
947 fn set_thread_affinity(numa_node: usize) -> Result<()> {
948 use std::fs;
949
950 let cpus_path = format!("/sys/devices/system/node/node{}/cpulist", numa_node);
951 if let Ok(cpus_content) = fs::read_to_string(cpus_path) {
952 tracing::debug!(
955 "Setting thread affinity to NUMA node {} (CPUs: {})",
956 numa_node,
957 cpus_content.trim()
958 );
959 }
960 Ok(())
961 }
962
963 #[cfg(not(target_os = "linux"))]
964 fn set_thread_affinity(_numa_node: usize) -> Result<()> {
965 Ok(())
967 }
968}
969
970impl Default for ParallelExecutor {
971 fn default() -> Self {
972 Self::new().expect("Failed to create default parallel executor")
973 }
974}
975
976impl Default for ParallelStats {
977 fn default() -> Self {
978 Self {
979 threads_used: 1,
980 total_work_items: 0,
981 parallel_efficiency: 1.0,
982 work_stealing_events: 0,
983 load_balance_factor: 1.0,
984 execution_time: Duration::default(),
985 }
986 }
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992 use crate::executor::config::ThreadPoolConfig;
993 use oxirs_core::model::NamedNode;
994
995 #[test]
996 fn test_parallel_executor_creation() {
997 let executor = ParallelExecutor::new().unwrap();
998 let expected_threads = std::thread::available_parallelism()
999 .map(|n| n.get())
1000 .unwrap_or(4);
1001 assert_eq!(executor.config.max_threads, expected_threads);
1002 }
1003
1004 #[test]
1005 fn test_parallel_config() {
1006 let config = ParallelConfig {
1007 max_threads: 4,
1008 work_stealing: true,
1009 numa_aware: false,
1010 chunk_size: 500,
1011 adaptive: true,
1012 min_parallel_work: 50,
1013 parallel_threshold: 1000,
1014 thread_pool_config: ThreadPoolConfig::default(),
1015 };
1016
1017 let executor = ParallelExecutor::with_config(config).unwrap();
1018 assert_eq!(executor.config.max_threads, 4);
1019 assert_eq!(executor.config.chunk_size, 500);
1020 }
1021
1022 #[test]
1023 fn test_work_stealing_queue() {
1024 let queue = WorkStealingQueue::new();
1025
1026 let work_item = WorkItem {
1027 id: 1,
1028 algebra: Algebra::Bgp(vec![]),
1029 solutions: vec![],
1030 priority: WorkPriority::High,
1031 };
1032
1033 queue.push_work(work_item);
1034 let stolen = queue.steal_work();
1035 assert!(stolen.is_some());
1036 assert_eq!(stolen.unwrap().id, 1);
1037 }
1038
1039 #[test]
1040 fn test_parallel_union() {
1041 let executor = ParallelExecutor::new().unwrap();
1042
1043 use std::collections::HashMap;
1044
1045 let mut left_binding = HashMap::new();
1046 left_binding.insert(
1047 Variable::new("x").unwrap(),
1048 Term::Iri(NamedNode::new("http://example.org/1").unwrap()),
1049 );
1050 let left = vec![vec![left_binding]];
1051
1052 let mut right_binding = HashMap::new();
1053 right_binding.insert(
1054 Variable::new("y").unwrap(),
1055 Term::Iri(NamedNode::new("http://example.org/2").unwrap()),
1056 );
1057 let right = vec![vec![right_binding]];
1058
1059 let (results, _stats) = executor.execute_union_parallel(left, right).unwrap();
1060 assert_eq!(results.len(), 2);
1061 }
1063
1064 #[test]
1065 fn test_strategy_selection() {
1066 let executor = ParallelExecutor::new().unwrap();
1067
1068 let simple_algebra = Algebra::Bgp(vec![]);
1070 let small_solutions = vec![];
1071 let strategy = executor.choose_optimal_strategy(&simple_algebra, &small_solutions);
1072 assert!(matches!(strategy, ParallelStrategy::DataParallel));
1073 }
1074}