Time-Travel Debugging
View SourceEvery update/2 call gets snapshotted -- the message, the model before, the model after. Step backwards and forwards through your app's history, diff any two points, restore old state into the live app. Disabled by default, zero overhead when off.
Enabling
Raxol.start_link(MyApp, time_travel: true)That's it. The Dispatcher now records a snapshot after every update/2.
Navigation
Raxol.Debug.TimeTravel keeps a cursor into the snapshot history:
alias Raxol.Debug.TimeTravel
{:ok, snapshot} = TimeTravel.current()
# Walk through history
{:ok, snapshot} = TimeTravel.step_back()
{:ok, snapshot} = TimeTravel.step_forward()
{:ok, snapshot} = TimeTravel.jump_to(42)
# Push a historical model back into the live app
# (sends {:restore_model, model} to the Dispatcher)
:ok = TimeTravel.restore()
# Resume recording after restoring
:ok = TimeTravel.resume()
# Pause recording while you poke around
:ok = TimeTravel.pause()
entries = TimeTravel.list_entries()
# => [%{index: 0, message: :inc, changed: true}, ...]
count = TimeTravel.count()
:ok = TimeTravel.clear()Diffing
Pick any two snapshots and see exactly what changed between them:
{:ok, changes} = TimeTravel.diff(10, 15)
# Each change is one of:
# {:changed, [:path, :to, :key], old_value, new_value}
# {:added, [:path, :to, :key], value}
# {:removed, [:path, :to, :key], value}You can also diff arbitrary maps directly with Snapshot.diff/2 -- it does recursive comparison and tracks the key path:
alias Raxol.Debug.Snapshot
Snapshot.diff(
%{count: 1, items: [1, 2]},
%{count: 2, items: [1, 2, 3]}
)
# => [
# {:changed, [:count], 1, 2},
# {:changed, [:items], [1, 2], [1, 2, 3]}
# ]
Snapshot.changed?(snapshot) # did the model actually change?
Snapshot.summary(snapshot) # "Snapshot #42: :inc (2 changes)"Export / Import
Save a debugging session to disk and load it later:
:ok = TimeTravel.export("debug_session.bin")
{:ok, count} = TimeTravel.import_file("debug_session.bin")
# => {:ok, 150}Uses Erlang's binary term format.
Manual Recording
TimeTravel.record/4 is called by the Dispatcher automatically, but you can also record snapshots yourself:
TimeTravel.record(message, model_before, model_after)Snapshots live in a CircularBuffer -- old ones get evicted when it's full, so memory stays bounded.