perf: Reduce allocations in Client.Do response decoding with a buffer pool - #4494
perf: Reduce allocations in Client.Do response decoding with a buffer pool#4494JamBalaya56562 wants to merge 4 commits into
Client.Do response decoding with a buffer pool#4494Conversation
…er pool Replace the per-call json.NewDecoder streaming decode with a pooled bytes.Buffer read followed by json.Unmarshal. io.ReadAll-style decoding allocated a geometrically growing buffer per response (about 1.57MB for a 500KB payload); reading into a pooled buffer cuts that to about 0.62MB and 13 allocations. Empty and whitespace-only bodies keep returning nil, matching the previous io.EOF handling. Original work by @merchantmoh-debug in google#4195, scoped to Client.Do decoding only as agreed with the maintainers there.
Add tests for the io.Writer path (success and write error), invalid JSON, whitespace-only bodies and the nil-v no-op, covering the decoding branches touched by the pooled-buffer change. Test cases provided by @gmlewis in the google#4195 review, adjusted to satisfy the current linters (extraneousnew, fmtpercentv, revive).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4494 +/- ##
=======================================
Coverage 98.53% 98.53%
=======================================
Files 195 195
Lines 17691 17704 +13
=======================================
+ Hits 17431 17444 +13
Misses 260 260 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
alexandear
left a comment
There was a problem hiding this comment.
My comments are the same as in #4195.
Replace the decode-helper benchmarks with a single BenchmarkDo that runs through Client.Do against a stubbed transport, so the measured numbers reflect the production code path instead of a re-implementation. The old-behavior baseline comes from running the same benchmark on master. Addresses review feedback from @alexandear in google#4195.
Cap the capacity of buffers returned to requestBufferPool at 1MB so an occasional very large response cannot pin memory in the pool. Add tests for the cap, for large-then-small body reuse, and for response body read errors. Addresses review feedback from @alexandear in google#4195.
|
@alexandear Addressed all three in 56f55d4 and 7e43661:
On the broader question of whether this is worth shipping at all: the numbers above are now measured on the production path rather than a re-implementation, which is the most concrete data I can offer; I'll leave that call to the maintainers. |
Supersedes #4195 — all credit for the idea and the core implementation goes to @merchantmoh-debug. That PR reached a shape the maintainers agreed on (scoped strictly to
Client.Dodecoding, with theNewRequestpooling stripped per the review discussion), but has been inactive for about three and a half months with only the coverage gap left, so this picks it up and carries it over the line on top of the currentmaster.What it does
Replaces the per-call
json.NewDecoder(resp.Body).Decode(v)inClient.Dowith a read into a pooledbytes.Bufferfollowed byjson.Unmarshal. The streaming decoder's internal buffering allocated a geometrically growing buffer per response; the pool amortizes that across calls. Buffers whose capacity grew beyond 1MB while reading an unusually large response are dropped instead of pooled, so they cannot pin memory.Benchmarks
BenchmarkDoruns through the realClient.Doagainst a stubbed transport; the baseline is the same benchmark run onmaster(34cec4ff). benchstat, n=8:Do/1KBDo/500KBsec/op improved as well (−70.7% / −82.9%, p=0.000), though the baseline runs had high variance on the measurement machine (±56% / ±122%), so the allocation metrics above are the reliable signal.
Semantics
nil, matching the previousio.EOFhandling (covered byTestDo_noContentand the newTestDo_whitespaceOnlyBody).json.Unmarshalis stricter about trailing data than the streaming decoder — this was discussed on perf: Boost decoding throughput and eliminate payload buffer allocations in core client #4195 and the affected test fixtures were already fixed in test: Fix invalid JSON payloads in actions workflow runs tests #4197.What's new relative to #4195
master.Client.Docoverage tests that @gmlewis wrote and posted in the perf: Boost decoding throughput and eliminate payload buffer allocations in core client #4195 review are now actually applied (TestDo_ioWriter,TestDo_ioWriter_error,TestDo_invalidJSON,TestDo_whitespaceOnlyBody,TestDo_nilV_noop), lightly adjusted to satisfy the current linters (extraneousnew,fmtpercentv,revive). This closes the codecov gap that was the last open item there.Client.Dopath withb.Loopand an on-masterbaseline instead of decode-helper re-implementations; oversized buffers are dropped from the pool (maxPooledBufferCap), with tests for the cap, large-then-small body reuse, and response body read errors.Verified with
go build ./...,gofmt, the full./github/test suite, the benchmarks above, andcustom-gcl(no findings on the changed code).cc @merchantmoh-debug — hope you don't mind me carrying this forward; happy to hand it back if you'd like to finish it yourself.