use super::models::ComplexityClass;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SizeMeasurement {
pub input_size: u64,
pub time_ms: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScalingResult {
pub best_fit: ComplexityClass,
pub r_squared: f64,
pub coefficients: Vec<f64>,
pub above_threshold: bool,
pub all_fits: Vec<(ComplexityClass, f64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScalingReport {
pub schema: String,
pub name: String,
pub command: String,
pub sizes: Vec<u64>,
pub repeat: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected: Option<ComplexityClass>,
pub measurements: Vec<SizeMeasurement>,
pub result: ScalingResult,
pub pass: bool,
pub verdict: String,
}
pub const SCALING_SCHEMA_V1: &str = "perfgate.scaling.v1";
impl ScalingReport {
pub fn new(
name: String,
command: String,
sizes: Vec<u64>,
repeat: u32,
expected: Option<ComplexityClass>,
measurements: Vec<SizeMeasurement>,
result: ScalingResult,
) -> Self {
let (pass, verdict) = Self::compute_verdict(&result, expected);
Self {
schema: SCALING_SCHEMA_V1.to_string(),
name,
command,
sizes,
repeat,
expected,
measurements,
result,
pass,
verdict,
}
}
fn compute_verdict(
result: &ScalingResult,
expected: Option<ComplexityClass>,
) -> (bool, String) {
if !result.above_threshold {
return (
false,
format!(
"No model fits well (best: {} with R^2={:.3})",
result.best_fit, result.r_squared
),
);
}
match expected {
Some(expected_class) => {
if super::is_complexity_degraded(expected_class, result.best_fit) {
(
false,
format!(
"Complexity degradation: expected {}, detected {} (R^2={:.3})",
expected_class, result.best_fit, result.r_squared
),
)
} else {
(
true,
format!(
"Scaling validated: detected {} (expected {}, R^2={:.3})",
result.best_fit, expected_class, result.r_squared
),
)
}
}
None => (
true,
format!(
"Detected complexity: {} (R^2={:.3})",
result.best_fit, result.r_squared
),
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn size_measurement_serde_round_trip() {
let m = SizeMeasurement {
input_size: 1000,
time_ms: 42.5,
};
let json = serde_json::to_string(&m).unwrap();
let back: SizeMeasurement = serde_json::from_str(&json).unwrap();
assert_eq!(back.input_size, m.input_size);
assert!((back.time_ms - m.time_ms).abs() < f64::EPSILON);
}
#[test]
fn scaling_result_serde_round_trip() {
let result = ScalingResult {
best_fit: ComplexityClass::ON,
r_squared: 0.99,
coefficients: vec![0.1, 0.5],
above_threshold: true,
all_fits: vec![(ComplexityClass::ON, 0.99), (ComplexityClass::ONLogN, 0.95)],
};
let json = serde_json::to_string(&result).unwrap();
let back: ScalingResult = serde_json::from_str(&json).unwrap();
assert_eq!(back.best_fit, ComplexityClass::ON);
assert!((back.r_squared - 0.99).abs() < f64::EPSILON);
}
#[test]
fn scaling_report_pass_without_expected() {
let result = ScalingResult {
best_fit: ComplexityClass::ON,
r_squared: 0.99,
coefficients: vec![0.1, 0.5],
above_threshold: true,
all_fits: vec![(ComplexityClass::ON, 0.99)],
};
let report = ScalingReport::new(
"test".into(),
"echo {n}".into(),
vec![100, 200, 400],
5,
None,
vec![],
result,
);
assert!(report.pass);
assert!(report.verdict.contains("Detected complexity: O(n)"));
}
#[test]
fn scaling_report_pass_with_matching_expected() {
let result = ScalingResult {
best_fit: ComplexityClass::ON,
r_squared: 0.99,
coefficients: vec![0.1, 0.5],
above_threshold: true,
all_fits: vec![(ComplexityClass::ON, 0.99)],
};
let report = ScalingReport::new(
"test".into(),
"echo {n}".into(),
vec![100, 200, 400],
5,
Some(ComplexityClass::ON),
vec![],
result,
);
assert!(report.pass);
assert!(report.verdict.contains("Scaling validated"));
}
#[test]
fn scaling_report_fail_with_degradation() {
let result = ScalingResult {
best_fit: ComplexityClass::ON2,
r_squared: 0.99,
coefficients: vec![0.001, 0.5],
above_threshold: true,
all_fits: vec![(ComplexityClass::ON2, 0.99)],
};
let report = ScalingReport::new(
"test".into(),
"echo {n}".into(),
vec![100, 200, 400],
5,
Some(ComplexityClass::ON),
vec![],
result,
);
assert!(!report.pass);
assert!(report.verdict.contains("Complexity degradation"));
}
#[test]
fn scaling_report_fail_low_r_squared() {
let result = ScalingResult {
best_fit: ComplexityClass::ON,
r_squared: 0.5,
coefficients: vec![0.1, 0.5],
above_threshold: false,
all_fits: vec![(ComplexityClass::ON, 0.5)],
};
let report = ScalingReport::new(
"test".into(),
"echo {n}".into(),
vec![100, 200, 400],
5,
Some(ComplexityClass::ON),
vec![],
result,
);
assert!(!report.pass);
assert!(report.verdict.contains("No model fits well"));
}
#[test]
fn scaling_report_schema() {
let result = ScalingResult {
best_fit: ComplexityClass::O1,
r_squared: 1.0,
coefficients: vec![5.0],
above_threshold: true,
all_fits: vec![(ComplexityClass::O1, 1.0)],
};
let report = ScalingReport::new(
"test".into(),
"echo {n}".into(),
vec![100],
1,
None,
vec![],
result,
);
assert_eq!(report.schema, SCALING_SCHEMA_V1);
}
#[test]
fn scaling_report_pass_better_than_expected() {
let result = ScalingResult {
best_fit: ComplexityClass::O1,
r_squared: 0.99,
coefficients: vec![5.0],
above_threshold: true,
all_fits: vec![(ComplexityClass::O1, 0.99)],
};
let report = ScalingReport::new(
"test".into(),
"echo {n}".into(),
vec![100, 200, 400],
5,
Some(ComplexityClass::ON),
vec![],
result,
);
assert!(report.pass);
}
}