use std::collections::VecDeque;
pub const HISTORY_MAX_BYTES: usize = 4 * 1024 * 1024;
pub struct Entry {
pub seq: u64,
pub at: jiff::Timestamp,
pub sig: u64,
pub ticks: u32,
pub frame: Vec<String>,
}
impl Entry {
fn bytes(&self) -> usize {
self.frame
.iter()
.map(|l| l.len() + std::mem::size_of::<String>())
.sum()
}
}
#[derive(Default)]
pub struct History {
entries: VecDeque<Entry>,
next_seq: u64,
total_bytes: usize,
}
impl History {
pub fn new() -> History {
History::default()
}
pub fn record(&mut self, sig: u64, lines: &[String], at: jiff::Timestamp) -> bool {
if let Some(newest) = self.entries.back_mut()
&& newest.sig == sig
{
newest.ticks += 1;
return false;
}
let entry = Entry {
seq: self.next_seq,
at,
sig,
ticks: 1,
frame: lines.to_vec(),
};
self.next_seq += 1;
self.total_bytes += entry.bytes();
self.entries.push_back(entry);
while self.total_bytes > HISTORY_MAX_BYTES && self.entries.len() > 2 {
if let Some(evicted) = self.entries.pop_front() {
self.total_bytes -= evicted.bytes();
}
}
true
}
pub fn newest_seq(&self) -> Option<u64> {
self.entries.back().map(|e| e.seq)
}
pub fn nearest(&self, seq: u64) -> Option<&Entry> {
self.entries
.iter()
.rev()
.find(|e| e.seq <= seq)
.or_else(|| self.entries.front())
}
pub fn prev(&self, seq: u64) -> Option<&Entry> {
self.entries.iter().rev().find(|e| e.seq < seq)
}
pub fn next(&self, seq: u64) -> Option<&Entry> {
self.entries.iter().find(|e| e.seq > seq)
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn bytes(&self) -> usize {
self.total_bytes
}
}
#[cfg(test)]
mod tests {
use super::*;
fn at() -> jiff::Timestamp {
jiff::Timestamp::from_second(1_785_067_200).expect("fixed timestamp")
}
fn frame(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn a_changed_frame_records_and_a_repeat_dedupes() {
let mut h = History::new();
assert!(h.record(1, &frame(&["a"]), at()));
assert!(!h.record(1, &frame(&["a"]), at()), "a repeat sig dedupes");
let entry = h.nearest(0).expect("one entry");
assert_eq!(entry.ticks, 2, "the deduped entry counts its ticks");
assert_eq!(h.newest_seq(), Some(0));
}
#[test]
fn entries_are_seq_addressed_monotonically() {
let mut h = History::new();
assert!(h.record(1, &frame(&["a"]), at()));
assert!(h.record(2, &frame(&["b"]), at()));
assert!(h.record(3, &frame(&["c"]), at()));
assert_eq!(h.newest_seq(), Some(2));
for seq in 0..=2 {
assert_eq!(h.nearest(seq).expect("present").seq, seq);
}
}
#[test]
fn prev_and_next_walk_the_ring() {
let mut h = History::new();
h.record(1, &frame(&["a"]), at());
h.record(2, &frame(&["b"]), at());
h.record(3, &frame(&["c"]), at());
assert_eq!(h.prev(2).expect("prev of newest").seq, 1);
assert_eq!(h.prev(1).expect("prev of middle").seq, 0);
assert!(h.prev(0).is_none(), "nothing before the oldest");
assert_eq!(h.next(0).expect("next of oldest").seq, 1);
assert_eq!(h.next(1).expect("next of middle").seq, 2);
}
#[test]
fn next_past_newest_is_none() {
let mut h = History::new();
h.record(1, &frame(&["a"]), at());
h.record(2, &frame(&["b"]), at());
assert!(h.next(1).is_none());
assert!(h.next(99).is_none());
}
#[test]
fn the_byte_cap_evicts_from_the_front_but_keeps_two() {
let big = "x".repeat(3 * 1024 * 1024);
let mut h = History::new();
assert!(h.record(1, &frame(&[&big]), at()));
assert!(h.record(2, &frame(&[&big]), at()));
assert!(h.record(3, &frame(&[&big]), at()));
assert_eq!(h.newest_seq(), Some(2));
assert_eq!(
h.nearest(0).expect("oldest survivor").seq,
1,
"the front entry was evicted"
);
assert!(h.prev(1).is_none(), "seq 0 is gone");
assert_eq!(h.next(1).expect("newest").seq, 2);
assert!(
h.bytes() > HISTORY_MAX_BYTES,
"the floor outranks the cap: two big entries stay"
);
}
#[test]
fn nearest_survives_eviction() {
let big = "x".repeat(3 * 1024 * 1024);
let mut h = History::new();
h.record(1, &frame(&[&big]), at());
h.record(2, &frame(&[&big]), at());
h.record(3, &frame(&[&big]), at());
assert_eq!(
h.nearest(0).expect("clamped").seq,
1,
"older than every survivor"
);
assert_eq!(h.nearest(1).expect("exact").seq, 1);
assert_eq!(h.nearest(2).expect("exact").seq, 2);
assert_eq!(h.nearest(99).expect("clamped").seq, 2, "past the newest");
assert!(History::new().nearest(0).is_none(), "empty history");
}
#[test]
fn bytes_accounts_frames() {
let lines = frame(&["abc", "defgh"]);
let mut h = History::new();
h.record(1, &lines, at());
let expected: usize = lines
.iter()
.map(|l| l.len() + std::mem::size_of::<String>())
.sum();
assert_eq!(h.bytes(), expected);
}
}