mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-05 22:43:59 +00:00
bfb7e9875e
Adds an alternative HTTPDoer backed by valyala/fasthttp for high-throughput
bots. Cuts per-call allocs from 102 to 56 in the cross-library bench
(within 8 of telego, which uses fasthttp by default), and per-call bytes
from 11.1 KiB to 6.6 KiB.
bot := client.New(token,
client.WithHTTPClient(client.NewFastHTTPDoer()),
)
Implementation notes:
- Wraps *fasthttp.Client behind the existing HTTPDoer (Do *http.Request)
interface, so RetryDoer, custom transports, observability middleware,
and the 1428 generated tests all keep working as-is.
- Translates *http.Request -> fasthttp.Request once per call and
returns a *http.Response whose Body releases the pooled fasthttp
response on Close (net/http contract).
- Recognises the bufferReadCloser / readerReadCloser shapes produced
by buildRequest and passes their underlying bytes straight to
SetBodyRaw -- no io.ReadAll, no copy.
- Honours ctx.Deadline via DoDeadline, falls back to WithFastHTTPReadTimeout
when no deadline is set. fasthttp.ErrTimeout maps to
context.DeadlineExceeded for errors.Is compatibility.
Default stays net/http: fasthttp is HTTP/1.1 only, doesn't compose with
the http.RoundTripper middleware ecosystem, and most users don't have
the throughput to notice. Bots making thousands of API calls/sec should
opt in.
Multipart/file-upload path remains on net/http per the agreed scope --
the perf bottleneck was JSON-method round-trip, not file uploads.
Time numbers in the report deferred until a quiet-system bench run;
allocs/bytes numbers (which are deterministic per code path) are
already updated.
192 lines
5.4 KiB
Go
192 lines
5.4 KiB
Go
// Round-trip benchmarks: build a SendMessage request, POST it to a local
|
|
// httptest.Server returning a canned `{"ok":true,"result":Message}` body,
|
|
// decode the response. Measures marshal + transport + unmarshal end-to-end.
|
|
//
|
|
// Each library's idiomatic "send a text message" call path is exercised
|
|
// through its public API. The mock server replies identically for every path,
|
|
// so any difference comes from serialization, HTTP plumbing, or response
|
|
// decoding inside each library.
|
|
package benchmarks
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/lukaszraczylo/go-telegram/api"
|
|
"github.com/lukaszraczylo/go-telegram/client"
|
|
"github.com/lukaszraczylo/go-telegram/test/benchmarks/shared"
|
|
|
|
echotron "github.com/NicoNex/echotron/v3"
|
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
|
gobot "github.com/go-telegram/bot"
|
|
telego "github.com/mymmrac/telego"
|
|
tele "gopkg.in/telebot.v3"
|
|
)
|
|
|
|
// Telegram-format token (digits:[\w-]{35}). telego enforces this format on construction.
|
|
const benchToken = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ_ab123456"
|
|
|
|
// BenchmarkCall_ours — lukaszraczylo/go-telegram with default net/http
|
|
// transport. Most users land here.
|
|
func BenchmarkCall_ours(b *testing.B) {
|
|
srv := shared.NewMockServer()
|
|
defer srv.Close()
|
|
bot := client.New(benchToken, client.WithBaseURL(srv.URL))
|
|
ctx := context.Background()
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
_, err := api.SendMessage(ctx, bot, &api.SendMessageParams{
|
|
ChatID: api.ChatIDFromInt(42),
|
|
Text: "hello",
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkCall_ours_fasthttp — lukaszraczylo/go-telegram with the
|
|
// opt-in fasthttp transport (client.NewFastHTTPDoer). Apples-to-apples
|
|
// against telego, which also runs on fasthttp by default.
|
|
func BenchmarkCall_ours_fasthttp(b *testing.B) {
|
|
srv := shared.NewMockServer()
|
|
defer srv.Close()
|
|
bot := client.New(benchToken,
|
|
client.WithBaseURL(srv.URL),
|
|
client.WithHTTPClient(client.NewFastHTTPDoer()),
|
|
)
|
|
ctx := context.Background()
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
_, err := api.SendMessage(ctx, bot, &api.SendMessageParams{
|
|
ChatID: api.ChatIDFromInt(42),
|
|
Text: "hello",
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkCall_gotba — go-telegram-bot-api/telegram-bot-api/v5.
|
|
func BenchmarkCall_gotba(b *testing.B) {
|
|
srv := shared.NewMockServer()
|
|
defer srv.Close()
|
|
// Endpoint is sprintf'd with token + method.
|
|
endpoint := srv.URL + "/bot%s/%s"
|
|
bot, err := tgbotapi.NewBotAPIWithClient(benchToken, endpoint, &http.Client{})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
msg := tgbotapi.NewMessage(42, "hello")
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
if _, err := bot.Send(msg); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkCall_telebot — gopkg.in/telebot.v3 (tucnak).
|
|
func BenchmarkCall_telebot(b *testing.B) {
|
|
srv := shared.NewMockServer()
|
|
defer srv.Close()
|
|
bot, err := tele.NewBot(tele.Settings{
|
|
Token: benchToken,
|
|
URL: srv.URL,
|
|
Synchronous: true,
|
|
Offline: true, // skip eager getMe call; we test sending
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
chat := &tele.Chat{ID: 42}
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
if _, err := bot.Send(chat, "hello"); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkCall_gobot — go-telegram/bot.
|
|
func BenchmarkCall_gobot(b *testing.B) {
|
|
srv := shared.NewMockServer()
|
|
defer srv.Close()
|
|
bot, err := gobot.New(benchToken,
|
|
gobot.WithServerURL(srv.URL),
|
|
gobot.WithSkipGetMe(),
|
|
)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
params := &gobot.SendMessageParams{ChatID: int64(42), Text: "hello"}
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
if _, err := bot.SendMessage(ctx, params); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkCall_telego — mymmrac/telego.
|
|
func BenchmarkCall_telego(b *testing.B) {
|
|
srv := shared.NewMockServer()
|
|
defer srv.Close()
|
|
bot, err := telego.NewBot(benchToken, telego.WithAPIServer(srv.URL))
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
params := &telego.SendMessageParams{
|
|
ChatID: telego.ChatID{ID: 42},
|
|
Text: "hello",
|
|
}
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
if _, err := bot.SendMessage(ctx, params); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkCall_echotron — NicoNex/echotron/v3.
|
|
//
|
|
// echotron expects a base URL ending in /bot<token>/ and ships built-in
|
|
// dual-level rate limiting (global 30/s, per-chat 20/min) on its unexported
|
|
// lclient field. The setters (SetGlobalRequestLimit / SetChatRequestLimit)
|
|
// are methods on the unexported type and have no public accessor through
|
|
// the API value, so the rate limiter cannot be disabled from outside the
|
|
// package without monkey-patching.
|
|
//
|
|
// Running this bench against the real path produces ~3s/op driven entirely
|
|
// by the per-chat token bucket — measuring rate limiting, not the library.
|
|
// We skip rather than publish a misleading number; the rate limiter is a
|
|
// feature of echotron and is documented as a caveat in the report.
|
|
func BenchmarkCall_echotron(b *testing.B) {
|
|
b.Skip("echotron has built-in rate limiting that cannot be disabled via the public API; see comment")
|
|
srv := shared.NewMockServer()
|
|
defer srv.Close()
|
|
base := srv.URL + "/bot" + benchToken + "/"
|
|
api := echotron.CustomAPI(base, benchToken)
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
if _, err := api.SendMessage("hello", 42, nil); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// silence unused-import if a build tag strips a lib.
|
|
var _ = strings.NewReader
|