Skip to content

Commit d2a9ee3

Browse files
feat(kernel range reader): Add buffer pooling support for large reads on regional buckets (#4852)
optimizing memory usage and reducing allocation overhead during large file reads by introducing a sync.Pool for buffer allocation and refactoring the vectored reading mechanism to allocate buffers on-demand (lazily).
1 parent d830c5c commit d2a9ee3

16 files changed

Lines changed: 1199 additions & 379 deletions

internal/buffer/buffer_pool.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package buffer
16+
17+
import (
18+
"sync"
19+
20+
"github.com/googlecloudplatform/gcsfuse/v3/internal/logger"
21+
"github.com/googlecloudplatform/gcsfuse/v3/internal/util"
22+
)
23+
24+
// Pool defines an interface for on-demand buffer allocation and recycling.
25+
type Pool interface {
26+
Get() []byte
27+
Put([]byte)
28+
}
29+
30+
// readPoolBufferSize is the size of each buffer in readBufferPool (1 MiB).
31+
const readPoolBufferSize = util.MiB
32+
33+
// Ensure FixedSizePool implements Pool at compile time.
34+
var _ Pool = (*FixedSizePool)(nil)
35+
36+
type FixedSizePool struct {
37+
// Pool is the underlying sync.Pool storing fixed-size byte slices.
38+
Pool *sync.Pool
39+
}
40+
41+
// NewFixedSizePool creates a new FixedSizePool with 1 MiB buffers.
42+
func NewFixedSizePool() *FixedSizePool {
43+
return &FixedSizePool{
44+
Pool: &sync.Pool{
45+
New: func() any {
46+
return new([readPoolBufferSize]byte)
47+
},
48+
},
49+
}
50+
}
51+
52+
func (bp *FixedSizePool) Get() []byte {
53+
return bp.Pool.Get().(*[readPoolBufferSize]byte)[:]
54+
}
55+
56+
func (bp *FixedSizePool) Put(buf []byte) {
57+
if cap(buf) != readPoolBufferSize {
58+
logger.Errorf("FixedSizePool::Put Buffer capacity does not match readPoolBufferSize: %d vs %d", cap(buf), readPoolBufferSize)
59+
return
60+
}
61+
bp.Pool.Put((*[readPoolBufferSize]byte)(buf[:cap(buf)]))
62+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package buffer
16+
17+
import (
18+
"testing"
19+
20+
"github.com/stretchr/testify/assert"
21+
)
22+
23+
func TestFixedSizePool_Get(t *testing.T) {
24+
// Arrange
25+
bp := NewFixedSizePool()
26+
27+
// Act
28+
buf := bp.Get()
29+
30+
// Assert
31+
assert.Equal(t, readPoolBufferSize, len(buf))
32+
assert.Equal(t, readPoolBufferSize, cap(buf))
33+
}
34+
35+
func TestFixedSizePool_Put(t *testing.T) {
36+
testCases := []struct {
37+
name string
38+
buf []byte
39+
}{
40+
{
41+
name: "ValidBuffer",
42+
buf: make([]byte, readPoolBufferSize),
43+
},
44+
{
45+
name: "CapacityLessThanPoolSize",
46+
buf: make([]byte, 10),
47+
},
48+
{
49+
name: "CapacityGreaterThanPoolSize",
50+
buf: make([]byte, readPoolBufferSize+10),
51+
},
52+
}
53+
54+
for _, tc := range testCases {
55+
t.Run(tc.name, func(t *testing.T) {
56+
// Arrange
57+
bp := NewFixedSizePool()
58+
59+
// Act & Assert
60+
assert.NotPanics(t, func() {
61+
bp.Put(tc.buf)
62+
})
63+
})
64+
}
65+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package buffer
16+
17+
// FakeBufferPool is a generic fake implementation of Pool used for testing across packages.
18+
type FakeBufferPool struct {
19+
Buffers [][]byte
20+
Idx int
21+
PutBuffers [][]byte
22+
ReturnNilOnExhaustion bool
23+
DefaultBufferSize int
24+
}
25+
26+
// Get returns the next available buffer from Buffers. If all buffers have been consumed,
27+
// it returns nil if ReturnNilOnExhaustion is true, or a newly allocated buffer of size
28+
// DefaultBufferSize (or 1024 if DefaultBufferSize is <= 0).
29+
func (p *FakeBufferPool) Get() []byte {
30+
if p.Idx < len(p.Buffers) {
31+
b := p.Buffers[p.Idx]
32+
p.Idx++
33+
return b
34+
}
35+
if p.ReturnNilOnExhaustion {
36+
return nil
37+
}
38+
size := p.DefaultBufferSize
39+
if size <= 0 {
40+
size = 1024
41+
}
42+
return make([]byte, size)
43+
}
44+
45+
// Put records the returned buffer in PutBuffers.
46+
func (p *FakeBufferPool) Put(b []byte) {
47+
p.PutBuffers = append(p.PutBuffers, b)
48+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package buffer
16+
17+
import (
18+
"io"
19+
)
20+
21+
// VectoredReadBuffer accumulates data across a sequence of pooled byte buffers up to maxSize.
22+
// It implements io.Writer and io.ReaderFrom, and provides ReadFromAt to support zero-allocation,
23+
// zero-copy reads from random-access sources (io.ReaderAt). Buffers are allocated lazily from pool
24+
// on demand as data is written or read.
25+
//
26+
// Invariant: For each slice b in buffers, len(b) tracks the number of valid populated (written or read)
27+
// bytes, and cap(b) tracks the total capacity allocated from the pool. We never use 3-index slicing
28+
// to shrink cap(b) so that Release() can safely return full-capacity buffers back to the pool.
29+
type VectoredReadBuffer struct {
30+
// buffers holds the slices of bytes allocated from pool.
31+
// For each buffer b in buffers, len(b) represents the bytes actually populated (written or read),
32+
// while cap(b) represents the full capacity allocated from the pool.
33+
buffers [][]byte
34+
35+
// pool is the underlying pool from which new byte buffers are allocated on demand.
36+
// If nil, no new buffers will be allocated once existing buffer capacity is exhausted.
37+
pool Pool
38+
39+
// maxSize is the maximum total number of bytes allowed to be populated across all buffers.
40+
// Once written reaches maxSize, subsequent writes will return io.ErrShortWrite or reads will stop.
41+
maxSize int64
42+
43+
// written tracks the cumulative number of bytes populated (written or read) into buffers so far.
44+
written int64
45+
}
46+
47+
// NewVectoredReadBuffer creates a new VectoredReadBuffer that allocates buffers on demand from pool.
48+
func NewVectoredReadBuffer(pool Pool, maxSize int64) *VectoredReadBuffer {
49+
return &VectoredReadBuffer{
50+
buffers: make([][]byte, 0, 2),
51+
pool: pool,
52+
maxSize: maxSize,
53+
}
54+
}
55+
56+
// availableBuffer returns a slice of the current buffer available for writing.
57+
// If the current buffer is full or no buffers exist, it allocates a new buffer
58+
// from the pool. Returns nil if no more buffers can be allocated (e.g. pool is nil
59+
// or maxSize is reached).
60+
func (v *VectoredReadBuffer) availableBuffer() []byte {
61+
if v.written >= v.maxSize {
62+
return nil
63+
}
64+
65+
avail := v.maxSize - v.written
66+
67+
// 1. Check if the active (last) buffer has remaining capacity.
68+
if len(v.buffers) > 0 {
69+
lastBuf := v.buffers[len(v.buffers)-1]
70+
if remCap := cap(lastBuf) - len(lastBuf); remCap > 0 {
71+
// Return the unpopulated tail of lastBuf, clamped by remaining maxSize.
72+
rem := int(min(int64(remCap), avail))
73+
return lastBuf[len(lastBuf) : len(lastBuf)+rem]
74+
}
75+
}
76+
77+
// 2. Fetch new buffer from pool.
78+
if v.pool != nil {
79+
if buf := v.pool.Get(); cap(buf) > 0 {
80+
// Append with length 0, preserving full pool capacity for Release().
81+
v.buffers = append(v.buffers, buf[:0])
82+
83+
// Clamp the returned slice length (NOT capacity) to remaining maxSize.
84+
return buf[:min(int64(cap(buf)), avail)]
85+
}
86+
}
87+
return nil
88+
}
89+
90+
func (v *VectoredReadBuffer) Write(p []byte) (int, error) {
91+
var totalWritten int
92+
for totalWritten < len(p) {
93+
buf := v.availableBuffer()
94+
if buf == nil {
95+
return totalWritten, io.ErrShortWrite
96+
}
97+
// Copy up to len(buf) (the available space in this chunk) bytes from p into the buffer.
98+
bytesCopied := copy(buf, p[totalWritten:])
99+
100+
// Expand the active buffer's length to include the newly written bytes.
101+
idx := len(v.buffers) - 1
102+
lastBuf := v.buffers[idx]
103+
v.buffers[idx] = lastBuf[:len(lastBuf)+bytesCopied]
104+
105+
// Track cumulative bytes written across all buffers and total bytes written in this call.
106+
v.written += int64(bytesCopied)
107+
totalWritten += bytesCopied
108+
}
109+
return totalWritten, nil
110+
}
111+
112+
func (v *VectoredReadBuffer) readIntoBuffers(readFn func(buf []byte) (int, error)) (int64, error) {
113+
var totalRead int64
114+
for {
115+
buf := v.availableBuffer()
116+
if buf == nil {
117+
return totalRead, nil
118+
}
119+
// Read up to len(buf) (the available space in this chunk) bytes directly into the buffer.
120+
bytesRead, err := readFn(buf)
121+
if bytesRead > 0 {
122+
// Expand the active buffer's length to include the newly read bytes.
123+
idx := len(v.buffers) - 1
124+
lastBuf := v.buffers[idx]
125+
v.buffers[idx] = lastBuf[:len(lastBuf)+bytesRead]
126+
127+
// Track cumulative bytes read across all buffers and total bytes read in this call.
128+
v.written += int64(bytesRead)
129+
totalRead += int64(bytesRead)
130+
}
131+
if err != nil {
132+
if err == io.EOF {
133+
return totalRead, nil
134+
}
135+
return totalRead, err
136+
}
137+
}
138+
}
139+
140+
// ReadFrom implements io.ReaderFrom. It reads data from r directly into the
141+
// underlying buffers, avoiding intermediate allocations and double-copying.
142+
func (v *VectoredReadBuffer) ReadFrom(r io.Reader) (int64, error) {
143+
return v.readIntoBuffers(r.Read)
144+
}
145+
146+
// ReadFromAt reads data from r starting at offset directly into the
147+
// underlying buffers, avoiding intermediate allocations and double-copying.
148+
func (v *VectoredReadBuffer) ReadFromAt(r io.ReaderAt, offset int64) (int64, error) {
149+
return v.readIntoBuffers(func(buf []byte) (int, error) {
150+
return r.ReadAt(buf, offset+v.written)
151+
})
152+
}
153+
154+
// Buffers returns the slices of bytes actually written to.
155+
func (v *VectoredReadBuffer) Buffers() [][]byte {
156+
// If the last buffer was allocated by availableBuffer() but nothing was written to it,
157+
// exclude it from the returned slice without reallocating.
158+
if len(v.buffers) > 0 && len(v.buffers[len(v.buffers)-1]) == 0 {
159+
return v.buffers[:len(v.buffers)-1]
160+
}
161+
return v.buffers
162+
}
163+
164+
// Release puts all allocated buffers back into the pool.
165+
func (v *VectoredReadBuffer) Release() {
166+
if v.pool == nil {
167+
return
168+
}
169+
for _, b := range v.buffers {
170+
v.pool.Put(b[:cap(b)])
171+
}
172+
v.buffers = nil
173+
}

0 commit comments

Comments
 (0)