Table of Contents

Struct SpatialMigrationTelemetry

Namespace
Typhon.Engine
Assembly
Typhon.Engine.dll

A snapshot of one archetype's spatial-partitioning counters, or of the whole engine's when obtained from GetSpatialTelemetryTotal(). Obtained from GetSpatialTelemetry(int).

public readonly struct SpatialMigrationTelemetry
Inherited Members

Remarks

Two clocks, deliberately. The ...Count / ...Ms members describe the most recently completed tick and are reset at the top of every tick fence; the Total... members only grow. A poll-based consumer (an OTel scrape every few seconds) that reads a per-tick value samples one arbitrary tick out of hundreds — use the cumulative members and differentiate for a rate; use the per-tick members from inside a tick loop, where "this tick" is exactly what you meant.

Zero means zero, never "unknown". An archetype with no cluster state, an out-of-range id and a tick in which nothing happened all report zero. ClustersScanned, DriftersDetected and DriftAbsorbedCount gained their producers in step 10 of claude/design/Spatial/vdb-cell-grid-and-migration.md; ReclusterBudgetUsedMs, RepairedEntityCount, RepairUnitCount and RepairUnitsRefused in step 12.

Cumulative members restart with the archetype's cluster state. InitializeArchetypes() reallocates the per-archetype state array, so a repeat call — rare, but explicitly tolerated — returns the totals to zero. They measure the life of the cluster state, not of the process.

Reading is allocation-free and lock-free: every member is a plain field read of live engine state, torn only across a fence boundary.

Properties

ActiveClusterCount

Clusters currently live. The denominator for every ratio above — a migration count means nothing without the population it came from.

public int ActiveClusterCount { get; }

Property Value

int

ClustersScanned

Clusters examined by the intra-cell drifter scan during the most recently completed tick.

public int ClustersScanned { get; }

Property Value

int

Remarks

Clusters that were WRITTEN this tick, not clusters that exist — a settled world scans nothing, which is the cheap half of the design's promise and the denominator that makes DriftersDetected mean anything.

CrossingsQueued

Cell-crossing requests the throttle found queued and charged last tick. Wave-2 K6.

public int CrossingsQueued { get; init; }

Property Value

int

DriftAbsorbedCount

Entities outside their cluster's target region by less than the intra-cell drift margin during the most recently completed tick, and therefore left alone.

public int DriftAbsorbedCount { get; }

Property Value

int

Remarks

Deliberately not folded into HysteresisAbsorbedCount. That counter is about cell-boundary oscillation and tunes MigrationHysteresisRatio; this one is about intra-cell drift and tunes ClusterDriftMarginRatio. They answer different questions and their margins move independently, so a single number would tune neither — which is the whole reason step 10 added a second counter rather than reusing the first.

Read as a fraction of DriftAbsorbedCount + DriftersDetected: near zero means the margin is too narrow to damp anything, near one means it is wide enough to be suppressing repairs the step exists to make.

DriftGatedClusters

Clusters that passed the intra-cell drift gate last tick — the population detection walked. Wave-2 K1.

public int DriftGatedClusters { get; init; }

Property Value

int

DriftSuppressedByDensity

Clusters that exceeded the configured floor (ClusterTargetExtentRatio) but not their cell's density-derived target, so the drift scan never ran on them — the work step 14's target function removed. A world in the 16–64 entities/cell basin reports every written cluster here and zero in DriftGatedClusters.

public int DriftSuppressedByDensity { get; init; }

Property Value

int

DriftersDetected

Entities found outside their cluster's target region during the most recently completed tick — candidates for intra-cell relocation.

public int DriftersDetected { get; }

Property Value

int

Remarks

Counts DETECTION, not outcome. An entity is counted here the moment the target-region rule rejects it, whether or not placement then found a better cluster to put it in — a cell whose every other cluster is full produces drifters and no migrations, and that gap is the signal you want, not noise to be suppressed. Read against MigrationCount to see it.

DriftersSpilled

Drifters whose cell had candidates but no free slot left this pass, filed for a fresh cluster at drain time rather than left in place (step 14).

public int DriftersSpilled { get; init; }

Property Value

int

DriftersUnplaced

Drifters that were detected but for which placement found no better cluster, during the most recently completed tick.

public int DriftersUnplaced { get; init; }

Property Value

int

Remarks

The gap DriftersDetected's own remarks point at, now counted rather than inferred: a cell whose every cluster is equally bad produces drifters and no migrations. Together the identity DriftersDetected = admitted + RelocationsThrottled + RelocationsSuperseded + DriftersUnplaced holds over a tick, which is what makes "a drifter is never both absorbed and throttled" checkable rather than merely asserted (AC-11.7).

DriftersUnplacedNoCandidate

Of DriftersUnplaced, those whose cell offered no candidate cluster at all. Wave-2 K2.

public int DriftersUnplacedNoCandidate { get; init; }

Property Value

int

HysteresisAbsorbedCount

Cell-boundary crossings that did not produce a migration during the most recently completed tick, because the entity was still inside the hysteresis margin. Read against MigrationCount: a high ratio means the margin is doing its job, a near-zero one means it is too narrow to absorb oscillation around the boundary.

public int HysteresisAbsorbedCount { get; }

Property Value

int

Remarks

The unit differs by write path. An archetype using the spatial write barrier (SetSpatialBarrierOnly) counts one per absorbed write, so an entity parked in the margin and written twice in a tick contributes two; every other archetype counts one per slot per tick, because its producer is a once-per-tick scan. The two agree on the overwhelmingly common workload of one spatial write per entity per tick, and diverge above that. Treat this as a rate signal for tuning, not an exact entity count.

MeasuredNsPerEntity

The measured per-entity migration cost, in nanoseconds, that this tick's budget was actually spent against (#872 step 11).

public double MeasuredNsPerEntity { get; init; }

Property Value

double

Remarks

An EWMA over MigrationTotalMs per migrant plus the repair planner's own measured cost, seeded from SpatialGridConfig.RepairNsPerEntity and clamped to a band around it. It replaces that constant as the operative number: AC-12.7 measured the real cost at 22x to 117x the design's estimate, and a budget calibrated on the wrong constant admits units costing many times what it thinks they do.

Blended across migration kinds. Cell crossings, intra-cell relocations and repair moves all contribute; splitting them needs per-class attribution inside the apply phases, where the staged records carry no kind. A blended measurement is nonetheless strictly better than a constant that cannot track the machine at all.

MigrationCount

Entities moved to a different cluster because they crossed a spatial cell boundary, during the most recently completed tick.

public int MigrationCount { get; }

Property Value

int

MigrationExecuteMs

Wall-clock milliseconds spent executing migrations during the most recently completed tick, summed across every worker that took a slice.

public double MigrationExecuteMs { get; }

Property Value

double

MigrationTotalMs

The whole cost of the most recently completed tick's migrations in milliseconds — the migrant loop plus the bulk index descent and the bulk EntityMap patch, summed across every worker.

public double MigrationTotalMs { get; init; }

Property Value

double

Remarks

Read this, not MigrationExecuteMs, for cost per entity. That one brackets the migrant loop alone, and since #872 step 6 the loop merely STAGES the index update — the descent that applies it happens in a later phase. The secondary index was measured at ~48 % of a migration's cost, so the older field under-reports by roughly half. Step 11 added the two missing timers to produce this one, and it is what the adaptive budget divides by MigrationCount.

CPU-milliseconds, not span. W workers each busy for 1 ms report 4, not 1.

PinsRejected

Pinned claims rejected at drain time and executed as first fit instead. Wave-2 K5.

public int PinsRejected { get; init; }

Property Value

int

PrepDetectMs

Prep's internal split for the most recently completed tick, in milliseconds of wall time, in phase order: snapshot, occupancy mask, index replay, min/max refresh, crossing detection, budget, repair plan, pre-size.

public double PrepDetectMs { get; init; }

Property Value

double

Remarks

Added because the design that proposes optimising Prep could not say which of its steps cost anything. The phase-level spans say Prep is 52 % of the fence; they do not say whether that is the occupancy mask, the min/max rescan or the decisions at the tail. Ranking the steps from what each one touches rather than from what each one costs is the same mistake one level down that the phase spans were added to prevent one level up.

PrepDirtyClusters

Clusters whose change word survived the occupancy mask — the size of the domain a sliced Prep would partition.

public int PrepDirtyClusters { get; init; }

Property Value

int

PrepMaskMs

Prep's internal split for the most recently completed tick, in milliseconds of wall time, in phase order: snapshot, occupancy mask, index replay, min/max refresh, crossing detection, budget, repair plan, pre-size.

public double PrepMaskMs { get; init; }

Property Value

double

Remarks

Added because the design that proposes optimising Prep could not say which of its steps cost anything. The phase-level spans say Prep is 52 % of the fence; they do not say whether that is the occupancy mask, the min/max rescan or the decisions at the tail. Ranking the steps from what each one touches rather than from what each one costs is the same mistake one level down that the phase spans were added to prevent one level up.

PrepPlanMs

Prep's internal split for the most recently completed tick, in milliseconds of wall time, in phase order: snapshot, occupancy mask, index replay, min/max refresh, crossing detection, budget, repair plan, pre-size.

public double PrepPlanMs { get; init; }

Property Value

double

Remarks

Added because the design that proposes optimising Prep could not say which of its steps cost anything. The phase-level spans say Prep is 52 % of the fence; they do not say whether that is the occupancy mask, the min/max rescan or the decisions at the tail. Ranking the steps from what each one touches rather than from what each one costs is the same mistake one level down that the phase spans were added to prevent one level up.

PrepPreSizeMs

Prep's internal split for the most recently completed tick, in milliseconds of wall time, in phase order: snapshot, occupancy mask, index replay, min/max refresh, crossing detection, budget, repair plan, pre-size.

public double PrepPreSizeMs { get; init; }

Property Value

double

Remarks

Added because the design that proposes optimising Prep could not say which of its steps cost anything. The phase-level spans say Prep is 52 % of the fence; they do not say whether that is the occupancy mask, the min/max rescan or the decisions at the tail. Ranking the steps from what each one touches rather than from what each one costs is the same mistake one level down that the phase spans were added to prevent one level up.

PrepShadowMs

Prep's internal split for the most recently completed tick, in milliseconds of wall time, in phase order: snapshot, occupancy mask, index replay, min/max refresh, crossing detection, budget, repair plan, pre-size.

public double PrepShadowMs { get; init; }

Property Value

double

Remarks

Added because the design that proposes optimising Prep could not say which of its steps cost anything. The phase-level spans say Prep is 52 % of the fence; they do not say whether that is the occupancy mask, the min/max rescan or the decisions at the tail. Ranking the steps from what each one touches rather than from what each one costs is the same mistake one level down that the phase spans were added to prevent one level up.

PrepSnapshotMs

Prep's internal split for the most recently completed tick, in milliseconds of wall time, in phase order: snapshot, occupancy mask, index replay, min/max refresh, crossing detection, budget, repair plan, pre-size.

public double PrepSnapshotMs { get; init; }

Property Value

double

Remarks

Added because the design that proposes optimising Prep could not say which of its steps cost anything. The phase-level spans say Prep is 52 % of the fence; they do not say whether that is the occupancy mask, the min/max rescan or the decisions at the tail. Ranking the steps from what each one touches rather than from what each one costs is the same mistake one level down that the phase spans were added to prevent one level up.

PrepThrottleMs

Prep's internal split for the most recently completed tick, in milliseconds of wall time, in phase order: snapshot, occupancy mask, index replay, min/max refresh, crossing detection, budget, repair plan, pre-size.

public double PrepThrottleMs { get; init; }

Property Value

double

Remarks

Added because the design that proposes optimising Prep could not say which of its steps cost anything. The phase-level spans say Prep is 52 % of the fence; they do not say whether that is the occupancy mask, the min/max rescan or the decisions at the tail. Ranking the steps from what each one touches rather than from what each one costs is the same mistake one level down that the phase spans were added to prevent one level up.

PrepZoneMapMs

Prep's internal split for the most recently completed tick, in milliseconds of wall time, in phase order: snapshot, occupancy mask, index replay, min/max refresh, crossing detection, budget, repair plan, pre-size.

public double PrepZoneMapMs { get; init; }

Property Value

double

Remarks

Added because the design that proposes optimising Prep could not say which of its steps cost anything. The phase-level spans say Prep is 52 % of the fence; they do not say whether that is the occupancy mask, the min/max rescan or the decisions at the tail. Ranking the steps from what each one touches rather than from what each one costs is the same mistake one level down that the phase spans were added to prevent one level up.

ReclusterBudgetUsedMs

Milliseconds of the per-tick re-clustering budget the repair path committed during the most recently completed tick.

public double ReclusterBudgetUsedMs { get; }

Property Value

double

Remarks

Projected, not measured, and the difference is the design. A repair unit is admitted only if the remaining budget covers its whole cost, so the estimate has to exist before the work does; reporting the elapsed time instead would report a number that gated nothing. The projection is entities x SpatialGridConfig.RepairNsPerEntity. Compare it against a measured tick time to find out whether that constant is honest — which is exactly what step 11's adaptive budget will do automatically.

RelocationSpendNs

Budget charged to admitted relocations last tick, in nanoseconds. Wave-2 K9.

public double RelocationSpendNs { get; init; }

Property Value

double

RelocationsAdmitted

Intra-cell relocations the throttle admitted last tick. Wave-2 K6.

public int RelocationsAdmitted { get; init; }

Property Value

int

RelocationsSuperseded

Intra-cell relocations dropped because a cell crossing already claimed the same entity during the most recently completed tick (#877).

public int RelocationsSuperseded { get; init; }

Property Value

int

Remarks

Not a budget signal, and that is why it is not folded into RelocationsThrottled. Drift detection runs in AabbRefresh, so its relocations are decided by the next tick's Prep — by which time the crossing detector may have filed a CellCrossing for the same entity. An entity that drifted to the edge of its cell is precisely the one most likely to leave it, so the overlap is the common case on a moving world, not a corner. The relocation is dropped because the crossing supersedes it: the entity is migrating regardless, to a cell the relocation's destination was never chosen for.

Before #877 both requests were executed. ExecuteMigrations' stale-source guard is an occupancy test rather than an identity test, so it only covered the case where the freed slot was still empty when the second request drained; when an unrelated migrant had claimed it, the relocation moved THAT entity to a destination chosen for someone else. A high reading here is normal and healthy. A high reading that starts tracking RelocationsThrottled is worth a look, because it means drift and crossings are competing for the same entities.

RelocationsThrottled

Intra-cell relocations the re-clustering budget refused during the most recently completed tick, and therefore dropped (#872 step 11).

public int RelocationsThrottled { get; init; }

Property Value

int

Remarks

Dropped, not deferred, and that is deliberate. A relocation's chosen destination was the least-enlargement cluster as of the AABBs of the tick that detected it; carrying it forward would apply a stale choice against bounds this tick's own migrations have moved. It is re-detected next tick from current data, so what this counts is deferred WORK, not lost work.

Read against DriftersDetected: a persistently high ratio means the budget is below the world's drift rate, and equilibrium tightness — not correctness — is what degrades. That is §5.6's stated failure mode, by design.

RepairBudgetStarvedNs

RelocationSpendNs on a tick where the repair planner refused at least one unit; zero otherwise. Budget spent on relocations while a repair was turned away. Wave-2 K9.

public double RepairBudgetStarvedNs { get; init; }

Property Value

double

RepairQueueDepth

Cells currently waiting in the repair priority queue. Unlike every other per-tick member this is a LEVEL, not a rate: it persists.

public int RepairQueueDepth { get; init; }

Property Value

int

RepairQueueEvicted

Candidates evicted from the repair queue since this cluster state was created, because the queue was at SpatialGridConfig.RepairQueueMaxCells. Cumulative.

public long RepairQueueEvicted { get; init; }

Property Value

long

Remarks

Read against RepairQueueDepth: a growing count while the depth sits at the cap says the cap is below what the world actually degrades, and cells are being forgotten. Zero at a depth below the cap is the healthy reading.

RepairQueueMaintenanceMs

Milliseconds spent maintaining the repair queue — absorbing nominations and re-ranking — during the most recently completed tick.

public double RepairQueueMaintenanceMs { get; init; }

Property Value

double

Remarks

AC-11.5's numerator: "a queue that costs more to maintain than the work it schedules is a net loss". Read as a fraction of ReclusterBudgetUsedMs. Re-ranking is lazy — triggered by new nominations or by a SpatialGrid.TierVersion change, not by the tick — so a settled world should report near zero here.

RepairUnitCount

Repair units admitted during the most recently completed tick. A unit is one cell's N worst clusters, or one whole cell.

public int RepairUnitCount { get; }

Property Value

int

RepairUnitsRefused

Repair units whose projected cost exceeded the remaining budget, and which were therefore never begun.

public int RepairUnitsRefused { get; }

Property Value

int

Remarks

A Morton sort cannot be halved — a partly re-sorted cell has paid the cost and banked only part of the benefit — so the budget admits whole units and refuses the rest outright. A persistently non-zero reading against a zero RepairUnitCount means the budget is below the cost of the smallest unit on offer and no repair can ever happen; raise ReclusterBudgetMs, or lower RepairWorstClustersPerUnit so a unit is smaller.

RepairValveFires

Repair units admitted by the safety valve during the most recently completed tick — begun despite insufficient remaining budget because the cell's degradation had reached SpatialGridConfig.ClusterRepairCriticalExtentRatio.

public int RepairValveFires { get; init; }

Property Value

int

Remarks

The only budget overshoot the engine permits, and it is bounded: the valve caps its unit at RepairWorstClustersPerUnit clusters and fires at most once per tick per archetype. A persistently non-zero reading means degradation is outrunning the budget — the condition §5.6's valve exists to bound rather than to hide, so raise ReclusterBudgetMs rather than treating the valve as the steady state.

RepairedEntityCount

Entities re-packed by the repair path — the full Morton re-sort — during the most recently completed tick.

public int RepairedEntityCount { get; }

Property Value

int

Remarks

Disjoint from MigrationCount in intent though not in mechanism: a repair emits ordinary migration requests, so the entities counted here are also counted there when the requests execute. This is the count the PLANNER committed to; that one is what the Migrate phase actually moved, and the two differ by the requests whose source slot had emptied in between.

SlotsScanned

Entity slots the AABB refresh pass actually read during the most recently completed tick.

public int SlotsScanned { get; init; }

Property Value

int

Remarks

The refresh's own cost, in the only unit that scales with the world: ClustersScanned is incremented after the pass has decided a cluster had something to say, so it cannot distinguish a pass that opened ten clusters from one that opened two thousand.

What a healthy value looks like. Roughly the occupied-slot count of the clusters that were WRITTEN this tick. If it tracks the whole population instead — 63 000 on a 64 000-entity world where 640 entities moved — the pass has lost its dirty gate, which is exactly the regression this counter was added to make visible.

TotalHysteresisAbsorbed

Cell-boundary crossings absorbed by the hysteresis margin since this archetype's cluster state was created. Cumulative twin of HysteresisAbsorbedCount, and subject to the same per-write-path unit caveat.

public long TotalHysteresisAbsorbed { get; }

Property Value

long

TotalMigrations

Migrations executed since this archetype's cluster state was created. Cumulative; differentiate over time for a rate.

public long TotalMigrations { get; }

Property Value

long