mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 23:15:14 +00:00
Avoid intermediate List allocations as items added to the list
Pick a reasonable middle ground between allocating larger chunks of memory (2048 * ~500b for pod slices) and having many small allocations as the list is resized by preallocating capacity based on the expected list size. At worst, we'll allocate a 1M slice for pods and only add a single pod to it (if the selector is very specific).
This commit is contained in:
parent
6a76931e2c
commit
ce0dc76901
@ -562,6 +562,14 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor
|
||||
return fmt.Errorf("no results were found, but etcd indicated there were more values remaining")
|
||||
}
|
||||
|
||||
// avoid small allocations for the result slice, since this can be called in many
|
||||
// different contexts and we don't know how significantly the result will be filtered
|
||||
if pred.Empty() {
|
||||
growSlice(v, len(getResp.Kvs))
|
||||
} else {
|
||||
growSlice(v, 2048, len(getResp.Kvs))
|
||||
}
|
||||
|
||||
// take items from the response until the bucket is full, filtering as we go
|
||||
for _, kv := range getResp.Kvs {
|
||||
if paging && int64(v.Len()) >= pred.Limit {
|
||||
@ -612,6 +620,37 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor
|
||||
return s.versioner.UpdateList(listObj, uint64(returnedRV), "")
|
||||
}
|
||||
|
||||
// growSlice takes a slice value and grows its capacity up
|
||||
// to the maximum of the passed sizes or maxCapacity, whichever
|
||||
// is smaller. Above maxCapacity decisions about allocation are left
|
||||
// to the Go runtime on append. This allows a caller to make an
|
||||
// educated guess about the potential size of the total list while
|
||||
// still avoiding overly aggressive initial allocation. If sizes
|
||||
// is empty maxCapacity will be used as the size to grow.
|
||||
func growSlice(v reflect.Value, maxCapacity int, sizes ...int) {
|
||||
cap := v.Cap()
|
||||
max := cap
|
||||
for _, size := range sizes {
|
||||
if size > max {
|
||||
max = size
|
||||
}
|
||||
}
|
||||
if len(sizes) == 0 || max > maxCapacity {
|
||||
max = maxCapacity
|
||||
}
|
||||
if max <= cap {
|
||||
return
|
||||
}
|
||||
if v.Len() > 0 {
|
||||
extra := reflect.MakeSlice(v.Type(), 0, max)
|
||||
reflect.Copy(extra, v)
|
||||
v.Set(extra)
|
||||
} else {
|
||||
extra := reflect.MakeSlice(v.Type(), 0, max)
|
||||
v.Set(extra)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch implements storage.Interface.Watch.
|
||||
func (s *store) Watch(ctx context.Context, key string, resourceVersion string, pred storage.SelectionPredicate) (watch.Interface, error) {
|
||||
return s.watch(ctx, key, resourceVersion, pred, false)
|
||||
|
@ -31,7 +31,6 @@ import (
|
||||
"github.com/coreos/etcd/integration"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
apitesting "k8s.io/apimachinery/pkg/api/testing"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@ -1236,3 +1235,68 @@ func Test_decodeContinue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_growSlice(t *testing.T) {
|
||||
type args struct {
|
||||
t reflect.Type
|
||||
initialCapacity int
|
||||
v reflect.Value
|
||||
maxCapacity int
|
||||
sizes []int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
cap int
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{v: reflect.ValueOf([]example.Pod{})},
|
||||
cap: 0,
|
||||
},
|
||||
{
|
||||
name: "no sizes",
|
||||
args: args{v: reflect.ValueOf([]example.Pod{}), maxCapacity: 10},
|
||||
cap: 10,
|
||||
},
|
||||
{
|
||||
name: "above maxCapacity",
|
||||
args: args{v: reflect.ValueOf([]example.Pod{}), maxCapacity: 10, sizes: []int{1, 12}},
|
||||
cap: 10,
|
||||
},
|
||||
{
|
||||
name: "takes max",
|
||||
args: args{v: reflect.ValueOf([]example.Pod{}), maxCapacity: 10, sizes: []int{8, 4}},
|
||||
cap: 8,
|
||||
},
|
||||
{
|
||||
name: "with existing capacity above max",
|
||||
args: args{initialCapacity: 12, maxCapacity: 10, sizes: []int{8, 4}},
|
||||
cap: 12,
|
||||
},
|
||||
{
|
||||
name: "with existing capacity below max",
|
||||
args: args{initialCapacity: 5, maxCapacity: 10, sizes: []int{8, 4}},
|
||||
cap: 8,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.initialCapacity > 0 {
|
||||
tt.args.v = reflect.ValueOf(make([]example.Pod, 0, tt.args.initialCapacity))
|
||||
}
|
||||
// reflection requires that the value be addressible in order to call set,
|
||||
// so we must ensure the value we created is available on the heap (not a problem
|
||||
// for normal usage)
|
||||
if !tt.args.v.CanAddr() {
|
||||
x := reflect.New(tt.args.v.Type())
|
||||
x.Elem().Set(tt.args.v)
|
||||
tt.args.v = x.Elem()
|
||||
}
|
||||
growSlice(tt.args.v, tt.args.maxCapacity, tt.args.sizes...)
|
||||
if tt.cap != tt.args.v.Cap() {
|
||||
t.Errorf("Unexpected capacity: got=%d want=%d", tt.args.v.Cap(), tt.cap)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user