diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 94dbc2a..065a16e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,6 +3,7 @@ ## General Guidelines - First general instruction - Second general instruction +- Prefer updating documentation comments for mathematical/complexity accuracy when refactoring; keep wire format and semantics unchanged unless explicitly requested. ## Versioning and Git Practices - Treat recent Clockworks changes as a minor version bump (semver). diff --git a/src/Distributed/VectorClock.cs b/src/Distributed/VectorClock.cs index 1e895c8..6fbe90c 100644 --- a/src/Distributed/VectorClock.cs +++ b/src/Distributed/VectorClock.cs @@ -1,3 +1,4 @@ +using System.Buffers.Binary; using System.Globalization; namespace Clockworks.Distributed; @@ -33,7 +34,7 @@ public enum VectorClockOrder /// /// /// Uses a performance-first sorted-array representation with parallel arrays -/// for node IDs and counters. This provides O(n) merge and compare operations +/// for node IDs and counters. This provides O(n+m) merge and compare operations /// while maintaining deterministic canonical ordering by node ID. /// /// @@ -100,14 +101,11 @@ public VectorClock Increment(ushort nodeId) var index = Array.BinarySearch(_nodeIds, nodeId); if (index >= 0) { - // Node exists, increment its counter - // Must copy both arrays to maintain immutability - var newNodeIds = new ushort[_nodeIds.Length]; + // Node exists; node IDs array does not change, so it can be shared safely. var newCounters = new ulong[_counters.Length]; - Array.Copy(_nodeIds, newNodeIds, _nodeIds.Length); Array.Copy(_counters, newCounters, _counters.Length); newCounters[index]++; - return new VectorClock(newNodeIds, newCounters); + return new VectorClock(_nodeIds, newCounters); } else { @@ -116,14 +114,21 @@ public VectorClock Increment(ushort nodeId) var newNodeIds = new ushort[_nodeIds.Length + 1]; var newCounters = new ulong[_counters.Length + 1]; - Array.Copy(_nodeIds, 0, newNodeIds, 0, insertIndex); - Array.Copy(_counters, 0, newCounters, 0, insertIndex); + if (insertIndex > 0) + { + Array.Copy(_nodeIds, 0, newNodeIds, 0, insertIndex); + Array.Copy(_counters, 0, newCounters, 0, insertIndex); + } newNodeIds[insertIndex] = nodeId; newCounters[insertIndex] = 1; - Array.Copy(_nodeIds, insertIndex, newNodeIds, insertIndex + 1, _nodeIds.Length - insertIndex); - Array.Copy(_counters, insertIndex, newCounters, insertIndex + 1, _counters.Length - insertIndex); + var tail = _nodeIds.Length - insertIndex; + if (tail > 0) + { + Array.Copy(_nodeIds, insertIndex, newNodeIds, insertIndex + 1, tail); + Array.Copy(_counters, insertIndex, newCounters, insertIndex + 1, tail); + } return new VectorClock(newNodeIds, newCounters); } @@ -140,49 +145,55 @@ public VectorClock Merge(VectorClock other) if (other._nodeIds == null || other._counters == null) return this; - // Merge two sorted arrays - var resultNodes = new List(); - var resultCounters = new List(); + // Merge two sorted arrays (upper bound = combined length) + var maxLen = _nodeIds.Length + other._nodeIds.Length; + var nodeIds = new ushort[maxLen]; + var counters = new ulong[maxLen]; - int i = 0, j = 0; + int i = 0, j = 0, k = 0; while (i < _nodeIds.Length || j < other._nodeIds.Length) { if (i >= _nodeIds.Length) { - // Exhausted this, add from other - resultNodes.Add(other._nodeIds[j]); - resultCounters.Add(other._counters[j]); + nodeIds[k] = other._nodeIds[j]; + counters[k] = other._counters[j]; j++; } else if (j >= other._nodeIds.Length) { - // Exhausted other, add from this - resultNodes.Add(_nodeIds[i]); - resultCounters.Add(_counters[i]); + nodeIds[k] = _nodeIds[i]; + counters[k] = _counters[i]; i++; } else if (_nodeIds[i] < other._nodeIds[j]) { - resultNodes.Add(_nodeIds[i]); - resultCounters.Add(_counters[i]); + nodeIds[k] = _nodeIds[i]; + counters[k] = _counters[i]; i++; } else if (_nodeIds[i] > other._nodeIds[j]) { - resultNodes.Add(other._nodeIds[j]); - resultCounters.Add(other._counters[j]); + nodeIds[k] = other._nodeIds[j]; + counters[k] = other._counters[j]; j++; } - else // Equal node IDs + else { - resultNodes.Add(_nodeIds[i]); - resultCounters.Add(Math.Max(_counters[i], other._counters[j])); + nodeIds[k] = _nodeIds[i]; + counters[k] = Math.Max(_counters[i], other._counters[j]); i++; j++; } + + k++; } - return new VectorClock(resultNodes.ToArray(), resultCounters.ToArray()); + if (k == maxLen) + return new VectorClock(nodeIds, counters); + + Array.Resize(ref nodeIds, k); + Array.Resize(ref counters, k); + return new VectorClock(nodeIds, counters); } /// @@ -190,29 +201,56 @@ public VectorClock Merge(VectorClock other) /// public VectorClockOrder Compare(VectorClock other) { + if (_nodeIds == null || _counters == null) + { + if (other._nodeIds == null || other._counters == null) + return VectorClockOrder.Equal; + return VectorClockOrder.Before; + } + + if (other._nodeIds == null || other._counters == null) + return VectorClockOrder.After; + var thisIsLessOrEqual = true; var otherIsLessOrEqual = true; - // Get all unique node IDs from both clocks - var allNodes = new HashSet(); - if (_nodeIds != null) - foreach (var node in _nodeIds) - allNodes.Add(node); - if (other._nodeIds != null) - foreach (var node in other._nodeIds) - allNodes.Add(node); - - foreach (var nodeId in allNodes) + // Linear merge-style comparison over two sorted arrays. + // Missing entries are treated as 0. + int i = 0, j = 0; + while (i < _nodeIds.Length || j < other._nodeIds.Length) { - var thisValue = Get(nodeId); - var otherValue = other.Get(nodeId); + ushort nodeId; + ulong thisValue; + ulong otherValue; + + if (j >= other._nodeIds.Length || (i < _nodeIds.Length && _nodeIds[i] < other._nodeIds[j])) + { + nodeId = _nodeIds[i]; + thisValue = _counters[i]; + otherValue = 0; + i++; + } + else if (i >= _nodeIds.Length || _nodeIds[i] > other._nodeIds[j]) + { + nodeId = other._nodeIds[j]; + thisValue = 0; + otherValue = other._counters[j]; + j++; + } + else + { + nodeId = _nodeIds[i]; + thisValue = _counters[i]; + otherValue = other._counters[j]; + i++; + j++; + } if (thisValue > otherValue) - thisIsLessOrEqual = false; // this > other, so this is NOT ≤ other - if (otherValue > thisValue) - otherIsLessOrEqual = false; // other > this, so other is NOT ≤ this + thisIsLessOrEqual = false; + else if (otherValue > thisValue) + otherIsLessOrEqual = false; - // Early exit if we know they're concurrent if (!thisIsLessOrEqual && !otherIsLessOrEqual) return VectorClockOrder.Concurrent; } @@ -223,7 +261,6 @@ public VectorClockOrder Compare(VectorClock other) return VectorClockOrder.Before; if (otherIsLessOrEqual) return VectorClockOrder.After; - return VectorClockOrder.Concurrent; } @@ -257,12 +294,7 @@ public void WriteTo(Span destination) if (destination.Length < requiredSize) throw new ArgumentException($"Destination must be at least {requiredSize} bytes", nameof(destination)); - // Write count as big-endian uint - var countValue = (uint)count; - destination[0] = (byte)(countValue >> 24); - destination[1] = (byte)(countValue >> 16); - destination[2] = (byte)(countValue >> 8); - destination[3] = (byte)countValue; + BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)count); if (count == 0) return; @@ -273,19 +305,10 @@ public void WriteTo(Span destination) var nodeId = _nodeIds![i]; var counter = _counters![i]; - // Write nodeId as big-endian ushort - destination[offset++] = (byte)(nodeId >> 8); - destination[offset++] = (byte)nodeId; - - // Write counter as big-endian ulong - destination[offset++] = (byte)(counter >> 56); - destination[offset++] = (byte)(counter >> 48); - destination[offset++] = (byte)(counter >> 40); - destination[offset++] = (byte)(counter >> 32); - destination[offset++] = (byte)(counter >> 24); - destination[offset++] = (byte)(counter >> 16); - destination[offset++] = (byte)(counter >> 8); - destination[offset++] = (byte)counter; + BinaryPrimitives.WriteUInt16BigEndian(destination.Slice(offset, 2), nodeId); + offset += 2; + BinaryPrimitives.WriteUInt64BigEndian(destination.Slice(offset, 8), counter); + offset += 8; } } @@ -306,7 +329,7 @@ public static VectorClock ReadFrom(ReadOnlySpan source) if (source.Length < 4) throw new ArgumentException("Source must be at least 4 bytes", nameof(source)); - var count = (uint)((source[0] << 24) | (source[1] << 16) | (source[2] << 8) | source[3]); + var count = BinaryPrimitives.ReadUInt32BigEndian(source); if (count > MaxEntries) throw new ArgumentOutOfRangeException(nameof(source), $"Vector clock entry count {count} exceeds max {MaxEntries}."); @@ -325,18 +348,10 @@ public static VectorClock ReadFrom(ReadOnlySpan source) var offset = 4; for (var i = 0; i < countValue; i++) { - // Read nodeId as big-endian ushort - nodeIds[i] = (ushort)((source[offset++] << 8) | source[offset++]); - - // Read counter as big-endian ulong - counters[i] = ((ulong)source[offset++] << 56) | - ((ulong)source[offset++] << 48) | - ((ulong)source[offset++] << 40) | - ((ulong)source[offset++] << 32) | - ((ulong)source[offset++] << 24) | - ((ulong)source[offset++] << 16) | - ((ulong)source[offset++] << 8) | - source[offset++]; + nodeIds[i] = BinaryPrimitives.ReadUInt16BigEndian(source.Slice(offset, 2)); + offset += 2; + counters[i] = BinaryPrimitives.ReadUInt64BigEndian(source.Slice(offset, 8)); + offset += 8; } var isSortedUnique = true;