mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-05 22:43:59 +00:00
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.
This commit is contained in:
@@ -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)
|
||||
+24
-4
@@ -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)}
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user