From b4c594e21e01d90687fb7aa896e832bf13e01d37 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 6 Jul 2024 10:08:28 +0200 Subject: [PATCH 1/3] Remove dependency on UUIDNext --- .../TypeId.Core/TypeId.Core.csproj | 1 - .../TypeId.Core/TypeIdDecoded.cs | 11 +- .../TypeId.Core/Uuid/GuidConverter.cs | 37 ++++++ .../TypeId.Core/Uuid/UuidDecoder.cs | 25 ++++ .../TypeId.Core/Uuid/UuidGenerator.cs | 114 ++++++++++++++++++ 5 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 src/FastIDs.TypeId/TypeId.Core/Uuid/GuidConverter.cs create mode 100644 src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs create mode 100644 src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs diff --git a/src/FastIDs.TypeId/TypeId.Core/TypeId.Core.csproj b/src/FastIDs.TypeId/TypeId.Core/TypeId.Core.csproj index 624dbe7..ffabd52 100644 --- a/src/FastIDs.TypeId/TypeId.Core/TypeId.Core.csproj +++ b/src/FastIDs.TypeId/TypeId.Core/TypeId.Core.csproj @@ -33,7 +33,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs b/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs index 630e613..367b801 100644 --- a/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs +++ b/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs @@ -1,13 +1,14 @@ using System.Runtime.InteropServices; using System.Text.Unicode; -using UUIDNext; -using UUIDNext.Generator; +using FastIDs.TypeId.Uuid; namespace FastIDs.TypeId; [StructLayout(LayoutKind.Auto)] public readonly struct TypeIdDecoded : IEquatable, ISpanFormattable, IUtf8SpanFormattable { + private static readonly UuidGenerator UuidGenerator = new(); + /// /// The type part of the TypeId. /// @@ -66,7 +67,7 @@ public int GetSuffix(Span utf8Output) /// DateTimeOffset representing the ID generation timestamp. public DateTimeOffset GetTimestamp() { - var (timestampMs, _) = UuidV7Generator.Decode(Id); + var (timestampMs, _) = UuidDecoder.Decode(Id); return DateTimeOffset.FromUnixTimeMilliseconds(timestampMs); } @@ -194,7 +195,7 @@ public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnly /// /// This method validates the type. If you are sure that type is valid use to skip type validation. /// - public static TypeIdDecoded New(string type) => FromUuidV7(type, Uuid.NewSequential()); + public static TypeIdDecoded New(string type) => FromUuidV7(type, UuidGenerator.New()); /// /// Generates new TypeId with the specified type and random UUIDv7. If is false, type is not validated. @@ -207,7 +208,7 @@ public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnly /// Use this method with set to false when you are sure that is valid. /// This method is a bit faster than (especially for longer types) because it skips type validation. /// - public static TypeIdDecoded New(string type, bool validateType) => validateType ? New(type) : new TypeIdDecoded(type, Uuid.NewSequential()); + public static TypeIdDecoded New(string type, bool validateType) => validateType ? New(type) : new TypeIdDecoded(type, UuidGenerator.New()); /// /// Generates new TypeId with the specified type and UUIDv7. diff --git a/src/FastIDs.TypeId/TypeId.Core/Uuid/GuidConverter.cs b/src/FastIDs.TypeId/TypeId.Core/Uuid/GuidConverter.cs new file mode 100644 index 0000000..a72c5a7 --- /dev/null +++ b/src/FastIDs.TypeId/TypeId.Core/Uuid/GuidConverter.cs @@ -0,0 +1,37 @@ +using System.Runtime.CompilerServices; + +namespace FastIDs.TypeId.Uuid; + +// The UUIDv7 implementation is extracted from https://github.com/mareek/UUIDNext to prevent transient dependency. +// TypeID doesn't require any UUID implementations except UUIDv7. +internal static class GuidConverter +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid CreateGuidFromBigEndianBytes(Span bytes) + { + SetVersion(bytes); + SetVariant(bytes); + return new Guid(bytes, true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetVersion(Span bytes) + { + const byte uuidVersion = 7; + const int versionByteIndex = 6; + //Erase upper 4 bits + bytes[versionByteIndex] &= 0b0000_1111; + //Set 4 upper bits to version + bytes[versionByteIndex] |= uuidVersion << 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetVariant(Span bytes) + { + const int variantByteIndex = 8; + //Erase upper 2 bits + bytes[variantByteIndex] &= 0b0011_1111; + //Set 2 upper bits to variant + bytes[variantByteIndex] |= 0b1000_0000; + } +} \ No newline at end of file diff --git a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs new file mode 100644 index 0000000..cb10347 --- /dev/null +++ b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs @@ -0,0 +1,25 @@ +using System.Buffers.Binary; + +namespace FastIDs.TypeId.Uuid; + +// The UUIDv7 implementation is extracted from https://github.com/mareek/UUIDNext to prevent transient dependency. +// TypeID doesn't require any UUID implementations except UUIDv7. +internal static class UuidDecoder +{ + public static (long timestampMs, short sequence) Decode(Guid guid) + { + Span bytes = stackalloc byte[16]; + guid.TryWriteBytes(bytes, bigEndian: true, out _); + + Span timestampBytes = stackalloc byte[8]; + bytes[..6].CopyTo(timestampBytes[2..]); + var timestampMs = BinaryPrimitives.ReadInt64BigEndian(timestampBytes); + + var sequenceBytes = bytes[6..8]; + //remove version information + sequenceBytes[0] &= 0b0000_1111; + var sequence = BinaryPrimitives.ReadInt16BigEndian(sequenceBytes); + + return (timestampMs, sequence); + } +} \ No newline at end of file diff --git a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs new file mode 100644 index 0000000..4719c7e --- /dev/null +++ b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs @@ -0,0 +1,114 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace FastIDs.TypeId.Uuid; + +// The UUIDv7 implementation is extracted from https://github.com/mareek/UUIDNext to prevent transient dependency. +// TypeID doesn't require any UUID implementations except UUIDv7. + +// All timestamps of type `long` in this class are Unix milliseconds unless stated otherwise. +internal class UuidGenerator +{ + private const int SequenceBitSize = 7; + private const int SequenceMaxValue = (1 << SequenceBitSize) - 1; + + private long _lastUsedTimestamp; + private long _timestampOffset; + private ushort _monotonicSequence; + + public Guid New() + { + Span bytes = stackalloc byte[16]; + + var timestamp = GetCurrentUnixMilliseconds(); + SetSequence(bytes[6..8], ref timestamp); + SetTimestamp(bytes[..6], timestamp); + RandomNumberGenerator.Fill(bytes[8..]); + + return GuidConverter.CreateGuidFromBigEndianBytes(bytes); + } + + // The implementation copied from DateTimeOffset.ToUnixTimeMilliseconds() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private long GetCurrentUnixMilliseconds() => DateTime.UtcNow.Ticks / 10000L - 62135596800000L; + + private static void SetTimestamp(Span bytes, long unixMs) + { + Span timestampBytes = stackalloc byte[8]; + BinaryPrimitives.TryWriteInt64BigEndian(timestampBytes, unixMs); + timestampBytes[2..].CopyTo(bytes); + } + + private void SetSequence(Span bytes, ref long timestamp) + { + ushort sequence; + var originalTimestamp = timestamp; + + lock (this) + { + sequence = GetSequenceNumber(ref timestamp); + if (sequence > SequenceMaxValue) + { + // if the sequence is greater than the max value, we take advantage + // of the anti-rewind mechanism to simulate a slight change in clock time + timestamp = originalTimestamp + 1; + sequence = GetSequenceNumber(ref timestamp); + } + } + + BinaryPrimitives.TryWriteUInt16BigEndian(bytes, sequence); + } + + private ushort GetSequenceNumber(ref long timestamp) + { + EnsureTimestampNeverMoveBackward(ref timestamp); + + if (timestamp == _lastUsedTimestamp) + { + _monotonicSequence += 1; + } + else + { + _lastUsedTimestamp = timestamp; + _monotonicSequence = GetSequenceSeed(); + } + + return _monotonicSequence; + } + + private void EnsureTimestampNeverMoveBackward(ref long timestamp) + { + var lastUsedTs = _lastUsedTimestamp; + if (_timestampOffset > 0 && timestamp > lastUsedTs) + { + // reset the offset to reduce the drift with the actual time when possible + _timestampOffset = 0; + return; + } + + var offsetTimestamp = timestamp + _timestampOffset; + if (offsetTimestamp < lastUsedTs) + { + // if the computer clock has moved backward since the last generated UUID, + // we add an offset to ensure the timestamp always move forward (See RFC Section 6.2) + _timestampOffset = lastUsedTs - timestamp; + timestamp = lastUsedTs; + return; + } + + // Happy path + timestamp = offsetTimestamp; + } + + + private static ushort GetSequenceSeed() + { + // following section 6.2 on "Fixed-Length Dedicated Counter Seeding", the initial value of the sequence is randomized + Span buffer = stackalloc byte[2]; + RandomNumberGenerator.Fill(buffer); + // Setting the highest bit to 0 mitigate the risk of a sequence overflow (see section 6.2) + buffer[0] &= 0b0000_0111; + return BinaryPrimitives.ReadUInt16BigEndian(buffer); + } +} \ No newline at end of file From 02c6269e530238e8f75a447dd92efc1ad8fa79b6 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Fri, 9 Aug 2024 01:41:27 +0200 Subject: [PATCH 2/3] Optimize UUID generation --- .../TypeId.Core/TypeIdDecoded.cs | 2 +- .../TypeId.Core/Uuid/UuidDecoder.cs | 18 +++++++----------- .../TypeId.Core/Uuid/UuidGenerator.cs | 19 +++++++++++-------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs b/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs index 367b801..1cf34e3 100644 --- a/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs +++ b/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs @@ -67,7 +67,7 @@ public int GetSuffix(Span utf8Output) /// DateTimeOffset representing the ID generation timestamp. public DateTimeOffset GetTimestamp() { - var (timestampMs, _) = UuidDecoder.Decode(Id); + var timestampMs = UuidDecoder.DecodeTimestamp(Id); return DateTimeOffset.FromUnixTimeMilliseconds(timestampMs); } diff --git a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs index cb10347..b76f7fd 100644 --- a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs +++ b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs @@ -6,20 +6,16 @@ namespace FastIDs.TypeId.Uuid; // TypeID doesn't require any UUID implementations except UUIDv7. internal static class UuidDecoder { - public static (long timestampMs, short sequence) Decode(Guid guid) + public static long DecodeTimestamp(Guid guid) { - Span bytes = stackalloc byte[16]; - guid.TryWriteBytes(bytes, bigEndian: true, out _); + // Allocating 2 bytes more to prepend timestamp data. + Span bytes = stackalloc byte[18]; + guid.TryWriteBytes(bytes[2..], bigEndian: true, out _); - Span timestampBytes = stackalloc byte[8]; - bytes[..6].CopyTo(timestampBytes[2..]); + var timestampBytes = bytes[..8]; var timestampMs = BinaryPrimitives.ReadInt64BigEndian(timestampBytes); - var sequenceBytes = bytes[6..8]; - //remove version information - sequenceBytes[0] &= 0b0000_1111; - var sequence = BinaryPrimitives.ReadInt16BigEndian(sequenceBytes); - - return (timestampMs, sequence); + return timestampMs; } + } \ No newline at end of file diff --git a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs index 4719c7e..0ec9dfc 100644 --- a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs +++ b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs @@ -19,25 +19,28 @@ internal class UuidGenerator public Guid New() { - Span bytes = stackalloc byte[16]; + // Allocating 2 bytes more to prepend timestamp data. + Span buffer = stackalloc byte[18]; + + // Offset bytes that are used in ID. + var idBytes = buffer[2..]; var timestamp = GetCurrentUnixMilliseconds(); - SetSequence(bytes[6..8], ref timestamp); - SetTimestamp(bytes[..6], timestamp); - RandomNumberGenerator.Fill(bytes[8..]); + SetSequence(idBytes[6..8], ref timestamp); + SetTimestamp(buffer[..8], timestamp); // Using full buffer because we need to account for two zero-bytes in front. + RandomNumberGenerator.Fill(idBytes[8..]); - return GuidConverter.CreateGuidFromBigEndianBytes(bytes); + return GuidConverter.CreateGuidFromBigEndianBytes(buffer); } // The implementation copied from DateTimeOffset.ToUnixTimeMilliseconds() [MethodImpl(MethodImplOptions.AggressiveInlining)] private long GetCurrentUnixMilliseconds() => DateTime.UtcNow.Ticks / 10000L - 62135596800000L; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SetTimestamp(Span bytes, long unixMs) { - Span timestampBytes = stackalloc byte[8]; - BinaryPrimitives.TryWriteInt64BigEndian(timestampBytes, unixMs); - timestampBytes[2..].CopyTo(bytes); + BinaryPrimitives.TryWriteInt64BigEndian(bytes, unixMs); } private void SetSequence(Span bytes, ref long timestamp) From 2a83f00b7cf3099f0cdc927ea1bb901139d3ecef Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Fri, 9 Aug 2024 01:50:51 +0200 Subject: [PATCH 3/3] Fix buffer slicing on UUID generation --- src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs index 0ec9dfc..9cf4103 100644 --- a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs +++ b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs @@ -30,7 +30,7 @@ public Guid New() SetTimestamp(buffer[..8], timestamp); // Using full buffer because we need to account for two zero-bytes in front. RandomNumberGenerator.Fill(idBytes[8..]); - return GuidConverter.CreateGuidFromBigEndianBytes(buffer); + return GuidConverter.CreateGuidFromBigEndianBytes(idBytes); } // The implementation copied from DateTimeOffset.ToUnixTimeMilliseconds()