Revision Chain Storage (on-disk layout)
The fixed-size, append-friendly layout that holds every live revision of a
Versionedcomponent on disk.
Status: โ Implemented ยท Visibility: Internal ยท Category: Revision
๐ฏ What it solves
MVCC needs somewhere to put the multiple concurrently-live revisions a Versioned component can accumulate while older snapshots still depend on them. That history has to be cheap to append to on every commit, cheap to walk on every read, and bounded in size so a hot entity doesn't grow without limit. A naive per-revision allocation (or a single growable array per entity) would fragment storage and make both append and reclaim expensive. Revision Chain Storage is the physical layout that makes append, walk, and reclaim all O(1)-ish operations instead of O(n) ones.
โ๏ธ How it works (in brief)
Each entity's component history for a given Versioned component type lives in a singly-linked chain of fixed 64-byte chunks โ a circular buffer, so the oldest slot is reused once a chunk fills and its entries are no longer needed. The first chunk carries a small header (lock, item/chain counts, owning entity's primary key, a monotonic commit counter) plus a handful of revision entries; once those fill up, the chain grows by linking in additional 64-byte overflow chunks. Each revision entry is tiny โ just enough to point at the actual component payload (stored separately, sized to the component) and to record when and by which transaction it was written. Capacity per chunk is derived from the entry's struct size at compile/runtime, not hardcoded, so the layout self-adjusts if the entry ever changes shape.
๐ป Usage
This is a storage primitive, not something application code allocates or walks directly โ it exists transparently under any Versioned component (the default storage mode), created on first write and grown automatically as revisions accumulate:
[Component("Game.Health", 1)] // StorageMode.Versioned is the default
struct Health { public int Current; public int Max; }
[Archetype]
partial class Unit : Archetype<Unit>
{
public static readonly Comp<Health> Health = Register<Health>();
}
using var tx = dbe.CreateQuickTransaction(DurabilityMode.Immediate);
var soldier = tx.Spawn<Unit>(Unit.Health.Set(new Health { Current = 100, Max = 100 }));
tx.Commit(); // allocates the entity's first revision-chain chunk
using var hit = dbe.CreateQuickTransaction(DurabilityMode.Immediate);
hit.OpenMut(soldier).Write(Unit.Health).Current -= 25;
hit.Commit(); // appends a new revision entry into the same chain
Read-only introspection of the storage footprint goes through the same segment enumeration as every other structure:
foreach (var seg in dbe.EnumerateStorageSegments())
{
if (seg.Kind == StorageSegmentKind.Revision)
{
Console.WriteLine($"revision table root={seg.RootPageIndex} stride={seg.Stride} " +
$"allocChunks={seg.AllocatedChunkCount} freeChunks={seg.FreeChunkCount}");
}
}
โ ๏ธ Guarantees & limits
- Every chunk is exactly 64 bytes (one cache line); chunk capacity (3 entries in the first chunk, 5 per overflow chunk today) is computed from the entry struct's size, not hardcoded โ a layout change flows through automatically.
- One revision-chain chunk is the fixed minimum cost of a
Versionedcomponent per entity, even if it's never updated again after creation. - A short revision history (up to the first chunk's capacity) needs no overflow allocation at all โ this is the steady state most entities should stay in; the revision garbage collector (see GC entry) is what keeps long-lived entities from drifting out of it.
- The chain is singly-linked and circular by design: appending past capacity links a new chunk rather than copying existing entries, and reclaiming the oldest slot is an index bump, not a deallocation.
- A component payload (
ComponentChunkId == 0) inside a revision entry means a tombstone โ a delete, not a missing record โ so older snapshots can still resolve what an entity looked like before it was deleted. - Layout details (header/entry field order, packed bit layout) are internal and not part of the application-facing API surface; only the aggregate footprint is exposed, via
EnumerateStorageSegments.
๐งช Tests
- CompRevStorageElementTests โ layout-level unit tests:
Sizeof_Is12Bytes,ChunkCapacity_Root_Is3/ChunkCapacity_Overflow_Is5, bitfield independence ofTSN/IsolationFlag/UowId/ComponentChunkId,Void/IsVoidtombstone semantics
๐ Related
- Deep dive: doc/in-depth-overview/05-revision.md
- Related feature: Revision Append & Chain Growth (what populates this layout), MVCC Snapshot Visibility (the reader that walks this chain)
- Sibling: Storage Mode: Versioned โ Versioned mode is the ECS-facing side of this on-disk layout
- Source:
ComponentRevisionManager,CompRevStorageHeader/CompRevStorageElement,RevisionEnumerator