Table of Contents

Class EventQueue<T>

Namespace
Typhon.Engine
Assembly
Typhon.Engine.dll

Typed event queue for inter-system communication. Producer systems push events; consumer systems drain them. Multi-producer / single-consumer: any number of workers may push concurrently, and the DAG guarantees every producer has completed before the consumer runs.

public sealed class EventQueue<T> : EventQueueBase

Type Parameters

T

The event type. No constraints — can be any struct or class.

Inheritance
EventQueue<T>
Inherited Members

Remarks

Per-worker segments, no atomics (#861). The queue owns one segment per worker slot, indexed by WorkerId. A worker only ever touches its own segment, so Push is a bounds-checked store and two plain increments — no Interlocked, no shared cache line. Correctness rests on slot disjointness, which DispatcherWorkerId establishes: no two threads that can run system code concurrently share a slot. Measured against the alternative (one shared tail advanced by Interlocked.Increment), segments are flat with worker count while the shared tail degrades roughly 545× at 8 producers.

Ordering. Drain(Span<T>) returns each slot's events in push order, slots in ascending order. Order ACROSS slots is not meaningful — which worker ran which chunk is not reproducible — so consumers must not depend on inter-event ordering.

Capacity is a hint, not a cap. A full segment doubles up to Capacity; beyond that a push is dropped and counted in OverflowCount. Growth happens on the owning worker, replacing only that worker's own buffer, and the high-water allocation survives Reset() — so a workload reaches its true working set within a few ticks and then never allocates again.

Visibility. Segment state is written with plain stores — a release per push is exactly the cost segmentation exists to avoid. The consumer instead issues ONE acquire fence per read (Typhon.Engine.EventQueue`1.AcquireSegments), which is free on x64 (JIT-folded) and an dmb ishld on arm64. That is necessary, not belt-and-braces: the scheduler's completion barrier is decremented with Interlocked but SPUN ON with a plain load, so without a fence here an arm64 reader may sink its segment loads above it and fold stale counts. Do not read a queue from outside system dispatch.

Constructors

EventQueue(string, int, bool)

Creates a new event queue.

public EventQueue(string name, int capacity = 1024, bool allowGrowth = true)

Parameters

name string

Diagnostic name for this queue.

capacity int

Expected events per tick across all workers. A power of 2. This is the initial allocation and the growth ceiling per worker segment — not a hard cap on the queue, which may grow to capacity per slot under skew.

allowGrowth bool

When false, a full segment drops instead of doubling — a fixed bound with loud telemetry, for callers who prefer it.

Properties

AllocatedCapacity

Items actually reserved across all segments right now — diagnostics only. Grows toward Capacity per segment.

public int AllocatedCapacity { get; }

Property Value

int

Capacity

Expected events per tick, as declared at construction — surfaced to the trace's Capacity for offline analysis (utilisation % against per-tick depth). Each worker segment grows on demand up to this figure, so a skewed tick can legitimately exceed it; see AllocatedCapacity for what is actually reserved.

public override int Capacity { get; }

Property Value

int

Remarks

The value passed at construction, unchanged. It must stay a construction-time constant: the profiler builds its one-shot EventQueueRecord catalog inside TyphonRuntime.Create, before the first tick and before any segment is allocated, and the Workbench divides per-tick depth by it. A fold over live buffers reported 0 for every queue in every trace.

Count

Number of items currently in the queue, summed across every worker slot.

public override int Count { get; }

Property Value

int

IsEmpty

True if the queue has no items.

public override bool IsEmpty { get; }

Property Value

bool

Remarks

O(1) in the overwhelmingly common "nothing was pushed" case; only a queue that saw traffic this tick pays the per-slot fold, and only because a mid-tick Drain can empty it again.

Name

Name of this event queue (for diagnostics).

public override string Name { get; }

Property Value

string

OverflowCount

Number of overflow events during the current tick — each is a Push dropped because the calling worker's segment was at its growth ceiling.

public override uint OverflowCount { get; }

Property Value

uint

PeakDepth

Maximum number of items observed in the queue at any point during the current tick.

public override uint PeakDepth { get; }

Property Value

uint

Remarks

A genuine queue-level high-water mark. Summing per-slot maxima was wrong in both directions: it added maxima observed at unrelated instants (slot 0 peaking at 100 and draining, then slot 1 doing the same, reported 200 for a queue that never held more than 100), and the partial-drain path never folded at all, under-reporting by 125x on a stackalloc[16] drain loop. Drain(Span<T>) now stamps total depth before removing anything, which is exact because Drain is single-consumer and single-threaded.

Produced

Number of Push calls that succeeded during the current tick.

public override uint Produced { get; }

Property Value

uint

Remarks

Derived, not accumulated. Every accepted push is either still live or has already been drained, and a dropped push never reaches a buffer — so Produced == Count + Consumed holds exactly. Maintaining a separate counter would add a second store to the hottest line in the engine to re-derive a number both operands already carry.

Methods

Drain(Span<T>)

Drains all events into the output span. Returns the number of events copied. After drain, the queue is empty.

public int Drain(Span<T> output)

Parameters

output Span<T>

Destination span. Must be large enough to hold Count events; a short span drains what fits and leaves the remainder in place.

Returns

int

Number of events copied.

Remarks

Single-consumer, and events arrive grouped by worker slot — see the ordering note on EventQueue<T>.

Reset()

Resets the queue to empty. Called at the start of each tick. Also clears the per-tick telemetry accumulators.

public override void Reset()