diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go index 643d5077..dd4a1aa4 100644 --- a/internal/encoding/json/encode.go +++ b/internal/encoding/json/encode.go @@ -481,10 +481,13 @@ func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { b, err := m.MarshalJSON() if err == nil { - e.Grow(len(b)) - out := e.AvailableBuffer() - out, err = appendCompact(out, b, opts.escapeHTML) - e.Buffer.Write(out) + // EDIT(begin): skip appendCompact validation - MarshalJSON output is already valid compact JSON. + // appendCompact scans every byte to validate/compact, which is O(n) per nested MarshalJSON call. + // For deeply nested structures this becomes a significant bottleneck. + // HTML escaping is also skipped because MarshalJSON implementations in this SDK use Marshal() + // internally, which already performs HTML escaping when escapeHTML is enabled (the default). + e.Buffer.Write(b) + // EDIT(end) } if err != nil { e.error(&MarshalerError{v.Type(), err, "MarshalJSON"}) @@ -507,10 +510,11 @@ func addrMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { m := va.Interface().(Marshaler) b, err := m.MarshalJSON() if err == nil { - e.Grow(len(b)) - out := e.AvailableBuffer() - out, err = appendCompact(out, b, opts.escapeHTML) - e.Buffer.Write(out) + // EDIT(begin): skip appendCompact validation - MarshalJSON output is already valid compact JSON. + // HTML escaping is also skipped because MarshalJSON implementations in this SDK use Marshal() + // internally, which already performs HTML escaping when escapeHTML is enabled (the default). + e.Buffer.Write(b) + // EDIT(end) } if err != nil { e.error(&MarshalerError{v.Type(), err, "MarshalJSON"}) diff --git a/internal/encoding/json/encode_test.go b/internal/encoding/json/encode_test.go new file mode 100644 index 00000000..1904835c --- /dev/null +++ b/internal/encoding/json/encode_test.go @@ -0,0 +1,199 @@ +package json + +import ( + "bytes" + "strings" + "testing" +) + +// Inner implements MarshalJSON to trigger the optimized code path +type benchInner struct { + Name string `json:"name"` + Value int `json:"value"` +} + +func (b benchInner) MarshalJSON() ([]byte, error) { + return Marshal(struct { + Name string `json:"name"` + Value int `json:"value"` + }{b.Name, b.Value}) +} + +// Nested structure with multiple MarshalJSON calls +type benchNested struct { + Inner benchInner `json:"inner"` + Items []int `json:"items"` +} + +func (b benchNested) MarshalJSON() ([]byte, error) { + return Marshal(struct { + Inner benchInner `json:"inner"` + Items []int `json:"items"` + }{b.Inner, b.Items}) +} + +// Deeply nested to amplify the effect +type benchDeep struct { + Level1 benchNested `json:"level1"` + Level2 benchNested `json:"level2"` + Data string `json:"data"` +} + +func (b benchDeep) MarshalJSON() ([]byte, error) { + return Marshal(struct { + Level1 benchNested `json:"level1"` + Level2 benchNested `json:"level2"` + Data string `json:"data"` + }{b.Level1, b.Level2, b.Data}) +} + +func BenchmarkMarshalNestedMarshalJSON(b *testing.B) { + data := benchDeep{ + Level1: benchNested{ + Inner: benchInner{Name: "test1", Value: 100}, + Items: []int{1, 2, 3, 4, 5}, + }, + Level2: benchNested{ + Inner: benchInner{Name: "test2", Value: 200}, + Items: []int{6, 7, 8, 9, 10}, + }, + Data: "some test data here", + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Marshal(data) + if err != nil { + b.Fatal(err) + } + } +} + +// Slice of nested structs - common real-world pattern +func BenchmarkMarshalSliceOfNestedMarshalJSON(b *testing.B) { + data := make([]benchDeep, 50) + for i := range data { + data[i] = benchDeep{ + Level1: benchNested{ + Inner: benchInner{Name: "test1", Value: i}, + Items: []int{1, 2, 3, 4, 5}, + }, + Level2: benchNested{ + Inner: benchInner{Name: "test2", Value: i * 2}, + Items: []int{6, 7, 8, 9, 10}, + }, + Data: "some test data here that is a bit longer to simulate real payloads", + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Marshal(data) + if err != nil { + b.Fatal(err) + } + } +} + +// Test that HTML escaping is preserved for nested MarshalJSON calls +type htmlTestInner struct { + Content string `json:"content"` +} + +func (h htmlTestInner) MarshalJSON() ([]byte, error) { + return Marshal(struct { + Content string `json:"content"` + }{h.Content}) +} + +type htmlTestOuter struct { + Inner htmlTestInner `json:"inner"` +} + +func (h htmlTestOuter) MarshalJSON() ([]byte, error) { + return Marshal(struct { + Inner htmlTestInner `json:"inner"` + }{h.Inner}) +} + +func TestMarshalHTMLEscapeWithNestedMarshalJSON(t *testing.T) { + // Test that HTML-sensitive characters are escaped in nested MarshalJSON + data := htmlTestOuter{ + Inner: htmlTestInner{ + Content: "", + }, + } + + result, err := Marshal(data) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // The < and > should be escaped as \u003c and \u003e + if strings.Contains(string(result), "