Skip to content

Commit fa2c78c

Browse files
authored
Use Time.AppendFormat when possible (#786)
Fixes #783. We can take advantage of Time.AppendFormat by adding a new exported method to the JSON encoder, and upcasting when encoding time to use the append-based method. This avoids an allocation to convert the time to a string before appending to the buffer. The benchmarks use the production config, which uses a nanos encoder so to understand the performance difference, I tweaked `BenchmarkZapJSON` to use RFC3339TimeEncoder and ran benchmarks: ``` > benchcmp -best old new benchmark old ns/op new ns/op delta BenchmarkZapJSON-12 514 497 -3.31% benchmark old allocs new allocs delta BenchmarkZapJSON-12 5 4 -20.00% benchmark old bytes new bytes delta BenchmarkZapJSON-12 1297 1265 -2.47% ``` I also wrote a benchmark that only logs a simple message using the RFC3339TimeEncoder, ``` func BenchmarkTimeEncoder(b *testing.B) { cfg := NewProductionConfig().EncoderConfig cfg.EncodeTime = zapcore.RFC3339TimeEncoder logger := New( zapcore.NewCore( zapcore.NewJSONEncoder(cfg), &ztest.Discarder{}, DebugLevel, )) b.ResetTimer() for i := 0; i < b.N; i++ { logger.Info("test") } } ``` Results: ``` > benchcmp -best old new benchmark old ns/op new ns/op delta BenchmarkTimeEncoder-12 695 571 -17.84% benchmark old allocs new allocs delta BenchmarkTimeEncoder-12 1 0 -100.00% benchmark old bytes new bytes delta BenchmarkTimeEncoder-12 32 0 -100.00% ```
1 parent 127ea09 commit fa2c78c

File tree

5 files changed

+99
-11
lines changed

5 files changed

+99
-11
lines changed

buffer/buffer.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
// package's zero-allocation formatters.
2424
package buffer // import "go.uber.org/zap/buffer"
2525

26-
import "strconv"
26+
import (
27+
"strconv"
28+
"time"
29+
)
2730

2831
const _size = 1024 // by default, create 1 KiB buffers
2932

@@ -49,6 +52,11 @@ func (b *Buffer) AppendInt(i int64) {
4952
b.bs = strconv.AppendInt(b.bs, i, 10)
5053
}
5154

55+
// AppendTime appends the time formatted using the specified layout.
56+
func (b *Buffer) AppendTime(t time.Time, layout string) {
57+
b.bs = t.AppendFormat(b.bs, layout)
58+
}
59+
5260
// AppendUint appends an unsigned integer to the underlying buffer (assuming
5361
// base 10).
5462
func (b *Buffer) AppendUint(i uint64) {

buffer/buffer_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"bytes"
2525
"strings"
2626
"testing"
27+
"time"
2728

2829
"github.com/stretchr/testify/assert"
2930
)
@@ -46,6 +47,7 @@ func TestBufferWrites(t *testing.T) {
4647
// Intenationally introduce some floating-point error.
4748
{"AppendFloat32", func() { buf.AppendFloat(float64(float32(3.14)), 32) }, "3.14"},
4849
{"AppendWrite", func() { buf.Write([]byte("foo")) }, "foo"},
50+
{"AppendTime", func() { buf.AppendTime(time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC), time.RFC3339) }, "2000-01-02T03:04:05Z"},
4951
}
5052

5153
for _, tt := range tests {

zapcore/encoder.go

+25-3
Original file line numberDiff line numberDiff line change
@@ -112,21 +112,43 @@ func EpochNanosTimeEncoder(t time.Time, enc PrimitiveArrayEncoder) {
112112
enc.AppendInt64(t.UnixNano())
113113
}
114114

115+
func encodeTimeLayout(t time.Time, layout string, enc PrimitiveArrayEncoder) {
116+
type appendTimeEncoder interface {
117+
AppendTimeLayout(time.Time, string)
118+
}
119+
120+
if enc, ok := enc.(appendTimeEncoder); ok {
121+
enc.AppendTimeLayout(t, layout)
122+
return
123+
}
124+
125+
enc.AppendString(t.Format(layout))
126+
}
127+
115128
// ISO8601TimeEncoder serializes a time.Time to an ISO8601-formatted string
116129
// with millisecond precision.
130+
//
131+
// If enc supports AppendTimeLayout(t time.Time,layout string), it's used
132+
// instead of appending a pre-formatted string value.
117133
func ISO8601TimeEncoder(t time.Time, enc PrimitiveArrayEncoder) {
118-
enc.AppendString(t.Format("2006-01-02T15:04:05.000Z0700"))
134+
encodeTimeLayout(t, "2006-01-02T15:04:05.000Z0700", enc)
119135
}
120136

121137
// RFC3339TimeEncoder serializes a time.Time to an RFC3339-formatted string.
138+
//
139+
// If enc supports AppendTimeLayout(t time.Time,layout string), it's used
140+
// instead of appending a pre-formatted string value.
122141
func RFC3339TimeEncoder(t time.Time, enc PrimitiveArrayEncoder) {
123-
enc.AppendString(t.Format(time.RFC3339))
142+
encodeTimeLayout(t, time.RFC3339, enc)
124143
}
125144

126145
// RFC3339NanoTimeEncoder serializes a time.Time to an RFC3339-formatted string
127146
// with nanosecond precision.
147+
//
148+
// If enc supports AppendTimeLayout(t time.Time,layout string), it's used
149+
// instead of appending a pre-formatted string value.
128150
func RFC3339NanoTimeEncoder(t time.Time, enc PrimitiveArrayEncoder) {
129-
enc.AppendString(t.Format(time.RFC3339Nano))
151+
encodeTimeLayout(t, time.RFC3339Nano, enc)
130152
}
131153

132154
// UnmarshalText unmarshals text to a TimeEncoder.

zapcore/json_encoder.go

+6
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,12 @@ func (enc *jsonEncoder) AppendString(val string) {
266266
enc.buf.AppendByte('"')
267267
}
268268

269+
func (enc *jsonEncoder) AppendTimeLayout(time time.Time, layout string) {
270+
enc.buf.AppendByte('"')
271+
enc.buf.AppendTime(time, layout)
272+
enc.buf.AppendByte('"')
273+
}
274+
269275
func (enc *jsonEncoder) AppendTime(val time.Time) {
270276
cur := enc.buf.Len()
271277
enc.EncodeTime(val, enc)

zapcore/json_encoder_impl_test.go

+57-7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ import (
3636
"go.uber.org/multierr"
3737
)
3838

39+
var _defaultEncoderConfig = EncoderConfig{
40+
EncodeTime: EpochTimeEncoder,
41+
EncodeDuration: SecondsDurationEncoder,
42+
}
43+
3944
func TestJSONClone(t *testing.T) {
4045
// The parent encoder is created with plenty of excess capacity.
4146
parent := &jsonEncoder{buf: bufferpool.Get()}
@@ -224,7 +229,55 @@ func TestJSONEncoderObjectFields(t *testing.T) {
224229

225230
for _, tt := range tests {
226231
t.Run(tt.desc, func(t *testing.T) {
227-
assertOutput(t, tt.expected, tt.f)
232+
assertOutput(t, _defaultEncoderConfig, tt.expected, tt.f)
233+
})
234+
}
235+
}
236+
237+
func TestJSONEncoderTimeFormats(t *testing.T) {
238+
date := time.Date(2000, time.January, 2, 3, 4, 5, 6, time.UTC)
239+
240+
f := func(e Encoder) {
241+
e.AddTime("k", date)
242+
e.AddArray("a", ArrayMarshalerFunc(func(enc ArrayEncoder) error {
243+
enc.AppendTime(date)
244+
return nil
245+
}))
246+
}
247+
tests := []struct {
248+
desc string
249+
cfg EncoderConfig
250+
expected string
251+
}{
252+
{
253+
desc: "time.Time ISO8601",
254+
cfg: EncoderConfig{
255+
EncodeDuration: NanosDurationEncoder,
256+
EncodeTime: ISO8601TimeEncoder,
257+
},
258+
expected: `"k":"2000-01-02T03:04:05.000Z","a":["2000-01-02T03:04:05.000Z"]`,
259+
},
260+
{
261+
desc: "time.Time RFC3339",
262+
cfg: EncoderConfig{
263+
EncodeDuration: NanosDurationEncoder,
264+
EncodeTime: RFC3339TimeEncoder,
265+
},
266+
expected: `"k":"2000-01-02T03:04:05Z","a":["2000-01-02T03:04:05Z"]`,
267+
},
268+
{
269+
desc: "time.Time RFC3339Nano",
270+
cfg: EncoderConfig{
271+
EncodeDuration: NanosDurationEncoder,
272+
EncodeTime: RFC3339NanoTimeEncoder,
273+
},
274+
expected: `"k":"2000-01-02T03:04:05.000000006Z","a":["2000-01-02T03:04:05.000000006Z"]`,
275+
},
276+
}
277+
278+
for _, tt := range tests {
279+
t.Run(tt.desc, func(t *testing.T) {
280+
assertOutput(t, tt.cfg, tt.expected, f)
228281
})
229282
}
230283
}
@@ -324,7 +377,7 @@ func TestJSONEncoderArrays(t *testing.T) {
324377
return nil
325378
}))
326379
}
327-
assertOutput(t, `"array":`+tt.expected, func(enc Encoder) {
380+
assertOutput(t, _defaultEncoderConfig, `"array":`+tt.expected, func(enc Encoder) {
328381
err := f(enc)
329382
assert.NoError(t, err, "Unexpected error adding array to JSON encoder.")
330383
})
@@ -336,11 +389,8 @@ func assertJSON(t *testing.T, expected string, enc *jsonEncoder) {
336389
assert.Equal(t, expected, enc.buf.String(), "Encoded JSON didn't match expectations.")
337390
}
338391

339-
func assertOutput(t testing.TB, expected string, f func(Encoder)) {
340-
enc := &jsonEncoder{buf: bufferpool.Get(), EncoderConfig: &EncoderConfig{
341-
EncodeTime: EpochTimeEncoder,
342-
EncodeDuration: SecondsDurationEncoder,
343-
}}
392+
func assertOutput(t testing.TB, cfg EncoderConfig, expected string, f func(Encoder)) {
393+
enc := &jsonEncoder{buf: bufferpool.Get(), EncoderConfig: &cfg}
344394
f(enc)
345395
assert.Equal(t, expected, enc.buf.String(), "Unexpected encoder output after adding.")
346396

0 commit comments

Comments
 (0)