use crate::format::wire::{push_u32, push_u64};
use crate::format::{FormatError, Header, RecWriter, SessionMeta, SnapshotPolicy, kind};
use crate::probe::DeterminismProbe;
use std::io::{BufWriter, Write};
use std::path::Path;
const LIGHT_HASH_BATCH_LEN: usize = 64;
#[derive(Debug)]
pub enum RecordError {
Format(FormatError),
NonSequentialTick {
expected: u64,
got: u64,
},
#[cfg(feature = "serde")]
InputEncode(postcard::Error),
}
impl std::fmt::Display for RecordError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Format(err) => write!(f, "{err}"),
Self::NonSequentialTick { expected, got } => write!(
f,
"non-sequential tick: expected {expected}, got {got}, \
record_tick must be called once per tick in order"
),
#[cfg(feature = "serde")]
Self::InputEncode(err) => write!(f, "cannot encode typed inputs: {err}"),
}
}
}
impl std::error::Error for RecordError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Format(err) => Some(err),
Self::NonSequentialTick { .. } => None,
#[cfg(feature = "serde")]
Self::InputEncode(err) => Some(err),
}
}
}
impl From<FormatError> for RecordError {
fn from(err: FormatError) -> Self {
Self::Format(err)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecorderConfig {
pub session_meta: SessionMeta,
pub full_hash_interval: u32,
pub snapshot: SnapshotPolicy,
pub hash_algo_id: u16,
pub input_format_id: u64,
}
impl Default for RecorderConfig {
fn default() -> Self {
Self {
session_meta: SessionMeta::default(),
full_hash_interval: 300,
snapshot: SnapshotPolicy::Off,
hash_algo_id: 0,
input_format_id: 0,
}
}
}
pub struct Recorder<W: Write> {
writer: RecWriter<W>,
full_hash_interval: u32,
snapshot: SnapshotPolicy,
next_tick: Option<u64>,
ticks_recorded: u64,
batch_first_tick: u64,
light_hashes: Vec<u64>,
last_inputs: Vec<u8>,
scratch: Vec<u8>,
}
impl Recorder<BufWriter<std::fs::File>> {
pub fn create<P: AsRef<Path>>(path: P, config: RecorderConfig) -> Result<Self, RecordError> {
let file = std::fs::File::create(path).map_err(FormatError::from)?;
Self::new(BufWriter::new(file), config)
}
}
impl<W: Write> Recorder<W> {
pub fn new(sink: W, config: RecorderConfig) -> Result<Self, RecordError> {
let header = Header {
meta: config.session_meta,
config: crate::format::ConfigEcho {
full_hash_interval: config.full_hash_interval,
snapshot_policy: config.snapshot,
hash_algo_id: config.hash_algo_id,
input_format_id: config.input_format_id,
},
};
Ok(Self {
writer: RecWriter::new(sink, &header)?,
full_hash_interval: config.full_hash_interval,
snapshot: config.snapshot,
next_tick: None,
ticks_recorded: 0,
batch_first_tick: 0,
light_hashes: Vec::with_capacity(LIGHT_HASH_BATCH_LEN),
last_inputs: Vec::new(),
scratch: Vec::with_capacity(1024),
})
}
pub fn record_tick(
&mut self,
tick: u64,
inputs: &[u8],
probe: &dyn DeterminismProbe,
) -> Result<(), RecordError> {
let first_tick_of_session = self.next_tick.is_none();
if let Some(expected) = self.next_tick
&& tick != expected
{
return Err(RecordError::NonSequentialTick {
expected,
got: tick,
});
}
self.next_tick = Some(tick + 1);
if first_tick_of_session || inputs != self.last_inputs.as_slice() {
self.scratch.clear();
push_u64(&mut self.scratch, tick);
self.scratch.extend_from_slice(inputs);
self.writer
.write_raw_chunk(kind::INPUT_FRAME, tick, &self.scratch)?;
self.last_inputs.clear();
self.last_inputs.extend_from_slice(inputs);
}
if self.light_hashes.is_empty() {
self.batch_first_tick = tick;
}
self.light_hashes.push(probe.light_hash());
if self.light_hashes.len() == LIGHT_HASH_BATCH_LEN {
self.flush_light_hashes()?;
}
if self.full_hash_interval > 0 && tick % u64::from(self.full_hash_interval) == 0 {
self.scratch.clear();
push_u64(&mut self.scratch, tick);
push_u64(&mut self.scratch, probe.full_hash());
self.writer
.write_raw_chunk(kind::FULL_HASH, tick, &self.scratch)?;
}
self.ticks_recorded += 1;
Ok(())
}
#[cfg(feature = "serde")]
pub fn record_tick_typed<I: serde::Serialize + ?Sized>(
&mut self,
tick: u64,
inputs: &I,
probe: &dyn DeterminismProbe,
) -> Result<(), RecordError> {
let bytes = postcard::to_allocvec(inputs).map_err(RecordError::InputEncode)?;
self.record_tick(tick, &bytes, probe)
}
pub fn wants_snapshot(&self, tick: u64) -> bool {
match self.snapshot {
SnapshotPolicy::Off => false,
SnapshotPolicy::Every(n) => n > 0 && tick % u64::from(n) == 0,
}
}
pub fn record_snapshot(&mut self, tick: u64, state: &[u8]) -> Result<(), RecordError> {
self.scratch.clear();
push_u64(&mut self.scratch, tick);
self.scratch.extend_from_slice(state);
self.writer
.write_raw_chunk(kind::SNAPSHOT, tick, &self.scratch)?;
Ok(())
}
pub fn record_marker(&mut self, tick: u64, label: &str) -> Result<(), RecordError> {
self.scratch.clear();
push_u64(&mut self.scratch, tick);
crate::format::wire::push_str(&mut self.scratch, label)?;
self.writer
.write_raw_chunk(kind::MARKER, tick, &self.scratch)?;
Ok(())
}
pub fn finish(mut self) -> Result<W, RecordError> {
self.flush_light_hashes()?;
Ok(self.writer.finish(self.ticks_recorded)?)
}
fn flush_light_hashes(&mut self) -> Result<(), RecordError> {
if self.light_hashes.is_empty() {
return Ok(());
}
self.scratch.clear();
push_u64(&mut self.scratch, self.batch_first_tick);
push_u32(&mut self.scratch, self.light_hashes.len() as u32);
for hash in &self.light_hashes {
push_u64(&mut self.scratch, *hash);
}
self.writer.write_raw_chunk(
kind::LIGHT_HASH_BATCH,
self.batch_first_tick,
&self.scratch,
)?;
self.light_hashes.clear();
Ok(())
}
}