1pub struct GridView<'a> {
5 pub width: u32,
7 pub height: u32,
9 pub cells: &'a [u8],
11 pub palette: &'static [[u8; 4]],
13}
14
15impl std::fmt::Debug for GridView<'_> {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 f.debug_struct("GridView")
19 .field("width", &self.width)
20 .field("height", &self.height)
21 .field("palette_len", &self.palette.len())
22 .finish_non_exhaustive()
23 }
24}
25
26pub struct PointView<'a> {
31 pub pos_x: &'a [f32],
33 pub pos_y: &'a [f32],
35 pub world_w: f32,
37 pub world_h: f32,
39 pub color: Option<&'a [u8]>,
41 pub palette: &'static [[u8; 4]],
43}
44
45impl std::fmt::Debug for PointView<'_> {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("PointView")
49 .field("len", &self.pos_x.len())
50 .field("world_w", &self.world_w)
51 .field("world_h", &self.world_h)
52 .field("colored", &self.color.is_some())
53 .field("palette_len", &self.palette.len())
54 .finish_non_exhaustive()
55 }
56}
57
58pub struct EdgeView<'a> {
60 pub src: &'a [u32],
62 pub dst: &'a [u32],
64 pub color: Option<&'a [u8]>,
66 pub palette: &'static [[u8; 4]],
68 pub directed: bool,
70 pub version: u64,
72}
73
74impl std::fmt::Debug for EdgeView<'_> {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("EdgeView")
78 .field("len", &self.src.len())
79 .field("colored", &self.color.is_some())
80 .field("palette_len", &self.palette.len())
81 .field("directed", &self.directed)
82 .field("version", &self.version)
83 .finish_non_exhaustive()
84 }
85}
86
87#[derive(Debug, Clone)]
89pub enum StatValue {
90 Scalar(f64),
92 Vector2D {
94 x: f64,
96 y: f64,
98 },
99 Histogram {
101 edges: Vec<f64>,
103 counts: Vec<u64>,
105 },
106}
107
108impl StatValue {
109 pub fn scalar(&self) -> f64 {
112 match self {
113 Self::Scalar(v) => *v,
114 Self::Vector2D { x, y } => x.hypot(*y),
115 Self::Histogram { counts, .. } => counts.iter().sum::<u64>() as f64,
116 }
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct StatEntry {
123 pub label: &'static str,
125 pub value: StatValue,
127 pub color: [u8; 4],
129}
130
131#[derive(Debug, Clone)]
133pub struct StatDescriptor {
134 pub label: &'static str,
136 pub color: [u8; 4],
138}
139
140impl StatDescriptor {
141 pub const fn new(label: &'static str, color: [u8; 4]) -> Self {
143 Self { label, color }
144 }
145}
146
147pub fn stat_entries(descriptors: &'static [StatDescriptor], values: Vec<StatValue>) -> Vec<StatEntry> {
152 descriptors
153 .iter()
154 .zip(values)
155 .map(|(d, value)| StatEntry {
156 label: d.label,
157 value,
158 color: d.color,
159 })
160 .collect()
161}
162
163pub struct StatsHistory {
169 columns: Vec<Vec<f64>>,
171 full: Vec<Option<Vec<StatValue>>>,
173 ticks: Vec<u64>,
175 descriptors: Vec<StatDescriptor>,
176 write_count: usize,
178 capacity: Option<usize>,
180}
181
182impl std::fmt::Debug for StatsHistory {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 let labels: Vec<&str> = self.descriptors.iter().map(|descriptor| descriptor.label).collect();
186 f.debug_struct("StatsHistory")
187 .field("labels", &labels)
188 .field("len", &self.len())
189 .field("write_count", &self.write_count)
190 .field("capacity", &self.capacity)
191 .finish_non_exhaustive()
192 }
193}
194
195impl StatsHistory {
196 pub fn new(descriptors: Vec<StatDescriptor>, capacity: Option<usize>) -> Self {
199 let reserve = capacity.unwrap_or(0);
200 let columns = vec![Vec::with_capacity(reserve); descriptors.len()];
201 let full = vec![None; descriptors.len()];
202 let ticks = Vec::with_capacity(reserve);
203 Self {
204 columns,
205 full,
206 ticks,
207 descriptors,
208 write_count: 0,
209 capacity,
210 }
211 }
212
213 pub fn push_entries(&mut self, stats: &[StatEntry], tick: u64) {
222 let newest = self.len().checked_sub(1).and_then(|j| self.buf_index(j));
223 if let Some(idx) = newest
224 && self.ticks[idx] == tick
225 {
226 self.write_at(idx, stats, tick);
227 return;
228 }
229
230 if self.write_count == 0 {
231 for (slot, entry) in self.full.iter_mut().zip(stats) {
232 if !matches!(entry.value, StatValue::Scalar(_)) {
233 *slot = Some(Vec::new());
234 }
235 }
236 }
237
238 match self.capacity {
239 Some(capacity) if self.write_count >= capacity => {
240 self.write_at(self.write_count % capacity, stats, tick);
242 }
243 _ => {
244 for (col, entry) in self.columns.iter_mut().zip(stats) {
245 col.push(entry.value.scalar());
246 }
247 for (slot, entry) in self.full.iter_mut().zip(stats) {
248 if let Some(values) = slot {
249 values.push(entry.value.clone());
250 }
251 }
252 self.ticks.push(tick);
253 }
254 }
255 self.write_count += 1;
256 }
257
258 fn write_at(&mut self, idx: usize, stats: &[StatEntry], tick: u64) {
260 for (col, entry) in self.columns.iter_mut().zip(stats) {
261 col[idx] = entry.value.scalar();
262 }
263 for (slot, entry) in self.full.iter_mut().zip(stats) {
264 if let Some(values) = slot {
265 values[idx] = entry.value.clone();
266 }
267 }
268 self.ticks[idx] = tick;
269 }
270
271 pub fn descriptors(&self) -> &[StatDescriptor] {
273 &self.descriptors
274 }
275
276 pub fn len(&self) -> usize {
278 self.capacity.map_or(self.write_count, |cap| self.write_count.min(cap))
279 }
280
281 pub fn is_empty(&self) -> bool {
283 self.write_count == 0
284 }
285
286 pub fn capacity(&self) -> Option<usize> {
288 self.capacity
289 }
290
291 pub fn write_count(&self) -> usize {
293 self.write_count
294 }
295
296 fn buf_index(&self, j: usize) -> Option<usize> {
298 if j >= self.len() {
299 return None;
300 }
301 match self.capacity {
302 Some(capacity) => Some((self.write_count.saturating_sub(capacity) + j) % capacity),
303 None => Some(j),
304 }
305 }
306
307 pub fn get(&self, col: usize, j: usize) -> Option<(f64, u64)> {
309 let idx = self.buf_index(j)?;
310 let value = self.columns.get(col)?.get(idx).copied()?;
311 let tick = self.ticks.get(idx).copied()?;
312 Some((value, tick))
313 }
314
315 pub fn tick(&self, j: usize) -> Option<u64> {
317 self.ticks.get(self.buf_index(j)?).copied()
318 }
319
320 pub fn entries(&self, j: usize) -> Option<Vec<StatEntry>> {
322 let idx = self.buf_index(j)?;
323 let entries = self
324 .descriptors
325 .iter()
326 .enumerate()
327 .map(|(col, desc)| {
328 let value = match self.full.get(col).and_then(Option::as_ref) {
329 Some(values) => values[idx].clone(),
330 None => StatValue::Scalar(self.columns[col][idx]),
331 };
332 StatEntry {
333 label: desc.label,
334 value,
335 color: desc.color,
336 }
337 })
338 .collect();
339 Some(entries)
340 }
341
342 pub fn heap_bytes(&self) -> usize {
344 let scalars = self.columns.iter().map(|c| c.capacity() * 8).sum::<usize>();
345 let full = self
346 .full
347 .iter()
348 .flatten()
349 .map(|values| {
350 values.capacity() * size_of::<StatValue>() + values.iter().map(stat_value_heap).sum::<usize>()
351 })
352 .sum::<usize>();
353 scalars + full + self.ticks.capacity() * 8
354 }
355
356 pub fn resize(&mut self, new_capacity: Option<usize>) {
358 let filled = self.len();
359 let keep = new_capacity.map_or(filled, |cap| filled.min(cap));
360 let skip = filled - keep;
361
362 let slots: Vec<usize> = (skip..filled).filter_map(|j| self.buf_index(j)).collect();
363
364 let new_columns: Vec<Vec<f64>> = self
365 .columns
366 .iter()
367 .map(|column| slots.iter().map(|&i| column[i]).collect())
368 .collect();
369 let new_full: Vec<Option<Vec<StatValue>>> = self
370 .full
371 .iter()
372 .map(|slot| {
373 slot.as_ref()
374 .map(|values| slots.iter().map(|&i| values[i].clone()).collect())
375 })
376 .collect();
377 let new_ticks: Vec<u64> = slots.iter().map(|&i| self.ticks[i]).collect();
378
379 self.columns = new_columns;
380 self.full = new_full;
381 self.ticks = new_ticks;
382 self.capacity = new_capacity;
383 self.write_count = keep;
384 }
385}
386
387fn stat_value_heap(value: &StatValue) -> usize {
389 match value {
390 StatValue::Scalar(_) | StatValue::Vector2D { .. } => 0,
391 StatValue::Histogram { edges, counts } => edges.capacity() * 8 + counts.capacity() * 8,
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::{StatDescriptor, StatEntry, StatValue, StatsHistory};
398
399 const C: [u8; 4] = [1, 2, 3, 255];
400
401 fn history(capacity: Option<usize>, labels: &[&'static str]) -> StatsHistory {
402 let descriptors = labels.iter().map(|l| StatDescriptor::new(l, C)).collect();
403 StatsHistory::new(descriptors, capacity)
404 }
405
406 fn scalars(values: &[f64]) -> Vec<StatEntry> {
407 values
408 .iter()
409 .map(|v| StatEntry {
410 label: "s",
411 value: StatValue::Scalar(*v),
412 color: C,
413 })
414 .collect()
415 }
416
417 fn vec2(x: f64, y: f64) -> Vec<StatEntry> {
418 vec![StatEntry {
419 label: "v",
420 value: StatValue::Vector2D { x, y },
421 color: C,
422 }]
423 }
424
425 #[test]
426 fn a_bounded_history_keeps_the_newest_entries() {
427 let mut h = history(Some(3), &["a"]);
428 for tick in 0u32..5 {
429 h.push_entries(&scalars(&[f64::from(tick)]), u64::from(tick));
430 }
431 assert_eq!(h.len(), 3);
432 assert_eq!(h.get(0, 0), Some((2.0, 2)));
433 assert_eq!(h.get(0, 2), Some((4.0, 4)));
434 assert_eq!(h.get(0, 3), None);
435 }
436
437 #[test]
440 fn a_sample_at_the_newest_tick_replaces_it() {
441 for (capacity, ticks) in [(None, 3u32), (Some(2), 2), (Some(2), 5)] {
442 let mut h = history(capacity, &["v"]);
443 for tick in 0..ticks {
444 h.push_entries(&vec2(f64::from(tick), 0.0), u64::from(tick));
445 }
446 let newest = u64::from(ticks - 1);
447 let (len, writes) = (h.len(), h.write_count());
448 h.push_entries(&vec2(3.0, 4.0), newest);
449
450 assert_eq!((h.len(), h.write_count()), (len, writes), "a second entry was added");
451 assert_eq!(h.get(0, len - 1), Some((5.0, newest)));
452 let sample = h.entries(len - 1).expect("the newest entry exists");
453 assert!(matches!(sample[0].value, StatValue::Vector2D { x, y } if x == 3.0 && y == 4.0));
454 assert_eq!(h.tick(len - 2), Some(newest - 1), "an older entry changed");
455 }
456 }
457
458 #[test]
459 fn an_unlimited_history_drops_nothing() {
460 let mut h = history(None, &["a"]);
461 for tick in 0u32..1000 {
462 h.push_entries(&scalars(&[f64::from(tick)]), u64::from(tick));
463 }
464 assert_eq!(h.len(), 1000);
465 assert_eq!(h.capacity(), None);
466 assert_eq!(h.get(0, 0), Some((0.0, 0)));
467 assert_eq!(h.get(0, 999), Some((999.0, 999)));
468 }
469
470 #[test]
472 fn a_vector_series_keeps_its_components() {
473 let mut h = history(Some(4), &["v"]);
474 h.push_entries(&vec2(3.0, 4.0), 0);
475 h.push_entries(&vec2(6.0, 8.0), 1);
476
477 assert_eq!(h.get(0, 0), Some((5.0, 0)));
479
480 let sample = h.entries(1).expect("index 1 exists");
481 assert!(
482 matches!(sample[0].value, StatValue::Vector2D { x, y } if x == 6.0 && y == 8.0),
483 "got {:?}",
484 sample[0].value
485 );
486 assert_eq!(sample[0].label, "v");
487 }
488
489 #[test]
490 fn a_scalar_series_rebuilds_from_its_own_column() {
491 let mut h = history(Some(4), &["a", "b"]);
492 h.push_entries(&scalars(&[1.0, 2.0]), 7);
493 let sample = h.entries(0).expect("index 0 exists");
494 assert_eq!(sample.len(), 2);
495 assert!(matches!(sample[0].value, StatValue::Scalar(v) if v == 1.0));
496 assert!(matches!(sample[1].value, StatValue::Scalar(v) if v == 2.0));
497 assert!(h.entries(1).is_none());
498 }
499
500 #[test]
502 fn full_values_wrap_with_their_column() {
503 let mut h = history(Some(2), &["v"]);
504 for i in 0u32..5 {
505 h.push_entries(&vec2(f64::from(i), 0.0), u64::from(i));
506 }
507 for (j, expected) in [3.0, 4.0].into_iter().enumerate() {
508 let sample = h.entries(j).expect("both slots are filled");
509 assert!(
510 matches!(sample[0].value, StatValue::Vector2D { x, .. } if x == expected),
511 "slot {j} got {:?}",
512 sample[0].value
513 );
514 assert_eq!(h.get(0, j).map(|(v, _)| v), Some(expected));
515 }
516 }
517
518 #[test]
519 fn shrinking_keeps_the_newest_and_growing_keeps_everything() {
520 let mut h = history(Some(10), &["v"]);
521 for i in 0u32..6 {
522 h.push_entries(&vec2(f64::from(i), 0.0), u64::from(i));
523 }
524
525 h.resize(Some(3));
526 assert_eq!(h.len(), 3);
527 assert_eq!(h.tick(0), Some(3));
528 assert!(matches!(h.entries(0).expect("kept")[0].value, StatValue::Vector2D { x, .. } if x == 3.0));
529
530 h.resize(None);
531 assert_eq!(h.len(), 3, "growing adds no samples back");
532 h.push_entries(&vec2(9.0, 0.0), 9);
533 assert_eq!(h.len(), 4);
534 assert_eq!(h.tick(3), Some(9));
535 }
536
537 #[test]
538 fn a_scalar_series_costs_less_than_a_vector_one() {
539 let mut scalar = history(Some(64), &["a"]);
540 let mut vector = history(Some(64), &["v"]);
541 for i in 0u32..64 {
542 scalar.push_entries(&scalars(&[f64::from(i)]), u64::from(i));
543 vector.push_entries(&vec2(f64::from(i), 0.0), u64::from(i));
544 }
545 assert!(
546 vector.heap_bytes() > scalar.heap_bytes(),
547 "{} vs {}",
548 vector.heap_bytes(),
549 scalar.heap_bytes()
550 );
551 }
552}