Telemetry & Runtime Inspection
Always-on, zero-allocation per-tick telemetry your game code can read directly โ no exporter required.
Status: ๐ง Partial ยท Visibility: Public ยท Level: ๐ฃ Advanced ยท Category: Runtime
๐ฏ What it solves
Game servers need to know now, from inside the process, whether the tick loop is healthy โ how long the last tick took, which systems are eating the budget, why a system didn't run. Waiting on an external metrics pipeline is too slow for an admin command or an in-game debug overlay, and too heavyweight if all you want is "did tick N overrun?". This feature gives every tick's vitals to game code as plain structs, with no allocation and no opt-in required.
โ๏ธ How it works (in brief)
Every tick, the scheduler writes one TickTelemetry record plus one SystemTelemetry record per system into a pre-allocated circular buffer (TickTelemetryRing, default 1024 ticks โ 17s at 60Hz). The ring is single-writer (tick driver thread) and safe for concurrent reads โ game code, an admin command, or a periodic logger can walk it from any thread. Only the most recent Capacity ticks are retained; reading a tick number outside that window throws. A second surface, IRuntimeInspector, is designed as a push-based hook (OnTickStart/OnSystemComplete/OnTickEnd) for future remote tooling (REST API, web explorer) โ it is not implemented; RuntimeOptions has no Inspector property today.
๐ป Usage
var ring = runtime.Telemetry;
// Inspect the most recent tick
ref readonly var tick = ref ring.GetTick(ring.NewestTick);
if (tick.OverrunRatio > 1.2f)
{
logger.LogWarning("Tick {N} overran: {Actual}ms / {Target}ms ({Level}, x{Mul})",
tick.TickNumber, tick.ActualDurationMs, tick.TargetDurationMs,
tick.CurrentLevel, tick.TickMultiplier);
}
// Per-system breakdown for that same tick
foreach (ref readonly var sys in ring.GetSystemMetrics(tick.TickNumber))
{
if (sys.WasSkipped)
{
Console.WriteLine($"system {sys.SystemIndex} skipped: {sys.SkipReason}");
}
}
// Walk recent history (oldest available -> newest)
for (var n = ring.OldestAvailableTick; n <= ring.NewestTick; n++)
{
ref readonly var t = ref ring.GetTick(n);
// aggregate, plot, export...
}
| Option | Default | Effect |
|---|---|---|
RuntimeOptions.TelemetryRingCapacity |
1024 | Ring size (must be a power of 2); trades retention window for memory (~capacity ร (32B + systemCount ร 48B)). |
โ ๏ธ Guarantees & limits
- Always on, zero allocation โ
Recordcopies into pre-allocated slots; there is no opt-in flag and no per-tick GC pressure. - Bounded retention โ only the last
TelemetryRingCapacityticks are kept;GetTick/GetSystemMetricsthrowArgumentOutOfRangeExceptiononce a tick scrolls out of the window. Read promptly if correlating with an external event (e.g. a player report). - Single writer โ only the tick driver thread calls
Record; readers get a consistent past-tick snapshot but must not assume the current in-flight tick's slot is stable. TickTelemetry.OverrunRatiois base-rate, not throttle-adjusted โ it is alwaysactual / target-at-1x, even whileTickMultiplier > 1. Use it to ask "are we over the engine's nominal budget", not "are we over our currently throttled budget".SystemTelemetry.StragglerGapUsis a placeholder today โ always0pending deeper Pipeline integration; don't rely on it for parallel-imbalance analysis yet.- No remote/out-of-process inspection โ
IRuntimeInspectoris designed but unimplemented; there is no REST endpoint or web explorer hook today. All access is in-process (game code, admin commands compiled into the server). - Reading is not free at scale โ
GetSystemMetricsreturns a span over a per-tick array sized to the full system count (engine-internal systems included); iterate only what you need on a hot path.
๐งช Tests
- TickTelemetryRingTests โ record/read round-trip, ring-wrap eviction of the oldest tick, out-of-window
GetTickthrows
๐ Related
- Related feature: Subscription Telemetry & Tracing