Enum TraceEventKind
Discriminant for a trace record. Every record starts with a 12-byte common header whose third byte carries this value; the kind determines how the bytes after the header are interpreted (span header or minimal instant header, then per-kind typed payload).
public enum TraceEventKind : byte
- Extension Methods
Fields
BTreeDelete = 41B+Tree delete. No payload.
BTreeInsert = 40B+Tree insert. No payload — kind is the only data.
BTreeNodeMerge = 43B+Tree node merge. No payload.
BTreeNodeSplit = 42B+Tree node split. No payload.
CheckpointCollect = 84Checkpoint phase: collect dirty page indices.
CheckpointCycle = 83Full checkpoint cycle. Required:
targetLsn: i64,reason: u8(CheckpointReason). Optional:dirtyPageCount: i32.CheckpointFsync = 86Checkpoint phase: fsync data file.
CheckpointRecycle = 88Checkpoint phase: recycle WAL segments below checkpoint LSN. Optional:
recycledCount: i32.CheckpointTransition = 87Checkpoint phase: transition UoW entries from WalDurable to Committed. Optional:
transitionedCount: i32.CheckpointWrite = 85Checkpoint phase: write dirty pages to data file. Optional:
writtenCount: i32.ClusterMigration = 60Cluster migration between spatial cells — one Migrate-phase SLICE, which mixes all three migration kinds because the pending queue is sorted by destination cell key. Required:
archetypeId: u16,migrationCount: i32(the slice length),componentCount: i32. Optional (added by #911, in mask-bit order):0x01 crossingCount: i32,0x02 relocationCount: i32,0x04 repairCount: i32— the three sum exactly tomigrationCount.The optional block is wire-additive. Before #911 this kind had no optionals and therefore no mask byte, so a pre-#911 record is a strict PREFIX of a post-#911 one: the mask sits after the required payload, and the header's own size field is what tells a reader whether it is there. A decoder must bound its mask read against the record end rather than assume it (see
chunkDecoder.ts'sdecodeOptionalMaskedFields).ConcurrencyAccessControlContention = 95AccessControl contention marker — flag-set instant, fires when a thread enters a wait. Payload: empty.
ConcurrencyAccessControlExclusiveAcquire = 92AccessControl exclusive (writer) acquire success. Payload:
threadId: u16,hadToWait: u8,elapsedUs: u16.ConcurrencyAccessControlExclusiveRelease = 93AccessControl exclusive (writer) release. Payload:
threadId: u16.ConcurrencyAccessControlPromotion = 94AccessControl shared→exclusive promotion (or exclusive→shared demotion via variant byte). Payload:
elapsedUs: u16,variant: u8(0=promote, 1=demote).AccessControl shared (reader) acquire success. Payload:
threadId: u16,hadToWait: u8,elapsedUs: u16.AccessControl shared (reader) release. Payload:
threadId: u16.ConcurrencyAccessControlSmallContention = 100AccessControlSmall contention marker. Payload: empty.
ConcurrencyAccessControlSmallExclusiveAcquire = 98AccessControlSmall exclusive acquire. Payload:
threadId: u16.ConcurrencyAccessControlSmallExclusiveRelease = 99AccessControlSmall exclusive release. Payload:
threadId: u16.AccessControlSmall shared acquire. Payload:
threadId: u16.AccessControlSmall shared release. Payload:
threadId: u16.ConcurrencyAdaptiveWaiterYieldOrSleep = 112AdaptiveWaiter Wait() call yielded or slept (transition only — NOT per-spin). Payload:
spinCountBefore: u16,kind: u8(1=yield, 2=sleep).ConcurrencyEpochAdvance = 108GlobalEpoch advance (Interlocked.Increment in ExitScope/ExitScopeUnordered). Payload:
newEpoch: u32.ConcurrencyEpochRefresh = 109RefreshScope — bump epoch mid-scope to release retired memory while staying pinned. Payload:
oldEpoch: u32,newEpoch: u32.ConcurrencyEpochScopeEnter = 106EpochGuard scope enter (PinCurrentThread). Payload:
currentEpoch: u32,depthBefore: u8,isDormantToActive: u8.ConcurrencyEpochScopeExit = 107EpochGuard scope exit (Dispose). Payload:
newEpoch: u32,isOutermost: u8.ConcurrencyEpochSlotClaim = 110EpochThreadRegistry slot claim. Payload:
slotIndex: u16,threadId: u16,activeCount: u16.ConcurrencyEpochSlotReclaim = 111EpochThreadRegistry dead-thread slot reclaim. Payload:
slotIndex: u16,oldOwner: u16,newOwner: u16.ConcurrencyOlcLatchMarkObsolete = 115OlcLatch.MarkObsolete — node retired, future readers will fail validation. Payload:
version: u32.ConcurrencyOlcLatchValidationFail = 116OlcLatch.ValidateVersion failed — version mismatch detected on optimistic re-read. Payload:
expectedVersion: u32,actualVersion: u32.ConcurrencyOlcLatchWriteLockAttempt = 113OlcLatch.TryWriteLock failed (raced or write-locked). Payload:
versionBefore: u32,success: u8(always 0 on emit).ConcurrencyOlcLatchWriteUnlock = 114OlcLatch.WriteUnlock — version bumped from oldVersion to newVersion. Payload:
oldVersion: u32,newVersion: u32.ConcurrencyResourceAccessing = 101ResourceAccessControl Accessing-mode acquire (try or wait). Payload:
success: u8,accessingCount: u8,elapsedUs: u16.ConcurrencyResourceContention = 105ResourceAccessControl contention marker. Payload: empty.
ConcurrencyResourceDestroy = 103ResourceAccessControl Destroy-mode acquire. Payload:
success: u8,elapsedUs: u16.ConcurrencyResourceModify = 102ResourceAccessControl Modify-mode acquire (try or wait). Payload:
success: u8,threadId: u16,elapsedUs: u16.ConcurrencyResourceModifyPromotion = 104ResourceAccessControl Modify promotion slow path (wait for accessors to drain). Payload:
elapsedUs: u16.DataIndexBTreeBulkInsert = 184BTree multi-value bulk-insert span. Payload:
bufferId: i32, entryCount: i32.DataIndexBTreeNodeCow = 186BTree node CoW (PreDirtyForWrite triggered). Payload:
srcChunkId: i32, dstChunkId: i32.DataIndexBTreeRangeScan = 181BTree range-scan span (covers the whole enumeration). Payload:
resultCount: i32, restartCount: u8.DataIndexBTreeRangeScanRevalidate = 182BTree range-scan OLC revalidate instant (per restart). Payload:
restartCount: u8.DataIndexBTreeRebalanceFallback = 183BTree pessimistic fallback after OLC failure. Payload:
reason: u8(0=LeafFull, 1=OlcFail).DataIndexBTreeRoot = 185BTree root operation (Init or Split, merged via op variant). Payload:
op: u8(0=Init, 1=Split),rootChunkId: i32, height: u8.DataIndexBTreeSearch = 180BTree FindLeaf slow-path instant (b-link hop / OLC restart). Payload:
retryReason: u8, restartCount: u8.DataMvccChainWalk = 178MVCC chain-walk slow path (only emitted when full walk runs, not the single-entry fast path). Payload:
tsn: i64, chainLen: u8, visibility: u8.DataMvccVersionCleanup = 179MVCC version-cleanup span. Payload:
pk: i64, entriesFreed: u16.DataTransactionCleanup = 177Transaction Cleanup span — wraps deferred-cleanup batch enqueue. Payload:
tsn: i64, entityCount: i32.DataTransactionConflict = 176Transaction Conflict instant — fired only when an actual conflict is detected during commit. Payload:
tsn: i64, pk: i64, componentTypeId: i32, conflictType: u8.DataTransactionInit = 173Transaction Init / snapshot acquire span. Payload:
tsn: i64, uowId: u16.DataTransactionPrepare = 174Transaction PrepareForMutation span (high-freq; default-suppressed). Payload:
tsn: i64.DataTransactionValidate = 175Transaction Validate span — wraps the commit-loop validation pass. Payload:
tsn: i64, entryCount: i32.DurabilityCheckpointBackpressure = 223Checkpoint staging-exhaustion backpressure span. Payload:
waitMs: u32, exhausted: u8.DurabilityCheckpointSleep = 224Checkpoint between-cycle sleep span. Payload:
sleepMs: u32,wakeReason: u8(0=timer, 1=force, 2=shutdown, 3=dirty-page pressure).With the dirty-page trigger armed (#830)
sleepMsis the POLL interval, not the checkpoint interval, so most spans of this kind end with wakeReason 0 and no cycle after them — the loop looked, found no pressure, and went back to sleep. Counting these as timer ticks would badly overstate the checkpoint cadence.DurabilityCheckpointWriteBatch = 222Checkpoint write-batch span. Payload:
writeBatchSize: i32, stagingAllocated: i32.DurabilityRecoveryDiscover = 226Recovery discover/enumerate WAL segments. Payload:
segCount: i32, totalBytes: i64, firstSegId: i32.DurabilityRecoveryFpi = 229Recovery FPI torn-page repair span. Payload:
fpiCount: i32, repairedCount: i32, mismatches: i32.DurabilityRecoveryRecord = 228Recovery per-record decode/apply (extreme-freq, deny-listed). Payload:
chunkType: u8, lsn: i64, size: i32.DurabilityRecoveryRedo = 230Recovery redo replay span. Payload:
recordsReplayed: i32, uowsReplayed: i32, durUs: u32.DurabilityRecoverySegment = 227Recovery per-segment scan. Payload:
segId: i32, recCount: i32, bytes: i64, truncated: u8.DurabilityRecoveryStart = 225Recovery start instant. Payload:
checkpointLsn: i64, reason: u8.DurabilityRecoveryTickFence = 232Recovery TickFence replay span. Payload:
tickFenceCount: i32, entries: i32, tickNumber: i64.DurabilityRecoveryUndo = 231Recovery undo (voided UoWs) span. Payload:
voidedUowCount: i32.DurabilityUowDeadline = 234UoW deadline check instant. Payload:
deadline: i64, remaining: i64, expired: u8.DurabilityUowState = 233UoW state transition instant. Payload:
from: u8, to: u8, uowId: u16, reason: u8.DurabilityWalBackpressure = 221WAL commit-buffer-full backpressure span. Payload:
waitUs: u32, producerThread: i32.DurabilityWalBuffer = 219WAL staging-buffer copy span. Payload:
bytesAligned: i32, pad: i32.DurabilityWalFrame = 220WAL per-frame CRC instant (extreme-freq, deny-listed). Payload:
frameCount: u16, crcStart: u32.DurabilityWalGroupCommit = 217WAL group-commit boundary instant. Payload:
triggerMs: u16, producerThread: i32.DurabilityWalOsWrite = 215WAL flush phase 2: OS write + fsync. Payload:
bytesAligned: i32, frameCount: i32, highLsn: i64.DurabilityWalQueue = 218WAL drain decision instant. Payload:
drainAttempt: u8, dataLen: i32, waitReason: u8.DurabilityWalQueueDrain = 214WAL flush phase 1: drain coalesce. Payload:
bytesAligned: i32, frameCount: i32.DurabilityWalSignal = 216WAL flush phase 3: LSN advance + signal. Payload:
highLsn: i64.EcsDestroy = 31Entity destroy. Required:
entityId: u64. Optional:cascadeCount: i32,tsn: i64.EcsQueryAny = 34Query any. Required:
archetypeTypeId: u16. Optional:found: bool,scanMode: u8.EcsQueryConstraintEnabled = 202EcsQuery Enabled/Disabled constraint add. Payload:
typeId: u16, enableBit: u8.EcsQueryConstruct = 199EcsQuery construct (archetype resolution). Payload:
targetArchId: u16, polymorphic: u8, maskSize: u8.EcsQueryCount = 33Query count. Required:
archetypeTypeId: u16. Optional:resultCount: i32,scanMode: u8.EcsQueryExecute = 32Query execute. Required:
archetypeTypeId: u16. Optional:resultCount: i32,scanMode: u8.EcsQueryMaskAnd = 200EcsQuery mask AND. Payload:
bitsBefore: u16, bitsAfter: u16, opType: u8.EcsQuerySpatialAttach = 203EcsQuery spatial predicate attach. Payload:
spatialType: u8, queryBox: 4xf32 (x1, y1, x2, y2).EcsQuerySubtreeExpand = 201EcsQuery subtree expand. Payload:
subtreeCount: u16, rootId: u16.EcsSpawn = 30Entity spawn. Required:
archetypeId: u16. Optional:entityId: u64,tsn: i64.EcsSpawnBatch = 36Batch entity spawn — one record for the whole batch. Required:
archetypeId: u16,routingId: u16,baseKey: i64,count: i32,tsn: i64.A batch is a closed-form range, not a list:
Transaction.SpawnBatchreserves all N keys with oneInterlocked.Addand stamps one routing id, so every id in the batch isnew EntityId(baseKey + n, routingId). Recording N EcsSpawn records would store ~56 B each of a value the reader can compute — 11 MiB for a 200K bulk load, against 24 B here.Why
routingIdis carried explicitly. It is already the low 16 bits of every id in the range, but a consumer rebuilding ids frombaseKeyalone would have to source it from somewhere — and the nearest candidate,archetypeId, is the *catalog* id, a different number for the same archetype. That substitution is the id-space landmine documented inclaude/design/Apps/Workbench/10-database-and-profiles.md§5.3; carrying both closes it at the wire level.EcsViewDeltaBufferOverflow = 206View delta-buffer overflow (operationally critical — never default-suppressed). Payload:
currentTsn: i64, tailTsn: i64, marginPagesLost: u16.EcsViewDeltaCacheMiss = 213View delta-cache miss (entity re-eval). Payload:
pk: i64, reason: u8.EcsViewIncrementalDrain = 205View IncrementalDrain. Payload:
deltaCount: i32, overflow: u8.EcsViewProcessEntry = 207View single-branch entry process (instant, high-freq, default-suppressed). Payload:
pk: i64, fieldIdx: u16, pass: u8.EcsViewProcessEntryOr = 208View OR-mode entry process (instant, high-freq, default-suppressed). Payload:
pk: i64, branchCount: u8, bitmapDelta: u32.EcsViewRefresh = 35View refresh. Required:
archetypeTypeId: u16. Optional:mode: u8,resultCount: i32,deltaCount: i32.EcsViewRefreshFull = 209View Refresh-from-overflow (Pull). Payload:
oldCount: i32, newCount: i32, requeryNs: u32.EcsViewRefreshFullOr = 210View RefreshFullOr. Payload:
oldCount: i32, newCount: i32, branchCount: u8.EcsViewRefreshPull = 204View RefreshPull (full re-query branch). Payload:
queryNs: u32, archetypeMaskBits: u16.EcsViewRegistryDeregister = 212ViewRegistry Deregister. Payload:
viewId: u16, fieldIdx: u16, regCount: u16.EcsViewRegistryRegister = 211ViewRegistry Register. Payload:
viewId: u16, fieldIdx: u16, regCount: u16.GcEnd = 8.NET runtime GC boundary — a garbage collection has ended. Payload:
u8 generation,u32 count,i64 pauseDurationTicks,u64 promotedBytes, fiveu64per-generation size-after values (Gen0/Gen1/Gen2/LOH/POH),u64 totalCommittedBytes. Sizes are snapshotted by the ingestion thread via GetGCMemoryInfo() on theGCEnd_V1event.GcStart = 7.NET runtime GC boundary — a garbage collection has started. Payload:
u8 generation,u8 reason(GcReason),u8 type(GcType),u32 count. Emitted by the profiler's GC ingestion thread on observingGCStart_V2. Only produced whenTelemetryConfig.ProfilerGcTracingActiveis set at class load.GcSuspension = 75.NET runtime Execution-Engine suspension window. Opened on
GCSuspendEEBegin_V1(ETW id 9), closed onGCRestartEEEnd_V1(ETW id 3). Payload:u8 reason(GcSuspendReason),u8 optMask(reserved).ParentSpanIdis always 0 (process-level, not caller-attributed). No Activity capture.Instant = 6Generic instant marker. Payload:
nameId: i32(interned),payload: i32.MemoryAlignmentWaste = 172Alignment-induced waste on an allocation (only emitted when waste > 0). Payload:
size: i32, alignment: i32, wastePctHundredths: u16.MemoryAllocEvent = 9Discrete unmanaged-memory allocation or free event. Every
PinnedMemoryBlockconstruct/dispose emits one of these whenTelemetryConfig.ProfilerMemoryAllocationsActiveis set. Payload:u8 direction(MemoryAllocDirection),u16 sourceTag(MemoryAllocSource),u64 sizeBytes,u64 totalAfterBytes. Wire size: 31 B.NamedSpan = 246User-defined span with inline UTF-8 null-terminated name. Used for dynamic-string call sites (tests, demo code). Payload: null-terminated UTF-8 bytes.
Was
200until 2026-05-10, which collided with EcsQueryMaskAnd. The collision was latent in production becauseEcsQueryMaskAndis default-suppressed, but unsuppressing it would have made the two kinds indistinguishable on the wire. Reassigned to 246 (next free slot above SchedulerSystemArchetype). Wire format bumped to v8 in CurrentVersion to signal the change.PageCacheAllocatePage = 53Page cache allocate page. Required:
filePageIndex: i32.PageCacheBackpressure = 59Page cache backpressure wait — clock-sweep retry loop couldn't find a free page. Required:
retryCount: i32,dirtyCount: i32,epochCount: i32. Suppressed by default alongside other PageCache.* kinds.PageCacheDiskRead = 51Page cache disk read. Required:
filePageIndex: i32.PageCacheDiskReadCompleted = 56Completion marker for an async PageCacheDiskRead. Required:
filePageIndex: i32. The record carries the original DiskRead span'sSpanIdas its ownSpanId(same value — the viewer uses this to correlate kickoff → completion) and aStartTimestampequal to the original'sStartTimestamp, so the record'sdurationTicksfield is the full async tail:completionTimestamp - beginTimestamp. Emitted from the thread-pool worker that completes theRandomAccess.ReadAsync.Suppressed by default alongside the other PageCache.* kinds. Opt in with
TyphonEvent.UnsuppressKind(PageCacheDiskReadCompleted)(must ALSO have PageCacheDiskRead unsuppressed, otherwise there's no kickoff span to correlate with and the completion is skipped at the call site). Reuses PageCacheEventCodec wire shape verbatim — decoder needs only a match-arm entry.PageCacheDiskWrite = 52Page cache disk write. Required:
filePageIndex: i32. Optional:pageCount: i32.PageCacheDiskWriteCompleted = 57Completion marker for an async PageCacheDiskWrite. Required:
filePageIndex: i32. Same correlation pattern as PageCacheDiskReadCompleted — record'sSpanIdmatches the original DiskWrite span,durationTicksis the full async tail including the OS write completion.PageCacheFetch = 50Page cache fetch. Required:
filePageIndex: i32.PageCacheFlush = 54Page cache flush. Required:
pageCount: i32.PageCacheFlushCompleted = 58Completion marker for an async PageCacheFlush. Required:
pageCount: i32(stored in the primaryfilePageIndexslot per Flush convention). Record'sdurationTickscovers the full flush tail:max(WriteAsync completions)+fsync. The delta between this record's duration and the max of the enclosed PageCacheDiskWriteCompleted events is pure fsync cost.PageEvicted = 55A cached page was displaced by PageCacheAllocatePage to make room for a new page being fetched. Required:
filePageIndex: i32(the displaced page's file index, NOT the incoming page).Emitted as a zero-duration span (start == end). Parents under the enclosing PageCacheAllocatePage span via TLS, so the viewer renders eviction events as instant markers nested inside the AllocatePage bar — "allocation ran for 12 µs and kicked out page N." Reuses the
PageCacheEventCodecwire shape (common header + span extension + 4 B filePageIndex + 1 B optMask), so no new codec is needed. Suppressed by default alongside the other PageCache.* kinds.PerTickSnapshot = 76Per-tick gauge snapshot — packed bundle of (gaugeId, value) pairs emitted once per tick by the scheduler thread at end-of-tick. Instant-style record: no span header extension, no duration semantics. Common header + fixed prefix (
u32 tickNumber,u16 fieldCount,u32 flags) then repeated{u16 gaugeId; u8 valueKind; [4 or 8 B] value}entries. Gated onTelemetryConfig.ProfilerGaugesActive. See GaugeId for the wire-stable gauge ID registry.Although this kind's numeric value is ≥ 10, it is not a span record. IsSpan(TraceEventKind) explicitly excludes it so the consumer never tries to read the 25-byte span header extension after the common header.
PhaseEnd = 3Tick phase ended. Payload:
phase: u8.PhaseStart = 2Tick phase started. Payload:
phase: u8(TickPhase enum).QueryArgs = 248Per-execution arguments payload emitted immediately after each QueryPlan when the query carries at least one evaluator (skipped when
EvaluatorCount == 0). Payload:evaluatorCount: u16,thresholds[evaluatorCount]: i64(widened threshold constants matchingFieldEvaluator.Threshold).QueryCount = 198Count-only path. Payload:
resultCount: i32.QueryDefinitionDescribe = 247One-shot definition descriptor emitted on first observation of a View/EcsQuery identity within a profiling session. Payload (variable-length):
kind: u8(0=View, 1=EcsQuery),localId: u32,targetComponentType: u16,primaryIndexFieldIdx: i16,sortFieldIdx: i16,sortDescending: u8,definitionSourceFileId: u16,definitionSourceLine: i32,definitionSourceMethodId: u16,evaluatorCount: u16,evaluators[evaluatorCount]: { fieldIdx: u16, op: u8, reserved: u8 },fieldDependencyCount: u16,fieldDependencies[fieldDependencyCount]: u16.QueryEstimate = 190Selectivity estimate. Payload:
fieldIdx: u16, cardinality: i64.QueryExecuteFilter = 195Filter pass (post-primary). Payload:
filterCount: u8, rejectedCount: i32.QueryExecuteIndexScan = 193B+Tree range-scan dispatch. Payload:
primaryFieldIdx: u16, mode: u8(0=Single, 1=Multi).QueryExecuteIterate = 194Index iteration (chunk-grain). Payload:
chunkCount: i32, entryCount: i32.QueryExecutePagination = 196Pagination (Skip/Take). Payload:
skip: i32, take: i32, earlyTerm: u8.QueryExecuteStorageMode = 197Storage-mode dispatch instant. Payload:
mode: u8(0=SV, 1=Versioned, 2=Transient).QueryParse = 187Query expression parse. Payload:
predicateCount: u16, branchCount: u8.QueryParseDnf = 188Query DNF normalization. Payload:
inBranches: u16, outBranches: u16.QueryPlan = 189Query plan build. Payload:
evaluatorCount: u8, indexFieldIdx: u16, rangeMin: i64, rangeMax: i64.QueryPlanPrimarySelect = 191Primary stream selection (instant). Payload:
candidates: u8, winnerIdx: u8, reason: u8.QueryPlanSort = 192Predicate ordering / sort by cardinality. Payload:
evaluatorCount: u8, sortNs: u32.QueueTickEnd = 244Per-(tick, queue) rollup emitted at end-of-tick. Captures queue-depth telemetry that's local to the engine's
EventQueueBaseaccumulators (peak depth, end-of-tick depth, overflow count, produced/consumed counts). Folded by IncrementalCacheBuilder into the v12 QueueTickSummaries section for the Workbench Data APIqueue/<name>tracks (#311). Payload:tick: u32, queueId: u16, peakDepth: u32, endOfTickDepth: u32, overflowCount: u32, produced: u32, consumed: u32= 26 bytes after the common header.RuntimePhaseSpan = 243Tick lifecycle phase span — covers the full duration of one TickPhase region inside
TyphonRuntime.OnTickEndInternal(SystemDispatch, WriteTickFence, UoWFlush, OutputPhase, TierIndexRebuild, DormancySweep). Replaces the previous PhaseStart+PhaseEnd instant pair on the producer side: a real span is opened at the top ofInspectorPhaseand disposed at the bottom, so child spans (PageCacheFlush,BTreeInsert, etc.) attach viaparentSpanIdfor proper hierarchy. Payload:phase: u8(TickPhase enum).RuntimePhaseUoWCreate = 161UoW create (per tick, OnTickStart). Payload:
tick: i64.RuntimePhaseUoWFlush = 162UoW flush (per tick, OnTickEnd). Payload:
tick: i64,changeCount: i32.RuntimeTransactionLifecycle = 163Per-system Transaction lifecycle span. Payload:
sysIdx: u16,txDurUs: u32,success: u8.SchedulerChunk = 10Scheduler chunk executed on a worker. Payload:
systemIdx: u16,chunkIdx: u16,totalChunks: u16,entitiesProcessed: i32.SchedulerDependencyFanOut = 155Dependency fan-out span over OnSystemComplete successor loop. Payload:
completingSysIdx: u16,succCount: u16,skippedCount: u16.SchedulerDependencyReady = 154Successor mark-ready (only when depsLeft → 0). Payload:
fromSysIdx: u16,toSysIdx: u16,fanOut: u16,predRemain: u16.SchedulerDispense = 153Successful chunk-grab CAS. Payload:
sysIdx: u16,chunkIdx: i32,workerId: u8.SchedulerGraphBuild = 159DAG build at startup. Payload:
sysCount: u16,edgeCount: u16,topoLen: u16.SchedulerGraphRebuild = 160DAG rebuild (dynamic system registration — design stub, no producer in Phase 4). Payload:
oldSysCount: u16,newSysCount: u16,reason: u8.SchedulerMetronomeWait = 241Metronome wait between ticks (TickDriver thread, Sleep→Yield→Spin phases). Span covers the entire gap from the moment
ExecuteCallbacksreturned (or timer started) until the next tick fires. Payload:scheduledTimestamp: i64, multiplier: u8, intentClass: u8, phaseFlags: u8(11 B).intentClass: 0=CatchUp (target already past), 1=Throttled (mult>1), 2=Headroom (mult==1 normal idle).phaseFlagsbits: 0x1=Sleep entered, 0x2=Yield entered, 0x4=Spin entered.SchedulerOverloadDetector = 242Per-tick OverloadDetector state snapshot (instant on TickDriver). Carries the full state used to drive escalation/deescalation decisions so an offline trace can audit why the engine throttled. Payload:
tick: i64, overrunRatio: f32, consecutiveOverrun: u16, consecutiveUnderrun: u16, consecutiveQueueGrowth: u16, queueDepth: i32, level: u8, multiplier: u8(24 B).SchedulerOverloadLevelChange = 156Overload level transition (only on change). Payload:
prevLvl: u8,newLvl: u8,ratio: f32,queueDepth: i32,oldMul: u8,newMul: u8.SchedulerOverloadSystemShed = 157Per-system shed/throttle decision (when overload level > Normal). Payload:
sysIdx: u16,level: u8,divisor: u16,decision: u8(0=shouldRunFalse, 1=throttled, 2=shed).SchedulerOverloadTickMultiplier = 158Tick multiplier applied (per tick). Payload:
tick: i64,multiplier: u8,intervalTicks: u8.SchedulerSystemArchetype = 245Per-(system, archetype) entity-touch rollup emitted at parallel-query completion. Captures the cross-dimension that SchedulerChunk (per-system) and EcsQueryExecute (per-archetype) leave separate. Feeds the Workbench Data Flow module's
archetype/,system-archetype/, andcomponent-family/*track families. Payload:systemIndex: u16, archetypeId: u16, entityCount: i32, chunkCount: i32= 12 bytes after the span header. Gated byTelemetryConfig.SchedulerArchetypeTouchesActive.SchedulerSystemCompletion = 147System completion (RecordSystemDone). Payload:
sysIdx: u16,reason: u8(0=ok, 1..N=SkipReason),durationUs: u32.SchedulerSystemQueueWait = 148Ready → first-grab latency (queue wait). Payload:
sysIdx: u16,queueWaitUs: u32.SchedulerSystemSingleThreaded = 149Single-threaded system execution span. Payload:
sysIdx: u16,isParallelQuery: u8,chunkCount: u16.SchedulerSystemStartExecution = 146System first chunk dispatched — execution boundary (after CAS, before work). Payload:
sysIdx: u16.SchedulerWorkerBetweenTick = 152Worker between-tick wait (kernel wait span). Payload:
workerId: u8,waitUs: u32,wakeReason: u8(0=woken, by the signal or by the backstop with no wake lost; 1=shutdown; 2=resumed by the backstop after a lost wake).SchedulerWorkerIdle = 150Worker idle span (per spin spell). Payload:
workerId: u8,spinCount: u16,idleUs: u32.SchedulerWorkerWake = 151Worker wake from kernel signal. Payload:
workerId: u8,delayUs: u32.SpatialArchetypeTelemetry = 66Per-archetype, per-tick snapshot of the spatial-maintenance counters, so a live consumer can read them without an RPC into the engine. Instant-shaped. Payload, all REQUIRED and in wire order — an instant has no optional-mask byte:
archetypeId: u16,activeClusters: i32,migrations: i32,migrationCpuMs: f32,hysteresisAbsorbed: i32,driftersDetected: i32,repairUnits: i32,repairUnitsRefused: i32,repairQueueDepth: i32,budgetUsedMs: f32,tightnessSamples: i32,extentRatio: f32,packingBound: f32,cellTreePromotions: i32,cellTreeDemotions: i32(58 bytes); then, appended for the maintenance controller (#906):queryClustersOpened: i64,queryCandidates: i64,queryHits: i64,budgetConfiguredMs: f32,budgetGrantedMs: f32,efficiencyTolerance: f32,candidatesPerHitSmoothed: f32,candidatesPerHitBest: f32,ticksAtWholeBudget: i32,controllerFlags: u8,efficiencyRebases: i32,repairCellsCooling: i32,repairValveFires: i32,repairedEntities: i32,repairQueueEvicted: i64,measuredNsPerEntity: f32,driftTargetBoost: f32(139 bytes); then, appended for the realm census:presentRealms: i32,runnableRealms: i32. 147 bytes. Emitted every tick for every archetype with cluster state, whatever path its fence took.The two realm counts are a census, not a dimension. Every other field on this record is summed across every realm the archetype lives in, and stays that way: the per-tick counters are owned per archetype, not per realm. The counts exist so a consumer reading SpatialRealmTelemetry rows — emitted for runnable realms only — can say how many realms it is NOT showing, instead of presenting a partial list as the whole.
Grow it only by appending — never reorder or remove a field. The Workbench decoder reads it by offset, and a record written before an append must stay a prefix of one written after. A record shorter than 139 bytes predates the fields it lacks: treat them as absent, which its size says, not as zero, because several read zero as a meaning — a configured budget of 0 is "no enforcement". The generated C# decoder zero-fills them.
The relocation outcome split is NOT here — it is SpatialRelocationOutcome, joined by (archetype, tick). Gated onSpatialArchetypeTelemetryActive.The attach transport is a one-way trace stream — there is no request/response channel from the Workbench to a running engine — so a live per-archetype surface has to ride an event. Shaped after SchedulerSystemArchetype, which is already a per-archetype per-tick record.
SpatialCellIndexAdd = 130CellSpatialIndex.Add — append cluster to cell's SoA. Payload:
cellKey: i32,slot: i32,clusterChunkId: i32,capacity: i32.SpatialCellIndexRemove = 132CellSpatialIndex.RemoveAt — swap-with-last. Payload:
cellKey: i32,slot: i32,swappedClusterId: i32.SpatialCellIndexUpdate = 131CellSpatialIndex.UpdateAt — in-place AABB rewrite at slot. Payload:
cellKey: i32,slot: i32.SpatialClusterAabbRefresh = 250Per-tick span around
RecomputeDirtyClusterAabbs. Fires once per archetype per tick — the existing SpatialCellIndexUpdate instant fires per per-cell index UpdateAt, but doesn't show the cost of the full pass (occupancy scan + bit-exact compare for unchanged clusters). Required payload:archetypeId: u16,clusterScanned: i32,aabbChanged: i32. Gated onSpatialCellIndexUpdateActive.SpatialClusterMigrationDetect = 133Cluster migration detected (cell crossing observed, queued for execute). Payload:
archetypeId: u16,clusterChunkId: i32,oldCellKey: i32,newCellKey: i32.SpatialClusterMigrationDetectScan = 249Per-tick span around
DetectClusterMigrations. Fires once per archetype per tick whenever the spatial path runs, regardless of whether any entity actually crossed a cell — the existing SpatialClusterMigrationDetect instant only fires per detected crossing, so it's silent for workloads where ants/units move within hysteresis every tick (e.g. AntHill). Required payload:archetypeId: u16,scanSlotCount: i32(slots iterated),migrationsQueued: i32,hysteresisAbsorbed: i32. Gated onSpatialClusterMigrationDetectActive.SpatialClusterMigrationHysteresis = 135Cluster migration hysteresis absorb — crossing detected but absorbed by margin (no migration queued). Payload:
archetypeId: u16,clusterChunkId: i32,escapeDistSq: f32.SpatialClusterMigrationQueue = 134Cluster migration queue append. Payload:
archetypeId: u16,clusterChunkId: i32,queueLen: u16.SpatialGridCellTierChange = 127Cell tier change (only emits when SetCellTier actually flips the tier byte). Payload:
cellKey: i32,oldTier: u8,newTier: u8.SpatialGridClusterCellAssign = 129Cluster-cell assignment (cluster claimed in a particular cell). Payload:
clusterChunkId: i32,cellKey: i32,archetypeId: u16.SpatialGridOccupancyChange = 128Cell occupancy change (Increment/Decrement merged via signed delta). Payload:
cellKey: i32,delta: i8,occBefore: u16,occAfter: u16.SpatialMaintainAabbValidate = 140RETIRED (#872 step 13). Was: degenerate AABB validation failure on the entity-tree write path.
SpatialMaintainBackPointerWrite = 141RETIRED (#872 step 13). Was: spatial back-pointer write (componentChunkId → leafChunkId + slotIndex).
SpatialMaintainInsert = 138RETIRED (#872 step 13). Was: Maintain.InsertSpatial — RTree.Insert + back-pointer + occupancy.
SpatialMaintainUpdateSlowPath = 139RETIRED (#872 step 13). Was: Maintain.UpdateSpatial slow path — escape detected, remove + reinsert.
SpatialQueryAabb = 117AABB query iterator pass. Payload:
nodesVisited: u16,leavesEntered: u16,resultCount: u16,restartCount: u8,categoryMask: u32.SpatialQueryCount = 122Count query (CountInAABB / CountInRadius merged via variant byte). Payload:
variant: u8(0=AABB, 1=Radius),nodesVisited: u16,resultCount: i32.SpatialQueryFrustum = 120Frustum query iterator pass. Payload:
nodesVisited: u16,resultCount: u16,planeCount: u8,restartCount: u8.SpatialQueryKnn = 121KNN query (full call covering all radius-expansion iterations). Payload:
k: u16,iterCount: u8,finalRadius: f32,resultCount: u16.SpatialQueryRadius = 118Radius (sphere) query iterator pass. Payload:
nodesVisited: u16,resultCount: u16,radius: f32,restartCount: u8.SpatialQueryRay = 119Ray query iterator pass. Payload:
nodesVisited: u16,resultCount: u16,maxDist: f32,restartCount: u8.SpatialRTreeBulkLoad = 126RTree bulk-load (init-time). Payload:
entityCount: i32,leafCount: i32.SpatialRTreeInsert = 123RTree insert (descent + leaf write or split fallout). Payload:
entityId: i64,depth: u8,didSplit: u8,restartCount: u8.SpatialRTreeNodeSplit = 125RTree node split (sub-event of Insert when leaf is full). Payload:
depth: u8,splitAxis: u8,leftCount: u8,rightCount: u8.SpatialRTreeRemove = 124RTree remove (leaf modify, possibly cascade up). Payload:
entityId: i64,leafCollapse: u8.SpatialRealmRates = 70Per-REALM, per-archetype snapshot of what one realm's partition DID this tick — the maintenance rates, attributed to the realm that produced them. The rate twin of SpatialRealmTelemetry's shape. Instant-shaped.
Payload, all REQUIRED, tightly packed, in wire order:
realmId: u16,archetypeId: u16, then threef64(tightnessExtentSum,tightnessBoundSum,relocationSpendNs), then thirtyi32in the order the engine's own counter block declares them. 148 bytes. Grow it only by APPENDING, as with kinds 66 and 67.Why it is not part of kind 67. The two have different absence meanings and neither row set contains the other. Kind 67 is emitted for RUNNABLE realms, whether or not they did anything; this is emitted for realms the fence TOUCHED, runnable or not. One record carrying both would make a missing row mean two things at once, which is the ambiguity SO-01's "zero means zero, never unknown" exists to forbid. Keeping them apart also keeps the anomaly visible: a row here with no kind-67 row is a non-runnable realm that did maintenance work, which is worth an alert rather than a silent drop.
The tightness pair are SUMS, not means, and the sample count is beside them. Kind 66 sends means because it is already archetype-wide and has nothing left to fold. A consumer folding several realms cannot re-derive a mean from per-realm means — it would weight a realm that scanned one cluster equally with one that scanned ten thousand, which is the exact error SO-01 names for the archetype fold. Sums over the summed sample count are foldable across any set of realms; a zero sample count keeps "the fence wrote nothing" distinguishable from "the clusters are points".
largestArrivalRunfolds with MAX, never with +. It is the most crossings into one destination cell, so adding two realms' values would report a burst neither cell received. Wire-identical to its neighbours; the contract is on the consumer, stated here because nothing in the bytes says so — the same way kind 66 has to state that its migration time is CPU-milliseconds and not a span.No duration here is CPU-summed across workers, so that clause does NOT bind this record. The two counters it was written for (
migrationCpuMsand the apply ticks) are archetype-wide and stay on kind 66;relocationSpendNsis computed once per archetype per tick on the serial path, and the tightness pair are sums of ratios rather than elapsed time. Stated because the next reader will assume otherwise.Capped, and the cap is visible. Volume scales with the realm count, so the emission stops at a bound per archetype per tick. Kind 66 carries
ratesRealmsTouchedandratesRealmsEmittedfor the same archetype and tick: equal means nothing was truncated and an absent realm did no work;emitted < touchedmeans a consumer must say how many realms that worked it is NOT showing. One count could not separate those, because kind 66's other population figure ispresentRealms, and present is not touched.Gated on
SpatialRealmRatesActive, separately from kind 67 for the volume reason above.SpatialRealmTelemetry = 67Per-REALM, per-archetype snapshot of the SHAPE of one realm's partition, so a live consumer can ask "which realm is hot / which realm is shaped badly" instead of reading one number summed across every realm an archetype lives in. Instant-shaped. Payload, all REQUIRED and in wire order:
realmId: u16,archetypeId: u16,runState: u8(the engine'sRealmRunState: Dormant 0, Simulated 1, Active 2, Closing 3 — this assembly does not reference the engine, so the values are named here rather than linked),divisor: u8,cellSize: f32,cellCount: i32,gridDepth: i32,clusters: i32,clusterReach: f32,escapedClusters: i32,promotedCells: i32,blockedCells: i32,budgetConfiguredMs: f32,efficiencyTolerance: f32. 46 bytes. Grow it only by appending, as with kind 66.The last two are DECLARED, not enforced. Maintenance is budgeted per archetype and the budget actually spent comes from realm 0's grid, so two realms declaring different values both run under realm 0's. A consumer must label them as the realm's declaration; kind 66's
budgetConfiguredMsis the ceiling a grant was measured against.Structure, not rates. Every field here is per-realm state the realm itself owns — its grid, its own cell size, its own reach, its own configured budget. The per-tick RATE counters (migrations, repair units, budget spent, the tightness means) are deliberately NOT here: they live on
ArchetypeClusterState, one set per archetype rather than one per realm, so a realm-keyed copy of them would report the sum across every realm under one realm's name. That is worse than their absence, because it would look right. Splitting them is its own change; until then kind 66 remains the only source for them and is archetype-wide by construction.Emitted for RUNNABLE realms only (
Gated onRealms/02-runtime-lifecycle.md§6: per realm-archetype telemetry is "thousands/tick → runnable only"). A galaxy of a few thousand sleeping interiors would otherwise spend its trace bandwidth on rows that are all zero by definition. The realms left out are not silently missing: kind 66 carriespresentRealmsandrunnableRealmsfor the same archetype and tick, so a consumer can state how many rows it is not showing — and a realm going dormant is then visible as a row disappearing while the skipped count rises.SpatialRealmTelemetryActive.SpatialRelocationOutcome = 65Per-archetype, per-tick rollup of what the throttle did with the intra-cell relocations detected on the previous tick — the outcome split the counters already distinguish and the timeline did not. Instant-shaped. Payload, all REQUIRED and in wire order — an instant has no optional-mask byte, so
[Optional]does not exist for this shape:archetypeId: u16,admitted: i32,throttled: i32,superseded: i32,unplaced: i32,unplacedNoCandidate: i32,spilled: i32,pinsRejected: i32,crossingsQueued: i32. 34 bytes. Gated onSpatialClusterRelocationActive.One event per archetype per tick, not one per refused relocation: the refusal rate reaches 10^4-10^5/second on a moving world, which is the rate the deny-list exists to keep off the ring. The throttle resolves the whole split in one place, so the rollup is a read of values it has just computed.
SpatialRepairUnit = 64One admitted repair unit — a cell's N worst clusters, Morton-re-sorted into freshly allocated destinations. Emitted by the Prep-phase planner at the moment of admission, so the span brackets the plan and its execution is attributable to the cell it names. Required:
archetypeId: u16,cellKey: i32,clusterCount: i32,entityCount: i32. Optional, in mask-bit order:0x01 degradation: f32(the max-axis extent ÷ cell size the cell was ranked on),0x02 valveFired: u8,0x04 movedCount: i32(entities the plan actually moved;0means the cell was already packed). The producer assigns all three unconditionally, so every emitted record carries mask0x07. Gated onSpatialClusterRepairActive.The kind the timeline had no way to express: ClusterMigration brackets a whole Migrate slice with crossings, relocations and repairs mixed, so "which cell was repaired, and how degraded was it" could only be answered with throwaway counters on a branch.
SpatialTierIndexRebuild = 136TierClusterIndex rebuild span. Payload:
archetypeId: u16,clusterCount: i32,oldVersion: i32,newVersion: i32.SpatialTierIndexVersionSkip = 137TierClusterIndex version-skip — rebuild bypassed because version unchanged. Payload:
archetypeId: u16,version: i32,reason: u8.SpatialTriggerCacheInvalidate = 145Trigger static-tree cache invalidate (mutation observed bumps cached version). Payload:
regionId: u16,oldVersion: i32,newVersion: i32.SpatialTriggerEval = 143Trigger region eval span. Payload:
regionId: u16,occupantCount: u16,enterCount: u16,leaveCount: u16.SpatialTriggerOccupantDiff = 144Trigger occupant XOR-diff stats (NO bitmap). Payload:
regionId: u16,prevCount: u16,currCount: u16,enterCount: u16,leaveCount: u16.SpatialTriggerRegion = 142Trigger region create/destroy (merged via op variant). Payload:
op: u8(0=create, 1=destroy),regionId: u16,categoryMask: u32.StatisticsRebuild = 89Statistics rebuild for a ComponentTable. Required:
entityCount: i32,mutationCount: i32,samplingInterval: i32.StorageChunkSegmentGrow = 169ChunkBasedSegment.GrowChunkCapacity. Payload:
stride: i32, oldCap: i32, newCap: i32.StorageFileHandle = 170File handle Open/Close (merged via op variant). Payload:
op: u8(0=open, 1=close),filePathId: i32, modeOrReason: u8.StorageOccupancyMapGrow = 171OccupancyMap L3 grow path. Payload:
oldCap: i32, newCap: i32.StoragePageCacheDirtyWalk = 165Dirty-walk during checkpoint (collect dirty page indices). Payload:
rangeStart: i32, rangeLen: i32, dirtyMs: i32.StorageSegmentCreate = 166LogicalSegment.Create. Payload:
segmentId: i32, pageCount: i32.StorageSegmentGrow = 167LogicalSegment.Grow. Payload:
segmentId: i32, oldLen: i32, newLen: i32.StorageSegmentLoad = 168LogicalSegment.Load. Payload:
segmentId: i32, pageCount: i32.SubscriptionsFrameProduced = 71A frame was published for one session.
Payload, tightly packed:
sessionId: u64,sequence: i64,bytes: i32,reset: u8(1 for aRESETframe),degradeLevel: u8. 22 bytes. Append-only, as kinds 68-69.The firehose of the three, and gated alone for that reason: one record per served session per tick — 50 000 a second for 1 000 sessions at 50 Hz. Kinds 72 and 73 are what a trace is usually opened for; this one is for following a single session's frames against its client's log.
Gated on
SubscriptionsFrameProducedActive.SubscriptionsFrameSkipped = 72A session was due a frame and did not get one — back-pressure, never an idle tick (a tick with nothing to say is not a skip).
Payload, tightly packed:
sessionId: u64,cause: u8,degradeLevel: u8,skipRun: i32. 14 bytes. Append-only.cause: 1 gated (the degrade ladder's rate gate), 2 lagging (acknowledgement past the lag bound), 3 in flight full (K frames unacknowledged), 4 oversize (past the session's frame budget), 5 pool refused (frame memory), 6 owed reset (more owed than a frame holds; aRESETfollows).skipRunis the session's consecutive-skip run after this tick, the number the close bound is read against.Gated on
SubscriptionsFrameSkippedActive.SubscriptionsServerTelemetry = 68Server-wide push-replication figures, one record per stats emission (1 Hz, alongside the client-facing
STATSblock).Payload, tightly packed with no alignment padding, in this order:
sessions: i32,netOutBytesPerSec: f32,trackP99Ms: f32,durabilityWaitP99Ms: f32,framesSkipped: i64,framePoolRented: i32,framePoolBlocks: i32,framePoolBudgetSkips: i64,reportedSessions: i32. 44 bytes. Two fields belong here and are deliberately absent from this first version — the live NetId count and the server-wide dropped-command total. Neither the allocator nor the ingress table is reachable from the emission site, and threading a constructor dependency throughFrameAssemblerfor them is a worse trade than appending them later, which the append-only rule below exists to allow. They are omitted rather than sent as zero, because a field that always reads zero is worse than a field that is not there. Grow it only by APPENDING, as with kinds 66 and 67 — a reader keyed on offsets stays correct against an older producer, and the size guard is what lets a shorter record decode.A transport, not a measurement. Every value here is one the engine already computes for the
STATSwire block that goes to game clients (Subscriptions/internals/StatsEncoder.cs); this re-emits it on the profiler wire, because an attach session is a one-way stream with no way to call an accessor on the engine it watches.reportedSessionsis how many kind-69 rows accompany this record, so a panel can state how many sessions it is NOT showing rather than implying the list is complete.Gated on
SubscriptionsServerTelemetryActive.SubscriptionsSessionChanged = 73A session opened, closed, or moved on the degrade ladder.
Payload, tightly packed:
sessionId: u64,change: u8,degradeLevel: u8,closeReason: u8,closeCode: u16. 13 bytes. Append-only.change: 1 opened, 2 closed, 3 degraded (level rose), 4 recovered (level fell).degradeLevelis the level after a degrade or a recovery PLUS ONE — a recovery to full rate is level 0, a real level — and 0 on an open or a close, where it is not reported: the session table that opens and closes sessions does not hold the level; the session's last kind-72 or kind-73 record does.closeReasonisSessionCloseReason + 1andcloseCodethe code the client was given, both zero on every change but a close — the reason is offset because its first value,ClientLeft, is zero, and zero here means "not a close".Gated on
SubscriptionsSessionChangesActive.SubscriptionsSessionTelemetry = 69One connected session's push-replication figures. Emitted per session per stats emission, up to a cap.
Payload, tightly packed:
sessionId: u64,realmId: u16,bytesPerSec: f32,framesSkipped: i64,degradeLevel: i32. 26 bytes. Append-only, as above.realmIdis0xFFFFfor a session that has not been told its realm yet, which is the committed realm's own "-1 before the first RESET" widened to an unsigned field — a consumer can tell that from "in realm 0".Capped by design, and the cap is visible. Volume scales with the session count, not with the engine, so a server at its 8192-session limit would otherwise spend the trace on rows nobody reads. The emission stops at a bound and kind 68's
reportedSessionsagainst itssessionssays what was left out.Gated on
SubscriptionsSessionTelemetryActive, separately from kind 68 for that reason.SystemReady = 4A system became ready (all predecessors completed). Payload:
systemIdx: u16,predecessorCount: u16.SystemSkipped = 5A system was skipped. Payload:
systemIdx: u16,skipReason: u8.ThreadContextSwitch = 254OS thread context-switch — closes one ON-CPU slice for a Typhon-registered thread. Instant-style record produced by
EtwSchedulingPump. Payload:u8 targetSlotIdx(slot to re-attribute to, NOT the producer slot),u8 processorNumber,u8 waitReason(ThreadWaitReason),u8 threadState(post-switch System.Diagnostics.ThreadState),u8 gettingIdle(1 = CPU went to System Idle next),u32 durationQpc(QPC ticks the slice held the CPU, capped at uint.MaxValue ≈ 7 min),u32 readyTimeQpc(QPC ticks on the ready queue before this slice; 0 = unknown, uint.MaxValue = saturated). Common-header timestamp = the slice's START QPC tick. Gated onRuntimeThreadSchedulingActive.ThreadInfo = 77Per-slot thread identity — emitted once when a producer thread claims its slot. Carries the managed thread ID and a UTF-8 thread name so the viewer can label lanes with something meaningful ("DagScheduler", "TyphonProfilerConsumer", pool worker name, ...) instead of just the numeric slot index. Wire layout after the 12-byte common header:
i32 managedThreadId,u16 nameByteCount,byte[nameByteCount] nameUtf8. Instant-style (no span-header extension); see IsSpan(TraceEventKind).TickEnd = 1Scheduler tick ended. Payload:
overloadLevel: u8,tickMultiplier: u8.TickStart = 0Scheduler tick started (empty payload). Emitted at the top of each
DagScheduler.RunTick.TransactionCommit = 20Transaction commit. Required:
tsn: i64. Optional:componentCount: i32,conflictDetected: bool.TransactionCommitComponent = 22Per-component commit sub-span. Required:
tsn: i64,componentTypeId: i32.TransactionPersist = 23WAL serialization inside Transaction.Commit. Required:
tsn: i64. Optional:walLsn: i64.TransactionRollback = 21Transaction rollback. Required:
tsn: i64. Optional:componentCount: i32.WalFlush = 80WAL writer drain-write-signal cycle. Required:
batchByteCount: i32,frameCount: i32,highLsn: i64.WalSegmentRotate = 81WAL segment rotation. Required:
newSegmentIndex: i32.WalWait = 82Thread blocked waiting for WAL durability. Required:
targetLsn: i64. Emitted on the calling thread, not the WAL writer.WriteTickFenceCluster = 61Per-tick span around the per-archetype body inside
WriteClusterTickFence. Covers both the has-dirty branch (snapshot + occupancy mask + shadow + spatial + WAL serialize + dormancy sweep) and the clean branch (dormancy sweep + optional spatial AABB refresh on local occupancy bitmap). Required payload:archetypeId: u16. Optional payload:dirtyClusterCount: i32,entryCount: i32,hasShadow: u8,hasSpatial: u8,walPublished: u8. Gated onRuntimeWriteTickFenceClusterActive.WriteTickFenceClusterShadow = 62Per-tick span around
ProcessClusterShadowEntriesfor one cluster-eligible archetype. Drains per-index shadow buffers for cluster-backed B+Tree indexes. Required payload:archetypeId: u16,dirtyClusterCount: i32. Optional payload:totalShadowEntries: i32. Gated onRuntimeWriteTickFenceClusterShadowActive.WriteTickFenceClusterSpatial = 63Per-tick span around the cluster spatial-maintenance block:
DetectClusterMigrations+ExecuteMigrations+RecomputeDirtyClusterAabbsfor one archetype with a Dynamic spatial slot. Parent of the fine-grained spans SpatialClusterMigrationDetectScan + SpatialClusterAabbRefresh + ClusterMigration. Required payload:archetypeId: u16,dirtyClusterCount: i32. Optional:migrationsExecuted: i32. Gated onRuntimeWriteTickFenceClusterSpatialActive.WriteTickFenceShadow = 252Per-tick span around
ProcessShadowEntriesfor one ComponentTable (deferred index maintenance for non-Versioned indexed fields with shadow buffers). Required payload:componentTypeId: u16,indexedFieldCount: i32. Optional payload:totalShadowEntries: i32(sum of buffer counts across all indexed fields). Gated onRuntimeWriteTickFenceShadowActive.WriteTickFenceSpatial = 253RETIRED (#872 step 13). Was: per-tick span around
ProcessSpatialEntriesfor one ComponentTable. That pass maintained the entity-level R-Tree, which no longer exists; the value is reserved, not reused.WriteTickFenceTable = 251Per-tick span around the per-ComponentTable body inside
WriteTickFenceCore(WAL serialize + shadow + spatial + archive). Fires once per dirty SV/Transient table per tick. Required payload:componentTypeId: u16,dirtyEntryCount: i32. Optional payload:walPublished: u8(0/1),hasShadow: u8(0/1),hasSpatial: u8(0/1). Gated onRuntimeWriteTickFenceTableActive.
Remarks
Wire stability: numeric values are part of the .typhon-trace file format. Never renumber or reuse an ID — append new entries only.
Gaps in numbering are intentional so related categories can grow contiguously without renumbering existing kinds.
Span vs instant: kinds < 10 are instant events — they carry only the 12-byte common header plus optional minimal payload, no span extension (no duration, no spanId, no parent linkage). Kinds ≥ 10 are span events — they include the 25-byte span header extension plus an optional 16-byte trace context and per-kind typed payload. Use IsSpan(TraceEventKind) for the range check.