What a live window calls once per emulated frame: appends to the active
branch's log, and keeps a keyframe every so often. Never touches the bot
or Jev - it only writes down what already happened, which is what lets
crate::seek and crate::rebuild treat the log as ground truth.
How often a live recording writes a keyframe: the finest tier's spacing,
~2 s. crate::keyframes::KeyframeIndex::prune then thins them with age
(10 s after ~5 minutes, 30 s after ~an hour). Measured by
apps/replay-probe (2026-09-22): a 2 s keyframe is 115 KB on average (of
a 1.30 MB fixed-width snapshot), each write costs 13 ms on average on
the window's UI thread, and the first hour of play takes ~76 MB on disk.
22pub const RECORD_EVERY: u64 = keyframes::DENSE_FRAMES;
The absolute frame of the last entry appended (this branch's
fork_frame if nothing has been recorded to it yet).
34impl Recorder {
Open (or resume appending to) the log for a branch already forked at
fork_frame - crate::tree::Tree::fork, or fork_frame: 0 for a
fresh recording's root, which has no fork of its own. Keyframe
cadence is [RECORD_EVERY].
39 pub fn open(branch_dir: &Path, fork_frame: u64) -> io::Result<Self> { 40 fs::create_dir_all(branch_dir)?; 41 let log = LogWriter::create_or_append(&branch_dir.join("log.bin"))?; 42 let keyframes = KeyframeIndex::open(&branch_dir.join("keyframes"))?; 43 let last_frame = keyframes.frames().into_iter().max().unwrap_or(fork_frame).max(fork_frame); 44 Ok(Self { log, keyframes, last_frame, last_flush_at: Instant::now(), flush_every: Duration::from_secs(1) }) 45 }
A NEW branch's own keyframe at its fork point, so seeking within it
never has to consult its parent - crate::tree's owner_of counts
on this existing. Call once, right after [Self::open], for every
branch except a fresh recording's root (whose "keyframe at frame 0"
is the recording's own header instead).
One emulated frame just ran. snapshot is called only when a
keyframe happens to be due ([RECORD_EVERY]), so the (comparatively expensive)
console::Console::snapshot_fixed() is never paid for on a frame that
doesn't need it. Every keyframe write also prunes aged-out tiers
(crate::keyframes::KeyframeIndex::prune), so the tiering keeps
itself thinned as recording continues rather than needing a separate
pass. Returns whether a keyframe was written, so a caller that also
enforces crate::storage's disk cap knows when it is worth checking
again rather than doing it every frame.
[Self::record] with the snapshot supplied by a thunk, so the
cadence and pruning are testable without booting a cartridge.
71 fn record_snapshot(&mut self, entry: FrameEntry, snapshot: impl FnOnce() -> io::Result<Vec<u8>>) -> io::Result<bool> { 72 self.log.append(&entry)?; 73 self.last_frame += 1; 74 let now = Instant::now(); 75 if now.duration_since(self.last_flush_at) >= self.flush_every { 76 self.log.flush()?; 77 self.last_flush_at = now; 78 } 79 if self.last_frame % RECORD_EVERY == 0 { 80 self.keyframes.store(self.last_frame, &snapshot()?)?; 81 self.keyframes.prune(self.last_frame)?; 82 return Ok(true); 83 } 84 Ok(false) 85 }
The one way a keyframe's snapshot is taken: fixed-width
(Console::snapshot_fixed), because crate::keyframe deltas it at fixed
offsets against its neighbour. crate::seek restores with the matching
restore_fixed. Taking the console rather than bytes is what keeps a
varint snapshot from ever reaching the keyframe store.
101#[cfg(test)] 102mod tests { 103 use super::*; 104 use crate::format::FrameEntry; 105 106 fn scratch(tag: &str) -> std::path::PathBuf { 107 let dir = std::env::temp_dir().join(format!("jev-replay-recorder-test-{tag}-{}", std::process::id())); 108 let _ = fs::remove_dir_all(&dir); 109 dir 110 } 111 112 fn entry() -> FrameEntry { 113 FrameEntry { console_pad: 0, human_pad: 0, wram_diff: Vec::new(), decision: None } 114 } 115 116 #[test] 117 fn recording_appends_to_the_log_and_tracks_last_frame() { 118 let dir = scratch("append"); 119 let mut rec = Recorder::open(&dir, 0).unwrap(); 120 for i in 0..5u64 { 121 let wrote = rec.record_snapshot(entry(), || panic!("no keyframe should be due")).unwrap(); 122 assert!(!wrote, "frame {} is not a multiple of RECORD_EVERY", i + 1); 123 } 124 assert_eq!(rec.last_frame, 5); 125 rec.flush().unwrap(); 126 let back = crate::log::read_all(&dir.join("log.bin")).unwrap(); 127 assert_eq!(back.len(), 5); 128 } 129 130 #[test] 131 fn a_keyframe_is_written_every_record_every_frames() { 132 let dir = scratch("keyframe-due"); 133 let mut rec = Recorder::open(&dir, 0).unwrap(); 134 for i in 1..=RECORD_EVERY { 135 let wrote = rec.record_snapshot(entry(), || Ok(vec![1, 2, 3])).unwrap(); 136 assert_eq!(wrote, i == RECORD_EVERY, "frame {i}"); 137 } 138 let idx = KeyframeIndex::open(&dir.join("keyframes")).unwrap(); 139 assert_eq!(idx.frames(), vec![RECORD_EVERY]); 140 assert_eq!(idx.load(RECORD_EVERY).unwrap(), vec![1, 2, 3]); 141 } 142 143 #[test] 144 fn reopening_a_branch_resumes_last_frame_from_its_keyframes() { 145 let dir = scratch("resume"); 146 { 147 let mut rec = Recorder::open(&dir, 0).unwrap(); 148 for _ in 1..=RECORD_EVERY { 149 rec.record_snapshot(entry(), || Ok(vec![9; 10])).unwrap(); 150 } 151 } 152 let rec = Recorder::open(&dir, 0).unwrap(); 153 assert_eq!(rec.last_frame, RECORD_EVERY); 154 } 155 156 #[test] 157 fn opening_a_forked_branch_starts_at_its_fork_frame() { 158 // store_fork_keyframe itself needs a booted cartridge; the probe 159 // (apps/replay-probe) exercises it against the real console. 160 let dir = scratch("fork-open"); 161 let rec = Recorder::open(&dir, 500).unwrap(); 162 assert_eq!(rec.last_frame, 500); 163 } 164}