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}