mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-05 23:07:44 +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:
|
||||
// - Floating-point values are compared based on the raw bits,
|
||||
// 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
|
||||
// the AppendTo method to produce a textual representation of the value.
|
||||
// Thus, two values are equal if AppendTo produces the same bytes.
|
||||
@ -522,12 +525,16 @@ func (h *hasher) hashComplex128v(v addressableValue) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// hashString hashes v, of kind time.Time.
|
||||
// hashTimev hashes v, of kind time.Time.
|
||||
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())
|
||||
b := t.AppendFormat(h.scratch[:1], time.RFC3339Nano)
|
||||
b[0] = byte(len(b) - 1) // more than sufficient width; if not, good enough.
|
||||
h.HashBytes(b)
|
||||
_, offset := t.Zone()
|
||||
h.HashUint64(uint64(t.Unix()))
|
||||
h.HashUint32(uint32(t.Nanosecond()))
|
||||
h.HashUint32(uint32(offset))
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
import (
|
||||
"archive/tar"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash"
|
||||
"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) {
|
||||
switch runtime.GOARCH {
|
||||
case "amd64", "arm64", "arm", "386", "riscv64":
|
||||
@ -521,28 +527,28 @@ func TestGetTypeHasher(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "time",
|
||||
val: time.Unix(0, 0).In(time.UTC),
|
||||
out: "\x141970-01-01T00:00:00Z",
|
||||
val: time.Unix(1234, 5678).In(time.UTC),
|
||||
out: u64(1234) + u32(5678) + u32(0),
|
||||
},
|
||||
{
|
||||
name: "time_ptr", // addressable, as opposed to "time" test above
|
||||
val: ptrTo(time.Unix(0, 0).In(time.UTC)),
|
||||
out: "\x01\x141970-01-01T00:00:00Z",
|
||||
val: ptrTo(time.Unix(1234, 5678).In(time.UTC)),
|
||||
out: u8(1) + u64(1234) + u32(5678) + u32(0),
|
||||
},
|
||||
{
|
||||
name: "time_ptr_via_unexported",
|
||||
val: testtype.NewUnexportedAddressableTime(time.Unix(0, 0).In(time.UTC)),
|
||||
out: "\x01\x141970-01-01T00:00:00Z",
|
||||
val: testtype.NewUnexportedAddressableTime(time.Unix(1234, 5678).In(time.UTC)),
|
||||
out: u8(1) + u64(1234) + u32(5678) + u32(0),
|
||||
},
|
||||
{
|
||||
name: "time_ptr_via_unexported_value",
|
||||
val: *testtype.NewUnexportedAddressableTime(time.Unix(0, 0).In(time.UTC)),
|
||||
out: "\x141970-01-01T00:00:00Z",
|
||||
val: *testtype.NewUnexportedAddressableTime(time.Unix(1234, 5678).In(time.UTC)),
|
||||
out: u64(1234) + u32(5678) + u32(0),
|
||||
},
|
||||
{
|
||||
name: "time_custom_zone",
|
||||
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",
|
||||
@ -616,7 +622,7 @@ func TestGetTypeHasher(t *testing.T) {
|
||||
{
|
||||
name: "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 {
|
||||
@ -885,3 +891,48 @@ func (h *hashBuffer) Reset() {
|
||||
h.Hash.Reset()
|
||||
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