Class EventQueue<T>
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
TThe 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
namestringDiagnostic name for this queue.
capacityintExpected 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
capacityper slot under skew.allowGrowthboolWhen 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
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
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
IsEmpty
True if the queue has no items.
public override bool IsEmpty { get; }
Property Value
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
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
PeakDepth
Maximum number of items observed in the queue at any point during the current tick.
public override uint PeakDepth { get; }
Property Value
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
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
outputSpan<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()