1use serde::{Deserialize, Serialize};
12
13use crate::declaration::{Declaration, SeriesScope};
14
15pub 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 pub turns: u32,
26 pub control_rate: u32,
27 pub series: Vec<SeriesTrack>,
29 pub platform: Vec<SeriesTrack>,
31 pub events: Vec<EventMark>,
32 pub limits: Vec<String>,
34 pub result: Option<SessionResult>,
36}
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct SeriesTrack {
41 pub key: String,
42 pub lines: Vec<Line>,
44}
45
46#[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 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct SessionResult {
79 pub places: Vec<Vec<u32>>,
81 pub decided_by: String,
82 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 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#[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 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}