Skip to content

Commit 4c0f3b2

Browse files
nojnhuhaojea
authored andcommitted
Add generic buffer.TypedRingGrowing and shrinkable buffer.Ring
Co-authored-by: Antonio Ojea <[email protected]>
1 parent 0f33e8f commit 4c0f3b2

File tree

2 files changed

+293
-41
lines changed

2 files changed

+293
-41
lines changed

buffer/ring_growing.go

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,57 @@ limitations under the License.
1616

1717
package buffer
1818

19+
// defaultRingSize defines the default ring size if not specified
20+
const defaultRingSize = 16
21+
22+
// RingGrowingOptions sets parameters for [RingGrowing] and
23+
// [TypedRingGrowing].
24+
type RingGrowingOptions struct {
25+
// InitialSize is the number of pre-allocated elements in the
26+
// initial underlying storage buffer.
27+
InitialSize int
28+
}
29+
1930
// RingGrowing is a growing ring buffer.
2031
// Not thread safe.
21-
type RingGrowing struct {
22-
data []interface{}
32+
//
33+
// Deprecated: Use TypedRingGrowing[any] instead.
34+
type RingGrowing = TypedRingGrowing[any]
35+
36+
// NewRingGrowing constructs a new RingGrowing instance with provided parameters.
37+
//
38+
// Deprecated: Use NewTypedRingGrowing[any] instead.
39+
func NewRingGrowing(initialSize int) *RingGrowing {
40+
return NewTypedRingGrowing[any](RingGrowingOptions{InitialSize: initialSize})
41+
}
42+
43+
// TypedRingGrowing is a growing ring buffer.
44+
// The zero value has an initial size of 0 and is ready to use.
45+
// Not thread safe.
46+
type TypedRingGrowing[T any] struct {
47+
data []T
2348
n int // Size of Data
2449
beg int // First available element
2550
readable int // Number of data items available
2651
}
2752

28-
// NewRingGrowing constructs a new RingGrowing instance with provided parameters.
29-
func NewRingGrowing(initialSize int) *RingGrowing {
30-
return &RingGrowing{
31-
data: make([]interface{}, initialSize),
32-
n: initialSize,
53+
// NewTypedRingGrowing constructs a new TypedRingGrowing instance with provided parameters.
54+
func NewTypedRingGrowing[T any](opts RingGrowingOptions) *TypedRingGrowing[T] {
55+
return &TypedRingGrowing[T]{
56+
data: make([]T, opts.InitialSize),
57+
n: opts.InitialSize,
3358
}
3459
}
3560

3661
// ReadOne reads (consumes) first item from the buffer if it is available, otherwise returns false.
37-
func (r *RingGrowing) ReadOne() (data interface{}, ok bool) {
62+
func (r *TypedRingGrowing[T]) ReadOne() (data T, ok bool) {
3863
if r.readable == 0 {
39-
return nil, false
64+
return
4065
}
4166
r.readable--
4267
element := r.data[r.beg]
43-
r.data[r.beg] = nil // Remove reference to the object to help GC
68+
var zero T
69+
r.data[r.beg] = zero // Remove reference to the object to help GC
4470
if r.beg == r.n-1 {
4571
// Was the last element
4672
r.beg = 0
@@ -51,11 +77,14 @@ func (r *RingGrowing) ReadOne() (data interface{}, ok bool) {
5177
}
5278

5379
// WriteOne adds an item to the end of the buffer, growing it if it is full.
54-
func (r *RingGrowing) WriteOne(data interface{}) {
80+
func (r *TypedRingGrowing[T]) WriteOne(data T) {
5581
if r.readable == r.n {
5682
// Time to grow
5783
newN := r.n * 2
58-
newData := make([]interface{}, newN)
84+
if newN == 0 {
85+
newN = defaultRingSize
86+
}
87+
newData := make([]T, newN)
5988
to := r.beg + r.readable
6089
if to <= r.n {
6190
copy(newData, r.data[r.beg:to])
@@ -72,11 +101,70 @@ func (r *RingGrowing) WriteOne(data interface{}) {
72101
}
73102

74103
// Len returns the number of items in the buffer.
75-
func (r *RingGrowing) Len() int {
104+
func (r *TypedRingGrowing[T]) Len() int {
76105
return r.readable
77106
}
78107

79108
// Cap returns the capacity of the buffer.
80-
func (r *RingGrowing) Cap() int {
109+
func (r *TypedRingGrowing[T]) Cap() int {
81110
return r.n
82111
}
112+
113+
// RingOptions sets parameters for [Ring].
114+
type RingOptions struct {
115+
// InitialSize is the number of pre-allocated elements in the
116+
// initial underlying storage buffer.
117+
InitialSize int
118+
// NormalSize is the number of elements to allocate for new storage
119+
// buffers once the Ring is consumed and
120+
// can shrink again.
121+
NormalSize int
122+
}
123+
124+
// Ring is a dynamically-sized ring buffer which can grow and shrink as-needed.
125+
// The zero value has an initial size and normal size of 0 and is ready to use.
126+
// Not thread safe.
127+
type Ring[T any] struct {
128+
growing TypedRingGrowing[T]
129+
normalSize int // Limits the size of the buffer that is kept for reuse. Read-only.
130+
}
131+
132+
// NewRing constructs a new Ring instance with provided parameters.
133+
func NewRing[T any](opts RingOptions) *Ring[T] {
134+
return &Ring[T]{
135+
growing: *NewTypedRingGrowing[T](RingGrowingOptions{InitialSize: opts.InitialSize}),
136+
normalSize: opts.NormalSize,
137+
}
138+
}
139+
140+
// ReadOne reads (consumes) first item from the buffer if it is available,
141+
// otherwise returns false. When the buffer has been totally consumed and has
142+
// grown in size beyond its normal size, it shrinks down to its normal size again.
143+
func (r *Ring[T]) ReadOne() (data T, ok bool) {
144+
element, ok := r.growing.ReadOne()
145+
146+
if r.growing.readable == 0 && r.growing.n > r.normalSize {
147+
// The buffer is empty. Reallocate a new buffer so the old one can be
148+
// garbage collected.
149+
r.growing.data = make([]T, r.normalSize)
150+
r.growing.n = r.normalSize
151+
r.growing.beg = 0
152+
}
153+
154+
return element, ok
155+
}
156+
157+
// WriteOne adds an item to the end of the buffer, growing it if it is full.
158+
func (r *Ring[T]) WriteOne(data T) {
159+
r.growing.WriteOne(data)
160+
}
161+
162+
// Len returns the number of items in the buffer.
163+
func (r *Ring[T]) Len() int {
164+
return r.growing.Len()
165+
}
166+
167+
// Cap returns the capacity of the buffer.
168+
func (r *Ring[T]) Cap() int {
169+
return r.growing.Cap()
170+
}

buffer/ring_growing_test.go

Lines changed: 191 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,47 +17,211 @@ limitations under the License.
1717
package buffer
1818

1919
import (
20-
"reflect"
2120
"testing"
2221
)
2322

24-
func TestGrowth(t *testing.T) {
23+
func TestGrowthGrowing(t *testing.T) {
2524
t.Parallel()
26-
x := 10
27-
g := NewRingGrowing(1)
28-
for i := 0; i < x; i++ {
29-
if e, a := i, g.readable; !reflect.DeepEqual(e, a) {
30-
t.Fatalf("expected equal, got %#v, %#v", e, a)
31-
}
32-
g.WriteOne(i)
33-
}
34-
read := 0
35-
for g.readable > 0 {
36-
v, ok := g.ReadOne()
37-
if !ok {
38-
t.Fatal("expected true")
39-
}
40-
if read != v {
41-
t.Fatalf("expected %#v==%#v", read, v)
42-
}
43-
read++
25+
tests := map[string]struct {
26+
ring *TypedRingGrowing[int]
27+
initialSize int
28+
}{
29+
"implicit-zero": {
30+
ring: new(TypedRingGrowing[int]),
31+
},
32+
"explicit-zero": {
33+
ring: NewTypedRingGrowing[int](RingGrowingOptions{InitialSize: 0}),
34+
initialSize: 0,
35+
},
36+
"nonzero": {
37+
ring: NewTypedRingGrowing[int](RingGrowingOptions{InitialSize: 1}),
38+
initialSize: 1,
39+
},
4440
}
45-
if x != read {
46-
t.Fatalf("expected to have read %d items: %d", x, read)
41+
42+
for name, test := range tests {
43+
t.Run(name, func(t *testing.T) {
44+
initialSize := test.initialSize
45+
g := test.ring
46+
47+
if expected, actual := 0, g.Len(); expected != actual {
48+
t.Fatalf("expected Len to be %d, got %d", expected, actual)
49+
}
50+
if expected, actual := initialSize, g.Cap(); expected != actual {
51+
t.Fatalf("expected Cap to be %d, got %d", expected, actual)
52+
}
53+
54+
x := 10
55+
for i := 0; i < x; i++ {
56+
if e, a := i, g.readable; e != a {
57+
t.Fatalf("expected equal, got %#v, %#v", e, a)
58+
}
59+
g.WriteOne(i)
60+
}
61+
62+
if expected, actual := x, g.Len(); expected != actual {
63+
t.Fatalf("expected Len to be %d, got %d", expected, actual)
64+
}
65+
if expected, actual := 16, g.Cap(); expected != actual {
66+
t.Fatalf("expected Cap to be %d, got %d", expected, actual)
67+
}
68+
69+
read := 0
70+
for g.readable > 0 {
71+
v, ok := g.ReadOne()
72+
if !ok {
73+
t.Fatal("expected true")
74+
}
75+
if read != v {
76+
t.Fatalf("expected %#v==%#v", read, v)
77+
}
78+
read++
79+
}
80+
if x != read {
81+
t.Fatalf("expected to have read %d items: %d", x, read)
82+
}
83+
if expected, actual := 0, g.Len(); expected != actual {
84+
t.Fatalf("expected Len to be %d, got %d", expected, actual)
85+
}
86+
if expected, actual := 16, g.Cap(); expected != actual {
87+
t.Fatalf("expected Cap to be %d, got %d", expected, actual)
88+
}
89+
})
4790
}
48-
if g.readable != 0 {
49-
t.Fatalf("expected readable to be zero: %d", g.readable)
91+
92+
}
93+
94+
func TestGrowth(t *testing.T) {
95+
t.Parallel()
96+
97+
tests := map[string]struct {
98+
ring *Ring[int]
99+
initialSize int
100+
normalSize int
101+
}{
102+
"implicit-zero": {
103+
ring: new(Ring[int]),
104+
},
105+
"explicit-zero": {
106+
ring: NewRing[int](RingOptions{InitialSize: 0, NormalSize: 0}),
107+
initialSize: 0,
108+
normalSize: 0,
109+
},
110+
"smaller-initial-size": {
111+
ring: NewRing[int](RingOptions{InitialSize: 1, NormalSize: 2}),
112+
initialSize: 1,
113+
normalSize: 2,
114+
},
115+
"smaller-normal-size": {
116+
ring: NewRing[int](RingOptions{InitialSize: 2, NormalSize: 1}),
117+
initialSize: 2,
118+
normalSize: 1,
119+
},
50120
}
51-
if 16 != g.n {
52-
t.Fatalf("expected N to be 16: %d", g.n)
121+
122+
for name, test := range tests {
123+
t.Run(name, func(t *testing.T) {
124+
initialSize := test.initialSize
125+
normalSize := test.normalSize
126+
g := test.ring
127+
128+
if expected, actual := 0, g.Len(); expected != actual {
129+
t.Fatalf("expected Len to be %d, got %d", expected, actual)
130+
}
131+
if expected, actual := initialSize, g.Cap(); expected != actual {
132+
t.Fatalf("expected Cap to be %d, got %d", expected, actual)
133+
}
134+
135+
x := 10
136+
for i := 0; i < x; i++ {
137+
if e, a := i, g.growing.readable; e != a {
138+
t.Fatalf("expected equal, got %#v, %#v", e, a)
139+
}
140+
g.WriteOne(i)
141+
}
142+
143+
if expected, actual := x, g.Len(); expected != actual {
144+
t.Fatalf("expected Len to be %d, got %d", expected, actual)
145+
}
146+
if expected, actual := 16, g.Cap(); expected != actual {
147+
t.Fatalf("expected Cap to be %d, got %d", expected, actual)
148+
}
149+
150+
read := 0
151+
for g.growing.readable > 0 {
152+
v, ok := g.ReadOne()
153+
if !ok {
154+
t.Fatal("expected true")
155+
}
156+
if read != v {
157+
t.Fatalf("expected %#v==%#v", read, v)
158+
}
159+
read++
160+
}
161+
if x != read {
162+
t.Fatalf("expected to have read %d items: %d", x, read)
163+
}
164+
if expected, actual := 0, g.Len(); expected != actual {
165+
t.Fatalf("expected Len to be %d, got %d", expected, actual)
166+
}
167+
if expected, actual := normalSize, g.Cap(); expected != actual {
168+
t.Fatalf("expected Cap to be %d, got %d", expected, actual)
169+
}
170+
})
53171
}
54172
}
55173

56174
func TestEmpty(t *testing.T) {
57175
t.Parallel()
58-
g := NewRingGrowing(1)
176+
g := NewTypedRingGrowing[struct{}](RingGrowingOptions{InitialSize: 1})
59177
_, ok := g.ReadOne()
60178
if ok != false {
61179
t.Fatal("expected false")
62180
}
63181
}
182+
183+
const (
184+
spikeSize = 100 // Number of items to write during a spike
185+
normalSize = 64 // Normal capacity for the Ring type after shrinking
186+
initialSize = 16 // Initial capacity for buffers
187+
)
188+
189+
func BenchmarkTypedRingGrowing_Spike(b *testing.B) {
190+
b.ReportAllocs()
191+
var item int // ensure item is used
192+
193+
for i := 0; i < b.N; i++ {
194+
buffer := NewTypedRingGrowing[int](RingGrowingOptions{InitialSize: initialSize})
195+
196+
for j := 0; j < spikeSize; j++ {
197+
buffer.WriteOne(j)
198+
}
199+
200+
for buffer.Len() > 0 {
201+
item, _ = buffer.ReadOne()
202+
}
203+
}
204+
_ = item // use item
205+
}
206+
207+
func BenchmarkRing_Spike_And_Shrink(b *testing.B) {
208+
b.ReportAllocs()
209+
var item int // ensure item is used
210+
211+
for i := 0; i < b.N; i++ {
212+
// Create a new buffer for each benchmark iteration
213+
buffer := NewRing[int](RingOptions{
214+
InitialSize: initialSize,
215+
NormalSize: normalSize,
216+
})
217+
218+
for j := 0; j < spikeSize; j++ {
219+
buffer.WriteOne(j)
220+
}
221+
222+
for buffer.Len() > 0 {
223+
item, _ = buffer.ReadOne()
224+
}
225+
}
226+
_ = item // use item
227+
}

0 commit comments

Comments
 (0)