Optimistic parallel discrete-event simulation on the BEAM.
The engine handles every Time Warp mechanic — speculative execution, rollback,
anti-messages, GVT, fossil collection. A domain author implements only
TimeWarp.Model.
ids = [:a, :b, :c, :d]
{:ok, sim} =
TimeWarp.start_run(
model: TimeWarp.Examples.PHOLD,
lps: Map.new(ids, &{&1, %{ids: ids, max_delay: 10}}), # lp_id => init_args
seed: 42,
until: {:vtime, 1_000},
init_events: for(i <- 0..7, do: {Enum.at(ids, rem(i, 4)), i, :ping}),
gvt_interval_ms: 50,
# A binding window is the only bound on retained state — see the warning
# below. PHOLD is all-to-all and does not bound on its own.
time_window: {:vtime, 20},
check_purity: false, # the purity divergence detector
thrash_guard: nil # or {rollbacks_per_s, window_ms}
)
{:done, _} = TimeWarp.await(sim)
report = TimeWarp.report(sim)Options
:model— aTimeWarp.Modelimplementation (required).:lps—%{lp_id => init_args}(required).:seed— integer seed for per-LP PRNGs (default0).:until—{:vtime, t}run horizon, or:infinity(default).:init_events—[{target, at, payload}]genesis events kicking off the run.:gvt_interval_ms— wall-clock cadence of GVT cycles (default50).:check_purity— run the double-call purity probe (the divergence detector, defaultfalse).:thrash_guard—{rollbacks_per_s, window_ms}thrash circuit breaker. Off by default. Note it brakes on rollback rate, which is not the same as retained state — see the retention warning below.:time_window—{:vtime, W}to bound optimism to[GVT, GVT+W], or:none(default). Deadlock-free (the effective bound ismax(GVT+W, global_min_event)). W trades parallelism for conflict, not a free storm fix, and it is also the only bound on retained state — see both notes below.:cancellation—:aggressive(default) |:lazy. Lazy suppresses an anti-message when re-execution re-emits an identical event; it only pays on models whose straggler-path events are PRNG-free (seeTimeWarp.Model— it is inert and can amplify the storm on random models like PHOLD).:placement—%{lp_id => node}pinning LPs to nodes. Static, no migration; an LP not listed runs on the local node. LPs are addressed by pid, so a remote LP is mechanically identical to a local one and the straggler protocol is unchanged.:gvt_mode—:mattern(default) |:freeze. Mattern's colored-message algorithm is authoritative.:freezeis the earlier stop-the-world computation, kept as the test oracle and as the single end-of-run termination confirmation; it is not a steady-state path.:lp_call_timeout— milliseconds, default5_000. Timeout for the Coordinator's own calls into LPs (:dumpbehindreport/1,:gvt, the freeze-confirm poll). These execute inside the Coordinator process and an LP may be remote, so exceeding this does not fail one call — it fails the run. Raise it for high-latency links or large per-LP state.:quiescence_timeout_ms— milliseconds, default1_000. Ceiling on the freeze-confirm quiescence poll; exceeding it raises, meaning in-flight messages are not settling. The workable value scales with link latency, so a distributed run legitimately needs more than a local one.:net_delay— test-only. Artificial per-message delay in milliseconds, used by the suite to characterize behaviour under inter-node latency. Not a production knob.
One run at a time per BEAM
The Coordinator is a global singleton, so a node hosts one run. start_run/1
returns {:error, :run_active} while a run is in progress, and when it does
start it tears down every LP on the placement nodes — including any left by
a previous run. Concurrent runs are not supported: the teardown is not scoped to
a single run, so anything else living under TimeWarp.LPSupervisor is destroyed
with it. Await or stop a run before starting the next.
:none places no bound on retained state
GVT lag is what bounds memory. Fossil collection reclaims only below GVT, so
nothing is reclaimed while GVT is pinned near the floor — retained history,
output logs and per-event model state all accumulate. With :none there is no
limit on how far a logical process runs ahead of GVT, so a workload whose input
arrives far out of order can hold GVT down for an entire run and grow retained
state until the VM exhausts memory. A binding W caps the lag and therefore
caps the memory; it is the brake, and there is no separate one.
The cliff is steep rather than gradual. A 93-event run across three logical
processes under strictly-descending arrival finishes in 3.6 s at nine windows
per process (437,034 rollbacks) and fails to terminate at fifteen; the same
shape with W bound to 50 finishes in 237 ms with 220 rollbacks. Confirm a
workload bounds before raising until or widening it — max_gvt_lag in
report/1 is the direct readout, and an OOM here is almost always an unbounded
workload rather than a defect.
:time_window is a parallelism trade, not a storm fix
A tight W caps how far any LP speculates past GVT, so it bounds rollback
depth and the storm for any workload — but it does so by throttling
parallelism. A W tight enough to tame a high-conflict storm has throttled
execution toward conservative execution: the right answer there is that the
workload is a poor fit for optimism, not that a smaller W is a win. Find W
empirically per workload; a value tuned on one model says nothing about another.
Summary
Functions
Block until the run terminates. Returns {:done, info} or {:failed, reason}.
Snapshot of the run: GVT, per-LP state/stats, and aggregate totals.
Start a run. Returns {:ok, sim} where sim is the handle for await/report.
Cancel a run: tear down its LPs and return the engine to idle.
Types
@type config() :: keyword()
Functions
Block until the run terminates. Returns {:done, info} or {:failed, reason}.
Snapshot of the run: GVT, per-LP state/stats, and aggregate totals.
Start a run. Returns {:ok, sim} where sim is the handle for await/report.
Returns {:error, :run_active} if a run is already in progress — the active run
is left untouched. await/1 or stop_run/1 it first.
@spec stop_run(pid()) :: :ok
Cancel a run: tear down its LPs and return the engine to idle.
For a run that never reaches a terminal state on its own — an unbounded horizon
held open deliberately, or one abandoned early. Returns :ok.