mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-05 22:43:59 +00:00
test(bench): add allocation benchmarks for client/transport/dispatch hot paths
Hermetic benchmarks (no network) covering Call encode+decode, webhook ServeHTTP body parse, and Router dispatch (command/regex/filter). Use Go 1.24+ b.Loop() idiom. .benchstats/baseline.txt pins the pre-optimisation numbers for benchstat comparisons.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
pkg: github.com/lukaszraczylo/go-telegram/client
|
||||
cpu: Apple M4 Max
|
||||
BenchmarkCall_BoolResponse-16 1875306 633.9 ns/op 1957 B/op 18 allocs/op
|
||||
BenchmarkCall_StructResponse-16 1805024 665.2 ns/op 2005 B/op 18 allocs/op
|
||||
BenchmarkEncodeJSONBody-16 23345811 51.55 ns/op 96 B/op 2 allocs/op
|
||||
BenchmarkDecodeResult_Bool-16 23832240 50.37 ns/op 80 B/op 2 allocs/op
|
||||
BenchmarkDecodeResult_Struct-16 13511192 92.64 ns/op 144 B/op 2 allocs/op
|
||||
|
||||
pkg: github.com/lukaszraczylo/go-telegram/transport
|
||||
BenchmarkWebhook_ServeHTTP-16 465798 2564 ns/op 12707 B/op 24 allocs/op
|
||||
|
||||
pkg: github.com/lukaszraczylo/go-telegram/dispatch
|
||||
BenchmarkRouter_DispatchCommand-16 7303522 152.7 ns/op 416 B/op 5 allocs/op
|
||||
BenchmarkRouter_DispatchTextRegex-16 6740305 180.5 ns/op 428 B/op 5 allocs/op
|
||||
BenchmarkRouter_DispatchFilter-16 39479149 32.18 ns/op 96 B/op 2 allocs/op
|
||||
BenchmarkRouter_NewContext-16 208260764 5.790 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkExtractCommand-16 12988816 92.69 ns/op 0 B/op 0 allocs/op
|
||||
@@ -0,0 +1,94 @@
|
||||
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, err := encodeJSONBody(codec, req)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = r
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/api"
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
)
|
||||
|
||||
func BenchmarkRouter_DispatchCommand(b *testing.B) {
|
||||
r := New(client.New("t"))
|
||||
r.OnCommand("/start", func(c *Context, m *api.Message) error { return nil })
|
||||
u := cmdMessage("/start hello")
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
c := NewContext(ctx, r.bot, &u)
|
||||
_ = r.dispatch(c, &u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRouter_DispatchTextRegex(b *testing.B) {
|
||||
r := New(client.New("t"))
|
||||
r.OnText("^hello.*", func(c *Context, m *api.Message) error { return nil })
|
||||
u := api.Update{
|
||||
UpdateID: 1,
|
||||
Message: &api.Message{
|
||||
MessageID: 1, Date: 0,
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hello world",
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
c := NewContext(ctx, r.bot, &u)
|
||||
_ = r.dispatch(c, &u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRouter_DispatchFilter(b *testing.B) {
|
||||
r := New(client.New("t"))
|
||||
r.OnMessageFilter(
|
||||
func(m *api.Message) bool { return m != nil && m.Text == "ping" },
|
||||
func(c *Context, m *api.Message) error { return nil },
|
||||
)
|
||||
u := api.Update{
|
||||
UpdateID: 1,
|
||||
Message: &api.Message{
|
||||
MessageID: 1, Date: 0,
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "ping",
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
c := NewContext(ctx, r.bot, &u)
|
||||
_ = r.dispatch(c, &u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRouter_NewContext(b *testing.B) {
|
||||
bot := client.New("t")
|
||||
u := &api.Update{UpdateID: 1}
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = NewContext(ctx, bot, u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExtractCommand(b *testing.B) {
|
||||
text := "/start@BotName hello world"
|
||||
cmdLen := len("/start@BotName")
|
||||
m := &api.Message{
|
||||
MessageID: 1, Date: 0,
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: []api.MessageEntity{
|
||||
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(cmdLen)},
|
||||
},
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_, _, _ = extractCommand(m)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
)
|
||||
|
||||
const benchUpdateBody = `{"update_id":12345,"message":{"message_id":1,"date":1700000000,"chat":{"id":42,"type":"private"},"from":{"id":42,"is_bot":false,"first_name":"User"},"text":"hello world"}}`
|
||||
|
||||
func BenchmarkWebhook_ServeHTTP(b *testing.B) {
|
||||
w := NewWebhookServer(client.New("t"), WithBufferSize(1024))
|
||||
body := []byte(benchUpdateBody)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-w.Updates():
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
defer close(done)
|
||||
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
w.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
b.Fatalf("status %d", rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user