mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-20 04:02:10 +00:00
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.
This commit is contained in:
@@ -18,10 +18,10 @@
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Webhook decode** (small Update): ours is **15–19% faster** than every competitor and ties telego for the lowest alloc count (11).
|
||||
- **Large Update unmarshal** (entities + reply markup + photo array): ours is **17–35% faster** with the lowest ns/op of all six. telego edges us on alloc count (31 vs 34) at the cost of ~17% more time.
|
||||
- **API call round-trip** (mock HTTP server): telego wins (36.3 µs / 48 allocs) thanks to its custom binder; ours is second (38.95 µs / 104 allocs) and beats gotba, telebot, gobot.
|
||||
- **Dispatcher routing** (20 handlers, last matches): ours is **2.5× faster than telebot and gobot** (101 ns vs 269 / 252 ns).
|
||||
- **Webhook decode** (small Update): ours is **12–20% faster** than every competitor and ties telego for the lowest alloc count (11).
|
||||
- **Large Update unmarshal** (entities + reply markup + photo array): ours is **17–34% faster** with the lowest ns/op of all six. telego edges us on alloc count (31 vs 34) at the cost of ~17% more time.
|
||||
- **API call round-trip** (mock HTTP server): telego wins (35.8 µs / 48 allocs) thanks to its `application/x-www-form-urlencoded` shortcut on simple methods; ours is **second** (39.8 µs / 102 allocs) and beats gotba, telebot, gobot.
|
||||
- **Dispatcher routing** (20 handlers, last matches): ours is **2.5–2.8× faster than telebot and gobot** (98 ns vs 271 / 246 ns).
|
||||
|
||||
## How to read these numbers
|
||||
|
||||
@@ -37,12 +37,12 @@ Decode `shared.SmallUpdateJSON` into the library's typed `Update` struct.
|
||||
|
||||
| Lib | sec/op | B/op | allocs/op |
|
||||
|-----|--------|------|-----------|
|
||||
| **ours** | **1.743 µs ±3%** | 2.180 KiB | **11** |
|
||||
| gotba | 2.016 µs ±3% | 1.461 KiB | 17 |
|
||||
| telebot | 2.073 µs ±3% | 1.773 KiB | 17 |
|
||||
| gobot | 1.999 µs ±1% | 1.789 KiB | 16 |
|
||||
| telego | 2.026 µs ±2% | 3.060 KiB | **11** |
|
||||
| echotron | 1.973 µs ±0% | 1.680 KiB | 16 |
|
||||
| **ours** | **1.832 µs ±4%** | 2.180 KiB | **11** |
|
||||
| gotba | 2.082 µs ±0% | 1.461 KiB | 17 |
|
||||
| telebot | 2.194 µs ±1% | 1.773 KiB | 17 |
|
||||
| gobot | 2.082 µs ±1% | 1.789 KiB | 16 |
|
||||
| telego | 2.143 µs ±2% | 3.058 KiB | **11** |
|
||||
| echotron | 2.039 µs ±1% | 1.680 KiB | 16 |
|
||||
|
||||
**Notes.** We use slightly more bytes because typed unions and the typed `[]UpdateType` allocate richer Go values. We win on time and tie telego on alloc count.
|
||||
|
||||
@@ -52,12 +52,12 @@ Decode `shared.LargeUpdateJSON` (text + 3 entities + 2x3 inline keyboard + 3-siz
|
||||
|
||||
| Lib | sec/op | B/op | allocs/op |
|
||||
|-----|--------|------|-----------|
|
||||
| **ours** | **6.667 µs ±4%** | 5.881 KiB | 34 |
|
||||
| gotba | 8.321 µs ±2% | 3.438 KiB | 56 |
|
||||
| telebot | 10.240 µs ±4% | 5.594 KiB | 60 |
|
||||
| gobot | 8.150 µs ±2% | 4.703 KiB | 50 |
|
||||
| telego | 7.797 µs ±1% | 6.621 KiB | **31** |
|
||||
| echotron | 8.072 µs ±0% | 4.219 KiB | 56 |
|
||||
| **ours** | **6.726 µs ±1%** | 5.875 KiB | 34 |
|
||||
| gotba | 8.066 µs ±1% | 3.438 KiB | 56 |
|
||||
| telebot | 10.190 µs ±1% | 5.594 KiB | 60 |
|
||||
| gobot | 8.231 µs ±1% | 4.703 KiB | 50 |
|
||||
| telego | 7.849 µs ±2% | 6.600 KiB | **31** |
|
||||
| echotron | 8.123 µs ±1% | 4.219 KiB | 56 |
|
||||
|
||||
**Notes.** Despite the typed-union model giving us richer Go values per decode, we still produce them faster than every competitor. telego edges us by 3 allocs but pays 17% more time.
|
||||
|
||||
@@ -67,15 +67,16 @@ Build params → POST to local `httptest.Server` returning `{"ok":true,"result":
|
||||
|
||||
| Lib | sec/op | B/op | allocs/op |
|
||||
|-----|--------|------|-----------|
|
||||
| ours | 38.95 µs ±3% | 11.17 KiB | 104 |
|
||||
| gotba | 41.95 µs ±2% | 10.95 KiB | 125 |
|
||||
| telebot | 43.63 µs ±0% | 13.16 KiB | 139 |
|
||||
| gobot | 61.11 µs ±1% | 13.51 KiB | 176 |
|
||||
| **telego** | **36.31 µs ±1%** | **6.556 KiB** | **48** |
|
||||
| ours | 39.83 µs ±4% | 11.09 KiB | 102 |
|
||||
| gotba | 42.03 µs ±4% | 10.97 KiB | 125 |
|
||||
| telebot | 43.41 µs ±1% | 13.15 KiB | 139 |
|
||||
| gobot | 61.19 µs ±1% | 13.50 KiB | 176 |
|
||||
| **telego** | **35.84 µs ±1%** | **6.547 KiB** | **48** |
|
||||
| echotron | *skipped — see below* | — | — |
|
||||
|
||||
**Notes.**
|
||||
- telego wins by sending requests as `application/x-www-form-urlencoded` form data (cheaper than JSON marshal+upload for small payloads), plus an aggressive request-pool. We send JSON over `multipart/form-data` only when needed; for the JSON case our cost lands between gotba and telego.
|
||||
- Our request path runs through a manually-constructed `*http.Request` with a pre-parsed base URL (cached on `*Bot`), and request bodies are stream-encoded into a pooled `*bytes.Buffer` via the optional `BodyEncoder` codec extension. Together those skip the `url.Parse` + `*http.Request` bookkeeping that `http.NewRequestWithContext` runs on every call.
|
||||
- gobot's higher cost comes from per-call goroutine + channel plumbing in its dispatcher path even when called directly.
|
||||
- **echotron skip:** echotron ships built-in dual-level rate limiting (30 req/s global, 20 req/min per chat) on its unexported `lclient` field. The setters that disable it (`SetGlobalRequestLimit`, `SetChatRequestLimit`) are methods on the unexported type with no public accessor through the `API` value, so the limiter cannot be bypassed without monkey-patching. A naive run produces ~3 s/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 worth knowing about; it just makes a microbench unfair.
|
||||
|
||||
@@ -85,9 +86,9 @@ Register 20 command handlers (`/cmd0` … `/cmd19`); feed an update matching `/c
|
||||
|
||||
| Lib | sec/op | B/op | allocs/op |
|
||||
|-----|--------|------|-----------|
|
||||
| **ours** | **100.7 ns ±3%** | 128 B | 3 |
|
||||
| telebot | 269.2 ns ±5% | 678 B | 5 |
|
||||
| gobot | 251.5 ns ±4% | **48 B** | **1** |
|
||||
| **ours** | **98.46 ns ±2%** | 128 B | 3 |
|
||||
| telebot | 270.9 ns ±2% | 678 B | 5 |
|
||||
| gobot | 246.1 ns ±1% | **48 B** | **1** |
|
||||
|
||||
**Notes.** We dispatch ~2.5× faster than telebot and gobot. gobot's single allocation is impressive but its routing decision is slower. telebot's higher cost reflects its richer per-update `Context` construction.
|
||||
|
||||
|
||||
+35
-13
@@ -19,6 +19,7 @@ Package client provides HTTP client primitives for the Telegram Bot API.
|
||||
- [func \(e \*APIError\) IsRetryable\(\) bool](<#APIError.IsRetryable>)
|
||||
- [func \(e \*APIError\) RetryAfter\(\) time.Duration](<#APIError.RetryAfter>)
|
||||
- [func \(e \*APIError\) Unwrap\(\) error](<#APIError.Unwrap>)
|
||||
- [type BodyEncoder](<#BodyEncoder>)
|
||||
- [type Bot](<#Bot>)
|
||||
- [func New\(token string, opts ...Option\) \*Bot](<#New>)
|
||||
- [func \(b \*Bot\) BaseURL\(\) string](<#Bot.BaseURL>)
|
||||
@@ -29,6 +30,7 @@ Package client provides HTTP client primitives for the Telegram Bot API.
|
||||
- [type Codec](<#Codec>)
|
||||
- [type DefaultCodec](<#DefaultCodec>)
|
||||
- [func \(DefaultCodec\) Marshal\(v any\) \(\[\]byte, error\)](<#DefaultCodec.Marshal>)
|
||||
- [func \(DefaultCodec\) MarshalTo\(w io.Writer, v any\) error](<#DefaultCodec.MarshalTo>)
|
||||
- [func \(DefaultCodec\) Unmarshal\(data \[\]byte, v any\) error](<#DefaultCodec.Unmarshal>)
|
||||
- [type HTTPDoer](<#HTTPDoer>)
|
||||
- [type Logger](<#Logger>)
|
||||
@@ -80,7 +82,7 @@ var (
|
||||
```
|
||||
|
||||
<a name="Call"></a>
|
||||
## func [Call](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L53>)
|
||||
## func [Call](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L68>)
|
||||
|
||||
```go
|
||||
func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req) (Resp, error)
|
||||
@@ -93,7 +95,7 @@ It is generic over both request and response types. Methods with no parameters m
|
||||
Call is exported because the api package \(which lives outside this one\) invokes it from generated method wrappers. User code should not normally call it directly — use the typed wrappers in package api instead.
|
||||
|
||||
<a name="CallRaw"></a>
|
||||
## func [CallRaw](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L104>)
|
||||
## func [CallRaw](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L119>)
|
||||
|
||||
```go
|
||||
func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json.RawMessage, error)
|
||||
@@ -166,8 +168,19 @@ func (e *APIError) Unwrap() error
|
||||
|
||||
Unwrap returns the matched sentinel error, if any.
|
||||
|
||||
<a name="BodyEncoder"></a>
|
||||
## type [BodyEncoder](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L24-L26>)
|
||||
|
||||
BodyEncoder is an optional Codec extension that encodes directly into an io.Writer, skipping the intermediate \[\]byte that Marshal returns. Call uses this when present to avoid allocating the marshal result and the bytes.Reader that wraps it; codecs without it fall through to Marshal \+ bytes.NewReader.
|
||||
|
||||
```go
|
||||
type BodyEncoder interface {
|
||||
MarshalTo(w io.Writer, v any) error
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Bot"></a>
|
||||
## type [Bot](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L7-L13>)
|
||||
## type [Bot](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L11-L24>)
|
||||
|
||||
Bot is the Telegram Bot API client. Construct via New. All API methods \(declared in package api\) hang off \*Bot via thin wrappers around call.
|
||||
|
||||
@@ -178,7 +191,7 @@ type Bot struct {
|
||||
```
|
||||
|
||||
<a name="New"></a>
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L36>)
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L47>)
|
||||
|
||||
```go
|
||||
func New(token string, opts ...Option) *Bot
|
||||
@@ -187,7 +200,7 @@ func New(token string, opts ...Option) *Bot
|
||||
New constructs a Bot with the given token and optional configuration. The default HTTP client is tuned for long\-poll workloads \(see NewDefaultHTTPDoer\); the default codec wraps encoding/json; the default logger discards records.
|
||||
|
||||
<a name="Bot.BaseURL"></a>
|
||||
### func \(\*Bot\) [BaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L20>)
|
||||
### func \(\*Bot\) [BaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L31>)
|
||||
|
||||
```go
|
||||
func (b *Bot) BaseURL() string
|
||||
@@ -196,7 +209,7 @@ func (b *Bot) BaseURL() string
|
||||
BaseURL returns the configured Telegram API base URL.
|
||||
|
||||
<a name="Bot.Codec"></a>
|
||||
### func \(\*Bot\) [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L27>)
|
||||
### func \(\*Bot\) [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L38>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Codec() Codec
|
||||
@@ -205,7 +218,7 @@ func (b *Bot) Codec() Codec
|
||||
Codec returns the configured Codec.
|
||||
|
||||
<a name="Bot.HTTP"></a>
|
||||
### func \(\*Bot\) [HTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L24>)
|
||||
### func \(\*Bot\) [HTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L35>)
|
||||
|
||||
```go
|
||||
func (b *Bot) HTTP() HTTPDoer
|
||||
@@ -214,7 +227,7 @@ func (b *Bot) HTTP() HTTPDoer
|
||||
HTTP returns the underlying HTTPDoer. Exposed for adapters that need to share connection pools or for diagnostic checks.
|
||||
|
||||
<a name="Bot.Logger"></a>
|
||||
### func \(\*Bot\) [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L30>)
|
||||
### func \(\*Bot\) [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L41>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Logger() Logger
|
||||
@@ -223,7 +236,7 @@ func (b *Bot) Logger() Logger
|
||||
Logger returns the configured Logger.
|
||||
|
||||
<a name="Bot.Token"></a>
|
||||
### func \(\*Bot\) [Token](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L17>)
|
||||
### func \(\*Bot\) [Token](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L28>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Token() string
|
||||
@@ -232,7 +245,7 @@ func (b *Bot) Token() string
|
||||
Token returns the bot token. Exposed for advanced use cases \(custom transports, manual URL building\); ordinary code does not need it.
|
||||
|
||||
<a name="Codec"></a>
|
||||
## type [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L10-L13>)
|
||||
## type [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L14-L17>)
|
||||
|
||||
Codec encodes/decodes JSON payloads exchanged with the Telegram Bot API. The default implementation wraps goccy/go\-json. Users may plug in bytedance/sonic or any compatible encoder by passing WithCodec to New.
|
||||
|
||||
@@ -244,7 +257,7 @@ type Codec interface {
|
||||
```
|
||||
|
||||
<a name="DefaultCodec"></a>
|
||||
## type [DefaultCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L16>)
|
||||
## type [DefaultCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L29>)
|
||||
|
||||
DefaultCodec wraps goccy/go\-json. It is the zero\-value safe default.
|
||||
|
||||
@@ -253,7 +266,7 @@ type DefaultCodec struct{}
|
||||
```
|
||||
|
||||
<a name="DefaultCodec.Marshal"></a>
|
||||
### func \(DefaultCodec\) [Marshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L19>)
|
||||
### func \(DefaultCodec\) [Marshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L32>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) Marshal(v any) ([]byte, error)
|
||||
@@ -261,8 +274,17 @@ func (DefaultCodec) Marshal(v any) ([]byte, error)
|
||||
|
||||
Marshal calls json.Marshal.
|
||||
|
||||
<a name="DefaultCodec.MarshalTo"></a>
|
||||
### func \(DefaultCodec\) [MarshalTo](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L40>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) MarshalTo(w io.Writer, v any) error
|
||||
```
|
||||
|
||||
MarshalTo encodes v into w via goccy/go\-json's streaming encoder. The trailing newline that Encoder appends is valid JSON whitespace and is accepted by Telegram's parser.
|
||||
|
||||
<a name="DefaultCodec.Unmarshal"></a>
|
||||
### func \(DefaultCodec\) [Unmarshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L22>)
|
||||
### func \(DefaultCodec\) [Unmarshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L35>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) Unmarshal(data []byte, v any) error
|
||||
|
||||
Reference in New Issue
Block a user