From a2aa1192307f850a8e2d074a59094d1c269d9844 Mon Sep 17 00:00:00 2001 From: Moshe Katz Date: Sun, 9 Feb 2025 00:14:55 -0500 Subject: [PATCH 01/10] fix: prevent undefined behavior on nanosecond overflow --- uuid.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uuid.go b/uuid.go index 43f68f5..3579736 100644 --- a/uuid.go +++ b/uuid.go @@ -87,6 +87,7 @@ const ( type Timestamp uint64 const _100nsPerSecond = 10000000 +const _100nsPerMillisecond = 10000 // Time returns the time.Time representation of a Timestamp. // @@ -145,7 +146,7 @@ func TimestampFromV7(u UUID) (Timestamp, error) { int64(u[5]) // convert to format expected by Timestamp - tsNanos := epochStart + time.UnixMilli(t).UTC().UnixNano()/100 + tsNanos := epochStart + (t * _100nsPerMillisecond) return Timestamp(tsNanos), nil } From 3c2c7a62f3f9cc28f3644a7ed259b3632f6542a3 Mon Sep 17 00:00:00 2001 From: Moshe Katz Date: Sun, 9 Feb 2025 00:15:15 -0500 Subject: [PATCH 02/10] doc: update description of Timestamp type alias --- uuid.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uuid.go b/uuid.go index 3579736..4147905 100644 --- a/uuid.go +++ b/uuid.go @@ -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 From d344c6a92cb7f544ebb661aeaf685e046011364a Mon Sep 17 00:00:00 2001 From: Moshe Katz Date: Sun, 9 Feb 2025 00:15:45 -0500 Subject: [PATCH 03/10] test: add test (based on https://github.com/gofrs/uuid/issues/195#issuecomment-2645965874 ) --- uuid_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/uuid_test.go b/uuid_test.go index 6dc97f9..782a017 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -281,6 +281,56 @@ func TestTimestampFromV7(t *testing.T) { } } +func TestTimestampMinMaxV167(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", From cbd1ae5ecee51f69738f0d43c30bcf73ecbcbd6b Mon Sep 17 00:00:00 2001 From: Moshe Katz Date: Sun, 9 Feb 2025 00:21:17 -0500 Subject: [PATCH 04/10] test: other minor test fixes --- uuid_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uuid_test.go b/uuid_test.go index 782a017..d90d1a5 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -262,11 +262,11 @@ func TestTimestampFromV7(t *testing.T) { want Timestamp wanterr bool }{ - {u: Must(NewV1()), 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) From 619065c526ed8b3a389e61cdc5ee1490d623b578 Mon Sep 17 00:00:00 2001 From: Nathan <103947234+nathanmcgarvey-modopayments@users.noreply.github.com> Date: Sun, 9 Feb 2025 15:13:37 -0600 Subject: [PATCH 05/10] add benchmarks for TimestampFrom and Timestamp.Time (#1) --- generator_test.go | 11 ++++++ uuid_test.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/generator_test.go b/generator_test.go index 2f48668..605d450 100644 --- a/generator_test.go +++ b/generator_test.go @@ -1101,6 +1101,7 @@ func TestDefaultHWAddrFunc(t *testing.T) { } func BenchmarkGenerator(b *testing.B) { + b.ReportAllocs() b.Run("NewV1", func(b *testing.B) { for i := 0; i < b.N; i++ { NewV1() @@ -1121,6 +1122,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 { diff --git a/uuid_test.go b/uuid_test.go index d90d1a5..b47f7db 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -350,3 +350,94 @@ func BenchmarkFormat(b *testing.B) { }) } } + +var uuidBenchmarkSink UUID +var timestampBenchmarkSink Timestamp +var timeBenchmarkSink time.Time + +func BenchmarkTimestampFrom(b *testing.B) { + b.ReportAllocs() + + 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) { + b.ReportAllocs() + + 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) + } + } + }) + } + +} From 63d633eec48842beb1a3062a9c42e9bb17a9400f Mon Sep 17 00:00:00 2001 From: Moshe Katz Date: Sun, 9 Feb 2025 16:19:22 -0500 Subject: [PATCH 06/10] fix: put back incorrectly-removed test, and add another --- uuid_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/uuid_test.go b/uuid_test.go index b47f7db..6c38e98 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -262,6 +262,9 @@ 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}, + {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}, From 893c323d23750285cb2d3725994f113f757c91f0 Mon Sep 17 00:00:00 2001 From: Moshe Katz Date: Mon, 10 Feb 2025 11:21:02 -0500 Subject: [PATCH 07/10] Accept suggestions from @dylan-bourque --- generator_test.go | 1 - uuid_test.go | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/generator_test.go b/generator_test.go index 605d450..7d9f1fc 100644 --- a/generator_test.go +++ b/generator_test.go @@ -1101,7 +1101,6 @@ func TestDefaultHWAddrFunc(t *testing.T) { } func BenchmarkGenerator(b *testing.B) { - b.ReportAllocs() b.Run("NewV1", func(b *testing.B) { for i := 0; i < b.N; i++ { NewV1() diff --git a/uuid_test.go b/uuid_test.go index 6c38e98..6abfb7d 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -284,7 +284,7 @@ func TestTimestampFromV7(t *testing.T) { } } -func TestTimestampMinMaxV167(t *testing.T) { +func TestMinMaxTimestamps(t *testing.T) { tests := []struct { u UUID want time.Time @@ -359,8 +359,6 @@ var timestampBenchmarkSink Timestamp var timeBenchmarkSink time.Time func BenchmarkTimestampFrom(b *testing.B) { - b.ReportAllocs() - var err error numbUUIDs := 1000 if testing.Short() { @@ -400,8 +398,6 @@ func BenchmarkTimestampFrom(b *testing.B) { } func BenchmarkTimestampTime(b *testing.B) { - b.ReportAllocs() - var err error numbUUIDs := 1000 if testing.Short() { From b6353aeb6e8f74c2170be878f090b037d2e87d7d Mon Sep 17 00:00:00 2001 From: Moshe Katz Date: Mon, 10 Feb 2025 11:25:23 -0500 Subject: [PATCH 08/10] added more detail to comment --- uuid.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/uuid.go b/uuid.go index 4147905..18b2c9c 100644 --- a/uuid.go +++ b/uuid.go @@ -147,7 +147,10 @@ func TimestampFromV7(u UUID) (Timestamp, error) { (int64(u[4]) << 8) | int64(u[5]) - // convert to format expected by Timestamp + // UUIDv7 stores MS since 1979-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 1979. tsNanos := epochStart + (t * _100nsPerMillisecond) return Timestamp(tsNanos), nil } From 950e40ed6af2a64b1a7545a5ddbb8521e44bb122 Mon Sep 17 00:00:00 2001 From: Moshe Katz Date: Mon, 10 Feb 2025 12:33:37 -0500 Subject: [PATCH 09/10] fix typo --- uuid.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uuid.go b/uuid.go index 18b2c9c..31d0be2 100644 --- a/uuid.go +++ b/uuid.go @@ -147,10 +147,10 @@ func TimestampFromV7(u UUID) (Timestamp, error) { (int64(u[4]) << 8) | int64(u[5]) - // UUIDv7 stores MS since 1979-01-01 00:00:00, but the Timestamp + // 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 1979. + // the difference between October 1582 and January 1970. tsNanos := epochStart + (t * _100nsPerMillisecond) return Timestamp(tsNanos), nil } From 240f29615724d059eb6fa5158f7fa8b5329b77c7 Mon Sep 17 00:00:00 2001 From: Moshe Katz Date: Mon, 10 Feb 2025 15:49:52 -0500 Subject: [PATCH 10/10] use magic number instead of one-time-use constant --- uuid.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/uuid.go b/uuid.go index 31d0be2..990debd 100644 --- a/uuid.go +++ b/uuid.go @@ -89,7 +89,6 @@ const ( type Timestamp uint64 const _100nsPerSecond = 10000000 -const _100nsPerMillisecond = 10000 // Time returns the time.Time representation of a Timestamp. // @@ -151,7 +150,7 @@ func TimestampFromV7(u UUID) (Timestamp, error) { // 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 * _100nsPerMillisecond) + tsNanos := epochStart + (t * 10000) return Timestamp(tsNanos), nil }