namespace TravelEar.Core; /// /// Carries capture timestamps across a ring buffer hand-off (docs/DESIGN.md "Offset", /// REQ-OFFSET-MEASURE). The producer marks each block it writes with the ring position /// of the block's first sample, the block length, and the timestamp travelling with it; the /// consumer, on the other thread, resolves the position it is about to read to the timestamp of /// the newest mark whose span contains it. Positions may be monotonic (modulus 0) or wrapped /// ring indices (modulus = ring length): matching is done modulo the ring, so a read head that /// was jumped by a resync, or a ring that wrapped, still resolves to the right mark as long as /// every write marks (the newest mark always wins over a stale one from a ring ago). /// /// Timestamp (0) marks a block that carries no capture time (silence the /// mod generated): it resolves to "none" and, being newest, shadows older marks. /// /// /// Lock-free for the consumer: each slot is a seqlock, so a read torn by a concurrent write is /// detected and retried. Marks must come from one producer at a time (the callers serialize /// them); resolves may run on any single other thread. No allocation after construction. /// /// public sealed class FrameStampTable { public const long NoStamp = 0; public const int DefaultSlots = 16; private readonly long[] _version; private readonly long[] _position; private readonly int[] _length; private readonly long[] _timestamp; private int _next; // producer only private long _marks; // producer only; the version stamp public FrameStampTable(int slots = DefaultSlots) { if (slots < 1) throw new ArgumentOutOfRangeException(nameof(slots), slots, "Need at least one slot."); _version = new long[slots]; _position = new long[slots]; _length = new int[slots]; _timestamp = new long[slots]; } public int Slots => _version.Length; /// Marks made since construction. public long Marks => Volatile.Read(ref _marks); // [impl->REQ-OFFSET-MEASURE] /// Producer: records that the block starting at , samples long, carries . public void Mark(long position, int length, long timestamp) { if (length <= 0) throw new ArgumentOutOfRangeException(nameof(length), length, "A mark needs a positive length."); var i = _next; _next = (i + 1) % _version.Length; var stamp = (_marks + 1) * 2; // even = stable; odd = being written Interlocked.Exchange(ref _version[i], stamp - 1); // full fence: readers see "in progress" before any field _position[i] = position; _length[i] = length; _timestamp[i] = timestamp; Volatile.Write(ref _version[i], stamp); Volatile.Write(ref _marks, _marks + 1); } // [impl->REQ-OFFSET-MEASURE] /// /// Consumer: finds the newest mark whose span contains (modulo /// when it is positive). True with its timestamp when that mark /// carries one; false when no mark contains the position or the containing mark is /// . /// public bool TryResolve(long position, long modulus, out long timestamp) { timestamp = NoStamp; long bestVersion = 0; var bestTimestamp = NoStamp; for (var i = 0; i < _version.Length; i++) { for (var attempt = 0; attempt < 8; attempt++) { var v1 = Volatile.Read(ref _version[i]); if (v1 == 0) break; // never written if ((v1 & 1) != 0) continue; // being written: retry var pos = _position[i]; var len = _length[i]; var ts = _timestamp[i]; Interlocked.MemoryBarrier(); var v2 = Volatile.Read(ref _version[i]); if (v1 != v2) continue; // torn: retry if (v1 > bestVersion && Contains(pos, len, position, modulus)) { bestVersion = v1; bestTimestamp = ts; } break; } } if (bestVersion == 0 || bestTimestamp == NoStamp) return false; timestamp = bestTimestamp; return true; } private static bool Contains(long start, int length, long position, long modulus) { var d = position - start; if (modulus > 0) d = (d % modulus + modulus) % modulus; return d >= 0 && d < length; } }