Skip to content

Commit bac4588

Browse files
readyreader: add buffered reader (#9055)
This PR introduces a buffered `io.Reader` that automatically releases its read buffer when empty. To optimize memory usage, the reader defers buffer reallocation until data is available in the underlying `readyreader.Reader` (achieved by calling `ReadOnReady`). In a follow-up PR (#9032), the HTTP/2 framer will be updated to utilize this new buffered reader whenever the underlying reader implements the `readyreader.Reader` interface. The implementation and associated tests are based on the [standard library's](https://cs.opensource.google/go/go/+/refs/tags/go1.26.2:src/bufio/bufio.go;l=35). RELEASE NOTES: N/A --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 39a15f6 commit bac4588

2 files changed

Lines changed: 339 additions & 68 deletions

File tree

internal/transport/readyreader/ready_reader.go

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ type readState struct {
6363
buf *[]byte
6464
}
6565

66-
// newNonBlockingReader returns a ReadyReader if the passed reader supports
66+
// newNonBlocking returns a ReadyReader if the passed reader supports
6767
// non-memory-pinning reads, else nil.
68-
func newNonBlockingReader(r io.Reader) Reader {
68+
func newNonBlocking(r io.Reader) Reader {
6969
if rr, ok := r.(Reader); ok {
7070
return rr
7171
}
@@ -153,10 +153,101 @@ func (c *blockingReader) ReadOnReady(bufSize int, pool mem.BufferPool) (*[]byte,
153153

154154
// New detects if [syscall.RawConn] is available for non-memory-pinning reads.
155155
// If [syscall.RawConn] is unavailable, it falls back to using the simpler
156-
// [net.Conn] interface for reads.
156+
// [io.Reader] interface for reads.
157157
func New(r io.Reader) Reader {
158-
if r := newNonBlockingReader(r); r != nil {
158+
if r := newNonBlocking(r); r != nil {
159159
return r
160160
}
161161
return &blockingReader{reader: r}
162162
}
163+
164+
// bufReadyReader implements buffering for a ReadyReader object.
165+
// A new bufReadyReader is created by calling [NewBuffered].
166+
type bufReadyReader struct {
167+
buf *[]byte
168+
pool mem.BufferPool
169+
bufSize int
170+
rd Reader // reader provided by the caller
171+
r, w int // buf read and write positions
172+
err error
173+
constPool constBufferPool // stored as a field to avoid heap allocations.
174+
}
175+
176+
// NewBuffered returns a new [io.Reader] with a buffer of the specified size
177+
// which is allocated from the provided pool.
178+
func NewBuffered(rd Reader, size int, pool mem.BufferPool) io.Reader {
179+
return &bufReadyReader{
180+
rd: rd,
181+
pool: pool,
182+
bufSize: size,
183+
}
184+
}
185+
186+
func (b *bufReadyReader) readErr() error {
187+
err := b.err
188+
b.err = nil
189+
return err
190+
}
191+
192+
func (b *bufReadyReader) buffered() int { return b.w - b.r }
193+
194+
// Read reads data into p. It returns the number of bytes read into p. The
195+
// bytes are taken from at most one Read on the underlying [ReadyReader],
196+
// hence n may be less than len(p). If the underlying [ReadyReader] can return
197+
// a non-zero count with io.EOF, then this Read method can do so as well; see
198+
// the [io.Reader] docs.
199+
func (b *bufReadyReader) Read(p []byte) (n int, err error) {
200+
n = len(p)
201+
if n == 0 {
202+
if b.buffered() > 0 {
203+
return 0, nil
204+
}
205+
return 0, b.readErr()
206+
}
207+
if b.r == b.w {
208+
if b.err != nil {
209+
return 0, b.readErr()
210+
}
211+
if len(p) >= b.bufSize {
212+
// Large read, empty buffer.
213+
// Read directly into p to avoid copy.
214+
b.constPool.buffer = p
215+
_, n, b.err = b.rd.ReadOnReady(len(p), &b.constPool)
216+
return n, b.readErr()
217+
}
218+
// One read.
219+
b.r = 0
220+
b.w = 0
221+
b.buf, n, b.err = b.rd.ReadOnReady(b.bufSize, b.pool)
222+
if n == 0 {
223+
if b.buf != nil {
224+
b.pool.Put(b.buf)
225+
b.buf = nil
226+
}
227+
return 0, b.readErr()
228+
}
229+
b.w += n
230+
}
231+
232+
// copy as much as we can
233+
// b.buf must be non-nil since b.r != b.w.
234+
buf := *b.buf
235+
n = copy(p, buf[b.r:b.w])
236+
b.r += n
237+
if b.r == b.w {
238+
// Consumed entire buffer, release it.
239+
b.pool.Put(b.buf)
240+
b.buf = nil
241+
}
242+
return n, nil
243+
}
244+
245+
type constBufferPool struct {
246+
buffer []byte
247+
}
248+
249+
func (p *constBufferPool) Get(int) *[]byte {
250+
return &p.buffer
251+
}
252+
253+
func (p *constBufferPool) Put(*[]byte) {}

0 commit comments

Comments
 (0)