diff --git a/util/deephash/deephash.go b/util/deephash/deephash.go index 93ab1abc5..a5036028d 100644 --- a/util/deephash/deephash.go +++ b/util/deephash/deephash.go @@ -424,11 +424,56 @@ func genHashPtrToMemoryRange(eleType reflect.Type) typeHasherFunc { } } -const debug = false +const debug = true + +// canMaybeFastPath reports whether t is a type that we might be able to +// hash quickly at runtime with a typeHasherFunc. +// +// If it returns false, the slow path should be used directly. +func canMaybeFastPath(t reflect.Type) (ret bool) { + defer func() { + if !ret { + log.Printf("Can't on %v", t) + } + }() + ti := getTypeInfo(t) + if ti.isRecursive { + return false + } + switch t.Kind() { + case reflect.Map, reflect.Interface, reflect.Func, reflect.UnsafePointer, reflect.Chan: + return false + case reflect.Array: + return t.Size() == 0 || canMaybeFastPath(t.Elem()) + case reflect.Slice, reflect.Pointer: + return canMaybeFastPath(t.Elem()) + case reflect.Struct: + if t == timeTimeType || t.Implements(appenderToType) { + return true + } + for i, n := 0, t.NumField(); i < n; i++ { + sf := t.Field(i) + if sf.Type.Size() == 0 { + continue + } + if !canMaybeFastPath(sf.Type) { + return false + } + } + return true + } + return true +} func genTypeHasher(t reflect.Type) typeHasherFunc { + fastable := canMaybeFastPath(t) if debug { - log.Printf("generating func for %v", t) + log.Printf("generating func for %v; fastable=%v", t, fastable) + } + if !fastable { + return func(h *hasher, v reflect.Value) bool { + return false + } } switch t.Kind() { diff --git a/util/deephash/deephash_test.go b/util/deephash/deephash_test.go index 3f20b8e61..0b8211997 100644 --- a/util/deephash/deephash_test.go +++ b/util/deephash/deephash_test.go @@ -317,6 +317,45 @@ type RecursiveStruct struct { } } +func TestCanMaybeFastPath(t *testing.T) { + type RecursiveStruct struct { + v *RecursiveStruct + } + type RecursiveChan chan *RecursiveChan + type FastStruct struct{ _, _ string } + + tests := []struct { + val any + want bool + }{ + {val: 42, want: true}, + {val: "string", want: true}, + {val: 1 + 2i, want: true}, + {val: struct{}{}, want: true}, + {val: (*RecursiveStruct)(nil), want: false}, + {val: RecursiveStruct{}, want: false}, + {val: FastStruct{}, want: true}, + {val: time.Unix(0, 0), want: true}, + {val: structs.Incomparable{}, want: true}, // ignore its [0]func() + {val: tailcfg.NetPortRange{}, want: true}, // uses structs.Incomparable + {val: (*tailcfg.Node)(nil), want: true}, + {val: map[string]bool{}, want: false}, + {val: func() {}, want: false}, + {val: make(chan int), want: false}, + {val: unsafe.Pointer(nil), want: false}, + {val: make(RecursiveChan), want: false}, + {val: make(chan int), want: false}, + {val: tailcfg.SSHRule{}, want: false}, // contains a map + {val: (*tailcfg.SSHRule)(nil), want: false}, + } + for _, tt := range tests { + got := canMaybeFastPath(reflect.TypeOf(tt.val)) + if got != tt.want { + t.Errorf("for type %T: got %v, want %v", tt.val, got, tt.want) + } + } +} + type IntThenByte struct { i int b byte @@ -611,6 +650,17 @@ func TestGetTypeHasher(t *testing.T) { 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", }, + { + name: "no_hashing_for_mixed_fast_slow_type", + val: struct { + A, B string + M map[string]string + }{ + "foo", "bar", map[string]string{"alice": "bob"}, + }, + want: false, + out: "", // no "foo" or "bar" included + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {