diff --git a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go index df9917ed61c..b0acc840165 100644 --- a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go +++ b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go @@ -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) diff --git a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store_test.go b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store_test.go index 8b36b611096..d7c7f0f10d3 100644 --- a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store_test.go +++ b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store_test.go @@ -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) + } + }) + } +}