From 21dc3b444113ed1ba1cae81b322f0e1881cb0fd3 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Fri, 21 Mar 2025 06:09:23 +0000 Subject: [PATCH] benchmark to show inefficient linear search lookup goos: linux goarch: amd64 pkg: k8s.io/client-go/tools/cache cpu: Intel(R) Xeon(R) CPU @ 2.60GHz BenchmarkLister_Match_1k_100 BenchmarkLister_Match_1k_100-48 41910 28255 ns/op 16384 B/op 1 allocs/op BenchmarkLister_Match_10k_100 BenchmarkLister_Match_10k_100-48 3487 337728 ns/op 163848 B/op 1 allocs/op BenchmarkLister_Match_100k_100 BenchmarkLister_Match_100k_100-48 222 7040793 ns/op 1605659 B/op 1 allocs/op BenchmarkLister_Match_1M_100 BenchmarkLister_Match_1M_100-48 12 97962328 ns/op 16007172 B/op 1 allocs/op PASS ok k8s.io/client-go/tools/cache 10.480s Kubernetes-commit: 33fbce73ae203ffeb1b9ea63ac43d567d1bdb1ad --- tools/cache/listers_test.go | 140 ++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tools/cache/listers_test.go diff --git a/tools/cache/listers_test.go b/tools/cache/listers_test.go new file mode 100644 index 00000000..730807f6 --- /dev/null +++ b/tools/cache/listers_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +import ( + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +func TestListAll(t *testing.T) { + testCases := []struct { + name string + numObjects int + numMatching int + matchingLabels map[string]string + selector labels.Selector + expectedCount int + }{ + { + name: "subset match", + numObjects: 100000, + numMatching: 10000, + matchingLabels: map[string]string{"match": "true"}, + selector: labels.SelectorFromSet(map[string]string{"match": "true"}), + expectedCount: 10000, + }, + { + name: "all match", + numObjects: 1000000, + numMatching: 0, + matchingLabels: map[string]string{}, + selector: labels.Everything(), + expectedCount: 1000000, + }, + { + name: "no match", + numObjects: 1000000, + numMatching: 0, + matchingLabels: map[string]string{"nomatch": "true"}, + selector: labels.SelectorFromSet(map[string]string{"match": "true"}), + expectedCount: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + store := mustCreateStore(tc.numObjects, tc.numMatching, tc.matchingLabels) + + var matchingObjects int + appendFn := func(obj interface{}) { + matchingObjects++ + } + + err := ListAll(store, tc.selector, appendFn) + if err != nil { + t.Fatalf("ListAll returned an error: %v", err) + } + + if matchingObjects != tc.expectedCount { + t.Errorf("ListAll returned %d objects, expected %d", matchingObjects, tc.expectedCount) + } + }) + } +} + +func mustCreateStore(numObjects int, numMatching int, labels map[string]string) Store { + if numMatching > numObjects { + panic("there can not be more matches than objects") + } + store := NewStore(func(obj interface{}) (string, error) { + meta, err := meta.Accessor(obj) + if err != nil { + return "", err + } + return meta.GetName(), nil + }) + // add matching objects to the store + for i := 0; i < numObjects; i++ { + obj := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("obj-%d", i), + Labels: map[string]string{}, + }, + } + if i < numMatching { + obj.Labels = labels + } + err := store.Add(obj) + if err != nil { + panic("unexpected error") + } + } + return store +} + +func benchmarkLister(b *testing.B, numObjects int, numMatching int, label map[string]string) { + store := mustCreateStore(numObjects, numMatching, label) + selector := labels.SelectorFromSet(label) + b.ResetTimer() + for n := 0; n < b.N; n++ { + err := ListAll(store, selector, func(m interface{}) { + }) + if err != nil { + b.Fatalf("ListAll returned an error: %v", err) + } + } +} + +func BenchmarkLister_Match_1k_100(b *testing.B) { + benchmarkLister(b, 1000, 100, map[string]string{"match": "true"}) +} +func BenchmarkLister_Match_10k_100(b *testing.B) { + benchmarkLister(b, 10000, 100, map[string]string{"match": "true"}) +} + +func BenchmarkLister_Match_100k_100(b *testing.B) { + benchmarkLister(b, 100000, 100, map[string]string{"match": "true"}) +} + +func BenchmarkLister_Match_1M_100(b *testing.B) { + benchmarkLister(b, 1000000, 100, map[string]string{"match": "true"}) +}