1#![forbid(unsafe_code)]
61
62use std::collections::VecDeque;
63use std::time::{Duration, Instant};
64
65const DEFAULT_MAX_INPUT_LATENCY_MS: u64 = 50;
67
68const DEFAULT_DOMINANCE_THRESHOLD: u32 = 3;
70
71const DEFAULT_FAIRNESS_THRESHOLD: f64 = 0.5;
73
74const FAIRNESS_WINDOW_SIZE: usize = 16;
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum EventType {
80 Input,
82 Resize,
84 Tick,
86}
87
88pub type FairnessEventType = EventType;
90
91#[derive(Debug, Clone)]
93pub struct FairnessConfig {
94 pub input_priority_threshold: Duration,
96 pub enabled: bool,
98 pub dominance_threshold: u32,
100 pub fairness_threshold: f64,
102}
103
104impl Default for FairnessConfig {
105 fn default() -> Self {
106 Self {
107 input_priority_threshold: Duration::from_millis(DEFAULT_MAX_INPUT_LATENCY_MS),
108 enabled: true, dominance_threshold: DEFAULT_DOMINANCE_THRESHOLD,
110 fairness_threshold: DEFAULT_FAIRNESS_THRESHOLD,
111 }
112 }
113}
114
115impl FairnessConfig {
116 pub fn disabled() -> Self {
118 Self {
119 enabled: false,
120 ..Default::default()
121 }
122 }
123
124 pub fn with_max_latency(mut self, latency: Duration) -> Self {
126 self.input_priority_threshold = latency;
127 self
128 }
129
130 pub fn with_dominance_threshold(mut self, threshold: u32) -> Self {
132 self.dominance_threshold = threshold;
133 self
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum InterventionReason {
140 None,
142 InputLatency,
144 ResizeDominance,
146 FairnessIndex,
148}
149
150impl InterventionReason {
151 pub fn requires_intervention(&self) -> bool {
153 !matches!(self, InterventionReason::None)
154 }
155
156 #[must_use]
158 pub const fn as_str(self) -> &'static str {
159 match self {
160 Self::None => "none",
161 Self::InputLatency => "input_latency",
162 Self::ResizeDominance => "resize_dominance",
163 Self::FairnessIndex => "fairness_index",
164 }
165 }
166}
167
168#[derive(Debug, Clone)]
170pub struct FairnessDecision {
171 pub should_process: bool,
173 pub pending_input_latency: Option<Duration>,
175 pub reason: InterventionReason,
177 pub yield_to_input: bool,
179 pub jain_index: f64,
181}
182
183impl Default for FairnessDecision {
184 fn default() -> Self {
185 Self {
186 should_process: true,
187 pending_input_latency: None,
188 reason: InterventionReason::None,
189 yield_to_input: false,
190 jain_index: 1.0, }
192 }
193}
194
195#[derive(Debug, Clone)]
197pub struct FairnessLogEntry {
198 pub timestamp: Instant,
200 pub event_type: EventType,
202 pub duration: Duration,
204}
205
206#[derive(Debug, Clone, Default)]
208pub struct FairnessStats {
209 pub events_processed: u64,
211 pub input_events: u64,
213 pub resize_events: u64,
215 pub tick_events: u64,
217 pub total_checks: u64,
219 pub total_interventions: u64,
221 pub max_input_latency: Duration,
223}
224
225#[derive(Debug, Clone, Default)]
227pub struct InterventionCounts {
228 pub input_latency: u64,
230 pub resize_dominance: u64,
232 pub fairness_index: u64,
234}
235
236#[derive(Debug, Clone)]
238struct ProcessingRecord {
239 event_type: EventType,
241 duration: Duration,
243}
244
245#[derive(Debug)]
250pub struct InputFairnessGuard {
251 config: FairnessConfig,
252 stats: FairnessStats,
253 intervention_counts: InterventionCounts,
254
255 pending_input_arrival: Option<Instant>,
257 recent_input_arrival: Option<Instant>,
259
260 resize_dominance_count: u32,
262
263 processing_window: VecDeque<ProcessingRecord>,
265
266 input_time_us: u64,
268 resize_time_us: u64,
269}
270
271impl InputFairnessGuard {
272 pub fn new() -> Self {
274 Self::with_config(FairnessConfig::default())
275 }
276
277 pub fn with_config(config: FairnessConfig) -> Self {
279 Self {
280 config,
281 stats: FairnessStats::default(),
282 intervention_counts: InterventionCounts::default(),
283 pending_input_arrival: None,
284 recent_input_arrival: None,
285 resize_dominance_count: 0,
286 processing_window: VecDeque::with_capacity(FAIRNESS_WINDOW_SIZE),
287 input_time_us: 0,
288 resize_time_us: 0,
289 }
290 }
291
292 pub fn input_arrived(&mut self, now: Instant) {
296 if self.pending_input_arrival.is_none() {
297 self.pending_input_arrival = Some(now);
298 }
299 if self.recent_input_arrival.is_none() {
300 self.recent_input_arrival = Some(now);
301 }
302 }
303
304 pub fn check_fairness(&mut self, now: Instant) -> FairnessDecision {
308 self.stats.total_checks += 1;
309
310 if !self.config.enabled {
312 self.recent_input_arrival = None;
313 return FairnessDecision::default();
314 }
315
316 let jain = self.calculate_jain_index();
318
319 let pending_latency = self
321 .recent_input_arrival
322 .or(self.pending_input_arrival)
323 .map(|t| now.duration_since(t));
324 if let Some(latency) = pending_latency
325 && latency > self.stats.max_input_latency
326 {
327 self.stats.max_input_latency = latency;
328 }
329
330 let reason = self.determine_intervention_reason(pending_latency, jain);
332 let yield_to_input = reason.requires_intervention();
333
334 if yield_to_input {
335 self.stats.total_interventions += 1;
336 match reason {
337 InterventionReason::InputLatency => {
338 self.intervention_counts.input_latency += 1;
339 }
340 InterventionReason::ResizeDominance => {
341 self.intervention_counts.resize_dominance += 1;
342 }
343 InterventionReason::FairnessIndex => {
344 self.intervention_counts.fairness_index += 1;
345 }
346 InterventionReason::None => {}
347 }
348 self.resize_dominance_count = 0;
350 }
351
352 let decision = FairnessDecision {
353 should_process: !yield_to_input,
354 pending_input_latency: pending_latency,
355 reason,
356 yield_to_input,
357 jain_index: jain,
358 };
359
360 self.recent_input_arrival = None;
362
363 decision
364 }
365
366 pub fn event_processed(&mut self, event_type: EventType, duration: Duration, _now: Instant) {
368 self.stats.events_processed += 1;
369 match event_type {
370 EventType::Input => self.stats.input_events += 1,
371 EventType::Resize => self.stats.resize_events += 1,
372 EventType::Tick => self.stats.tick_events += 1,
373 }
374
375 if !self.config.enabled {
377 return;
378 }
379
380 let record = ProcessingRecord {
382 event_type,
383 duration,
384 };
385
386 if self.processing_window.len() >= FAIRNESS_WINDOW_SIZE
388 && let Some(old) = self.processing_window.pop_front()
389 {
390 match old.event_type {
391 EventType::Input => {
392 self.input_time_us = self
393 .input_time_us
394 .saturating_sub(old.duration.as_micros() as u64);
395 }
396 EventType::Resize => {
397 self.resize_time_us = self
398 .resize_time_us
399 .saturating_sub(old.duration.as_micros() as u64);
400 }
401 EventType::Tick => {}
402 }
403 }
404
405 match event_type {
407 EventType::Input => {
408 self.input_time_us += duration.as_micros() as u64;
409 self.pending_input_arrival = None;
410 self.resize_dominance_count = 0; }
412 EventType::Resize => {
413 self.resize_time_us += duration.as_micros() as u64;
414 self.resize_dominance_count += 1;
415 }
416 EventType::Tick => {}
417 }
418
419 self.processing_window.push_back(record);
420 }
421
422 fn calculate_jain_index(&self) -> f64 {
424 let x = self.input_time_us as f64;
426 let y = self.resize_time_us as f64;
427
428 if x == 0.0 && y == 0.0 {
429 return 1.0; }
431
432 let sum = x + y;
433 let sum_sq = x * x + y * y;
434
435 if sum_sq == 0.0 {
436 return 1.0;
437 }
438
439 (sum * sum) / (2.0 * sum_sq)
440 }
441
442 fn determine_intervention_reason(
444 &self,
445 pending_latency: Option<Duration>,
446 jain: f64,
447 ) -> InterventionReason {
448 if let Some(latency) = pending_latency
450 && latency >= self.config.input_priority_threshold
451 {
452 return InterventionReason::InputLatency;
453 }
454
455 if self.resize_dominance_count >= self.config.dominance_threshold {
457 return InterventionReason::ResizeDominance;
458 }
459
460 if jain < self.config.fairness_threshold && pending_latency.is_some() {
462 return InterventionReason::FairnessIndex;
463 }
464
465 InterventionReason::None
466 }
467
468 pub fn stats(&self) -> &FairnessStats {
470 &self.stats
471 }
472
473 pub fn intervention_counts(&self) -> &InterventionCounts {
475 &self.intervention_counts
476 }
477
478 pub fn config(&self) -> &FairnessConfig {
480 &self.config
481 }
482
483 pub fn resize_dominance_count(&self) -> u32 {
485 self.resize_dominance_count
486 }
487
488 pub fn is_enabled(&self) -> bool {
490 self.config.enabled
491 }
492
493 pub fn jain_index(&self) -> f64 {
495 self.calculate_jain_index()
496 }
497
498 pub fn has_pending_input(&self) -> bool {
500 self.pending_input_arrival.is_some()
501 }
502
503 pub fn reset(&mut self) {
505 self.pending_input_arrival = None;
506 self.recent_input_arrival = None;
507 self.resize_dominance_count = 0;
508 self.processing_window.clear();
509 self.input_time_us = 0;
510 self.resize_time_us = 0;
511 self.stats = FairnessStats::default();
512 self.intervention_counts = InterventionCounts::default();
513 }
514}
515
516impl Default for InputFairnessGuard {
517 fn default() -> Self {
518 Self::new()
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525
526 #[test]
527 fn default_config_is_enabled() {
528 let config = FairnessConfig::default();
529 assert!(config.enabled);
530 }
531
532 #[test]
533 fn disabled_config() {
534 let config = FairnessConfig::disabled();
535 assert!(!config.enabled);
536 }
537
538 #[test]
539 fn default_decision_allows_processing() {
540 let mut guard = InputFairnessGuard::default();
541 let decision = guard.check_fairness(Instant::now());
542 assert!(decision.should_process);
543 }
544
545 #[test]
546 fn event_processing_updates_stats() {
547 let mut guard = InputFairnessGuard::default();
548 let now = Instant::now();
549
550 guard.event_processed(EventType::Input, Duration::from_millis(10), now);
551 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
552 guard.event_processed(EventType::Tick, Duration::from_millis(1), now);
553
554 let stats = guard.stats();
555 assert_eq!(stats.events_processed, 3);
556 assert_eq!(stats.input_events, 1);
557 assert_eq!(stats.resize_events, 1);
558 assert_eq!(stats.tick_events, 1);
559 }
560
561 #[test]
562 fn test_jain_index_perfect_fairness() {
563 let mut guard = InputFairnessGuard::new();
564 let now = Instant::now();
565
566 guard.event_processed(EventType::Input, Duration::from_millis(10), now);
568 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
569
570 let jain = guard.jain_index();
571 assert!((jain - 1.0).abs() < 0.001, "Expected ~1.0, got {}", jain);
572 }
573
574 #[test]
575 fn test_jain_index_unfair() {
576 let mut guard = InputFairnessGuard::new();
577 let now = Instant::now();
578
579 guard.event_processed(EventType::Input, Duration::from_millis(1), now);
581 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
582
583 let jain = guard.jain_index();
584 assert!(jain < 0.6, "Expected unfair index < 0.6, got {}", jain);
586 }
587
588 #[test]
589 fn test_jain_index_empty() {
590 let guard = InputFairnessGuard::new();
591 let jain = guard.jain_index();
592 assert!((jain - 1.0).abs() < 0.001, "Empty should be fair (1.0)");
593 }
594
595 #[test]
596 fn test_latency_threshold_intervention() {
597 let config = FairnessConfig::default().with_max_latency(Duration::from_millis(20));
598 let mut guard = InputFairnessGuard::with_config(config);
599
600 let start = Instant::now();
601 guard.input_arrived(start);
602
603 let decision = guard.check_fairness(start + Duration::from_millis(25));
605 assert!(decision.yield_to_input);
606 assert_eq!(decision.reason, InterventionReason::InputLatency);
607 }
608
609 #[test]
610 fn test_resize_dominance_intervention() {
611 let config = FairnessConfig::default().with_dominance_threshold(2);
612 let mut guard = InputFairnessGuard::with_config(config);
613 let now = Instant::now();
614
615 guard.input_arrived(now);
617
618 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
620 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
621
622 let decision = guard.check_fairness(now);
623 assert!(decision.yield_to_input);
624 assert_eq!(decision.reason, InterventionReason::ResizeDominance);
625 }
626
627 #[test]
628 fn test_no_intervention_when_fair() {
629 let mut guard = InputFairnessGuard::new();
630 let now = Instant::now();
631
632 guard.event_processed(EventType::Input, Duration::from_millis(10), now);
634 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
635
636 let decision = guard.check_fairness(now);
637 assert!(!decision.yield_to_input);
638 assert_eq!(decision.reason, InterventionReason::None);
639 }
640
641 #[test]
642 fn test_fairness_index_intervention() {
643 let config = FairnessConfig {
644 input_priority_threshold: Duration::from_secs(10),
645 dominance_threshold: 100,
646 fairness_threshold: 0.9,
647 ..Default::default()
648 };
649 let mut guard = InputFairnessGuard::with_config(config);
650 let now = Instant::now();
651
652 guard.input_arrived(now);
653 guard.event_processed(EventType::Input, Duration::from_millis(1), now);
654 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
655
656 let decision = guard.check_fairness(now + Duration::from_millis(1));
657 assert!(decision.yield_to_input);
658 assert_eq!(decision.reason, InterventionReason::FairnessIndex);
659 }
660
661 #[test]
662 fn test_dominance_reset_on_input() {
663 let mut guard = InputFairnessGuard::new();
664 let now = Instant::now();
665
666 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
668 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
669 assert_eq!(guard.resize_dominance_count, 2);
670
671 guard.event_processed(EventType::Input, Duration::from_millis(5), now);
673 assert_eq!(guard.resize_dominance_count, 0);
674 }
675
676 #[test]
677 fn test_pending_input_cleared_on_processing() {
678 let mut guard = InputFairnessGuard::new();
679 let now = Instant::now();
680
681 guard.input_arrived(now);
682 assert!(guard.has_pending_input());
683
684 guard.event_processed(EventType::Input, Duration::from_millis(5), now);
685 assert!(!guard.has_pending_input());
686 }
687
688 #[test]
689 fn test_stats_tracking() {
690 let mut guard = InputFairnessGuard::new();
691 let now = Instant::now();
692
693 guard.check_fairness(now);
695 guard.check_fairness(now);
696
697 assert_eq!(guard.stats().total_checks, 2);
698 }
699
700 #[test]
701 fn test_sliding_window_eviction() {
702 let mut guard = InputFairnessGuard::new();
703 let now = Instant::now();
704
705 for _ in 0..(FAIRNESS_WINDOW_SIZE + 5) {
707 guard.event_processed(EventType::Input, Duration::from_millis(1), now);
708 }
709
710 assert_eq!(guard.processing_window.len(), FAIRNESS_WINDOW_SIZE);
711 }
712
713 #[test]
714 fn test_reset() {
715 let mut guard = InputFairnessGuard::new();
716 let now = Instant::now();
717
718 guard.input_arrived(now);
719 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
720 guard.check_fairness(now);
721
722 guard.reset();
723
724 assert!(!guard.has_pending_input());
725 assert_eq!(guard.resize_dominance_count, 0);
726 assert_eq!(guard.stats().total_checks, 0);
727 assert!(guard.processing_window.is_empty());
728 }
729
730 #[test]
733 fn test_invariant_jain_index_bounds() {
734 let mut guard = InputFairnessGuard::new();
736 let now = Instant::now();
737
738 for (input_ms, resize_ms) in [(1, 1), (1, 100), (100, 1), (50, 50), (0, 100), (100, 0)] {
740 guard.reset();
741 if input_ms > 0 {
742 guard.event_processed(EventType::Input, Duration::from_millis(input_ms), now);
743 }
744 if resize_ms > 0 {
745 guard.event_processed(EventType::Resize, Duration::from_millis(resize_ms), now);
746 }
747
748 let jain = guard.jain_index();
749 assert!(
750 (0.5..=1.0).contains(&jain),
751 "Jain index {} out of bounds for input={}, resize={}",
752 jain,
753 input_ms,
754 resize_ms
755 );
756 }
757 }
758
759 #[test]
760 fn test_invariant_intervention_resets_dominance() {
761 let config = FairnessConfig::default().with_dominance_threshold(2);
762 let mut guard = InputFairnessGuard::with_config(config);
763 let now = Instant::now();
764
765 guard.input_arrived(now);
767 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
768 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
769
770 let decision = guard.check_fairness(now);
772 assert!(decision.yield_to_input);
773 assert_eq!(guard.resize_dominance_count, 0);
774 }
775
776 #[test]
777 fn test_invariant_monotonic_stats() {
778 let mut guard = InputFairnessGuard::new();
779 let now = Instant::now();
780
781 let mut prev_checks = 0u64;
782 for _ in 0..10 {
783 guard.check_fairness(now);
784 assert!(guard.stats().total_checks > prev_checks);
785 prev_checks = guard.stats().total_checks;
786 }
787 }
788
789 #[test]
790 fn test_disabled_returns_no_intervention() {
791 let config = FairnessConfig::disabled();
792 let mut guard = InputFairnessGuard::with_config(config);
793 let now = Instant::now();
794
795 guard.input_arrived(now);
797 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
798 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
799 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
800
801 let decision = guard.check_fairness(now);
802 assert!(!decision.yield_to_input);
803 assert_eq!(decision.reason, InterventionReason::None);
804 }
805}