diff options
| author | Daniel Morsing <daniel.morsing@gmail.com> | 2026-01-25 14:42:26 +0000 |
|---|---|---|
| committer | Daniel Morsing <daniel.morsing@gmail.com> | 2026-04-07 10:40:43 -0700 |
| commit | da723e15d7492143459c65e4fecf2c17dc0fa1e4 (patch) | |
| tree | f17d3f17c506705a3a0718b4e00fa5915f8be711 /src/crypto/tls | |
| parent | 7394184e4ecf7cd1e938b80434f057caeb5ffa58 (diff) | |
| download | go-da723e15d7492143459c65e4fecf2c17dc0fa1e4.tar.xz | |
crypto/tls: avoid atLeastReader and associated allocations.
Go 1.21 introduced bytes.Buffer.AvailableBuffer. This mechanism lets us
write directly into a Buffer's backing memory while also implementing
custom logic without going through interfaces.
atLeastReader caused an allocation on every read. On workloads with very
small reads, these allocations can add up to substantial overhead. By
using Buffer.AvailableBytes(), we can avoid these allocations and an
interface indirection in the Read fast path.
Fixes #58249.
Change-Id: Icf26ec1dd7ef88154c47356ef9c26a516a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/739980
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
Diffstat (limited to 'src/crypto/tls')
| -rw-r--r-- | src/crypto/tls/conn.go | 49 |
1 files changed, 24 insertions, 25 deletions
diff --git a/src/crypto/tls/conn.go b/src/crypto/tls/conn.go index 9c662ef8f6..54a4d1a883 100644 --- a/src/crypto/tls/conn.go +++ b/src/crypto/tls/conn.go @@ -800,29 +800,6 @@ func (c *Conn) retryReadRecord(expectChangeCipherSpec bool) error { return c.readRecordOrCCS(expectChangeCipherSpec) } -// atLeastReader reads from R, stopping with EOF once at least N bytes have been -// read. It is different from an io.LimitedReader in that it doesn't cut short -// the last Read call, and in that it considers an early EOF an error. -type atLeastReader struct { - R io.Reader - N int64 -} - -func (r *atLeastReader) Read(p []byte) (int, error) { - if r.N <= 0 { - return 0, io.EOF - } - n, err := r.R.Read(p) - r.N -= int64(n) // won't underflow unless len(p) >= n > 9223372036854775809 - if r.N > 0 && err == io.EOF { - return n, io.ErrUnexpectedEOF - } - if r.N <= 0 && err == nil { - return n, io.EOF - } - return n, err -} - // readFromUntil reads from r into c.rawInput until c.rawInput contains // at least n bytes or else returns an error. func (c *Conn) readFromUntil(r io.Reader, n int) error { @@ -833,9 +810,31 @@ func (c *Conn) readFromUntil(r io.Reader, n int) error { // There might be extra input waiting on the wire. Make a best effort // attempt to fetch it so that it can be used in (*Conn).Read to // "predict" closeNotify alerts. + // TODO(dmo): we use bytes.MinRead here because we used the buffer + // ReadFrom mechanism to avoid allocations, but we've hoisted this + // loop for performance. We really should use our own heuristic here + // for how much to read ahead. c.rawInput.Grow(needs + bytes.MinRead) - _, err := c.rawInput.ReadFrom(&atLeastReader{r, int64(needs)}) - return err + for { + buf := c.rawInput.AvailableBuffer()[:needs+bytes.MinRead] + n, err := r.Read(buf) + // This write is just to update the internal state of the + // rawInput bytes.Buffer. It cannot fail. + c.rawInput.Write(buf[:n]) + needs -= n + if needs <= 0 { + if err == io.EOF { + err = nil + } + return err + } + if err == io.EOF { + return io.ErrUnexpectedEOF + } + if err != nil { + return err + } + } } // sendAlertLocked sends a TLS alert message. |
