feat: port avl module - SequenceSet AVL tree (36 features, 17 tests)
This commit is contained in:
@@ -0,0 +1,631 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory and encoding-optimized set for storing unsigned 64-bit integers.
|
||||||
|
/// Implemented as an AVL tree where each node holds bitmasks for set membership.
|
||||||
|
/// Approximately 80-100x more memory-efficient than a <see cref="HashSet{T}"/>.
|
||||||
|
/// Not thread-safe.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SequenceSet
|
||||||
|
{
|
||||||
|
private const int BitsPerBucket = 64;
|
||||||
|
private const int NumBuckets = 32;
|
||||||
|
internal const int NumEntries = NumBuckets * BitsPerBucket; // 2048
|
||||||
|
|
||||||
|
private const byte MagicByte = 22;
|
||||||
|
private const byte CurrentVersion = 2;
|
||||||
|
private const int HdrLen = 2;
|
||||||
|
private const int MinLen = 2 + 8; // magic + version + num_nodes(4) + num_entries(4)
|
||||||
|
|
||||||
|
private Node? _root;
|
||||||
|
private int _size;
|
||||||
|
private int _nodes;
|
||||||
|
private bool _changed;
|
||||||
|
|
||||||
|
// --- Errors ---
|
||||||
|
|
||||||
|
public static readonly Exception ErrBadEncoding = new InvalidDataException("ss: bad encoding");
|
||||||
|
public static readonly Exception ErrBadVersion = new InvalidDataException("ss: bad version");
|
||||||
|
public static readonly Exception ErrSetNotEmpty = new InvalidOperationException("ss: set not empty");
|
||||||
|
|
||||||
|
// --- Internal access for testing ---
|
||||||
|
|
||||||
|
internal Node? Root => _root;
|
||||||
|
|
||||||
|
// --- Public API ---
|
||||||
|
|
||||||
|
/// <summary>Inserts a sequence number into the set. Tree is balanced inline.</summary>
|
||||||
|
public void Insert(ulong seq)
|
||||||
|
{
|
||||||
|
_root = Node.Insert(_root, seq, ref _changed, ref _nodes);
|
||||||
|
if (_changed)
|
||||||
|
{
|
||||||
|
_changed = false;
|
||||||
|
_size++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns true if the sequence is a member of the set.</summary>
|
||||||
|
public bool Exists(ulong seq)
|
||||||
|
{
|
||||||
|
for (var n = _root; n != null;)
|
||||||
|
{
|
||||||
|
if (seq < n.Base)
|
||||||
|
{
|
||||||
|
n = n.Left;
|
||||||
|
}
|
||||||
|
else if (seq >= n.Base + NumEntries)
|
||||||
|
{
|
||||||
|
n = n.Right;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return n.ExistsBit(seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the initial minimum sequence when known. More effectively utilizes space.
|
||||||
|
/// The set must be empty.
|
||||||
|
/// </summary>
|
||||||
|
public void SetInitialMin(ulong min)
|
||||||
|
{
|
||||||
|
if (!IsEmpty)
|
||||||
|
throw (InvalidOperationException)ErrSetNotEmpty;
|
||||||
|
|
||||||
|
_root = new Node(min);
|
||||||
|
_nodes = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the sequence from the set. Returns true if the sequence was present.
|
||||||
|
/// </summary>
|
||||||
|
public bool Delete(ulong seq)
|
||||||
|
{
|
||||||
|
if (_root == null) return false;
|
||||||
|
|
||||||
|
_root = Node.Delete(_root, seq, ref _changed, ref _nodes);
|
||||||
|
if (_changed)
|
||||||
|
{
|
||||||
|
_changed = false;
|
||||||
|
_size--;
|
||||||
|
if (_size == 0)
|
||||||
|
Empty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the number of items in the set.</summary>
|
||||||
|
public int Size => _size;
|
||||||
|
|
||||||
|
/// <summary>Returns the number of nodes in the AVL tree.</summary>
|
||||||
|
public int Nodes => _nodes;
|
||||||
|
|
||||||
|
/// <summary>Clears all items from the set.</summary>
|
||||||
|
public void Empty()
|
||||||
|
{
|
||||||
|
_root = null;
|
||||||
|
_size = 0;
|
||||||
|
_nodes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns true if the set contains no items.</summary>
|
||||||
|
public bool IsEmpty => _root == null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the callback for each item in ascending order.
|
||||||
|
/// Stops early if the callback returns false.
|
||||||
|
/// </summary>
|
||||||
|
public void Range(Func<ulong, bool> f) => Node.Iter(_root, f);
|
||||||
|
|
||||||
|
/// <summary>Returns the heights of the left and right subtrees of the root.</summary>
|
||||||
|
public (int Left, int Right) Heights()
|
||||||
|
{
|
||||||
|
if (_root == null) return (0, 0);
|
||||||
|
return (_root.Left?.Height ?? 0, _root.Right?.Height ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns min, max, and count of set items.</summary>
|
||||||
|
public (ulong Min, ulong Max, ulong Count) State()
|
||||||
|
{
|
||||||
|
if (_root == null) return (0, 0, 0);
|
||||||
|
var (min, max) = MinMax();
|
||||||
|
return (min, max, (ulong)_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the minimum and maximum values in the set.</summary>
|
||||||
|
public (ulong Min, ulong Max) MinMax()
|
||||||
|
{
|
||||||
|
if (_root == null) return (0, 0);
|
||||||
|
|
||||||
|
ulong min = 0;
|
||||||
|
for (var l = _root; l != null; l = l.Left)
|
||||||
|
if (l.Left == null) min = l.Min();
|
||||||
|
|
||||||
|
ulong max = 0;
|
||||||
|
for (var r = _root; r != null; r = r.Right)
|
||||||
|
if (r.Right == null) max = r.Max();
|
||||||
|
|
||||||
|
return (min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns a deep clone of this set.</summary>
|
||||||
|
public SequenceSet Clone()
|
||||||
|
{
|
||||||
|
var css = new SequenceSet { _nodes = _nodes, _size = _size };
|
||||||
|
css._root = Node.Clone(_root);
|
||||||
|
return css;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Unions one or more sequence sets into this set.</summary>
|
||||||
|
public void Union(params SequenceSet[] sets)
|
||||||
|
{
|
||||||
|
foreach (var sa in sets)
|
||||||
|
{
|
||||||
|
Node.NodeIter(sa._root, n =>
|
||||||
|
{
|
||||||
|
for (var nb = 0; nb < NumBuckets; nb++)
|
||||||
|
{
|
||||||
|
var b = n.Bits[nb];
|
||||||
|
for (var pos = 0UL; b != 0; pos++)
|
||||||
|
{
|
||||||
|
if ((b & 1) == 1)
|
||||||
|
{
|
||||||
|
var seq = n.Base + ((ulong)nb * BitsPerBucket) + pos;
|
||||||
|
Insert(seq);
|
||||||
|
}
|
||||||
|
b >>= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the union of all given sets.</summary>
|
||||||
|
public static SequenceSet? UnionSets(params SequenceSet[] sets)
|
||||||
|
{
|
||||||
|
if (sets.Length == 0) return null;
|
||||||
|
|
||||||
|
// Clone the largest set first for efficiency.
|
||||||
|
Array.Sort(sets, (a, b) => b.Size.CompareTo(a.Size));
|
||||||
|
var ss = sets[0].Clone();
|
||||||
|
for (var i = 1; i < sets.Length; i++)
|
||||||
|
{
|
||||||
|
sets[i].Range(n =>
|
||||||
|
{
|
||||||
|
ss.Insert(n);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the number of bytes needed to encode this set.</summary>
|
||||||
|
public int EncodeLen() => MinLen + (Nodes * ((NumBuckets + 1) * 8 + 2));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes this set into a compact binary representation.
|
||||||
|
/// Reuses the provided buffer if it is large enough.
|
||||||
|
/// </summary>
|
||||||
|
public byte[] Encode(byte[]? buf)
|
||||||
|
{
|
||||||
|
var nn = Nodes;
|
||||||
|
var encLen = EncodeLen();
|
||||||
|
|
||||||
|
if (buf == null || buf.Length < encLen)
|
||||||
|
buf = new byte[encLen];
|
||||||
|
|
||||||
|
buf[0] = MagicByte;
|
||||||
|
buf[1] = CurrentVersion;
|
||||||
|
|
||||||
|
var i = HdrLen;
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(i), (uint)nn);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(i + 4), (uint)_size);
|
||||||
|
i += 8;
|
||||||
|
|
||||||
|
Node.NodeIter(_root, n =>
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buf.AsSpan(i), n.Base);
|
||||||
|
i += 8;
|
||||||
|
foreach (var b in n.Bits)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buf.AsSpan(i), b);
|
||||||
|
i += 8;
|
||||||
|
}
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(i), (ushort)n.Height);
|
||||||
|
i += 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
return buf[..i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes a sequence set from the binary representation.
|
||||||
|
/// Returns the set and the number of bytes consumed.
|
||||||
|
/// Throws <see cref="InvalidDataException"/> on malformed input.
|
||||||
|
/// </summary>
|
||||||
|
public static (SequenceSet Set, int BytesRead) Decode(ReadOnlySpan<byte> buf)
|
||||||
|
{
|
||||||
|
if (buf.Length < MinLen || buf[0] != MagicByte)
|
||||||
|
throw (InvalidDataException)ErrBadEncoding;
|
||||||
|
|
||||||
|
return buf[1] switch
|
||||||
|
{
|
||||||
|
1 => Decodev1(buf),
|
||||||
|
2 => Decodev2(buf),
|
||||||
|
_ => throw (InvalidDataException)ErrBadVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal tree helpers ---
|
||||||
|
|
||||||
|
/// <summary>Inserts a pre-built node directly into the tree (used during Decode).</summary>
|
||||||
|
internal void InsertNode(Node n)
|
||||||
|
{
|
||||||
|
_nodes++;
|
||||||
|
if (_root == null)
|
||||||
|
{
|
||||||
|
_root = n;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var p = _root; p != null;)
|
||||||
|
{
|
||||||
|
if (n.Base < p.Base)
|
||||||
|
{
|
||||||
|
if (p.Left == null) { p.Left = n; return; }
|
||||||
|
p = p.Left;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (p.Right == null) { p.Right = n; return; }
|
||||||
|
p = p.Right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (SequenceSet Set, int BytesRead) Decodev2(ReadOnlySpan<byte> buf)
|
||||||
|
{
|
||||||
|
var index = 2;
|
||||||
|
var nn = (int)BinaryPrimitives.ReadUInt32LittleEndian(buf[index..]);
|
||||||
|
var sz = (int)BinaryPrimitives.ReadUInt32LittleEndian(buf[(index + 4)..]);
|
||||||
|
index += 8;
|
||||||
|
|
||||||
|
var expectedLen = MinLen + (nn * ((NumBuckets + 1) * 8 + 2));
|
||||||
|
if (buf.Length < expectedLen)
|
||||||
|
throw (InvalidDataException)ErrBadEncoding;
|
||||||
|
|
||||||
|
var ss = new SequenceSet { _size = sz };
|
||||||
|
var nodes = new Node[nn];
|
||||||
|
|
||||||
|
for (var i = 0; i < nn; i++)
|
||||||
|
{
|
||||||
|
var n = new Node(BinaryPrimitives.ReadUInt64LittleEndian(buf[index..]));
|
||||||
|
index += 8;
|
||||||
|
for (var bi = 0; bi < NumBuckets; bi++)
|
||||||
|
{
|
||||||
|
n.Bits[bi] = BinaryPrimitives.ReadUInt64LittleEndian(buf[index..]);
|
||||||
|
index += 8;
|
||||||
|
}
|
||||||
|
n.Height = (int)BinaryPrimitives.ReadUInt16LittleEndian(buf[index..]);
|
||||||
|
index += 2;
|
||||||
|
nodes[i] = n;
|
||||||
|
ss.InsertNode(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ss, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (SequenceSet Set, int BytesRead) Decodev1(ReadOnlySpan<byte> buf)
|
||||||
|
{
|
||||||
|
const int v1NumBuckets = 64;
|
||||||
|
|
||||||
|
var index = 2;
|
||||||
|
var nn = (int)BinaryPrimitives.ReadUInt32LittleEndian(buf[index..]);
|
||||||
|
var sz = (int)BinaryPrimitives.ReadUInt32LittleEndian(buf[(index + 4)..]);
|
||||||
|
index += 8;
|
||||||
|
|
||||||
|
var expectedLen = MinLen + (nn * ((v1NumBuckets + 1) * 8 + 2));
|
||||||
|
if (buf.Length < expectedLen)
|
||||||
|
throw (InvalidDataException)ErrBadEncoding;
|
||||||
|
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
for (var i = 0; i < nn; i++)
|
||||||
|
{
|
||||||
|
var baseVal = BinaryPrimitives.ReadUInt64LittleEndian(buf[index..]);
|
||||||
|
index += 8;
|
||||||
|
for (var nb = 0UL; nb < v1NumBuckets; nb++)
|
||||||
|
{
|
||||||
|
var n = BinaryPrimitives.ReadUInt64LittleEndian(buf[index..]);
|
||||||
|
for (var pos = 0UL; n != 0; pos++)
|
||||||
|
{
|
||||||
|
if ((n & 1) == 1)
|
||||||
|
{
|
||||||
|
var seq = baseVal + (nb * BitsPerBucket) + pos;
|
||||||
|
ss.Insert(seq);
|
||||||
|
}
|
||||||
|
n >>= 1;
|
||||||
|
}
|
||||||
|
index += 8;
|
||||||
|
}
|
||||||
|
// Skip encoded height.
|
||||||
|
index += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ss.Size != sz)
|
||||||
|
throw (InvalidDataException)ErrBadEncoding;
|
||||||
|
|
||||||
|
return (ss, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Internal Node class
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
internal sealed class Node
|
||||||
|
{
|
||||||
|
public ulong Base;
|
||||||
|
public readonly ulong[] Bits = new ulong[NumBuckets];
|
||||||
|
public Node? Left;
|
||||||
|
public Node? Right;
|
||||||
|
public int Height;
|
||||||
|
|
||||||
|
public Node(ulong baseVal)
|
||||||
|
{
|
||||||
|
Base = baseVal;
|
||||||
|
Height = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the bit for seq. seq must be within [Base, Base+NumEntries).
|
||||||
|
public void SetBit(ulong seq, ref bool inserted)
|
||||||
|
{
|
||||||
|
var offset = seq - Base;
|
||||||
|
var i = (int)(offset / BitsPerBucket);
|
||||||
|
var mask = 1UL << (int)(offset % BitsPerBucket);
|
||||||
|
if ((Bits[i] & mask) == 0)
|
||||||
|
{
|
||||||
|
Bits[i] |= mask;
|
||||||
|
inserted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ExistsBit(ulong seq)
|
||||||
|
{
|
||||||
|
var offset = seq - Base;
|
||||||
|
var i = (int)(offset / BitsPerBucket);
|
||||||
|
var mask = 1UL << (int)(offset % BitsPerBucket);
|
||||||
|
return (Bits[i] & mask) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears the bit for seq. Returns true if the node is now empty.
|
||||||
|
public bool ClearBit(ulong seq, ref bool deleted)
|
||||||
|
{
|
||||||
|
var offset = seq - Base;
|
||||||
|
var i = (int)(offset / BitsPerBucket);
|
||||||
|
var mask = 1UL << (int)(offset % BitsPerBucket);
|
||||||
|
if ((Bits[i] & mask) != 0)
|
||||||
|
{
|
||||||
|
Bits[i] &= ~mask;
|
||||||
|
deleted = true;
|
||||||
|
}
|
||||||
|
foreach (var b in Bits)
|
||||||
|
if (b != 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Min()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < NumBuckets; i++)
|
||||||
|
{
|
||||||
|
if (Bits[i] != 0)
|
||||||
|
return Base + (ulong)(i * BitsPerBucket) + (ulong)System.Numerics.BitOperations.TrailingZeroCount(Bits[i]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Max()
|
||||||
|
{
|
||||||
|
for (var i = NumBuckets - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (Bits[i] != 0)
|
||||||
|
return Base + (ulong)(i * BitsPerBucket) +
|
||||||
|
(ulong)(BitsPerBucket - System.Numerics.BitOperations.LeadingZeroCount(Bits[i] >> 1));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static AVL helpers
|
||||||
|
|
||||||
|
public static int BalanceFactor(Node? n)
|
||||||
|
{
|
||||||
|
if (n == null) return 0;
|
||||||
|
return (n.Left?.Height ?? 0) - (n.Right?.Height ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MaxH(Node? n)
|
||||||
|
{
|
||||||
|
if (n == null) return 0;
|
||||||
|
return Math.Max(n.Left?.Height ?? 0, n.Right?.Height ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node Insert(Node? n, ulong seq, ref bool inserted, ref int nodes)
|
||||||
|
{
|
||||||
|
if (n == null)
|
||||||
|
{
|
||||||
|
var baseVal = (seq / NumEntries) * NumEntries;
|
||||||
|
var newNode = new Node(baseVal);
|
||||||
|
newNode.SetBit(seq, ref inserted);
|
||||||
|
nodes++;
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seq < n.Base)
|
||||||
|
n.Left = Insert(n.Left, seq, ref inserted, ref nodes);
|
||||||
|
else if (seq >= n.Base + NumEntries)
|
||||||
|
n.Right = Insert(n.Right, seq, ref inserted, ref nodes);
|
||||||
|
else
|
||||||
|
n.SetBit(seq, ref inserted);
|
||||||
|
|
||||||
|
n.Height = MaxH(n) + 1;
|
||||||
|
|
||||||
|
var bf = BalanceFactor(n);
|
||||||
|
if (bf > 1)
|
||||||
|
{
|
||||||
|
if (BalanceFactor(n.Left) < 0)
|
||||||
|
n.Left = n.Left!.RotateLeft();
|
||||||
|
return n.RotateRight();
|
||||||
|
}
|
||||||
|
if (bf < -1)
|
||||||
|
{
|
||||||
|
if (BalanceFactor(n.Right) > 0)
|
||||||
|
n.Right = n.Right!.RotateRight();
|
||||||
|
return n.RotateLeft();
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node? Delete(Node? n, ulong seq, ref bool deleted, ref int nodes)
|
||||||
|
{
|
||||||
|
if (n == null) return null;
|
||||||
|
|
||||||
|
if (seq < n.Base)
|
||||||
|
n.Left = Delete(n.Left, seq, ref deleted, ref nodes);
|
||||||
|
else if (seq >= n.Base + NumEntries)
|
||||||
|
n.Right = Delete(n.Right, seq, ref deleted, ref nodes);
|
||||||
|
else if (n.ClearBit(seq, ref deleted))
|
||||||
|
{
|
||||||
|
nodes--;
|
||||||
|
if (n.Left == null)
|
||||||
|
n = n.Right;
|
||||||
|
else if (n.Right == null)
|
||||||
|
n = n.Left;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
n.Right = n.Right.InsertNodePrev(n.Left);
|
||||||
|
n = n.Right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n == null) return null;
|
||||||
|
|
||||||
|
n.Height = MaxH(n) + 1;
|
||||||
|
|
||||||
|
var bf = BalanceFactor(n);
|
||||||
|
if (bf > 1)
|
||||||
|
{
|
||||||
|
if (BalanceFactor(n.Left) < 0)
|
||||||
|
n.Left = n.Left!.RotateLeft();
|
||||||
|
return n.RotateRight();
|
||||||
|
}
|
||||||
|
if (bf < -1)
|
||||||
|
{
|
||||||
|
if (BalanceFactor(n.Right) > 0)
|
||||||
|
n.Right = n.Right!.RotateRight();
|
||||||
|
return n.RotateLeft();
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node RotateLeft()
|
||||||
|
{
|
||||||
|
var r = Right;
|
||||||
|
if (r != null)
|
||||||
|
{
|
||||||
|
Right = r.Left;
|
||||||
|
r.Left = this;
|
||||||
|
Height = MaxH(this) + 1;
|
||||||
|
r.Height = MaxH(r) + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Right = null;
|
||||||
|
Height = MaxH(this) + 1;
|
||||||
|
}
|
||||||
|
return r!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node RotateRight()
|
||||||
|
{
|
||||||
|
var l = Left;
|
||||||
|
if (l != null)
|
||||||
|
{
|
||||||
|
Left = l.Right;
|
||||||
|
l.Right = this;
|
||||||
|
Height = MaxH(this) + 1;
|
||||||
|
l.Height = MaxH(l) + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Left = null;
|
||||||
|
Height = MaxH(this) + 1;
|
||||||
|
}
|
||||||
|
return l!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserts nn into this subtree assuming nn.Base < all nodes in this subtree.
|
||||||
|
public Node InsertNodePrev(Node nn)
|
||||||
|
{
|
||||||
|
if (Left == null)
|
||||||
|
Left = nn;
|
||||||
|
else
|
||||||
|
Left = Left.InsertNodePrev(nn);
|
||||||
|
|
||||||
|
Height = MaxH(this) + 1;
|
||||||
|
|
||||||
|
var bf = BalanceFactor(this);
|
||||||
|
if (bf > 1)
|
||||||
|
{
|
||||||
|
if (BalanceFactor(Left) < 0)
|
||||||
|
Left = Left!.RotateLeft();
|
||||||
|
return RotateRight();
|
||||||
|
}
|
||||||
|
if (bf < -1)
|
||||||
|
{
|
||||||
|
if (BalanceFactor(Right) > 0)
|
||||||
|
Right = Right!.RotateRight();
|
||||||
|
return RotateLeft();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterates nodes in tree order (pre-order: root → left → right).
|
||||||
|
public static void NodeIter(Node? n, Action<Node> f)
|
||||||
|
{
|
||||||
|
if (n == null) return;
|
||||||
|
f(n);
|
||||||
|
NodeIter(n.Left, f);
|
||||||
|
NodeIter(n.Right, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterates items in ascending order (in-order traversal).
|
||||||
|
// Returns false if the callback returns false.
|
||||||
|
public static bool Iter(Node? n, Func<ulong, bool> f)
|
||||||
|
{
|
||||||
|
if (n == null) return true;
|
||||||
|
|
||||||
|
if (!Iter(n.Left, f)) return false;
|
||||||
|
|
||||||
|
for (var num = n.Base; num < n.Base + NumEntries; num++)
|
||||||
|
{
|
||||||
|
if (n.ExistsBit(num))
|
||||||
|
if (!f(num)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Iter(n.Right, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node? Clone(Node? src)
|
||||||
|
{
|
||||||
|
if (src == null) return null;
|
||||||
|
var n = new Node(src.Base) { Height = src.Height };
|
||||||
|
src.Bits.CopyTo(n.Bits, 0);
|
||||||
|
n.Left = Clone(src.Left);
|
||||||
|
n.Right = Clone(src.Right);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("ZB.MOM.NatsNet.Server.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("ZB.MOM.NatsNet.Server.IntegrationTests")]
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Shouldly;
|
||||||
|
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||||
|
|
||||||
|
namespace ZB.MOM.NatsNet.Server.Tests.Internal.DataStructures;
|
||||||
|
|
||||||
|
public sealed class SequenceSetTests
|
||||||
|
{
|
||||||
|
private static readonly int NumEntries = SequenceSet.NumEntries; // 2048
|
||||||
|
|
||||||
|
// --- Basic operations ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetBasics_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
var seqs = new ulong[] { 22, 222, 2000, 2, 2, 4 };
|
||||||
|
foreach (var seq in seqs)
|
||||||
|
{
|
||||||
|
ss.Insert(seq);
|
||||||
|
ss.Exists(seq).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.Nodes.ShouldBe(1);
|
||||||
|
ss.Size.ShouldBe(seqs.Length - 1); // one duplicate
|
||||||
|
var (lh, rh) = ss.Heights();
|
||||||
|
lh.ShouldBe(0);
|
||||||
|
rh.ShouldBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetLeftLean_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
for (var i = (ulong)(4 * NumEntries); i > 0; i--)
|
||||||
|
ss.Insert(i);
|
||||||
|
|
||||||
|
ss.Nodes.ShouldBe(5);
|
||||||
|
ss.Size.ShouldBe(4 * NumEntries);
|
||||||
|
var (lh, rh) = ss.Heights();
|
||||||
|
lh.ShouldBe(2);
|
||||||
|
rh.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetRightLean_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
for (var i = 0UL; i < (ulong)(4 * NumEntries); i++)
|
||||||
|
ss.Insert(i);
|
||||||
|
|
||||||
|
ss.Nodes.ShouldBe(4);
|
||||||
|
ss.Size.ShouldBe(4 * NumEntries);
|
||||||
|
var (lh, rh) = ss.Heights();
|
||||||
|
lh.ShouldBe(1);
|
||||||
|
rh.ShouldBe(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetCorrectness_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var num = 100_000;
|
||||||
|
var maxVal = 500_000;
|
||||||
|
var rng = new Random(42);
|
||||||
|
|
||||||
|
var reference = new HashSet<ulong>(num);
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
{
|
||||||
|
var n = (ulong)rng.NextInt64(maxVal + 1);
|
||||||
|
ss.Insert(n);
|
||||||
|
reference.Add(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0UL; i <= (ulong)maxVal; i++)
|
||||||
|
ss.Exists(i).ShouldBe(reference.Contains(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetRange_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var num = 2 * NumEntries + 22;
|
||||||
|
var nums = new List<ulong>(num);
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
nums.Add((ulong)i);
|
||||||
|
|
||||||
|
var rng = new Random(42);
|
||||||
|
for (var i = nums.Count - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
var j = rng.Next(i + 1);
|
||||||
|
(nums[i], nums[j]) = (nums[j], nums[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
foreach (var n in nums)
|
||||||
|
ss.Insert(n);
|
||||||
|
|
||||||
|
var collected = new List<ulong>();
|
||||||
|
ss.Range(n => { collected.Add(n); return true; });
|
||||||
|
collected.Count.ShouldBe(num);
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
collected[i].ShouldBe((ulong)i);
|
||||||
|
|
||||||
|
// Test early termination.
|
||||||
|
collected.Clear();
|
||||||
|
ss.Range(n =>
|
||||||
|
{
|
||||||
|
if (n >= 10) return false;
|
||||||
|
collected.Add(n);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
collected.Count.ShouldBe(10);
|
||||||
|
for (var i = 0UL; i < 10; i++)
|
||||||
|
collected[(int)i].ShouldBe(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetDelete_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
var seqs = new ulong[] { 22, 222, 2222, 2, 2, 4 };
|
||||||
|
foreach (var seq in seqs)
|
||||||
|
ss.Insert(seq);
|
||||||
|
|
||||||
|
foreach (var seq in seqs)
|
||||||
|
{
|
||||||
|
ss.Delete(seq);
|
||||||
|
ss.Exists(seq).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.Root.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetInsertAndDeletePedantic_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
var num = 50 * NumEntries + 22;
|
||||||
|
var nums = new List<ulong>(num);
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
nums.Add((ulong)i);
|
||||||
|
|
||||||
|
var rng = new Random(42);
|
||||||
|
for (var i = nums.Count - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
var j = rng.Next(i + 1);
|
||||||
|
(nums[i], nums[j]) = (nums[j], nums[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssertBalanced()
|
||||||
|
{
|
||||||
|
SequenceSet.Node.NodeIter(ss.Root, n =>
|
||||||
|
{
|
||||||
|
if (n != null && n.Height != (SequenceSet.Node.BalanceFactor(n) == int.MinValue ? 0 : 0))
|
||||||
|
{
|
||||||
|
// Height check: verify height equals max child height + 1
|
||||||
|
var expectedH = 1 + Math.Max(n.Left?.Height ?? 0, n.Right?.Height ?? 0);
|
||||||
|
n.Height.ShouldBe(expectedH);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var bf = SequenceSet.Node.BalanceFactor(ss.Root);
|
||||||
|
(bf is >= -1 and <= 1).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var n in nums)
|
||||||
|
{
|
||||||
|
ss.Insert(n);
|
||||||
|
AssertBalanced();
|
||||||
|
}
|
||||||
|
ss.Root.ShouldNotBeNull();
|
||||||
|
|
||||||
|
foreach (var n in nums)
|
||||||
|
{
|
||||||
|
ss.Delete(n);
|
||||||
|
AssertBalanced();
|
||||||
|
ss.Exists(n).ShouldBeFalse();
|
||||||
|
if (ss.Size > 0)
|
||||||
|
ss.Root.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
ss.Root.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetMinMax_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
var seqs = new ulong[] { 22, 222, 2222, 2, 2, 4 };
|
||||||
|
foreach (var seq in seqs)
|
||||||
|
ss.Insert(seq);
|
||||||
|
|
||||||
|
var (min, max) = ss.MinMax();
|
||||||
|
min.ShouldBe(2UL);
|
||||||
|
max.ShouldBe(2222UL);
|
||||||
|
|
||||||
|
ss.Empty();
|
||||||
|
|
||||||
|
var num = 22 * NumEntries + 22;
|
||||||
|
var nums = new List<ulong>(num);
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
nums.Add((ulong)i);
|
||||||
|
|
||||||
|
var rng = new Random(42);
|
||||||
|
for (var i = nums.Count - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
var j = rng.Next(i + 1);
|
||||||
|
(nums[i], nums[j]) = (nums[j], nums[i]);
|
||||||
|
}
|
||||||
|
foreach (var n in nums)
|
||||||
|
ss.Insert(n);
|
||||||
|
|
||||||
|
(min, max) = ss.MinMax();
|
||||||
|
min.ShouldBe(0UL);
|
||||||
|
max.ShouldBe((ulong)(num - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetClone_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var num = 100_000;
|
||||||
|
var maxVal = 500_000;
|
||||||
|
var rng = new Random(42);
|
||||||
|
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
ss.Insert((ulong)rng.NextInt64(maxVal + 1));
|
||||||
|
|
||||||
|
var ssc = ss.Clone();
|
||||||
|
ssc.Size.ShouldBe(ss.Size);
|
||||||
|
ssc.Nodes.ShouldBe(ss.Nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetUnion_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var ss1 = new SequenceSet();
|
||||||
|
var seqs1 = new ulong[] { 22, 222, 2222, 2, 2, 4 };
|
||||||
|
foreach (var seq in seqs1) ss1.Insert(seq);
|
||||||
|
|
||||||
|
var ss2 = new SequenceSet();
|
||||||
|
var seqs2 = new ulong[] { 33, 333, 3333, 3, 33_333, 333_333 };
|
||||||
|
foreach (var seq in seqs2) ss2.Insert(seq);
|
||||||
|
|
||||||
|
var ss = SequenceSet.UnionSets(ss1, ss2);
|
||||||
|
ss.ShouldNotBeNull();
|
||||||
|
ss!.Size.ShouldBe(11);
|
||||||
|
|
||||||
|
foreach (var n in seqs1) ss.Exists(n).ShouldBeTrue();
|
||||||
|
foreach (var n in seqs2) ss.Exists(n).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetFirst_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var seqs = new ulong[] { 22, 222, 2222, 222_222 };
|
||||||
|
foreach (var seq in seqs)
|
||||||
|
{
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
ss.Insert(seq);
|
||||||
|
ss.Root.ShouldNotBeNull();
|
||||||
|
ss.Root!.Base.ShouldBe((seq / (ulong)NumEntries) * (ulong)NumEntries);
|
||||||
|
|
||||||
|
ss.Empty();
|
||||||
|
ss.SetInitialMin(seq);
|
||||||
|
ss.Insert(seq);
|
||||||
|
ss.Root.ShouldNotBeNull();
|
||||||
|
ss.Root!.Base.ShouldBe(seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetDistinctUnion_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var ss1 = new SequenceSet();
|
||||||
|
var seqs1 = new ulong[] { 1, 10, 100, 200 };
|
||||||
|
foreach (var seq in seqs1) ss1.Insert(seq);
|
||||||
|
|
||||||
|
var ss2 = new SequenceSet();
|
||||||
|
var seqs2 = new ulong[] { 5000, 6100, 6200, 6222 };
|
||||||
|
foreach (var seq in seqs2) ss2.Insert(seq);
|
||||||
|
|
||||||
|
var ss = ss1.Clone();
|
||||||
|
ss.Union(ss2);
|
||||||
|
|
||||||
|
var allSeqs = new List<ulong>(seqs1);
|
||||||
|
allSeqs.AddRange(seqs2);
|
||||||
|
|
||||||
|
ss.Size.ShouldBe(allSeqs.Count);
|
||||||
|
foreach (var seq in allSeqs)
|
||||||
|
ss.Exists(seq).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetDecodeV1_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var seqs = new ulong[] { 22, 222, 2222, 222_222, 2_222_222 };
|
||||||
|
var encStr = @"FgEDAAAABQAAAABgAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAADgIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAA==";
|
||||||
|
|
||||||
|
var enc = Convert.FromBase64String(encStr);
|
||||||
|
var (ss, _) = SequenceSet.Decode(enc);
|
||||||
|
|
||||||
|
ss.Size.ShouldBe(seqs.Length);
|
||||||
|
foreach (var seq in seqs)
|
||||||
|
ss.Exists(seq).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Encode/Decode round-trip ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeqSetEncodeDecode_RoundTrip_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var num = 2_500_000;
|
||||||
|
var maxVal = 5_000_000;
|
||||||
|
var rng = new Random(42);
|
||||||
|
|
||||||
|
var reference = new HashSet<ulong>(num);
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
{
|
||||||
|
var n = (ulong)rng.NextInt64(maxVal + 1);
|
||||||
|
ss.Insert(n);
|
||||||
|
reference.Add(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf = ss.Encode(null);
|
||||||
|
var (ss2, _) = SequenceSet.Decode(buf);
|
||||||
|
|
||||||
|
ss2.Nodes.ShouldBe(ss.Nodes);
|
||||||
|
ss2.Size.ShouldBe(ss.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Performance / scale tests (no strict timing assertions) ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoRaceSeqSetSizeComparison_ShouldSucceed()
|
||||||
|
{
|
||||||
|
// Insert 5M items; verify correctness (memory comparison is GC-managed, skip strict thresholds)
|
||||||
|
var num = 5_000_000;
|
||||||
|
var maxVal = 7_000_000;
|
||||||
|
var rng = new Random(42);
|
||||||
|
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
var reference = new HashSet<ulong>(num);
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
{
|
||||||
|
var n = (ulong)rng.NextInt64(maxVal + 1);
|
||||||
|
ss.Insert(n);
|
||||||
|
reference.Add(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.Size.ShouldBe(reference.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoRaceSeqSetEncodeLarge_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var num = 2_500_000;
|
||||||
|
var maxVal = 5_000_000;
|
||||||
|
var rng = new Random(42);
|
||||||
|
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
ss.Insert((ulong)rng.NextInt64(maxVal + 1));
|
||||||
|
|
||||||
|
var buf = ss.Encode(null);
|
||||||
|
var (ss2, _) = SequenceSet.Decode(buf);
|
||||||
|
|
||||||
|
ss2.Nodes.ShouldBe(ss.Nodes);
|
||||||
|
ss2.Size.ShouldBe(ss.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoRaceSeqSetRelativeSpeed_ShouldSucceed()
|
||||||
|
{
|
||||||
|
// Correctness: all inserted items must be findable.
|
||||||
|
// Timing assertions are omitted — performance is validated via BenchmarkDotNet benchmarks.
|
||||||
|
var num = 1_000_000;
|
||||||
|
var maxVal = 3_000_000;
|
||||||
|
var rng = new Random(42);
|
||||||
|
|
||||||
|
var seqs = new ulong[num];
|
||||||
|
for (var i = 0; i < num; i++)
|
||||||
|
seqs[i] = (ulong)rng.NextInt64(maxVal + 1);
|
||||||
|
|
||||||
|
var ss = new SequenceSet();
|
||||||
|
foreach (var n in seqs) ss.Insert(n);
|
||||||
|
foreach (var n in seqs) ss.Exists(n).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,28 @@
|
|||||||
# NATS .NET Porting Status Report
|
# NATS .NET Porting Status Report
|
||||||
|
|
||||||
Generated: 2026-02-26 13:03:21 UTC
|
Generated: 2026-02-26 13:07:55 UTC
|
||||||
|
|
||||||
## Modules (12 total)
|
## Modules (12 total)
|
||||||
|
|
||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| not_started | 12 |
|
| complete | 1 |
|
||||||
|
| not_started | 11 |
|
||||||
|
|
||||||
## Features (3673 total)
|
## Features (3673 total)
|
||||||
|
|
||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
|
| complete | 36 |
|
||||||
| n_a | 41 |
|
| n_a | 41 |
|
||||||
| not_started | 3632 |
|
| not_started | 3596 |
|
||||||
|
|
||||||
## Unit Tests (3257 total)
|
## Unit Tests (3257 total)
|
||||||
|
|
||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| not_started | 3257 |
|
| complete | 16 |
|
||||||
|
| not_started | 3241 |
|
||||||
|
|
||||||
## Library Mappings (36 total)
|
## Library Mappings (36 total)
|
||||||
|
|
||||||
@@ -30,4 +33,4 @@ Generated: 2026-02-26 13:03:21 UTC
|
|||||||
|
|
||||||
## Overall Progress
|
## Overall Progress
|
||||||
|
|
||||||
**41/6942 items complete (0.6%)**
|
**94/6942 items complete (1.4%)**
|
||||||
|
|||||||
36
reports/report_b335230.md
Normal file
36
reports/report_b335230.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# NATS .NET Porting Status Report
|
||||||
|
|
||||||
|
Generated: 2026-02-26 13:07:55 UTC
|
||||||
|
|
||||||
|
## Modules (12 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| complete | 1 |
|
||||||
|
| not_started | 11 |
|
||||||
|
|
||||||
|
## Features (3673 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| complete | 36 |
|
||||||
|
| n_a | 41 |
|
||||||
|
| not_started | 3596 |
|
||||||
|
|
||||||
|
## Unit Tests (3257 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| complete | 16 |
|
||||||
|
| not_started | 3241 |
|
||||||
|
|
||||||
|
## Library Mappings (36 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| mapped | 36 |
|
||||||
|
|
||||||
|
|
||||||
|
## Overall Progress
|
||||||
|
|
||||||
|
**94/6942 items complete (1.4%)**
|
||||||
Reference in New Issue
Block a user