use serde::{Deserialize, Serialize};
use crate::declaration::{Declaration, SeriesScope};
pub const METRICS_FORMAT: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Metrics {
pub format: u32,
pub arena_id: String,
pub arena_version: String,
pub seats: u32,
pub turns: u32,
pub control_rate: u32,
pub series: Vec<SeriesTrack>,
pub platform: Vec<SeriesTrack>,
pub events: Vec<EventMark>,
pub limits: Vec<String>,
pub result: Option<SessionResult>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeriesTrack {
pub key: String,
pub lines: Vec<Line>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Line {
pub seat: Option<u32>,
pub points: Vec<(u32, f32)>,
}
impl Line {
pub fn at(&self, turn: u32) -> Option<f32> {
let after = self.points.partition_point(|(at, _)| *at <= turn);
after.checked_sub(1).map(|index| self.points[index].1)
}
pub fn dense(&self, turns: u32) -> Vec<f32> {
(0..=turns)
.map(|turn| self.at(turn).unwrap_or(0.0))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EventMark {
pub turn: u32,
pub key: String,
pub seat: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionResult {
pub places: Vec<Vec<u32>>,
pub decided_by: String,
pub stats: Vec<StatValues>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StatValues {
pub key: String,
pub values: Vec<f64>,
}
impl SessionResult {
pub fn finish(&self) -> (Vec<u32>, usize) {
let order = self.places.iter().flatten().copied().collect();
(order, self.places.first().map_or(0, Vec::len))
}
}
impl Metrics {
pub fn to_json(&self) -> Vec<u8> {
serde_json::to_vec(self).expect("metrics always serialise")
}
pub fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
pub fn series(&self, key: &str) -> Option<&SeriesTrack> {
self.series.iter().find(|track| track.key == key)
}
}
#[derive(Debug, Clone)]
pub struct MetricsRecorder {
metrics: Metrics,
keys: Vec<String>,
}
impl MetricsRecorder {
pub fn new(declaration: &Declaration, seats: u32) -> Self {
let series = declaration
.series
.iter()
.map(|decl| SeriesTrack {
key: decl.key.clone(),
lines: match decl.scope {
SeriesScope::PerSeat => (0..seats)
.map(|seat| Line {
seat: Some(seat),
points: Vec::new(),
})
.collect(),
SeriesScope::Session => vec![Line {
seat: None,
points: Vec::new(),
}],
},
})
.collect();
Self {
metrics: Metrics {
format: METRICS_FORMAT,
arena_id: declaration.arena_id.clone(),
arena_version: declaration.arena_version.clone(),
seats,
turns: 0,
control_rate: declaration.control_rate,
series,
platform: Vec::new(),
events: Vec::new(),
limits: Vec::new(),
result: None,
},
keys: declaration
.events
.iter()
.map(|event| event.key.clone())
.collect(),
}
}
pub fn turn(
&mut self,
turn: u32,
series: &[Vec<f32>],
events: &[(u32, Option<u32>)],
) -> Result<(), String> {
if series.len() != self.metrics.series.len() {
return Err(format!(
"{} series reported, {} declared",
series.len(),
self.metrics.series.len()
));
}
for (track, values) in self.metrics.series.iter_mut().zip(series) {
if values.len() != track.lines.len() {
return Err(format!(
"series `{}` reported {} values for {} lines",
track.key,
values.len(),
track.lines.len()
));
}
for (line, value) in track.lines.iter_mut().zip(values) {
if line
.points
.last()
.is_none_or(|(_, last)| last.to_bits() != value.to_bits())
{
line.points.push((turn, *value));
}
}
}
for (index, seat) in events {
let key = self
.keys
.get(*index as usize)
.ok_or_else(|| format!("event {index} isn't declared"))?;
self.metrics.events.push(EventMark {
turn,
key: key.clone(),
seat: *seat,
});
}
self.metrics.turns = turn;
Ok(())
}
pub fn limits(&mut self, limits: Vec<String>) {
self.metrics.limits = limits;
}
pub fn platform(&mut self, track: SeriesTrack) {
self.metrics.platform.push(track);
}
pub fn finish(
mut self,
declaration: &Declaration,
places: Vec<Vec<u32>>,
decided_by: String,
stats: Vec<Vec<f64>>,
) -> Result<Metrics, String> {
if stats.len() != declaration.stats.len() {
return Err(format!(
"{} stats reported, {} declared",
stats.len(),
declaration.stats.len()
));
}
let seats = self.metrics.seats;
let mut seen: Vec<u32> = places.iter().flatten().copied().collect();
seen.sort_unstable();
if seen != (0..seats).collect::<Vec<_>>() || places.iter().any(Vec::is_empty) {
return Err(format!(
"places {places:?} must list every seat exactly once"
));
}
let stats = declaration
.stats
.iter()
.zip(stats)
.map(|(decl, values)| {
if values.len() == seats as usize {
Ok(StatValues {
key: decl.key.clone(),
values,
})
} else {
Err(format!(
"stat `{}` has {} values for {seats} seats",
decl.key,
values.len()
))
}
})
.collect::<Result<_, _>>()?;
self.metrics.result = Some(SessionResult {
places,
decided_by,
stats,
});
Ok(self.metrics)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::declaration::tests::sample;
#[test]
fn records_on_change_and_reads_back_densely() {
let declaration = sample();
let mut recorder = MetricsRecorder::new(&declaration, 2);
for (turn, value) in [(0, 1.0), (1, 1.0), (2, 3.0), (3, 3.0)] {
recorder.turn(turn, &[vec![value]], &[]).unwrap();
}
recorder.turn(4, &[vec![3.0]], &[(0, Some(1))]).unwrap();
let metrics = recorder
.finish(
&declaration,
vec![vec![1], vec![0]],
"By hits".into(),
vec![vec![1.0, 4.0]],
)
.unwrap();
let line = &metrics.series("distance").unwrap().lines[0];
assert_eq!(line.points, vec![(0, 1.0), (2, 3.0)]);
assert_eq!(line.dense(4), vec![1.0, 1.0, 3.0, 3.0, 3.0]);
assert_eq!(
metrics.events,
vec![EventMark {
turn: 4,
key: "hit".into(),
seat: Some(1)
}]
);
assert_eq!(metrics.result.as_ref().unwrap().finish(), (vec![1, 0], 1));
assert_eq!(Metrics::from_json(&metrics.to_json()).unwrap(), metrics);
}
#[test]
fn rejects_undeclared_reports_and_partial_places() {
let declaration = sample();
let mut recorder = MetricsRecorder::new(&declaration, 2);
assert!(recorder.turn(1, &[], &[]).is_err());
assert!(recorder.turn(1, &[vec![0.0]], &[(7, None)]).is_err());
assert!(
recorder
.clone()
.finish(
&declaration,
vec![vec![0]],
"x".into(),
vec![vec![0.0, 0.0]]
)
.is_err()
);
assert!(
recorder
.finish(
&declaration,
vec![vec![0, 1]],
"Draw".into(),
vec![vec![0.0, 0.0]]
)
.is_ok()
);
}
}