mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-05 22:43:59 +00:00
perf(client): pool *bytes.Buffer for response body reads
Replace io.ReadAll(resp.Body) on the typed Call/callMultipart paths with a sync.Pool-backed bytes.Buffer + ReadFrom. Saves the 512B initial allocation that ReadAll grows from on every successful call. The pool only covers paths whose decoder copies strings out of the input (decodeResult delegates to goccy/go-json, which copies). CallRaw and callMultipartRaw return slices that alias the buffer storage, so they keep the io.ReadAll path; pooling there would need a defensive copy that defeats the saving. putRespBuf caps Cap() at 64 KiB before returning to the pool so a single oversized response (e.g. large getFile metadata) doesn't bloat the pool for the rest of the process. Bench delta on Call_BoolResponse: 14 allocs -> 13 allocs, 1842B -> 1331B, 526ns -> 479ns. Same shape on Call_StructResponse (16 -> 15, 1973B -> 1462B).
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
After: static-headers + bool-fast-path + lazy-Values + typed-fields + resp-buffer-pool
|
||||||
|
goos: darwin
|
||||||
|
goarch: arm64
|
||||||
|
cpu: Apple M4 Max
|
||||||
|
|
||||||
|
pkg: github.com/lukaszraczylo/go-telegram/client
|
||||||
|
BenchmarkCall_BoolResponse-16 4811347 478.7 ns/op 1331 B/op 13 allocs/op
|
||||||
|
BenchmarkCall_StructResponse-16 4038770 591.6 ns/op 1462 B/op 15 allocs/op
|
||||||
|
BenchmarkEncodeJSONBody-16 47025052 51.30 ns/op 96 B/op 2 allocs/op
|
||||||
|
BenchmarkDecodeResult_Bool-16 853161562 2.824 ns/op 0 B/op 0 allocs/op
|
||||||
|
BenchmarkDecodeResult_Struct-16 26811634 88.80 ns/op 144 B/op 2 allocs/op
|
||||||
|
|
||||||
|
pkg: github.com/lukaszraczylo/go-telegram/dispatch
|
||||||
|
BenchmarkRouter_DispatchCommand-16 34631486 69.19 ns/op 96 B/op 1 allocs/op
|
||||||
|
BenchmarkRouter_DispatchTextRegex-16 23260198 106.6 ns/op 112 B/op 2 allocs/op
|
||||||
|
BenchmarkRouter_DispatchFilter-16 126697654 19.03 ns/op 96 B/op 1 allocs/op
|
||||||
|
BenchmarkRouter_NewContext-16 1000000000 1.600 ns/op 0 B/op 0 allocs/op
|
||||||
|
BenchmarkExtractCommand-16 27345622 87.25 ns/op 0 B/op 0 allocs/op
|
||||||
|
|
||||||
|
Cumulative deltas vs baseline:
|
||||||
|
Call_BoolResponse: 634ns / 18 allocs / 1957B -> 479ns / 13 allocs / 1331B (-24% / -5 / -626B)
|
||||||
|
Call_StructResponse: 665ns / 18 allocs / 2005B -> 592ns / 15 allocs / 1462B (-11% / -3 / -543B)
|
||||||
|
DecodeResult_Bool: 50ns / 2 allocs / 80B -> 2.8ns / 0 allocs / 0B
|
||||||
|
DispatchCommand: 153ns / 5 allocs / 416B -> 69ns / 1 alloc / 96B (-55% / -4 / -320B)
|
||||||
|
DispatchTextRegex: 181ns / 5 allocs / 428B -> 107ns / 2 allocs / 112B (-41% / -3 / -316B)
|
||||||
|
DispatchFilter: 32ns / 2 allocs / 96B -> 19ns / 1 alloc / 96B (-41% / -1)
|
||||||
+27
-3
@@ -8,14 +8,36 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
headerJSONValue = []string{"application/json"}
|
headerJSONValue = []string{"application/json"}
|
||||||
rawOKTrueBody = []byte(`{"ok":true,"result":true}`)
|
rawOKTrueBody = []byte(`{"ok":true,"result":true}`)
|
||||||
rawOKFalseBody = []byte(`{"ok":true,"result":false}`)
|
rawOKFalseBody = []byte(`{"ok":true,"result":false}`)
|
||||||
|
|
||||||
|
// respBufPool reuses *bytes.Buffer for response body reads. Used on
|
||||||
|
// paths whose decoder copies strings out of the input (decodeResult,
|
||||||
|
// which delegates to goccy/go-json), so the buffer can be returned to
|
||||||
|
// the pool as soon as Unmarshal has run. CallRaw and callMultipartRaw
|
||||||
|
// return slices that alias the buffer and therefore cannot use the
|
||||||
|
// pool without an extra copy that would defeat the point.
|
||||||
|
respBufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxPooledBufCap caps the buffer size returned to respBufPool. Buffers
|
||||||
|
// larger than this are dropped on the floor so a single huge response
|
||||||
|
// (e.g. a large getFile metadata payload) doesn't bloat the pool for the
|
||||||
|
// rest of the process lifetime.
|
||||||
|
const maxPooledBufCap = 64 * 1024
|
||||||
|
|
||||||
|
func putRespBuf(buf *bytes.Buffer) {
|
||||||
|
if buf.Cap() > maxPooledBufCap {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respBufPool.Put(buf)
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -63,12 +85,14 @@ func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req
|
|||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
raw, err := io.ReadAll(resp.Body)
|
buf := respBufPool.Get().(*bytes.Buffer)
|
||||||
if err != nil {
|
buf.Reset()
|
||||||
|
defer putRespBuf(buf)
|
||||||
|
if _, err := buf.ReadFrom(resp.Body); err != nil {
|
||||||
return zero, &NetworkError{Err: err}
|
return zero, &NetworkError{Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
return decodeResult[Resp](b.codec, raw)
|
return decodeResult[Resp](b.codec, buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallRaw is like Call but returns the raw JSON of the result field
|
// CallRaw is like Call but returns the raw JSON of the result field
|
||||||
|
|||||||
+6
-3
@@ -1,6 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
"io"
|
"io"
|
||||||
@@ -81,12 +82,14 @@ func callMultipart[Resp any](ctx context.Context, b *Bot, method string, mp mult
|
|||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
raw, err := io.ReadAll(resp.Body)
|
buf := respBufPool.Get().(*bytes.Buffer)
|
||||||
if err != nil {
|
buf.Reset()
|
||||||
|
defer putRespBuf(buf)
|
||||||
|
if _, err := buf.ReadFrom(resp.Body); err != nil {
|
||||||
_ = pr.CloseWithError(err)
|
_ = pr.CloseWithError(err)
|
||||||
return zero, &NetworkError{Err: err}
|
return zero, &NetworkError{Err: err}
|
||||||
}
|
}
|
||||||
return decodeResult[Resp](b.codec, raw)
|
return decodeResult[Resp](b.codec, buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
// callMultipartRaw is callMultipart's sibling that returns the raw result
|
// callMultipartRaw is callMultipart's sibling that returns the raw result
|
||||||
|
|||||||
Reference in New Issue
Block a user