Skip to content

Commit e8754df

Browse files
committed
Initial Implementation
1 parent e7c1fc8 commit e8754df

File tree

6 files changed

+343
-0
lines changed

6 files changed

+343
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Build
2+
bin/
3+
obj/
4+
5+
# Benchmark Results
6+
BenchmarkDotNet.Artifacts/

UUIDv7.Tests/Program.fs

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
open System
2+
open System.Diagnostics
3+
open BenchmarkDotNet.Attributes
4+
5+
open UUIDv7
6+
open BenchmarkDotNet.Running
7+
8+
[<MemoryDiagnoser>]
9+
type Benchmark1() =
10+
11+
[<Benchmark>]
12+
member __.Test1k() =
13+
let mutable uuid = UUIDv7()
14+
15+
for i in 0..1_000 do
16+
uuid <- UUIDv7.New()
17+
18+
uuid
19+
20+
[<Benchmark>]
21+
member __.Test1M() =
22+
let mutable uuid = UUIDv7.New()
23+
24+
for i in 0..1_000_000 do
25+
uuid <- UUIDv7.New()
26+
27+
uuid
28+
29+
[<Benchmark>]
30+
member __.Test10M() =
31+
let mutable uuid = UUIDv7.New()
32+
33+
for i in 0..10_000_000 do
34+
uuid <- UUIDv7.New()
35+
36+
uuid
37+
38+
[<Benchmark>]
39+
member __.Test1kToString() =
40+
let mutable s = ""
41+
42+
for i in 0..1_000 do
43+
s <- UUIDv7.New().ToString()
44+
45+
s
46+
47+
[<Benchmark>]
48+
member __.Test1kGuid() =
49+
let mutable s = Guid.Empty
50+
51+
for i in 0..1_000 do
52+
s <- Guid.NewGuid()
53+
54+
s
55+
56+
57+
[<EntryPoint>]
58+
let main _ =
59+
let _ = BenchmarkRunner.Run<Benchmark1>()
60+
0

UUIDv7.Tests/UUIDv7.Tests.fsproj

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net7.0</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Compile Include="Program.fs" />
10+
</ItemGroup>
11+
<ItemGroup>
12+
<ProjectReference Include="../UUIDv7/UUIDv7.fsproj" />
13+
</ItemGroup>
14+
<ItemGroup>
15+
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
16+
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.9" />
17+
</ItemGroup>
18+
19+
</Project>

UUIDv7.sln

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "UUIDv7", "UUIDv7\UUIDv7.fsproj", "{56E53B01-B630-4643-B210-D2FDF008F82F}"
7+
EndProject
8+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "UUIDv7.Tests", "UUIDv7.Tests\UUIDv7.Tests.fsproj", "{3671DE56-B48B-414B-8B86-9B37F85DE01F}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(SolutionProperties) = preSolution
16+
HideSolutionNode = FALSE
17+
EndGlobalSection
18+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
19+
{56E53B01-B630-4643-B210-D2FDF008F82F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20+
{56E53B01-B630-4643-B210-D2FDF008F82F}.Debug|Any CPU.Build.0 = Debug|Any CPU
21+
{56E53B01-B630-4643-B210-D2FDF008F82F}.Release|Any CPU.ActiveCfg = Release|Any CPU
22+
{56E53B01-B630-4643-B210-D2FDF008F82F}.Release|Any CPU.Build.0 = Release|Any CPU
23+
{3671DE56-B48B-414B-8B86-9B37F85DE01F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24+
{3671DE56-B48B-414B-8B86-9B37F85DE01F}.Debug|Any CPU.Build.0 = Debug|Any CPU
25+
{3671DE56-B48B-414B-8B86-9B37F85DE01F}.Release|Any CPU.ActiveCfg = Release|Any CPU
26+
{3671DE56-B48B-414B-8B86-9B37F85DE01F}.Release|Any CPU.Build.0 = Release|Any CPU
27+
EndGlobalSection
28+
EndGlobal

UUIDv7/Library.fs

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
namespace UUIDv7
2+
3+
// https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html#name-uuidv7-layout-and-bit-order
4+
// UUIDv7 has microsecond resolution, 64-bit timestamp, 16-bit sequence number, and 64-bit node ID.
5+
6+
open System
7+
open System.Diagnostics
8+
open System.Security.Cryptography
9+
open System.Text
10+
11+
module internal HexConverter =
12+
let hexCharsUpper = "0123456789ABCDEF"
13+
let hexCharsLower = "0123456789abcdef"
14+
15+
[<Literal>]
16+
let Mask = 0xF000_0000_0000_0000UL
17+
18+
let private guidBuilder = StringBuilder(36)
19+
20+
let private formatGuidB (hexChars: string) (high: uint64) (low: uint64) (sb: StringBuilder) =
21+
22+
let rec loopHigh i x =
23+
match i with
24+
| 0 -> ()
25+
| _ ->
26+
sb.Append(hexChars.[int ((x &&& Mask) >>> 60)])
27+
|> ignore
28+
29+
if i = 9 || i = 5 || i = 1 then
30+
sb.Append('-') |> ignore
31+
32+
loopHigh (i - 1) (x <<< 4)
33+
34+
let rec loopLow i x =
35+
match i with
36+
| 0 -> ()
37+
| _ ->
38+
sb.Append(hexChars.[int ((x &&& Mask) >>> 60)])
39+
|> ignore
40+
41+
if i = 13 then sb.Append('-') |> ignore
42+
loopLow (i - 1) (x <<< 4)
43+
44+
loopHigh 16 high
45+
loopLow 16 low
46+
47+
let formatGuid uppercase high low =
48+
let hexChars =
49+
if uppercase then
50+
hexCharsUpper
51+
else
52+
hexCharsLower
53+
54+
guidBuilder.Clear() |> ignore
55+
formatGuidB hexChars high low guidBuilder
56+
guidBuilder.ToString()
57+
58+
[<Struct>]
59+
type UUIDv7(high: uint64, low: uint64) =
60+
member _.High = high
61+
member _.Low = low
62+
63+
member _.Version = (high <<< 48) >>> 60
64+
65+
member _.Variant = low >>> 62
66+
67+
member _.ToUnixTimeSeconds() =
68+
DateTimeOffset.FromUnixTimeSeconds(int64 (high >>> 28))
69+
70+
member _.FullTimestamp =
71+
let low12 = (high &&& 0xFFFUL)
72+
let seconds = TimeSpan.FromSeconds(float (high >>> 28))
73+
74+
let usec =
75+
let x = (((high >>> 16) &&& 0xFFFUL) <<< 12) ||| low12
76+
TimeSpan.FromMilliseconds((float x) / 1000.0)
77+
78+
DateTimeOffset.UnixEpoch + seconds + usec
79+
80+
override _.ToString() = HexConverter.formatGuid false high low
81+
82+
module private UUIDv7Helpers =
83+
84+
// Figure 4: UUIDv7 Field and Bit Layout - Encoding Example (Microsecond Precision)
85+
// 0 1 2 3
86+
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
87+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
88+
// | unixts |
89+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90+
// |unixts | usec | ver | usec |
91+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
92+
// |var| seq | rand |
93+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
94+
// | rand |
95+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
96+
97+
98+
[<Literal>]
99+
let UuidVer = 0x7000UL
100+
101+
[<Literal>]
102+
let TSTopMask = 0xFFFFFFFFFFFF0000UL
103+
104+
[<Literal>]
105+
let TSBottomMask = 0xFFFUL
106+
107+
[<Literal>]
108+
let SeqInit = 0b1000_0000_0000_0000UL
109+
110+
[<Literal>]
111+
let SeqMax = 0b1011_1111_1111_1111UL
112+
113+
[<Literal>]
114+
let RandMask = 0xFFFF_FFFF_FFFFUL
115+
116+
type LowBits(rng: RandomNumberGenerator) =
117+
let mutable i = SeqInit
118+
let gate = obj ()
119+
120+
let mutable lastTs = 0UL
121+
let rand = Array.zeroCreate<byte> (8 * 256)
122+
let mutable randI = 0uy
123+
124+
let fromSeq seqId =
125+
let span = Span(rand)
126+
127+
if randI = 0uy then
128+
rng.GetNonZeroBytes(span)
129+
130+
let x =
131+
(seqId <<< 48)
132+
||| ((BitConverter.ToUInt64(span.Slice((int randI) <<< 3, 8)))
133+
&&& RandMask)
134+
135+
randI <- randI + 1uy
136+
x
137+
138+
new() = LowBits(RandomNumberGenerator.Create())
139+
140+
member _.TryNext(timeStamp) =
141+
lock gate (fun () ->
142+
if timeStamp <> lastTs then
143+
lastTs <- timeStamp
144+
i <- SeqInit
145+
ValueSome(fromSeq SeqInit)
146+
else
147+
match i with
148+
| SeqMax -> ValueNone
149+
| x ->
150+
i <- i + 1UL
151+
ValueSome(fromSeq x)
152+
153+
)
154+
155+
type private HighBits() =
156+
let mutable unixTime = 0UL
157+
let mutable msTicks = 0L
158+
159+
let updateTime () =
160+
let now = DateTimeOffset.UtcNow
161+
unixTime <- uint64 (now.ToUnixTimeSeconds()) <<< 28
162+
163+
msTicks <-
164+
TimeSpan
165+
.FromMilliseconds(
166+
float now.Millisecond
167+
)
168+
.Ticks
169+
170+
do updateTime ()
171+
172+
let usFreq =
173+
match Stopwatch.Frequency / 1_000_000L with
174+
| 0L ->
175+
raise (
176+
PlatformNotSupportedException(
177+
"Stopwatch.Frequency < 1_000_000L. High resolution timer is required."
178+
)
179+
)
180+
| usFreq -> usFreq // highestSetBit (uint64 usFreq)
181+
182+
let stopwatch = Stopwatch.StartNew()
183+
184+
member _.Next() =
185+
let uSecs = uint64 ((msTicks + stopwatch.ElapsedTicks) / usFreq)
186+
187+
let uSecs =
188+
if uSecs > 10_000_000UL then
189+
lock stopwatch (fun () ->
190+
// Correct for tick drift
191+
updateTime ()
192+
193+
stopwatch.Restart()
194+
uint64 ((msTicks + stopwatch.ElapsedTicks) / usFreq))
195+
else
196+
uSecs
197+
198+
let bottom12 = uSecs &&& TSBottomMask
199+
let ts = (uSecs <<< 4) + unixTime
200+
let top48 = ts &&& TSTopMask
201+
top48 ||| UuidVer ||| bottom12
202+
203+
let mutable private timestamp = Unchecked.defaultof<_>
204+
let mutable private clock = Unchecked.defaultof<_>
205+
206+
let rec nextUuid () =
207+
if obj.ReferenceEquals(timestamp, null) then
208+
timestamp <- HighBits()
209+
clock <- LowBits()
210+
211+
let high = timestamp.Next()
212+
213+
match clock.TryNext high with
214+
| ValueSome low -> UUIDv7(high, low)
215+
| ValueNone -> nextUuid ()
216+
217+
type UUIDv7 with
218+
static member New() = UUIDv7Helpers.nextUuid ()

UUIDv7/UUIDv7.fsproj

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Compile Include="Library.fs" />
10+
</ItemGroup>
11+
12+
</Project>

0 commit comments

Comments
 (0)