mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-16 03:31:39 +00:00
util/deephash: use binary encoding of time.Time (#5352)
Formatting a time.Time as RFC3339 is slow. See https://go.dev/issue/54093 Now that we have efficient hashing of fixed-width integers, just hash the time.Time as a binary value. Performance: Hash-24 19.0µs ± 1% 18.6µs ± 1% -2.03% (p=0.000 n=10+9) TailcfgNode-24 1.79µs ± 1% 1.40µs ± 1% -21.74% (p=0.000 n=10+9) Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
parent
0476c8ebc6
commit
548fa63e49
@ -11,6 +11,9 @@
|
|||||||
// The definition of equality is identical to reflect.DeepEqual except:
|
// The definition of equality is identical to reflect.DeepEqual except:
|
||||||
// - Floating-point values are compared based on the raw bits,
|
// - Floating-point values are compared based on the raw bits,
|
||||||
// which means that NaNs (with the same bit pattern) are treated as equal.
|
// which means that NaNs (with the same bit pattern) are treated as equal.
|
||||||
|
// - time.Time are compared based on whether they are the same instant in time
|
||||||
|
// and also in the same zone offset. Monotonic measurements and zone names
|
||||||
|
// are ignored as part of the hash.
|
||||||
// - Types which implement interface { AppendTo([]byte) []byte } use
|
// - Types which implement interface { AppendTo([]byte) []byte } use
|
||||||
// the AppendTo method to produce a textual representation of the value.
|
// the AppendTo method to produce a textual representation of the value.
|
||||||
// Thus, two values are equal if AppendTo produces the same bytes.
|
// Thus, two values are equal if AppendTo produces the same bytes.
|
||||||
@ -522,12 +525,16 @@ func (h *hasher) hashComplex128v(v addressableValue) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// hashString hashes v, of kind time.Time.
|
// hashTimev hashes v, of kind time.Time.
|
||||||
func (h *hasher) hashTimev(v addressableValue) bool {
|
func (h *hasher) hashTimev(v addressableValue) bool {
|
||||||
|
// Include the zone offset (but not the name) to keep
|
||||||
|
// Hash(t1) == Hash(t2) being semantically equivalent to
|
||||||
|
// t1.Format(time.RFC3339Nano) == t2.Format(time.RFC3339Nano).
|
||||||
t := *(*time.Time)(v.Addr().UnsafePointer())
|
t := *(*time.Time)(v.Addr().UnsafePointer())
|
||||||
b := t.AppendFormat(h.scratch[:1], time.RFC3339Nano)
|
_, offset := t.Zone()
|
||||||
b[0] = byte(len(b) - 1) // more than sufficient width; if not, good enough.
|
h.HashUint64(uint64(t.Unix()))
|
||||||
h.HashBytes(b)
|
h.HashUint32(uint32(t.Nanosecond()))
|
||||||
|
h.HashUint32(uint32(offset))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ package deephash
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
@ -400,6 +401,11 @@ func TestCanMemHash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func u8(n uint8) string { return string([]byte{n}) }
|
||||||
|
func u16(n uint16) string { return string(binary.LittleEndian.AppendUint16(nil, n)) }
|
||||||
|
func u32(n uint32) string { return string(binary.LittleEndian.AppendUint32(nil, n)) }
|
||||||
|
func u64(n uint64) string { return string(binary.LittleEndian.AppendUint64(nil, n)) }
|
||||||
|
|
||||||
func TestGetTypeHasher(t *testing.T) {
|
func TestGetTypeHasher(t *testing.T) {
|
||||||
switch runtime.GOARCH {
|
switch runtime.GOARCH {
|
||||||
case "amd64", "arm64", "arm", "386", "riscv64":
|
case "amd64", "arm64", "arm", "386", "riscv64":
|
||||||
@ -521,28 +527,28 @@ func TestGetTypeHasher(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "time",
|
name: "time",
|
||||||
val: time.Unix(0, 0).In(time.UTC),
|
val: time.Unix(1234, 5678).In(time.UTC),
|
||||||
out: "\x141970-01-01T00:00:00Z",
|
out: u64(1234) + u32(5678) + u32(0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "time_ptr", // addressable, as opposed to "time" test above
|
name: "time_ptr", // addressable, as opposed to "time" test above
|
||||||
val: ptrTo(time.Unix(0, 0).In(time.UTC)),
|
val: ptrTo(time.Unix(1234, 5678).In(time.UTC)),
|
||||||
out: "\x01\x141970-01-01T00:00:00Z",
|
out: u8(1) + u64(1234) + u32(5678) + u32(0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "time_ptr_via_unexported",
|
name: "time_ptr_via_unexported",
|
||||||
val: testtype.NewUnexportedAddressableTime(time.Unix(0, 0).In(time.UTC)),
|
val: testtype.NewUnexportedAddressableTime(time.Unix(1234, 5678).In(time.UTC)),
|
||||||
out: "\x01\x141970-01-01T00:00:00Z",
|
out: u8(1) + u64(1234) + u32(5678) + u32(0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "time_ptr_via_unexported_value",
|
name: "time_ptr_via_unexported_value",
|
||||||
val: *testtype.NewUnexportedAddressableTime(time.Unix(0, 0).In(time.UTC)),
|
val: *testtype.NewUnexportedAddressableTime(time.Unix(1234, 5678).In(time.UTC)),
|
||||||
out: "\x141970-01-01T00:00:00Z",
|
out: u64(1234) + u32(5678) + u32(0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "time_custom_zone",
|
name: "time_custom_zone",
|
||||||
val: time.Unix(1655311822, 0).In(time.FixedZone("FOO", -60*60)),
|
val: time.Unix(1655311822, 0).In(time.FixedZone("FOO", -60*60)),
|
||||||
out: "\x192022-06-15T15:50:22-01:00",
|
out: u64(1655311822) + u32(0) + u32(math.MaxUint32-60*60+1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "time_nil",
|
name: "time_nil",
|
||||||
@ -616,7 +622,7 @@ func TestGetTypeHasher(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "tailcfg.Node",
|
name: "tailcfg.Node",
|
||||||
val: &tailcfg.Node{},
|
val: &tailcfg.Node{},
|
||||||
out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x140001-01-01T00:00:00Z\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x140001-01-01T00:00:00Z\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + u64(uint64(time.Time{}.Unix())) + u32(0) + u32(0) + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + u64(uint64(time.Time{}.Unix())) + u32(0) + u32(0) + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -885,3 +891,48 @@ func (h *hashBuffer) Reset() {
|
|||||||
h.Hash.Reset()
|
h.Hash.Reset()
|
||||||
h.B = h.B[:0]
|
h.B = h.B[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FuzzTime(f *testing.F) {
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(0), false, "", 0)
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(0), true, "", 0)
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(0), true, "hello", 0)
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(0), true, "", 1234)
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(0), true, "hello", 1234)
|
||||||
|
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(1), false, "", 0)
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(1), true, "", 0)
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(1), true, "hello", 0)
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(1), true, "", 1234)
|
||||||
|
f.Add(int64(0), int64(0), false, "", 0, int64(0), int64(1), true, "hello", 1234)
|
||||||
|
|
||||||
|
f.Add(int64(math.MaxInt64), int64(math.MaxInt64), false, "", 0, int64(math.MaxInt64), int64(math.MaxInt64), false, "", 0)
|
||||||
|
f.Add(int64(math.MaxInt64), int64(math.MaxInt64), false, "", 0, int64(math.MaxInt64), int64(math.MaxInt64), true, "", 0)
|
||||||
|
f.Add(int64(math.MaxInt64), int64(math.MaxInt64), false, "", 0, int64(math.MaxInt64), int64(math.MaxInt64), true, "hello", 0)
|
||||||
|
f.Add(int64(math.MaxInt64), int64(math.MaxInt64), false, "", 0, int64(math.MaxInt64), int64(math.MaxInt64), true, "", 1234)
|
||||||
|
f.Add(int64(math.MaxInt64), int64(math.MaxInt64), false, "", 0, int64(math.MaxInt64), int64(math.MaxInt64), true, "hello", 1234)
|
||||||
|
|
||||||
|
f.Add(int64(math.MinInt64), int64(math.MinInt64), false, "", 0, int64(math.MinInt64), int64(math.MinInt64), false, "", 0)
|
||||||
|
f.Add(int64(math.MinInt64), int64(math.MinInt64), false, "", 0, int64(math.MinInt64), int64(math.MinInt64), true, "", 0)
|
||||||
|
f.Add(int64(math.MinInt64), int64(math.MinInt64), false, "", 0, int64(math.MinInt64), int64(math.MinInt64), true, "hello", 0)
|
||||||
|
f.Add(int64(math.MinInt64), int64(math.MinInt64), false, "", 0, int64(math.MinInt64), int64(math.MinInt64), true, "", 1234)
|
||||||
|
f.Add(int64(math.MinInt64), int64(math.MinInt64), false, "", 0, int64(math.MinInt64), int64(math.MinInt64), true, "hello", 1234)
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T,
|
||||||
|
s1, ns1 int64, loc1 bool, name1 string, off1 int,
|
||||||
|
s2, ns2 int64, loc2 bool, name2 string, off2 int,
|
||||||
|
) {
|
||||||
|
t1 := time.Unix(s1, ns1)
|
||||||
|
if loc1 {
|
||||||
|
t1.In(time.FixedZone(name1, off1))
|
||||||
|
}
|
||||||
|
t2 := time.Unix(s2, ns2)
|
||||||
|
if loc2 {
|
||||||
|
t2.In(time.FixedZone(name2, off2))
|
||||||
|
}
|
||||||
|
got := Hash(&t1) == Hash(&t2)
|
||||||
|
want := t1.Format(time.RFC3339Nano) == t2.Format(time.RFC3339Nano)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("time.Time(%s) == time.Time(%s) mismatches hash equivalent", t1.Format(time.RFC3339Nano), t2.Format(time.RFC3339Nano))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user