Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,12 @@ func WithVersion(version string) RequestOption {
}
}

var requestBufferPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}

// NewRequest creates an API request. A relative URL can be provided in urlStr,
// in which case it is resolved relative to the BaseURL of the Client.
// Relative URLs should always be specified without a preceding slash. If
Expand Down Expand Up @@ -1114,12 +1120,24 @@ func (c *Client) Do(req *http.Request, v any) (*Response, error) {
case io.Writer:
_, err = io.Copy(v, resp.Body)
default:
decErr := json.NewDecoder(resp.Body).Decode(v)
if decErr == io.EOF {
decErr = nil // ignore EOF errors caused by empty response body
}
if decErr != nil {
err = decErr
respBuf := requestBufferPool.Get().(*bytes.Buffer)
defer func() {
respBuf.Reset()
requestBufferPool.Put(respBuf)
}()

_, readErr := respBuf.ReadFrom(resp.Body)
if readErr != nil {
err = readErr
} else if respBuf.Len() > 0 {
b := respBuf.Bytes()
decErr := json.Unmarshal(b, v)
if decErr != nil && len(bytes.TrimSpace(b)) == 0 {
decErr = nil // ignore errors caused by empty response body
}
if decErr != nil {
err = decErr
}
}
}
return resp, err
Expand Down
88 changes: 88 additions & 0 deletions github/github_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2026 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package github

import (
"bytes"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
)

// legacyDecodeResponse simulates the behavior before Symmetrical Pooling
// (io.ReadAll -> json.Unmarshal).
func legacyDecodeResponse(resp *http.Response, v any) error {
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if len(data) > 0 {
return json.Unmarshal(data, v)
}
return nil
}

// pooledDecodeResponse simulates the new behavior with Symmetrical Pooling
// (requestBufferPool -> ReadFrom -> json.Unmarshal).
func pooledDecodeResponse(resp *http.Response, v any) error {
respBuf := requestBufferPool.Get().(*bytes.Buffer)
defer func() {
respBuf.Reset()
requestBufferPool.Put(respBuf)
}()

_, err := respBuf.ReadFrom(resp.Body)
if err != nil {
return err
}
if respBuf.Len() > 0 {
b := respBuf.Bytes()
return json.Unmarshal(b, v)
}
return nil
}

type dummyReadCloser struct {
io.Reader
}

func (d *dummyReadCloser) Close() error { return nil }

func BenchmarkDecodeResponse_Legacy(b *testing.B) {
payload, _ := json.Marshal(map[string]string{"title": "benchmark_test", "body": strings.Repeat("a", 1024*500)}) // 500KB JSON

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
resp := &http.Response{
Body: &dummyReadCloser{Reader: bytes.NewReader(payload)},
}
var v map[string]string
b.StartTimer()

_ = legacyDecodeResponse(resp, &v)
}
}

func BenchmarkDecodeResponse_Pooled(b *testing.B) {
payload, _ := json.Marshal(map[string]string{"title": "benchmark_test", "body": strings.Repeat("a", 1024*500)}) // 500KB JSON

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
resp := &http.Response{
Body: &dummyReadCloser{Reader: bytes.NewReader(payload)},
}
var v map[string]string
b.StartTimer()

_ = pooledDecodeResponse(resp, &v)
}
}
Loading