Skip to content

Commit f7eb51a

Browse files
update buffer read writer to use []byte array
1 parent 5192dbb commit f7eb51a

2 files changed

Lines changed: 104 additions & 61 deletions

File tree

lib/store/base/buffer_readwriter.go

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,54 +17,96 @@ package base
1717
import (
1818
"fmt"
1919
"io"
20-
21-
"github.com/aws/aws-sdk-go/aws"
20+
"sync"
21+
"sync/atomic"
2222
)
2323

2424
var _ FileReadWriter = &BufferReadWriter{}
2525

26-
// BufferReadWriter implements FileReadWriter interface for in-memory buffering.
26+
// BufferReadWriter implements FileReadWriter for in-memory buffering.
27+
//
28+
// When created with size > 0, WriteAt takes a fast path using RLock, allowing
29+
// concurrent goroutines writing to non-overlapping byte ranges to proceed in
30+
// parallel with no serialization. The maximum written extent is tracked via an
31+
// atomic so that Bytes(), Size(), Read, and ReadAt return only the data that
32+
// was actually written.
33+
//
34+
// When created with size == 0, every WriteAt that extends the buffer acquires a
35+
// full write lock to grow the backing slice.
36+
//
37+
// Write, Read, ReadAt, and Seek must not be called concurrently with each other
38+
// or with WriteAt.
2739
type BufferReadWriter struct {
28-
buf *aws.WriteAtBuffer
29-
offset int64
40+
mu sync.RWMutex
41+
buf []byte
42+
written atomic.Int64
43+
offset int64
3044
}
3145

32-
// NewBufferReadWriter creates a new BufferReadWriter with an initial capacity of size bytes.
46+
// NewBufferReadWriter creates a new BufferReadWriter pre-allocated to size bytes.
47+
// Pass the exact blob size when known to enable lock-free
48+
// concurrent WriteAt calls for non-overlapping shard ranges.
3349
func NewBufferReadWriter(size uint64) *BufferReadWriter {
34-
bytesSlice := make([]byte, 0, size)
35-
buf := aws.NewWriteAtBuffer(bytesSlice)
36-
// Although this is default, this is explicitly set to notify that we are reserving
37-
// only as much capacity as needed
38-
buf.GrowthCoeff = 1
39-
40-
return &BufferReadWriter{
41-
buf: buf,
42-
offset: 0,
43-
}
50+
return &BufferReadWriter{buf: make([]byte, size)}
4451
}
4552

46-
// Write implements io.Writer by using WriteAt with current write offset.
53+
// Write implements io.Writer using the current sequential write offset.
4754
func (b *BufferReadWriter) Write(p []byte) (n int, err error) {
48-
n, err = b.buf.WriteAt(p, b.offset)
55+
n, err = b.WriteAt(p, b.offset)
4956
b.offset += int64(n)
5057
return n, err
5158
}
5259

53-
// WriteAt implements io.WriterAt for parallel writes.
54-
func (b *BufferReadWriter) WriteAt(p []byte, off int64) (n int, err error) {
60+
// WriteAt implements io.WriterAt.
61+
//
62+
// Fast path (off+len(p) within pre-allocated buffer): multiple goroutines may
63+
// call WriteAt concurrently, provided their byte ranges do not overlap.
64+
// Slow path (write extends beyond current buffer): acquires an exclusive lock
65+
// to grow the buffer, then writes.
66+
func (b *BufferReadWriter) WriteAt(p []byte, off int64) (int, error) {
5567
if off < 0 {
5668
return 0, fmt.Errorf("negative offset")
5769
}
58-
return b.buf.WriteAt(p, off)
70+
end := off + int64(len(p))
71+
if end < off {
72+
return 0, fmt.Errorf("write at offset %d length %d overflows int64", off, len(p))
73+
}
74+
75+
b.mu.RLock()
76+
if end <= int64(len(b.buf)) {
77+
n := copy(b.buf[off:], p)
78+
for {
79+
cur := b.written.Load()
80+
if end <= cur || b.written.CompareAndSwap(cur, end) {
81+
break
82+
}
83+
}
84+
b.mu.RUnlock()
85+
return n, nil
86+
}
87+
b.mu.RUnlock()
88+
89+
b.mu.Lock()
90+
defer b.mu.Unlock()
91+
if end > int64(len(b.buf)) {
92+
grown := make([]byte, end)
93+
copy(grown, b.buf)
94+
b.buf = grown
95+
}
96+
n := copy(b.buf[off:], p)
97+
if end > b.written.Load() {
98+
b.written.Store(end)
99+
}
100+
return n, nil
59101
}
60102

61103
// Read implements io.Reader for sequential reads.
62104
func (b *BufferReadWriter) Read(p []byte) (n int, err error) {
63-
bufBytes := b.buf.Bytes()
64-
if b.offset >= int64(len(bufBytes)) {
105+
written := b.written.Load()
106+
if b.offset >= written {
65107
return 0, io.EOF
66108
}
67-
n = copy(p, bufBytes[b.offset:])
109+
n = copy(p, b.buf[b.offset:written])
68110
b.offset += int64(n)
69111
if n < len(p) {
70112
err = io.EOF
@@ -77,11 +119,14 @@ func (b *BufferReadWriter) ReadAt(p []byte, off int64) (n int, err error) {
77119
if off < 0 {
78120
return 0, fmt.Errorf("negative offset")
79121
}
80-
bufBytes := b.buf.Bytes()
81-
if off >= int64(len(bufBytes)) {
122+
b.mu.RLock()
123+
buf := b.buf
124+
written := b.written.Load()
125+
b.mu.RUnlock()
126+
if off >= written {
82127
return 0, io.EOF
83128
}
84-
n = copy(p, bufBytes[off:])
129+
n = copy(p, buf[off:written])
85130
if n < len(p) {
86131
err = io.EOF
87132
}
@@ -91,23 +136,19 @@ func (b *BufferReadWriter) ReadAt(p []byte, off int64) (n int, err error) {
91136
// Seek implements io.Seeker.
92137
func (b *BufferReadWriter) Seek(offset int64, whence int) (int64, error) {
93138
var newOffset int64
94-
bufSize := int64(len(b.buf.Bytes()))
95-
96139
switch whence {
97140
case io.SeekStart:
98141
newOffset = offset
99142
case io.SeekCurrent:
100143
newOffset = b.offset + offset
101144
case io.SeekEnd:
102-
newOffset = bufSize + offset
145+
newOffset = b.written.Load() + offset
103146
default:
104147
return 0, fmt.Errorf("invalid whence: %d", whence)
105148
}
106-
107149
if newOffset < 0 {
108150
return 0, fmt.Errorf("negative position: %d", newOffset)
109151
}
110-
111152
b.offset = newOffset
112153
return newOffset, nil
113154
}
@@ -117,10 +158,8 @@ func (b *BufferReadWriter) Close() error {
117158
return nil
118159
}
119160

120-
// Size returns the size of the buffer
121-
func (b *BufferReadWriter) Size() int64 {
122-
return int64(len(b.buf.Bytes()))
123-
}
161+
// Size returns the largest end offset written so far.
162+
func (b *BufferReadWriter) Size() int64 { return b.written.Load() }
124163

125164
// Cancel is no-op
126165
func (b *BufferReadWriter) Cancel() error {
@@ -132,7 +171,7 @@ func (b *BufferReadWriter) Commit() error {
132171
return nil
133172
}
134173

135-
// Bytes returns the full buffer
174+
// Bytes returns the bytes that have been written so far.
136175
func (b *BufferReadWriter) Bytes() []byte {
137-
return b.buf.Bytes()
176+
return b.buf[:b.written.Load()]
138177
}

lib/store/base/buffer_readwriter_test.go

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package base
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"io"
2021
"runtime"
@@ -314,7 +315,6 @@ func TestBufferReadWriter_TestReader(t *testing.T) {
314315

315316
// TestBufferReadWriter_ConcurrentWriteAt validates that concurrent writes to
316317
// non-overlapping byte ranges on a pre-sized buffer produce correct results.
317-
// Run with -race to confirm no data races.
318318
func TestBufferReadWriter_ConcurrentWriteAt(t *testing.T) {
319319
const numShards, shardSize = 10, 1024
320320
data := make([]byte, numShards*shardSize)
@@ -323,35 +323,40 @@ func TestBufferReadWriter_ConcurrentWriteAt(t *testing.T) {
323323
}
324324

325325
buf := NewBufferReadWriter(numShards * shardSize)
326+
errs := make([]error, numShards)
326327
var wg sync.WaitGroup
327328
for i := 0; i < numShards; i++ {
328329
wg.Add(1)
329330
go func(shard int) {
330331
defer wg.Done()
331-
off := int64(shard * shardSize)
332-
_, err := buf.WriteAt(data[off:off+shardSize], off)
333-
require.NoError(t, err)
332+
off := shard * shardSize
333+
_, errs[shard] = buf.WriteAt(data[off:off+shardSize], int64(off))
334334
}(i)
335335
}
336336
wg.Wait()
337+
338+
require.NoError(t, errors.Join(errs...))
337339
assert.Equal(t, data, buf.Bytes())
338340
}
339341

340-
// totalMutexContentions returns the total number of mutex contention events
341-
// recorded in the runtime mutex profile since profiling was enabled.
342342
func totalMutexContentions() int64 {
343-
records := make([]runtime.BlockProfileRecord, 10000)
344-
n, _ := runtime.MutexProfile(records)
345-
var total int64
346-
for i := 0; i < n; i++ {
347-
total += records[i].Count
343+
size := 1024
344+
for {
345+
records := make([]runtime.BlockProfileRecord, size)
346+
n, ok := runtime.MutexProfile(records)
347+
if ok {
348+
var total int64
349+
for i := 0; i < n; i++ {
350+
total += records[i].Count
351+
}
352+
return total
353+
}
354+
size = n + 64
348355
}
349-
return total
350356
}
351357

352358
// benchmarkWriteAt is the shared helper for all WriteAt benchmarks.
353359
// numShards goroutines each write a non-overlapping 4 MiB shard concurrently,
354-
// matching the transfermanager production workload.
355360
// initSize controls the buffer's initial allocation:
356361
// - initSize == totalSize: pre-sized fast path (production case)
357362
// - initSize == 0: dynamic growth (sequential / wrong-size case)
@@ -369,25 +374,29 @@ func benchmarkWriteAt(b *testing.B, numShards int, initSize uint64) {
369374
}
370375
}
371376

372-
runtime.SetMutexProfileFraction(1)
377+
prev := runtime.SetMutexProfileFraction(1)
378+
defer runtime.SetMutexProfileFraction(prev)
373379
b.ResetTimer()
374380
b.SetBytes(int64(totalSize))
375381
b.ReportAllocs()
376382

377383
startContentions := totalMutexContentions()
378384

385+
errs := make([]error, numShards)
379386
for i := 0; i < b.N; i++ {
380387
buf := NewBufferReadWriter(initSize)
381388
var wg sync.WaitGroup
382389
for shard := 0; shard < numShards; shard++ {
383390
wg.Add(1)
384391
go func(s int) {
385392
defer wg.Done()
386-
_, err := buf.WriteAt(shards[s], int64(s)*shardSize)
387-
require.NoError(b, err)
393+
_, errs[s] = buf.WriteAt(shards[s], int64(s)*shardSize)
388394
}(shard)
389395
}
390396
wg.Wait()
397+
if err := errors.Join(errs...); err != nil {
398+
b.Fatal(err)
399+
}
391400
}
392401

393402
b.StopTimer()
@@ -396,13 +405,8 @@ func benchmarkWriteAt(b *testing.B, numShards int, initSize uint64) {
396405
}
397406
}
398407

399-
// BenchmarkBufferReadWriter_WriteAt exercises three buffer initialisation
400-
// strategies × three shard counts, giving a full picture of throughput and
401-
// mutex contention under varying concurrency and pre-allocation.
402-
//
403-
// Run before and after the implementation change, then compare with:
404-
//
405-
// benchstat bench-results/before.txt bench-results/after.txt
408+
// BenchmarkBufferReadWriter_WriteAt exercises three buffer initialization
409+
// strategies × three shard counts.
406410
func BenchmarkBufferReadWriter_WriteAt(b *testing.B) {
407411
const shardSize = 4 * 1024 * 1024
408412
cases := []struct {

0 commit comments

Comments
 (0)