diff --git a/buffer/ring_growing.go b/buffer/ring_growing.go index 77243575..f710935a 100644 --- a/buffer/ring_growing.go +++ b/buffer/ring_growing.go @@ -16,31 +16,54 @@ limitations under the License. package buffer +// RingGrowingOptions sets parameters for [RingGrowing] and +// [TypedRingGrowing]. +type RingGrowingOptions struct { + // InitialSize is the number of pre-allocated elements in the + // initial underlying storage buffer. + InitialSize int +} + // RingGrowing is a growing ring buffer. // Not thread safe. -type RingGrowing struct { - data []interface{} +// +// Deprecated: Use TypedRingGrowing[any] instead. +type RingGrowing = TypedRingGrowing[any] + +// NewRingGrowing constructs a new RingGrowing instance with provided parameters. +// +// Deprecated: Use NewTypedRingGrowing[any] instead. +func NewRingGrowing(initialSize int) *RingGrowing { + return NewTypedRingGrowing[any](RingGrowingOptions{InitialSize: initialSize}) +} + +// TypedRingGrowing is a growing ring buffer. +// The zero value has an initial size of 0 and is ready to use. +// Not thread safe. +type TypedRingGrowing[T any] struct { + data []T n int // Size of Data beg int // First available element readable int // Number of data items available } -// NewRingGrowing constructs a new RingGrowing instance with provided parameters. -func NewRingGrowing(initialSize int) *RingGrowing { - return &RingGrowing{ - data: make([]interface{}, initialSize), - n: initialSize, +// NewTypedRingGrowing constructs a new TypedRingGrowing instance with provided parameters. +func NewTypedRingGrowing[T any](opts RingGrowingOptions) *TypedRingGrowing[T] { + return &TypedRingGrowing[T]{ + data: make([]T, opts.InitialSize), + n: opts.InitialSize, } } // ReadOne reads (consumes) first item from the buffer if it is available, otherwise returns false. -func (r *RingGrowing) ReadOne() (data interface{}, ok bool) { +func (r *TypedRingGrowing[T]) ReadOne() (data T, ok bool) { if r.readable == 0 { - return nil, false + return } r.readable-- element := r.data[r.beg] - r.data[r.beg] = nil // Remove reference to the object to help GC + var zero T + r.data[r.beg] = zero // Remove reference to the object to help GC if r.beg == r.n-1 { // Was the last element r.beg = 0 @@ -51,11 +74,14 @@ func (r *RingGrowing) ReadOne() (data interface{}, ok bool) { } // WriteOne adds an item to the end of the buffer, growing it if it is full. -func (r *RingGrowing) WriteOne(data interface{}) { +func (r *TypedRingGrowing[T]) WriteOne(data T) { if r.readable == r.n { // Time to grow newN := r.n * 2 - newData := make([]interface{}, newN) + if newN == 0 { + newN = 1 + } + newData := make([]T, newN) to := r.beg + r.readable if to <= r.n { copy(newData, r.data[r.beg:to]) @@ -72,11 +98,69 @@ func (r *RingGrowing) WriteOne(data interface{}) { } // Len returns the number of items in the buffer. -func (r *RingGrowing) Len() int { +func (r *TypedRingGrowing[T]) Len() int { return r.readable } // Cap returns the capacity of the buffer. -func (r *RingGrowing) Cap() int { +func (r *TypedRingGrowing[T]) Cap() int { return r.n } + +// RingGrowingOptions sets parameters for [Ring]. +type RingOptions struct { + // InitialSize is the number of pre-allocated elements in the + // initial underlying storage buffer. + InitialSize int + // NormalSize is the number of elements to allocate for new storage + // buffers once the Ring is consumed. + NormalSize int +} + +// Ring is a dynamically-sized ring buffer which can grow and shrink as-needed. +// The zero value has an initial size and normal size of 0 and is ready to use. +// Not thread safe. +type Ring[T any] struct { + growing TypedRingGrowing[T] + normalSize int // Limits the size of the buffer that is kept for reuse. Read-only. +} + +// NewRing constructs a new Ring instance with provided parameters. +func NewRing[T any](opts RingOptions) *Ring[T] { + return &Ring[T]{ + growing: *NewTypedRingGrowing[T](RingGrowingOptions{InitialSize: opts.InitialSize}), + normalSize: opts.NormalSize, + } +} + +// ReadOne reads (consumes) first item from the buffer if it is available, +// otherwise returns false. When the buffer has been totally consumed and has +// grown in size, it shrinks down to its initial size. +func (r *Ring[T]) ReadOne() (data T, ok bool) { + element, ok := r.growing.ReadOne() + + if r.growing.readable == 0 && r.growing.n > r.normalSize { + // The buffer is empty. Reallocate a new buffer so the old one can be + // garbage collected. + r.growing.data = make([]T, r.normalSize) + r.growing.n = r.normalSize + r.growing.beg = 0 + } + + return element, ok +} + +// WriteOne adds an item to the end of the buffer, growing it if it is full. +func (r *Ring[T]) WriteOne(data T) { + r.growing.WriteOne(data) +} + +// Len returns the number of items in the buffer. +func (r *Ring[T]) Len() int { + return r.growing.Len() +} + +// Cap returns the capacity of the buffer. +func (r *Ring[T]) Cap() int { + return r.growing.Cap() +} diff --git a/buffer/ring_growing_test.go b/buffer/ring_growing_test.go index 6b0c3833..45fbd7e7 100644 --- a/buffer/ring_growing_test.go +++ b/buffer/ring_growing_test.go @@ -17,45 +17,163 @@ limitations under the License. package buffer import ( - "reflect" "testing" ) -func TestGrowth(t *testing.T) { +func TestGrowthGrowing(t *testing.T) { t.Parallel() - x := 10 - g := NewRingGrowing(1) - for i := 0; i < x; i++ { - if e, a := i, g.readable; !reflect.DeepEqual(e, a) { - t.Fatalf("expected equal, got %#v, %#v", e, a) - } - g.WriteOne(i) - } - read := 0 - for g.readable > 0 { - v, ok := g.ReadOne() - if !ok { - t.Fatal("expected true") - } - if read != v { - t.Fatalf("expected %#v==%#v", read, v) - } - read++ + tests := map[string]struct { + ring *TypedRingGrowing[int] + initialSize int + }{ + "implicit-zero": { + ring: new(TypedRingGrowing[int]), + }, + "explicit-zero": { + ring: NewTypedRingGrowing[int](RingGrowingOptions{InitialSize: 0}), + initialSize: 0, + }, + "nonzero": { + ring: NewTypedRingGrowing[int](RingGrowingOptions{InitialSize: 1}), + initialSize: 1, + }, } - if x != read { - t.Fatalf("expected to have read %d items: %d", x, read) + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + initialSize := test.initialSize + g := test.ring + + if expected, actual := 0, g.Len(); expected != actual { + t.Fatalf("expected Len to be %d, got %d", expected, actual) + } + if expected, actual := initialSize, g.Cap(); expected != actual { + t.Fatalf("expected Cap to be %d, got %d", expected, actual) + } + + x := 10 + for i := 0; i < x; i++ { + if e, a := i, g.readable; e != a { + t.Fatalf("expected equal, got %#v, %#v", e, a) + } + g.WriteOne(i) + } + + if expected, actual := x, g.Len(); expected != actual { + t.Fatalf("expected Len to be %d, got %d", expected, actual) + } + if expected, actual := 16, g.Cap(); expected != actual { + t.Fatalf("expected Cap to be %d, got %d", expected, actual) + } + + read := 0 + for g.readable > 0 { + v, ok := g.ReadOne() + if !ok { + t.Fatal("expected true") + } + if read != v { + t.Fatalf("expected %#v==%#v", read, v) + } + read++ + } + if x != read { + t.Fatalf("expected to have read %d items: %d", x, read) + } + if expected, actual := 0, g.Len(); expected != actual { + t.Fatalf("expected Len to be %d, got %d", expected, actual) + } + if expected, actual := 16, g.Cap(); expected != actual { + t.Fatalf("expected Cap to be %d, got %d", expected, actual) + } + }) } - if g.readable != 0 { - t.Fatalf("expected readable to be zero: %d", g.readable) + +} + +func TestGrowth(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + ring *Ring[int] + initialSize int + normalSize int + }{ + "implicit-zero": { + ring: new(Ring[int]), + }, + "explicit-zero": { + ring: NewRing[int](RingOptions{InitialSize: 0, NormalSize: 0}), + initialSize: 0, + normalSize: 0, + }, + "smaller-initial-size": { + ring: NewRing[int](RingOptions{InitialSize: 1, NormalSize: 2}), + initialSize: 1, + normalSize: 2, + }, + "smaller-normal-size": { + ring: NewRing[int](RingOptions{InitialSize: 2, NormalSize: 1}), + initialSize: 2, + normalSize: 1, + }, } - if 16 != g.n { - t.Fatalf("expected N to be 16: %d", g.n) + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + initialSize := test.initialSize + normalSize := test.normalSize + g := test.ring + + if expected, actual := 0, g.Len(); expected != actual { + t.Fatalf("expected Len to be %d, got %d", expected, actual) + } + if expected, actual := initialSize, g.Cap(); expected != actual { + t.Fatalf("expected Cap to be %d, got %d", expected, actual) + } + + x := 10 + for i := 0; i < x; i++ { + if e, a := i, g.growing.readable; e != a { + t.Fatalf("expected equal, got %#v, %#v", e, a) + } + g.WriteOne(i) + } + + if expected, actual := x, g.Len(); expected != actual { + t.Fatalf("expected Len to be %d, got %d", expected, actual) + } + if expected, actual := 16, g.Cap(); expected != actual { + t.Fatalf("expected Cap to be %d, got %d", expected, actual) + } + + read := 0 + for g.growing.readable > 0 { + v, ok := g.ReadOne() + if !ok { + t.Fatal("expected true") + } + if read != v { + t.Fatalf("expected %#v==%#v", read, v) + } + read++ + } + if x != read { + t.Fatalf("expected to have read %d items: %d", x, read) + } + if expected, actual := 0, g.Len(); expected != actual { + t.Fatalf("expected Len to be %d, got %d", expected, actual) + } + if expected, actual := normalSize, g.Cap(); expected != actual { + t.Fatalf("expected Cap to be %d, got %d", expected, actual) + } + }) } } func TestEmpty(t *testing.T) { t.Parallel() - g := NewRingGrowing(1) + g := NewTypedRingGrowing[struct{}](RingGrowingOptions{InitialSize: 1}) _, ok := g.ReadOne() if ok != false { t.Fatal("expected false")