mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-07-07 21:14:43 +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"
|
"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
|
// Call is the single point through which every Telegram Bot API method
|
||||||
// invocation flows. It marshals the request, signs the URL with the bot
|
// invocation flows. It marshals the request, signs the URL with the bot
|
||||||
// token, dispatches via HTTPDoer, decodes the Result envelope, and
|
// 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 {
|
if err != nil {
|
||||||
return zero, &NetworkError{Err: err}
|
return zero, &NetworkError{Err: err}
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header["Content-Type"] = headerJSONValue
|
||||||
httpReq.Header.Set("Accept", "application/json")
|
httpReq.Header["Accept"] = headerJSONValue
|
||||||
|
|
||||||
resp, err := b.http.Do(httpReq)
|
resp, err := b.http.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -91,8 +97,8 @@ func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &NetworkError{Err: err}
|
return nil, &NetworkError{Err: err}
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header["Content-Type"] = headerJSONValue
|
||||||
httpReq.Header.Set("Accept", "application/json")
|
httpReq.Header["Accept"] = headerJSONValue
|
||||||
|
|
||||||
resp, err := b.http.Do(httpReq)
|
resp, err := b.http.Do(httpReq)
|
||||||
if err != nil {
|
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
|
// decodeResult unmarshals raw into Result[Resp] and translates non-OK
|
||||||
// responses into *APIError.
|
// 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) {
|
func decodeResult[Resp any](codec Codec, raw []byte) (Resp, error) {
|
||||||
var zero Resp
|
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]
|
var env Result[Resp]
|
||||||
if err := codec.Unmarshal(raw, &env); err != nil {
|
if err := codec.Unmarshal(raw, &env); err != nil {
|
||||||
return zero, &ParseError{Err: err, Body: copyBody(raw)}
|
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}
|
return zero, &NetworkError{Err: err}
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header["Accept"] = headerJSONValue
|
||||||
|
|
||||||
resp, err := b.http.Do(req)
|
resp, err := b.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,7 +125,7 @@ func callMultipartRaw(ctx context.Context, b *Bot, method string, mp multipartRe
|
|||||||
return nil, &NetworkError{Err: err}
|
return nil, &NetworkError{Err: err}
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header["Accept"] = headerJSONValue
|
||||||
|
|
||||||
resp, err := b.http.Do(req)
|
resp, err := b.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user