Skip to content

Commit 51a7fe7

Browse files
committed
feat: increase timestamp precision for v7 uuids
Inspired by https://brandur.org/fragments/uuid-v7-monotonicity and the https://www.rfc-editor.org/rfc/rfc9562.html#monotonicity_counters, Use increased timestamp precision to replace the first 12 bits of `rand_a` field. This helps ensure monotonicity within the same millisecond.
1 parent 5115ed8 commit 51a7fe7

File tree

5 files changed

+84
-18
lines changed

5 files changed

+84
-18
lines changed

bench/benchmark_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package bench
2+
3+
import (
4+
"testing"
5+
6+
"github.com/cmackenzie1/go-uuid"
7+
guid "github.com/google/uuid"
8+
)
9+
10+
func BenchmarkNewV4(b *testing.B) {
11+
for i := 0; i < b.N; i++ {
12+
a, _ := uuid.NewV4()
13+
_ = a // prevent compiler optimization
14+
}
15+
}
16+
17+
func BenchmarkNewV7(b *testing.B) {
18+
for i := 0; i < b.N; i++ {
19+
a, _ := uuid.NewV7()
20+
_ = a // prevent compiler optimization
21+
}
22+
}
23+
24+
func BenchmarkGoogleV4(b *testing.B) {
25+
for i := 0; i < b.N; i++ {
26+
a, _ := guid.NewRandom()
27+
_ = a // prevent compiler optimization
28+
}
29+
}
30+
31+
func BenchmarkGoogleV7(b *testing.B) {
32+
for i := 0; i < b.N; i++ {
33+
a, _ := guid.NewV7()
34+
_ = a // prevent compiler optimization
35+
}
36+
}

bench/go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/cmackenzie1/go-uuid/bench
2+
3+
go 1.19
4+
5+
require (
6+
github.com/cmackenzie1/go-uuid v1.1.3
7+
github.com/google/uuid v1.6.0
8+
)
9+
10+
replace github.com/cmackenzie1/go-uuid => ../

bench/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

uuid.go

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package uuid
22

33
import (
44
"crypto/rand"
5-
"encoding/binary"
65
"encoding/hex"
76
"errors"
87
"fmt"
@@ -24,8 +23,8 @@ type UUID [16]byte
2423
// Nil represents the zero-value UUID
2524
var Nil UUID
2625

27-
// NewV4 returns a UUID Version 4 as defined in RFC9562. Random bits
28-
// are generated using crypto/rand.
26+
// NewV4 returns a Version 4 UUID as defined in [RFC9562]. Random bits
27+
// are generated using [crypto/rand].
2928
//
3029
// 0 1 2 3
3130
// 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
@@ -38,6 +37,8 @@ var Nil UUID
3837
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
3938
// | random_c |
4039
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
40+
//
41+
// [RFC9562]: https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7
4142
func NewV4() (UUID, error) {
4243
var uuid UUID
4344

@@ -53,11 +54,12 @@ func NewV4() (UUID, error) {
5354
return uuid, nil
5455
}
5556

56-
// NewV7 returns a UUID Version 7 as defined in the drafted revision for RFC9562.
57-
// Random bits are generated using crypto/rand.
58-
// Due to millisecond resolution of the timestamp, UUIDs generated during the
59-
// same millisecond will sort arbitrarily.
60-
// https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7
57+
// NewV7 returns a Version 7 UUID as defined in [RFC9562].
58+
// Random bits are generated using [crypto/rand].
59+
//
60+
// This function employs method 3 (Replace Leftmost Random Bits with Increased Clock Precision)
61+
// to increase the clock precision of the UUID. This helps support scenarios where
62+
// several UUIDs are generated within the same millisecond.
6163
//
6264
// 0 1 2 3
6365
// 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
@@ -70,15 +72,38 @@ func NewV4() (UUID, error) {
7072
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
7173
// | rand_b |
7274
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
75+
//
76+
// [RFC9562]: https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7
7377
func NewV7() (UUID, error) {
7478
var uuid UUID
7579

7680
t := time.Now()
77-
ms := t.UnixMilli() & ((1 << 48) - 1) // 48 bit timestamp
78-
binary.BigEndian.PutUint64(uuid[:], uint64(ms<<16)) // lower 48 bits. Right 0 padded
81+
ms := t.UnixMilli()
82+
83+
// Extract each byte from the 48-bit timestamp
84+
uuid[0] = byte(ms >> 40) // Most significant byte
85+
uuid[1] = byte(ms >> 32)
86+
uuid[2] = byte(ms >> 24)
87+
uuid[3] = byte(ms >> 16)
88+
uuid[4] = byte(ms >> 8)
89+
uuid[5] = byte(ms) // Least significant byte
90+
91+
// Calculate sub-millisecond precision for rand_a (12 bits)
92+
ns := t.UnixNano()
93+
94+
// Calculate sub-millisecond precision by:
95+
// 1. Taking nanoseconds modulo 1 million to get just the sub-millisecond portion
96+
// 2. Multiply by 4096 (2^12) to scale to 12 bits of precision
97+
// 3. Divide by 1 million to normalize back to a 12-bit fraction
98+
// This provides monotonically increasing values within the same millisecond
99+
subMs := ((ns % 1_000_000) * (1 << 12)) / 1_000_000
100+
101+
// Fill the increased clock precision into "rand_a" bits
102+
uuid[6] = byte(subMs >> 8)
103+
uuid[7] = byte(subMs)
79104

80105
// Fill the rest with random data
81-
_, err := io.ReadFull(rand.Reader, uuid[6:])
106+
_, err := io.ReadFull(rand.Reader, uuid[8:])
82107
if err != nil {
83108
return UUID{}, err
84109
}

uuid_test.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,3 @@ func TestPrint(t *testing.T) {
109109
u, _ = NewV7()
110110
t.Logf("v7: %s %v", u, u[:])
111111
}
112-
113-
func BenchmarkNewV4(b *testing.B) {
114-
for i := 0; i < b.N; i++ {
115-
a, _ := NewV4()
116-
_ = a // prevent compiler optimization
117-
}
118-
}

0 commit comments

Comments
 (0)