1use 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#[derive(Debug, Clone)]
24pub struct ParallelValidationConfig {
25 pub max_threads: usize,
27 pub batch_size: usize,
29 pub enable_work_stealing: bool,
31 pub idle_timeout: Duration,
33 pub enable_load_balancing: bool,
35 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#[derive(Debug, Clone)]
58struct ValidationWorkItem {
59 constraint: Constraint,
61 context: ConstraintContext,
63 priority: usize,
65 estimated_cost: f64,
67}
68
69#[derive(Debug, Clone)]
71#[allow(dead_code)]
72struct ValidationWorkResult {
73 work_item: ValidationWorkItem,
75 result: ConstraintEvaluationResult,
77 execution_time: Duration,
79 worker_id: usize,
81}
82
83#[derive(Debug, Clone, Default)]
85pub struct WorkerStats {
86 pub items_processed: usize,
88 pub total_processing_time: Duration,
90 pub avg_processing_time: Duration,
92 pub cache_hits: usize,
94 pub cache_misses: usize,
96 pub work_stolen: usize,
98 pub work_given: usize,
100}
101
102#[derive(Debug, Clone)]
104pub struct WorkerPoolStats {
105 pub worker_stats: HashMap<usize, WorkerStats>,
107 pub total_items_processed: usize,
109 pub total_execution_time: Duration,
111 pub parallel_efficiency: f64,
113 pub load_balance_score: f64,
115 pub overall_cache_hit_rate: f64,
117}
118
119#[derive(Debug)]
121pub struct ParallelValidationEngine {
122 config: ParallelValidationConfig,
124 cache: Arc<ConstraintCache>,
126 dependency_analyzer: Arc<ConstraintDependencyAnalyzer>,
128 stats: Arc<RwLock<WorkerPoolStats>>,
130 active_workers: Arc<AtomicUsize>,
132}
133
134impl ParallelValidationEngine {
135 pub fn new(config: ParallelValidationConfig) -> Self {
137 let cache = Arc::new(ConstraintCache::new(
138 50000, 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 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 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 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 let work_chunks = self.distribute_work(work_items, num_threads);
194
195 let results = self.execute_parallel_validation(store, work_chunks, num_threads)?;
197
198 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 let speedup_ratio = self.calculate_speedup_ratio(total_execution_time, &results);
210 let efficiency_score = speedup_ratio / num_threads as f64;
211
212 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 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()]; 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 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 fn calculate_priority(&self, constraint: &Constraint, estimated_cost: f64) -> usize {
278 let selectivity = self
279 .dependency_analyzer
280 .estimate_constraint_selectivity(constraint);
281
282 let priority_score = selectivity * estimated_cost;
284 (priority_score * 1000.0) as usize
285 }
286
287 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 let mut thread_loads = vec![0.0; num_threads];
298
299 for item in work_items {
300 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 for (i, item) in work_items.into_iter().enumerate() {
314 chunks[i % num_threads].push(item);
315 }
316 }
317
318 chunks
319 }
320
321 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 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 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 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 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 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 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; }
410 } else {
411 break; };
413
414 let start_time = Instant::now();
416
417 let result =
419 if let Some(cached_result) = cache.get(&work_item.constraint, &work_item.context) {
420 cached_result
421 } else {
422 ConstraintEvaluationResult::satisfied() };
426
427 let execution_time = start_time.elapsed();
428
429 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 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 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 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 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 let cache_stats = self.cache.stats();
493 stats.overall_cache_hit_rate = cache_stats.hit_rate();
494 }
495
496 Ok(())
497 }
498
499 pub fn get_stats(&self) -> WorkerPoolStats {
501 self.stats
502 .read()
503 .expect("stats lock should not be poisoned")
504 .clone()
505 }
506
507 pub fn get_active_worker_count(&self) -> usize {
509 self.active_workers.load(Ordering::SeqCst)
510 }
511
512 pub fn update_config(&mut self, config: ParallelValidationConfig) {
514 self.config = config;
515 }
516}
517
518#[derive(Debug, Clone)]
520pub struct ParallelValidationResult {
521 pub validation_report: ValidationReport,
523 pub total_work_items: usize,
525 pub processed_items: usize,
527 pub parallel_execution_time: Duration,
529 pub speedup_ratio: f64,
531 pub efficiency_score: f64,
533}
534
535impl ParallelValidationResult {
536 pub fn is_effective(&self) -> bool {
538 self.speedup_ratio > 1.5 && self.efficiency_score > 0.5
539 }
540
541 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}