Skip to main content

pfx_bench/
summary.rs

1use serde_json::{Map, Number, Value};
2
3pub const FRAME_MS: f64 = 1000.0 / 60.0;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct Spread {
7    pub p50: f64,
8    pub p95: f64,
9    pub p99: f64,
10    pub max: f64,
11}
12
13impl Spread {
14    pub fn of(values: &[f64]) -> Option<Self> {
15        if values.is_empty() {
16            return None;
17        }
18        let mut sorted = values.to_vec();
19        sorted.sort_by(f64::total_cmp);
20        let rank = |percent: f64| {
21            let at = (percent / 100.0 * sorted.len() as f64).ceil() as usize;
22            sorted[at.clamp(1, sorted.len()) - 1]
23        };
24        Some(Self {
25            p50: rank(50.0),
26            p95: rank(95.0),
27            p99: rank(99.0),
28            max: sorted[sorted.len() - 1],
29        })
30    }
31
32    fn json(spread: Option<Self>) -> Value {
33        let Some(spread) = spread else {
34            return Value::Null;
35        };
36        let mut object = Map::new();
37        for (key, value) in [
38            ("p50", spread.p50),
39            ("p95", spread.p95),
40            ("p99", spread.p99),
41            ("max", spread.max),
42        ] {
43            object.insert(key.into(), milliseconds(value));
44        }
45        Value::Object(object)
46    }
47}
48
49fn milliseconds(value: f64) -> Value {
50    Number::from_f64((value * 1000.0).round() / 1000.0).map_or(Value::Null, Value::Number)
51}
52
53#[derive(Clone, Debug, PartialEq)]
54pub struct Summary {
55    pub frames: u64,
56    pub dropped: u64,
57    pub sim: Option<Spread>,
58    pub submit: Option<Spread>,
59    pub gpu: Option<Spread>,
60    pub over: u64,
61}
62
63pub fn summarize(stats: &str) -> Result<Summary, String> {
64    let mut sim = Vec::new();
65    let mut submit = Vec::new();
66    let mut gpu = Vec::new();
67    let mut over = 0;
68    let mut frames = 0;
69    let mut dropped = 0;
70    for line in stats.lines().filter(|line| !line.trim().is_empty()) {
71        let value: Value = serde_json::from_str(line)
72            .map_err(|error| format!("frame stats line {line:?}: {error}"))?;
73        if value.get("end").and_then(Value::as_bool) == Some(true) {
74            frames = value["frames"].as_u64().unwrap_or(frames);
75            dropped = value["dropped"].as_u64().unwrap_or(0);
76            continue;
77        }
78        if value["tick"].as_u64() == Some(0) {
79            continue;
80        }
81        let sim_ms = value["sim_ms"].as_f64();
82        let submit_ms = value["submit_ms"].as_f64();
83        let gpu_ms = value["gpu_ms"].as_f64();
84        sim.extend(sim_ms);
85        submit.extend(submit_ms);
86        gpu.extend(gpu_ms);
87        let cpu_ms = sim_ms.unwrap_or(0.0) + submit_ms.unwrap_or(0.0);
88        if cpu_ms > FRAME_MS || gpu_ms.is_some_and(|ms| ms > FRAME_MS) {
89            over += 1;
90        }
91    }
92    Ok(Summary {
93        frames,
94        dropped,
95        sim: Spread::of(&sim),
96        submit: Spread::of(&submit),
97        gpu: Spread::of(&gpu),
98        over,
99    })
100}
101
102impl Summary {
103    pub fn json(&self, fields: Map<String, Value>) -> Value {
104        let mut object = Map::new();
105        object.insert("schema_version".into(), 1.into());
106        object.insert("bench".into(), true.into());
107        object.extend(fields);
108        object.insert("frames".into(), self.frames.into());
109        object.insert("dropped".into(), self.dropped.into());
110        object.insert("sim_ms".into(), Spread::json(self.sim));
111        object.insert("submit_ms".into(), Spread::json(self.submit));
112        object.insert("gpu_ms".into(), Spread::json(self.gpu));
113        object.insert("over_16_67".into(), self.over.into());
114        Value::Object(object)
115    }
116}