Struct SpatialMigrationTelemetry
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
ClustersScanned
Clusters examined by the intra-cell drifter scan during the most recently completed tick.
public int ClustersScanned { get; }
Property Value
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
RelocationsAdmitted
Intra-cell relocations the throttle admitted last tick. Wave-2 K6.
public int RelocationsAdmitted { get; init; }
Property Value
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
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
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
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
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
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
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
RepairUnitsRefused
Repair units whose projected cost exceeded the remaining budget, and which were therefore never begun.
public int RepairUnitsRefused { get; }
Property Value
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
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
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
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
TotalMigrations
Migrations executed since this archetype's cluster state was created. Cumulative; differentiate over time for a rate.
public long TotalMigrations { get; }