#![forbid(unsafe_code)]
use std::collections::VecDeque;
use web_time::{Duration, Instant};
const DEFAULT_MAX_INPUT_LATENCY_MS: u64 = 50;
const DEFAULT_DOMINANCE_THRESHOLD: u32 = 3;
const DEFAULT_FAIRNESS_THRESHOLD: f64 = 0.8;
const FAIRNESS_WINDOW_SIZE: usize = 16;
const FAIRNESS_THRESHOLD_EPSILON: f64 = 1e-12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventType {
Input,
Resize,
Tick,
}
pub type FairnessEventType = EventType;
#[derive(Debug, Clone)]
pub struct FairnessConfig {
pub input_priority_threshold: Duration,
pub enabled: bool,
pub dominance_threshold: u32,
pub fairness_threshold: f64,
}
impl Default for FairnessConfig {
fn default() -> Self {
Self {
input_priority_threshold: Duration::from_millis(DEFAULT_MAX_INPUT_LATENCY_MS),
enabled: true, dominance_threshold: DEFAULT_DOMINANCE_THRESHOLD,
fairness_threshold: DEFAULT_FAIRNESS_THRESHOLD,
}
}
}
impl FairnessConfig {
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
#[must_use]
pub fn with_max_latency(mut self, latency: Duration) -> Self {
self.input_priority_threshold = latency;
self
}
#[must_use]
pub fn with_dominance_threshold(mut self, threshold: u32) -> Self {
self.dominance_threshold = threshold;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterventionReason {
None,
InputLatency,
ResizeDominance,
FairnessIndex,
}
impl InterventionReason {
pub fn requires_intervention(&self) -> bool {
!matches!(self, InterventionReason::None)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::InputLatency => "input_latency",
Self::ResizeDominance => "resize_dominance",
Self::FairnessIndex => "fairness_index",
}
}
}
#[derive(Debug, Clone)]
pub struct FairnessDecision {
pub should_process: bool,
pub pending_input_latency: Option<Duration>,
pub reason: InterventionReason,
pub yield_to_input: bool,
pub jain_index: f64,
}
impl Default for FairnessDecision {
fn default() -> Self {
Self {
should_process: true,
pending_input_latency: None,
reason: InterventionReason::None,
yield_to_input: false,
jain_index: 1.0, }
}
}
#[derive(Debug, Clone)]
pub struct FairnessLogEntry {
pub timestamp: Instant,
pub event_type: EventType,
pub duration: Duration,
}
#[derive(Debug, Clone, Default)]
pub struct FairnessStats {
pub events_processed: u64,
pub input_events: u64,
pub resize_events: u64,
pub tick_events: u64,
pub total_checks: u64,
pub total_interventions: u64,
pub max_input_latency: Duration,
}
#[derive(Debug, Clone, Default)]
pub struct InterventionCounts {
pub input_latency: u64,
pub resize_dominance: u64,
pub fairness_index: u64,
}
#[derive(Debug, Clone)]
struct ProcessingRecord {
event_type: EventType,
duration: Duration,
}
#[derive(Debug)]
pub struct InputFairnessGuard {
config: FairnessConfig,
stats: FairnessStats,
intervention_counts: InterventionCounts,
pending_input_arrival: Option<Instant>,
recent_input_arrival: Option<Instant>,
resize_dominance_count: u32,
processing_window: VecDeque<ProcessingRecord>,
input_time_us: u128,
resize_time_us: u128,
}
impl InputFairnessGuard {
pub fn new() -> Self {
Self::with_config(FairnessConfig::default())
}
pub fn with_config(config: FairnessConfig) -> Self {
Self {
config,
stats: FairnessStats::default(),
intervention_counts: InterventionCounts::default(),
pending_input_arrival: None,
recent_input_arrival: None,
resize_dominance_count: 0,
processing_window: VecDeque::with_capacity(FAIRNESS_WINDOW_SIZE),
input_time_us: 0,
resize_time_us: 0,
}
}
pub fn input_arrived(&mut self, now: Instant) {
if self.pending_input_arrival.is_none() {
self.pending_input_arrival = Some(now);
}
self.recent_input_arrival = Some(now);
}
pub fn check_fairness(&mut self, now: Instant) -> FairnessDecision {
self.stats.total_checks += 1;
if !self.config.enabled {
self.recent_input_arrival = None;
return FairnessDecision::default();
}
let jain = self.calculate_jain_index();
let has_pending_input = self.pending_input_arrival.is_some();
let pending_latency = self
.pending_input_arrival
.or(self.recent_input_arrival)
.map(|t| now.checked_duration_since(t).unwrap_or(Duration::ZERO));
if has_pending_input
&& let Some(latency) = pending_latency
&& latency > self.stats.max_input_latency
{
self.stats.max_input_latency = latency;
}
let reason = self.determine_intervention_reason(pending_latency, jain, has_pending_input);
let yield_to_input = reason.requires_intervention();
if yield_to_input {
self.stats.total_interventions += 1;
match reason {
InterventionReason::InputLatency => {
self.intervention_counts.input_latency += 1;
}
InterventionReason::ResizeDominance => {
self.intervention_counts.resize_dominance += 1;
}
InterventionReason::FairnessIndex => {
self.intervention_counts.fairness_index += 1;
}
InterventionReason::None => {}
}
self.resize_dominance_count = 0;
}
let decision = FairnessDecision {
should_process: !yield_to_input,
pending_input_latency: if has_pending_input {
pending_latency
} else {
None
},
reason,
yield_to_input,
jain_index: jain,
};
self.recent_input_arrival = None;
decision
}
pub fn event_processed(&mut self, event_type: EventType, duration: Duration, _now: Instant) {
self.stats.events_processed += 1;
match event_type {
EventType::Input => self.stats.input_events += 1,
EventType::Resize => self.stats.resize_events += 1,
EventType::Tick => self.stats.tick_events += 1,
}
if !self.config.enabled {
return;
}
let record = ProcessingRecord {
event_type,
duration,
};
if self.processing_window.len() >= FAIRNESS_WINDOW_SIZE
&& let Some(old) = self.processing_window.pop_front()
{
match old.event_type {
EventType::Input => {
self.input_time_us =
self.input_time_us.saturating_sub(old.duration.as_micros());
}
EventType::Resize => {
self.resize_time_us =
self.resize_time_us.saturating_sub(old.duration.as_micros());
}
EventType::Tick => {}
}
}
match event_type {
EventType::Input => {
self.input_time_us = self.input_time_us.saturating_add(duration.as_micros());
self.pending_input_arrival = None;
self.resize_dominance_count = 0; }
EventType::Resize => {
self.resize_time_us = self.resize_time_us.saturating_add(duration.as_micros());
self.resize_dominance_count = self.resize_dominance_count.saturating_add(1);
}
EventType::Tick => {}
}
self.processing_window.push_back(record);
}
fn calculate_jain_index(&self) -> f64 {
let x = self.input_time_us as f64;
let y = self.resize_time_us as f64;
if x == 0.0 && y == 0.0 {
return 1.0; }
let sum = x + y;
let sum_sq = x * x + y * y;
if sum_sq == 0.0 {
return 1.0;
}
(sum * sum) / (2.0 * sum_sq)
}
fn determine_intervention_reason(
&self,
pending_latency: Option<Duration>,
jain: f64,
has_pending_input: bool,
) -> InterventionReason {
if has_pending_input
&& let Some(latency) = pending_latency
&& latency >= self.config.input_priority_threshold
{
return InterventionReason::InputLatency;
}
if has_pending_input && self.resize_dominance_count >= self.config.dominance_threshold {
return InterventionReason::ResizeDominance;
}
if has_pending_input
&& jain + FAIRNESS_THRESHOLD_EPSILON < self.config.fairness_threshold
&& self.resize_time_us > self.input_time_us
{
return InterventionReason::FairnessIndex;
}
InterventionReason::None
}
pub fn stats(&self) -> &FairnessStats {
&self.stats
}
pub fn intervention_counts(&self) -> &InterventionCounts {
&self.intervention_counts
}
pub fn config(&self) -> &FairnessConfig {
&self.config
}
pub fn resize_dominance_count(&self) -> u32 {
self.resize_dominance_count
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub fn jain_index(&self) -> f64 {
self.calculate_jain_index()
}
pub fn has_pending_input(&self) -> bool {
self.pending_input_arrival.is_some()
}
pub fn reset(&mut self) {
self.pending_input_arrival = None;
self.recent_input_arrival = None;
self.resize_dominance_count = 0;
self.processing_window.clear();
self.input_time_us = 0;
self.resize_time_us = 0;
self.stats = FairnessStats::default();
self.intervention_counts = InterventionCounts::default();
}
}
impl Default for InputFairnessGuard {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_is_enabled() {
let config = FairnessConfig::default();
assert!(config.enabled);
}
#[test]
fn default_fairness_threshold_is_above_two_class_floor() {
let config = FairnessConfig::default();
assert!(config.fairness_threshold > 0.5 + FAIRNESS_THRESHOLD_EPSILON);
}
#[test]
fn disabled_config() {
let config = FairnessConfig::disabled();
assert!(!config.enabled);
}
#[test]
fn default_decision_allows_processing() {
let mut guard = InputFairnessGuard::default();
let decision = guard.check_fairness(Instant::now());
assert!(decision.should_process);
}
#[test]
fn event_processing_updates_stats() {
let mut guard = InputFairnessGuard::default();
let now = Instant::now();
guard.event_processed(EventType::Input, Duration::from_millis(10), now);
guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
guard.event_processed(EventType::Tick, Duration::from_millis(1), now);
let stats = guard.stats();
assert_eq!(stats.events_processed, 3);
assert_eq!(stats.input_events, 1);
assert_eq!(stats.resize_events, 1);
assert_eq!(stats.tick_events, 1);
}
#[test]
fn event_processing_duration_counters_do_not_truncate() {
let mut guard = InputFairnessGuard::default();
let now = Instant::now();
guard.event_processed(EventType::Input, Duration::MAX, now);
guard.event_processed(EventType::Input, Duration::from_micros(1), now);
guard.event_processed(EventType::Resize, Duration::MAX, now);
guard.event_processed(EventType::Resize, Duration::from_micros(1), now);
let expected = Duration::MAX.as_micros().saturating_add(1);
assert_eq!(guard.input_time_us, expected);
assert_eq!(guard.resize_time_us, expected);
}
#[test]
fn test_jain_index_perfect_fairness() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.event_processed(EventType::Input, Duration::from_millis(10), now);
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
let jain = guard.jain_index();
assert!((jain - 1.0).abs() < 0.001, "Expected ~1.0, got {}", jain);
}
#[test]
fn test_jain_index_unfair() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.event_processed(EventType::Input, Duration::from_millis(1), now);
guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
let jain = guard.jain_index();
assert!(jain < 0.6, "Expected unfair index < 0.6, got {}", jain);
}
#[test]
fn test_jain_index_empty() {
let guard = InputFairnessGuard::new();
let jain = guard.jain_index();
assert!((jain - 1.0).abs() < 0.001, "Empty should be fair (1.0)");
}
#[test]
fn test_latency_threshold_intervention() {
let config = FairnessConfig::default().with_max_latency(Duration::from_millis(20));
let mut guard = InputFairnessGuard::with_config(config);
let start = Instant::now();
guard.input_arrived(start);
let decision = guard.check_fairness(start + Duration::from_millis(25));
assert!(decision.yield_to_input);
assert_eq!(decision.reason, InterventionReason::InputLatency);
}
#[test]
fn test_resize_dominance_intervention() {
let config = FairnessConfig::default().with_dominance_threshold(2);
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
let decision = guard.check_fairness(now);
assert!(decision.yield_to_input);
assert_eq!(decision.reason, InterventionReason::ResizeDominance);
}
#[test]
fn test_no_intervention_when_fair() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.event_processed(EventType::Input, Duration::from_millis(10), now);
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
let decision = guard.check_fairness(now);
assert!(!decision.yield_to_input);
assert_eq!(decision.reason, InterventionReason::None);
}
#[test]
fn test_fairness_index_intervention() {
let config = FairnessConfig {
input_priority_threshold: Duration::from_secs(10),
dominance_threshold: 100,
fairness_threshold: 0.9,
..Default::default()
};
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.event_processed(EventType::Input, Duration::from_millis(1), now);
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
let decision = guard.check_fairness(now + Duration::from_millis(1));
assert!(decision.yield_to_input);
assert_eq!(decision.reason, InterventionReason::FairnessIndex);
}
#[test]
fn fairness_index_triggers_when_input_is_starved_in_window() {
let config = FairnessConfig {
input_priority_threshold: Duration::from_secs(10),
dominance_threshold: 100,
fairness_threshold: 0.9,
..Default::default()
};
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
let decision = guard.check_fairness(now);
assert_eq!(decision.reason, InterventionReason::FairnessIndex);
assert!(decision.yield_to_input);
}
#[test]
fn test_dominance_reset_on_input() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
assert_eq!(guard.resize_dominance_count, 2);
guard.event_processed(EventType::Input, Duration::from_millis(5), now);
assert_eq!(guard.resize_dominance_count, 0);
}
#[test]
fn test_pending_input_cleared_on_processing() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.input_arrived(now);
assert!(guard.has_pending_input());
guard.event_processed(EventType::Input, Duration::from_millis(5), now);
assert!(!guard.has_pending_input());
}
#[test]
fn no_intervention_without_pending_input_under_resize_flood() {
let config = FairnessConfig {
input_priority_threshold: Duration::from_millis(1),
dominance_threshold: 1,
fairness_threshold: 0.99,
enabled: true,
};
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
let decision = guard.check_fairness(now + Duration::from_millis(50));
assert!(!decision.yield_to_input);
assert_eq!(decision.reason, InterventionReason::None);
assert!(decision.pending_input_latency.is_none());
}
#[test]
fn processed_input_does_not_cause_spurious_followup_intervention() {
let config = FairnessConfig {
input_priority_threshold: Duration::from_millis(1),
dominance_threshold: 1,
fairness_threshold: 0.99,
enabled: true,
};
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Input, Duration::from_millis(1), now);
guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
let decision = guard.check_fairness(now + Duration::from_millis(50));
assert!(!decision.yield_to_input);
assert_eq!(decision.reason, InterventionReason::None);
assert!(decision.pending_input_latency.is_none());
}
#[test]
fn test_stats_tracking() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.check_fairness(now);
guard.check_fairness(now);
assert_eq!(guard.stats().total_checks, 2);
}
#[test]
fn test_sliding_window_eviction() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
for _ in 0..(FAIRNESS_WINDOW_SIZE + 5) {
guard.event_processed(EventType::Input, Duration::from_millis(1), now);
}
assert_eq!(guard.processing_window.len(), FAIRNESS_WINDOW_SIZE);
}
#[test]
fn test_reset() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
guard.check_fairness(now);
guard.reset();
assert!(!guard.has_pending_input());
assert_eq!(guard.resize_dominance_count, 0);
assert_eq!(guard.stats().total_checks, 0);
assert!(guard.processing_window.is_empty());
}
#[test]
fn test_invariant_jain_index_bounds() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
for (input_ms, resize_ms) in [(1, 1), (1, 100), (100, 1), (50, 50), (0, 100), (100, 0)] {
guard.reset();
if input_ms > 0 {
guard.event_processed(EventType::Input, Duration::from_millis(input_ms), now);
}
if resize_ms > 0 {
guard.event_processed(EventType::Resize, Duration::from_millis(resize_ms), now);
}
let jain = guard.jain_index();
assert!(
(0.5..=1.0).contains(&jain),
"Jain index {} out of bounds for input={}, resize={}",
jain,
input_ms,
resize_ms
);
}
}
#[test]
fn test_invariant_intervention_resets_dominance() {
let config = FairnessConfig::default().with_dominance_threshold(2);
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
guard.event_processed(EventType::Resize, Duration::from_millis(5), now);
let decision = guard.check_fairness(now);
assert!(decision.yield_to_input);
assert_eq!(guard.resize_dominance_count, 0);
}
#[test]
fn test_invariant_monotonic_stats() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
let mut prev_checks = 0u64;
for _ in 0..10 {
guard.check_fairness(now);
assert!(guard.stats().total_checks > prev_checks);
prev_checks = guard.stats().total_checks;
}
}
#[test]
fn test_disabled_returns_no_intervention() {
let config = FairnessConfig::disabled();
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
let decision = guard.check_fairness(now);
assert!(!decision.yield_to_input);
assert_eq!(decision.reason, InterventionReason::None);
}
#[test]
fn fairness_decision_fields_match_state() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
let d = guard.check_fairness(now);
assert!(d.pending_input_latency.is_none());
assert_eq!(d.reason, InterventionReason::None);
assert!(!d.yield_to_input);
assert!(d.should_process);
assert!((d.jain_index - 1.0).abs() < f64::EPSILON);
guard.input_arrived(now);
let later = now + Duration::from_millis(10);
let d = guard.check_fairness(later);
assert!(d.pending_input_latency.is_some());
let lat = d.pending_input_latency.unwrap();
assert!(lat >= Duration::from_millis(10));
}
#[test]
fn jain_index_exact_values() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.event_processed(EventType::Input, Duration::from_millis(100), now);
guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
let j = guard.jain_index();
assert!(
(j - 1.0).abs() < 1e-9,
"Equal allocation should yield 1.0, got {j}"
);
guard.reset();
guard.event_processed(EventType::Input, Duration::from_millis(1), now);
guard.event_processed(EventType::Resize, Duration::from_millis(100), now);
let j = guard.jain_index();
assert!(j > 0.5, "F should be > 0.5 for two types, got {j}");
assert!(j < 0.6, "F should be < 0.6 for 1:100 ratio, got {j}");
}
#[test]
fn jain_index_bounded_across_ratios() {
let ratios: &[(u64, u64)] = &[
(0, 0),
(1, 0),
(0, 1),
(1, 1),
(1, 1000),
(1000, 1),
(50, 50),
(100, 1),
(999, 1),
];
for &(input_ms, resize_ms) in ratios {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
if input_ms > 0 {
guard.event_processed(EventType::Input, Duration::from_millis(input_ms), now);
}
if resize_ms > 0 {
guard.event_processed(EventType::Resize, Duration::from_millis(resize_ms), now);
}
let j = guard.jain_index();
assert!(
(0.5..=1.0).contains(&j),
"Jain index out of bounds for ({input_ms}, {resize_ms}): {j}"
);
}
}
#[test]
fn intervention_reason_priority_order() {
let config = FairnessConfig {
input_priority_threshold: Duration::from_millis(20),
dominance_threshold: 2,
fairness_threshold: 0.9, enabled: true,
};
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
let later = now + Duration::from_millis(100);
let d = guard.check_fairness(later);
assert_eq!(
d.reason,
InterventionReason::InputLatency,
"InputLatency should have highest priority"
);
assert!(d.yield_to_input);
}
#[test]
fn resize_dominance_triggers_after_threshold() {
let config = FairnessConfig {
dominance_threshold: 3,
fairness_threshold: 0.5,
..FairnessConfig::default()
};
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
let d = guard.check_fairness(now);
assert_eq!(d.reason, InterventionReason::None);
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
let d = guard.check_fairness(now);
assert_eq!(d.reason, InterventionReason::ResizeDominance);
assert!(d.yield_to_input);
}
#[test]
fn intervention_counts_track_each_reason() {
let config = FairnessConfig {
input_priority_threshold: Duration::from_millis(10),
dominance_threshold: 2,
fairness_threshold: 0.8,
enabled: true,
};
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.input_arrived(now);
let later = now + Duration::from_millis(50);
guard.check_fairness(later);
let counts = guard.intervention_counts();
assert_eq!(counts.input_latency, 1);
assert_eq!(counts.resize_dominance, 0);
assert_eq!(counts.fairness_index, 0);
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
guard.check_fairness(now);
let counts = guard.intervention_counts();
assert_eq!(counts.resize_dominance, 1);
}
#[test]
fn fairness_stable_across_repeated_check_cycles() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
for i in 0..50 {
let t = now + Duration::from_millis(i * 16);
guard.event_processed(EventType::Input, Duration::from_millis(5), t);
guard.event_processed(EventType::Resize, Duration::from_millis(5), t);
let d = guard.check_fairness(t);
assert!(!d.yield_to_input, "Unexpected intervention at cycle {i}");
assert!(
d.jain_index > 0.95,
"Jain index degraded at cycle {i}: {}",
d.jain_index
);
}
let stats = guard.stats();
assert_eq!(stats.events_processed, 100);
assert_eq!(stats.input_events, 50);
assert_eq!(stats.resize_events, 50);
assert_eq!(stats.total_interventions, 0);
}
#[test]
fn fairness_index_degrades_under_resize_flood() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.event_processed(EventType::Input, Duration::from_millis(5), now);
for _ in 0..15 {
guard.event_processed(EventType::Resize, Duration::from_millis(20), now);
}
let j = guard.jain_index();
assert!(
j < 0.55,
"Jain index should be low under resize flood, got {j}"
);
}
#[test]
fn max_input_latency_tracked_across_checks() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.input_arrived(now);
guard.check_fairness(now + Duration::from_millis(30));
guard.input_arrived(now + Duration::from_millis(50));
guard.check_fairness(now + Duration::from_millis(100));
let stats = guard.stats();
assert!(stats.max_input_latency >= Duration::from_millis(30));
}
#[test]
fn max_input_latency_ignores_recent_when_no_pending_input() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Input, Duration::from_millis(1), now);
guard.check_fairness(now + Duration::from_millis(100));
assert_eq!(guard.stats().max_input_latency, Duration::ZERO);
}
#[test]
fn sliding_window_evicts_oldest_entries() {
let mut guard = InputFairnessGuard::new();
let now = Instant::now();
for _ in 0..16 {
guard.event_processed(EventType::Resize, Duration::from_millis(10), now);
}
for _ in 0..16 {
guard.event_processed(EventType::Input, Duration::from_millis(10), now);
}
let j = guard.jain_index();
assert!(
j < 0.6,
"After full eviction to input-only, Jain should be ~0.5, got {j}"
);
}
#[test]
fn custom_config_thresholds_work() {
let config = FairnessConfig {
input_priority_threshold: Duration::from_millis(200),
dominance_threshold: 10,
fairness_threshold: 0.3,
enabled: true,
};
let mut guard = InputFairnessGuard::with_config(config);
let now = Instant::now();
guard.input_arrived(now);
guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
guard.event_processed(EventType::Resize, Duration::from_millis(50), now);
let later = now + Duration::from_millis(100);
let d = guard.check_fairness(later);
assert_eq!(d.reason, InterventionReason::None);
assert!(!d.yield_to_input);
}
}