Skip to main content

oxirs_shacl/optimization/
parallel.rs

1//! Parallel validation capabilities for SHACL constraint evaluation
2//!
3//! This module provides thread-safe parallel processing for SHACL validation,
4//! with work distribution, load balancing, and performance monitoring.
5
6use crate::{
7    constraints::{Constraint, ConstraintContext, ConstraintEvaluationResult},
8    optimization::core::{ConstraintCache, ConstraintDependencyAnalyzer},
9    report::ValidationReport,
10    Result, ShaclError, Shape, ShapeId,
11};
12use indexmap::IndexMap;
13use oxirs_core::{model::Term, Store};
14use std::collections::HashMap;
15use std::sync::{
16    atomic::{AtomicUsize, Ordering},
17    Arc, Mutex, RwLock,
18};
19use std::thread;
20use std::time::{Duration, Instant};
21
22/// Configuration for parallel validation
23#[derive(Debug, Clone)]
24pub struct ParallelValidationConfig {
25    /// Maximum number of worker threads
26    pub max_threads: usize,
27    /// Work batch size per thread
28    pub batch_size: usize,
29    /// Enable work stealing between threads
30    pub enable_work_stealing: bool,
31    /// Thread pool idle timeout
32    pub idle_timeout: Duration,
33    /// Enable load balancing
34    pub enable_load_balancing: bool,
35    /// Maximum queue size per worker
36    pub max_queue_size: usize,
37}
38
39impl Default for ParallelValidationConfig {
40    fn default() -> Self {
41        let num_cpus = thread::available_parallelism()
42            .map(|n| n.get())
43            .unwrap_or(4);
44
45        Self {
46            max_threads: num_cpus,
47            batch_size: 100,
48            enable_work_stealing: true,
49            idle_timeout: Duration::from_secs(30),
50            enable_load_balancing: true,
51            max_queue_size: 1000,
52        }
53    }
54}
55
56/// Work item for parallel validation
57#[derive(Debug, Clone)]
58struct ValidationWorkItem {
59    /// Constraint to evaluate
60    constraint: Constraint,
61    /// Evaluation context
62    context: ConstraintContext,
63    /// Priority (lower values = higher priority)
64    priority: usize,
65    /// Estimated execution cost
66    estimated_cost: f64,
67}
68
69/// Result of parallel validation work
70#[derive(Debug, Clone)]
71#[allow(dead_code)]
72struct ValidationWorkResult {
73    /// Work item that was processed
74    work_item: ValidationWorkItem,
75    /// Evaluation result
76    result: ConstraintEvaluationResult,
77    /// Actual execution time
78    execution_time: Duration,
79    /// Worker thread ID that processed this
80    worker_id: usize,
81}
82
83/// Statistics for worker thread performance
84#[derive(Debug, Clone, Default)]
85pub struct WorkerStats {
86    /// Number of work items processed
87    pub items_processed: usize,
88    /// Total processing time
89    pub total_processing_time: Duration,
90    /// Average processing time per item
91    pub avg_processing_time: Duration,
92    /// Number of cache hits
93    pub cache_hits: usize,
94    /// Number of cache misses
95    pub cache_misses: usize,
96    /// Work items stolen from other threads
97    pub work_stolen: usize,
98    /// Work items stolen by other threads
99    pub work_given: usize,
100}
101
102/// Statistics for the entire worker pool
103#[derive(Debug, Clone)]
104pub struct WorkerPoolStats {
105    /// Statistics per worker thread
106    pub worker_stats: HashMap<usize, WorkerStats>,
107    /// Total work items processed
108    pub total_items_processed: usize,
109    /// Total parallel execution time
110    pub total_execution_time: Duration,
111    /// Parallel efficiency ratio (actual speedup / theoretical max)
112    pub parallel_efficiency: f64,
113    /// Load balancing effectiveness
114    pub load_balance_score: f64,
115    /// Cache effectiveness across all workers
116    pub overall_cache_hit_rate: f64,
117}
118
119/// Parallel validation engine
120#[derive(Debug)]
121pub struct ParallelValidationEngine {
122    /// Configuration
123    config: ParallelValidationConfig,
124    /// Shared constraint cache
125    cache: Arc<ConstraintCache>,
126    /// Dependency analyzer for work prioritization
127    dependency_analyzer: Arc<ConstraintDependencyAnalyzer>,
128    /// Worker pool statistics
129    stats: Arc<RwLock<WorkerPoolStats>>,
130    /// Active worker count
131    active_workers: Arc<AtomicUsize>,
132}
133
134impl ParallelValidationEngine {
135    /// Create a new parallel validation engine
136    pub fn new(config: ParallelValidationConfig) -> Self {
137        let cache = Arc::new(ConstraintCache::new(
138            50000, // Large cache for parallel processing
139            Duration::from_secs(600),
140        ));
141
142        let dependency_analyzer = Arc::new(ConstraintDependencyAnalyzer::default());
143
144        let stats = Arc::new(RwLock::new(WorkerPoolStats {
145            worker_stats: HashMap::new(),
146            total_items_processed: 0,
147            total_execution_time: Duration::ZERO,
148            parallel_efficiency: 0.0,
149            load_balance_score: 0.0,
150            overall_cache_hit_rate: 0.0,
151        }));
152
153        Self {
154            config,
155            cache,
156            dependency_analyzer,
157            stats,
158            active_workers: Arc::new(AtomicUsize::new(0)),
159        }
160    }
161
162    /// Validate constraints in parallel
163    pub fn validate_parallel(
164        &mut self,
165        store: &dyn Store,
166        shapes: &IndexMap<ShapeId, Shape>,
167        focus_nodes: &[Term],
168    ) -> Result<ParallelValidationResult> {
169        let start_time = Instant::now();
170
171        // Prepare work items
172        let work_items = self.prepare_work_items(store, shapes, focus_nodes)?;
173        let total_work_items = work_items.len();
174
175        if work_items.is_empty() {
176            return Ok(ParallelValidationResult {
177                validation_report: ValidationReport::new(),
178                total_work_items: 0,
179                processed_items: 0,
180                parallel_execution_time: Duration::ZERO,
181                speedup_ratio: 1.0,
182                efficiency_score: 1.0,
183            });
184        }
185
186        // Determine optimal number of threads
187        let num_threads = std::cmp::min(
188            self.config.max_threads,
189            std::cmp::max(1, work_items.len() / self.config.batch_size),
190        );
191
192        // Split work among threads
193        let work_chunks = self.distribute_work(work_items, num_threads);
194
195        // Execute parallel validation
196        let results = self.execute_parallel_validation(store, work_chunks, num_threads)?;
197
198        // Combine results
199        let mut validation_report = ValidationReport::new();
200        for result in &results {
201            if let Some(violation) = result.result.clone().into_violation() {
202                validation_report.add_violation(violation);
203            }
204        }
205
206        let total_execution_time = start_time.elapsed();
207
208        // Calculate performance metrics
209        let speedup_ratio = self.calculate_speedup_ratio(total_execution_time, &results);
210        let efficiency_score = speedup_ratio / num_threads as f64;
211
212        // Update statistics
213        self.update_pool_statistics(total_execution_time, &results)?;
214
215        Ok(ParallelValidationResult {
216            validation_report,
217            total_work_items,
218            processed_items: results.len(),
219            parallel_execution_time: total_execution_time,
220            speedup_ratio,
221            efficiency_score,
222        })
223    }
224
225    /// Prepare work items from shapes and focus nodes
226    fn prepare_work_items(
227        &self,
228        _store: &dyn Store,
229        shapes: &IndexMap<ShapeId, Shape>,
230        focus_nodes: &[Term],
231    ) -> Result<Vec<ValidationWorkItem>> {
232        let mut work_items = Vec::new();
233
234        for (shape_id, shape) in shapes {
235            if !shape.is_active() {
236                continue;
237            }
238
239            for focus_node in focus_nodes {
240                for (_, constraint) in &shape.constraints {
241                    let values = vec![focus_node.clone()]; // Simplified for parallel processing
242
243                    let mut context = ConstraintContext::new(focus_node.clone(), shape_id.clone())
244                        .with_values(values);
245                    if let Some(path) = &shape.path {
246                        context = context.with_path(path.clone());
247                    }
248
249                    let estimated_cost = self
250                        .dependency_analyzer
251                        .estimate_constraint_cost(constraint);
252                    let priority = self.calculate_priority(constraint, estimated_cost);
253
254                    work_items.push(ValidationWorkItem {
255                        constraint: constraint.clone(),
256                        context,
257                        priority,
258                        estimated_cost,
259                    });
260                }
261            }
262        }
263
264        // Sort by priority and cost for optimal scheduling
265        work_items.sort_by(|a, b| {
266            a.priority.cmp(&b.priority).then_with(|| {
267                a.estimated_cost
268                    .partial_cmp(&b.estimated_cost)
269                    .unwrap_or(std::cmp::Ordering::Equal)
270            })
271        });
272
273        Ok(work_items)
274    }
275
276    /// Calculate priority for a constraint (lower = higher priority)
277    fn calculate_priority(&self, constraint: &Constraint, estimated_cost: f64) -> usize {
278        let selectivity = self
279            .dependency_analyzer
280            .estimate_constraint_selectivity(constraint);
281
282        // High selectivity and low cost = high priority (low value)
283        let priority_score = selectivity * estimated_cost;
284        (priority_score * 1000.0) as usize
285    }
286
287    /// Distribute work among threads
288    fn distribute_work(
289        &self,
290        work_items: Vec<ValidationWorkItem>,
291        num_threads: usize,
292    ) -> Vec<Vec<ValidationWorkItem>> {
293        let mut chunks = vec![Vec::new(); num_threads];
294
295        if self.config.enable_load_balancing {
296            // Load-balanced distribution based on estimated cost
297            let mut thread_loads = vec![0.0; num_threads];
298
299            for item in work_items {
300                // Find thread with minimum load
301                let min_load_thread = thread_loads
302                    .iter()
303                    .enumerate()
304                    .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
305                    .map(|(i, _)| i)
306                    .unwrap_or(0);
307
308                thread_loads[min_load_thread] += item.estimated_cost;
309                chunks[min_load_thread].push(item);
310            }
311        } else {
312            // Simple round-robin distribution
313            for (i, item) in work_items.into_iter().enumerate() {
314                chunks[i % num_threads].push(item);
315            }
316        }
317
318        chunks
319    }
320
321    /// Execute parallel validation with worker threads
322    fn execute_parallel_validation(
323        &self,
324        _store: &dyn Store,
325        work_chunks: Vec<Vec<ValidationWorkItem>>,
326        num_threads: usize,
327    ) -> Result<Vec<ValidationWorkResult>> {
328        let results = Arc::new(Mutex::new(Vec::new()));
329        let work_queues: Arc<Vec<Mutex<Vec<ValidationWorkItem>>>> =
330            Arc::new(work_chunks.into_iter().map(Mutex::new).collect());
331
332        let mut handles = Vec::new();
333
334        for worker_id in 0..num_threads {
335            let cache = Arc::clone(&self.cache);
336            let work_queues = Arc::clone(&work_queues);
337            let results = Arc::clone(&results);
338            let config = self.config.clone();
339            let active_workers = Arc::clone(&self.active_workers);
340
341            // Note: This is a simplified implementation
342            // In practice, we'd need a thread-safe Store interface
343            let handle = thread::spawn(move || {
344                active_workers.fetch_add(1, Ordering::SeqCst);
345
346                let worker_results = Self::worker_thread(worker_id, cache, work_queues, config);
347
348                // Collect results
349                if let Ok(mut results_guard) = results.lock() {
350                    results_guard.extend(worker_results);
351                }
352
353                active_workers.fetch_sub(1, Ordering::SeqCst);
354            });
355
356            handles.push(handle);
357        }
358
359        // Wait for all workers to complete
360        for handle in handles {
361            handle
362                .join()
363                .map_err(|_| ShaclError::ValidationEngine("Worker thread panicked".to_string()))?;
364        }
365
366        let final_results = results
367            .lock()
368            .expect("results mutex should not be poisoned")
369            .clone();
370        Ok(final_results)
371    }
372
373    /// Worker thread implementation
374    fn worker_thread(
375        worker_id: usize,
376        cache: Arc<ConstraintCache>,
377        work_queues: Arc<Vec<Mutex<Vec<ValidationWorkItem>>>>,
378        config: ParallelValidationConfig,
379    ) -> Vec<ValidationWorkResult> {
380        let mut worker_results = Vec::new();
381        let my_queue_id = worker_id;
382
383        loop {
384            // Try to get work from own queue first
385            let work_item = match work_queues[my_queue_id].lock() {
386                Ok(mut my_queue) => my_queue.pop(),
387                _ => None,
388            };
389
390            let work_item = if let Some(item) = work_item {
391                item
392            } else if config.enable_work_stealing {
393                // Try to steal work from other queues
394                let mut stolen_item = None;
395                for (queue_id, queue) in work_queues.iter().enumerate() {
396                    if queue_id != my_queue_id {
397                        if let Ok(mut other_queue) = queue.lock() {
398                            if let Some(item) = other_queue.pop() {
399                                stolen_item = Some(item);
400                                break;
401                            }
402                        }
403                    }
404                }
405                if let Some(item) = stolen_item {
406                    item
407                } else {
408                    break; // No more work available
409                }
410            } else {
411                break; // No work stealing, exit
412            };
413
414            // Process work item
415            let start_time = Instant::now();
416
417            // Check cache first
418            let result =
419                if let Some(cached_result) = cache.get(&work_item.constraint, &work_item.context) {
420                    cached_result
421                } else {
422                    // In practice, this would evaluate the constraint against a thread-safe store
423                    // For now, we'll create a placeholder result
424                    ConstraintEvaluationResult::satisfied() // Simplified
425                };
426
427            let execution_time = start_time.elapsed();
428
429            // Cache the result if not from cache
430            cache.put(
431                &work_item.constraint,
432                &work_item.context,
433                result.clone(),
434                execution_time,
435            );
436
437            worker_results.push(ValidationWorkResult {
438                work_item,
439                result,
440                execution_time,
441                worker_id,
442            });
443        }
444
445        worker_results
446    }
447
448    /// Calculate speedup ratio compared to sequential execution
449    fn calculate_speedup_ratio(
450        &self,
451        parallel_time: Duration,
452        results: &[ValidationWorkResult],
453    ) -> f64 {
454        let total_sequential_time: Duration = results.iter().map(|r| r.execution_time).sum();
455
456        if parallel_time.as_nanos() > 0 {
457            total_sequential_time.as_nanos() as f64 / parallel_time.as_nanos() as f64
458        } else {
459            1.0
460        }
461    }
462
463    /// Update pool statistics
464    fn update_pool_statistics(
465        &self,
466        total_time: Duration,
467        results: &[ValidationWorkResult],
468    ) -> Result<()> {
469        if let Ok(mut stats) = self.stats.write() {
470            stats.total_items_processed = results.len();
471            stats.total_execution_time = total_time;
472
473            // Calculate per-worker statistics
474            let mut worker_item_counts: HashMap<usize, usize> = HashMap::new();
475            for result in results {
476                *worker_item_counts.entry(result.worker_id).or_insert(0) += 1;
477            }
478
479            // Calculate load balance score (lower variance = better balance)
480            if !worker_item_counts.is_empty() {
481                let mean = results.len() as f64 / worker_item_counts.len() as f64;
482                let variance: f64 = worker_item_counts
483                    .values()
484                    .map(|&count| (count as f64 - mean).powi(2))
485                    .sum::<f64>()
486                    / worker_item_counts.len() as f64;
487
488                stats.load_balance_score = 1.0 / (1.0 + variance.sqrt());
489            }
490
491            // Update cache statistics
492            let cache_stats = self.cache.stats();
493            stats.overall_cache_hit_rate = cache_stats.hit_rate();
494        }
495
496        Ok(())
497    }
498
499    /// Get current worker pool statistics
500    pub fn get_stats(&self) -> WorkerPoolStats {
501        self.stats
502            .read()
503            .expect("stats lock should not be poisoned")
504            .clone()
505    }
506
507    /// Get number of active workers
508    pub fn get_active_worker_count(&self) -> usize {
509        self.active_workers.load(Ordering::SeqCst)
510    }
511
512    /// Update configuration
513    pub fn update_config(&mut self, config: ParallelValidationConfig) {
514        self.config = config;
515    }
516}
517
518/// Result of parallel validation
519#[derive(Debug, Clone)]
520pub struct ParallelValidationResult {
521    /// The validation report with all violations
522    pub validation_report: ValidationReport,
523    /// Total number of work items
524    pub total_work_items: usize,
525    /// Number of items actually processed
526    pub processed_items: usize,
527    /// Total parallel execution time
528    pub parallel_execution_time: Duration,
529    /// Speedup ratio compared to sequential
530    pub speedup_ratio: f64,
531    /// Parallel efficiency (speedup / number of threads)
532    pub efficiency_score: f64,
533}
534
535impl ParallelValidationResult {
536    /// Check if parallel processing was effective
537    pub fn is_effective(&self) -> bool {
538        self.speedup_ratio > 1.5 && self.efficiency_score > 0.5
539    }
540
541    /// Get performance summary
542    pub fn performance_summary(&self) -> String {
543        format!(
544            "Processed {} items with {:.2}x speedup, {:.1}% efficiency",
545            self.processed_items,
546            self.speedup_ratio,
547            self.efficiency_score * 100.0
548        )
549    }
550}