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
ArrivalCellsTouched
Distinct destination cells receiving at least one cell crossing in this tick's drain (#910 T0). A rate.
public int ArrivalCellsTouched { get; init; }
Property Value
CellTreeDemotions
Cell halves that fell back from a per-cell R-Tree to the linear scan during the most recently completed tick.
public int CellTreeDemotions { get; init; }
Property Value
Remarks
See CellTreePromotions — the two are read together.
CellTreePromotions
Cell halves promoted from the linear scan to a per-cell R-Tree during the most recently completed tick.
public int CellTreePromotions { get; init; }
Property Value
Remarks
Published because "does a cell half ever actually promote in a real workload?" was unanswerable without a debugger. Promotion needs a
half at or above SpatialOptions.CellTreePromoteThreshold clusters AND a mean extent at or below CellTreePromoteTightness, and the engine
measures 0.63-1.03 of the cell under motion — so a persistent zero here is the finding, not a broken counter.
Read against CellTreeDemotions: both non-zero and tracking each other means the promote/demote gap is too narrow and cells are thrashing between two O(clusters) rebuilds.
ClampedDestinations
Cell crossings whose position lay outside the grid — WorldMin to WorldMax, rounded out to whole cells — and were clamped into an
edge cell (#910 T0). The entity still lands there, as it always did; the engine used to absorb this silently. It counts crossings, not entities:
one already in an edge cell and written further out files nothing. A rate.
public int ClampedDestinations { get; init; }
Property Value
ClusterReach
How far past its own cell every cell-walking query reaches for this archetype's clusters, in world units: the largest in-world overhang of any cluster the queries do not visit by name (#911 O2; bounded 2026-09-13). The named ones are counted in EscapedClusterCount.
public float ClusterReach { get; init; }
Property Value
Remarks
A LEVEL, recomputed at every fence. It was a running maximum that never fell, and one transient outlier — an entity teleported and not yet migrated — then widened every later query of the archetype for the rest of the process. It now describes the boxes the coming tick's queries read, so it falls once the cluster that raised it is fixed. GetSpatialTelemetryTotal() takes the max across archetypes, never the sum: summing would widen every walk by bounds no single archetype has.
Only the part of a box inside the grid counts — an edge cell's outward side faces no cell, and every query range is clamped into the grid. Between fences only a spawn raises it.
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.
CrossingsExecuted
Cell-crossing migrations executed during the most recently completed tick.
public int CrossingsExecuted { get; init; }
Property Value
Remarks
This, RelocationsExecuted and RepairsExecuted sum EXACTLY to MigrationCount — the split is checkable rather than trusted, which is the property #911 gave the trace record and #912 brought to this surface.
Executed, not queued. CrossingsQueued and RelocationsAdmitted are decisions taken a phase earlier and a tick earlier respectively; these three are what the drain actually moved. The gap between a queued relocation and an executed one is RelocationsSuperseded plus the stale-source guard.
Always counted, unlike the trace record's split. A Migrate slice mixes all three kinds by construction, so attributing a per-entity cost to one of them needs this; and needing the profiler on to get it would perturb the bracket the cost is measured in.
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
DriftTargetBoost
The throttle's multiplier on the drift target (step 14, D2): 1 is none, and at its cap the target reaches the cell and relocation detection is off. It rises while relocations are throttled, the budget controller's floor included, and decays once they are not. A LEVEL. GetSpatialTelemetryTotal() takes the maximum across archetypes. Zero, below the 1 that means no boost, when there is no cluster state to read: an out-of-range id, or an engine that has none.
public float DriftTargetBoost { 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
EscapedClusterCount
Clusters whose in-world overhang exceeds ClusterReach, which every query therefore tests by name — at most 16; past that the reach widens to cover the rest. A level, recomputed at every fence; the total sums it.
public int EscapedClusterCount { get; init; }
Property Value
FenceBranchPath
Which branch the tick fence's Prep phase selected for this archetype: 0 no work, 1 the clean-bitmap spatial refresh (local occupancy
bits, no WAL), 2 the dirty-bitmap path (full snapshot plus WAL). A LEVEL describing the last fence, not a counter.
public byte FenceBranchPath { get; init; }
Property Value
Remarks
The one number that says why a tick's fence cost what it did: the same archetype alternating between 1 and 2 is the difference between a refresh and a full snapshot, and a steady 0 means the fence did nothing for it at all. GetSpatialTelemetryTotal() takes the maximum across archetypes, which reports the heaviest branch any of them ran.
FinalizeLockAcquisitions
Exclusive acquisitions of the archetype's finalize latch during the most recently completed tick.
public long FinalizeLockAcquisitions { get; init; }
Property Value
Remarks
The archetype-wide serialisation point, and the only one a fence worker can reach. Twenty-one call sites take it: the three bulk enqueues, the per-archetype array growths, the new-cluster slow paths and the cell-tree segment creation. A phase whose per-worker CPU rises with the worker count is serialised here or it is not serialised at all — so this staying flat while per-entity cost rises ELIMINATES the latch, which is a result worth as much as naming it.
A count, not a held time. Timing it would cost two timestamps per acquisition against critical sections frequently shorter than that, and would change what it measures. #912's method note says it: count operations, do not time a phase already known to be off.
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.
JumpCrossings
Cell crossings whose destination cell was not adjacent to the source cell — more than one cell away on some axis (#910 T0). A teleport, a respawn, a shuttle; ordinary motion crosses into a face, edge or corner neighbour and never counts here. A rate, counted when a crossing is filed (SO-01).
public int JumpCrossings { get; init; }
Property Value
LargestArrivalRun
The most cell crossings into any one destination cell in this tick's drain — the size of the largest arrival (#910 T0). A per-tick maximum, so GetSpatialTelemetryTotal() takes the maximum across archetypes rather than the sum.
public int LargestArrivalRun { get; init; }
Property Value
MeanClusterExtentRatio
Mean measured tightness over TightnessSampleCount clusters: each cluster's largest axis extent as a fraction of its cell's edge.
public double MeanClusterExtentRatio { get; init; }
Property Value
Remarks
Zero when TightnessSampleCount is zero. This is the quantity every table of the design's §5.8 reports.
MeanPackingBound
Mean packing bound over the same clusters: (slotsPerCluster / entitiesInCell)^(1/d) as a fraction of the cell edge — the tightest a full
cluster can be in that cell without fragmenting.
public double MeanPackingBound { get; init; }
Property Value
Remarks
Geometry, not tuning, and independent of ClusterTargetPackingSlack: it is published even in constant mode, where the gates ignore it. It is
1 for any cell holding no more entities than one cluster's slots — the 16-64 per-cell basin the density guidance recommends — which is why
intra-cell maintenance correctly switches itself off there and why a tightness of 0.9 in that basin is not a defect.
MeanTightnessToBound
MeanClusterExtentRatio divided by MeanPackingBound — measured extent against what geometry allows. The number the
spatial partitioning subsystem is judged on. 1 is optimal packing; the engine measured ~1.24 on a Morton-ordered bulk load and ~1.56 on a
random-order one.
public double MeanTightnessToBound { get; }
Property Value
Remarks
A ratio of means, not a mean of ratios. The two differ whenever the sampled clusters sit in cells of differing population, and the ratio of means is what the design's tables quote — a mean extent read against a bound. Both operands are published so a consumer that wants the other statistic can say so explicitly rather than inheriting one silently. Zero when TightnessSampleCount is zero.
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
MigrationEpilogueMs
Milliseconds the tick's Migrate slices spent after their last migrant — the three accessor disposals and the migration span's publish — summed across slices. Part of MigrationExecuteMs, not additional to it.
public double MigrationEpilogueMs { get; init; }
Property Value
Remarks
Grows with MigrationSliceCount rather than with MigrationCount, which is exactly what makes it worth publishing separately: it is the term a per-entity reading mis-attributes.
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
MigrationPrologueMs
Milliseconds the tick's Migrate slices spent before their first migrant — span construction and the three chunk-accessor rentals — summed across slices. Part of MigrationExecuteMs, not additional to it.
public double MigrationPrologueMs { get; init; }
Property Value
Remarks
Grows with MigrationSliceCount rather than with MigrationCount, which is exactly what makes it worth publishing separately: it is the term a per-entity reading mis-attributes.
MigrationSliceCount
ExecuteMigrations slices that ran during the most recently completed tick — the number of spans summed into MigrationExecuteMs.
public int MigrationSliceCount { get; init; }
Property Value
Remarks
Read it before dividing MigrationExecuteMs by MigrationCount. That quotient is a per-entity cost only while this is 1. The parallel fence sizes the Migrate phase's slices from the worker count, so raising W raises the number of spans summed into the numerator while the workload fixes the denominator, and every per-slice fixed cost inside the bracket — three chunk-accessor rentals and the span construction — is charged again to every entity in the tick.
This is what #912 was: 425 -> 844 -> 1 440 ns/entity at W = 2/4/8 was recorded as contention in the relocation drain for three steps,
and nothing published the quantity that would have told the two apart. The decomposition is
(Prologue + Epilogue) / MigrationCount for the per-slice term and the remainder for the per-entity one.
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.
PrepSortMs
Putting the drain prefix in destination-cell order (#910), between the repair plan and the pre-size — the sort the Migrate phase's Prepare ran before #910 moved it into Prep, where the serial fence runs it too.
public double PrepSortMs { get; init; }
Property Value
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.
QueryCandidates
Entities in the clusters those queries opened: every occupied slot, whether or not the query went on to test it. A query that stops at its first match still counts its whole cluster, so the count is the same whichever narrowphase the machine runs.
public long QueryCandidates { get; init; }
Property Value
QueryCandidatesPerHit
QueryCandidates over QueryHits: entities a match cost. 1 would be a partition whose every candidate matched.
What intra-cell maintenance buys, measured where it is spent.
public double QueryCandidatesPerHit { get; }
Property Value
Remarks
A ratio of the tick's sums, so a query with many matches weighs more than one with few, and GetSpatialTelemetryTotal() derives it from its own sums. Its floor depends on the query shape — a radius query's box holds more than its sphere — so compare it with itself over time, not across games.
Zero when nothing matched, including a tick whose queries opened clusters and matched nothing: below the floor of 1, so a consumer tracking the best value seen must work from the sums, not from this ratio.
QueryCandidatesPerHitBest
The lowest QueryCandidatesPerHitSmoothed since the budget controller last re-based it, or since its first signal: the controller's set point. A LEVEL. Zero without a signal.
public double QueryCandidatesPerHitBest { get; init; }
Property Value
Remarks
The granted share of the budget is (smoothed / best - 1) / QueryEfficiencyTolerance, clamped to (0, 1]. It is only ever lowered, except by a
re-base (TotalEfficiencyRebases). GetSpatialTelemetryTotal() gives the hits-weighted mean of the
archetypes' bests, the weighting its smoothed value carries.
QueryCandidatesPerHitSmoothed
The budget controller's input: candidates per hit over about the last twenty ticks, as smoothed candidates over smoothed hits. A LEVEL. Zero while the archetype's range queries hit too little to steer by.
public double QueryCandidatesPerHitSmoothed { get; init; }
Property Value
QueryClustersOpened
Clusters this archetype's range queries opened since the previous fence — the queries the systems of the tick this fence closes ran.
public long QueryClustersOpened { get; init; }
Property Value
Remarks
Range queries only: AABB and radius queries through MoveNext, Count or Fill, one at a time or batched
(CountRadius, ForEachInRadius) — the game's, and the engine's own interest and trigger systems'. A batch counts what its members'
own queries would have: a cluster it opens once for five members counts five. Nearest-neighbour, ray and frustum queries are not counted.
A rate, like the per-tick members, though queries produce it rather than the fence: each thread counts its own queries, and the fence takes the delta once per tick (SO-02). Read it against QueryHits: clusters per match is the per-cluster side of what a query pays, which QueryCandidatesPerHit does not see.
QueryHits
Matches those queries returned.
public long QueryHits { get; init; }
Property Value
ReclusterBudgetGrantedMs
The re-clustering budget this archetype was granted for the most recently completed tick, in milliseconds: ReclusterBudgetMs times the share
its queries' efficiency earned (rule TH-04). What the repair planner, the throttle and the drift scan spent against.
public double ReclusterBudgetGrantedMs { get; init; }
Property Value
Remarks
Read against ReclusterBudgetUsedMs, the part repair committed. The whole configured budget when the controller is off
(QueryEfficiencyTolerance 0) or the archetype's range queries hit too little to steer by; next to nothing while they test as few entities per
match as they ever have. GetSpatialTelemetryTotal() sums it: what the tick granted across archetypes.
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
RelocationsExecuted
Intra-cell relocations executed during the most recently completed tick.
public int RelocationsExecuted { get; init; }
Property Value
Remarks
This, RelocationsExecuted and RepairsExecuted sum EXACTLY to MigrationCount — the split is checkable rather than trusted, which is the property #911 gave the trace record and #912 brought to this surface.
Executed, not queued. CrossingsQueued and RelocationsAdmitted are decisions taken a phase earlier and a tick earlier respectively; these three are what the drain actually moved. The gap between a queued relocation and an executed one is RelocationsSuperseded plus the stale-source guard.
Always counted, unlike the trace record's split. A Migrate slice mixes all three kinds by construction, so attributing a per-entity cost to one of them needs this; and needing the profiler on to get it would perturb the bracket the cost is measured in.
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
RepairCellsCooling
Cells waiting out SpatialGridConfig.RepairCooldownTicks after a repair. A LEVEL, like RepairQueueDepth, and disjoint from it: a
cooling cell is not a queue candidate.
public int RepairCellsCooling { get; init; }
Property Value
Remarks
What separates "nothing was repaired because the degraded cells were repaired recently" from "nothing was repaired because nothing degraded" when RepairUnitCount reads zero. Always zero with the cooldown disabled.
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.
Only on ticks granted the whole budget: below it (ReclusterBudgetGrantedMs) the budget controller is withholding it by design (TH-04),
and units go unrepaired on purpose — refused, or at the controller's floor not even priced, so this reads zero there.
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 — unless
ReclusterBudgetGrantedMs sits below it, in which case the budget controller is withholding the budget and the valve is what still
bounds degradation meanwhile (TH-04).
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.
RepairsExecuted
Repair moves executed during the most recently completed tick.
public int RepairsExecuted { get; init; }
Property Value
Remarks
This, RelocationsExecuted and RepairsExecuted sum EXACTLY to MigrationCount — the split is checkable rather than trusted, which is the property #911 gave the trace record and #912 brought to this surface.
Executed, not queued. CrossingsQueued and RelocationsAdmitted are decisions taken a phase earlier and a tick earlier respectively; these three are what the drain actually moved. The gap between a queued relocation and an executed one is RelocationsSuperseded plus the stale-source guard.
Always counted, unlike the trace record's split. A Migrate slice mixes all three kinds by construction, so attributing a per-entity cost to one of them needs this; and needing the profiler on to get it would perturb the bracket the cost is measured in.
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.
TicksAtWholeBudget
Consecutive ticks the budget controller has granted the whole budget because the queries sat at or past the tolerance: the re-base window's progress. A LEVEL. GetSpatialTelemetryTotal() takes the maximum across archetypes.
public int TicksAtWholeBudget { get; init; }
Property Value
TightnessSampleCount
Clusters that contributed a tightness reading during the most recently completed tick — the denominator of MeanClusterExtentRatio and MeanPackingBound.
public int TightnessSampleCount { get; init; }
Property Value
Remarks
Clusters WRITTEN this tick, not clusters that exist. The reading is folded into the AABB refresh, where the extent is already in registers and the cell's population has already been read for the density target. A settled world therefore reports zero samples — and that is why this member exists: without it a mean of zero over nothing is indistinguishable from clusters that are genuinely points, and "zero means zero, never unknown" would be violated by the means rather than honoured.
A full-population reading is a different measurement — SpatialPartitionMatrix.MeasurePartition sweeps every active cluster for it — and
costs O(active clusters), which is precisely what this path exists not to pay.
TotalEfficiencyRebases
Times the budget controller re-based its best since this archetype's cluster state was created: each one a level the whole budget failed to bring back within the tolerance for 200 consecutive ticks, accepted as the world's (TH-04). Cumulative; differentiate for a rate.
public long TotalEfficiencyRebases { get; init; }
Property Value
Remarks
The controller's "maintenance could not keep up" event. Rare is healthy; a steady rate says the world degrades faster than the configured budget repairs, which a fixed budget would have lost as well. GetSpatialTelemetryTotal() sums it.
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; }
Property Value
ZoneMapBatchOpens
Indexed-field slots covered by the tick's Migrate-slice zone-map batches — one per indexed field per slice (#926).
public int ZoneMapBatchOpens { get; init; }
Property Value
Remarks
Read it against MigrationSliceCount, never against MigrationCount. The identity is
ZoneMapBatchOpens == MigrationSliceCount x indexed fields, and the point of publishing it is that the right-hand side does not mention the
migration count at all. Before #926 the Migrate loop took the zone map's archetype-wide grow latch once per migrant per field — two atomics on one
cache line, from every worker — and the fix was to hold one batch per field for the whole slice. A number that starts tracking the migration count
again is that fix regressing, and no timing is needed to see it.
Slots, not latches, and the two differ. A String64 field is indexed but has no zone map (there is no numeric min/max to summarise), so its slot is covered by the batch pass and holds no latch. Counting slots is what makes the identity above exact — the migrant loop's field id counts fields, not maps — but it means an archetype indexed only on String64 reports a non-zero figure having acquired nothing.
Zero on the SERIAL fence, which deliberately opens no batches — it has one writer and nothing to amortise, and holding a batch there would strand a destination past the pinned generation with no growing fallback available. Also zero on a tick that migrated nothing, and for an archetype with no indexed fields. All three mean what they say.