// 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/ 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