Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proper timestamp calculation for UUID v7 #199

Merged
merged 10 commits into from
Feb 10, 2025
10 changes: 10 additions & 0 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,16 @@ func BenchmarkGenerator(b *testing.B) {
NewV5(NamespaceDNS, "www.example.com")
}
})
b.Run("NewV6", func(b *testing.B) {
for i := 0; i < b.N; i++ {
NewV6()
}
})
b.Run("NewV7", func(b *testing.B) {
for i := 0; i < b.N; i++ {
NewV7()
}
})
}

type faultyReader struct {
Expand Down
13 changes: 9 additions & 4 deletions uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ const (
)

// Timestamp is the count of 100-nanosecond intervals since 00:00:00.00,
// 15 October 1582 within a V1 UUID. This type has no meaning for other
// UUID versions since they don't have an embedded timestamp.
// 15 October 1582 within a V1 or V6 UUID, or as a common intermediate
// representation of the (Unix Millisecond) timestamp within a V7 UUID.
// This type has no meaning for other UUID versions since they don't
// have an embedded timestamp.
type Timestamp uint64

const _100nsPerSecond = 10000000
Expand Down Expand Up @@ -144,8 +146,11 @@ func TimestampFromV7(u UUID) (Timestamp, error) {
(int64(u[4]) << 8) |
int64(u[5])

// convert to format expected by Timestamp
tsNanos := epochStart + time.UnixMilli(t).UTC().UnixNano()/100
// UUIDv7 stores MS since 1970-01-01 00:00:00, but the Timestamp
// type stores 100-nanosecond increments since 1582-10-15 00:00:00.
// This conversion multiplies ms by 10,000 to get 100-ns chunks and adds
// the difference between October 1582 and January 1970.
tsNanos := epochStart + (t * 10000)
return Timestamp(tsNanos), nil
}

Expand Down
142 changes: 141 additions & 1 deletion uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,14 @@ func TestTimestampFromV7(t *testing.T) {
want Timestamp
wanterr bool
}{
// These non-V7 versions should not be able to be provided to TimestampFromV7
{u: Must(NewV1()), wanterr: true},
kohenkatz marked this conversation as resolved.
Show resolved Hide resolved
{u: NewV3(NamespaceDNS, "a.example.com"), wanterr: true},
// v7 is unix_ts_ms, so zero value time is unix epoch
{u: Must(FromString("00000000-0000-7000-0000-000000000000")), want: 122192928000000000},
{u: Must(FromString("018a8fec-3ced-7164-995f-93c80cbdc575")), want: 139139245386050000},
{u: Must(FromString("ffffffff-ffff-7fff-ffff-ffffffffffff")), want: Timestamp(epochStart + time.UnixMilli((1<<48)-1).UTC().UnixNano()/100)},
// Calculated as `(1<<48)-1` milliseconds, times 100 ns per ms, plus epoch offset from 1970 to 1582.
{u: Must(FromString("ffffffff-ffff-7fff-bfff-ffffffffffff")), want: 2936942695106550000},
}
for _, tt := range tests {
got, err := TimestampFromV7(tt.u)
Expand All @@ -281,6 +284,56 @@ func TestTimestampFromV7(t *testing.T) {
}
}

func TestMinMaxTimestamps(t *testing.T) {
tests := []struct {
u UUID
want time.Time
}{

// v1 min and max
{u: Must(FromString("00000000-0000-1000-8000-000000000000")), want: time.Date(1582, 10, 15, 0, 0, 0, 0, time.UTC)}, //1582-10-15 0:00:00 (UTC)
{u: Must(FromString("ffffffff-ffff-1fff-bfff-ffffffffffff")), want: time.Date(5236, 3, 31, 21, 21, 00, 684697500, time.UTC)}, //5236-03-31 21:21:00 (UTC)

// v6 min and max
{u: Must(FromString("00000000-0000-6000-8000-000000000000")), want: time.Date(1582, 10, 15, 0, 0, 0, 0, time.UTC)}, //1582-10-15 0:00:00 (UTC)
{u: Must(FromString("ffffffff-ffff-6fff-bfff-ffffffffffff")), want: time.Date(5236, 3, 31, 21, 21, 00, 684697500, time.UTC)}, //5236-03-31 21:21:00 (UTC)

// v7 min and max
{u: Must(FromString("00000000-0000-7000-8000-000000000000")), want: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, //1970-01-01 0:00:00 (UTC)
{u: Must(FromString("ffffffff-ffff-7fff-bfff-ffffffffffff")), want: time.Date(10889, 8, 2, 5, 31, 50, 655000000, time.UTC)}, //10889-08-02 5:31:50.655 (UTC)
}
for _, tt := range tests {
var got Timestamp
var err error
var functionName string

switch tt.u.Version() {
case V1:
functionName = "TimestampFromV1"
got, err = TimestampFromV1(tt.u)
case V6:
functionName = "TimestampFromV6"
got, err = TimestampFromV6(tt.u)
case V7:
functionName = "TimestampFromV7"
got, err = TimestampFromV7(tt.u)
}

if err != nil {
t.Errorf(functionName+"(%v) got error %v, want %v", tt.u, err, tt.want)
}

tm, err := got.Time()
if err != nil {
t.Errorf(functionName+"(%v) got error %v, want %v", tt.u, err, tt.want)
}

if !tt.want.Equal(tm) {
t.Errorf(functionName+"(%v) got %v, want %v", tt.u, tm.UTC(), tt.want)
}
}
}

func BenchmarkFormat(b *testing.B) {
var tests = []string{
"%s",
Expand All @@ -300,3 +353,90 @@ func BenchmarkFormat(b *testing.B) {
})
}
}

var uuidBenchmarkSink UUID
var timestampBenchmarkSink Timestamp
var timeBenchmarkSink time.Time

func BenchmarkTimestampFrom(b *testing.B) {
var err error
numbUUIDs := 1000
if testing.Short() {
numbUUIDs = 10
}

funcs := []struct {
name string
create func() (UUID, error)
timestamp func(UUID) (Timestamp, error)
}{
{"v1", NewV1, TimestampFromV1},
{"v6", NewV6, TimestampFromV6},
{"v7", NewV7, TimestampFromV7},
}

for _, fns := range funcs {
b.Run(fns.name, func(b *testing.B) {
// Make sure we don't just encode the same string over and over again as that will hit memory caches unrealistically
uuids := make([]UUID, numbUUIDs)
for i := 0; i < numbUUIDs; i++ {
uuids[i] = Must(fns.create())
if !testing.Short() {
time.Sleep(1 * time.Millisecond)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
timestampBenchmarkSink, err = fns.timestamp(uuids[i%numbUUIDs])

if err != nil {
b.Fatal(err)
}
}
})
}
}

func BenchmarkTimestampTime(b *testing.B) {
var err error
numbUUIDs := 1000
if testing.Short() {
numbUUIDs = 10
}

funcs := []struct {
name string
create func() (UUID, error)
timestamp func(UUID) (Timestamp, error)
}{
{"v1", NewV1, TimestampFromV1},
{"v6", NewV6, TimestampFromV6},
{"v7", NewV7, TimestampFromV7},
}

for _, fns := range funcs {
b.Run(fns.name, func(b *testing.B) {
// Make sure we don't just encode the same string over and over again as that will hit memory caches unrealistically
uuids := make([]UUID, numbUUIDs)
timestamps := make([]Timestamp, numbUUIDs)
for i := 0; i < numbUUIDs; i++ {
uuids[i] = Must(fns.create())
timestamps[i], err = fns.timestamp(uuids[i])
if err != nil {
b.Fatal(err)
}
if !testing.Short() {
time.Sleep(1 * time.Millisecond)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
timeBenchmarkSink, err = timestamps[i%numbUUIDs].Time()
if err != nil {
b.Fatal(err)
}
}
})
}

}
Loading