Table of Contents

Parallel Tick Fence (WriteTickFence)

Spreads the post-tick WriteTickFence step — cluster migrations, AABB refresh, WAL publish — across the worker pool instead of running it serially on one thread.

Status: ✅ Implemented · Visibility: Public · Level: 🟣 Advanced · Category: Runtime

🎯 What it solves

WriteTickFence applies cluster migrations (moving an entity to a cluster for its new grid cell when it crosses a cell boundary), recomputes spatial AABBs, and publishes WAL records for the tick's dirty cluster content. Run serially on the single TickDriver thread, it can dominate tick time on a large simulation with heavy migration/AABB churn — one thread working while the rest of the worker pool sits idle, capping how far a tick can scale with core count. The Parallel Tick Fence spreads that work across all workers so it scales the same way the rest of the tick DAG does.

⚙️ How it works (in brief)

The fence is split into four chained phases — Prep, Migrate, AabbRefresh, Finalize — each dispatched as a chunk-parallel system after the user's tick DAG completes. A per-tick work planner sizes chunks from measured per-unit cost, continuously recalibrated from a sliding window of recent ticks, so work is bin-packed evenly across workers rather than split by a fixed count. The parallel dispatch is entirely internal — it is configured via RuntimeOptions, not invoked.

WriteTickFence itself is public, and calling it is not optional outside the runtime. Under TyphonRuntime the fence is invoked automatically at tick end and application code never touches it. A host embedding the engine without the runtime must call dbe.WriteTickFence(n) itself, once per tick — see Embedding without the runtime. What is internal is how the fence spreads its work across workers, not whether the fence runs.

💻 Usage

var options = new RuntimeOptions
{
    EnableParallelFence = true,       // default — parallelize WriteTickFence across workers
    FenceChunkOversubscription = 2,   // chunk-count cap = oversubscription × WorkerCount
    AdaptiveFenceCost = true,         // recalibrate migration/AABB cost from measured wall-time
};

using var runtime = TyphonRuntime.Create(dbe, schedule => { /* ... */ }, options);
Option Default Effect
EnableParallelFence true Parallel fence sub-DAG vs. the single-threaded WriteTickFence
FenceChunkOversubscription 2 Chunk-count cap = oversubscription × WorkerCount; smooths per-worker preemption jitter
FenceCostModel AntHill-calibrated defaults Seeds per-unit cost (migration ≈ 33µs/entity, AABB ≈ 2.4µs/cluster)
AdaptiveFenceCost true Continuously recalibrates FenceCostModel from a 64-tick sliding window; disable to pin the static seed (repeatable benchmarks)

⚠️ Guarantees & limits

  • Application code never interacts with the fence DAG directly — there is no API surface beyond the RuntimeOptions knobs above.
  • EnableParallelFence = false falls back to the serial WriteTickFence — useful for diagnostics or as a regression safety valve; behavior is otherwise equivalent.
  • Requires the engine's WAL durability mode (mandatory engine-wide) — both the parallel and serial fence paths rely on it to drain dirty pages.
  • Chunk sizing targets ~200µs of CPU per chunk; below that floor, per-dispatch overhead (worker wake, page cache attach) dominates the chunk's own cost.
  • Per-chunk dirty-page tracking uses a local pooled ChangeSet, its marks released at chunk end (via ReleaseDirtyMarks) — does not change the engine's WAL/checkpoint contract for the rest of the tick.
  • Runs after the user's tick DAG completes and before UoW.Flush() — its WAL publishes become durable in the same fsync as the tick's other commits, not a tick later.