Files
go-telegram/client/call_bench_test.go
T
lukaszraczylo 75c7ce3119 perf(client): pool req-body buffer + manual http.Request with cached URL
Two changes that together cut allocs/call from 15 to 13 (client-internal
bench) and per-call CPU from 600ns to 455ns (-24%) on the no-HTTP path:

1. Codec gets an optional BodyEncoder extension (MarshalTo io.Writer).
   When present, encodeJSONBody stream-encodes the request directly into
   a pooled *bytes.Buffer instead of allocating a [2-step] Marshal+Reader
   pair. DefaultCodec implements it via goccy/go-json.NewEncoder.
2. *Bot caches the parsed base URL on construction. buildRequest skips
   net/http.NewRequestWithContext for the common case and constructs
   *http.Request manually — clones the URL by value, sets the method
   path, and populates ContentLength + GetBody from the body's concrete
   type so RetryDoer's body-replay across attempts still works.

Cross-library bench (sendMessage round-trip vs httptest.Server): -2
allocs/call (104 -> 102), bytes -1.2%, time within noise (real HTTP
plumbing dominates). The CPU win is visible on synthetic stub-doer
benches and translates to lower GC pressure on sustained-throughput
workloads.

Slow-path fallback preserved for codecs that don't implement BodyEncoder
and for *Bot instances where url.Parse on the configured base failed —
they take the original NewRequestWithContext path.
2026-05-10 22:36:57 +01:00

98 lines
2.3 KiB
Go

package client
import (
"bytes"
"context"
"io"
"net/http"
"testing"
)
// stubDoer returns the same canned response body for every request. It
// is intentionally minimal — testify mock has its own overhead that
// would dominate the per-call cost we want to measure.
type stubDoer struct{ body []byte }
func (s *stubDoer) Do(_ *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(s.body)),
Header: http.Header{},
}, nil
}
type benchSendReq struct {
ChatID int64 `json:"chat_id"`
Text string `json:"text"`
}
type benchMsgResp struct {
MessageID int64 `json:"message_id"`
Date int64 `json:"date"`
Text string `json:"text"`
}
func BenchmarkCall_BoolResponse(b *testing.B) {
d := &stubDoer{body: []byte(`{"ok":true,"result":true}`)}
bot := New("123:abc", WithHTTPClient(d))
ctx := context.Background()
req := &benchSendReq{ChatID: 42, Text: "hi"}
b.ReportAllocs()
for b.Loop() {
if _, err := Call[*benchSendReq, bool](ctx, bot, "setMyCommands", req); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkCall_StructResponse(b *testing.B) {
d := &stubDoer{body: []byte(`{"ok":true,"result":{"message_id":1,"date":0,"text":"ok"}}`)}
bot := New("123:abc", WithHTTPClient(d))
ctx := context.Background()
req := &benchSendReq{ChatID: 42, Text: "hi"}
b.ReportAllocs()
for b.Loop() {
if _, err := Call[*benchSendReq, benchMsgResp](ctx, bot, "sendMessage", req); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkEncodeJSONBody(b *testing.B) {
codec := DefaultCodec{}
req := &benchSendReq{ChatID: 42, Text: "hello, world"}
b.ReportAllocs()
for b.Loop() {
r, pooled, err := encodeJSONBody(codec, req)
if err != nil {
b.Fatal(err)
}
_ = r
if pooled != nil {
putReqBuf(pooled)
}
}
}
func BenchmarkDecodeResult_Bool(b *testing.B) {
codec := DefaultCodec{}
raw := []byte(`{"ok":true,"result":true}`)
b.ReportAllocs()
for b.Loop() {
if _, err := decodeResult[bool](codec, raw); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkDecodeResult_Struct(b *testing.B) {
codec := DefaultCodec{}
raw := []byte(`{"ok":true,"result":{"message_id":1,"date":0,"text":"ok"}}`)
b.ReportAllocs()
for b.Loop() {
if _, err := decodeResult[benchMsgResp](codec, raw); err != nil {
b.Fatal(err)
}
}
}