-
Notifications
You must be signed in to change notification settings - Fork 202
Add generic buffer.TypedRingGrowing and shrinkable buffer.Ring #323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
34865d4
d7b43d9
0315323
c9c88e1
133f6ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I was wondering whether two different options are needed, but I can imaging that |
||||||
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() | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
queue.FIFO
is set up to not require any constructors like this, so users can initialize one directly as the zero value and get a valid, empty queue. One detail I couldn't quite iron out to enable that here is how to make thenormalSize
configurable. There it's hardcoded to 4 which seems reasonable for where that's used, but I see informers' notification buffers usingRingGrowing
are initialized to 1024, so shrinking all the way down to 4 could result in significantly more allocations. So if there isn't a single golden value for every use case, I think that value probably has to be fed in through a constructor (or possibly a method).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that a constructor parameter would be good. Two int parameters with no clear obvious ordering will be hard to read at call sites, so I prefer the config struct pattern.
We could use optional
With*
methods, but that raises the question whether they return a deep copy of the instance or completely new ones - let's not go there.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the zero value
Ring[T]
: let's document it asNewRing()
with zero initial size and zero normal size. Perhaps not very useful, but at least it's then documented.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added options structs and mentioned zero values in doc strings.