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

perf: optimize AsciiJSON.Render method by using fmt.Appendf and reusing temp buffer #4175

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

1911860538
Copy link
Contributor

@1911860538 1911860538 commented Mar 6, 2025

Below is my local test code:

json.go

// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
	r.WriteContentType(w)
	ret, err := json.Marshal(r.Data)
	if err != nil {
		return err
	}

	var buffer bytes.Buffer
	for _, r := range bytesconv.BytesToString(ret) {
		cvt := string(r)
		if r >= 128 {
			cvt = fmt.Sprintf("\\u%04x", int64(r))
		}
		buffer.WriteString(cvt)
	}

	_, err = w.Write(buffer.Bytes())
	return err
}

// RenderOptimized optimized the rendering of AsciiJSON
func (r AsciiJSON) RenderOptimized(w http.ResponseWriter) (err error) {
	r.WriteContentType(w)
	ret, err := json.Marshal(r.Data)
	if err != nil {
		return err
	}

	var buffer bytes.Buffer
	escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences

	for _, r := range bytesconv.BytesToString(ret) {
		if r >= 128 {
			escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
			buffer.Write(escapeBuf)
		} else {
			buffer.WriteByte(byte(r))
		}
	}

	_, err = w.Write(buffer.Bytes())
	return err
}

json_test.go

package render

import (
	"bytes"
	"net/http/httptest"
	"testing"
)

// Test to ensure Render and RenderOptimized produce the same output
func TestRenderConsistency(t *testing.T) {
	testCases := []struct {
		name string
		data interface{}
	}{
		{"ASCII Only", map[string]string{"message": "hello, world!"}},
		{"Non-ASCII", map[string]string{"message": "你好, 世界!"}}, // Chinese characters
		{"Mixed ASCII & Non-ASCII", map[string]string{"message": "Hello, 你好!"}},
		{"Special Characters", map[string]string{"message": "© 𝄞 𝛑"}},
		{"Empty String", map[string]string{"message": ""}},
		{"Numbers", map[string]interface{}{"number": 12345}},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			r := AsciiJSON{Data: tc.data}

			w1 := httptest.NewRecorder()
			err1 := r.Render(w1)
			if err1 != nil {
				t.Fatalf("Render failed: %v", err1)
			}

			w2 := httptest.NewRecorder()
			err2 := r.RenderOptimized(w2)
			if err2 != nil {
				t.Fatalf("RenderOptimized failed: %v", err2)
			}

			// Compare outputs
			if !bytes.Equal(w1.Body.Bytes(), w2.Body.Bytes()) {
				t.Errorf("Mismatch in Render and RenderOptimized output:\nExpected: %s\nGot: %s", w1.Body.Bytes(), w2.Body.Bytes())
			}
		})
	}
}

func BenchmarkRenderNonASCII(b *testing.B) {
	data := map[string]string{"message": "你好, 世界! Hello, World!"}
	asciiJSON := AsciiJSON{Data: data}

	w := httptest.NewRecorder()

	b.ResetTimer()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		_ = asciiJSON.Render(w)
	}
}

func BenchmarkRenderOptimizedNonASCII(b *testing.B) {
	data := map[string]string{"message": "你好, 世界! Hello, World!"}
	asciiJSON := AsciiJSON{Data: data}

	w := httptest.NewRecorder()

	b.ResetTimer()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		_ = asciiJSON.RenderOptimized(w)
	}
}

func BenchmarkRender(b *testing.B) {
	data := map[string]string{"message": "hello, world!"}
	asciiJSON := AsciiJSON{Data: data}

	w := httptest.NewRecorder()

	b.ResetTimer()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		_ = asciiJSON.Render(w)
	}
}

func BenchmarkRenderOptimized(b *testing.B) {
	data := map[string]string{"message": "hello, world!"}
	asciiJSON := AsciiJSON{Data: data}

	w := httptest.NewRecorder()

	b.ResetTimer()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		_ = asciiJSON.RenderOptimized(w)
	}
}

test output

=== RUN   TestRenderConsistency
=== RUN   TestRenderConsistency/ASCII_Only
--- PASS: TestRenderConsistency/ASCII_Only (0.00s)
=== RUN   TestRenderConsistency/Non-ASCII
--- PASS: TestRenderConsistency/Non-ASCII (0.00s)
=== RUN   TestRenderConsistency/Mixed_ASCII_&_Non-ASCII
--- PASS: TestRenderConsistency/Mixed_ASCII_&_Non-ASCII (0.00s)
=== RUN   TestRenderConsistency/Special_Characters
--- PASS: TestRenderConsistency/Special_Characters (0.00s)
=== RUN   TestRenderConsistency/Empty_String
--- PASS: TestRenderConsistency/Empty_String (0.00s)
=== RUN   TestRenderConsistency/Numbers
--- PASS: TestRenderConsistency/Numbers (0.00s)
--- PASS: TestRenderConsistency (0.00s)
PASS

Process finished with the exit code 0

benchmark output

goos: darwin
goarch: arm64
pkg: github.com/gin-gonic/gin/render
BenchmarkRenderNonASCII
BenchmarkRenderNonASCII-11             	 1741088	       678.6 ns/op	     410 B/op	      13 allocs/op
BenchmarkRenderOptimizedNonASCII
BenchmarkRenderOptimizedNonASCII-11    	 2353917	       512.6 ns/op	     322 B/op	       9 allocs/op
BenchmarkRender
BenchmarkRender-11                     	 3574114	       339.9 ns/op	     251 B/op	       5 allocs/op
BenchmarkRenderOptimized
BenchmarkRenderOptimized-11            	 4619854	       258.9 ns/op	     234 B/op	       5 allocs/op
PASS

Process finished with the exit code 0

…ng temp buffer

per: use bytesconv.BytesToString(ret) instead of string(str)
@1911860538
Copy link
Contributor Author

@appleboy CC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant