From 79b16b082fa4402c90fa1f31ec4d2e5695006b60 Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Fri, 23 Jan 2026 19:29:06 -0500 Subject: [PATCH 1/2] perf: Optimize Stringify allocations (~2x faster) --- github/strings.go | 31 ++++++++- github/strings_benchmark_test.go | 37 +++++++++++ github/strings_test.go | 106 +++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 github/strings_benchmark_test.go diff --git a/github/strings.go b/github/strings.go index 0158c9a1fdc..dfbe3c685f9 100644 --- a/github/strings.go +++ b/github/strings.go @@ -9,17 +9,30 @@ import ( "bytes" "fmt" "reflect" + "strconv" + "sync" ) var timestampType = reflect.TypeFor[Timestamp]() +var bufferPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, +} + // Stringify attempts to create a reasonable string representation of types in // the GitHub library. It does things like resolve pointers to their values // and omits struct fields with nil values. func Stringify(message any) string { - var buf bytes.Buffer + buf := bufferPool.Get().(*bytes.Buffer) + defer func() { + buf.Reset() + bufferPool.Put(buf) + }() + v := reflect.ValueOf(message) - stringifyValue(&buf, v) + stringifyValue(buf, v) return buf.String() } @@ -34,8 +47,20 @@ func stringifyValue(w *bytes.Buffer, val reflect.Value) { v := reflect.Indirect(val) switch v.Kind() { + case reflect.Bool: + w.Write(strconv.AppendBool(w.Bytes(), v.Bool())[w.Len():]) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + w.Write(strconv.AppendInt(w.Bytes(), v.Int(), 10)[w.Len():]) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + w.Write(strconv.AppendUint(w.Bytes(), v.Uint(), 10)[w.Len():]) + case reflect.Float32: + w.Write(strconv.AppendFloat(w.Bytes(), v.Float(), 'g', -1, 32)[w.Len():]) + case reflect.Float64: + w.Write(strconv.AppendFloat(w.Bytes(), v.Float(), 'g', -1, 64)[w.Len():]) case reflect.String: - fmt.Fprintf(w, `"%v"`, v) + w.WriteByte('"') + w.WriteString(v.String()) + w.WriteByte('"') case reflect.Slice: w.WriteByte('[') for i := range v.Len() { diff --git a/github/strings_benchmark_test.go b/github/strings_benchmark_test.go new file mode 100644 index 00000000000..22bc0a2a29c --- /dev/null +++ b/github/strings_benchmark_test.go @@ -0,0 +1,37 @@ +// Copyright 2013 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 ( + "testing" +) + +type BenchmarkStruct struct { + Name string + Age int + Active bool + Score float32 + Rank float64 + Tags []string + Pointer *int +} + +func BenchmarkStringify(b *testing.B) { + val := 42 + s := &BenchmarkStruct{ + Name: "benchmark", + Age: 30, + Active: true, + Score: 1.1, + Rank: 99.999999, + Tags: []string{"go", "github", "api"}, + Pointer: Ptr(val), + } + b.ResetTimer() + for b.Loop() { + Stringify(s) + } +} diff --git a/github/strings_test.go b/github/strings_test.go index accd8529362..3dfb46ea687 100644 --- a/github/strings_test.go +++ b/github/strings_test.go @@ -81,6 +81,92 @@ func TestStringify(t *testing.T) { } } +func TestStringify_Primitives(t *testing.T) { + t.Parallel() + tests := []struct { + in any + out string + }{ + // Bool + {true, "true"}, + {false, "false"}, + + // Int variants + {int(1), "1"}, + {int8(2), "2"}, + {int16(3), "3"}, + {int32(4), "4"}, + {int64(5), "5"}, + + // Uint variants + {uint(6), "6"}, + {uint8(7), "7"}, + {uint16(8), "8"}, + {uint32(9), "9"}, + {uint64(10), "10"}, + {uintptr(11), "11"}, + + // Float variants (Precision Correctness) + {float32(1.1), "1.1"}, + {float64(1.1), "1.1"}, + {float32(1.0000001), "1.0000001"}, + {float64(1.000000000000001), "1.000000000000001"}, + + // Boundary Cases + {int8(-128), "-128"}, + {int8(127), "127"}, + {uint64(18446744073709551615), "18446744073709551615"}, + + // String Optimization + {"hello", `"hello"`}, + {"", `""`}, + } + + for i, tt := range tests { + s := Stringify(tt.in) + if s != tt.out { + t.Errorf("%v. Stringify(%T) => %q, want %q", i, tt.in, s, tt.out) + } + } +} + +func TestStringify_BufferPool(t *testing.T) { + t.Parallel() + // Verify that concurrent usage of Stringify is safe and doesn't corrupt buffers. + // While we can't easily verify reuse without exposing internal metrics, + // we can verify correctness under load which implies proper Reset() handling. + const goroutines = 10 + const iterations = 100 + + errCh := make(chan error, goroutines) + + for range goroutines { + go func() { + for range iterations { + // Use a mix of types to exercise different code paths + s1 := Stringify(123) + if s1 != "123" { + errCh <- fmt.Errorf("got %q, want %q", s1, "123") + return + } + + s2 := Stringify("test") + if s2 != `"test"` { + errCh <- fmt.Errorf("got %q, want %q", s2, `"test"`) + return + } + } + errCh <- nil + }() + } + + for range goroutines { + if err := <-errCh; err != nil { + t.Error(err) + } + } +} + // Directly test the String() methods on various GitHub types. We don't do an // exhaustive test of all the various field types, since TestStringify() above // takes care of that. Rather, we just make sure that Stringify() is being @@ -143,3 +229,23 @@ func TestString(t *testing.T) { } } } + +func TestStringify_Floats(t *testing.T) { + t.Parallel() + tests := []struct { + in any + out string + }{ + {float32(1.1), "1.1"}, + {float64(1.1), "1.1"}, + {float32(1.0000001), "1.0000001"}, + {struct{ F float32 }{1.1}, "{F:1.1}"}, + } + + for i, tt := range tests { + s := Stringify(tt.in) + if s != tt.out { + t.Errorf("%v. Stringify(%v) = %q, want %q", i, tt.in, s, tt.out) + } + } +} From 9e260bd61721b996660a3f315380b28c4cb0810e Mon Sep 17 00:00:00 2001 From: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:49:16 -0500 Subject: [PATCH 2/2] Apply suggestions from code review --- github/strings_benchmark_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/strings_benchmark_test.go b/github/strings_benchmark_test.go index 22bc0a2a29c..bec928f2bcd 100644 --- a/github/strings_benchmark_test.go +++ b/github/strings_benchmark_test.go @@ -1,4 +1,4 @@ -// Copyright 2013 The go-github AUTHORS. All rights reserved. +// 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.