Record & Replay
Capture a robot's execution and replay it later for debugging or regression testing. Unlike external bag tools, HORUS recording is built into the scheduler — zero serialization overhead, tick-perfect determinism, and mixed replay for what-if testing.
When To Use This
- Debugging a bug that only reproduces with specific sensor data
- Regression testing a new controller against recorded inputs
- Comparing two algorithm versions on identical data
- Capturing field data for offline analysis
Prerequisites
- HORUS installed (Installation Guide)
- Basic understanding of nodes and topics (Quick Start)
horus.toml
[package]
name = "record-replay-demo"
version = "0.1.0"
description = "Record and replay demonstration"
Complete Code
use horus::prelude::*;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, LogSummary)]
#[repr(C)]
struct SensorData {
value: f32,
timestamp_ns: u64,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, LogSummary)]
#[repr(C)]
struct ControlCmd {
output: f32,
}
// ── Sensor (publishes simulated readings) ─────────────────
struct Sensor {
pub_data: Topic<SensorData>,
tick: u64,
}
impl Sensor {
fn new() -> Result<Self> {
Ok(Self {
pub_data: Topic::new("sensor.data")?,
tick: 0,
})
}
}
impl Node for Sensor {
fn name(&self) -> &str { "sensor" }
fn tick(&mut self) {
let t = self.tick as f32 * 0.01;
let _ = self.pub_data.send(SensorData {
value: (t * 3.0).sin() * 5.0,
timestamp_ns: horus::now().as_nanos() as u64,
});
self.tick += 1;
}
}
// ── Controller (processes sensor data) ────────────────────
struct Controller {
sub_data: Topic<SensorData>,
pub_cmd: Topic<ControlCmd>,
}
impl Controller {
fn new() -> Result<Self> {
Ok(Self {
sub_data: Topic::new("sensor.data")?,
pub_cmd: Topic::new("ctrl.cmd")?,
})
}
}
impl Node for Controller {
fn name(&self) -> &str { "controller" }
fn tick(&mut self) {
if let Some(data) = self.sub_data.recv() {
let output = -0.5 * data.value; // Simple proportional
let _ = self.pub_cmd.send(ControlCmd { output });
}
}
}
fn main() -> Result<()> {
// ── Step 1: Record ────────────────────────────────────
println!("=== Recording 5 seconds ===");
let mut sched = Scheduler::new()
.tick_rate(100_u64.hz())
.with_recording();
sched.add(Sensor::new()?).order(0).build()?;
sched.add(Controller::new()?).order(1).build()?;
sched.run_for(std::time::Duration::from_secs(5))?;
let paths = sched.stop_recording()?;
println!("Saved to: {:?}", paths);
// ── Step 2: Full replay ───────────────────────────────
// Replays ALL nodes exactly as recorded
println!("\n=== Full replay ===");
let sched_path = paths.iter()
.find(|p| p.to_string_lossy().contains("scheduler@"))
.expect("scheduler recording");
let mut replay = Scheduler::replay_from(sched_path.clone())?;
replay.run()?;
// ── Step 3: Mixed replay (the powerful part) ──────────
// Replay recorded sensor, run NEW controller live
println!("\n=== Mixed replay: recorded sensor + live controller ===");
let sensor_path = paths.iter()
.find(|p| p.to_string_lossy().contains("sensor@"))
.expect("sensor recording");
let mut mixed = Scheduler::new()
.tick_rate(100_u64.hz());
mixed.add_replay(sensor_path.clone(), 0)?; // Recorded sensor
mixed.add(Controller::new()?).order(1).build()?; // Live controller
mixed.run_for(std::time::Duration::from_secs(5))?;
Ok(())
}
Understanding the Code
.with_recording()/recording=Truecaptures every node's topic inputs and outputs each tick as raw shared memory bytes — zero serialization overheadstop_recording()flushes to disk and returns file paths (one.horusfile per node + one scheduler metadata file)replay_from()loads the scheduler recording and replays all nodes with the original timing and dataadd_replay()is the key differentiator from external bag tools — it replays one node's recorded outputs while running other nodes live, enabling regression testing without re-recording
CLI Workflows
# Record during any run
horus run --record my_session
# List and inspect
horus record list --long
horus record info my_session
# Full replay (all nodes)
horus record replay my_session
horus record replay my_session --speed 0.5 --start-tick 100
# Mixed replay (recorded sensor + live code)
horus record inject my_session --nodes sensor
# Compare two runs
horus record diff session_v1 session_v2
# Export for analysis
horus record export my_session --output data.csv --format csv
# Cleanup
horus record clean --max-age-days 30
horus record delete old_session
Common Errors
| Symptom | Cause | Fix |
|---|---|---|
| Empty recording | .with_recording() not set | Add to scheduler builder or use --record CLI flag |
| Replay output differs | Code changed between record and replay | Expected for mixed replay; use replay_from for exact reproduction |
inject --nodes X has no effect | Topic name mismatch | Names are case-sensitive and dot-separated |
FileNotFoundError on replay | Wrong session name | Run horus record list to see available sessions |
See Also
- Record & Replay Reference — Full API docs, design decisions, trade-offs
- Debug with Record & Replay Tutorial — Step-by-step debugging walkthrough
- BlackBox Flight Recorder — Lightweight always-on crash forensics
- Deterministic Mode — Bit-identical replay requirements