Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Taking advantage of Time.AppendFormat #783

Closed
vrischmann opened this issue Feb 10, 2020 · 2 comments · Fixed by #786
Closed

Taking advantage of Time.AppendFormat #783

vrischmann opened this issue Feb 10, 2020 · 2 comments · Fixed by #786

Comments

@vrischmann
Copy link

Hi,

I'm opening this issue to discuss a potential API change to the buffer type.

I have a custom encoder where times are encoded as strings formatted with the Format method from Go. Currently I have to do this:

enc.buf.AppendString(value.UTC().Format(time.RFC3339))

but it would be nice if we could just use the AppendFormat method to avoid an allocation.

I'm thinking of adding a method to the buffer type with this signature: AppendTime(t time.Time, layout string).

Another solution would be to allow users to set the byte slice used by the buffer type so that I could write this:

buf := enc.buf.Bytes()
buf = value.AppendFormat(buf, time.RFC3339)
enc.buf.SetBytes(buf)

less elegant than having AppendTime but maybe a good solution if you don't want to special case times in the buffer type.

@prashantv
Copy link
Collaborator

I'm not opposed to adding the AppendFormat function to the buffer type. It currently only deals with types that are part of the language, but I don't think supporting common standard library types is a bad idea.

That said, if we do support AppendFormat in the buffer type, it should be used within zap itself by the default encoder. This may be tricky since the TimeEncoder type receives a PrimitiveArrayEncoder which doesn't (intentionally) support append time.

Since we can't extend the interface in 1.0, the included time encoders in zap could try and cast the passed in encoder to see if it supports an AppendFormat function, and if so, use it. I'm not sure what the performance impact of the cast would be, so we should measure that.

prashantv added a commit that referenced this issue Feb 13, 2020
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%
```
prashantv added a commit that referenced this issue Feb 19, 2020
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%
```
@vrischmann
Copy link
Author

Just want to say thanks for working on this, much appreciated.

cgxxv pushed a commit to cgxxv/zap that referenced this issue Mar 25, 2022
Fixes uber-go#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%
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging a pull request may close this issue.

2 participants