From da2742152147c49cd4f061de21833151ca4b58b8 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 10 May 2026 02:32:00 +0100 Subject: [PATCH] perf(client): static headers + bool fast-path in decodeResult Two changes on the Call hot path: * Replace httpReq.Header.Set("Content-Type", "application/json") (and Accept) with direct map writes against a package-level []string. Both keys are already canonical so the canonicalising path inside Header.Set was pure overhead; saves the per-call []string{val} allocation x2. * Add a bool fast-path in decodeResult: ~60% of Telegram methods return bool, and the API emits the envelope with no whitespace, so a bytes.Equal check against the two canonical bodies short-circuits the generic Result[bool] Unmarshal entirely. any(true).(Resp) does not box thanks to Go's static bool interface values. Combined effect on Call_BoolResponse: 18 -> 14 allocs/op, 634ns -> 526ns. DecodeResult_Bool isolation bench: 50ns / 2 allocs -> 2.87ns / 0 allocs. --- .benchstats/after_step1.txt | 17 +++++++++++++++++ client/call.go | 28 ++++++++++++++++++++++++---- client/multipart.go | 4 ++-- 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 .benchstats/after_step1.txt diff --git a/.benchstats/after_step1.txt b/.benchstats/after_step1.txt new file mode 100644 index 0000000..9d66e68 --- /dev/null +++ b/.benchstats/after_step1.txt @@ -0,0 +1,17 @@ +After: static-header-slices + bool-fast-path +goos: darwin +goarch: arm64 +pkg: github.com/lukaszraczylo/go-telegram/client +cpu: Apple M4 Max +BenchmarkCall_BoolResponse-16 4597374 526.3 ns/op 1842 B/op 14 allocs/op +BenchmarkCall_StructResponse-16 3740096 679.6 ns/op 1973 B/op 16 allocs/op +BenchmarkEncodeJSONBody-16 41997352 58.35 ns/op 96 B/op 2 allocs/op +BenchmarkDecodeResult_Bool-16 863273641 2.872 ns/op 0 B/op 0 allocs/op +BenchmarkDecodeResult_Struct-16 19988096 100.3 ns/op 144 B/op 2 allocs/op + +Deltas vs baseline (allocs/op): + Call_BoolResponse: 18 -> 14 (-4) + Call_StructResponse: 18 -> 16 (-2) + DecodeResult_Bool: 2 -> 0 (-2, also -94% ns) + DecodeResult_Struct: 2 -> 2 (flat) + EncodeJSONBody: 2 -> 2 (flat) diff --git a/client/call.go b/client/call.go index 1c20c77..76a173a 100644 --- a/client/call.go +++ b/client/call.go @@ -10,6 +10,12 @@ import ( "reflect" ) +var ( + headerJSONValue = []string{"application/json"} + rawOKTrueBody = []byte(`{"ok":true,"result":true}`) + rawOKFalseBody = []byte(`{"ok":true,"result":false}`) +) + // Call is the single point through which every Telegram Bot API method // invocation flows. It marshals the request, signs the URL with the bot // token, dispatches via HTTPDoer, decodes the Result envelope, and @@ -44,8 +50,8 @@ func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req if err != nil { return zero, &NetworkError{Err: err} } - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Accept", "application/json") + httpReq.Header["Content-Type"] = headerJSONValue + httpReq.Header["Accept"] = headerJSONValue resp, err := b.http.Do(httpReq) if err != nil { @@ -91,8 +97,8 @@ func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json if err != nil { return nil, &NetworkError{Err: err} } - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Accept", "application/json") + httpReq.Header["Content-Type"] = headerJSONValue + httpReq.Header["Accept"] = headerJSONValue resp, err := b.http.Do(httpReq) if err != nil { @@ -139,8 +145,22 @@ func encodeJSONBody(codec Codec, req any) (io.Reader, error) { // decodeResult unmarshals raw into Result[Resp] and translates non-OK // responses into *APIError. +// +// Bool fast path: ~60% of Telegram methods return bool. The Telegram API +// emits the result envelope with no whitespace, so a byte-equality check +// against the two canonical bodies skips the generic Unmarshal entirely. +// Anything that doesn't match exactly (e.g. responses with extra fields, +// errors) falls through to the slow path. func decodeResult[Resp any](codec Codec, raw []byte) (Resp, error) { var zero Resp + if _, isBool := any(zero).(bool); isBool { + switch { + case bytes.Equal(raw, rawOKTrueBody): + return any(true).(Resp), nil + case bytes.Equal(raw, rawOKFalseBody): + return any(false).(Resp), nil + } + } var env Result[Resp] if err := codec.Unmarshal(raw, &env); err != nil { return zero, &ParseError{Err: err, Body: copyBody(raw)} diff --git a/client/multipart.go b/client/multipart.go index 722d7ad..e3f418f 100644 --- a/client/multipart.go +++ b/client/multipart.go @@ -69,7 +69,7 @@ func callMultipart[Resp any](ctx context.Context, b *Bot, method string, mp mult return zero, &NetworkError{Err: err} } req.Header.Set("Content-Type", mw.FormDataContentType()) - req.Header.Set("Accept", "application/json") + req.Header["Accept"] = headerJSONValue resp, err := b.http.Do(req) if err != nil { @@ -125,7 +125,7 @@ func callMultipartRaw(ctx context.Context, b *Bot, method string, mp multipartRe return nil, &NetworkError{Err: err} } req.Header.Set("Content-Type", mw.FormDataContentType()) - req.Header.Set("Accept", "application/json") + req.Header["Accept"] = headerJSONValue resp, err := b.http.Do(req) if err != nil {