Snapshots
Snapshots let you checkpoint the VM state after a historical replay and restore it instantly on the next start, avoiding expensive full replays.
Why Use Snapshots
In a server deployment, each running script typically processes thousands of historical bars before reaching the live feed. Replaying all historical bars on every restart is expensive. Snapshots solve this by serializing the complete VM state — variables, chart data, strategy state, and configuration — into a binary blob that can be persisted and restored later.
Cold start (first run)
├─ Build Instance
├─ Pull all historical bars from DataProvider ← slow (thousands of bars)
├─ save_state() → Vec<u8>
└─ Persist bytes to storage
Warm restart (subsequent runs)
├─ Load persisted bytes
├─ restore_state(&bytes) → Instance ← fast (no replay)
└─ run() — automatically resumes from the next barSaving State
Call save_state() after the last confirmed bar to produce a serializable byte blob.
Simple: save after full replay
use navi_vm::{Instance, TimeFrame, snapshot::SnapshotError};
let mut instance = Instance::builder(provider, source, TimeFrame::days(1), "NASDAQ:AAPL")
.build().await?
.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;
// Save — the current bar must be confirmed
let snapshot: Vec<u8> = instance.save_state()?;
// Persist to database, disk, object storage, etc.
persist_to_storage(&snapshot);Advanced: checkpoint mid-stream via RunHandle
Instance::run() returns a RunHandle that implements DerefMut<Target = Instance>, so you can call save_state() directly through the handle without dropping the run. Use current_bar_confirmed() to guard the checkpoint so you only save at confirmed-bar boundaries:
use navi_vm::{Event, Instance, TimeFrame};
let mut handle = instance.run("NASDAQ:AAPL", TimeFrame::days(1));
while let Some(result) = handle.next_event().await {
match result? {
Event::BarEnd => {
if handle.current_bar_confirmed() {
let snapshot = handle.save_state()?;
persist_to_storage(&snapshot);
}
}
_ => {}
}
}WARNING
The current bar must be confirmed before saving. If you are in the middle of a realtime bar, confirm it first. save_state() returns SnapshotError::UnconfirmedBar if the bar has not been confirmed.
When a new realtime timestamp arrives, Navi first emits the previous realtime bar's auto-confirm events and only then starts the new realtime bar. That auto-confirm BarEnd is a safe checkpoint boundary for save_state(), so current_bar_confirmed() will be true there.
Restoring State
Use Instance::restore_state() to decode the snapshot bytes into a RestoreBuilder, then configure it and call build():
let bytes: Vec<u8> = load_from_storage();
let instance = Instance::restore_state(&bytes)?
.with_data_provider(provider) // required: supply a data provider
.build()?
.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;
// run() automatically resumes from the next bar after the snapshotCandlestick Data
Candlestick data is not included in the snapshot. When run() is called on a restored instance, the VM automatically passes last_bar_time + 1 to the data provider so that only new bars are fetched.
RestoreBuilder Options
| Method | Description |
|---|---|
with_data_provider(provider) | Inject a DataProvider for request.security() and bar data |
build() | Finish restoring and return the Instance |
Invalidation
Snapshots embed the compiled program and a format version. A snapshot is only valid for the exact binary and script that created it. You must discard the snapshot and do a full cold replay when:
- Navi is upgraded — the snapshot format may have changed
- The Navi script source changes — a different compiled program
- Input parameter values change — inputs affect execution state
A simple invalidation strategy: store the snapshot alongside a hash of the Navi source. On startup, compare the hash; if it differs, delete the snapshot and replay from scratch.
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn source_hash(source: &str) -> u64 {
let mut hasher = DefaultHasher::new();
source.hash(&mut hasher);
hasher.finish()
}
// Save
let hash = source_hash(source);
persist_snapshot(&snapshot, hash);
// Restore
let (bytes, saved_hash) = load_snapshot();
if source_hash(source) != saved_hash {
// Script changed — discard snapshot and cold replay
} else {
let instance = Instance::restore_state(&bytes)?.build()?;
// ...
}Error Handling
| Error | Cause |
|---|---|
SnapshotError::UnconfirmedBar | save_state() called before confirming the current bar |
SnapshotError::Encode(msg) | Bincode serialization failed |
SnapshotError::Decode(msg) | Bincode deserialization failed (corrupt or truncated data) |
SnapshotError::VersionMismatch { expected, got } | Snapshot was created with a different Navi version |
Snapshots vs. reset()
Both snapshots and instance.reset() let you re-run a script, but they serve different purposes:
| Snapshot | reset() | |
|---|---|---|
| Retains accumulated state | ✅ | ❌ |
| Skips historical replay on next start | ✅ | ❌ |
| Suitable for server warm restart | ✅ | ❌ |
| Suitable for parameter sweeps | ❌ | ✅ |
| Re-runs from bar 0 | ❌ | ✅ |
Use reset() when you want to replay the same bars from scratch (e.g. comparing different input settings). Use snapshots when you want to resume execution from where you left off.
Next Steps
- JIT Compilation — enabling ahead-of-time compilation for faster execution