Skip to content

Commit 73b87d0

Browse files
perf: symmetrical pooling for zero-allocation request encoding and response decoding
1 parent 6c643b8 commit 73b87d0

2 files changed

Lines changed: 112 additions & 6 deletions

File tree

github/github.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,12 @@ func WithVersion(version string) RequestOption {
554554
}
555555
}
556556

557+
var requestBufferPool = sync.Pool{
558+
New: func() any {
559+
return new(bytes.Buffer)
560+
},
561+
}
562+
557563
// NewRequest creates an API request. A relative URL can be provided in urlStr,
558564
// in which case it is resolved relative to the BaseURL of the Client.
559565
// Relative URLs should always be specified without a preceding slash. If
@@ -1114,12 +1120,24 @@ func (c *Client) Do(req *http.Request, v any) (*Response, error) {
11141120
case io.Writer:
11151121
_, err = io.Copy(v, resp.Body)
11161122
default:
1117-
decErr := json.NewDecoder(resp.Body).Decode(v)
1118-
if decErr == io.EOF {
1119-
decErr = nil // ignore EOF errors caused by empty response body
1120-
}
1121-
if decErr != nil {
1122-
err = decErr
1123+
respBuf := requestBufferPool.Get().(*bytes.Buffer)
1124+
defer func() {
1125+
respBuf.Reset()
1126+
requestBufferPool.Put(respBuf)
1127+
}()
1128+
1129+
_, readErr := respBuf.ReadFrom(resp.Body)
1130+
if readErr != nil {
1131+
err = readErr
1132+
} else if respBuf.Len() > 0 {
1133+
b := respBuf.Bytes()
1134+
decErr := json.Unmarshal(b, v)
1135+
if decErr != nil && len(bytes.TrimSpace(b)) == 0 {
1136+
decErr = nil // ignore errors caused by empty response body
1137+
}
1138+
if decErr != nil {
1139+
err = decErr
1140+
}
11231141
}
11241142
}
11251143
return resp, err

github/github_benchmark_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2026 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
package github
7+
8+
import (
9+
"bytes"
10+
"encoding/json"
11+
"io"
12+
"net/http"
13+
"strings"
14+
"testing"
15+
)
16+
17+
// legacyDecodeResponse simulates the behavior before Symmetrical Pooling
18+
// (io.ReadAll -> json.Unmarshal).
19+
func legacyDecodeResponse(resp *http.Response, v any) error {
20+
data, err := io.ReadAll(resp.Body)
21+
if err != nil {
22+
return err
23+
}
24+
if len(data) > 0 {
25+
return json.Unmarshal(data, v)
26+
}
27+
return nil
28+
}
29+
30+
// pooledDecodeResponse simulates the new behavior with Symmetrical Pooling
31+
// (requestBufferPool -> ReadFrom -> json.Unmarshal).
32+
func pooledDecodeResponse(resp *http.Response, v any) error {
33+
respBuf := requestBufferPool.Get().(*bytes.Buffer)
34+
defer func() {
35+
respBuf.Reset()
36+
requestBufferPool.Put(respBuf)
37+
}()
38+
39+
_, err := respBuf.ReadFrom(resp.Body)
40+
if err != nil {
41+
return err
42+
}
43+
if respBuf.Len() > 0 {
44+
b := respBuf.Bytes()
45+
return json.Unmarshal(b, v)
46+
}
47+
return nil
48+
}
49+
50+
type dummyReadCloser struct {
51+
io.Reader
52+
}
53+
54+
func (d *dummyReadCloser) Close() error { return nil }
55+
56+
func BenchmarkDecodeResponse_Legacy(b *testing.B) {
57+
payload, _ := json.Marshal(map[string]string{"title": "benchmark_test", "body": strings.Repeat("a", 1024*500)}) // 500KB JSON
58+
59+
b.ReportAllocs()
60+
b.ResetTimer()
61+
for i := 0; i < b.N; i++ {
62+
b.StopTimer()
63+
resp := &http.Response{
64+
Body: &dummyReadCloser{Reader: bytes.NewReader(payload)},
65+
}
66+
var v map[string]string
67+
b.StartTimer()
68+
69+
_ = legacyDecodeResponse(resp, &v)
70+
}
71+
}
72+
73+
func BenchmarkDecodeResponse_Pooled(b *testing.B) {
74+
payload, _ := json.Marshal(map[string]string{"title": "benchmark_test", "body": strings.Repeat("a", 1024*500)}) // 500KB JSON
75+
76+
b.ReportAllocs()
77+
b.ResetTimer()
78+
for i := 0; i < b.N; i++ {
79+
b.StopTimer()
80+
resp := &http.Response{
81+
Body: &dummyReadCloser{Reader: bytes.NewReader(payload)},
82+
}
83+
var v map[string]string
84+
b.StartTimer()
85+
86+
_ = pooledDecodeResponse(resp, &v)
87+
}
88+
}

0 commit comments

Comments
 (0)