1use super::config::*;
8use super::optimizer::{Adaptation, AdaptationPriority, AdaptationType, StreamingDataPoint};
9use super::performance::PerformanceTracker;
10
11use crate::utils::{scalar_or, try_scalar_str};
12use scirs2_core::numeric::Float;
13use std::cmp::Ordering;
14use std::collections::{BinaryHeap, HashMap, VecDeque};
15use std::time::{Duration, Instant};
16
17pub struct AdaptiveBuffer<A: Float + Send + Sync> {
19 config: BufferConfig,
21 buffer: BinaryHeap<PrioritizedDataPoint<A>>,
23 secondary_buffer: VecDeque<StreamingDataPoint<A>>,
25 quality_metrics: BufferQualityMetrics<A>,
27 sizing_strategy: BufferSizingStrategy<A>,
29 statistics: BufferStatistics<A>,
31 last_processing: Instant,
33 size_change_log: VecDeque<SizeChangeEvent>,
35 feature_statistics: RunningFeatureStatistics<A>,
37}
38
39const DEFAULT_EXPECTED_PROCESSING_TIME: Duration = Duration::from_millis(100);
42
43const LATENCY_SMOOTHING: f64 = 0.1;
45
46#[derive(Debug, Clone)]
48struct RunningFeatureStatistics<A: Float + Send + Sync> {
49 means: Vec<A>,
51 m2: Vec<A>,
53 sample_count: usize,
55}
56
57impl<A: Float + Send + Sync> Default for RunningFeatureStatistics<A> {
58 fn default() -> Self {
59 Self {
60 means: Vec::new(),
61 m2: Vec::new(),
62 sample_count: 0,
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct PrioritizedDataPoint<A: Float + Send + Sync> {
70 pub data_point: StreamingDataPoint<A>,
72 pub priority_score: A,
74 pub buffer_timestamp: Instant,
76 pub expected_processing_time: Duration,
78 pub freshness_score: A,
80 pub relevance_score: A,
82}
83
84#[derive(Debug, Clone)]
86pub struct BufferQualityMetrics<A: Float + Send + Sync> {
87 pub average_quality: A,
89 pub quality_variance: A,
91 pub min_quality: A,
93 pub max_quality: A,
95 pub freshness_distribution: Vec<A>,
97 pub priority_distribution: Vec<A>,
99 pub quality_trend: QualityTrend<A>,
101}
102
103#[derive(Debug, Clone)]
105pub struct QualityTrend<A: Float + Send + Sync> {
106 pub recent_changes: VecDeque<A>,
108 pub trend_direction: TrendDirection,
110 pub trend_magnitude: A,
112 pub confidence: A,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum TrendDirection {
119 Improving,
121 Degrading,
123 Stable,
125 Oscillating,
127}
128
129pub struct BufferSizingStrategy<A: Float + Send + Sync> {
131 strategy_type: BufferSizeStrategy,
134 initial_size: usize,
136 target_size: usize,
138 adjustment_params: SizeAdjustmentParams<A>,
140}
141
142#[derive(Debug, Clone)]
144pub struct SizeAdjustmentParams<A: Float + Send + Sync> {
145 pub growth_rate: A,
147 pub shrinkage_rate: A,
149 pub stability_threshold: A,
151 pub performance_sensitivity: A,
153 pub quality_sensitivity: A,
155 pub memory_sensitivity: A,
157}
158
159#[derive(Debug, Clone)]
161pub struct SizingPerformanceFeedback<A: Float + Send + Sync> {
162 pub buffer_size: usize,
164 pub processing_latency: Duration,
166 pub throughput: A,
168 pub quality_score: A,
170 pub memory_usage: usize,
172 pub timestamp: Instant,
174}
175
176#[derive(Debug, Clone)]
178pub struct SizingEvent {
179 pub timestamp: Instant,
181 pub old_size: usize,
183 pub new_size: usize,
185 pub reason: SizingReason,
187 pub performance_impact: Option<f64>,
189}
190
191#[derive(Debug, Clone)]
193pub enum SizingReason {
194 PerformanceOptimization,
196 QualityImprovement,
198 MemoryPressure,
200 LatencyRequirement,
202 ThroughputOptimization,
204 Manual,
206 Configuration,
208}
209
210#[derive(Debug, Clone)]
212pub enum RetentionStrategy {
213 FIFO,
215 LIFO,
217 LRU,
219 Priority,
221 Quality,
223 Age,
225 Hybrid,
227 Adaptive,
229}
230
231#[derive(Debug, Clone)]
233pub struct AgeBasedRetention {
234 pub max_age: Duration,
236 pub soft_age_limit: Duration,
238 pub age_weight: f64,
240 pub adaptive_limits: bool,
242}
243
244#[derive(Debug, Clone)]
246pub struct QualityBasedRetention<A: Float + Send + Sync> {
247 pub min_quality_threshold: A,
249 pub quality_weight: A,
251 pub adaptive_thresholds: bool,
253 pub quality_targets: QualityDistributionTargets<A>,
255}
256
257#[derive(Debug, Clone)]
259pub struct QualityDistributionTargets<A: Float + Send + Sync> {
260 pub high_quality_target: A,
262 pub medium_quality_target: A,
264 pub low_quality_target: A,
266 pub high_quality_threshold: A,
268 pub medium_quality_threshold: A,
269}
270
271#[derive(Debug, Clone)]
273pub struct RelevanceBasedRetention<A: Float + Send + Sync> {
274 pub relevance_method: RelevanceMethod,
276 pub relevance_weight: A,
278 pub temporal_decay: bool,
280 pub decay_rate: A,
282}
283
284#[derive(Debug, Clone)]
286pub enum RelevanceMethod {
287 Distance,
289 Similarity,
291 FeatureImportance,
293 Uncertainty,
295 Diversity,
297 Custom(String),
299}
300
301#[derive(Debug, Clone)]
303pub struct RetentionWeights<A: Float + Send + Sync> {
304 pub age_weight: A,
306 pub quality_weight: A,
308 pub relevance_weight: A,
310 pub priority_weight: A,
312 pub freshness_weight: A,
314 pub diversity_weight: A,
316}
317
318#[derive(Debug, Clone)]
320pub struct RetentionScore<A: Float + Send + Sync> {
321 pub overall_score: A,
323 pub component_scores: HashMap<String, A>,
325 pub should_retain: bool,
327 pub confidence: A,
329 pub timestamp: Instant,
331}
332
333#[derive(Debug, Clone)]
335pub struct RetentionPerformanceFeedback<A: Float + Send + Sync> {
336 pub items_retained: usize,
338 pub items_discarded: usize,
340 pub retained_quality: A,
342 pub discarded_quality: A,
344 pub performance_impact: A,
346 pub timestamp: Instant,
348}
349
350#[derive(Debug, Clone)]
352pub struct BufferStatistics<A: Float + Send + Sync> {
353 pub total_items_processed: u64,
355 pub total_items_discarded: u64,
357 pub avg_buffer_utilization: A,
359 pub peak_buffer_utilization: A,
361 pub avg_processing_latency: Duration,
363 pub throughput_stats: ThroughputStatistics<A>,
365 pub quality_stats: QualityStatistics<A>,
367 pub memory_stats: MemoryStatistics,
369}
370
371#[derive(Debug, Clone)]
373pub struct ThroughputStatistics<A: Float + Send + Sync> {
374 pub current_throughput: A,
376 pub avg_throughput: A,
378 pub peak_throughput: A,
380 pub throughput_trend: TrendDirection,
382 pub stability: A,
384}
385
386#[derive(Debug, Clone)]
388pub struct QualityStatistics<A: Float + Send + Sync> {
389 pub current_avg_quality: A,
391 pub historical_avg_quality: A,
393 pub quality_improvement_rate: A,
395 pub quality_distribution: HashMap<String, A>,
397 pub predicted_quality: Option<A>,
399}
400
401#[derive(Debug, Clone)]
403pub struct MemoryStatistics {
404 pub current_usage_bytes: usize,
406 pub peak_usage_bytes: usize,
408 pub avg_usage_bytes: usize,
410 pub memory_efficiency: f64,
412 pub fragmentation: f64,
414}
415
416#[derive(Debug, Clone)]
418pub struct SizeChangeEvent {
419 pub timestamp: Instant,
421 pub old_size: usize,
423 pub new_size: usize,
425 pub change_magnitude: i32,
427 pub reason: String,
429}
430
431impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum + std::fmt::Debug>
432 AdaptiveBuffer<A>
433{
434 pub fn new(config: &StreamingConfig) -> Result<Self, String> {
436 let buffer_config = config.buffer_config.clone();
437
438 let quality_metrics = BufferQualityMetrics {
439 average_quality: A::zero(),
440 quality_variance: A::zero(),
441 min_quality: A::one(),
442 max_quality: A::zero(),
443 freshness_distribution: Vec::new(),
444 priority_distribution: Vec::new(),
445 quality_trend: QualityTrend {
446 recent_changes: VecDeque::with_capacity(50),
447 trend_direction: TrendDirection::Stable,
448 trend_magnitude: A::zero(),
449 confidence: A::zero(),
450 },
451 };
452
453 let sizing_strategy = BufferSizingStrategy::new(
454 buffer_config.size_strategy.clone(),
455 buffer_config.initial_size,
456 );
457
458 let statistics = BufferStatistics {
459 total_items_processed: 0,
460 total_items_discarded: 0,
461 avg_buffer_utilization: A::zero(),
462 peak_buffer_utilization: A::zero(),
463 avg_processing_latency: Duration::ZERO,
464 throughput_stats: ThroughputStatistics {
465 current_throughput: A::zero(),
466 avg_throughput: A::zero(),
467 peak_throughput: A::zero(),
468 throughput_trend: TrendDirection::Stable,
469 stability: A::zero(),
470 },
471 quality_stats: QualityStatistics {
472 current_avg_quality: A::zero(),
473 historical_avg_quality: A::zero(),
474 quality_improvement_rate: A::zero(),
475 quality_distribution: HashMap::new(),
476 predicted_quality: None,
477 },
478 memory_stats: MemoryStatistics {
479 current_usage_bytes: 0,
480 peak_usage_bytes: 0,
481 avg_usage_bytes: 0,
482 memory_efficiency: 0.0,
483 fragmentation: 0.0,
484 },
485 };
486
487 Ok(Self {
488 config: buffer_config,
489 buffer: BinaryHeap::new(),
490 secondary_buffer: VecDeque::new(),
491 quality_metrics,
492 sizing_strategy,
493 statistics,
494 last_processing: Instant::now(),
495 size_change_log: VecDeque::with_capacity(100),
496 feature_statistics: RunningFeatureStatistics::default(),
497 })
498 }
499
500 pub fn add_batch(&mut self, batch: Vec<StreamingDataPoint<A>>) -> Result<(), String> {
502 for data_point in batch {
503 self.add_single_point(data_point)?;
504 }
505
506 self.update_quality_metrics()?;
508
509 self.check_buffer_resizing()?;
511
512 if self.current_size() > self.sizing_strategy.target_size {
514 self.apply_retention_policy()?;
515 }
516
517 Ok(())
518 }
519
520 fn add_single_point(&mut self, data_point: StreamingDataPoint<A>) -> Result<(), String> {
522 let priority_score = self.calculate_priority_score(&data_point)?;
524
525 let freshness_score = self.calculate_freshness_score(&data_point);
529 let relevance_score = self.calculate_relevance_score(&data_point)?;
530 self.update_feature_statistics(&data_point);
531
532 let prioritized_point = PrioritizedDataPoint {
533 data_point,
534 priority_score,
535 buffer_timestamp: Instant::now(),
536 expected_processing_time: self.estimated_processing_time(),
540 freshness_score,
541 relevance_score,
542 };
543
544 if priority_score >= try_scalar_str::<A, _>(self.config.quality_threshold)? {
546 self.buffer.push(prioritized_point);
547 } else {
548 self.secondary_buffer
550 .push_back(prioritized_point.data_point);
551 }
552
553 self.statistics.total_items_processed += 1;
555
556 Ok(())
557 }
558
559 fn calculate_priority_score(&self, data_point: &StreamingDataPoint<A>) -> Result<A, String> {
561 let mut score = data_point.quality_score;
562
563 let age = data_point.timestamp.elapsed().as_secs_f64();
565 let recency_bonus = try_scalar_str::<A, _>(1.0 / (1.0 + age / 3600.0))?; score = score + recency_bonus * try_scalar_str::<A, _>(0.1)?;
567
568 let novelty_score = self.calculate_novelty_score(data_point)?;
570 score = score + novelty_score * try_scalar_str::<A, _>(0.2)?;
571
572 Ok(score)
573 }
574
575 fn calculate_novelty_score(&self, data_point: &StreamingDataPoint<A>) -> Result<A, String> {
577 if self.buffer.is_empty() {
579 return try_scalar_str::<A, _>(0.5); }
581
582 let recent_points: Vec<_> = self.buffer.iter().take(10).collect();
584 if recent_points.is_empty() {
585 return try_scalar_str::<A, _>(0.5);
586 }
587
588 let mut total_distance = A::zero();
589 for recent_point in &recent_points {
590 let distance = self.calculate_feature_distance(
591 &data_point.features,
592 &recent_point.data_point.features,
593 )?;
594 total_distance = total_distance + distance;
595 }
596
597 let avg_distance = total_distance / try_scalar_str::<A, _>(recent_points.len())?;
598
599 let normalized_novelty = avg_distance / (avg_distance + A::one());
601 Ok(normalized_novelty)
602 }
603
604 fn calculate_feature_distance(
606 &self,
607 features1: &scirs2_core::ndarray::Array1<A>,
608 features2: &scirs2_core::ndarray::Array1<A>,
609 ) -> Result<A, String> {
610 if features1.len() != features2.len() {
611 return Err("Feature vectors have different lengths".to_string());
612 }
613
614 let mut distance = A::zero();
615 for (f1, f2) in features1.iter().zip(features2.iter()) {
616 let diff = *f1 - *f2;
617 distance = distance + diff * diff;
618 }
619
620 Ok(distance.sqrt())
621 }
622
623 fn calculate_freshness_score(&self, data_point: &StreamingDataPoint<A>) -> A {
625 let age_seconds = data_point.timestamp.elapsed().as_secs_f64();
626 let max_age = 3600.0; let freshness = (max_age - age_seconds.min(max_age)) / max_age;
629 scalar_or(freshness.max(0.0), A::zero())
630 }
631
632 fn calculate_relevance_score(&self, data_point: &StreamingDataPoint<A>) -> Result<A, String> {
653 let supervision_bonus = if data_point.target.is_some() {
654 A::from(0.2).ok_or_else(|| "0.2 is not representable".to_string())?
655 } else {
656 A::zero()
657 };
658
659 let statistics = &self.feature_statistics;
660 if statistics.sample_count < 2 || statistics.means.is_empty() {
661 return Ok((data_point.quality_score + supervision_bonus).min(A::one()));
662 }
663
664 let count = A::from(statistics.sample_count)
665 .ok_or_else(|| "sample count is not representable".to_string())?;
666 let mut squared_z_total = A::zero();
667 let mut compared = 0usize;
668 for (index, &value) in data_point.features.iter().enumerate() {
669 let Some(&mean) = statistics.means.get(index) else {
670 continue;
671 };
672 let Some(&m2) = statistics.m2.get(index) else {
673 continue;
674 };
675 let variance = m2 / count;
676 if variance <= A::zero() {
677 continue;
678 }
679 let z = (value - mean) / variance.sqrt();
680 squared_z_total = squared_z_total + z * z;
681 compared += 1;
682 }
683
684 if compared == 0 {
685 return Ok((data_point.quality_score + supervision_bonus).min(A::one()));
686 }
687
688 let compared_count =
689 A::from(compared).ok_or_else(|| "compared count is not representable".to_string())?;
690 let rms_z = (squared_z_total / compared_count).sqrt();
692 let typicality = A::one() / (A::one() + rms_z);
695
696 Ok((typicality + supervision_bonus).min(A::one()))
697 }
698
699 fn update_feature_statistics(&mut self, data_point: &StreamingDataPoint<A>) {
702 let statistics = &mut self.feature_statistics;
703 if statistics.means.len() < data_point.features.len() {
704 statistics
705 .means
706 .resize(data_point.features.len(), A::zero());
707 statistics.m2.resize(data_point.features.len(), A::zero());
708 }
709 statistics.sample_count = statistics.sample_count.saturating_add(1);
710 let Some(count) = A::from(statistics.sample_count) else {
711 return;
712 };
713
714 for (index, &value) in data_point.features.iter().enumerate() {
716 if value.is_nan() {
717 continue;
718 }
719 let mean = statistics.means[index];
720 let delta = value - mean;
721 let new_mean = mean + delta / count;
722 statistics.means[index] = new_mean;
723 statistics.m2[index] = statistics.m2[index] + delta * (value - new_mean);
724 }
725 }
726
727 pub fn feature_statistics_sample_count(&self) -> usize {
729 self.feature_statistics.sample_count
730 }
731
732 fn estimated_processing_time(&self) -> Duration {
737 if self.statistics.avg_processing_latency > Duration::ZERO {
738 return self.statistics.avg_processing_latency;
739 }
740 let throughput = self
741 .statistics
742 .throughput_stats
743 .avg_throughput
744 .to_f64()
745 .unwrap_or(0.0);
746 if throughput > 0.0 {
747 return Duration::from_secs_f64(1.0 / throughput);
748 }
749 DEFAULT_EXPECTED_PROCESSING_TIME
750 }
751
752 pub fn average_processing_latency(&self) -> Duration {
754 self.statistics.avg_processing_latency
755 }
756
757 pub fn record_processing_duration(&mut self, duration: Duration) {
766 let previous = self.statistics.avg_processing_latency;
767 self.statistics.avg_processing_latency = if previous == Duration::ZERO {
768 duration
769 } else {
770 let smoothed = LATENCY_SMOOTHING * duration.as_secs_f64()
773 + (1.0 - LATENCY_SMOOTHING) * previous.as_secs_f64();
774 Duration::from_secs_f64(smoothed.max(0.0))
775 };
776 }
777
778 pub fn get_batch_for_processing(&mut self) -> Result<Vec<StreamingDataPoint<A>>, String> {
780 let batch_size = self.calculate_optimal_batch_size()?;
781 let mut processing_batch = Vec::with_capacity(batch_size);
782
783 while processing_batch.len() < batch_size && !self.buffer.is_empty() {
785 if let Some(prioritized_point) = self.buffer.pop() {
786 processing_batch.push(prioritized_point.data_point);
787 }
788 }
789
790 while processing_batch.len() < batch_size && !self.secondary_buffer.is_empty() {
792 if let Some(data_point) = self.secondary_buffer.pop_front() {
793 processing_batch.push(data_point);
794 }
795 }
796
797 self.last_processing = Instant::now();
799
800 self.update_throughput_stats(processing_batch.len())?;
802
803 Ok(processing_batch)
804 }
805
806 fn calculate_optimal_batch_size(&self) -> Result<usize, String> {
808 let mut batch_size = self.config.initial_size.min(32); let buffer_utilization =
812 self.current_size() as f64 / self.sizing_strategy.target_size as f64;
813 if buffer_utilization > 0.8 {
814 batch_size = (batch_size as f64 * 1.5) as usize; } else if buffer_utilization < 0.3 {
816 batch_size = (batch_size as f64 * 0.7) as usize; }
818
819 if self.statistics.avg_processing_latency > Duration::from_millis(500) {
821 batch_size = (batch_size as f64 * 0.8) as usize; }
823
824 Ok(batch_size.max(1).min(self.current_size().min(100)))
826 }
827
828 fn update_quality_metrics(&mut self) -> Result<(), String> {
830 if self.buffer.is_empty() && self.secondary_buffer.is_empty() {
831 return Ok(());
832 }
833
834 let mut quality_sum = A::zero();
835 let mut quality_values = Vec::new();
836
837 for prioritized_point in &self.buffer {
839 let quality = prioritized_point.data_point.quality_score;
840 quality_sum = quality_sum + quality;
841 quality_values.push(quality);
842 }
843
844 for data_point in &self.secondary_buffer {
846 let quality = data_point.quality_score;
847 quality_sum = quality_sum + quality;
848 quality_values.push(quality);
849 }
850
851 if !quality_values.is_empty() {
852 let count = try_scalar_str::<A, _>(quality_values.len())?;
853 self.quality_metrics.average_quality = quality_sum / count;
854
855 self.quality_metrics.min_quality =
857 quality_values.iter().cloned().fold(A::one(), A::min);
858 self.quality_metrics.max_quality =
859 quality_values.iter().cloned().fold(A::zero(), A::max);
860
861 let mean = self.quality_metrics.average_quality;
863 let variance_sum = quality_values
864 .iter()
865 .map(|&q| (q - mean) * (q - mean))
866 .sum::<A>();
867 self.quality_metrics.quality_variance = variance_sum / count;
868
869 self.update_quality_trend(self.quality_metrics.average_quality)?;
871 }
872
873 Ok(())
874 }
875
876 fn update_quality_trend(&mut self, current_quality: A) -> Result<(), String> {
878 let trend = &mut self.quality_metrics.quality_trend;
879
880 if trend.recent_changes.len() >= 50 {
882 trend.recent_changes.pop_front();
883 }
884 trend.recent_changes.push_back(current_quality);
885
886 if trend.recent_changes.len() >= 10 {
888 let recent: Vec<A> = trend.recent_changes.iter().cloned().collect();
889 let half = recent.len() / 2;
890 let first_count =
891 A::from(half).ok_or_else(|| "half-window size is not representable".to_string())?;
892 let second_count = A::from(recent.len() - half)
893 .ok_or_else(|| "half-window size is not representable".to_string())?;
894 let first_half_avg = recent.iter().take(half).cloned().sum::<A>() / first_count;
895 let second_half_avg = recent.iter().skip(half).cloned().sum::<A>() / second_count;
896
897 let change = second_half_avg - first_half_avg;
898 let change_threshold =
899 A::from(0.05).ok_or_else(|| "0.05 is not representable".to_string())?; trend.trend_direction = if change > change_threshold {
902 TrendDirection::Improving
903 } else if change < -change_threshold {
904 TrendDirection::Degrading
905 } else {
906 TrendDirection::Stable
907 };
908
909 trend.trend_magnitude = change.abs();
910
911 let variance_of = |values: &[A], count: A, mean: A| -> A {
918 if values.len() < 2 {
919 return A::zero();
920 }
921 let denominator = count - A::one();
922 if denominator <= A::zero() {
923 return A::zero();
924 }
925 values
926 .iter()
927 .fold(A::zero(), |acc, &v| acc + (v - mean) * (v - mean))
928 / denominator
929 };
930 let first_slice = &recent[..half];
931 let second_slice = &recent[half..];
932 let first_variance = variance_of(first_slice, first_count, first_half_avg);
933 let second_variance = variance_of(second_slice, second_count, second_half_avg);
934 let standard_error =
935 (first_variance / first_count + second_variance / second_count).sqrt();
936
937 trend.confidence = if standard_error > A::zero() {
938 let t_statistic = (change / standard_error).abs();
939 t_statistic / (A::one() + t_statistic)
940 } else if change.abs() > A::zero() {
941 A::one()
944 } else {
945 A::zero()
946 };
947 }
948
949 Ok(())
950 }
951
952 fn check_buffer_resizing(&mut self) -> Result<(), String> {
954 if !self.config.enable_adaptive_sizing {
955 return Ok(());
956 }
957
958 let current_size = self.current_size();
959 let target_size = self.sizing_strategy.target_size;
960 let utilization = current_size as f64 / target_size as f64;
961
962 let should_resize = if utilization > 0.9 {
964 Some(SizingReason::ThroughputOptimization)
966 } else if utilization < 0.3 && target_size > self.config.min_size {
967 Some(SizingReason::MemoryPressure)
969 } else {
970 None
971 };
972
973 if let Some(reason) = should_resize {
974 self.resize_buffer(reason)?;
975 }
976
977 Ok(())
978 }
979
980 fn resize_buffer(&mut self, reason: SizingReason) -> Result<(), String> {
982 let old_size = self.sizing_strategy.target_size;
983
984 let growing = matches!(reason, SizingReason::ThroughputOptimization);
988 let shrinking = matches!(reason, SizingReason::MemoryPressure);
989 if !growing && !shrinking {
990 return Ok(()); }
992
993 let new_size = match &self.sizing_strategy.strategy_type {
994 BufferSizeStrategy::Fixed => return Ok(()),
996 BufferSizeStrategy::Linear { growth_rate } => {
998 let step = ((self.sizing_strategy.initial_size as f64) * growth_rate.abs())
999 .round()
1000 .max(1.0) as usize;
1001 if growing {
1002 old_size.saturating_add(step)
1003 } else {
1004 old_size.saturating_sub(step)
1005 }
1006 }
1007 BufferSizeStrategy::Exponential { base } => {
1009 let base = if *base > 1.0 { *base } else { 2.0 };
1010 if growing {
1011 ((old_size as f64) * base) as usize
1012 } else {
1013 ((old_size as f64) / base) as usize
1014 }
1015 }
1016 BufferSizeStrategy::Adaptive | BufferSizeStrategy::ResourceBased => {
1020 if growing {
1021 let growth_factor = 1.0
1022 + self
1023 .sizing_strategy
1024 .adjustment_params
1025 .growth_rate
1026 .to_f64()
1027 .unwrap_or(0.2);
1028 ((old_size as f64) * growth_factor) as usize
1029 } else {
1030 let shrink_factor = 1.0
1031 - self
1032 .sizing_strategy
1033 .adjustment_params
1034 .shrinkage_rate
1035 .to_f64()
1036 .unwrap_or(0.2);
1037 ((old_size as f64) * shrink_factor) as usize
1038 }
1039 }
1040 };
1041
1042 let bounded_size = self.bound_target_size(new_size);
1044
1045 if bounded_size != old_size {
1046 self.sizing_strategy.target_size = bounded_size;
1047
1048 let change_event = SizeChangeEvent {
1050 timestamp: Instant::now(),
1051 old_size,
1052 new_size: bounded_size,
1053 change_magnitude: bounded_size as i32 - old_size as i32,
1054 reason: format!("{:?}", reason),
1055 };
1056
1057 if self.size_change_log.len() >= 100 {
1058 self.size_change_log.pop_front();
1059 }
1060 self.size_change_log.push_back(change_event);
1061 }
1062
1063 Ok(())
1064 }
1065
1066 fn apply_retention_policy(&mut self) -> Result<(), String> {
1068 let target_size = self.sizing_strategy.target_size;
1069 let current_size = self.current_size();
1070
1071 if current_size <= target_size {
1072 return Ok(());
1073 }
1074
1075 let items_to_remove = current_size - target_size;
1076 let mut removed_count = 0;
1077
1078 while removed_count < items_to_remove && !self.secondary_buffer.is_empty() {
1080 if self.should_remove_from_secondary()? {
1081 self.secondary_buffer.pop_front();
1082 removed_count += 1;
1083 self.statistics.total_items_discarded += 1;
1084 } else {
1085 break;
1086 }
1087 }
1088
1089 let mut temp_buffer = Vec::new();
1091 while let Some(item) = self.buffer.pop() {
1092 temp_buffer.push(item);
1093 }
1094
1095 temp_buffer.sort_by(|a, b| {
1097 let score_a = self
1098 .calculate_retention_score(&a.data_point)
1099 .unwrap_or(A::zero());
1100 let score_b = self
1101 .calculate_retention_score(&b.data_point)
1102 .unwrap_or(A::zero());
1103 score_b.partial_cmp(&score_a).unwrap_or(Ordering::Equal)
1104 });
1105
1106 let items_to_keep = (temp_buffer.len()).saturating_sub(items_to_remove - removed_count);
1108 for item in temp_buffer.into_iter().take(items_to_keep) {
1109 self.buffer.push(item);
1110 }
1111
1112 Ok(())
1113 }
1114
1115 fn should_remove_from_secondary(&self) -> Result<bool, String> {
1117 if let Some(oldest) = self.secondary_buffer.front() {
1119 let age = oldest.timestamp.elapsed();
1120 Ok(age > Duration::from_secs(3600)) } else {
1122 Ok(false)
1123 }
1124 }
1125
1126 fn calculate_retention_score(&self, data_point: &StreamingDataPoint<A>) -> Result<A, String> {
1128 let age_score = self.calculate_age_score(data_point);
1129 let quality_score = data_point.quality_score;
1130 let freshness_score = self.calculate_freshness_score(data_point);
1131
1132 let retention_score = quality_score * try_scalar_str::<A, _>(0.5)?
1134 + freshness_score * try_scalar_str::<A, _>(0.3)?
1135 + age_score * try_scalar_str::<A, _>(0.2)?;
1136
1137 Ok(retention_score)
1138 }
1139
1140 fn calculate_age_score(&self, data_point: &StreamingDataPoint<A>) -> A {
1142 let age_seconds = data_point.timestamp.elapsed().as_secs_f64();
1143 let max_age = 7200.0; let age_score = (max_age - age_seconds.min(max_age)) / max_age;
1146 scalar_or(age_score.max(0.0), A::zero())
1147 }
1148
1149 fn update_throughput_stats(&mut self, items_processed: usize) -> Result<(), String> {
1151 let time_since_last = self.last_processing.elapsed().as_secs_f64();
1152 if time_since_last > 0.0 {
1153 let current_throughput = items_processed as f64 / time_since_last;
1154 let throughput_value = try_scalar_str::<A, _>(current_throughput)?;
1155
1156 self.statistics.throughput_stats.current_throughput = throughput_value;
1157
1158 let alpha = try_scalar_str::<A, _>(0.1)?; self.statistics.throughput_stats.avg_throughput = alpha * throughput_value
1161 + (A::one() - alpha) * self.statistics.throughput_stats.avg_throughput;
1162
1163 self.statistics.throughput_stats.peak_throughput = self
1165 .statistics
1166 .throughput_stats
1167 .peak_throughput
1168 .max(throughput_value);
1169 }
1170
1171 Ok(())
1172 }
1173
1174 pub fn current_size(&self) -> usize {
1176 self.buffer.len() + self.secondary_buffer.len()
1177 }
1178
1179 pub fn time_since_last_processing(&self) -> Duration {
1181 self.last_processing.elapsed()
1182 }
1183
1184 pub fn get_quality_metrics(&self) -> BufferQualityMetrics<A> {
1186 self.quality_metrics.clone()
1187 }
1188
1189 pub fn compute_size_adaptation(
1191 &self,
1192 performance_tracker: &PerformanceTracker<A>,
1193 ) -> Result<Option<Adaptation<A>>, String> {
1194 let recent_performance = performance_tracker.get_recent_performance(10);
1196 if recent_performance.is_empty() {
1197 return Ok(None);
1198 }
1199
1200 let avg_processing_time = recent_performance
1209 .iter()
1210 .map(|p| p.processing_duration.as_secs_f64() * 1000.0)
1211 .sum::<f64>()
1212 / recent_performance.len() as f64;
1213
1214 if avg_processing_time > 1000.0 {
1216 let adaptation = Adaptation {
1218 adaptation_type: AdaptationType::BufferSize,
1219 magnitude: try_scalar_str::<A, _>(-0.2)?, target_component: "adaptive_buffer".to_string(),
1221 parameters: std::collections::HashMap::new(),
1222 priority: AdaptationPriority::Normal,
1223 timestamp: Instant::now(),
1224 };
1225 return Ok(Some(adaptation));
1226 }
1227
1228 let avg_utilization = self.current_size() as f64 / self.sizing_strategy.target_size as f64;
1230 if avg_processing_time < 100.0 && avg_utilization < 0.3 {
1231 let adaptation = Adaptation {
1232 adaptation_type: AdaptationType::BufferSize,
1233 magnitude: try_scalar_str::<A, _>(0.3)?, target_component: "adaptive_buffer".to_string(),
1235 parameters: std::collections::HashMap::new(),
1236 priority: AdaptationPriority::Low,
1237 timestamp: Instant::now(),
1238 };
1239 return Ok(Some(adaptation));
1240 }
1241
1242 Ok(None)
1243 }
1244
1245 fn memory_bounded_capacity(&self) -> Option<usize> {
1258 let feature_dim = self.feature_statistics.means.len();
1259 if feature_dim == 0 {
1260 return None;
1261 }
1262 let per_item = std::mem::size_of::<PrioritizedDataPoint<A>>()
1264 + 2 * feature_dim * std::mem::size_of::<A>();
1265 if per_item == 0 {
1266 return None;
1267 }
1268 let budget_bytes = self.config.memory_limit_mb.saturating_mul(1024 * 1024);
1269 Some((budget_bytes / per_item).max(1))
1270 }
1271
1272 fn bound_target_size(&self, proposed: usize) -> usize {
1275 let bounded = proposed.max(self.config.min_size).min(self.config.max_size);
1276 match self.memory_bounded_capacity() {
1277 Some(capacity) => bounded.min(capacity).max(1),
1278 None => bounded,
1279 }
1280 }
1281
1282 #[cfg(test)]
1284 pub(crate) fn memory_bounded_capacity_for_test(&self) -> Option<usize> {
1285 self.memory_bounded_capacity()
1286 }
1287
1288 #[cfg(test)]
1290 pub(crate) fn bound_target_size_for_test(&self, proposed: usize) -> usize {
1291 self.bound_target_size(proposed)
1292 }
1293
1294 pub fn apply_size_adaptation(&mut self, adaptation: &Adaptation<A>) -> Result<(), String> {
1296 if adaptation.adaptation_type == AdaptationType::BufferSize {
1297 let current_target = self.sizing_strategy.target_size;
1298 let change_factor = A::one() + adaptation.magnitude;
1299 let new_target =
1300 (current_target as f64 * change_factor.to_f64().unwrap_or(1.0)) as usize;
1301
1302 let bounded_target = self.bound_target_size(new_target);
1304
1305 if bounded_target != current_target {
1306 self.sizing_strategy.target_size = bounded_target;
1307
1308 let change_event = SizeChangeEvent {
1310 timestamp: Instant::now(),
1311 old_size: current_target,
1312 new_size: bounded_target,
1313 change_magnitude: bounded_target as i32 - current_target as i32,
1314 reason: "adaptation".to_string(),
1315 };
1316
1317 if self.size_change_log.len() >= 100 {
1318 self.size_change_log.pop_front();
1319 }
1320 self.size_change_log.push_back(change_event);
1321 }
1322 }
1323
1324 Ok(())
1325 }
1326
1327 pub fn last_size_change(&self) -> f32 {
1329 if let Some(last_change) = self.size_change_log.back() {
1330 last_change.change_magnitude as f32
1331 } else {
1332 0.0
1333 }
1334 }
1335
1336 pub fn reset(&mut self) -> Result<(), String> {
1338 self.buffer.clear();
1339 self.secondary_buffer.clear();
1340
1341 self.quality_metrics = BufferQualityMetrics {
1342 average_quality: A::zero(),
1343 quality_variance: A::zero(),
1344 min_quality: A::one(),
1345 max_quality: A::zero(),
1346 freshness_distribution: Vec::new(),
1347 priority_distribution: Vec::new(),
1348 quality_trend: QualityTrend {
1349 recent_changes: VecDeque::with_capacity(50),
1350 trend_direction: TrendDirection::Stable,
1351 trend_magnitude: A::zero(),
1352 confidence: A::zero(),
1353 },
1354 };
1355
1356 self.statistics.total_items_processed = 0;
1357 self.statistics.total_items_discarded = 0;
1358 self.last_processing = Instant::now();
1359 self.size_change_log.clear();
1360
1361 Ok(())
1362 }
1363
1364 pub fn get_diagnostics(&self) -> BufferDiagnostics {
1366 BufferDiagnostics {
1367 current_size: self.current_size(),
1368 target_size: self.sizing_strategy.target_size,
1369 utilization: self.current_size() as f64 / self.sizing_strategy.target_size as f64,
1370 average_quality: self.quality_metrics.average_quality.to_f64().unwrap_or(0.0),
1371 total_processed: self.statistics.total_items_processed,
1372 total_discarded: self.statistics.total_items_discarded,
1373 size_changes: self.size_change_log.len(),
1374 }
1375 }
1376}
1377
1378impl<A: Float + Send + Sync + Send + Sync> Ord for PrioritizedDataPoint<A> {
1380 fn cmp(&self, other: &Self) -> Ordering {
1381 self.priority_score
1382 .partial_cmp(&other.priority_score)
1383 .unwrap_or(Ordering::Equal)
1384 }
1385}
1386
1387impl<A: Float + Send + Sync + Send + Sync> PartialOrd for PrioritizedDataPoint<A> {
1388 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1389 Some(self.cmp(other))
1390 }
1391}
1392
1393impl<A: Float + Send + Sync + Send + Sync> PartialEq for PrioritizedDataPoint<A> {
1394 fn eq(&self, other: &Self) -> bool {
1395 self.priority_score == other.priority_score
1396 }
1397}
1398
1399impl<A: Float + Send + Sync + Send + Sync> Eq for PrioritizedDataPoint<A> {}
1400
1401impl<A: Float + Send + Sync + Send + Sync> BufferSizingStrategy<A> {
1402 fn new(strategy_type: BufferSizeStrategy, initial_size: usize) -> Self {
1403 Self {
1404 strategy_type,
1405 initial_size,
1406 target_size: initial_size,
1407 adjustment_params: SizeAdjustmentParams {
1408 growth_rate: scalar_or(0.2, A::zero()),
1409 shrinkage_rate: scalar_or(0.15, A::zero()),
1410 stability_threshold: scalar_or(0.05, A::zero()),
1411 performance_sensitivity: scalar_or(0.1, A::zero()),
1412 quality_sensitivity: scalar_or(0.1, A::zero()),
1413 memory_sensitivity: scalar_or(0.2, A::zero()),
1414 },
1415 }
1416 }
1417}
1418
1419#[derive(Debug, Clone)]
1421pub struct BufferDiagnostics {
1422 pub current_size: usize,
1423 pub target_size: usize,
1424 pub utilization: f64,
1425 pub average_quality: f64,
1426 pub total_processed: u64,
1427 pub total_discarded: u64,
1428 pub size_changes: usize,
1429}
1430
1431#[cfg(test)]
1432mod buffering_regression_tests {
1433 use super::super::performance::{DataStatistics, PerformanceSnapshot};
1434 use super::super::resource_management::ResourceUsage;
1435 use super::*;
1436 use scirs2_core::ndarray::Array1;
1437
1438 fn point(features: Vec<f64>, target: Option<f64>) -> StreamingDataPoint<f64> {
1439 StreamingDataPoint {
1440 features: Array1::from_vec(features),
1441 target: target.map(|t| Array1::from_vec(vec![t])),
1442 timestamp: Instant::now(),
1443 source_id: None,
1444 quality_score: 1.0,
1445 metadata: HashMap::new(),
1446 }
1447 }
1448
1449 fn buffer() -> AdaptiveBuffer<f64> {
1450 AdaptiveBuffer::new(&StreamingConfig::default()).expect("buffer")
1451 }
1452
1453 fn snapshot_with(processing: Duration) -> PerformanceSnapshot<f64> {
1454 PerformanceSnapshot {
1455 timestamp: Instant::now(),
1456 processing_duration: processing,
1457 loss: 1.0,
1458 accuracy: None,
1459 convergence_rate: None,
1460 gradient_norm: None,
1461 parameter_update_magnitude: None,
1462 data_statistics: DataStatistics::default(),
1463 resource_usage: ResourceUsage::default(),
1464 custom_metrics: HashMap::new(),
1465 }
1466 }
1467
1468 #[test]
1473 fn relevance_score_discriminates_typical_from_atypical_points() {
1474 let mut buffer = buffer();
1475
1476 for i in 0..40 {
1478 let value = 10.0 + 0.1 * ((i % 5) as f64 - 2.0);
1479 buffer.update_feature_statistics(&point(vec![value], None));
1480 }
1481 assert_eq!(buffer.feature_statistics_sample_count(), 40);
1482
1483 let typical = buffer
1484 .calculate_relevance_score(&point(vec![10.0], None))
1485 .expect("typical");
1486 let atypical = buffer
1487 .calculate_relevance_score(&point(vec![10_000.0], None))
1488 .expect("atypical");
1489
1490 assert!(
1491 typical > atypical,
1492 "B1 regression: relevance did not discriminate \
1493 (typical={typical}, atypical={atypical})"
1494 );
1495 assert!(
1496 (typical - 0.7).abs() > 1e-9 || (atypical - 0.7).abs() > 1e-9,
1497 "B1 regression: both scores are still the hard-coded 0.7"
1498 );
1499 assert!(
1500 atypical < 0.1,
1501 "a 100-sigma point should score near zero relevance, got {atypical}"
1502 );
1503 }
1504
1505 #[test]
1508 fn relevance_score_rewards_labelled_points() {
1509 let mut buffer = buffer();
1510 for i in 0..40 {
1511 let value = 10.0 + 0.1 * ((i % 5) as f64 - 2.0);
1512 buffer.update_feature_statistics(&point(vec![value], None));
1513 }
1514
1515 let unlabelled = buffer
1517 .calculate_relevance_score(&point(vec![10.6], None))
1518 .expect("unlabelled");
1519 let labelled = buffer
1520 .calculate_relevance_score(&point(vec![10.6], Some(1.0)))
1521 .expect("labelled");
1522 assert!(
1523 labelled > unlabelled,
1524 "a labelled point must be at least as relevant \
1525 (labelled={labelled}, unlabelled={unlabelled})"
1526 );
1527 }
1528
1529 #[test]
1536 fn size_adaptation_uses_measured_processing_time_not_snapshot_age() {
1537 let config = StreamingConfig::default();
1538 let mut tracker = PerformanceTracker::<f64>::new(&config).expect("tracker");
1539 for _ in 0..10 {
1540 tracker
1541 .add_performance(snapshot_with(Duration::from_millis(1)))
1542 .expect("add_performance");
1543 }
1544 std::thread::sleep(Duration::from_millis(60));
1546
1547 let mut buffer = buffer();
1548 for i in 0..200 {
1550 buffer
1551 .add_batch(vec![point(vec![i as f64], None)])
1552 .expect("add_batch");
1553 }
1554
1555 let adaptation = buffer
1556 .compute_size_adaptation(&tracker)
1557 .expect("compute_size_adaptation");
1558 if let Some(adaptation) = adaptation {
1559 assert!(
1560 adaptation.magnitude > 0.0,
1561 "B2 regression: a 1ms-per-batch workload produced a shrink \
1562 request (magnitude={}), which can only come from reading \
1563 snapshot age as processing time",
1564 adaptation.magnitude
1565 );
1566 }
1567 }
1568
1569 #[test]
1572 fn size_adaptation_still_shrinks_for_genuinely_slow_processing() {
1573 let config = StreamingConfig::default();
1574 let mut tracker = PerformanceTracker::<f64>::new(&config).expect("tracker");
1575 for _ in 0..10 {
1576 tracker
1577 .add_performance(snapshot_with(Duration::from_millis(1500)))
1578 .expect("add_performance");
1579 }
1580
1581 let buffer = buffer();
1582 let adaptation = buffer
1583 .compute_size_adaptation(&tracker)
1584 .expect("compute_size_adaptation")
1585 .expect("a 1.5s-per-batch workload must request a smaller buffer");
1586 assert!(
1587 adaptation.magnitude < 0.0,
1588 "expected a shrink request, got magnitude {}",
1589 adaptation.magnitude
1590 );
1591 }
1592
1593 #[test]
1597 fn processing_latency_is_recorded_and_shrinks_the_batch_size() {
1598 let mut buffer = buffer();
1599 assert_eq!(
1600 buffer.average_processing_latency(),
1601 Duration::ZERO,
1602 "no latency should be claimed before any measurement"
1603 );
1604
1605 for i in 0..200 {
1606 buffer
1607 .add_batch(vec![point(vec![i as f64], None)])
1608 .expect("add_batch");
1609 }
1610 let fast_batch = buffer
1611 .calculate_optimal_batch_size()
1612 .expect("calculate_optimal_batch_size");
1613
1614 for _ in 0..40 {
1616 buffer.record_processing_duration(Duration::from_millis(2000));
1617 }
1618 assert!(
1619 buffer.average_processing_latency() > Duration::from_millis(500),
1620 "B2 regression: recorded latency did not reach the statistics \
1621 (got {:?})",
1622 buffer.average_processing_latency()
1623 );
1624
1625 let slow_batch = buffer
1626 .calculate_optimal_batch_size()
1627 .expect("calculate_optimal_batch_size");
1628 assert!(
1629 slow_batch < fast_batch,
1630 "B2 regression: the slow-processing branch is still unreachable \
1631 ({fast_batch} -> {slow_batch})"
1632 );
1633 }
1634
1635 #[test]
1639 fn quality_trend_confidence_reflects_the_real_fit() {
1640 let mut clean = buffer();
1642 for i in 0..10 {
1643 let quality = if i < 5 { 0.2 } else { 0.9 };
1644 clean
1645 .update_quality_trend(quality)
1646 .expect("update_quality_trend");
1647 }
1648 let clean_confidence = clean.get_quality_metrics().quality_trend.confidence;
1649
1650 let mut noisy = buffer();
1652 for i in 0..10 {
1653 let quality = if i % 2 == 0 { 0.1 } else { 0.9 };
1654 noisy
1655 .update_quality_trend(quality)
1656 .expect("update_quality_trend");
1657 }
1658 let noisy_confidence = noisy.get_quality_metrics().quality_trend.confidence;
1659
1660 assert!(
1661 clean_confidence > noisy_confidence,
1662 "B3 regression: confidence did not distinguish a clean step from \
1663 noise (clean={clean_confidence}, noisy={noisy_confidence})"
1664 );
1665 assert!(
1666 (clean_confidence - 0.8).abs() > 1e-9 || (noisy_confidence - 0.8).abs() > 1e-9,
1667 "B3 regression: both confidences are still the hard-coded 0.8"
1668 );
1669 assert_eq!(
1670 clean.get_quality_metrics().quality_trend.trend_direction,
1671 TrendDirection::Improving
1672 );
1673 }
1674}