| // Copyright 2022 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package slog |
| |
| import ( |
| "fmt" |
| "testing" |
| "time" |
| "unsafe" |
| ) |
| |
| func TestEqual(t *testing.T) { |
| var x, y int |
| vals := []Attr{ |
| {}, |
| Int64("key", 1), |
| Int64("key", 2), |
| Float64("key", 3.5), |
| Float64("key", 3.7), |
| Bool("key", true), |
| Bool("key", false), |
| Any("key", &x), |
| Any("key", &y), |
| } |
| for i, v1 := range vals { |
| for j, v2 := range vals { |
| got := v1.Equal(v2) |
| want := i == j |
| if got != want { |
| t.Errorf("%v.Equal(%v): got %t, want %t", v1, v2, got, want) |
| } |
| } |
| } |
| } |
| |
| func TestNilAttr(t *testing.T) { |
| n := Any("k", nil) |
| if g := n.Value(); g != nil { |
| t.Errorf("got %#v, want nil", g) |
| } |
| } |
| |
| func panics(f func()) (b bool) { |
| defer func() { |
| if x := recover(); x != nil { |
| b = true |
| } |
| }() |
| f() |
| return false |
| } |
| |
| func TestString(t *testing.T) { |
| for _, test := range []struct { |
| v Attr |
| want string |
| }{ |
| {Int64("key", -3), "-3"}, |
| {Float64("key", .15), "0.15"}, |
| {Bool("key", true), "true"}, |
| {String("key", "foo"), "foo"}, |
| {Any("key", time.Duration(3*time.Second)), "3s"}, |
| } { |
| if got := test.v.String(); got != test.want { |
| t.Errorf("%#v: got %q, want %q", test.v, got, test.want) |
| } |
| } |
| } |
| |
| func TestAttrNoAlloc(t *testing.T) { |
| // Assign values just to make sure the compiler doesn't optimize away the statements. |
| var ( |
| i int64 |
| u uint64 |
| f float64 |
| b bool |
| s string |
| x any |
| p = &i |
| d time.Duration |
| ) |
| a := int(testing.AllocsPerRun(5, func() { |
| i = Int64("key", 1).Int64() |
| u = Uint64("key", 1).Uint64() |
| f = Float64("key", 1).Float64() |
| b = Bool("key", true).Bool() |
| s = String("key", "foo").String() |
| d = Duration("key", d).Duration() |
| x = Any("key", p).Value() |
| })) |
| if a != 0 { |
| t.Errorf("got %d allocs, want zero", a) |
| } |
| _ = u |
| _ = f |
| _ = b |
| _ = s |
| _ = x |
| } |
| |
| func TestAnyLevelAlloc(t *testing.T) { |
| // Because typical Levels are small integers, |
| // they are zero-alloc. |
| var a Attr |
| x := DebugLevel + 100 |
| wantAllocs(t, 0, func() { a = Any("k", x) }) |
| _ = a |
| } |
| |
| func TestAnyLevel(t *testing.T) { |
| x := DebugLevel + 100 |
| a := Any("k", x) |
| v := a.Value() |
| if _, ok := v.(Level); !ok { |
| t.Errorf("wanted Level, got %T", v) |
| } |
| } |
| |
| //////////////// Benchmark for accessing Attr values |
| |
| // The "As" form is the slowest. |
| // The switch-panic and visitor times are almost the same. |
| // BenchmarkDispatch/switch-checked-8 8669427 137.7 ns/op |
| // BenchmarkDispatch/As-8 8212087 145.3 ns/op |
| // BenchmarkDispatch/Visit-8 8926146 135.3 ns/op |
| func BenchmarkDispatch(b *testing.B) { |
| kvs := []Attr{ |
| Int64("i", 32768), |
| Uint64("u", 0xfacecafe), |
| String("s", "anything"), |
| Bool("b", true), |
| Float64("f", 1.2345), |
| Duration("d", time.Second), |
| Any("a", b), |
| } |
| var ( |
| ii int64 |
| s string |
| bb bool |
| u uint64 |
| d time.Duration |
| f float64 |
| a any |
| ) |
| b.Run("switch-checked", func(b *testing.B) { |
| for i := 0; i < b.N; i++ { |
| for _, kv := range kvs { |
| switch kv.Kind() { |
| case StringKind: |
| s = kv.String() |
| case Int64Kind: |
| ii = kv.Int64() |
| case Uint64Kind: |
| u = kv.Uint64() |
| case Float64Kind: |
| f = kv.Float64() |
| case BoolKind: |
| bb = kv.Bool() |
| case DurationKind: |
| d = kv.Duration() |
| case AnyKind: |
| a = kv.Value() |
| default: |
| panic("bad kind") |
| } |
| } |
| } |
| _ = ii |
| _ = s |
| _ = bb |
| _ = u |
| _ = d |
| _ = f |
| _ = a |
| |
| }) |
| b.Run("As", func(b *testing.B) { |
| for i := 0; i < b.N; i++ { |
| for _, kv := range kvs { |
| if v, ok := kv.AsString(); ok { |
| s = v |
| } else if v, ok := kv.AsInt64(); ok { |
| ii = v |
| } else if v, ok := kv.AsUint64(); ok { |
| u = v |
| } else if v, ok := kv.AsFloat64(); ok { |
| f = v |
| } else if v, ok := kv.AsBool(); ok { |
| bb = v |
| } else if v, ok := kv.AsDuration(); ok { |
| d = v |
| } else if v, ok := kv.AsAny(); ok { |
| a = v |
| } else { |
| panic("bad kind") |
| } |
| } |
| } |
| _ = ii |
| _ = s |
| _ = bb |
| _ = u |
| _ = d |
| _ = f |
| _ = a |
| }) |
| |
| b.Run("Visit", func(b *testing.B) { |
| v := &setVisitor{} |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| for _, kv := range kvs { |
| kv.Visit(v) |
| } |
| } |
| }) |
| } |
| |
| type setVisitor struct { |
| i int64 |
| s string |
| b bool |
| u uint64 |
| d time.Duration |
| f float64 |
| a any |
| } |
| |
| func (v *setVisitor) String(s string) { v.s = s } |
| func (v *setVisitor) Int64(i int64) { v.i = i } |
| func (v *setVisitor) Uint64(x uint64) { v.u = x } |
| func (v *setVisitor) Float64(x float64) { v.f = x } |
| func (v *setVisitor) Bool(x bool) { v.b = x } |
| func (v *setVisitor) Duration(x time.Duration) { v.d = x } |
| func (v *setVisitor) Any(x any) { v.a = x } |
| |
| // When dispatching on all types, the "As" functions are slightly slower |
| // than switching on the kind and then calling a function that checks |
| // the kind again. See BenchmarkDispatch above. |
| |
| func (a Attr) AsString() (string, bool) { |
| if a.Kind() == StringKind { |
| return a.str(), true |
| } |
| return "", false |
| } |
| |
| func (a Attr) AsInt64() (int64, bool) { |
| if a.Kind() == Int64Kind { |
| return int64(a.num), true |
| } |
| return 0, false |
| } |
| |
| func (a Attr) AsUint64() (uint64, bool) { |
| if a.Kind() == Uint64Kind { |
| return a.num, true |
| } |
| return 0, false |
| } |
| |
| func (a Attr) AsFloat64() (float64, bool) { |
| if a.Kind() == Float64Kind { |
| return a.float(), true |
| } |
| return 0, false |
| } |
| |
| func (a Attr) AsBool() (bool, bool) { |
| if a.Kind() == BoolKind { |
| return a.bool(), true |
| } |
| return false, false |
| } |
| |
| func (a Attr) AsDuration() (time.Duration, bool) { |
| if a.Kind() == DurationKind { |
| return a.duration(), true |
| } |
| return 0, false |
| } |
| |
| func (a Attr) AsAny() (any, bool) { |
| if a.Kind() == AnyKind { |
| return a.any, true |
| } |
| return nil, false |
| } |
| |
| // Problem: adding a type means adding a method, which is a breaking change. |
| // Using an unexported method to force embedding will make programs compile, |
| // But they will panic at runtime when we call the new method. |
| type Visitor interface { |
| String(string) |
| Int64(int64) |
| Uint64(uint64) |
| Float64(float64) |
| Bool(bool) |
| Duration(time.Duration) |
| Any(any) |
| } |
| |
| func (a Attr) Visit(v Visitor) { |
| switch a.Kind() { |
| case StringKind: |
| v.String(a.str()) |
| case Int64Kind: |
| v.Int64(int64(a.num)) |
| case Uint64Kind: |
| v.Uint64(a.num) |
| case BoolKind: |
| v.Bool(a.bool()) |
| case Float64Kind: |
| v.Float64(a.float()) |
| case DurationKind: |
| v.Duration(a.duration()) |
| case AnyKind: |
| v.Any(a.any) |
| default: |
| panic("bad kind") |
| } |
| } |
| |
| // A Attr with "unsafe" strings is significantly faster: |
| // safe: 1785 ns/op, 0 allocs |
| // unsafe: 690 ns/op, 0 allocs |
| |
| // Run this with and without -tags unsafe_kvs to compare. |
| func BenchmarkUnsafeStrings(b *testing.B) { |
| b.ReportAllocs() |
| dst := make([]Attr, 100) |
| src := make([]Attr, len(dst)) |
| b.Logf("Attr size = %d", unsafe.Sizeof(Attr{})) |
| for i := range src { |
| src[i] = String("k", fmt.Sprintf("string#%d", i)) |
| } |
| b.ResetTimer() |
| var d string |
| for i := 0; i < b.N; i++ { |
| copy(dst, src) |
| for _, a := range dst { |
| d = a.String() |
| } |
| } |
| _ = d |
| } |