1use crate::algebra::Algebra;
8use crate::cardinality_estimator::CardinalityEstimator;
9use crate::cost_model::CostModel;
10use anyhow::{anyhow, Result};
11use scirs2_core::metrics::{Counter, Timer};
12use scirs2_core::profiling::Profiler;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::{Arc, RwLock};
16use std::time::{Duration, Instant};
17use tracing::{debug, info};
18
19pub struct AdaptiveExecutor {
21 optimizer: Arc<RwLock<AdaptiveOptimizer>>,
23 config: AdaptiveConfig,
25 profiler: Profiler,
27 metrics: AdaptiveMetrics,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AdaptiveConfig {
34 pub enable_adaptive: bool,
36 pub re_opt_trigger_percent: f64,
38 pub re_opt_trigger_seconds: u64,
40 pub min_reopt_interval_seconds: u64,
42 pub plan_switch_threshold: f64,
44 pub deviation_threshold: f64,
46 pub max_reoptimizations: usize,
48}
49
50impl Default for AdaptiveConfig {
51 fn default() -> Self {
52 Self {
53 enable_adaptive: true,
54 re_opt_trigger_percent: 0.1, re_opt_trigger_seconds: 5,
56 min_reopt_interval_seconds: 5,
57 plan_switch_threshold: 2.0, deviation_threshold: 5.0, max_reoptimizations: 3,
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct RuntimeStatistics {
67 pub operator_stats: HashMap<OperatorId, OperatorStats>,
69 pub execution_time: Duration,
71 pub rows_processed: u64,
73 pub start_time: Instant,
75}
76
77impl Default for RuntimeStatistics {
78 fn default() -> Self {
79 Self {
80 operator_stats: HashMap::new(),
81 execution_time: Duration::ZERO,
82 rows_processed: 0,
83 start_time: Instant::now(),
84 }
85 }
86}
87
88impl RuntimeStatistics {
89 pub fn update_from_batch(&mut self, batch: &BatchResult) -> Result<()> {
91 self.rows_processed += batch.rows_produced;
92 self.execution_time = self.start_time.elapsed();
93
94 for (op_id, op_result) in &batch.operator_results {
95 let stats = self
96 .operator_stats
97 .entry(op_id.clone())
98 .or_insert_with(|| OperatorStats::new(op_id.clone()));
99
100 stats.actual_cardinality += op_result.rows_produced;
101 stats.actual_time_ms += op_result.execution_time_ms;
102 stats.update_deviation();
103 }
104
105 Ok(())
106 }
107
108 pub fn max_deviation(&self) -> f64 {
110 self.operator_stats
111 .values()
112 .map(|s| s.deviation)
113 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
114 .unwrap_or(1.0)
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct OperatorStats {
121 pub operator_id: OperatorId,
123 pub actual_cardinality: u64,
125 pub estimated_cardinality: u64,
127 pub actual_time_ms: f64,
129 pub estimated_time_ms: f64,
131 pub deviation: f64,
133}
134
135impl OperatorStats {
136 pub fn new(operator_id: OperatorId) -> Self {
137 Self {
138 operator_id,
139 actual_cardinality: 0,
140 estimated_cardinality: 1,
141 actual_time_ms: 0.0,
142 estimated_time_ms: 1.0,
143 deviation: 1.0,
144 }
145 }
146
147 pub fn update_deviation(&mut self) {
148 if self.estimated_cardinality > 0 {
149 self.deviation = self.actual_cardinality as f64 / self.estimated_cardinality as f64;
150 }
151 }
152
153 pub fn set_estimates(&mut self, cardinality: u64, time_ms: f64) {
154 self.estimated_cardinality = cardinality;
155 self.estimated_time_ms = time_ms;
156 }
157}
158
159pub type OperatorId = String;
161
162#[derive(Debug, Clone)]
164pub struct BatchResult {
165 pub rows_produced: u64,
167 pub operator_results: HashMap<OperatorId, OperatorResult>,
169 pub is_complete: bool,
171}
172
173#[derive(Debug, Clone)]
175pub struct OperatorResult {
176 pub rows_produced: u64,
178 pub execution_time_ms: f64,
180}
181
182#[derive(Debug, Clone)]
184pub struct QueryPlan {
185 pub algebra: Algebra,
187 pub estimated_cost: f64,
189 pub estimated_total_rows: u64,
191 pub operator_estimates: HashMap<OperatorId, u64>,
193}
194
195#[allow(dead_code)]
197pub struct AdaptiveOptimizer {
198 cardinality_estimator: Arc<RwLock<CardinalityEstimator>>,
200 cost_model: Arc<RwLock<CostModel>>,
202}
203
204impl AdaptiveOptimizer {
205 pub fn new(
206 cardinality_estimator: Arc<RwLock<CardinalityEstimator>>,
207 cost_model: Arc<RwLock<CostModel>>,
208 ) -> Self {
209 Self {
210 cardinality_estimator,
211 cost_model,
212 }
213 }
214
215 pub fn update_cardinality_estimate(&mut self, _op_id: OperatorId, actual: u64) -> Result<()> {
217 debug!("Updated cardinality estimate: actual={}", actual);
220 Ok(())
221 }
222
223 pub fn update_cost_estimate(&mut self, _op_id: OperatorId, actual_time_ms: f64) -> Result<()> {
225 debug!("Updated cost estimate: actual_time_ms={}", actual_time_ms);
227 Ok(())
228 }
229
230 pub fn optimize(&self, _algebra: &Algebra) -> Result<QueryPlan> {
232 Ok(QueryPlan {
235 algebra: Algebra::Bgp(vec![]),
236 estimated_cost: 100.0,
237 estimated_total_rows: 1000,
238 operator_estimates: HashMap::new(),
239 })
240 }
241}
242
243pub struct AdaptiveMetrics {
245 pub reoptimizations: Counter,
247 pub plan_switches: Counter,
249 pub reopt_time: Timer,
251 pub queries_improved: Counter,
253}
254
255impl Default for AdaptiveMetrics {
256 fn default() -> Self {
257 Self {
258 reoptimizations: Counter::new("adaptive.reoptimizations".to_string()),
259 plan_switches: Counter::new("adaptive.plan_switches".to_string()),
260 reopt_time: Timer::new("adaptive.reopt_time".to_string()),
261 queries_improved: Counter::new("adaptive.queries_improved".to_string()),
262 }
263 }
264}
265
266impl AdaptiveExecutor {
267 pub fn new(
269 cardinality_estimator: Arc<RwLock<CardinalityEstimator>>,
270 cost_model: Arc<RwLock<CostModel>>,
271 config: AdaptiveConfig,
272 ) -> Self {
273 let optimizer = Arc::new(RwLock::new(AdaptiveOptimizer::new(
274 cardinality_estimator,
275 cost_model,
276 )));
277
278 Self {
279 optimizer,
280 config,
281 profiler: Profiler::new(),
282 metrics: AdaptiveMetrics::default(),
283 }
284 }
285
286 pub async fn execute_adaptive(
288 &mut self,
289 query: &Algebra,
290 initial_plan: QueryPlan,
291 ) -> Result<QueryResults> {
292 let mut current_plan = initial_plan;
293 let mut stats = RuntimeStatistics {
294 start_time: Instant::now(),
295 ..Default::default()
296 };
297 let mut last_reopt = Instant::now();
298
299 let start_time = Instant::now();
300
301 let mut executor = CheckpointedExecutor::new(current_plan.clone())?;
303
304 loop {
305 let batch_result = executor.execute_batch(1000).await?;
307
308 stats.update_from_batch(&batch_result)?;
310
311 let elapsed = start_time.elapsed();
313 let should_reopt = self.should_reoptimize(&stats, elapsed, last_reopt.elapsed())?;
314
315 if should_reopt {
316 info!(
317 "Triggering adaptive re-optimization at {}s",
318 elapsed.as_secs_f64()
319 );
320
321 self.metrics.reoptimizations.inc();
322 self.profiler.start();
323
324 let refined_plan = self.reoptimize_with_statistics(query, &stats)?;
326
327 if self.is_plan_significantly_better(¤t_plan, &refined_plan, &stats)? {
329 let improvement =
330 self.estimate_improvement(¤t_plan, &refined_plan, &stats)?;
331 info!(
332 "Switching to new plan (estimated {}x improvement)",
333 improvement
334 );
335
336 let checkpoint = executor.checkpoint()?;
338
339 current_plan = refined_plan;
341 executor = CheckpointedExecutor::new_from_checkpoint(
342 current_plan.clone(),
343 checkpoint,
344 )?;
345
346 self.metrics.plan_switches.inc();
347 last_reopt = Instant::now();
348 } else {
349 info!("New plan not significantly better, continuing with current plan");
350 }
351 }
352
353 if batch_result.is_complete {
355 break;
356 }
357 }
358
359 executor.finalize()
360 }
361
362 fn should_reoptimize(
364 &self,
365 stats: &RuntimeStatistics,
366 elapsed: Duration,
367 since_last_reopt: Duration,
368 ) -> Result<bool> {
369 if !self.config.enable_adaptive {
370 return Ok(false);
371 }
372
373 if since_last_reopt.as_secs() < self.config.min_reopt_interval_seconds {
375 return Ok(false);
376 }
377
378 if elapsed.as_secs() >= self.config.re_opt_trigger_seconds {
380 debug!("Re-optimization triggered by time threshold");
381 return Ok(true);
382 }
383
384 let max_deviation = stats.max_deviation();
386
387 if max_deviation > self.config.deviation_threshold {
388 info!("Large deviation detected: {}x", max_deviation);
389 return Ok(true);
390 }
391
392 Ok(false)
393 }
394
395 fn reoptimize_with_statistics(
397 &self,
398 query: &Algebra,
399 stats: &RuntimeStatistics,
400 ) -> Result<QueryPlan> {
401 let mut optimizer = self
403 .optimizer
404 .write()
405 .map_err(|e| anyhow!("Failed to acquire optimizer lock: {}", e))?;
406
407 for (op_id, op_stats) in &stats.operator_stats {
408 optimizer.update_cardinality_estimate(op_id.clone(), op_stats.actual_cardinality)?;
409 optimizer.update_cost_estimate(op_id.clone(), op_stats.actual_time_ms)?;
410 }
411
412 let new_plan = optimizer.optimize(query)?;
414 Ok(new_plan)
415 }
416
417 fn is_plan_significantly_better(
419 &self,
420 current_plan: &QueryPlan,
421 new_plan: &QueryPlan,
422 stats: &RuntimeStatistics,
423 ) -> Result<bool> {
424 let current_remaining_cost = self.estimate_remaining_cost(current_plan, stats)?;
426 let new_remaining_cost = self.estimate_remaining_cost(new_plan, stats)?;
427
428 let improvement = current_remaining_cost / new_remaining_cost;
429 Ok(improvement > self.config.plan_switch_threshold)
430 }
431
432 fn estimate_remaining_cost(&self, plan: &QueryPlan, stats: &RuntimeStatistics) -> Result<f64> {
433 let processed = stats.rows_processed;
435 let total_estimated = plan.estimated_total_rows.max(1);
436 let remaining_percent = if processed < total_estimated {
437 (total_estimated - processed) as f64 / total_estimated as f64
438 } else {
439 0.1 };
441
442 Ok(plan.estimated_cost * remaining_percent)
443 }
444
445 fn estimate_improvement(
446 &self,
447 current: &QueryPlan,
448 new: &QueryPlan,
449 stats: &RuntimeStatistics,
450 ) -> Result<f64> {
451 let current_cost = self.estimate_remaining_cost(current, stats)?;
452 let new_cost = self.estimate_remaining_cost(new, stats)?.max(0.1);
453 Ok(current_cost / new_cost)
454 }
455
456 pub fn get_config(&self) -> &AdaptiveConfig {
458 &self.config
459 }
460
461 pub fn get_profiler(&self) -> &Profiler {
463 &self.profiler
464 }
465
466 pub fn get_metrics(&self) -> &AdaptiveMetrics {
468 &self.metrics
469 }
470}
471
472#[allow(dead_code)]
474pub struct CheckpointedExecutor {
475 plan: QueryPlan,
476 state: ExecutorState,
477 rows_produced: u64,
478}
479
480#[derive(Debug, Clone, Default)]
482pub struct ExecutorState {
483 #[allow(clippy::derivable_impls)]
485 pub operator_states: HashMap<OperatorId, OperatorState>,
486 pub rows_processed: u64,
488 pub intermediate_results: Vec<u8>, }
491
492#[derive(Debug, Clone)]
494pub struct OperatorState {
495 pub operator_id: OperatorId,
497 pub data: Vec<u8>,
499 pub rows_processed: u64,
501}
502
503impl CheckpointedExecutor {
504 pub fn new(plan: QueryPlan) -> Result<Self> {
506 Ok(Self {
507 plan,
508 state: ExecutorState::default(),
509 rows_produced: 0,
510 })
511 }
512
513 pub fn new_from_checkpoint(plan: QueryPlan, checkpoint: ExecutorState) -> Result<Self> {
515 Ok(Self {
516 plan,
517 state: checkpoint,
518 rows_produced: 0,
519 })
520 }
521
522 pub async fn execute_batch(&mut self, batch_size: u64) -> Result<BatchResult> {
524 let rows_produced = batch_size.min(100); self.rows_produced += rows_produced;
529 self.state.rows_processed += rows_produced;
530
531 let mut operator_results = HashMap::new();
532 operator_results.insert(
533 "scan_op".to_string(),
534 OperatorResult {
535 rows_produced,
536 execution_time_ms: 10.0,
537 },
538 );
539
540 let is_complete = self.rows_produced >= 1000;
542
543 Ok(BatchResult {
544 rows_produced,
545 operator_results,
546 is_complete,
547 })
548 }
549
550 pub fn checkpoint(&self) -> Result<ExecutorState> {
552 Ok(self.state.clone())
553 }
554
555 pub fn finalize(self) -> Result<QueryResults> {
557 Ok(QueryResults {
558 rows: self.rows_produced,
559 execution_time: Duration::from_millis(100),
560 })
561 }
562}
563
564#[derive(Debug, Clone)]
566pub struct QueryResults {
567 pub rows: u64,
569 pub execution_time: Duration,
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use crate::cardinality_estimator::EstimatorConfig;
577 use crate::cost_model::CostModelConfig;
578
579 #[tokio::test]
580 async fn test_adaptive_executor_basic() -> Result<()> {
581 let estimator = Arc::new(RwLock::new(CardinalityEstimator::new(
582 EstimatorConfig::default(),
583 )));
584 let cost_model = Arc::new(RwLock::new(CostModel::new(CostModelConfig::default())));
585 let config = AdaptiveConfig::default();
586
587 let mut executor = AdaptiveExecutor::new(estimator, cost_model, config);
588
589 let query = Algebra::Bgp(vec![]);
590 let plan = QueryPlan {
591 algebra: query.clone(),
592 estimated_cost: 1000.0,
593 estimated_total_rows: 10000,
594 operator_estimates: HashMap::new(),
595 };
596
597 let results = executor.execute_adaptive(&query, plan).await?;
598 assert!(results.rows > 0);
599
600 Ok(())
601 }
602
603 #[tokio::test]
604 async fn test_checkpointing() -> Result<()> {
605 let plan = QueryPlan {
606 algebra: Algebra::Bgp(vec![]),
607 estimated_cost: 100.0,
608 estimated_total_rows: 1000,
609 operator_estimates: HashMap::new(),
610 };
611
612 let mut executor = CheckpointedExecutor::new(plan.clone())?;
613
614 let _batch1 = executor.execute_batch(100).await?;
616 let _batch2 = executor.execute_batch(100).await?;
617
618 let checkpoint = executor.checkpoint()?;
620 assert_eq!(checkpoint.rows_processed, 200);
621
622 let mut executor2 = CheckpointedExecutor::new_from_checkpoint(plan, checkpoint)?;
624 let _batch3 = executor2.execute_batch(100).await?;
625
626 Ok(())
627 }
628
629 #[test]
630 fn test_runtime_statistics() {
631 let mut stats = RuntimeStatistics {
632 start_time: Instant::now(),
633 ..Default::default()
634 };
635
636 let batch = BatchResult {
637 rows_produced: 100,
638 operator_results: {
639 let mut map = HashMap::new();
640 map.insert(
641 "op1".to_string(),
642 OperatorResult {
643 rows_produced: 100,
644 execution_time_ms: 50.0,
645 },
646 );
647 map
648 },
649 is_complete: false,
650 };
651
652 stats.update_from_batch(&batch).ok();
653 assert_eq!(stats.rows_processed, 100);
654 }
655
656 #[test]
657 fn test_deviation_calculation() {
658 let mut op_stats = OperatorStats::new("test_op".to_string());
659 op_stats.set_estimates(100, 10.0);
660 op_stats.actual_cardinality = 500;
661 op_stats.update_deviation();
662
663 assert!((op_stats.deviation - 5.0).abs() < 0.01);
664 }
665
666 #[test]
667 fn test_config_defaults() {
668 let config = AdaptiveConfig::default();
669 assert!(config.enable_adaptive);
670 assert_eq!(config.re_opt_trigger_seconds, 5);
671 assert_eq!(config.min_reopt_interval_seconds, 5);
672 assert_eq!(config.plan_switch_threshold, 2.0);
673 assert_eq!(config.deviation_threshold, 5.0);
674 }
675}