use std::collections::{BTreeMap, VecDeque};
use std::sync::{Mutex, MutexGuard, PoisonError};
use kernel::{EventKind, EventRecord, RunId};
use wire::LiveOutput;
const KEPT_BYTES_PER_RUN: usize = 64 * 1024;
#[derive(Default)]
pub(crate) struct OutputRing {
runs: Mutex<BTreeMap<RunId, Kept>>,
}
#[derive(Default)]
struct Kept {
pieces: VecDeque<LiveOutput>,
bytes: usize,
}
impl OutputRing {
pub(crate) fn keep(&self, piece: &LiveOutput) {
let mut runs = self.runs();
let kept = runs.entry(piece.run).or_default();
kept.bytes = kept.bytes.saturating_add(piece.text.len());
kept.pieces.push_back(piece.clone());
while kept.bytes > KEPT_BYTES_PER_RUN && kept.pieces.len() > 1 {
let Some(oldest) = kept.pieces.pop_front() else {
break;
};
kept.bytes = kept.bytes.saturating_sub(oldest.text.len());
}
}
pub(crate) fn settle(&self, record: &EventRecord) {
if record.kind() == EventKind::ToolResult {
self.runs().remove(&record.run());
}
}
pub(crate) fn so_far(&self) -> Vec<LiveOutput> {
self.runs()
.values()
.flat_map(|kept| kept.pieces.iter().cloned())
.collect()
}
fn runs(&self) -> MutexGuard<'_, BTreeMap<RunId, Kept>> {
self.runs.lock().unwrap_or_else(PoisonError::into_inner)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing, reason = "test code")]
mod tests {
use super::*;
use kernel::{EventDraft, GENESIS_PREV, Payload, Seq, TimeMs};
use wire::OutputStream;
fn piece(run: RunId, text: &str) -> LiveOutput {
LiveOutput {
run,
stream: OutputStream::Out,
text: text.to_owned(),
}
}
fn result_of(run: RunId) -> EventRecord {
let draft = EventDraft {
run,
t: TimeMs::new(0),
who: "mason@lab.1".into(),
addr: None,
kind: EventKind::ToolResult,
data: Payload::new(serde_json::Map::new()).unwrap(),
ig: false,
};
EventRecord::from_draft(draft, Seq::FIRST, GENESIS_PREV)
}
#[test]
fn a_page_opening_mid_command_sees_what_was_written_until_the_result_lands() {
let (building, testing) = (RunId::from_bytes([1; 16]), RunId::from_bytes([2; 16]));
let ring = OutputRing::default();
let written = [
piece(building, "compiling\n"),
piece(testing, "running 3 tests\n"),
];
written.iter().for_each(|each| ring.keep(each));
assert_eq!(ring.so_far(), written.to_vec());
ring.settle(&result_of(building));
assert_eq!(ring.so_far(), vec![written[1].clone()]);
}
#[test]
fn a_flooding_run_keeps_only_its_newest_bytes() {
let run = RunId::from_bytes([3; 16]);
let ring = OutputRing::default();
let line = "x".repeat(1024);
(0..100).for_each(|_| ring.keep(&piece(run, &line)));
ring.keep(&piece(run, "last\n"));
let kept = ring.so_far();
let bytes: usize = kept.iter().map(|each| each.text.len()).sum();
assert!(bytes <= KEPT_BYTES_PER_RUN, "kept {bytes} bytes");
assert_eq!(kept.last().map(|each| each.text.as_str()), Some("last\n"));
}
}