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

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

int

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

int

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

int

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

int

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

float

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

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.

CrossingsExecuted

Cell-crossing migrations executed during the most recently completed tick.

public int CrossingsExecuted { get; init; }

Property Value

int

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

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

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

float

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

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

int

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

byte

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

long

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

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.

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

int

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

int

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

double

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

double

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

double

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

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

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

double

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

double

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

double

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

int

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

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.

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

double

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.

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

long

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

double

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

double

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

double

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

long

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

long

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

double

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

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

RelocationsExecuted

Intra-cell relocations executed during the most recently completed tick.

public int RelocationsExecuted { get; init; }

Property Value

int

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

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

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

int

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

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. 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

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 — 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

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.

RepairsExecuted

Repair moves executed during the most recently completed tick.

public int RepairsExecuted { get; init; }

Property Value

int

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

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.

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

int

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

int

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

long

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

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

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

int

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.