Skip to main content

botstadia_interface/
metrics.rs

1//! The metrics file: everything the site draws about a session, beside its
2//! replay archive.
3//!
4//! It carries the platform's per-turn metrics and the arena's declared series,
5//! events, stats and result, so the site draws a replay page from this file
6//! and the declaration alone, and never decodes an arena's frames. A series
7//! that would repeat itself is recorded on change, to keep the file small.
8//!
9//! JSON, since the site reads it on every visit and it stays small.
10
11use serde::{Deserialize, Serialize};
12
13use crate::declaration::{Declaration, SeriesScope};
14
15/// Bumped on any change to these types.
16pub const METRICS_FORMAT: u32 = 1;
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct Metrics {
20    pub format: u32,
21    pub arena_id: String,
22    pub arena_version: String,
23    pub seats: u32,
24    /// Turns played after turn 0.
25    pub turns: u32,
26    pub control_rate: u32,
27    /// The arena's declared series, by key.
28    pub series: Vec<SeriesTrack>,
29    /// The platform's own per-turn series (fuel, wall time, misses, …), by key.
30    pub platform: Vec<SeriesTrack>,
31    pub events: Vec<EventMark>,
32    /// The limits the session ran under, worded for the page.
33    pub limits: Vec<String>,
34    /// Set once the session finished with a result.
35    pub result: Option<SessionResult>,
36}
37
38/// One series' values, recorded on change.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct SeriesTrack {
41    pub key: String,
42    /// One line per seat, or one for the whole session.
43    pub lines: Vec<Line>,
44}
45
46/// A line's points, each `(turn, value)`, recorded only when the value
47/// changes. A value holds until the next point.
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct Line {
50    pub seat: Option<u32>,
51    pub points: Vec<(u32, f32)>,
52}
53
54impl Line {
55    /// The value at `turn`.
56    pub fn at(&self, turn: u32) -> Option<f32> {
57        let after = self.points.partition_point(|(at, _)| *at <= turn);
58        after.checked_sub(1).map(|index| self.points[index].1)
59    }
60
61    /// Every turn's value from 0 to `turns`, filling held values in.
62    pub fn dense(&self, turns: u32) -> Vec<f32> {
63        (0..=turns)
64            .map(|turn| self.at(turn).unwrap_or(0.0))
65            .collect()
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct EventMark {
71    pub turn: u32,
72    pub key: String,
73    pub seat: Option<u32>,
74}
75
76/// How a session finished.
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct SessionResult {
79    /// Places from first to last, each listing the seats that share it.
80    pub places: Vec<Vec<u32>>,
81    pub decided_by: String,
82    /// Each declared stat's values, one per seat, by key.
83    pub stats: Vec<StatValues>,
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct StatValues {
88    pub key: String,
89    pub values: Vec<f64>,
90}
91
92impl SessionResult {
93    /// Seats from first place to last, with how many share first.
94    pub fn finish(&self) -> (Vec<u32>, usize) {
95        let order = self.places.iter().flatten().copied().collect();
96        (order, self.places.first().map_or(0, Vec::len))
97    }
98}
99
100impl Metrics {
101    pub fn to_json(&self) -> Vec<u8> {
102        serde_json::to_vec(self).expect("metrics always serialise")
103    }
104
105    pub fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
106        serde_json::from_slice(bytes)
107    }
108
109    pub fn series(&self, key: &str) -> Option<&SeriesTrack> {
110        self.series.iter().find(|track| track.key == key)
111    }
112}
113
114/// Builds a metrics file turn by turn, from the arena's reports, recording
115/// each line only on change.
116#[derive(Debug, Clone)]
117pub struct MetricsRecorder {
118    metrics: Metrics,
119    keys: Vec<String>,
120}
121
122impl MetricsRecorder {
123    pub fn new(declaration: &Declaration, seats: u32) -> Self {
124        let series = declaration
125            .series
126            .iter()
127            .map(|decl| SeriesTrack {
128                key: decl.key.clone(),
129                lines: match decl.scope {
130                    SeriesScope::PerSeat => (0..seats)
131                        .map(|seat| Line {
132                            seat: Some(seat),
133                            points: Vec::new(),
134                        })
135                        .collect(),
136                    SeriesScope::Session => vec![Line {
137                        seat: None,
138                        points: Vec::new(),
139                    }],
140                },
141            })
142            .collect();
143        Self {
144            metrics: Metrics {
145                format: METRICS_FORMAT,
146                arena_id: declaration.arena_id.clone(),
147                arena_version: declaration.arena_version.clone(),
148                seats,
149                turns: 0,
150                control_rate: declaration.control_rate,
151                series,
152                platform: Vec::new(),
153                events: Vec::new(),
154                limits: Vec::new(),
155                result: None,
156            },
157            keys: declaration
158                .events
159                .iter()
160                .map(|event| event.key.clone())
161                .collect(),
162        }
163    }
164
165    /// Record one turn's series values (one entry per declared series, each
166    /// with one value per line) and events (by declared index).
167    pub fn turn(
168        &mut self,
169        turn: u32,
170        series: &[Vec<f32>],
171        events: &[(u32, Option<u32>)],
172    ) -> Result<(), String> {
173        if series.len() != self.metrics.series.len() {
174            return Err(format!(
175                "{} series reported, {} declared",
176                series.len(),
177                self.metrics.series.len()
178            ));
179        }
180        for (track, values) in self.metrics.series.iter_mut().zip(series) {
181            if values.len() != track.lines.len() {
182                return Err(format!(
183                    "series `{}` reported {} values for {} lines",
184                    track.key,
185                    values.len(),
186                    track.lines.len()
187                ));
188            }
189            for (line, value) in track.lines.iter_mut().zip(values) {
190                if line
191                    .points
192                    .last()
193                    .is_none_or(|(_, last)| last.to_bits() != value.to_bits())
194                {
195                    line.points.push((turn, *value));
196                }
197            }
198        }
199        for (index, seat) in events {
200            let key = self
201                .keys
202                .get(*index as usize)
203                .ok_or_else(|| format!("event {index} isn't declared"))?;
204            self.metrics.events.push(EventMark {
205                turn,
206                key: key.clone(),
207                seat: *seat,
208            });
209        }
210        self.metrics.turns = turn;
211        Ok(())
212    }
213
214    pub fn limits(&mut self, limits: Vec<String>) {
215        self.metrics.limits = limits;
216    }
217
218    pub fn platform(&mut self, track: SeriesTrack) {
219        self.metrics.platform.push(track);
220    }
221
222    pub fn finish(
223        mut self,
224        declaration: &Declaration,
225        places: Vec<Vec<u32>>,
226        decided_by: String,
227        stats: Vec<Vec<f64>>,
228    ) -> Result<Metrics, String> {
229        if stats.len() != declaration.stats.len() {
230            return Err(format!(
231                "{} stats reported, {} declared",
232                stats.len(),
233                declaration.stats.len()
234            ));
235        }
236        let seats = self.metrics.seats;
237        let mut seen: Vec<u32> = places.iter().flatten().copied().collect();
238        seen.sort_unstable();
239        if seen != (0..seats).collect::<Vec<_>>() || places.iter().any(Vec::is_empty) {
240            return Err(format!(
241                "places {places:?} must list every seat exactly once"
242            ));
243        }
244        let stats = declaration
245            .stats
246            .iter()
247            .zip(stats)
248            .map(|(decl, values)| {
249                if values.len() == seats as usize {
250                    Ok(StatValues {
251                        key: decl.key.clone(),
252                        values,
253                    })
254                } else {
255                    Err(format!(
256                        "stat `{}` has {} values for {seats} seats",
257                        decl.key,
258                        values.len()
259                    ))
260                }
261            })
262            .collect::<Result<_, _>>()?;
263        self.metrics.result = Some(SessionResult {
264            places,
265            decided_by,
266            stats,
267        });
268        Ok(self.metrics)
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::declaration::tests::sample;
276
277    #[test]
278    fn records_on_change_and_reads_back_densely() {
279        let declaration = sample();
280        let mut recorder = MetricsRecorder::new(&declaration, 2);
281        for (turn, value) in [(0, 1.0), (1, 1.0), (2, 3.0), (3, 3.0)] {
282            recorder.turn(turn, &[vec![value]], &[]).unwrap();
283        }
284        recorder.turn(4, &[vec![3.0]], &[(0, Some(1))]).unwrap();
285        let metrics = recorder
286            .finish(
287                &declaration,
288                vec![vec![1], vec![0]],
289                "By hits".into(),
290                vec![vec![1.0, 4.0]],
291            )
292            .unwrap();
293        let line = &metrics.series("distance").unwrap().lines[0];
294        assert_eq!(line.points, vec![(0, 1.0), (2, 3.0)]);
295        assert_eq!(line.dense(4), vec![1.0, 1.0, 3.0, 3.0, 3.0]);
296        assert_eq!(
297            metrics.events,
298            vec![EventMark {
299                turn: 4,
300                key: "hit".into(),
301                seat: Some(1)
302            }]
303        );
304        assert_eq!(metrics.result.as_ref().unwrap().finish(), (vec![1, 0], 1));
305        assert_eq!(Metrics::from_json(&metrics.to_json()).unwrap(), metrics);
306    }
307
308    #[test]
309    fn rejects_undeclared_reports_and_partial_places() {
310        let declaration = sample();
311        let mut recorder = MetricsRecorder::new(&declaration, 2);
312        assert!(recorder.turn(1, &[], &[]).is_err());
313        assert!(recorder.turn(1, &[vec![0.0]], &[(7, None)]).is_err());
314        assert!(
315            recorder
316                .clone()
317                .finish(
318                    &declaration,
319                    vec![vec![0]],
320                    "x".into(),
321                    vec![vec![0.0, 0.0]]
322                )
323                .is_err()
324        );
325        assert!(
326            recorder
327                .finish(
328                    &declaration,
329                    vec![vec![0, 1]],
330                    "Draw".into(),
331                    vec![vec![0.0, 0.0]]
332                )
333                .is_ok()
334        );
335    }
336}