From 2823e3e9e022b84e118b424bc9ba413e9b3ea5fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Sat, 25 Aug 2018 00:23:44 +0200 Subject: [PATCH] results: Use github.com/mailru/easyjson for JSON encoding It's much faster. --- Gopkg.lock | 16 ++++ Gopkg.toml | 4 + lib/results.go | 28 +++++-- lib/results_easyjson.go | 164 ++++++++++++++++++++++++++++++++++++++++ lib/results_test.go | 68 ++++++++++++++--- 5 files changed, 266 insertions(+), 14 deletions(-) create mode 100644 lib/results_easyjson.go diff --git a/Gopkg.lock b/Gopkg.lock index e525a87d..b6d17b34 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -62,6 +62,19 @@ pruneopts = "UT" revision = "a7d76c6f093a59b94a01c6c2b8429122d444a8cc" +[[projects]] + branch = "master" + digest = "1:aa3d8d42865c42626b5c1add193692d045b3188b1479f0a0a88690d21fe20083" + name = "github.com/mailru/easyjson" + packages = [ + ".", + "buffer", + "jlexer", + "jwriter", + ] + pruneopts = "UT" + revision = "60711f1a8329503b04e1c88535f419d0bb440bff" + [[projects]] branch = "master" digest = "1:7ca2584fa7da0520cd2d1136a10194fe5a5b220bdb215074ab6f7b5ad91115f4" @@ -144,6 +157,9 @@ "github.com/dgryski/go-lttb", "github.com/google/go-cmp/cmp", "github.com/influxdata/tdigest", + "github.com/mailru/easyjson", + "github.com/mailru/easyjson/jlexer", + "github.com/mailru/easyjson/jwriter", "github.com/shurcooL/vfsgen", "github.com/streadway/quantile", "github.com/tsenart/go-tsz", diff --git a/Gopkg.toml b/Gopkg.toml index 7ee2ae03..1794f102 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -68,3 +68,7 @@ [[constraint]] branch = "master" name = "github.com/c2h5oh/datasize" + +[[constraint]] + branch = "master" + name = "github.com/mailru/easyjson" diff --git a/lib/results.go b/lib/results.go index 56e89bd8..2754cb51 100644 --- a/lib/results.go +++ b/lib/results.go @@ -1,15 +1,18 @@ package vegeta import ( + "bufio" "bytes" "encoding/base64" "encoding/csv" "encoding/gob" - "encoding/json" "io" "sort" "strconv" "time" + + "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" ) func init() { @@ -212,12 +215,27 @@ func NewCSVDecoder(rd io.Reader) Decoder { // NewJSONEncoder returns an Encoder that dumps the given *Results as a JSON // object. func NewJSONEncoder(w io.Writer) Encoder { - enc := json.NewEncoder(w) - return func(r *Result) error { return enc.Encode(r) } + var jw jwriter.Writer + return func(r *Result) error { + (*jsonResult)(r).encode(&jw) + if jw.Error != nil { + return jw.Error + } + jw.RawByte('\n') + _, err := jw.DumpTo(w) + return err + } } // NewJSONDecoder returns a Decoder that decodes JSON encoded Results. func NewJSONDecoder(r io.Reader) Decoder { - dec := json.NewDecoder(r) - return func(r *Result) error { return dec.Decode(r) } + rd := bufio.NewReader(r) + return func(r *Result) (err error) { + var jl jlexer.Lexer + if jl.Data, err = rd.ReadSlice('\n'); err != nil { + return err + } + (*jsonResult)(r).decode(&jl) + return jl.Error() + } } diff --git a/lib/results_easyjson.go b/lib/results_easyjson.go new file mode 100644 index 00000000..17f10923 --- /dev/null +++ b/lib/results_easyjson.go @@ -0,0 +1,164 @@ +// This file has been modified from the original generated code to make it work with +// type alias jsonResult so that the methods aren't exposed in Result. +package vegeta + +import ( + "time" + + "github.com/mailru/easyjson/jlexer" + "github.com/mailru/easyjson/jwriter" +) + +type jsonResult Result + +func (out *jsonResult) decode(in *jlexer.Lexer) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeString() + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "attack": + out.Attack = string(in.String()) + case "seq": + out.Seq = uint64(in.Uint64()) + case "code": + out.Code = uint16(in.Uint16()) + case "timestamp": + if data := in.Raw(); in.Ok() { + in.AddError((out.Timestamp).UnmarshalJSON(data)) + } + case "latency": + out.Latency = time.Duration(in.Int64()) + case "bytes_out": + out.BytesOut = uint64(in.Uint64()) + case "bytes_in": + out.BytesIn = uint64(in.Uint64()) + case "error": + out.Error = string(in.String()) + case "body": + if in.IsNull() { + in.Skip() + out.Body = nil + } else { + out.Body = in.Bytes() + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} + +func (in *jsonResult) encode(out *jwriter.Writer) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"attack\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Attack)) + } + { + const prefix string = ",\"seq\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Uint64(uint64(in.Seq)) + } + { + const prefix string = ",\"code\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Uint16(uint16(in.Code)) + } + { + const prefix string = ",\"timestamp\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Raw((in.Timestamp).MarshalJSON()) + } + { + const prefix string = ",\"latency\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Int64(int64(in.Latency)) + } + { + const prefix string = ",\"bytes_out\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Uint64(uint64(in.BytesOut)) + } + { + const prefix string = ",\"bytes_in\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Uint64(uint64(in.BytesIn)) + } + { + const prefix string = ",\"error\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Error)) + } + { + const prefix string = ",\"body\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Base64Bytes(in.Body) + } + out.RawByte('}') +} diff --git a/lib/results_test.go b/lib/results_test.go index ee4ca624..2c7b07e1 100644 --- a/lib/results_test.go +++ b/lib/results_test.go @@ -3,6 +3,7 @@ package vegeta import ( "bytes" "io" + "math/rand" "reflect" "testing" "testing/quick" @@ -79,19 +80,23 @@ func TestEncoding(t *testing.T) { var buf bytes.Buffer enc := tc.enc(&buf) - if err := enc(&want); err != nil { - t.Fatal(err) + for j := 0; j < 2; j++ { + if err := enc(&want); err != nil { + t.Fatal(err) + } } dec := tc.dec(&buf) - var got Result - if err := dec(&got); err != nil { - t.Fatalf("err: %q buffer: %s", err, buf.String()) - } + for j := 0; j < 2; j++ { + var got Result + if err := dec(&got); err != nil { + t.Fatalf("err: %q buffer: %s", err, buf.String()) + } - if !got.Equal(want) { - t.Logf("\ngot: %#v\nwant: %#v\n", got, want) - return false + if !got.Equal(want) { + t.Logf("\ngot: %#v\nwant: %#v\n", got, want) + return false + } } return true @@ -103,3 +108,48 @@ func TestEncoding(t *testing.T) { }) } } + +func BenchmarkEncodings(b *testing.B) { + b.StopTimer() + b.ResetTimer() + + rng := rand.New(rand.NewSource(0)) + zf := rand.NewZipf(rng, 3, 2, 1000) + began := time.Now() + results := make([]Result, 1e5) + + for i := 0; i < cap(results); i++ { + results[i] = Result{ + Attack: "Big Bang!", + Seq: uint64(i), + Timestamp: began.Add(time.Duration(i) * time.Millisecond), + Latency: time.Duration(zf.Uint64()) * time.Millisecond, + } + } + + for _, tc := range []struct { + encoding string + enc func(io.Writer) Encoder + dec func(io.Reader) Decoder + }{ + {"gob", NewEncoder, NewDecoder}, + {"csv", NewCSVEncoder, NewCSVDecoder}, + {"json", NewJSONEncoder, NewJSONDecoder}, + } { + var buf bytes.Buffer + enc := tc.enc(&buf) + + b.Run(tc.encoding+"-encode", func(b *testing.B) { + for i := 0; i < b.N; i++ { + enc.Encode(&results[i%len(results)]) + } + }) + + dec := tc.dec(&buf) + b.Run(tc.encoding+"-decode", func(b *testing.B) { + for i := 0; i < b.N; i++ { + dec.Decode(&results[i%len(results)]) + } + }) + } +}