Skip to main content

optirs_core/streaming/adaptive_streaming/
buffering.rs

1// Adaptive buffering strategies for streaming optimization
2//
3// This module provides sophisticated buffer management including adaptive sizing,
4// quality-based filtering, priority queuing, and intelligent data retention
5// strategies for streaming optimization scenarios.
6
7use 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
17/// Adaptive buffer for managing streaming data with quality-based retention
18pub struct AdaptiveBuffer<A: Float + Send + Sync> {
19    /// Buffer configuration
20    config: BufferConfig,
21    /// Main data buffer with priority queue
22    buffer: BinaryHeap<PrioritizedDataPoint<A>>,
23    /// Secondary buffer for low-quality data
24    secondary_buffer: VecDeque<StreamingDataPoint<A>>,
25    /// Buffer quality metrics
26    quality_metrics: BufferQualityMetrics<A>,
27    /// Buffer sizing strategy
28    sizing_strategy: BufferSizingStrategy<A>,
29    /// Buffer statistics
30    statistics: BufferStatistics<A>,
31    /// Last processing timestamp
32    last_processing: Instant,
33    /// Size change tracking
34    size_change_log: VecDeque<SizeChangeEvent>,
35    /// Running per-feature statistics backing the relevance score.
36    feature_statistics: RunningFeatureStatistics<A>,
37}
38
39/// Per-item processing time assumed only until a real latency or throughput
40/// measurement exists.
41const DEFAULT_EXPECTED_PROCESSING_TIME: Duration = Duration::from_millis(100);
42
43/// Smoothing factor for the processing-latency moving average.
44const LATENCY_SMOOTHING: f64 = 0.1;
45
46/// Welford accumulators for the buffer's per-feature distribution.
47#[derive(Debug, Clone)]
48struct RunningFeatureStatistics<A: Float + Send + Sync> {
49    /// Running per-coordinate mean.
50    means: Vec<A>,
51    /// Running per-coordinate sum of squared deviations.
52    m2: Vec<A>,
53    /// Number of observations folded in.
54    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/// Data point with priority information for buffering
68#[derive(Debug, Clone)]
69pub struct PrioritizedDataPoint<A: Float + Send + Sync> {
70    /// The actual data point
71    pub data_point: StreamingDataPoint<A>,
72    /// Priority score (higher = more important)
73    pub priority_score: A,
74    /// Buffer insertion timestamp
75    pub buffer_timestamp: Instant,
76    /// Expected processing time
77    pub expected_processing_time: Duration,
78    /// Data freshness score
79    pub freshness_score: A,
80    /// Relevance score for current model
81    pub relevance_score: A,
82}
83
84/// Buffer quality metrics for adaptive management
85#[derive(Debug, Clone)]
86pub struct BufferQualityMetrics<A: Float + Send + Sync> {
87    /// Average quality score of buffered data
88    pub average_quality: A,
89    /// Quality variance
90    pub quality_variance: A,
91    /// Minimum quality in buffer
92    pub min_quality: A,
93    /// Maximum quality in buffer
94    pub max_quality: A,
95    /// Data freshness distribution
96    pub freshness_distribution: Vec<A>,
97    /// Priority distribution
98    pub priority_distribution: Vec<A>,
99    /// Quality trend over time
100    pub quality_trend: QualityTrend<A>,
101}
102
103/// Quality trend analysis
104#[derive(Debug, Clone)]
105pub struct QualityTrend<A: Float + Send + Sync> {
106    /// Recent quality changes
107    pub recent_changes: VecDeque<A>,
108    /// Trend direction
109    pub trend_direction: TrendDirection,
110    /// Trend magnitude
111    pub trend_magnitude: A,
112    /// Trend confidence
113    pub confidence: A,
114}
115
116/// Trend direction for quality analysis
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum TrendDirection {
119    /// Quality improving
120    Improving,
121    /// Quality degrading
122    Degrading,
123    /// Quality stable
124    Stable,
125    /// Quality oscillating
126    Oscillating,
127}
128
129/// Buffer sizing strategy implementation
130pub struct BufferSizingStrategy<A: Float + Send + Sync> {
131    /// The configured sizing strategy, which decides *how* `target_size`
132    /// moves (and, for `Fixed`, that it does not move at all).
133    strategy_type: BufferSizeStrategy,
134    /// Initial size, the base for `Linear` steps.
135    initial_size: usize,
136    /// Target size
137    target_size: usize,
138    /// Size adjustment parameters
139    adjustment_params: SizeAdjustmentParams<A>,
140}
141
142/// Parameters for size adjustment
143#[derive(Debug, Clone)]
144pub struct SizeAdjustmentParams<A: Float + Send + Sync> {
145    /// Growth rate for increasing buffer size
146    pub growth_rate: A,
147    /// Shrinkage rate for decreasing buffer size
148    pub shrinkage_rate: A,
149    /// Stability threshold (minimum change for adjustment)
150    pub stability_threshold: A,
151    /// Performance sensitivity
152    pub performance_sensitivity: A,
153    /// Quality sensitivity
154    pub quality_sensitivity: A,
155    /// Memory pressure sensitivity
156    pub memory_sensitivity: A,
157}
158
159/// Performance feedback for buffer sizing
160#[derive(Debug, Clone)]
161pub struct SizingPerformanceFeedback<A: Float + Send + Sync> {
162    /// Buffer size when feedback was recorded
163    pub buffer_size: usize,
164    /// Processing latency
165    pub processing_latency: Duration,
166    /// Throughput (items per second)
167    pub throughput: A,
168    /// Quality score achieved
169    pub quality_score: A,
170    /// Memory usage
171    pub memory_usage: usize,
172    /// Timestamp of feedback
173    pub timestamp: Instant,
174}
175
176/// Buffer sizing event
177#[derive(Debug, Clone)]
178pub struct SizingEvent {
179    /// Event timestamp
180    pub timestamp: Instant,
181    /// Old buffer size
182    pub old_size: usize,
183    /// New buffer size
184    pub new_size: usize,
185    /// Reason for size change
186    pub reason: SizingReason,
187    /// Performance impact
188    pub performance_impact: Option<f64>,
189}
190
191/// Reasons for buffer size changes
192#[derive(Debug, Clone)]
193pub enum SizingReason {
194    /// Performance optimization
195    PerformanceOptimization,
196    /// Quality improvement
197    QualityImprovement,
198    /// Memory pressure
199    MemoryPressure,
200    /// Latency requirements
201    LatencyRequirement,
202    /// Throughput optimization
203    ThroughputOptimization,
204    /// Manual adjustment
205    Manual,
206    /// Configuration change
207    Configuration,
208}
209
210/// Data retention strategies
211#[derive(Debug, Clone)]
212pub enum RetentionStrategy {
213    /// First In, First Out
214    FIFO,
215    /// Last In, First Out
216    LIFO,
217    /// Least Recently Used
218    LRU,
219    /// Priority-based retention
220    Priority,
221    /// Quality-based retention
222    Quality,
223    /// Age-based retention
224    Age,
225    /// Hybrid retention combining multiple factors
226    Hybrid,
227    /// Adaptive retention based on performance
228    Adaptive,
229}
230
231/// Age-based retention configuration
232#[derive(Debug, Clone)]
233pub struct AgeBasedRetention {
234    /// Maximum age for data retention
235    pub max_age: Duration,
236    /// Soft age limit (start considering for removal)
237    pub soft_age_limit: Duration,
238    /// Age weight in retention scoring
239    pub age_weight: f64,
240    /// Enable adaptive age limits
241    pub adaptive_limits: bool,
242}
243
244/// Quality-based retention configuration
245#[derive(Debug, Clone)]
246pub struct QualityBasedRetention<A: Float + Send + Sync> {
247    /// Minimum quality threshold
248    pub min_quality_threshold: A,
249    /// Quality weight in retention scoring
250    pub quality_weight: A,
251    /// Enable adaptive quality thresholds
252    pub adaptive_thresholds: bool,
253    /// Quality distribution targets
254    pub quality_targets: QualityDistributionTargets<A>,
255}
256
257/// Target quality distribution for buffer content
258#[derive(Debug, Clone)]
259pub struct QualityDistributionTargets<A: Float + Send + Sync> {
260    /// Target percentage of high-quality data
261    pub high_quality_target: A,
262    /// Target percentage of medium-quality data
263    pub medium_quality_target: A,
264    /// Target percentage of low-quality data
265    pub low_quality_target: A,
266    /// Quality boundaries
267    pub high_quality_threshold: A,
268    pub medium_quality_threshold: A,
269}
270
271/// Relevance-based retention configuration
272#[derive(Debug, Clone)]
273pub struct RelevanceBasedRetention<A: Float + Send + Sync> {
274    /// Relevance calculation method
275    pub relevance_method: RelevanceMethod,
276    /// Relevance weight in retention scoring
277    pub relevance_weight: A,
278    /// Enable temporal relevance decay
279    pub temporal_decay: bool,
280    /// Relevance decay rate
281    pub decay_rate: A,
282}
283
284/// Methods for calculating data relevance
285#[derive(Debug, Clone)]
286pub enum RelevanceMethod {
287    /// Distance-based relevance
288    Distance,
289    /// Similarity-based relevance
290    Similarity,
291    /// Feature importance-based relevance
292    FeatureImportance,
293    /// Model uncertainty-based relevance
294    Uncertainty,
295    /// Diversity-based relevance
296    Diversity,
297    /// Custom relevance function
298    Custom(String),
299}
300
301/// Weights for different retention factors
302#[derive(Debug, Clone)]
303pub struct RetentionWeights<A: Float + Send + Sync> {
304    /// Age weight
305    pub age_weight: A,
306    /// Quality weight
307    pub quality_weight: A,
308    /// Relevance weight
309    pub relevance_weight: A,
310    /// Priority weight
311    pub priority_weight: A,
312    /// Freshness weight
313    pub freshness_weight: A,
314    /// Diversity weight
315    pub diversity_weight: A,
316}
317
318/// Retention score for a data point
319#[derive(Debug, Clone)]
320pub struct RetentionScore<A: Float + Send + Sync> {
321    /// Overall retention score
322    pub overall_score: A,
323    /// Individual component scores
324    pub component_scores: HashMap<String, A>,
325    /// Retention decision
326    pub should_retain: bool,
327    /// Confidence in decision
328    pub confidence: A,
329    /// Scoring timestamp
330    pub timestamp: Instant,
331}
332
333/// Performance feedback for retention decisions
334#[derive(Debug, Clone)]
335pub struct RetentionPerformanceFeedback<A: Float + Send + Sync> {
336    /// Number of items retained
337    pub items_retained: usize,
338    /// Number of items discarded
339    pub items_discarded: usize,
340    /// Quality of retained items
341    pub retained_quality: A,
342    /// Quality of discarded items
343    pub discarded_quality: A,
344    /// Performance impact
345    pub performance_impact: A,
346    /// Feedback timestamp
347    pub timestamp: Instant,
348}
349
350/// Buffer statistics for monitoring and optimization
351#[derive(Debug, Clone)]
352pub struct BufferStatistics<A: Float + Send + Sync> {
353    /// Total items processed
354    pub total_items_processed: u64,
355    /// Total items discarded
356    pub total_items_discarded: u64,
357    /// Average buffer utilization
358    pub avg_buffer_utilization: A,
359    /// Peak buffer utilization
360    pub peak_buffer_utilization: A,
361    /// Average processing latency
362    pub avg_processing_latency: Duration,
363    /// Throughput statistics
364    pub throughput_stats: ThroughputStatistics<A>,
365    /// Quality statistics
366    pub quality_stats: QualityStatistics<A>,
367    /// Memory usage statistics
368    pub memory_stats: MemoryStatistics,
369}
370
371/// Throughput statistics
372#[derive(Debug, Clone)]
373pub struct ThroughputStatistics<A: Float + Send + Sync> {
374    /// Current throughput (items per second)
375    pub current_throughput: A,
376    /// Average throughput
377    pub avg_throughput: A,
378    /// Peak throughput
379    pub peak_throughput: A,
380    /// Throughput trend
381    pub throughput_trend: TrendDirection,
382    /// Throughput stability
383    pub stability: A,
384}
385
386/// Quality statistics for buffer content
387#[derive(Debug, Clone)]
388pub struct QualityStatistics<A: Float + Send + Sync> {
389    /// Current average quality
390    pub current_avg_quality: A,
391    /// Historical average quality
392    pub historical_avg_quality: A,
393    /// Quality improvement rate
394    pub quality_improvement_rate: A,
395    /// Quality distribution
396    pub quality_distribution: HashMap<String, A>,
397    /// Quality prediction
398    pub predicted_quality: Option<A>,
399}
400
401/// Memory usage statistics
402#[derive(Debug, Clone)]
403pub struct MemoryStatistics {
404    /// Current memory usage in bytes
405    pub current_usage_bytes: usize,
406    /// Peak memory usage in bytes
407    pub peak_usage_bytes: usize,
408    /// Average memory usage in bytes
409    pub avg_usage_bytes: usize,
410    /// Memory efficiency (useful data / total memory)
411    pub memory_efficiency: f64,
412    /// Memory fragmentation
413    pub fragmentation: f64,
414}
415
416/// Size change tracking event
417#[derive(Debug, Clone)]
418pub struct SizeChangeEvent {
419    /// Change timestamp
420    pub timestamp: Instant,
421    /// Size before change
422    pub old_size: usize,
423    /// Size after change
424    pub new_size: usize,
425    /// Change magnitude
426    pub change_magnitude: i32,
427    /// Reason for change
428    pub reason: String,
429}
430
431impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum + std::fmt::Debug>
432    AdaptiveBuffer<A>
433{
434    /// Creates a new adaptive buffer
435    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    /// Adds a batch of data points to the buffer
501    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        // Update quality metrics after batch addition
507        self.update_quality_metrics()?;
508
509        // Check if buffer needs resizing
510        self.check_buffer_resizing()?;
511
512        // Apply retention policy if buffer is too large
513        if self.current_size() > self.sizing_strategy.target_size {
514            self.apply_retention_policy()?;
515        }
516
517        Ok(())
518    }
519
520    /// Adds a single data point to the buffer
521    fn add_single_point(&mut self, data_point: StreamingDataPoint<A>) -> Result<(), String> {
522        // Calculate priority score for the data point
523        let priority_score = self.calculate_priority_score(&data_point)?;
524
525        // Calculate freshness and relevance scores. Relevance is measured
526        // against the statistics of the points seen *before* this one, so the
527        // point cannot make itself look typical.
528        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            // Real estimate from the measured average processing latency,
537            // falling back to the observed throughput when no latency has been
538            // recorded yet.
539            expected_processing_time: self.estimated_processing_time(),
540            freshness_score,
541            relevance_score,
542        };
543
544        // Add to appropriate buffer based on quality
545        if priority_score >= try_scalar_str::<A, _>(self.config.quality_threshold)? {
546            self.buffer.push(prioritized_point);
547        } else {
548            // Add to secondary buffer for potential later processing
549            self.secondary_buffer
550                .push_back(prioritized_point.data_point);
551        }
552
553        // Update statistics
554        self.statistics.total_items_processed += 1;
555
556        Ok(())
557    }
558
559    /// Calculates priority score for a data point
560    fn calculate_priority_score(&self, data_point: &StreamingDataPoint<A>) -> Result<A, String> {
561        let mut score = data_point.quality_score;
562
563        // Adjust score based on recency
564        let age = data_point.timestamp.elapsed().as_secs_f64();
565        let recency_bonus = try_scalar_str::<A, _>(1.0 / (1.0 + age / 3600.0))?; // Hour-based decay
566        score = score + recency_bonus * try_scalar_str::<A, _>(0.1)?;
567
568        // Adjust score based on feature variance (novelty)
569        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    /// Calculates novelty score based on feature variance
576    fn calculate_novelty_score(&self, data_point: &StreamingDataPoint<A>) -> Result<A, String> {
577        // Simple novelty calculation based on distance from recent data
578        if self.buffer.is_empty() {
579            return try_scalar_str::<A, _>(0.5); // Medium novelty for first data
580        }
581
582        // Calculate average distance from recent buffer content
583        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        // Normalize to 0-1 range
600        let normalized_novelty = avg_distance / (avg_distance + A::one());
601        Ok(normalized_novelty)
602    }
603
604    /// Calculates distance between feature vectors
605    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    /// Calculates freshness score based on data age
624    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; // 1 hour maximum age
627
628        let freshness = (max_age - age_seconds.min(max_age)) / max_age;
629        scalar_or(freshness.max(0.0), A::zero())
630    }
631
632    /// Calculates how relevant a data point is to what the buffer currently
633    /// holds.
634    ///
635    /// B1: this used to `return Ok(0.7)` for every point, so relevance
636    /// contributed a constant to the priority score and therefore had no effect
637    /// on ordering whatsoever.
638    ///
639    /// Relevance is now a real, bounded function of two measurable properties:
640    ///
641    /// - **Typicality**: the Mahalanobis-style standardised distance of the
642    ///   point's features from the buffer's running per-feature mean and
643    ///   standard deviation. A point that looks like the recent stream is
644    ///   relevant to the model currently being fitted; one many sigmas away is
645    ///   less so (novelty is scored separately, by
646    ///   `calculate_novelty_score`, and combined with a different weight).
647    /// - **Supervision**: a labelled point supports a gradient step while an
648    ///   unlabelled one cannot, so it is genuinely more relevant.
649    ///
650    /// With no history yet there is nothing to be relevant *to*, so the point's
651    /// own quality score is used as the only available estimate.
652    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        // Root-mean-square z score across the compared coordinates.
691        let rms_z = (squared_z_total / compared_count).sqrt();
692        // Map [0, inf) monotonically onto (0, 1]: a point sitting on the mean
693        // scores 1, a 1-sigma point 0.5, a 3-sigma point 0.25.
694        let typicality = A::one() / (A::one() + rms_z);
695
696        Ok((typicality + supervision_bonus).min(A::one()))
697    }
698
699    /// Folds a data point into the running per-feature statistics that back the
700    /// relevance score.
701    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        // Welford update per coordinate.
715        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    /// Number of data points folded into the relevance statistics.
728    pub fn feature_statistics_sample_count(&self) -> usize {
729        self.feature_statistics.sample_count
730    }
731
732    /// Per-item processing time expected from the measured average latency.
733    ///
734    /// Falls back to the observed throughput's reciprocal, and only then to a
735    /// documented default when neither has been measured yet.
736    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    /// Average processing latency measured over recent batches.
753    pub fn average_processing_latency(&self) -> Duration {
754        self.statistics.avg_processing_latency
755    }
756
757    /// Folds a real, measured batch processing duration into the buffer's
758    /// latency statistics.
759    ///
760    /// B2: `statistics.avg_processing_latency` was initialised to
761    /// `Duration::ZERO` and never written by anything, so the
762    /// `avg_processing_latency > 500ms` branch in
763    /// `calculate_optimal_batch_size` was unreachable dead code and the buffer
764    /// never shrank its batches under load.
765    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            // Exponential moving average with the same smoothing factor used
771            // for throughput, so the two statistics track at the same rate.
772            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    /// Gets a batch of data for processing
779    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        // Extract high-priority items from main buffer
784        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        // Fill remaining space with secondary buffer items if needed
791        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        // Update last processing time
798        self.last_processing = Instant::now();
799
800        // Update throughput statistics
801        self.update_throughput_stats(processing_batch.len())?;
802
803        Ok(processing_batch)
804    }
805
806    /// Calculates optimal batch size based on current conditions
807    fn calculate_optimal_batch_size(&self) -> Result<usize, String> {
808        let mut batch_size = self.config.initial_size.min(32); // Default reasonable batch size
809
810        // Adjust based on buffer fullness
811        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; // Larger batches when buffer is full
815        } else if buffer_utilization < 0.3 {
816            batch_size = (batch_size as f64 * 0.7) as usize; // Smaller batches when buffer is sparse
817        }
818
819        // Adjust based on processing latency
820        if self.statistics.avg_processing_latency > Duration::from_millis(500) {
821            batch_size = (batch_size as f64 * 0.8) as usize; // Smaller batches for slow processing
822        }
823
824        // Ensure minimum and maximum bounds
825        Ok(batch_size.max(1).min(self.current_size().min(100)))
826    }
827
828    /// Updates quality metrics for the buffer
829    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        // Collect quality scores from main buffer
838        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        // Collect quality scores from secondary buffer
845        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            // Update min/max quality
856            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            // Calculate quality variance
862            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            // Update quality trend
870            self.update_quality_trend(self.quality_metrics.average_quality)?;
871        }
872
873        Ok(())
874    }
875
876    /// Updates quality trend analysis
877    fn update_quality_trend(&mut self, current_quality: A) -> Result<(), String> {
878        let trend = &mut self.quality_metrics.quality_trend;
879
880        // Add current quality to recent changes
881        if trend.recent_changes.len() >= 50 {
882            trend.recent_changes.pop_front();
883        }
884        trend.recent_changes.push_back(current_quality);
885
886        // Analyze trend if we have enough data
887        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())?; // 5% change threshold
900
901            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            // B3: confidence used to be the constant 0.8, which made it useless
912            // for deciding whether to act on the trend. It is now Welch's
913            // two-sample t statistic for "the two halves have different means",
914            // mapped monotonically into [0, 1): a large, consistent shift
915            // relative to the within-half spread gives high confidence, while a
916            // shift that is small compared to the noise gives low confidence.
917            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                // Zero within-half variance and a non-zero shift is a perfectly
942                // clean step change.
943                A::one()
944            } else {
945                A::zero()
946            };
947        }
948
949        Ok(())
950    }
951
952    /// Checks if buffer needs resizing
953    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        // Check if resize is needed
963        let should_resize = if utilization > 0.9 {
964            // Buffer is nearly full - consider growing
965            Some(SizingReason::ThroughputOptimization)
966        } else if utilization < 0.3 && target_size > self.config.min_size {
967            // Buffer is underutilized - consider shrinking
968            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    /// Resizes the buffer based on current conditions
981    fn resize_buffer(&mut self, reason: SizingReason) -> Result<(), String> {
982        let old_size = self.sizing_strategy.target_size;
983
984        // `BufferConfig::size_strategy` used to be accepted, stored and never
985        // consulted: every strategy resized by the same `adjustment_params`
986        // multipliers, so a buffer configured `Fixed` still grew and shrank.
987        let growing = matches!(reason, SizingReason::ThroughputOptimization);
988        let shrinking = matches!(reason, SizingReason::MemoryPressure);
989        if !growing && !shrinking {
990            return Ok(()); // no sizing signal
991        }
992
993        let new_size = match &self.sizing_strategy.strategy_type {
994            // A fixed buffer is fixed.
995            BufferSizeStrategy::Fixed => return Ok(()),
996            // Additive steps of `growth_rate * initial_size`.
997            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            // Multiplicative steps of the configured base.
1008            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            // Performance- and resource-driven sizing both use the tuned
1017            // sensitivity parameters; `bound_target_size` then applies the
1018            // configured memory budget on top.
1019            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        // Apply size bounds, including the configured memory budget (CF1).
1043        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            // Log the size change
1049            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    /// Applies retention policy to manage buffer size
1067    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        // Apply retention policy to secondary buffer first
1079        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        // If still need to remove items, apply to main buffer
1090        let mut temp_buffer = Vec::new();
1091        while let Some(item) = self.buffer.pop() {
1092            temp_buffer.push(item);
1093        }
1094
1095        // Sort by retention score and keep the best items
1096        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        // Keep only the target number of items
1107        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    /// Determines if an item should be removed from secondary buffer
1116    fn should_remove_from_secondary(&self) -> Result<bool, String> {
1117        // Simple policy: remove oldest items first
1118        if let Some(oldest) = self.secondary_buffer.front() {
1119            let age = oldest.timestamp.elapsed();
1120            Ok(age > Duration::from_secs(3600)) // Remove items older than 1 hour
1121        } else {
1122            Ok(false)
1123        }
1124    }
1125
1126    /// Calculates retention score for a data point
1127    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        // Weighted combination
1133        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    /// Calculates age score for retention
1141    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; // 2 hours
1144
1145        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    /// Updates throughput statistics
1150    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            // Update average throughput (simple moving average)
1159            let alpha = try_scalar_str::<A, _>(0.1)?; // Smoothing factor
1160            self.statistics.throughput_stats.avg_throughput = alpha * throughput_value
1161                + (A::one() - alpha) * self.statistics.throughput_stats.avg_throughput;
1162
1163            // Update peak throughput
1164            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    /// Gets current buffer size (total items across all buffers)
1175    pub fn current_size(&self) -> usize {
1176        self.buffer.len() + self.secondary_buffer.len()
1177    }
1178
1179    /// Gets time since last processing
1180    pub fn time_since_last_processing(&self) -> Duration {
1181        self.last_processing.elapsed()
1182    }
1183
1184    /// Gets current buffer quality metrics
1185    pub fn get_quality_metrics(&self) -> BufferQualityMetrics<A> {
1186        self.quality_metrics.clone()
1187    }
1188
1189    /// Computes size adaptation based on performance feedback
1190    pub fn compute_size_adaptation(
1191        &self,
1192        performance_tracker: &PerformanceTracker<A>,
1193    ) -> Result<Option<Adaptation<A>>, String> {
1194        // Get recent performance data
1195        let recent_performance = performance_tracker.get_recent_performance(10);
1196        if recent_performance.is_empty() {
1197            return Ok(None);
1198        }
1199
1200        // Calculate average processing time.
1201        //
1202        // B2: this used `p.timestamp.elapsed()` — the *age* of each snapshot,
1203        // which grows without bound the longer the process runs. Every stream
1204        // therefore looked slower and slower until the "reduce buffer size"
1205        // branch latched permanently. `processing_duration` is the measured cost
1206        // of the step that produced the snapshot, which is what this decision
1207        // actually needs.
1208        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 processing is too slow, suggest reducing buffer size
1215        if avg_processing_time > 1000.0 {
1216            // More than 1 second
1217            let adaptation = Adaptation {
1218                adaptation_type: AdaptationType::BufferSize,
1219                magnitude: try_scalar_str::<A, _>(-0.2)?, // Reduce by 20%
1220                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        // If processing is very fast and buffer is often empty, suggest increasing size
1229        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)?, // Increase by 30%
1234                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    /// Maximum number of buffered items that fit inside
1246    /// `BufferConfig::memory_limit_mb` (CF1).
1247    ///
1248    /// The per-item footprint is *measured*, not assumed: it is the size of a
1249    /// `PrioritizedDataPoint<A>` plus the heap held by its feature (and target)
1250    /// vectors, whose dimensionality is known from the running per-feature
1251    /// statistics. Returns `None` until at least one data point has been
1252    /// observed, since there is no measurement to bound against before then.
1253    ///
1254    /// `memory_limit_mb` used to be a config field nothing read, so a buffer
1255    /// configured with a 128 MB budget would still grow to `max_size` items
1256    /// regardless of how wide each sample was.
1257    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        // `features` plus a possible `target` of the same width.
1263        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    /// Clamps a proposed target size to the configured item bounds *and* the
1273    /// memory budget.
1274    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    /// Test-only view of [`Self::memory_bounded_capacity`].
1283    #[cfg(test)]
1284    pub(crate) fn memory_bounded_capacity_for_test(&self) -> Option<usize> {
1285        self.memory_bounded_capacity()
1286    }
1287
1288    /// Test-only view of [`Self::bound_target_size`].
1289    #[cfg(test)]
1290    pub(crate) fn bound_target_size_for_test(&self, proposed: usize) -> usize {
1291        self.bound_target_size(proposed)
1292    }
1293
1294    /// Applies size adaptation to the buffer
1295    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            // Apply bounds, including the configured memory budget (CF1).
1303            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                // Log the change
1309                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    /// Gets the last size change amount
1328    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    /// Resets the buffer to initial state
1337    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    /// Gets diagnostic information
1365    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
1378// Implement Ord for PrioritizedDataPoint to work with BinaryHeap
1379impl<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/// Diagnostic information for buffer management
1420#[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    /// B1: `calculate_relevance_score` returned the constant `0.7` for every
1469    /// point, so relevance carried zero information. It must now discriminate: a
1470    /// point sitting on the buffer's running mean is more relevant than one many
1471    /// standard deviations away.
1472    #[test]
1473    fn relevance_score_discriminates_typical_from_atypical_points() {
1474        let mut buffer = buffer();
1475
1476        // Establish a tight distribution around 10.0.
1477        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    /// B1: a labelled point can support a gradient step and an unlabelled one
1506    /// cannot, so supervision must raise relevance.
1507    #[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        // Use an off-centre value so neither score saturates at the 1.0 clamp.
1516        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    /// B2: `compute_size_adaptation` measured "processing time" as
1530    /// `snapshot.timestamp.elapsed()` — the snapshot's *age*. Age grows without
1531    /// bound as the process runs, so after a while every stream looked slower
1532    /// than one second per batch and the "shrink the buffer" branch latched
1533    /// permanently. Snapshots that took 1ms each must produce no shrink request
1534    /// no matter how old they are.
1535    #[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        // Let the snapshots visibly age; under the bug this is what was measured.
1545        std::thread::sleep(Duration::from_millis(60));
1546
1547        let mut buffer = buffer();
1548        // Fill past 30% utilisation so the "grow" branch is not taken either.
1549        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    /// B2: a genuinely slow workload must still be caught, so the fix does not
1570    /// simply blind the detector.
1571    #[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    /// B2: `statistics.avg_processing_latency` was initialised to
1594    /// `Duration::ZERO` and never written by anything, making the
1595    /// `> 500ms` branch of `calculate_optimal_batch_size` unreachable.
1596    #[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        // Report a genuinely slow batch several times so the EMA clears 500ms.
1615        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    /// B3: `trend.confidence` was the constant `0.8`, so no caller could tell a
1636    /// clean step change from pure noise. A clean, low-noise step must now score
1637    /// high confidence and a noisy no-op must score low.
1638    #[test]
1639    fn quality_trend_confidence_reflects_the_real_fit() {
1640        // Clean step: first half at 0.2, second half at 0.9, no within-half noise.
1641        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        // Noisy, trendless series alternating around the same mean.
1651        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}