diff --git a/go.mod b/go.mod index 1c772810a4..762dcf3bea 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/rs/zerolog v1.20.0 github.com/streadway/amqp v1.0.0 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 + github.com/ugorji/go/codec v1.2.4 github.com/urfave/cli/v2 v2.2.0 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.16.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.16.0 diff --git a/go.sum b/go.sum index b428a0a06e..b2d9d03e88 100644 --- a/go.sum +++ b/go.sum @@ -700,7 +700,12 @@ github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= +github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc= +github.com/ugorji/go v1.2.4/go.mod h1:EuaSCk8iZMdIspsu6HXH7X2UGKw1ezO4wCfGszGmmo4= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8= +github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= diff --git a/internal/codec/codec.go b/internal/codec/codec.go new file mode 100644 index 0000000000..dbeebfa5ce --- /dev/null +++ b/internal/codec/codec.go @@ -0,0 +1,64 @@ +// Package codec is a unified place for configuring and allocating JSON encoders +// and decoders. +package codec + +import ( + "io" + "sync" + + "github.com/ugorji/go/codec" +) + +var jsonHandle codec.JsonHandle + +func init() { + // This is documented to cause "smart buffering". + jsonHandle.WriterBufferSize = 4096 + jsonHandle.ReaderBufferSize = 4096 +} + +// Encoder and decoder pools, to reuse if possible. +var ( + encPool = sync.Pool{ + New: func() interface{} { + return codec.NewEncoder(nil, &jsonHandle) + }, + } + decPool = sync.Pool{ + New: func() interface{} { + return codec.NewDecoder(nil, &jsonHandle) + }, + } +) + +// Encoder encodes. +type Encoder = codec.Encoder + +// GetEncoder returns an encoder configured to write to w. +func GetEncoder(w io.Writer) *Encoder { + e := encPool.Get().(*Encoder) + e.Reset(w) + return e +} + +// PutEncoder returns an encoder to the pool. +func PutEncoder(e *Encoder) { + e.Reset(nil) + encPool.Put(e) +} + +// Decoder decodes. +type Decoder = codec.Decoder + +// GetDecoder returns a decoder configured to read from r. +func GetDecoder(r io.Reader) *Decoder { + d := decPool.Get().(*Decoder) + d.Reset(r) + return d +} + +// PutDecoder returns a decoder to the pool. +func PutDecoder(d *Decoder) { + d.Reset(nil) + decPool.Put(d) +} diff --git a/internal/codec/codec_test.go b/internal/codec/codec_test.go new file mode 100644 index 0000000000..c984027984 --- /dev/null +++ b/internal/codec/codec_test.go @@ -0,0 +1,74 @@ +package codec + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Example() { + enc := GetEncoder(os.Stdout) + defer PutEncoder(enc) + enc.MustEncode([]string{"a", "slice", "of", "strings"}) + fmt.Fprintln(os.Stdout) + enc.MustEncode(nil) + fmt.Fprintln(os.Stdout) + enc.MustEncode(map[string]string{}) + fmt.Fprintln(os.Stdout) + // Output: ["a","slice","of","strings"] + // null + // {} +} + +func BenchmarkDecode(b *testing.B) { + b.ReportAllocs() + want := map[string]string{ + "a": strings.Repeat(`A`, 2048), + "b": strings.Repeat(`B`, 2048), + "c": strings.Repeat(`C`, 2048), + "d": strings.Repeat(`D`, 2048), + } + got := make(map[string]string, len(want)) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + dec := GetDecoder(JSONReader(want)) + err := dec.Decode(&got) + PutDecoder(dec) + if err != nil { + b.Error(err) + } + if !cmp.Equal(got, want) { + b.Error(cmp.Diff(got, want)) + } + } +} + +func BenchmarkDecodeStdlib(b *testing.B) { + b.ReportAllocs() + want := map[string]string{ + "a": strings.Repeat(`A`, 2048), + "b": strings.Repeat(`B`, 2048), + "c": strings.Repeat(`C`, 2048), + "d": strings.Repeat(`D`, 2048), + } + got := make(map[string]string, len(want)) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + x, err := json.Marshal(want) + if err != nil { + b.Error(err) + } + if err := json.Unmarshal(x, &got); err != nil { + b.Error(err) + } + if !cmp.Equal(got, want) { + b.Error(cmp.Diff(got, want)) + } + } +} diff --git a/internal/codec/reader.go b/internal/codec/reader.go new file mode 100644 index 0000000000..86d9ffe65c --- /dev/null +++ b/internal/codec/reader.go @@ -0,0 +1,20 @@ +package codec + +import "io" + +// JSONReader returns an io.ReadCloser backed by a pipe being fed by a JSON +// encoder. +func JSONReader(v interface{}) io.ReadCloser { + r, w := io.Pipe() + // This unsupervised goroutine should be fine, because the writer will error + // once the reader is closed. + go func() { + enc := GetEncoder(w) + defer PutEncoder(enc) + defer w.Close() + if err := enc.Encode(v); err != nil { + w.CloseWithError(err) + } + }() + return r +}