From fd632a9a35cfc1c42b2a93d9560df2201f84c536 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Tue, 23 Apr 2024 21:45:44 +0900 Subject: [PATCH 1/2] perf(libs/json): Lower heap overhead of JSON encoding (#2846) --- Many RPC methods require JSON marshalled responses. We saw this taking a notable amount of heap allocation in query serving full nodes. This PR removes some extra heap allocations that were being done. We avoided using the more efficient encoder.Encode before, because it added a newline. This PR changes the function signature for these private methods to be using *bytes.Buffer, and then uses the in-buffer methods (rather than a second copy). We then just truncate the final byte after each such call, which does not waste any allocations. I added a benchmark for the most complex test case. OLD: ``` BenchmarkJsonMarshalStruct-12 78992 15542 ns/op 4487 B/op 191 allocs/op ``` New: ``` BenchmarkJsonMarshalStruct-12 93346 11132 ns/op 3245 B/op 58 allocs/op ``` Roughly a 3-4x reduction in the number of allocations, and 20% speedup. #### PR checklist - [x] Tests written/updated - Existing tests cover this - [x] Changelog entry added in `.changelog` (we use [unclog](https://github.com/informalsystems/unclog) to manage our changelog) - [x] Updated relevant documentation (`docs/` or `spec/`) and code comments - [x] Title follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) spec (cherry picked from commit eb096019917f332649d972c83b52acb478119b64) # Conflicts: # .changelog/v0.37.5/improvements/2846-speedup-json-encoding.md # libs/json/encoder.go --- .../2846-speedup-json-encoding.md | 2 ++ libs/json/encoder.go | 26 ++++++++++++++----- libs/json/encoder_test.go | 17 ++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 .changelog/v0.37.5/improvements/2846-speedup-json-encoding.md diff --git a/.changelog/v0.37.5/improvements/2846-speedup-json-encoding.md b/.changelog/v0.37.5/improvements/2846-speedup-json-encoding.md new file mode 100644 index 00000000000..026b5a43989 --- /dev/null +++ b/.changelog/v0.37.5/improvements/2846-speedup-json-encoding.md @@ -0,0 +1,2 @@ +- `[libs/json]` Lower the memory overhead of JSON encoding by using JSON encoders internally + ([\#2846](https://github.com/cometbft/cometbft/pull/2846)). diff --git a/libs/json/encoder.go b/libs/json/encoder.go index 11990e2af6c..9c5438c8008 100644 --- a/libs/json/encoder.go +++ b/libs/json/encoder.go @@ -42,7 +42,11 @@ func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { return buf.Bytes(), nil } +<<<<<<< HEAD func encode(w io.Writer, v interface{}) error { +======= +func encode(w *bytes.Buffer, v any) error { +>>>>>>> eb0960199 (perf(libs/json): Lower heap overhead of JSON encoding (#2846)) // Bare nil values can't be reflected, so we must handle them here. if v == nil { return writeStr(w, "null") @@ -60,7 +64,7 @@ func encode(w io.Writer, v interface{}) error { return encodeReflect(w, rv) } -func encodeReflect(w io.Writer, rv reflect.Value) error { +func encodeReflect(w *bytes.Buffer, rv reflect.Value) error { if !rv.IsValid() { return errors.New("invalid reflect value") } @@ -115,7 +119,7 @@ func encodeReflect(w io.Writer, rv reflect.Value) error { } } -func encodeReflectList(w io.Writer, rv reflect.Value) error { +func encodeReflectList(w *bytes.Buffer, rv reflect.Value) error { // Emit nil slices as null. if rv.Kind() == reflect.Slice && rv.IsNil() { return writeStr(w, "null") @@ -150,7 +154,7 @@ func encodeReflectList(w io.Writer, rv reflect.Value) error { return writeStr(w, "]") } -func encodeReflectMap(w io.Writer, rv reflect.Value) error { +func encodeReflectMap(w *bytes.Buffer, rv reflect.Value) error { if rv.Type().Key().Kind() != reflect.String { return errors.New("map key must be string") } @@ -181,7 +185,7 @@ func encodeReflectMap(w io.Writer, rv reflect.Value) error { return writeStr(w, "}") } -func encodeReflectStruct(w io.Writer, rv reflect.Value) error { +func encodeReflectStruct(w *bytes.Buffer, rv reflect.Value) error { sInfo := makeStructInfo(rv.Type()) if err := writeStr(w, "{"); err != nil { return err @@ -212,7 +216,7 @@ func encodeReflectStruct(w io.Writer, rv reflect.Value) error { return writeStr(w, "}") } -func encodeReflectInterface(w io.Writer, rv reflect.Value) error { +func encodeReflectInterface(w *bytes.Buffer, rv reflect.Value) error { // Get concrete value and dereference pointers. for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface { if rv.IsNil() { @@ -237,14 +241,24 @@ func encodeReflectInterface(w io.Writer, rv reflect.Value) error { return writeStr(w, "}") } +<<<<<<< HEAD func encodeStdlib(w io.Writer, v interface{}) error { // Doesn't stream the output because that adds a newline, as per: // https://golang.org/pkg/encoding/json/#Encoder.Encode blob, err := json.Marshal(v) +======= +func encodeStdlib(w *bytes.Buffer, v any) error { + // Stream the output of the JSON marshaling directly into the buffer. + // The stdlib encoder will write a newline, so we must truncate it, + // which is why we pass in a bytes.Buffer throughout, not io.Writer. + enc := json.NewEncoder(w) + err := enc.Encode(v) +>>>>>>> eb0960199 (perf(libs/json): Lower heap overhead of JSON encoding (#2846)) if err != nil { return err } - _, err = w.Write(blob) + // Remove the last byte from the buffer + w.Truncate(w.Len() - 1) return err } diff --git a/libs/json/encoder_test.go b/libs/json/encoder_test.go index e6eb18a1225..8cf536b26be 100644 --- a/libs/json/encoder_test.go +++ b/libs/json/encoder_test.go @@ -102,3 +102,20 @@ func TestMarshal(t *testing.T) { }) } } + +func BenchmarkJsonMarshalStruct(b *testing.B) { + s := "string" + sPtr := &s + i64 := int64(64) + ti := time.Date(2020, 6, 2, 18, 5, 13, 4346374, time.FixedZone("UTC+2", 2*60*60)) + car := &Car{Wheels: 4} + boat := Boat{Sail: true} + for i := 0; i < b.N; i++ { + _, _ = json.Marshal(Struct{ + Bool: true, Float64: 3.14, Int32: 32, Int64: 64, Int64Ptr: &i64, + String: "foo", StringPtrPtr: &sPtr, Bytes: []byte{1, 2, 3}, + Time: ti, Car: car, Boat: boat, Vehicles: []Vehicle{car, boat}, + Child: &Struct{Bool: false, String: "child"}, private: "private", + }) + } +} From 1733fd1a6acc4d9bffd7ca21519139024f8c6e5f Mon Sep 17 00:00:00 2001 From: Andy Nogueira Date: Tue, 23 Apr 2024 10:19:56 -0400 Subject: [PATCH 2/2] fixing conflicts (#2876) --- libs/json/encoder.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/libs/json/encoder.go b/libs/json/encoder.go index 9c5438c8008..67112384fdb 100644 --- a/libs/json/encoder.go +++ b/libs/json/encoder.go @@ -42,11 +42,7 @@ func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { return buf.Bytes(), nil } -<<<<<<< HEAD -func encode(w io.Writer, v interface{}) error { -======= func encode(w *bytes.Buffer, v any) error { ->>>>>>> eb0960199 (perf(libs/json): Lower heap overhead of JSON encoding (#2846)) // Bare nil values can't be reflected, so we must handle them here. if v == nil { return writeStr(w, "null") @@ -241,19 +237,12 @@ func encodeReflectInterface(w *bytes.Buffer, rv reflect.Value) error { return writeStr(w, "}") } -<<<<<<< HEAD -func encodeStdlib(w io.Writer, v interface{}) error { - // Doesn't stream the output because that adds a newline, as per: - // https://golang.org/pkg/encoding/json/#Encoder.Encode - blob, err := json.Marshal(v) -======= func encodeStdlib(w *bytes.Buffer, v any) error { // Stream the output of the JSON marshaling directly into the buffer. // The stdlib encoder will write a newline, so we must truncate it, // which is why we pass in a bytes.Buffer throughout, not io.Writer. enc := json.NewEncoder(w) err := enc.Encode(v) ->>>>>>> eb0960199 (perf(libs/json): Lower heap overhead of JSON encoding (#2846)) if err != nil { return err }