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 JsonpJSON rendering to reduce allocations #4176

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

Conversation

1911860538
Copy link
Contributor

Below is my local test code:

json.go

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

	if r.Callback == "" {
		_, err = w.Write(ret)
		return err
	}

	callback := template.JSEscapeString(r.Callback)
	if _, err = w.Write(bytesconv.StringToBytes(callback)); err != nil {
		return err
	}

	if _, err = w.Write(bytesconv.StringToBytes("(")); err != nil {
		return err
	}

	if _, err = w.Write(ret); err != nil {
		return err
	}

	if _, err = w.Write(bytesconv.StringToBytes(");")); err != nil {
		return err
	}

	return nil
}

func (r JsonpJSON) RenderOptimized(w http.ResponseWriter) (err error) {
	r.WriteContentType(w)
	ret, err := json.Marshal(r.Data)
	if err != nil {
		return err
	}

	if r.Callback == "" {
		_, err = w.Write(ret)
		return err
	}

	template.JSEscape(w, bytesconv.StringToBytes(r.Callback))

	if _, err = w.Write(bytesconv.StringToBytes("(")); err != nil {
		return err
	}

	if _, err = w.Write(ret); err != nil {
		return err
	}

	if _, err = w.Write(bytesconv.StringToBytes(");")); err != nil {
		return err
	}

	return nil
}

json_test.go

package render

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

func TestRenderConsistency(t *testing.T) {
	data := map[string]string{"message": "hello"}

	tests := []struct {
		name     string
		callback string
	}{
		{"Without Callback", ""},
		{"With Callback", "callbackFunc"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := JsonpJSON{
				Callback: tt.callback,
				Data:     data,
			}

			rec1 := httptest.NewRecorder()
			rec2 := httptest.NewRecorder()

			err1 := r.Render(rec1)
			err2 := r.RenderOptimized(rec2)

			if err1 != nil || err2 != nil {
				t.Fatalf("Unexpected error: Render=%v, RenderOptimized=%v", err1, err2)
			}

			result1 := rec1.Body.Bytes()
			result2 := rec2.Body.Bytes()

			if !bytes.Equal(result1, result2) {
				t.Errorf("Render and RenderOptimized produced different outputs:\nRender: %s\nRenderOptimized: %s", result1, result2)
			}
		})
	}
}

func BenchmarkRender(b *testing.B) {
	r := JsonpJSON{Callback: "testCallback", Data: map[string]string{"message": "hello, world!"}}
	w := httptest.NewRecorder()

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

func BenchmarkRenderOptimized(b *testing.B) {
	r := JsonpJSON{Callback: "testCallback", Data: map[string]string{"message": "hello, world!"}}
	w := httptest.NewRecorder()

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

test output

=== RUN   TestRenderConsistency
=== RUN   TestRenderConsistency/Without_Callback
--- PASS: TestRenderConsistency/Without_Callback (0.00s)
=== RUN   TestRenderConsistency/With_Callback
--- PASS: TestRenderConsistency/With_Callback (0.00s)
--- PASS: TestRenderConsistency (0.00s)
PASS

Process finished with the exit code 0

benchmark output

goos: darwin
goarch: amd64
pkg: github.com/gin-gonic/gin/render
cpu: Intel(R) Core(TM) i7-8569U CPU @ 2.80GHz
BenchmarkRender
BenchmarkRender-8            	 2310268	       474.0 ns/op	     228 B/op	       4 allocs/op
BenchmarkRenderOptimized
BenchmarkRenderOptimized-8   	 2778184	       422.8 ns/op	     208 B/op	       4 allocs/op
PASS

Process finished with the exit code 0

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