Table of Contents

Class TickTelemetryRing

Namespace
Typhon.Engine
Assembly
Typhon.Engine.dll

Pre-allocated circular buffer for tick telemetry. Zero allocation on Record(in TickTelemetry, ReadOnlySpan<SystemTelemetry>). Single writer (tick driver thread), multiple readers (diagnostics, metrics).

public sealed class TickTelemetryRing
Inheritance
TickTelemetryRing
Inherited Members

Remarks

Default capacity: 1024 entries (~17 seconds at 60Hz). Memory: 1024 × (sizeof(TickTelemetry) + systemCount × sizeof(SystemTelemetry)). For a 20-system DAG: ~1024 × (32 + 20 × 48) ≈ ~1 MB.

Constructors

TickTelemetryRing(int, int)

Creates a new telemetry ring buffer.

public TickTelemetryRing(int capacity, int systemCount)

Parameters

capacity int

Must be a power of 2.

systemCount int

Number of systems in the DAG.

Properties

Capacity

Ring buffer capacity.

public int Capacity { get; }

Property Value

int

NewestTick

Tick number of the newest recorded entry, or -1 if no ticks recorded yet.

public long NewestTick { get; }

Property Value

long

OldestAvailableTick

Tick number of the oldest available entry, or -1 if no ticks recorded yet.

public long OldestAvailableTick { get; }

Property Value

long

TotalTicksRecorded

Number of ticks recorded so far (may exceed capacity — only last Capacity are retained).

public long TotalTicksRecorded { get; }

Property Value

long

Methods

GetSystemMetrics(long)

Returns the per-system telemetry for a given tick number.

public ReadOnlySpan<SystemTelemetry> GetSystemMetrics(long tickNumber)

Parameters

tickNumber long

Absolute tick number.

Returns

ReadOnlySpan<SystemTelemetry>

Span over the system metrics array for this tick.

Exceptions

ArgumentOutOfRangeException

Tick is not in the buffer.

GetTick(long)

Returns the tick telemetry for a given tick number.

public ref readonly TickTelemetry GetTick(long tickNumber)

Parameters

tickNumber long

Absolute tick number.

Returns

TickTelemetry

Reference to the stored telemetry. Only valid if the tick is still in the buffer.

Exceptions

ArgumentOutOfRangeException

Tick is not in the buffer (too old or not yet recorded).

Record(in TickTelemetry, ReadOnlySpan<SystemTelemetry>)

Records a tick's telemetry data into the ring buffer. Called at the end of each tick by the scheduler. Zero allocation — copies data into pre-allocated slots.

public void Record(in TickTelemetry tick, ReadOnlySpan<SystemTelemetry> systems)

Parameters

tick TickTelemetry

Tick-level telemetry.

systems ReadOnlySpan<SystemTelemetry>

Per-system telemetry for this tick. Length must equal systemCount.

TryGetRange(long, out long, out long)

The readable tick range at or after fromInclusive, or false when there is none.

public bool TryGetRange(long fromInclusive, out long first, out long last)

Parameters

fromInclusive long

The oldest tick the caller wants. Clamped up to OldestAvailableTick; the caller owns what "the window" means, so this takes an absolute tick rather than a width and never reinterprets one.

first long

The oldest readable tick in the range. Undefined when this returns false.

last long

The newest readable tick in the range — NewestTick. Undefined when this returns false.

Returns

bool

true when for (var t = first; t <= last; t++) is safe to pass to GetTick(long).

Remarks

Every window pass over this ring should start here, because the hand-written form has a trap and three shipped copies of it fell in. OldestAvailableTick and NewestTick both report -1 while the ring is empty, so the natural for (var t = Math.Max(oldest, tick - window); t <= newest; t++) evaluates to t = -1; -1 <= -1 and calls GetTick(long)(-1), which throws. Empty is not an edge case: the ring is written at the END of a tick, so it is the state every consumer sees on tick 0.

That throw is unusually destructive on a tick-path consumer and is why this exists rather than a note in each caller. StatsEncoder's three passes are reached from SubscriptionsContext.Reset, which runs BEFORE the ring is written — so the throw stopped the recording that would have made the next tick's call legal, and the condition sustained itself for every tick of the run. It surfaced as 508 ticks and not one telemetry record emitted, with every gate and null check passing; not as an exception anyone saw.

TryGetSystemMetrics(long, out ReadOnlySpan<SystemTelemetry>)

One tick's per-system telemetry, or false when the ring no longer holds it. The non-throwing peer of GetSystemMetrics(long).

public bool TryGetSystemMetrics(long tickNumber, out ReadOnlySpan<SystemTelemetry> systems)

Parameters

tickNumber long

The tick to read.

systems ReadOnlySpan<SystemTelemetry>

The tick's per-system metrics. Empty when this returns false.

Returns

bool

false when tickNumber has been evicted, or has not been recorded.

Remarks

Paired with TryGetTick(long, out TickTelemetry) for the same reason — see its remarks. A walk that guards one and not the other still throws.

TryGetTick(long, out TickTelemetry)

One tick's telemetry, or false when the ring no longer holds it. The non-throwing peer of GetTick(long).

public bool TryGetTick(long tickNumber, out TickTelemetry tick)

Parameters

tickNumber long

The tick to read.

tick TickTelemetry

The tick's telemetry. default when this returns false.

Returns

bool

false when tickNumber has been evicted, or has not been recorded.

Remarks

A range from TryGetRange(long, out long, out long) is not a promise that every tick in it still exists when you get there. The clamp is a snapshot and the validation is live: the tick driver is a concurrent writer, so on a window as wide as Capacity the oldest end clamps EXACTLY to OldestAvailableTick, and a single tick recorded between resolving the range and reading its first element evicts it. GetTick(long) then throws — out of whatever public method was reading, which for TyphonRuntime.ReadStats means an operator's stats call fails rather than returning a slightly short window.

A reader walking a range wants that skipped, not raised: losing the oldest sample of a percentile is the tearing such a reader already accepts by reading a live ring at all, and it is strictly less wrong than no answer. GetTick(long) stays for a caller naming one tick it believes is present, where a throw is the right answer to a bug.