1use crate::dictionary::NodeId;
15use crate::error::{Result, TdbError};
16use crate::query_hints::IndexType;
17use crate::query_optimizer::{QueryOptimizer, QueryPattern, QueryPlan};
18use parking_lot::RwLock;
19use std::collections::HashMap;
20use std::sync::Arc;
21use std::time::{Duration, Instant};
22
23pub struct AdaptiveExecutor {
25 optimizer: Arc<QueryOptimizer>,
27 history: Arc<RwLock<ExecutionHistory>>,
29 config: AdaptiveConfig,
31}
32
33#[derive(Debug, Clone)]
35pub struct AdaptiveConfig {
36 pub enabled: bool,
38 pub reoptimize_threshold: f64,
40 pub min_samples: usize,
42 pub max_history_entries: usize,
44 pub enable_plan_switching: bool,
46}
47
48impl Default for AdaptiveConfig {
49 fn default() -> Self {
50 Self {
51 enabled: true,
52 reoptimize_threshold: 2.0, min_samples: 3,
54 max_history_entries: 100,
55 enable_plan_switching: true,
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct ExecutionStats {
63 pub pattern: QueryPattern,
65 pub index_used: IndexType,
67 pub execution_time: Duration,
69 pub actual_results: usize,
71 pub estimated_results: usize,
73 pub was_optimal: bool,
75 pub timestamp: Instant,
77}
78
79impl ExecutionStats {
80 pub fn estimation_error_ratio(&self) -> f64 {
82 if self.estimated_results == 0 {
83 return if self.actual_results == 0 {
84 1.0
85 } else {
86 f64::INFINITY
87 };
88 }
89 self.actual_results as f64 / self.estimated_results as f64
90 }
91
92 pub fn has_significant_deviation(&self, threshold: f64) -> bool {
94 let ratio = self.estimation_error_ratio();
95 ratio > threshold || ratio < (1.0 / threshold)
96 }
97}
98
99#[derive(Debug, Default)]
101struct ExecutionHistory {
102 entries: HashMap<QueryPattern, Vec<ExecutionStats>>,
104}
105
106impl ExecutionHistory {
107 fn add_entry(&mut self, stats: ExecutionStats, max_entries: usize) {
109 let entries = self.entries.entry(stats.pattern.clone()).or_default();
110
111 entries.push(stats);
112
113 if entries.len() > max_entries {
115 entries.remove(0);
116 }
117 }
118
119 fn avg_execution_time(&self, pattern: &QueryPattern) -> Option<Duration> {
121 let entries = self.entries.get(pattern)?;
122 if entries.is_empty() {
123 return None;
124 }
125
126 let total_nanos: u128 = entries.iter().map(|e| e.execution_time.as_nanos()).sum();
127 let avg_nanos = total_nanos / entries.len() as u128;
128
129 Some(Duration::from_nanos(avg_nanos as u64))
130 }
131
132 fn avg_estimation_error(&self, pattern: &QueryPattern) -> Option<f64> {
134 let entries = self.entries.get(pattern)?;
135 if entries.is_empty() {
136 return None;
137 }
138
139 let total_error: f64 = entries.iter().map(|e| e.estimation_error_ratio()).sum();
140 Some(total_error / entries.len() as f64)
141 }
142
143 fn sample_count(&self, pattern: &QueryPattern) -> usize {
145 self.entries.get(pattern).map_or(0, |e| e.len())
146 }
147
148 fn has_sufficient_history(&self, pattern: &QueryPattern, min_samples: usize) -> bool {
150 self.sample_count(pattern) >= min_samples
151 }
152}
153
154impl AdaptiveExecutor {
155 pub fn new(optimizer: Arc<QueryOptimizer>) -> Self {
157 Self {
158 optimizer,
159 history: Arc::new(RwLock::new(ExecutionHistory::default())),
160 config: AdaptiveConfig::default(),
161 }
162 }
163
164 pub fn with_config(optimizer: Arc<QueryOptimizer>, config: AdaptiveConfig) -> Self {
166 Self {
167 optimizer,
168 history: Arc::new(RwLock::new(ExecutionHistory::default())),
169 config,
170 }
171 }
172
173 pub fn create_plan(&self, pattern: QueryPattern) -> Result<QueryPlan> {
175 if !self.config.enabled {
176 let hints = crate::query_hints::QueryHints::new();
178 return self.optimizer.optimize(pattern, &hints);
179 }
180
181 let history = self.history.read();
183 if history.has_sufficient_history(&pattern, self.config.min_samples) {
184 if let Some(avg_error) = history.avg_estimation_error(&pattern) {
186 let mut hints = crate::query_hints::QueryHints::new();
188
189 if avg_error > self.config.reoptimize_threshold {
191 hints = hints.with_caching(false);
193 }
194
195 return self.optimizer.optimize(pattern, &hints);
196 }
197 }
198
199 let hints = crate::query_hints::QueryHints::new();
201 self.optimizer.optimize(pattern, &hints)
202 }
203
204 pub fn record_execution(
206 &self,
207 pattern: QueryPattern,
208 plan: &QueryPlan,
209 actual_results: usize,
210 execution_time: Duration,
211 ) {
212 let stats = ExecutionStats {
213 pattern: pattern.clone(),
214 index_used: plan.index,
215 execution_time,
216 actual_results,
217 estimated_results: plan.estimated_results,
218 was_optimal: !self.should_reoptimize(plan, actual_results),
219 timestamp: Instant::now(),
220 };
221
222 let mut history = self.history.write();
223 history.add_entry(stats, self.config.max_history_entries);
224 }
225
226 pub fn should_reoptimize(&self, plan: &QueryPlan, actual_results: usize) -> bool {
228 if !self.config.enabled {
229 return false;
230 }
231
232 let ratio = if plan.estimated_results == 0 {
233 if actual_results == 0 {
234 1.0
235 } else {
236 return true; }
238 } else {
239 actual_results as f64 / plan.estimated_results as f64
240 };
241
242 ratio > self.config.reoptimize_threshold || ratio < (1.0 / self.config.reoptimize_threshold)
243 }
244
245 pub fn get_statistics(&self, pattern: &QueryPattern) -> Option<PatternStatistics> {
247 let history = self.history.read();
248
249 if !history.has_sufficient_history(pattern, self.config.min_samples) {
250 return None;
251 }
252
253 Some(PatternStatistics {
254 sample_count: history.sample_count(pattern),
255 avg_execution_time: history.avg_execution_time(pattern)?,
256 avg_estimation_error: history.avg_estimation_error(pattern)?,
257 })
258 }
259
260 pub fn clear_history(&self) {
262 let mut history = self.history.write();
263 history.entries.clear();
264 }
265
266 pub fn tracked_patterns_count(&self) -> usize {
268 let history = self.history.read();
269 history.entries.len()
270 }
271
272 pub fn total_samples(&self) -> usize {
274 let history = self.history.read();
275 history.entries.values().map(|v| v.len()).sum()
276 }
277
278 pub fn reoptimize(&self, pattern: QueryPattern, actual_results: usize) -> Result<QueryPlan> {
280 let mut hints = crate::query_hints::QueryHints::new();
282
283 if actual_results > 10000 {
285 hints = hints.with_caching(false);
287 }
288
289 self.optimizer.optimize(pattern, &hints)
290 }
291}
292
293#[derive(Debug, Clone)]
295pub struct PatternStatistics {
296 pub sample_count: usize,
298 pub avg_execution_time: Duration,
300 pub avg_estimation_error: f64,
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::statistics::{StatisticsConfig, TripleStatistics};
308
309 fn create_test_executor() -> AdaptiveExecutor {
310 let stats = Arc::new(TripleStatistics::new(StatisticsConfig::default()));
311 let optimizer = Arc::new(QueryOptimizer::new(stats));
312 AdaptiveExecutor::new(optimizer)
313 }
314
315 #[test]
316 fn test_adaptive_executor_creation() {
317 let executor = create_test_executor();
318 assert_eq!(executor.tracked_patterns_count(), 0);
319 assert_eq!(executor.total_samples(), 0);
320 }
321
322 #[test]
323 fn test_execution_stats_error_ratio() {
324 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
325 let stats = ExecutionStats {
326 pattern,
327 index_used: IndexType::SPO,
328 execution_time: Duration::from_millis(100),
329 actual_results: 200,
330 estimated_results: 100,
331 was_optimal: false,
332 timestamp: Instant::now(),
333 };
334
335 assert_eq!(stats.estimation_error_ratio(), 2.0);
337 assert!(stats.has_significant_deviation(1.5));
338 }
339
340 #[test]
341 fn test_record_and_retrieve_execution() {
342 let executor = create_test_executor();
343 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
344 let plan = QueryPlan::new(pattern.clone(), IndexType::SPO, 100);
345
346 executor.record_execution(pattern.clone(), &plan, 100, Duration::from_millis(50));
348 executor.record_execution(pattern.clone(), &plan, 110, Duration::from_millis(55));
349 executor.record_execution(pattern.clone(), &plan, 90, Duration::from_millis(45));
350
351 let stats = executor.get_statistics(&pattern);
353 assert!(stats.is_some());
354
355 let stats = stats.unwrap();
356 assert_eq!(stats.sample_count, 3);
357 assert!(stats.avg_execution_time.as_millis() >= 45);
358 assert!(stats.avg_execution_time.as_millis() <= 55);
359 }
360
361 #[test]
362 fn test_reoptimization_trigger() {
363 let executor = create_test_executor();
364 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
365 let plan = QueryPlan::new(pattern.clone(), IndexType::SPO, 100);
366
367 assert!(!executor.should_reoptimize(&plan, 120));
369
370 assert!(executor.should_reoptimize(&plan, 300));
372
373 assert!(executor.should_reoptimize(&plan, 30));
375 }
376
377 #[test]
378 fn test_history_size_limit() {
379 let config = AdaptiveConfig {
380 max_history_entries: 5,
381 ..Default::default()
382 };
383
384 let stats = Arc::new(TripleStatistics::new(StatisticsConfig::default()));
385 let optimizer = Arc::new(QueryOptimizer::new(stats));
386 let executor = AdaptiveExecutor::with_config(optimizer, config);
387
388 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
389 let plan = QueryPlan::new(pattern.clone(), IndexType::SPO, 100);
390
391 for i in 0..10 {
393 executor.record_execution(pattern.clone(), &plan, 100 + i, Duration::from_millis(50));
394 }
395
396 let stats = executor.get_statistics(&pattern).unwrap();
398 assert_eq!(stats.sample_count, 5);
399 }
400
401 #[test]
402 fn test_disabled_adaptive_execution() {
403 let config = AdaptiveConfig {
404 enabled: false,
405 ..Default::default()
406 };
407
408 let stats = Arc::new(TripleStatistics::new(StatisticsConfig::default()));
409 let optimizer = Arc::new(QueryOptimizer::new(stats));
410 let executor = AdaptiveExecutor::with_config(optimizer, config);
411
412 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
413 let plan = QueryPlan::new(pattern.clone(), IndexType::SPO, 100);
414
415 assert!(!executor.should_reoptimize(&plan, 1000));
417 }
418
419 #[test]
420 fn test_clear_history() {
421 let executor = create_test_executor();
422 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
423 let plan = QueryPlan::new(pattern.clone(), IndexType::SPO, 100);
424
425 for _ in 0..5 {
427 executor.record_execution(pattern.clone(), &plan, 100, Duration::from_millis(50));
428 }
429
430 assert_eq!(executor.total_samples(), 5);
431
432 executor.clear_history();
433
434 assert_eq!(executor.total_samples(), 0);
435 assert_eq!(executor.tracked_patterns_count(), 0);
436 }
437
438 #[test]
439 fn test_insufficient_samples_no_stats() {
440 let executor = create_test_executor();
441 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
442 let plan = QueryPlan::new(pattern.clone(), IndexType::SPO, 100);
443
444 executor.record_execution(pattern.clone(), &plan, 100, Duration::from_millis(50));
446 executor.record_execution(pattern.clone(), &plan, 100, Duration::from_millis(50));
447
448 assert!(executor.get_statistics(&pattern).is_none());
450 }
451
452 #[test]
453 fn test_pattern_specific_tracking() {
454 let executor = create_test_executor();
455
456 let pattern1 = QueryPattern::new(Some(NodeId::new(1)), None, None);
457 let pattern2 = QueryPattern::new(None, Some(NodeId::new(2)), None);
458
459 let plan1 = QueryPlan::new(pattern1.clone(), IndexType::SPO, 100);
460 let plan2 = QueryPlan::new(pattern2.clone(), IndexType::POS, 200);
461
462 for _ in 0..3 {
464 executor.record_execution(pattern1.clone(), &plan1, 100, Duration::from_millis(50));
465 executor.record_execution(pattern2.clone(), &plan2, 200, Duration::from_millis(100));
466 }
467
468 assert_eq!(executor.tracked_patterns_count(), 2);
470 assert_eq!(executor.total_samples(), 6);
471
472 let stats1 = executor.get_statistics(&pattern1).unwrap();
473 let stats2 = executor.get_statistics(&pattern2).unwrap();
474
475 assert_eq!(stats1.sample_count, 3);
476 assert_eq!(stats2.sample_count, 3);
477 assert!(stats1.avg_execution_time < stats2.avg_execution_time);
478 }
479}