1#![forbid(unsafe_code)]
61
62use std::collections::VecDeque;
63use web_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 #[must_use]
126 pub fn with_max_latency(mut self, latency: Duration) -> Self {
127 self.input_priority_threshold = latency;
128 self
129 }
130
131 #[must_use]
133 pub fn with_dominance_threshold(mut self, threshold: u32) -> Self {
134 self.dominance_threshold = threshold;
135 self
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum InterventionReason {
142 None,
144 InputLatency,
146 ResizeDominance,
148 FairnessIndex,
150}
151
152impl InterventionReason {
153 pub fn requires_intervention(&self) -> bool {
155 !matches!(self, InterventionReason::None)
156 }
157
158 #[must_use]
160 pub const fn as_str(self) -> &'static str {
161 match self {
162 Self::None => "none",
163 Self::InputLatency => "input_latency",
164 Self::ResizeDominance => "resize_dominance",
165 Self::FairnessIndex => "fairness_index",
166 }
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct FairnessDecision {
173 pub should_process: bool,
175 pub pending_input_latency: Option<Duration>,
177 pub reason: InterventionReason,
179 pub yield_to_input: bool,
181 pub jain_index: f64,
183}
184
185impl Default for FairnessDecision {
186 fn default() -> Self {
187 Self {
188 should_process: true,
189 pending_input_latency: None,
190 reason: InterventionReason::None,
191 yield_to_input: false,
192 jain_index: 1.0, }
194 }
195}
196
197#[derive(Debug, Clone)]
199pub struct FairnessLogEntry {
200 pub timestamp: Instant,
202 pub event_type: EventType,
204 pub duration: Duration,
206}
207
208#[derive(Debug, Clone, Default)]
210pub struct FairnessStats {
211 pub events_processed: u64,
213 pub input_events: u64,
215 pub resize_events: u64,
217 pub tick_events: u64,
219 pub total_checks: u64,
221 pub total_interventions: u64,
223 pub max_input_latency: Duration,
225}
226
227#[derive(Debug, Clone, Default)]
229pub struct InterventionCounts {
230 pub input_latency: u64,
232 pub resize_dominance: u64,
234 pub fairness_index: u64,
236}
237
238#[derive(Debug, Clone)]
240struct ProcessingRecord {
241 event_type: EventType,
243 duration: Duration,
245}
246
247#[derive(Debug)]
252pub struct InputFairnessGuard {
253 config: FairnessConfig,
254 stats: FairnessStats,
255 intervention_counts: InterventionCounts,
256
257 pending_input_arrival: Option<Instant>,
259 recent_input_arrival: Option<Instant>,
261
262 resize_dominance_count: u32,
264
265 processing_window: VecDeque<ProcessingRecord>,
267
268 input_time_us: u64,
270 resize_time_us: u64,
271}
272
273impl InputFairnessGuard {
274 pub fn new() -> Self {
276 Self::with_config(FairnessConfig::default())
277 }
278
279 pub fn with_config(config: FairnessConfig) -> Self {
281 Self {
282 config,
283 stats: FairnessStats::default(),
284 intervention_counts: InterventionCounts::default(),
285 pending_input_arrival: None,
286 recent_input_arrival: None,
287 resize_dominance_count: 0,
288 processing_window: VecDeque::with_capacity(FAIRNESS_WINDOW_SIZE),
289 input_time_us: 0,
290 resize_time_us: 0,
291 }
292 }
293
294 pub fn input_arrived(&mut self, now: Instant) {
298 if self.pending_input_arrival.is_none() {
299 self.pending_input_arrival = Some(now);
300 }
301 if self.recent_input_arrival.is_none() {
302 self.recent_input_arrival = Some(now);
303 }
304 }
305
306 pub fn check_fairness(&mut self, now: Instant) -> FairnessDecision {
310 self.stats.total_checks += 1;
311
312 if !self.config.enabled {
314 self.recent_input_arrival = None;
315 return FairnessDecision::default();
316 }
317
318 let jain = self.calculate_jain_index();
320
321 let pending_latency = self
323 .recent_input_arrival
324 .or(self.pending_input_arrival)
325 .map(|t| now.duration_since(t));
326 if let Some(latency) = pending_latency
327 && latency > self.stats.max_input_latency
328 {
329 self.stats.max_input_latency = latency;
330 }
331
332 let reason = self.determine_intervention_reason(pending_latency, jain);
334 let yield_to_input = reason.requires_intervention();
335
336 if yield_to_input {
337 self.stats.total_interventions += 1;
338 match reason {
339 InterventionReason::InputLatency => {
340 self.intervention_counts.input_latency += 1;
341 }
342 InterventionReason::ResizeDominance => {
343 self.intervention_counts.resize_dominance += 1;
344 }
345 InterventionReason::FairnessIndex => {
346 self.intervention_counts.fairness_index += 1;
347 }
348 InterventionReason::None => {}
349 }
350 self.resize_dominance_count = 0;
352 }
353
354 let decision = FairnessDecision {
355 should_process: !yield_to_input,
356 pending_input_latency: pending_latency,
357 reason,
358 yield_to_input,
359 jain_index: jain,
360 };
361
362 self.recent_input_arrival = None;
364
365 decision
366 }
367
368 pub fn event_processed(&mut self, event_type: EventType, duration: Duration, _now: Instant) {
370 self.stats.events_processed += 1;
371 match event_type {
372 EventType::Input => self.stats.input_events += 1,
373 EventType::Resize => self.stats.resize_events += 1,
374 EventType::Tick => self.stats.tick_events += 1,
375 }
376
377 if !self.config.enabled {
379 return;
380 }
381
382 let record = ProcessingRecord {
384 event_type,
385 duration,
386 };
387
388 if self.processing_window.len() >= FAIRNESS_WINDOW_SIZE
390 && let Some(old) = self.processing_window.pop_front()
391 {
392 match old.event_type {
393 EventType::Input => {
394 self.input_time_us = self
395 .input_time_us
396 .saturating_sub(old.duration.as_micros() as u64);
397 }
398 EventType::Resize => {
399 self.resize_time_us = self
400 .resize_time_us
401 .saturating_sub(old.duration.as_micros() as u64);
402 }
403 EventType::Tick => {}
404 }
405 }
406
407 match event_type {
409 EventType::Input => {
410 self.input_time_us += duration.as_micros() as u64;
411 self.pending_input_arrival = None;
412 self.resize_dominance_count = 0; }
414 EventType::Resize => {
415 self.resize_time_us += duration.as_micros() as u64;
416 self.resize_dominance_count += 1;
417 }
418 EventType::Tick => {}
419 }
420
421 self.processing_window.push_back(record);
422 }
423
424 fn calculate_jain_index(&self) -> f64 {
426 let x = self.input_time_us as f64;
428 let y = self.resize_time_us as f64;
429
430 if x == 0.0 && y == 0.0 {
431 return 1.0; }
433
434 let sum = x + y;
435 let sum_sq = x * x + y * y;
436
437 if sum_sq == 0.0 {
438 return 1.0;
439 }
440
441 (sum * sum) / (2.0 * sum_sq)
442 }
443
444 fn determine_intervention_reason(
446 &self,
447 pending_latency: Option<Duration>,
448 jain: f64,
449 ) -> InterventionReason {
450 if let Some(latency) = pending_latency
452 && latency >= self.config.input_priority_threshold
453 {
454 return InterventionReason::InputLatency;
455 }
456
457 if self.resize_dominance_count >= self.config.dominance_threshold {
459 return InterventionReason::ResizeDominance;
460 }
461
462 if jain < self.config.fairness_threshold && pending_latency.is_some() {
464 return InterventionReason::FairnessIndex;
465 }
466
467 InterventionReason::None
468 }
469
470 pub fn stats(&self) -> &FairnessStats {
472 &self.stats
473 }
474
475 pub fn intervention_counts(&self) -> &InterventionCounts {
477 &self.intervention_counts
478 }
479
480 pub fn config(&self) -> &FairnessConfig {
482 &self.config
483 }
484
485 pub fn resize_dominance_count(&self) -> u32 {
487 self.resize_dominance_count
488 }
489
490 pub fn is_enabled(&self) -> bool {
492 self.config.enabled
493 }
494
495 pub fn jain_index(&self) -> f64 {
497 self.calculate_jain_index()
498 }
499
500 pub fn has_pending_input(&self) -> bool {
502 self.pending_input_arrival.is_some()
503 }
504
505 pub fn reset(&mut self) {
507 self.pending_input_arrival = None;
508 self.recent_input_arrival = None;
509 self.resize_dominance_count = 0;
510 self.processing_window.clear();
511 self.input_time_us = 0;
512 self.resize_time_us = 0;
513 self.stats = FairnessStats::default();
514 self.intervention_counts = InterventionCounts::default();
515 }
516}
517
518impl Default for InputFairnessGuard {
519 fn default() -> Self {
520 Self::new()
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn default_config_is_enabled() {
530 let config = FairnessConfig::default();
531 assert!(config.enabled);
532 }
533
534 #[test]
535 fn disabled_config() {
536 let config = FairnessConfig::disabled();
537 assert!(!config.enabled);
538 }
539
540 #[test]
541 fn default_decision_allows_processing() {
542 let mut guard = InputFairnessGuard::default();
543 let decision = guard.check_fairness(Instant::now());
544 assert!(decision.should_process);
545 }
546
547 #[test]
548 fn event_processing_updates_stats() {
549 let mut guard = InputFairnessGuard::default();
550 let now = Instant::now();
551
552 guard.event_processed(EventType::Input, Duration::from_millis(10), now);
553 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
554 guard.event_processed(EventType::Tick, Duration::from_millis(1), now);
555
556 let stats = guard.stats();
557 assert_eq!(stats.events_processed, 3);
558 assert_eq!(stats.input_events, 1);
559 assert_eq!(stats.resize_events, 1);
560 assert_eq!(stats.tick_events, 1);
561 }
562
563 #[test]
564 fn test_jain_index_perfect_fairness() {
565 let mut guard = InputFairnessGuard::new();
566 let now = Instant::now();
567
568 guard.event_processed(EventType::Input, Duration::from_millis(10), now);
570 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
571
572 let jain = guard.jain_index();
573 assert!((jain - 1.0).abs() < 0.001, "Expected ~1.0, got {}", jain);
574 }
575
576 #[test]
577 fn test_jain_index_unfair() {
578 let mut guard = InputFairnessGuard::new();
579 let now = Instant::now();
580
581 guard.event_processed(EventType::Input, Duration::from_millis(1), now);
583 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
584
585 let jain = guard.jain_index();
586 assert!(jain < 0.6, "Expected unfair index < 0.6, got {}", jain);
588 }
589
590 #[test]
591 fn test_jain_index_empty() {
592 let guard = InputFairnessGuard::new();
593 let jain = guard.jain_index();
594 assert!((jain - 1.0).abs() < 0.001, "Empty should be fair (1.0)");
595 }
596
597 #[test]
598 fn test_latency_threshold_intervention() {
599 let config = FairnessConfig::default().with_max_latency(Duration::from_millis(20));
600 let mut guard = InputFairnessGuard::with_config(config);
601
602 let start = Instant::now();
603 guard.input_arrived(start);
604
605 let decision = guard.check_fairness(start + Duration::from_millis(25));
607 assert!(decision.yield_to_input);
608 assert_eq!(decision.reason, InterventionReason::InputLatency);
609 }
610
611 #[test]
612 fn test_resize_dominance_intervention() {
613 let config = FairnessConfig::default().with_dominance_threshold(2);
614 let mut guard = InputFairnessGuard::with_config(config);
615 let now = Instant::now();
616
617 guard.input_arrived(now);
619
620 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
622 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
623
624 let decision = guard.check_fairness(now);
625 assert!(decision.yield_to_input);
626 assert_eq!(decision.reason, InterventionReason::ResizeDominance);
627 }
628
629 #[test]
630 fn test_no_intervention_when_fair() {
631 let mut guard = InputFairnessGuard::new();
632 let now = Instant::now();
633
634 guard.event_processed(EventType::Input, Duration::from_millis(10), now);
636 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
637
638 let decision = guard.check_fairness(now);
639 assert!(!decision.yield_to_input);
640 assert_eq!(decision.reason, InterventionReason::None);
641 }
642
643 #[test]
644 fn test_fairness_index_intervention() {
645 let config = FairnessConfig {
646 input_priority_threshold: Duration::from_secs(10),
647 dominance_threshold: 100,
648 fairness_threshold: 0.9,
649 ..Default::default()
650 };
651 let mut guard = InputFairnessGuard::with_config(config);
652 let now = Instant::now();
653
654 guard.input_arrived(now);
655 guard.event_processed(EventType::Input, Duration::from_millis(1), now);
656 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
657
658 let decision = guard.check_fairness(now + Duration::from_millis(1));
659 assert!(decision.yield_to_input);
660 assert_eq!(decision.reason, InterventionReason::FairnessIndex);
661 }
662
663 #[test]
664 fn test_dominance_reset_on_input() {
665 let mut guard = InputFairnessGuard::new();
666 let now = Instant::now();
667
668 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
670 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
671 assert_eq!(guard.resize_dominance_count, 2);
672
673 guard.event_processed(EventType::Input, Duration::from_millis(5), now);
675 assert_eq!(guard.resize_dominance_count, 0);
676 }
677
678 #[test]
679 fn test_pending_input_cleared_on_processing() {
680 let mut guard = InputFairnessGuard::new();
681 let now = Instant::now();
682
683 guard.input_arrived(now);
684 assert!(guard.has_pending_input());
685
686 guard.event_processed(EventType::Input, Duration::from_millis(5), now);
687 assert!(!guard.has_pending_input());
688 }
689
690 #[test]
691 fn test_stats_tracking() {
692 let mut guard = InputFairnessGuard::new();
693 let now = Instant::now();
694
695 guard.check_fairness(now);
697 guard.check_fairness(now);
698
699 assert_eq!(guard.stats().total_checks, 2);
700 }
701
702 #[test]
703 fn test_sliding_window_eviction() {
704 let mut guard = InputFairnessGuard::new();
705 let now = Instant::now();
706
707 for _ in 0..(FAIRNESS_WINDOW_SIZE + 5) {
709 guard.event_processed(EventType::Input, Duration::from_millis(1), now);
710 }
711
712 assert_eq!(guard.processing_window.len(), FAIRNESS_WINDOW_SIZE);
713 }
714
715 #[test]
716 fn test_reset() {
717 let mut guard = InputFairnessGuard::new();
718 let now = Instant::now();
719
720 guard.input_arrived(now);
721 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
722 guard.check_fairness(now);
723
724 guard.reset();
725
726 assert!(!guard.has_pending_input());
727 assert_eq!(guard.resize_dominance_count, 0);
728 assert_eq!(guard.stats().total_checks, 0);
729 assert!(guard.processing_window.is_empty());
730 }
731
732 #[test]
735 fn test_invariant_jain_index_bounds() {
736 let mut guard = InputFairnessGuard::new();
738 let now = Instant::now();
739
740 for (input_ms, resize_ms) in [(1, 1), (1, 100), (100, 1), (50, 50), (0, 100), (100, 0)] {
742 guard.reset();
743 if input_ms > 0 {
744 guard.event_processed(EventType::Input, Duration::from_millis(input_ms), now);
745 }
746 if resize_ms > 0 {
747 guard.event_processed(EventType::Resize, Duration::from_millis(resize_ms), now);
748 }
749
750 let jain = guard.jain_index();
751 assert!(
752 (0.5..=1.0).contains(&jain),
753 "Jain index {} out of bounds for input={}, resize={}",
754 jain,
755 input_ms,
756 resize_ms
757 );
758 }
759 }
760
761 #[test]
762 fn test_invariant_intervention_resets_dominance() {
763 let config = FairnessConfig::default().with_dominance_threshold(2);
764 let mut guard = InputFairnessGuard::with_config(config);
765 let now = Instant::now();
766
767 guard.input_arrived(now);
769 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
770 guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
771
772 let decision = guard.check_fairness(now);
774 assert!(decision.yield_to_input);
775 assert_eq!(guard.resize_dominance_count, 0);
776 }
777
778 #[test]
779 fn test_invariant_monotonic_stats() {
780 let mut guard = InputFairnessGuard::new();
781 let now = Instant::now();
782
783 let mut prev_checks = 0u64;
784 for _ in 0..10 {
785 guard.check_fairness(now);
786 assert!(guard.stats().total_checks > prev_checks);
787 prev_checks = guard.stats().total_checks;
788 }
789 }
790
791 #[test]
792 fn test_disabled_returns_no_intervention() {
793 let config = FairnessConfig::disabled();
794 let mut guard = InputFairnessGuard::with_config(config);
795 let now = Instant::now();
796
797 guard.input_arrived(now);
799 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
800 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
801 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
802
803 let decision = guard.check_fairness(now);
804 assert!(!decision.yield_to_input);
805 assert_eq!(decision.reason, InterventionReason::None);
806 }
807
808 #[test]
813 fn fairness_decision_fields_match_state() {
814 let mut guard = InputFairnessGuard::new();
815 let now = Instant::now();
816
817 let d = guard.check_fairness(now);
819 assert!(d.pending_input_latency.is_none());
820 assert_eq!(d.reason, InterventionReason::None);
821 assert!(!d.yield_to_input);
822 assert!(d.should_process);
823 assert!((d.jain_index - 1.0).abs() < f64::EPSILON);
824
825 guard.input_arrived(now);
827 let later = now + Duration::from_millis(10);
828 let d = guard.check_fairness(later);
829 assert!(d.pending_input_latency.is_some());
830 let lat = d.pending_input_latency.unwrap();
831 assert!(lat >= Duration::from_millis(10));
832 }
833
834 #[test]
835 fn jain_index_exact_values() {
836 let mut guard = InputFairnessGuard::new();
837 let now = Instant::now();
838
839 guard.event_processed(EventType::Input, Duration::from_millis(100), now);
841 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
842 let j = guard.jain_index();
843 assert!(
844 (j - 1.0).abs() < 1e-9,
845 "Equal allocation should yield 1.0, got {j}"
846 );
847
848 guard.reset();
849
850 guard.event_processed(EventType::Input, Duration::from_millis(1), now);
853 guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
854 let j = guard.jain_index();
855 assert!(j > 0.5, "F should be > 0.5 for two types, got {j}");
856 assert!(j < 0.6, "F should be < 0.6 for 1:100 ratio, got {j}");
857 }
858
859 #[test]
860 fn jain_index_bounded_across_ratios() {
861 let ratios: &[(u64, u64)] = &[
863 (0, 0),
864 (1, 0),
865 (0, 1),
866 (1, 1),
867 (1, 1000),
868 (1000, 1),
869 (50, 50),
870 (100, 1),
871 (999, 1),
872 ];
873 for &(input_ms, resize_ms) in ratios {
874 let mut guard = InputFairnessGuard::new();
875 let now = Instant::now();
876 if input_ms > 0 {
877 guard.event_processed(EventType::Input, Duration::from_millis(input_ms), now);
878 }
879 if resize_ms > 0 {
880 guard.event_processed(EventType::Resize, Duration::from_millis(resize_ms), now);
881 }
882 let j = guard.jain_index();
883 assert!(
884 (0.5..=1.0).contains(&j),
885 "Jain index out of bounds for ({input_ms}, {resize_ms}): {j}"
886 );
887 }
888 }
889
890 #[test]
891 fn intervention_reason_priority_order() {
892 let config = FairnessConfig {
894 input_priority_threshold: Duration::from_millis(20),
895 dominance_threshold: 2,
896 fairness_threshold: 0.9, enabled: true,
898 };
899 let mut guard = InputFairnessGuard::with_config(config);
900 let now = Instant::now();
901
902 guard.input_arrived(now);
905 guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
907 guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
908 guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
909
910 let later = now + Duration::from_millis(100);
912 let d = guard.check_fairness(later);
913
914 assert_eq!(
916 d.reason,
917 InterventionReason::InputLatency,
918 "InputLatency should have highest priority"
919 );
920 assert!(d.yield_to_input);
921 }
922
923 #[test]
924 fn resize_dominance_triggers_after_threshold() {
925 let config = FairnessConfig {
926 dominance_threshold: 3,
927 ..FairnessConfig::default()
928 };
929 let mut guard = InputFairnessGuard::with_config(config);
930 let now = Instant::now();
931
932 guard.input_arrived(now);
934
935 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
937 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
938 let d = guard.check_fairness(now);
939 assert_eq!(d.reason, InterventionReason::None);
940
941 guard.input_arrived(now);
943
944 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
946 let d = guard.check_fairness(now);
947 assert_eq!(d.reason, InterventionReason::ResizeDominance);
948 assert!(d.yield_to_input);
949 }
950
951 #[test]
952 fn intervention_counts_track_each_reason() {
953 let config = FairnessConfig {
954 input_priority_threshold: Duration::from_millis(10),
955 dominance_threshold: 2,
956 fairness_threshold: 0.8,
957 enabled: true,
958 };
959 let mut guard = InputFairnessGuard::with_config(config);
960 let now = Instant::now();
961
962 guard.input_arrived(now);
964 let later = now + Duration::from_millis(50);
965 guard.check_fairness(later);
966
967 let counts = guard.intervention_counts();
968 assert_eq!(counts.input_latency, 1);
969 assert_eq!(counts.resize_dominance, 0);
970 assert_eq!(counts.fairness_index, 0);
971
972 guard.input_arrived(now);
974 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
975 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
976 guard.check_fairness(now);
977
978 let counts = guard.intervention_counts();
979 assert_eq!(counts.resize_dominance, 1);
980 }
981
982 #[test]
983 fn fairness_stable_across_repeated_check_cycles() {
984 let mut guard = InputFairnessGuard::new();
985 let now = Instant::now();
986
987 for i in 0..50 {
989 let t = now + Duration::from_millis(i * 16);
990 guard.event_processed(EventType::Input, Duration::from_millis(5), t);
991 guard.event_processed(EventType::Resize, Duration::from_millis(5), t);
992 let d = guard.check_fairness(t);
993
994 assert!(!d.yield_to_input, "Unexpected intervention at cycle {i}");
996 assert!(
998 d.jain_index > 0.95,
999 "Jain index degraded at cycle {i}: {}",
1000 d.jain_index
1001 );
1002 }
1003
1004 let stats = guard.stats();
1005 assert_eq!(stats.events_processed, 100);
1006 assert_eq!(stats.input_events, 50);
1007 assert_eq!(stats.resize_events, 50);
1008 assert_eq!(stats.total_interventions, 0);
1009 }
1010
1011 #[test]
1012 fn fairness_index_degrades_under_resize_flood() {
1013 let mut guard = InputFairnessGuard::new();
1014 let now = Instant::now();
1015
1016 guard.event_processed(EventType::Input, Duration::from_millis(5), now);
1018 for _ in 0..15 {
1019 guard.event_processed(EventType::Resize, Duration::from_millis(20), now);
1020 }
1021
1022 let j = guard.jain_index();
1023 assert!(
1026 j < 0.55,
1027 "Jain index should be low under resize flood, got {j}"
1028 );
1029 }
1030
1031 #[test]
1032 fn max_input_latency_tracked_across_checks() {
1033 let mut guard = InputFairnessGuard::new();
1034 let now = Instant::now();
1035
1036 guard.input_arrived(now);
1037 guard.check_fairness(now + Duration::from_millis(30));
1038
1039 guard.input_arrived(now + Duration::from_millis(50));
1040 guard.check_fairness(now + Duration::from_millis(100));
1041
1042 let stats = guard.stats();
1043 assert!(stats.max_input_latency >= Duration::from_millis(30));
1045 }
1046
1047 #[test]
1048 fn sliding_window_evicts_oldest_entries() {
1049 let mut guard = InputFairnessGuard::new();
1050 let now = Instant::now();
1051
1052 for _ in 0..16 {
1055 guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
1056 }
1057
1058 for _ in 0..16 {
1060 guard.event_processed(EventType::Input, Duration::from_millis(10), now);
1061 }
1062
1063 let j = guard.jain_index();
1066 assert!(
1068 j < 0.6,
1069 "After full eviction to input-only, Jain should be ~0.5, got {j}"
1070 );
1071 }
1072
1073 #[test]
1074 fn custom_config_thresholds_work() {
1075 let config = FairnessConfig {
1076 input_priority_threshold: Duration::from_millis(200),
1077 dominance_threshold: 10,
1078 fairness_threshold: 0.3,
1079 enabled: true,
1080 };
1081 let mut guard = InputFairnessGuard::with_config(config);
1082 let now = Instant::now();
1083
1084 guard.input_arrived(now);
1086 guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
1087 guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
1088 guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
1089
1090 let later = now + Duration::from_millis(100);
1091 let d = guard.check_fairness(later);
1092 assert_eq!(d.reason, InterventionReason::None);
1093 assert!(!d.yield_to_input);
1094 }
1095}