Skip to content

Commit 2673c61

Browse files
jbaeric
authored andcommitted
log/slog/internal/benchmarks: slog benchmarks
Add a suite of benchmarks for the LogAttrs method, which is intended to be fast. Updates golang#56345. Change-Id: If43f9f250bd588247c539bed87f81be7f5428c6d Reviewed-on: https://go-review.googlesource.com/c/go/+/478200 TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Jonathan Amsterdam <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 202b0b9 commit 2673c61

File tree

5 files changed

+392
-1
lines changed

5 files changed

+392
-1
lines changed

src/go/build/deps_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ var depsRules = `
386386
log/slog/internal, log/slog/internal/buffer,
387387
slices
388388
< log/slog
389-
< log/slog/internal/slogtest;
389+
< log/slog/internal/slogtest, log/slog/internal/benchmarks;
390390
391391
NET, log
392392
< net/mail;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package benchmarks contains benchmarks for slog.
6+
//
7+
// These benchmarks are loosely based on github.com/uber-go/zap/benchmarks.
8+
// They have the following desirable properties:
9+
//
10+
// - They test a complete log event, from the user's call to its return.
11+
//
12+
// - The benchmarked code is run concurrently in multiple goroutines, to
13+
// better simulate a real server (the most common environment for structured
14+
// logs).
15+
//
16+
// - Some handlers are optimistic versions of real handlers, doing real-world
17+
// tasks as fast as possible (and sometimes faster, in that an
18+
// implementation may not be concurrency-safe). This gives us an upper bound
19+
// on handler performance, so we can evaluate the (handler-independent) core
20+
// activity of the package in an end-to-end context without concern that a
21+
// slow handler implementation is skewing the results.
22+
//
23+
// - We also test the built-in handlers, for comparison.
24+
package benchmarks
25+
26+
import (
27+
"errors"
28+
"log/slog"
29+
"time"
30+
)
31+
32+
const testMessage = "Test logging, but use a somewhat realistic message length."
33+
34+
var (
35+
testTime = time.Date(2022, time.May, 1, 0, 0, 0, 0, time.UTC)
36+
testString = "7e3b3b2aaeff56a7108fe11e154200dd/7819479873059528190"
37+
testInt = 32768
38+
testDuration = 23 * time.Second
39+
testError = errors.New("fail")
40+
)
41+
42+
var testAttrs = []slog.Attr{
43+
slog.String("string", testString),
44+
slog.Int("status", testInt),
45+
slog.Duration("duration", testDuration),
46+
slog.Time("time", testTime),
47+
slog.Any("error", testError),
48+
}
49+
50+
const wantText = "time=1651363200 level=0 msg=Test logging, but use a somewhat realistic message length. string=7e3b3b2aaeff56a7108fe11e154200dd/7819479873059528190 status=32768 duration=23000000000 time=1651363200 error=fail\n"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package benchmarks
6+
7+
import (
8+
"context"
9+
"flag"
10+
"internal/race"
11+
"io"
12+
"log/slog"
13+
"log/slog/internal"
14+
"testing"
15+
)
16+
17+
func init() {
18+
flag.BoolVar(&internal.IgnorePC, "nopc", false, "do not invoke runtime.Callers")
19+
}
20+
21+
// We pass Attrs inline because it affects allocations: building
22+
// up a list outside of the benchmarked code and passing it in with "..."
23+
// reduces measured allocations.
24+
25+
func BenchmarkAttrs(b *testing.B) {
26+
ctx := context.Background()
27+
for _, handler := range []struct {
28+
name string
29+
h slog.Handler
30+
skipRace bool
31+
}{
32+
{"disabled", disabledHandler{}, false},
33+
{"async discard", newAsyncHandler(), true},
34+
{"fastText discard", newFastTextHandler(io.Discard), false},
35+
{"Text discard", slog.NewTextHandler(io.Discard), false},
36+
{"JSON discard", slog.NewJSONHandler(io.Discard), false},
37+
} {
38+
logger := slog.New(handler.h)
39+
b.Run(handler.name, func(b *testing.B) {
40+
if handler.skipRace && race.Enabled {
41+
b.Skip("skipping benchmark in race mode")
42+
}
43+
for _, call := range []struct {
44+
name string
45+
f func()
46+
}{
47+
{
48+
// The number should match nAttrsInline in slog/record.go.
49+
// This should exercise the code path where no allocations
50+
// happen in Record or Attr. If there are allocations, they
51+
// should only be from Duration.String and Time.String.
52+
"5 args",
53+
func() {
54+
logger.LogAttrs(nil, slog.LevelInfo, testMessage,
55+
slog.String("string", testString),
56+
slog.Int("status", testInt),
57+
slog.Duration("duration", testDuration),
58+
slog.Time("time", testTime),
59+
slog.Any("error", testError),
60+
)
61+
},
62+
},
63+
{
64+
"5 args ctx",
65+
func() {
66+
logger.LogAttrs(ctx, slog.LevelInfo, testMessage,
67+
slog.String("string", testString),
68+
slog.Int("status", testInt),
69+
slog.Duration("duration", testDuration),
70+
slog.Time("time", testTime),
71+
slog.Any("error", testError),
72+
)
73+
},
74+
},
75+
{
76+
"10 args",
77+
func() {
78+
logger.LogAttrs(nil, slog.LevelInfo, testMessage,
79+
slog.String("string", testString),
80+
slog.Int("status", testInt),
81+
slog.Duration("duration", testDuration),
82+
slog.Time("time", testTime),
83+
slog.Any("error", testError),
84+
slog.String("string", testString),
85+
slog.Int("status", testInt),
86+
slog.Duration("duration", testDuration),
87+
slog.Time("time", testTime),
88+
slog.Any("error", testError),
89+
)
90+
},
91+
},
92+
{
93+
// Try an extreme value to see if the results are reasonable.
94+
"40 args",
95+
func() {
96+
logger.LogAttrs(nil, slog.LevelInfo, testMessage,
97+
slog.String("string", testString),
98+
slog.Int("status", testInt),
99+
slog.Duration("duration", testDuration),
100+
slog.Time("time", testTime),
101+
slog.Any("error", testError),
102+
slog.String("string", testString),
103+
slog.Int("status", testInt),
104+
slog.Duration("duration", testDuration),
105+
slog.Time("time", testTime),
106+
slog.Any("error", testError),
107+
slog.String("string", testString),
108+
slog.Int("status", testInt),
109+
slog.Duration("duration", testDuration),
110+
slog.Time("time", testTime),
111+
slog.Any("error", testError),
112+
slog.String("string", testString),
113+
slog.Int("status", testInt),
114+
slog.Duration("duration", testDuration),
115+
slog.Time("time", testTime),
116+
slog.Any("error", testError),
117+
slog.String("string", testString),
118+
slog.Int("status", testInt),
119+
slog.Duration("duration", testDuration),
120+
slog.Time("time", testTime),
121+
slog.Any("error", testError),
122+
slog.String("string", testString),
123+
slog.Int("status", testInt),
124+
slog.Duration("duration", testDuration),
125+
slog.Time("time", testTime),
126+
slog.Any("error", testError),
127+
slog.String("string", testString),
128+
slog.Int("status", testInt),
129+
slog.Duration("duration", testDuration),
130+
slog.Time("time", testTime),
131+
slog.Any("error", testError),
132+
slog.String("string", testString),
133+
slog.Int("status", testInt),
134+
slog.Duration("duration", testDuration),
135+
slog.Time("time", testTime),
136+
slog.Any("error", testError),
137+
)
138+
},
139+
},
140+
} {
141+
b.Run(call.name, func(b *testing.B) {
142+
b.ReportAllocs()
143+
b.RunParallel(func(pb *testing.PB) {
144+
for pb.Next() {
145+
call.f()
146+
}
147+
})
148+
})
149+
}
150+
})
151+
}
152+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package benchmarks
6+
7+
// Handlers for benchmarking.
8+
9+
import (
10+
"context"
11+
"fmt"
12+
"io"
13+
"log/slog"
14+
"log/slog/internal/buffer"
15+
"strconv"
16+
"time"
17+
)
18+
19+
// A fastTextHandler writes a Record to an io.Writer in a format similar to
20+
// slog.TextHandler, but without quoting or locking. It has a few other
21+
// performance-motivated shortcuts, like writing times as seconds since the
22+
// epoch instead of strings.
23+
//
24+
// It is intended to represent a high-performance Handler that synchronously
25+
// writes text (as opposed to binary).
26+
type fastTextHandler struct {
27+
w io.Writer
28+
}
29+
30+
func newFastTextHandler(w io.Writer) slog.Handler {
31+
return &fastTextHandler{w: w}
32+
}
33+
34+
func (h *fastTextHandler) Enabled(context.Context, slog.Level) bool { return true }
35+
36+
func (h *fastTextHandler) Handle(_ context.Context, r slog.Record) error {
37+
buf := buffer.New()
38+
defer buf.Free()
39+
40+
if !r.Time.IsZero() {
41+
buf.WriteString("time=")
42+
h.appendTime(buf, r.Time)
43+
buf.WriteByte(' ')
44+
}
45+
buf.WriteString("level=")
46+
*buf = strconv.AppendInt(*buf, int64(r.Level), 10)
47+
buf.WriteByte(' ')
48+
buf.WriteString("msg=")
49+
buf.WriteString(r.Message)
50+
r.Attrs(func(a slog.Attr) {
51+
buf.WriteByte(' ')
52+
buf.WriteString(a.Key)
53+
buf.WriteByte('=')
54+
h.appendValue(buf, a.Value)
55+
})
56+
buf.WriteByte('\n')
57+
_, err := h.w.Write(*buf)
58+
return err
59+
}
60+
61+
func (h *fastTextHandler) appendValue(buf *buffer.Buffer, v slog.Value) {
62+
switch v.Kind() {
63+
case slog.KindString:
64+
buf.WriteString(v.String())
65+
case slog.KindInt64:
66+
*buf = strconv.AppendInt(*buf, v.Int64(), 10)
67+
case slog.KindUint64:
68+
*buf = strconv.AppendUint(*buf, v.Uint64(), 10)
69+
case slog.KindFloat64:
70+
*buf = strconv.AppendFloat(*buf, v.Float64(), 'g', -1, 64)
71+
case slog.KindBool:
72+
*buf = strconv.AppendBool(*buf, v.Bool())
73+
case slog.KindDuration:
74+
*buf = strconv.AppendInt(*buf, v.Duration().Nanoseconds(), 10)
75+
case slog.KindTime:
76+
h.appendTime(buf, v.Time())
77+
case slog.KindAny:
78+
a := v.Any()
79+
switch a := a.(type) {
80+
case error:
81+
buf.WriteString(a.Error())
82+
default:
83+
fmt.Fprint(buf, a)
84+
}
85+
default:
86+
panic(fmt.Sprintf("bad kind: %s", v.Kind()))
87+
}
88+
}
89+
90+
func (h *fastTextHandler) appendTime(buf *buffer.Buffer, t time.Time) {
91+
*buf = strconv.AppendInt(*buf, t.Unix(), 10)
92+
}
93+
94+
func (h *fastTextHandler) WithAttrs([]slog.Attr) slog.Handler {
95+
panic("fastTextHandler: With unimplemented")
96+
}
97+
98+
func (*fastTextHandler) WithGroup(string) slog.Handler {
99+
panic("fastTextHandler: WithGroup unimplemented")
100+
}
101+
102+
// An asyncHandler simulates a Handler that passes Records to a
103+
// background goroutine for processing.
104+
// Because sending to a channel can be expensive due to locking,
105+
// we simulate a lock-free queue by adding the Record to a ring buffer.
106+
// Omitting the locking makes this little more than a copy of the Record,
107+
// but that is a worthwhile thing to measure because Records are on the large
108+
// side. Since nothing actually reads from the ring buffer, it can handle an
109+
// arbitrary number of Records without either blocking or allocation.
110+
type asyncHandler struct {
111+
ringBuffer [100]slog.Record
112+
next int
113+
}
114+
115+
func newAsyncHandler() *asyncHandler {
116+
return &asyncHandler{}
117+
}
118+
119+
func (*asyncHandler) Enabled(context.Context, slog.Level) bool { return true }
120+
121+
func (h *asyncHandler) Handle(_ context.Context, r slog.Record) error {
122+
h.ringBuffer[h.next] = r.Clone()
123+
h.next = (h.next + 1) % len(h.ringBuffer)
124+
return nil
125+
}
126+
127+
func (*asyncHandler) WithAttrs([]slog.Attr) slog.Handler {
128+
panic("asyncHandler: With unimplemented")
129+
}
130+
131+
func (*asyncHandler) WithGroup(string) slog.Handler {
132+
panic("asyncHandler: WithGroup unimplemented")
133+
}
134+
135+
// A disabledHandler's Enabled method always returns false.
136+
type disabledHandler struct{}
137+
138+
func (disabledHandler) Enabled(context.Context, slog.Level) bool { return false }
139+
func (disabledHandler) Handle(context.Context, slog.Record) error { panic("should not be called") }
140+
141+
func (disabledHandler) WithAttrs([]slog.Attr) slog.Handler {
142+
panic("disabledHandler: With unimplemented")
143+
}
144+
145+
func (disabledHandler) WithGroup(string) slog.Handler {
146+
panic("disabledHandler: WithGroup unimplemented")
147+
}

0 commit comments

Comments
 (0)