public sealed class RingBufferSinkThe always-on log sink: the last N records in a ring, in memory, in every build. The editor console reads it live and the crash reporter dumps it.
Remarks
A log that only exists on disk is a log nobody has when it matters — the interesting moment is usually the thirty seconds before a crash, and asking a player for a file is asking for nothing. A bounded ring costs a fixed amount of memory and always has that thirty seconds.
Per-category minimum levels come from LogFilter, so "turn on verbose asset loading without drowning in render spam" works. Rate limiting is available and off: everything else is a view of the log and can afford to lose repeats, whereas this is the record the crash reporter dumps, and a ring that dropped the four thousand identical lines before the crash would be hiding the shape of the failure.
Where this is not what doc 13 asks for, and why it stays that way. Records hold a formatted String, not UTF-8 bytes with the structured fields intact. An enabled line therefore allocates 128 bytes — an 88-byte LogRecord and the 40-byte message — against exactly zero disabled, both measured by AllocationTests with GetAllocatedBytesForCurrentThread rather than argued for here.
⚠ Packing bytes would move that cost, not remove it. The floor is set by ILogger.Log<TState>, not by this class: the generated state is a struct reachable only through IReadOnlyList<KeyValuePair<string, object?>>, so reading the structured fields boxes the state once and every value-type argument again — measured at 56 B/line for a single int — and the formatter's contract is to hand back a String, measured at 40 B/line on its own. Encoding that string into a byte ring copies it; it does not un-allocate it. Doc 13's "near-zero when enabled (the sink writes UTF-8 directly)" is reachable only by leaving [LoggerMessage] behind, which is what ZLoggerFileSink does and is ADR-008's decision to revisit, not this sink's.
⚠ And it would cost properties this shape has for free. The ring holds one reference per slot, so a wrap replaces exactly one whole record and no reader ever sees half of one; a byte ring wrapping mid-record leaves a fragment, and a fragment cut inside a multi-byte UTF-8 sequence is a decode error rather than a truncation. Exception is an object reference and cannot be packed at all without formatting it at write time, which allocates more than it saves. The editor console — which has since been written — collapses rows on the tuple (Level, Category, Message) and searches the message as text, so it would have to decode on read.
⚠ The claim that used to stand here — that logging never happens on a hot path, because [HotPath] methods are barred from it — was not true, and is still not. The attribute has an analyzer now (VXHP0001, #1161) and members marked with it, but that rule is about allocation and says nothing whatever about logging. What is actually true is weaker, and is the real reason the cost does not matter: logging does occur in per-frame code, and each such site is individually latched, watermarked, de-duplicated or interval-throttled, so its steady-state cost is a compare rather than a record.
Fields and properties (5)
public const int DefaultCapacityHow many records the ring holds when no capacity is given.
public int CapacityHow many records the ring holds.
public int CountHow many records it currently holds.
public long DroppedCountHow many records have been overwritten. Distinguishes "the log is missing its beginning" from "nothing was logged", which a bare ring cannot.
public long WrittenHow many records have ever reached the ring, the overwritten ones included.
Methods (6)
public RingBufferSink(int capacity = 100000, LogFilter? filter = null)Creates a sink holding at most records.
public LogRecord[] Snapshot()Copies the records out, oldest first.
public int CopyTail(Span<LogRecord> destination)Copies the newest records out, oldest of them first.
public int CopySince(ref long sequence, Span<LogRecord> destination)Copies the records written since a sequence number.
public void Clear()Empties the ring. Does not reset DroppedCount.
protected override void Write(LogRecord record)Writes one record wherever this sink writes.
Used by (21)
- AllocationTestsVixen.Core.Diagnostics.Tests
- AppBuilderVixen.App.Hosting
- AppServicesVixen.App.Hosting
- ContainerWiringTestsVixen.Ui.Tests
- DiagnosticOverlayTestsVixen.Engine.Tests
- DiagnosticsTestsVixen.Core.Diagnostics.Tests
- InertBindingTestsVixen.Ui.Tests
- LogOverlayVixen.Engine
- PerSurfaceMediaTestsVixen.Ui.Tests
- SinkTestsVixen.Core.Diagnostics.Tests
- StyleDiagnosticDrainTestsVixen.Ui.Tests
- ChromeFixtureVixen.Editor.Ui.Tests
- ConsoleModelVixen.Editor.Ui
- ConsoleTestsVixen.Editor.Ui.Tests
- ConsoleViewTestsVixen.Editor.Ui.Tests
- DeviceLossTestsVixen.App.Tests
- EditorApplicationVixen.Editor.App
- EditorLogVixen.Editor.App
- EditorWorldRendererTestsVixen.Editor.App.Tests
- FrameCaptureTestsVixen.App.Tests
- StartupSceneTestsVixen.App.Tests