Table of Contents

Class DatabaseEngine

Namespace
Typhon.Engine
Assembly
Typhon.Engine.dll

The open of a bundle a restore wrote (plan 1413 §2.9, BKP-05): restore.json names a new WAL timeline, which the open adopts before the WAL manager starts, then deletes the marker. A marker it cannot trust, one naming another database, or one beside a WAL directory other than the bundle's refuses the open, writing nothing.

public class DatabaseEngine : ResourceNode, IResource, IDisposable, IMetricSource, IDebugPropertiesProvider
Inheritance
DatabaseEngine
Implements
Inherited Members
Extension Methods

Remarks

DatabaseEngine registers itself under the DataEngine subsystem in the resource tree. ComponentTables are registered as children of this engine.

Properties

Backup

Online backup of this running database: await engine.Backup.CreateAsync(storePath).

public IBackupService Backup { get; }

Property Value

IBackupService

CommitPipelineFailure

The failure that stopped this engine's commits: a commit whose records were appended threw before its publish completed (AP-03), so memory holds a partial publish. Null while the engine commits. Once set, every commit that would write is refused, the checkpoint stays below that commit's records, and the engine closes as a crash, so the next open replays them. Only reopening clears it.

public Exception CommitPipelineFailure { get; }

Property Value

Exception

CurrentTsn

The next transaction number this database will hand out — its position in a globally monotonic, restart-durable sequence.

public long CurrentTsn { get; }

Property Value

long

Remarks

Public because it is the right-hand side of the drift measure: a profiling capture records the transaction window it covered (#614), and comparing its upper bound against this says how far behind the database a capture has fallen — "this profile is 845,331 transactions behind" rather than a vague "this is old". Cheap: a field read, no lock.

DBD

Registry of the component and archetype schema definitions registered on this engine instance.

public DatabaseDefinitions DBD { get; }

Property Value

DatabaseDefinitions

DatabaseId

Durable identity of this database — minted once when the database is created and never rewritten, so it survives reopen, move, rename and restore. A bundle copied on disk keeps the same id: the value identifies a database lineage, not a file, which is exactly what pairing a profiling capture to its database needs (see claude/design/Apps/Workbench/10-database-and-profiles.md, D-2).

public Guid DatabaseId { get; }

Property Value

Guid

Remarks

Persisted as Typhon.Engine.DatabaseEngine.BK_DatabaseId in the bootstrap dictionary, packed as four ints (a Guid is exactly 16 bytes). Databases created before this key existed adopt an id on their first open by a build that knows about it — see Typhon.Engine.DatabaseEngine.EnsureDatabaseIdentity.

DurableLsn

The highest LSN the WAL writer has made durable — the watermark every commit's durability wait is measured against. Zero when there is no WAL.

public long DurableLsn { get; }

Property Value

long

Remarks

Public because "has my write reached the disk yet?" is an application question, and until now there was no way to ask it. GetWalTotalBytes() is public for the same class of reason, but it answers in segment-sized steps, so it cannot say whether a particular tick's records were persisted — a preallocated segment absorbs thousands of records without changing size.

It advances asynchronously, from the writer thread. Read it twice and the second value may be higher for reasons that have nothing to do with the caller. The guarantee worth relying on is directional: once a commit's wait has returned, this is at or above that commit's LSN, and it never goes backwards.

EpochManager

Epoch manager coordinating safe, lock-free memory reclamation across concurrent readers and writers.

public EpochManager EpochManager { get; }

Property Value

EpochManager

IsDisposed

true once the engine has been disposed; further operations on it are invalid.

public bool IsDisposed { get; }

Property Value

bool

IsNewlyCreated

true when this open created the database bundle (it did not exist before); false when it reopened an existing one. Use it to gate one-time bootstrap work (initial data, first-run migrations). Note the engine already offers a crash-safe, declarative alternative via Seed(int, Action<Transaction>); prefer that for seeding, and this flag for cheaper conditional logic. It reflects file existence at open, so after a crash mid-initialisation it reads false — it is not, by itself, a "seeding completed" signal.

public bool IsNewlyCreated { get; }

Property Value

bool

LastFenceMigrationParallelism

How many workers' worth of CPU one unit of span bought in the previous tick's migration phases — summed CPU over elapsed span across Migrate, IndexMassUpdate and EntityMapUpdate.

public double LastFenceMigrationParallelism { get; }

Property Value

double

Remarks

This is the number that makes the summed-CPU members readable, and it is why they must never be presented without it. MigrationTotalMs and its siblings are CPU-milliseconds summed across workers: W workers each busy for one millisecond report W. Raising the worker count therefore RAISES them on unchanged work, and read alone that looks like contention. It is contention only where the summed CPU rises FASTER than this ratio does; where the two rise together, the work was merely spread and the wall cost did not move. Dividing a summed-CPU figure by this yields the elapsed cost — the thing a frame budget is actually spent in.

Divide only a numerator this ratio's denominator covers. It spans three phases, so the matching numerator is MigrationTotalMs — the whole migration. MigrationExecuteMs brackets the migrant loop alone, roughly half a migration since the index and EntityMap applies moved into their own phases, and dividing it by this ratio produces the elapsed time of nothing at all.

A LEVEL, and the one member that is deliberately STALE rather than reset. A tick whose migration phases did no work leaves the previous value standing rather than reporting zero, because a zero here would read as "infinitely parallel" wherever it is used as a divisor. That is the one documented exception to the surface's zero-means-zero discipline, and it means this member alone can describe a tick other than the last one.

Below 1 is a real reading, not a fault. It means the phases' elapsed span exceeded the CPU they spent — dispatch and barrier overhead larger than the work, which is what a parallel fence looks like on a population too small to be worth splitting.

1 exactly on a host driving WriteTickFence(long, ChangeSet) itself: one thread, so summed CPU IS elapsed.

LastFenceSpanMs

How long the previous tick's partitioning fence took, in milliseconds — the span, not summed CPU. The interval runs from the start of Prep's Prepare to the end of the last phase that dispatched a chunk, so it covers the six phase spans plus the scheduler's gaps between them.

public double LastFenceSpanMs { get; }

Property Value

double

Remarks

This is the number that answers "what did the fence cost the frame". The sum of the six phase spans is what the partitioning COSTS; this is what the host WAITS, and a frame budget is spent in the second. Read MigrationTotalMs against it: that one is CPU-milliseconds summed across workers, so their ratio is roughly the parallelism the migration work actually achieved. Presenting the summed figure as the fence's duration is the error that made an 8 ms budget buy one repair unit.

Engine-wide, deliberately not on SpatialMigrationTelemetry. One fence serves every archetype, so hanging it off a per-archetype snapshot would invite GetSpatialTelemetryTotal() to sum one tick's fence once per archetype.

Zero on a host that drives WriteTickFence(long, ChangeSet) itself rather than running the parallel fence: the phase-exec systems that time it never run, so there is no span to report. That zero means "the parallel fence did not drive this tick", not "the fence was free".

LastFenceStallMs

How long the previous tick's partitioning fence blocked the host, in milliseconds — the whole fence, measured on the tick thread, from the moment the fence window opens to the moment it closes. This is the interruption a host feels: no user system runs inside it at any worker count.

public double LastFenceStallMs { get; }

Property Value

double

Remarks

Read this, not LastFenceSpanMs, to answer "how long was user code stopped". The span starts at Prep's Prepare, so it omits the serial prep that runs first on the tick thread — the fence-context reset, the dormancy drain and ProcessTableFence over every component table. That prep is single-threaded by construction, so no worker count shrinks it, and a sweep across worker counts that reads only the span reports a speed-up the host never receives. LastFenceStallMs - LastFenceSpanMs is that serial remainder, and it is Amdahl's fraction for the fence.

Engine-wide, for the same reason as the span: one fence serves every archetype.

Zero on a host that drives WriteTickFence(long, ChangeSet) itself, which is the same zero-means-zero discipline as the span — such a host is holding its own stopwatch around the call and does not need this one.

MMF

Backing paged memory-mapped file store holding all persisted segments of this database.

public ManagedPagedMMF MMF { get; }

Property Value

ManagedPagedMMF

OpenCellStateRebuildMs

Milliseconds spent reconstructing the cluster-to-cell mapping at open (RebuildCellState).

public double OpenCellStateRebuildMs { get; }

Property Value

double

Remarks

The cell layer is transient — nothing about the grid is persisted, so every open reconstructs it from entity positions in an O(entities) sweep. This figure and OpenClusterAabbRebuildMs are what decide whether that reconstruction stays affordable at target entity counts or the grid must be persisted. Zero on a database with no cluster-eligible archetypes, or when every archetype opened with no active clusters.

Describes the most recent InitializeArchetypes() call, not the sum of all of them: a repeat call reallocates the per-archetype state it measures, so every other counter restarts with it and a lifetime sum here would be the one figure that did not.

OpenClusterAabbRebuildMs

Milliseconds spent recomputing per-cluster AABBs and the per-cell spatial index at open (RebuildClusterAabbs).

public double OpenClusterAabbRebuildMs { get; }

Property Value

double

Remarks

See OpenCellStateRebuildMs — the two are halves of the same startup sweep and are read together.

PageBodyReadCount

Number of page bodies read through TryReadPageBody(int, Span<byte>) since the engine opened. The Database File Map's detail tier is viewport-scoped, never a full-file scan — the A2 tests assert this counter grows only by the size of the requested region.

public long PageBodyReadCount { get; }

Property Value

long

Realms

The engine's realms: Register(RealmId, RealmConfig) before InitializeArchetypes(), and what is registered.

public RealmRegistry Realms { get; }

Property Value

RealmRegistry

StorageHealth

How the page cache keeps up with the work (#1382): Healthy while allocations find a slot at once, then Pressured, Overloaded and Starved as back-pressure lasts and pages stop being served. The engine slows the work rather than failing it; this is where an application sees that the cache or the disk it gave the engine is too small for its load — also in the log on every change, the profiler's gauges and the resource tree.

public StorageHealthState StorageHealth { get; }

Property Value

StorageHealthState

Methods

ArchetypeIdOf<TArch>()

What ArchetypeId carries for TArch in THIS database, so an application can tell which archetype an entity id names. Typhon.Engine.DatabaseEngine.NoRoutingId when this database has no such archetype.

public ushort ArchetypeIdOf<TArch>() where TArch : Archetype<TArch>

Returns

ushort

Type Parameters

TArch

The archetype.

Remarks

ArchetypeId is public and, until this method existed, there was nothing to compare it against: the catalog-to-routing map is internal, so an application holding an id of unknown provenance — one a client named, one read out of a spatial query over a union, one stored in a component — could not discover which archetype it belonged to without opening it and guessing from a component read.

Routing ids are per database and assigned in registration order (resumed above the persisted high-water mark on reopen), so the value is stable for the life of a database and must not be persisted by an application as if it were the schema's own id — that is Typhon.Engine.Internals.ArchetypeMetadata.ArchetypeId, the catalog id.

BeginBulkLoad(BulkLoadOptions)

Open a new BulkLoadSession. Acquires the engine-wide exclusive bulk gate; throws BulkSessionAlreadyActiveException if a session is already open.

public BulkLoadSession BeginBulkLoad(BulkLoadOptions options = null)

Parameters

options BulkLoadOptions

Optional configuration. Defaults to a new BulkLoadOptions if null.

Returns

BulkLoadSession

A fresh, open bulk session. Caller must CompleteBulkLoad() or Dispose() when done; the engine-wide gate is held until then.

Remarks

See claude/design/Durability/BulkLoad/01-api.md for the API reference and lifecycle. A bulk session is thread-affine — only the thread that opened it may call methods on it. Regular transactions can run concurrently and see the pre-bulk snapshot.

Mutate through the returned session's Spawn<TArch>(params ReadOnlySpan<ComponentValue>), Update<T>(EntityId, in T), and Destroy(EntityId), then call CompleteBulkLoad() to commit and checkpoint the bulk durably. Disposing without completing discards it — none of the bulk's revisions become visible (UR-03).

Exceptions

BulkSessionAlreadyActiveException

A bulk session is already open on this engine.

ClassifyAllPages(Span<StoragePageType>)

Classifies every file page by semantic type into dest (length ≥ file page count). Built entirely from in-memory structures — the occupancy bitmap and the segment registry — with no data-page I/O. A page owned by no enumerated segment and not a reserved root page resolves to Unknown.

public void ClassifyAllPages(Span<StoragePageType> dest)

Parameters

dest Span<StoragePageType>

ConfigureRealms(int)

Sizes the engine's realm table: realm ids are [0, maxRealms). Must be called before InitializeArchetypes() and before any Register(RealmId, RealmConfig) of a realm other than 0. Without it the engine hosts one realm — the single-world form.

public void ConfigureRealms(int maxRealms)

Parameters

maxRealms int

Exceptions

ArgumentOutOfRangeException

maxRealms is not in [1, 65 535] (0xFFFF is None).

ConfigureSpatialGrid(SpatialGridConfig)

Sets the spatial grid configuration for this engine. Must be called before InitializeArchetypes(). Only required when at least one cluster-eligible archetype has a spatial component — non-spatial engines never need this call.

public void ConfigureSpatialGrid(SpatialGridConfig config)

Parameters

config SpatialGridConfig

Exceptions

InvalidOperationException

Thrown if called after InitializeArchetypes().

CreateUnitOfWork(DurabilityMode, TimeSpan)

Creates a new Unit of Work — the durability boundary for user operations. All transactions must be created through a UoW.

public UnitOfWork CreateUnitOfWork(DurabilityMode durabilityMode = DurabilityMode.Deferred, TimeSpan timeout = default)

Parameters

durabilityMode DurabilityMode

Controls when WAL records become crash-safe. Default is Deferred.

timeout TimeSpan

Lifetime timeout for this UoW. Default uses DefaultUowTimeout.

Returns

UnitOfWork

A new UnitOfWork in Pending state.

Exceptions

ResourceExhaustedException

All UoW registry slots are in use and the deadline expired.

Dispose(bool)

Releases engine resources following the standard dispose pattern. Idempotent — a no-op once IsDisposed is set. Runs the core teardown inside a try/finally so an owned service provider is still released even if a teardown step throws.

protected override void Dispose(bool disposing)

Parameters

disposing bool

true when called from Dispose(); false when called from the finalizer.

EnumerateStorageSegments()

Enumerates every live logical segment's on-disk footprint — the per-ComponentTable segments plus the occupancy-bitmap segment. Authoritative: walks the component-table registry rather than the page cache's lazy segment cache. Read-only; consumes only in-memory structures.

public IReadOnlyList<StorageSegmentDescriptor> EnumerateStorageSegments()

Returns

IReadOnlyList<StorageSegmentDescriptor>

ForceCheckpoint()

Triggers an immediate checkpoint cycle. Flushes all dirty data pages, advances CheckpointLSN, and recycles WAL segments. No-op if WAL/checkpoint is not configured.

public void ForceCheckpoint()

GetAllComponentTables()

Returns all registered ComponentTables. Used by Typhon.Engine.DatabaseEngine.StatisticsWorker to iterate tables, and by external tooling (e.g., the Workbench Schema Inspector) to enumerate the schema. Values returns a stable snapshot, so concurrent registration is safe.

public IEnumerable<ComponentTable> GetAllComponentTables()

Returns

IEnumerable<ComponentTable>

GetArchetypeClusterChunkCount(ushort)

Number of active cluster chunks for the given archetype in this engine. Returns 0 for legacy archetypes (non-cluster storage) or if the archetype has no cluster state yet. Paired with GetArchetypeEntityCount(ushort) the caller can derive occupancy for cluster archetypes: entityCount / (chunkCount * ArchetypeClusterInfo.ClusterSize).

public int GetArchetypeClusterChunkCount(ushort archetypeId)

Parameters

archetypeId ushort

Returns

int

GetArchetypeEntityCount(ushort)

Current entity count for the given archetype in this engine. Returns 0 if the archetype has no state in this engine (not registered or not yet initialized). Used by external tooling (Workbench Schema Inspector) to populate the Archetype panel — intentionally a scalar accessor so the internal Typhon.Engine.Internals.ArchetypeEngineState type does not need to leak into the public surface.

public long GetArchetypeEntityCount(ushort archetypeId)

Parameters

archetypeId ushort

Returns

long

GetComponentTable(Type)

Returns the ComponentTable registered for type, or null if it is not registered.

public ComponentTable GetComponentTable(Type type)

Parameters

type Type

The registered component type.

Returns

ComponentTable

GetComponentTable<T>()

Returns the ComponentTable registered for T, or null if none is registered.

public ComponentTable GetComponentTable<T>() where T : unmanaged

Returns

ComponentTable

Type Parameters

T

The registered unmanaged component type.

GetDebugProperties()

Gets detailed debug properties for diagnostic drill-down.

public IReadOnlyDictionary<string, object> GetDebugProperties()

Returns

IReadOnlyDictionary<string, object>

A dictionary of property names to values. Values should be primitives, strings, or types that have meaningful ToString() implementations.

Remarks

This method may allocate (returns new dictionary) since it's called infrequently. Callers should cache results if needed across multiple accesses.

Property naming convention: Use dot-notation for nested concepts (e.g., "ComponentSegment.Allocated", "Contention.MaxWaitUs").

GetIndexRef<T, TKey>(Expression<Func<T, TKey>>)

Returns an IndexRef for a secondary indexed field of component T. Resolve once (cold path), reuse many times at zero cost (hot path).

public IndexRef GetIndexRef<T, TKey>(Expression<Func<T, TKey>> keySelector) where T : unmanaged

Parameters

keySelector Expression<Func<T, TKey>>

Returns

IndexRef

Type Parameters

T
TKey

GetPKIndexRef<T>()

Returns an IndexRef for the primary key index of component T. Resolve once (cold path), reuse many times at zero cost (hot path).

public IndexRef GetPKIndexRef<T>() where T : unmanaged

Returns

IndexRef

Type Parameters

T

GetPageResidency(int, out bool, out bool)

Reports whether file page filePageIndex is currently resident in the page cache and, if so, whether it is dirty. Non-faulting — it never triggers page I/O — so it must be called before TryReadPageBody(int, Span<byte>) for that page if pre-introspection residency is wanted (the read faults the page in). An out-of-range index yields resident = false.

public void GetPageResidency(int filePageIndex, out bool resident, out bool dirty)

Parameters

filePageIndex int
resident bool
dirty bool

GetRequiredAssemblies()

The schema-assembly manifest persisted in this database: the identity of every .NET assembly that declares a stored component or archetype. Read from the AssemblyR1 catalog, which is loaded on every open (including schemaless), so this is available without any user schema DLL. The core engine assembly is intentionally excluded — it is always loaded. Consumed by tooling (the Workbench) to locate and load the schema assemblies a file depends on.

public IReadOnlyList<AssemblyName> GetRequiredAssemblies()

Returns

IReadOnlyList<AssemblyName>

GetSchemaHistory()

Returns all schema history entries from the audit trail, ordered by primary key (chronological).

public IReadOnlyList<SchemaHistoryR1> GetSchemaHistory()

Returns

IReadOnlyList<SchemaHistoryR1>

GetSpatialGridOccupancy()

The spatial grid's occupancy and memory, or an all-zero snapshot when no grid is configured (#872 step 8, AC-8.5 and AC-8.7).

public SpatialGridOccupancy GetSpatialGridOccupancy()

Returns

SpatialGridOccupancy

Remarks

What each figure answers. BlockCount and OccupiedCellCount against BlockCellCapacity give IntraBlockFill, which is Q3's measurement: a low fill argues for replacing the dense per-block int[] with a bitmask plus compaction (P2), a high one says the dense array is right. ResidentBytes against DenseEquivalentBytes is C2's argument made observable — the dense predecessor allocated a 64-byte descriptor for every cell the world bounds implied, occupied or not.

It is also the guard on the one discipline the sparse grid depends on. A read path that resolves a cell WITH creation — a query broadphase, a tier sweep — materialises a cell per coordinate it touches. Every answer stays correct and nothing fails; the only symptom is ResidentBytes climbing toward DenseEquivalentBytes. See rule VG-02.

Read without a lock, so a snapshot taken during a tick can mix values from either side of a cell creation. These are diagnostic counters.

GetSpatialGridOccupancy(RealmId)

One realm's grid occupancy (Realms D6). An unregistered realm yields an all-zero snapshot.

public SpatialGridOccupancy GetSpatialGridOccupancy(RealmId realm)

Parameters

realm RealmId

Returns

SpatialGridOccupancy

GetSpatialTelemetry(int)

Reads one archetype's spatial-partitioning counters. Allocation-free; never throws.

public SpatialMigrationTelemetry GetSpatialTelemetry(int archetypeId)

Parameters

archetypeId int

The archetype's runtime id. Out of range, or an archetype with no cluster state, yields an all-zero snapshot.

Returns

SpatialMigrationTelemetry

A snapshot of the archetype's counters. See SpatialMigrationTelemetry for which members describe the last tick and which are cumulative.

Remarks

The snapshot is taken field by field without a lock, so a read racing the tick fence can mix values from either side of it. That is deliberate: these are diagnostic counters, and serializing a reader against the fence would cost more than the inconsistency is worth. Call it from the tick loop — after the fence, before the next tick — for a coherent view.

GetSpatialTelemetryTotal()

Sums GetSpatialTelemetry(int) across every archetype in this engine. Allocation-free; never throws.

public SpatialMigrationTelemetry GetSpatialTelemetryTotal()

Returns

SpatialMigrationTelemetry

An engine-wide snapshot. MigrationExecuteMs is a sum across archetypes, not a wall-clock duration — parallel fence workers overlap, so it can exceed the tick's elapsed time.

GetSpatialTelemetry<TArch>()

Reads one archetype's spatial-partitioning counters, naming the archetype by type instead of by id. Allocation-free; never throws.

public SpatialMigrationTelemetry GetSpatialTelemetry<TArch>() where TArch : Archetype<TArch>

Returns

SpatialMigrationTelemetry

A snapshot of the archetype's counters, or an all-zero snapshot when it has no cluster state in this engine.

Type Parameters

TArch

The archetype whose counters to read.

Remarks

The same reader as GetSpatialTelemetry(int) — see its remarks for when the snapshot is coherent. This overload exists because the id is a catalog detail: a caller that has the archetype type should not have to go looking for its number.

GetWalTotalBytes()

Total byte size of the write-ahead log across all segment files (0 when no WAL is active).

public long GetWalTotalBytes()

Returns

long

InitializeArchetypes()

Initialize ECS archetype storage. For each registered archetype, allocates a per-archetype RawValueHashMap and connects component slots to their ComponentTables. Must be called after all components are registered.

public void InitializeArchetypes()

MarkClusterSlotDirty(int, int, int)

Mark a single entity slot as dirty in the cluster dirty bitmap. Call from game systems that use the direct cluster iteration path (GetSpan<T>(Comp<T>)) and need migration detection or WAL tracking. The chunkId comes from ChunkId. Thread-safe (uses Or(ref int, int) internally via SetDirty).

public void MarkClusterSlotDirty(int archetypeId, int chunkId, int slotIndex)

Parameters

archetypeId int
chunkId int
slotIndex int

Open(string, Action<TyphonOptions>, ILoggerFactory)

Opens (or creates) a database backed by a self-contained service container — the standalone counterpart to the DI AddTyphon(IServiceCollection, Action<TyphonOptions>) path. The returned engine owns the container and disposes it as part of its own disposal, so using var dbe = DatabaseEngine.Open(...) is leak-free.

public static DatabaseEngine Open(string databaseFile, Action<TyphonOptions> configure = null, ILoggerFactory loggerFactory = null)

Parameters

databaseFile string

The database path; the canonical extension is .typhon. See DatabaseFile(string).

configure Action<TyphonOptions>

Optional further configuration — component/archetype registrations and advanced options.

loggerFactory ILoggerFactory

Optional logger factory. When null, logging resolves to a no-op (no output) while still satisfying the engine's required loggers.

Returns

DatabaseEngine

A ready-to-use engine that owns and disposes its private container.

ReadMetrics(IMetricWriter)

Writes current metric values to the provided writer.

public void ReadMetrics(IMetricWriter writer)

Parameters

writer IMetricWriter

The writer to report metrics to. Call one or more Write* methods.

Remarks

Called during snapshot collection (every 1-5 seconds). Implementations should:

  • Read fields and write to writer quickly (target < 100ns)
  • Not allocate (no new objects, no LINQ, no string formatting)
  • Not acquire locks (read fields directly, accept slight inconsistency)
  • Not call other IMetricSource.ReadMetrics() — the graph handles recursion

Only report metric kinds that are relevant to this resource. For example, a TransactionPool might only report Capacity and Throughput, not Memory or DiskIO.

RegisterArchetype(Type)

Finalizes an [Archetype] into the process-global catalog (feature #514 Phase 5). Called by the per-assembly generated [ModuleInitializer] for every archetype at assembly load — the barrier that replaces the manual Archetype<T>.Touch() startup calls. Runs the archetype's static ctor (declaring its Comp<T> components) then finalizes it and its parent chain. Idempotent — an already-finalized archetype is a no-op. Public because the generated registrar lives in the consumer's own assembly and cannot reach engine internals.

public static void RegisterArchetype(Type archetypeType)

Parameters

archetypeType Type

The [Archetype] class type to finalize.

RegisterByteMigration(string, int, int, int, int, ByteMigrationFunc)

Registers a byte-level migration function for scenarios where the old struct type is no longer available in code. Must be called before RegisterComponentFromAccessor<T>(ChangeSet, SchemaValidationMode) / RegisterComponentByType(Type, ChangeSet, SchemaValidationMode) for the target component.

public void RegisterByteMigration(string componentName, int fromRevision, int toRevision, int oldSize, int newSize, ByteMigrationFunc func)

Parameters

componentName string
fromRevision int
toRevision int
oldSize int
newSize int
func ByteMigrationFunc

RegisterComponentByType(Type, ChangeSet, SchemaValidationMode)

Non-generic entry point for registering a component when the type is only known at runtime — e.g. types discovered via reflection from a plugin or user-supplied schema DLL, as the Workbench does when loading *.schema.dll into a collectible AssemblyLoadContext.

public bool RegisterComponentByType(Type componentType, ChangeSet changeSet = null, SchemaValidationMode schemaValidation = SchemaValidationMode.Enforce)

Parameters

componentType Type

A closed unmanaged value type tagged with [Component]. The unmanaged constraint from the generic overload is verified at runtime by the CLR when the method is specialized; non-blittable or reference-type inputs will throw from deep inside MakeGenericMethod(params Type[]).

changeSet ChangeSet

Optional transactional change set. See RegisterComponentFromAccessor<T>(ChangeSet, SchemaValidationMode).

schemaValidation SchemaValidationMode

Schema validation policy (default: Enforce).

Returns

bool

Forwarded from RegisterComponentFromAccessor<T>(ChangeSet, SchemaValidationMode) — true on success.

Remarks

Internally invokes the generic RegisterComponentFromAccessor<T>(ChangeSet, SchemaValidationMode) via MakeGenericMethod(params Type[]). Any TargetInvocationException raised by reflection is unwrapped with ExceptionDispatchInfo so callers observe the real underlying exception — e.g. SchemaValidationException, SchemaDowngradeException, SchemaMigrationException — with its original stack trace preserved.

Exceptions

ArgumentNullException

componentType is null.

ArgumentException

componentType is not a closed value type.

See Also

RegisterComponentCollectionFactory<T>()

Registers the AOT-safe backing-store factory for a ComponentCollection<T> element type. Called by source-generated component schema providers (feature #514) for each collection field — the element type is a compile-time generic argument, so no MakeGenericType/Activator is needed. Idempotent.

public static void RegisterComponentCollectionFactory<T>() where T : unmanaged

Type Parameters

T

The collection's element type.

RegisterComponentFromAccessor<T>(ChangeSet, SchemaValidationMode)

Registers component type T with the engine: builds its schema definition from the component accessor, validates it against any persisted schema for the same component name, and creates or loads the backing ComponentTable.

public bool RegisterComponentFromAccessor<T>(ChangeSet changeSet = null, SchemaValidationMode schemaValidation = SchemaValidationMode.Enforce) where T : unmanaged

Parameters

changeSet ChangeSet

Optional change set to enlist the registration writes in.

schemaValidation SchemaValidationMode

How a persisted schema is reconciled with the runtime type; default Enforce.

Returns

bool

true on success; false when the component definition could not be built.

Type Parameters

T

A closed unmanaged value type tagged with [Component].

Remarks

When a persisted schema exists for this component, schemaValidation governs how differences are reconciled. A Transient component may not declare a ComponentCollection field — that combination is rejected at registration.

Exceptions

InvalidOperationException

A Transient component declares a ComponentCollection field.

RegisterMigration<TOld, TNew>(MigrationFunc<TOld, TNew>)

Registers a strongly-typed migration function that transforms component data from TOld to TNew. Both types must have [Component] attributes with the same Name but different Revisions. Must be called before RegisterComponentFromAccessor<T>(ChangeSet, SchemaValidationMode) / RegisterComponentByType(Type, ChangeSet, SchemaValidationMode) for the target component.

public void RegisterMigration<TOld, TNew>(MigrationFunc<TOld, TNew> func) where TOld : unmanaged where TNew : unmanaged

Parameters

func MigrationFunc<TOld, TNew>

Type Parameters

TOld
TNew

ResetPeaks()

Resets all high-water mark metrics (PeakBytes, MaxWaitUs, MaxUs, etc.) to current values.

public void ResetPeaks()

Remarks

Called by ResetAllPeaks() to enable windowed peak measurements. After displaying or exporting peak values, this method resets them so subsequent peaks represent new maxima since the last reset.

If this node has no high-water marks, implement as an empty method. Plain writes are sufficient — snapshots are taken every 1-5 seconds, so eventual visibility is acceptable.

RunStorageIntegrityCheck()

Audits storage-level invariants and returns every violation found. Pure read-only — touches the occupancy bitmap, the segment registry, and each segment's forward header chain; no data-page mutation, no allocation beyond a small issue list. Safe to call at any time on a live engine.

public StorageIntegrityReport RunStorageIntegrityCheck()

Returns

StorageIntegrityReport

Remarks

Two classes of check, each independent:

  • Popcount canary — the count of set bits in the occupancy bitmap must equal the sum of every registered segment's Pages.Length, plus each segment's directory-map extension pages (the pages outside the root that hold the page-index list when the segment owns more than 500 data pages — they are bit-set but not part of Pages), plus each directory page's CK-05 twin (the alternate slot, bit-set but in no segment list), plus the reserved root pages (0..InitialReservedPageCount-1), plus the occupancy-reserve pages (data, map-extension, and the map-extension twin) held by the page-allocator machinery. Any orphan (bit set, no claimant) or phantom (claimant, bit clear) is reported as a hard durability/structural bug.
  • Chunk-segment capacity — for every Typhon.Engine.Internals.ChunkBasedSegment<TStore>, AllocatedChunkCount + FreeChunkCount must equal ChunkCapacity. Desync indicates the segment's allocated count drifted from its on-page chunk bitmaps.
  • Cluster MVCC visibility summary — every cluster's ClusterMaxBorn / ClusterMaxDied pair, recomputed from the archetype's EntityMap and compared against the maintained one. A summary that claims more visibility than its entities justify makes the SoA scan skip its per-entity probe and emit a phantom; see ClusterVisibilitySummaryUnsound.

SetClusterDormancy<TArch>(int, int)

Enables cluster dormancy for TArch: a cluster with no entity written for thresholdTicks consecutive ticks is put to sleep, and a sleeping cluster is skipped by system dispatch and by the replication projection pass.

public void SetClusterDormancy<TArch>(int thresholdTicks, int heartbeatTicks = 0) where TArch : Archetype<TArch>

Parameters

thresholdTicks int

Consecutive clean ticks before sleeping. 0 disables dormancy, which is the default.

heartbeatTicks int

When > 0, a sleeping cluster is woken on a staggered schedule every this many ticks. 0 for none.

Type Parameters

TArch

The cluster archetype.

Remarks

The mechanism itself is older than this entry point (issue #233) and had no public way to turn it on, which is the likeliest reason nothing used it. What it buys is the same thing Unreal's net dormancy buys: work proportional to what MOVES rather than to what EXISTS, on every consumer that honours it.

The precondition, and it is sharp. Sleep is decided from the per-entity dirty bitmap, and WriteSpatial<T>(Comp<T>, int, in T) deliberately raises no dirty bit — so an archetype whose entities only MOVE looks clean, is put to sleep while still in motion, stops being dispatched, and freezes. An application that combines the two must mark the column itself (MarkDirty<T>(Comp<T>)) on a cluster it moved. That is a contract of WriteSpatial, not of dormancy.

Cost when unused: nothing measurable. Every consumer gates on SleepingClusterCount > 0, which stays zero while the threshold is zero, so the check folds to one predictable-not-taken branch.

Exceptions

InvalidOperationException

The archetype is not registered, or is not a cluster archetype.

SetSpatialBarrierOnly<TArch>(bool)

Declare that TArch uses ClusterRef.WriteSpatial as the exclusive writer of its spatial component. Skips the fence-time legacy scan for this archetype — both DetectClusterMigrations's dirtyBits sweep and RecomputeDirtyClusterAabbs's ActiveClusterIds iteration are replaced with sparse iteration over Typhon.Engine.Internals.ArchetypeClusterState.ClusterProcessBitmap.

Only set when EVERY spatial-field write on this archetype goes through WriteSpatial. Mutations via raw GetSpan or OpenMut + Write will be invisible to the engine's spatial maintenance after this is enabled. The TYPHON009 analyzer flags non-WriteSpatial mutation sites — once those are zero, it's safe to opt in.

public void SetSpatialBarrierOnly<TArch>(bool value = true) where TArch : Archetype<TArch>

Parameters

value bool

Type Parameters

TArch

TryReadPageBody(int, Span<byte>)

Copies the raw page (8 KiB — PageBaseHeader + metadata region + chunk data) of file page filePageIndex into dest. The read goes through the page cache via the no-clock-sweep path (RequestPageEpochNoSweep): it does not bump the eviction heuristic and does not satisfy the page's pending CRC verification — the caller recomputes the CRC itself. Returns false on an out-of-range index or a lost eviction race; true with dest filled otherwise.

public bool TryReadPageBody(int filePageIndex, Span<byte> dest)

Parameters

filePageIndex int
dest Span<byte>

Returns

bool

WriteTickFence(long, ChangeSet)

Closes a tick: publishes the tick's pending work and establishes the crash-recovery boundary. Must be called once per tick.

public long WriteTickFence(long tickNumber, ChangeSet changeSet = null)

Parameters

tickNumber long

Monotonic tick identifier. Must increase between calls.

changeSet ChangeSet

ChangeSet for dirty-page tracking across the whole fence. Under the runtime this is the per-tick UoW's shared ChangeSet (see Typhon.Engine.UnitOfWork.ChangeSet), which consolidates the fence's dirty pages with the rest of the tick. Pass null when driving the fence from a host without a per-tick UoW — this method then creates and commits a one-shot ChangeSet itself, which is correct but yields per-fence rather than per-tick consolidation.

Returns

long

Highest LSN written, or 0 if nothing was serialized.

Remarks

Four responsibilities, not one:

  • serializes dirty SingleVersion component data to the WAL — one TickFence chunk per SV ComponentTable;
  • executes the tick's pending cluster migrations (entities that crossed a spatial cell boundary);
  • recomputes cluster spatial AABBs and the per-cell spatial index;
  • drains zone maps and dirty-bit deltas.

Under TyphonRuntime this is automatic — the runtime calls it from its tick-end callback and application code does not. A host that does NOT use the runtime must call it itself, once per tick, after that tick's writes have been committed and before the next tick begins. See the embedding guide (doc/guide/embedding-without-the-runtime.md).

What happens if it is never called. Nothing throws and nothing is corrupted — the database silently stops maintaining itself: SingleVersion components degrade to Transient-like durability (no crash recovery), queued cluster migrations never execute, and spatial AABBs and zone maps go stale, so spatial queries return results computed against old positions, and a cluster a destroy empties is never reclaimed — its chunk stays allocated until the database is closed or reopened (#582, CLUSTERWALK-03).

Threading — a caller obligation, not an enforced one (rule EW-01). No other thread may mutate the database while this runs: the fence rewrites cluster B+Trees, the EntityMap, the per-cell spatial index and the cluster-to-cell map, and a concurrent writer corrupts them silently. Under TyphonRuntime it holds by construction — the fence runs from the scheduler's tick-end callback, after every system has completed, and each system's Transaction is committed and disposed in its own epilogue. A host driving the fence itself owns the same guarantee; trivially satisfied single-threaded, unchecked otherwise.

The one trap inside the runtime is a side transaction. TickContext.CreateSideTransaction hands back an ORDINARY transaction, so it can write an indexed field and mutate a B+Tree, and the caller owns its Commit and Dispose — nothing joins it to the tick. A side transaction held past the end of the system that created it, and committed while the fence runs, is exactly the concurrent mutation this contract forbids. Commit and dispose it before the system returns.

Checkpointing is separate and automatic (timer, dirty-page threshold, back-pressure, graceful shutdown). ForceCheckpoint() is not part of the tick loop.

Events

OnMigrationProgress

Raised during schema migration to report progress to subscribers.

public event EventHandler<MigrationProgressEventArgs> OnMigrationProgress

Event Type

EventHandler<MigrationProgressEventArgs>

StorageHealthChanged

Raised on every change of StorageHealth, on the engine thread that saw it — an allocation's, or the checkpoint's. A handler must return quickly and must not block or call back into the engine; one that throws is ignored.

public event EventHandler<StorageHealthChangedEventArgs> StorageHealthChanged

Event Type

EventHandler<StorageHealthChangedEventArgs>