use std::collections::VecDeque;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RenderStage {
Layout,
Diff,
Present,
}
impl RenderStage {
pub const ALL: [RenderStage; 3] = [Self::Layout, Self::Diff, Self::Present];
pub fn name(self) -> &'static str {
match self {
Self::Layout => "layout",
Self::Diff => "diff",
Self::Present => "present",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct StageObservation {
pub layout_us: f64,
pub diff_us: f64,
pub present_us: f64,
}
impl StageObservation {
pub fn get(&self, stage: RenderStage) -> f64 {
match stage {
RenderStage::Layout => self.layout_us,
RenderStage::Diff => self.diff_us,
RenderStage::Present => self.present_us,
}
}
pub fn total_us(&self) -> f64 {
self.layout_us + self.diff_us + self.present_us
}
}
#[derive(Debug, Clone)]
pub struct StageAlert {
pub stage: RenderStage,
pub is_alert: bool,
pub observed: f64,
pub threshold: f64,
pub e_value: f64,
pub calibration_count: usize,
}
#[derive(Debug, Clone)]
pub struct FrameResult {
pub stages: [StageAlert; 3],
}
impl FrameResult {
pub fn any_alert(&self) -> bool {
self.stages.iter().any(|s| s.is_alert)
}
pub fn alerting_stages(&self) -> Vec<RenderStage> {
self.stages
.iter()
.filter(|s| s.is_alert)
.map(|s| s.stage)
.collect()
}
pub fn stage(&self, stage: RenderStage) -> &StageAlert {
&self.stages[stage as usize]
}
}
#[derive(Debug, Clone)]
pub struct StagedConfig {
pub alpha: f64,
pub max_calibration: usize,
pub min_calibration: usize,
pub lambda: f64,
}
impl Default for StagedConfig {
fn default() -> Self {
Self {
alpha: 0.05,
max_calibration: 500,
min_calibration: 10,
lambda: 0.5,
}
}
}
const E_MIN: f64 = 1e-12;
const E_MAX: f64 = 1e12;
#[derive(Debug, Clone)]
struct StageState {
calibration: VecDeque<f64>,
mean: f64,
m2: f64,
n: u64,
e_value: f64,
}
impl StageState {
fn new() -> Self {
Self {
calibration: VecDeque::new(),
mean: 0.0,
m2: 0.0,
n: 0,
e_value: 1.0,
}
}
fn calibrate(&mut self, value: f64, max_samples: usize) {
self.n += 1;
let delta = value - self.mean;
self.mean += delta / self.n as f64;
let delta2 = value - self.mean;
self.m2 += delta * delta2;
self.calibration.push_back(value);
while self.calibration.len() > max_samples {
self.calibration.pop_front();
}
}
fn variance(&self) -> f64 {
if self.n < 2 {
return 1.0;
}
(self.m2 / (self.n - 1) as f64).max(1e-10)
}
fn conformal_threshold(&self, alpha: f64) -> f64 {
if self.calibration.is_empty() {
return f64::MAX;
}
let n = self.calibration.len();
let mut sorted: Vec<f64> = self.calibration.iter().copied().collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let quantile_idx = (((1.0 - alpha) * (n + 1) as f64).ceil() as usize).min(n) - 1;
sorted[quantile_idx.min(n - 1)]
}
fn update_e_process(&mut self, value: f64, lambda: f64) {
let std = self.variance().sqrt();
let z = if std > 1e-10 {
(value - self.mean) / std
} else {
0.0
};
let log_e = lambda * z - lambda * lambda / 2.0;
self.e_value = (self.e_value * log_e.exp()).clamp(E_MIN, E_MAX);
}
}
#[derive(Debug, Clone)]
pub struct StagedConformalPredictor {
config: StagedConfig,
states: [StageState; 3],
}
impl Default for StagedConformalPredictor {
fn default() -> Self {
Self::new(StagedConfig::default())
}
}
impl StagedConformalPredictor {
pub fn new(config: StagedConfig) -> Self {
Self {
config,
states: [StageState::new(), StageState::new(), StageState::new()],
}
}
pub fn calibrate(&mut self, stage: RenderStage, value: f64) {
self.states[stage as usize].calibrate(value, self.config.max_calibration);
}
pub fn calibrate_frame(&mut self, obs: &StageObservation) {
for stage in RenderStage::ALL {
self.calibrate(stage, obs.get(stage));
}
}
pub fn observe_frame(&mut self, obs: StageObservation) -> FrameResult {
let mut alerts = [
self.observe_stage(RenderStage::Layout, obs.layout_us),
self.observe_stage(RenderStage::Diff, obs.diff_us),
self.observe_stage(RenderStage::Present, obs.present_us),
];
alerts[0].stage = RenderStage::Layout;
alerts[1].stage = RenderStage::Diff;
alerts[2].stage = RenderStage::Present;
FrameResult { stages: alerts }
}
fn observe_stage(&mut self, stage: RenderStage, value: f64) -> StageAlert {
let state = &mut self.states[stage as usize];
let threshold = state.conformal_threshold(self.config.alpha);
let calibration_count = state.calibration.len();
state.update_e_process(value, self.config.lambda);
let is_alert = calibration_count >= self.config.min_calibration
&& value > threshold
&& state.e_value > 1.0 / self.config.alpha;
StageAlert {
stage,
is_alert,
observed: value,
threshold,
e_value: state.e_value,
calibration_count,
}
}
pub fn calibration_count(&self, stage: RenderStage) -> usize {
self.states[stage as usize].calibration.len()
}
pub fn reset(&mut self) {
self.states = [StageState::new(), StageState::new(), StageState::new()];
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let cfg = StagedConfig::default();
assert_eq!(cfg.alpha, 0.05);
assert_eq!(cfg.max_calibration, 500);
assert_eq!(cfg.min_calibration, 10);
}
#[test]
fn render_stage_names() {
assert_eq!(RenderStage::Layout.name(), "layout");
assert_eq!(RenderStage::Diff.name(), "diff");
assert_eq!(RenderStage::Present.name(), "present");
}
#[test]
fn stage_observation_total() {
let obs = StageObservation {
layout_us: 100.0,
diff_us: 50.0,
present_us: 200.0,
};
assert!((obs.total_us() - 350.0).abs() < 1e-10);
}
#[test]
fn stage_observation_get() {
let obs = StageObservation {
layout_us: 100.0,
diff_us: 50.0,
present_us: 200.0,
};
assert!((obs.get(RenderStage::Layout) - 100.0).abs() < 1e-10);
assert!((obs.get(RenderStage::Diff) - 50.0).abs() < 1e-10);
assert!((obs.get(RenderStage::Present) - 200.0).abs() < 1e-10);
}
#[test]
fn no_alert_during_calibration() {
let mut pred = StagedConformalPredictor::default();
for _ in 0..5 {
pred.calibrate(RenderStage::Layout, 100.0);
}
let result = pred.observe_frame(StageObservation {
layout_us: 999.0, diff_us: 0.0,
present_us: 0.0,
});
assert!(!result.stage(RenderStage::Layout).is_alert);
}
#[test]
fn alert_on_regression() {
let mut pred = StagedConformalPredictor::default();
for _ in 0..50 {
pred.calibrate_frame(&StageObservation {
layout_us: 100.0,
diff_us: 50.0,
present_us: 200.0,
});
}
let mut alerted = false;
for _ in 0..20 {
let result = pred.observe_frame(StageObservation {
layout_us: 500.0, diff_us: 50.0,
present_us: 200.0,
});
if result.any_alert() {
alerted = true;
assert!(result.stage(RenderStage::Layout).is_alert);
assert!(!result.stage(RenderStage::Diff).is_alert);
assert!(!result.stage(RenderStage::Present).is_alert);
break;
}
}
assert!(alerted, "Should have alerted on 5x layout regression");
}
#[test]
fn no_alert_on_normal() {
let mut pred = StagedConformalPredictor::default();
for _ in 0..50 {
pred.calibrate_frame(&StageObservation {
layout_us: 100.0,
diff_us: 50.0,
present_us: 200.0,
});
}
for _ in 0..20 {
let result = pred.observe_frame(StageObservation {
layout_us: 100.0,
diff_us: 50.0,
present_us: 200.0,
});
assert!(!result.any_alert(), "Should not alert on normal frames");
}
}
#[test]
fn independent_stage_tracking() {
let mut pred = StagedConformalPredictor::default();
for _ in 0..50 {
pred.calibrate(RenderStage::Layout, 100.0);
}
assert_eq!(pred.calibration_count(RenderStage::Layout), 50);
assert_eq!(pred.calibration_count(RenderStage::Diff), 0);
assert_eq!(pred.calibration_count(RenderStage::Present), 0);
}
#[test]
fn reset_clears_state() {
let mut pred = StagedConformalPredictor::default();
for _ in 0..20 {
pred.calibrate(RenderStage::Layout, 100.0);
}
assert_eq!(pred.calibration_count(RenderStage::Layout), 20);
pred.reset();
assert_eq!(pred.calibration_count(RenderStage::Layout), 0);
}
#[test]
fn alerting_stages_list() {
let result = FrameResult {
stages: [
StageAlert {
stage: RenderStage::Layout,
is_alert: true,
observed: 500.0,
threshold: 120.0,
e_value: 100.0,
calibration_count: 50,
},
StageAlert {
stage: RenderStage::Diff,
is_alert: false,
observed: 50.0,
threshold: 80.0,
e_value: 0.5,
calibration_count: 50,
},
StageAlert {
stage: RenderStage::Present,
is_alert: true,
observed: 800.0,
threshold: 250.0,
e_value: 200.0,
calibration_count: 50,
},
],
};
let alerting = result.alerting_stages();
assert_eq!(alerting.len(), 2);
assert!(alerting.contains(&RenderStage::Layout));
assert!(alerting.contains(&RenderStage::Present));
}
#[test]
fn calibration_window_bounded() {
let cfg = StagedConfig {
max_calibration: 20,
..Default::default()
};
let mut pred = StagedConformalPredictor::new(cfg);
for i in 0..100 {
pred.calibrate(RenderStage::Layout, i as f64);
}
assert_eq!(pred.calibration_count(RenderStage::Layout), 20);
}
}