Skip to content

Commit 4ae2545

Browse files
committed
perf: Add full program benchmark for kustomize build
This change introduces a benchmarking test that constructs a complete kustomization tree using various features of Kustomize. This update aims to address several objectives: * Demonstrating current performance challenges in Kustomize in a reproducible manner. * Evaluating the effects of performance enhancements. * Guarding against potential performance setbacks and inadvertent quadratic behavior in the future. * Considering the possibility of incorporating profile-guided optimization (PGO) in future iterations. Usage: go test -run=x -bench=BenchmarkBuild ./kustomize/commands/build # sigs.k8s.io/kustomize/kustomize/v5/commands/build.test pkg: sigs.k8s.io/kustomize/kustomize/v5/commands/build BenchmarkBuild-8 1 8523677542 ns/op PASS ok sigs.k8s.io/kustomize/kustomize/v5/commands/build 8.798s *Currently*, this benchmark requires 3000 seconds to run on my machine. In order to run it on master today, you need to add `-timeout=30m` to the `go test` command. The dataset size was chosen because I believe it represents a real workload which we could get a runtime of less than 10 seconds. Updates #5084 Notes on PGO: Real-life profiles would be better, but creating one based on a benchmark should not hurt: https://go.dev/doc/pgo#collecting-profiles > Will PGO with an unrepresentative profile make my program slower than no PGO? > It should not. While a profile that is not representative of production behavior will result in optimizations in cold parts of the application, it should not make hot parts of the application slower. If you encounter a program where PGO results in worse performance than disabling PGO, please file an issue at https://go.dev/issue/new. Collecting a profile: go test -cpuprofile cpu1.pprof -run=^$ -bench ^BenchmarkBuild$ sigs.k8s.io/kustomize/kustomize/v5/commands/build go build -pgo=./cpu1.pprof -o kust-pgo ./kustomize go build -o kust-nopgo ./kustomize Compare PGO and non-PGO-builds: ./kust-pgo build -o /dev/null testdata/ 21.88s user 2.00s system 176% cpu 13.505 total ./kust-nopgo build -o /dev/null testdata/ 22.76s user 1.98s system 174% cpu 14.170 total
1 parent e002b49 commit 4ae2545

File tree

1 file changed

+196
-0
lines changed

1 file changed

+196
-0
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Copyright 2023 The Kubernetes Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package build_test
5+
6+
import (
7+
"bytes"
8+
"fmt"
9+
"path/filepath"
10+
"testing"
11+
12+
. "sigs.k8s.io/kustomize/kustomize/v5/commands/build"
13+
"sigs.k8s.io/kustomize/kyaml/filesys"
14+
)
15+
16+
// GenConfig configures the generation of a kustomization tree for benchmarking purposes
17+
type GenConfig struct {
18+
// The number of plain file resources to generate
19+
fileResources int
20+
21+
// The number of subdirectories to generate
22+
resources int
23+
24+
// The number of patches to generate
25+
patches int
26+
27+
// Whether to generate a namespace field
28+
namespaced bool
29+
30+
// The name prefix to use (if any)
31+
namePrefix string
32+
33+
// The name suffix to use (if any)
34+
nameSuffix string
35+
36+
// Common labels to use (if any)
37+
commonLabels map[string]string
38+
39+
// Common annotations to use (if any)
40+
commonAnnotations map[string]string
41+
}
42+
43+
func makeKustomization(configs []GenConfig, fSys filesys.FileSystem, path, id string, depth int) error {
44+
cfg := configs[depth]
45+
if err := fSys.MkdirAll(path); err != nil {
46+
return fmt.Errorf("failed to make directory %v: %w", path, err)
47+
}
48+
49+
var buf bytes.Buffer
50+
if cfg.namespaced {
51+
fmt.Fprintf(&buf, "namespace: %s\n", id)
52+
}
53+
54+
if cfg.namePrefix != "" {
55+
fmt.Fprintf(&buf, "namePrefix: %s\n", cfg.namePrefix)
56+
}
57+
58+
if cfg.nameSuffix != "" {
59+
fmt.Fprintf(&buf, "nameSuffix: %s\n", cfg.nameSuffix)
60+
}
61+
62+
if len(cfg.commonLabels) > 0 {
63+
fmt.Fprintf(&buf, "commonLabels:\n")
64+
for k, v := range cfg.commonLabels {
65+
fmt.Fprintf(&buf, " %s: %s\n", k, v)
66+
}
67+
}
68+
69+
if len(cfg.commonAnnotations) > 0 {
70+
fmt.Fprintf(&buf, "commonAnnotations:\n")
71+
for k, v := range cfg.commonAnnotations {
72+
fmt.Fprintf(&buf, " %s: %s\n", k, v)
73+
}
74+
}
75+
76+
if cfg.fileResources > 0 || cfg.resources > 0 {
77+
fmt.Fprintf(&buf, "resources:\n")
78+
for res := 0; res < cfg.fileResources; res++ {
79+
fn := fmt.Sprintf("res%d.yaml", res)
80+
fmt.Fprintf(&buf, " - %v\n", fn)
81+
82+
cm := fmt.Sprintf(`kind: ConfigMap
83+
apiVersion: v1
84+
metadata:
85+
name: %s-%d
86+
labels:
87+
foo: bar
88+
annotations:
89+
baz: blatti
90+
data:
91+
k: v
92+
`, id, res)
93+
if err := fSys.WriteFile(filepath.Join(path, fn), []byte(cm)); err != nil {
94+
return fmt.Errorf("failed to write file resource: %w", err)
95+
}
96+
}
97+
98+
for res := 0; res < cfg.resources; res++ {
99+
fn := fmt.Sprintf("res%d", res)
100+
fmt.Fprintf(&buf, " - %v\n", fn)
101+
if err := makeKustomization(configs, fSys, path+"/"+fn, fmt.Sprintf("%s-%d", id, res), depth+1); err != nil {
102+
return fmt.Errorf("failed to make kustomization: %w", err)
103+
}
104+
}
105+
}
106+
107+
for res := 0; res < cfg.patches; res++ {
108+
if res == 0 {
109+
fmt.Fprintf(&buf, "patches:\n")
110+
}
111+
112+
// alternate between json and yaml patches to test both kinds
113+
if res%2 == 0 {
114+
fn := fmt.Sprintf("patch%d.yaml", res)
115+
fmt.Fprintf(&buf, " - path: %v\n", fn)
116+
cmPatch := fmt.Sprintf(`kind: ConfigMap
117+
apiVersion: v1
118+
metadata:
119+
name: %s-%d
120+
data:
121+
k: v2
122+
`, id, res)
123+
if err := fSys.WriteFile(filepath.Join(path, fn), []byte(cmPatch)); err != nil {
124+
return fmt.Errorf("failed to write patch: %w", err)
125+
}
126+
} else {
127+
fn := fmt.Sprintf("patch%d.json", res)
128+
fmt.Fprintf(&buf, ` - path: %v
129+
target:
130+
version: v1
131+
kind: ConfigMap
132+
name: %s-%d
133+
`, fn, id, res-1)
134+
patch := `[{"op": "add", "path": "/data/k2", "value": "3"} ]`
135+
if err := fSys.WriteFile(filepath.Join(path, fn), []byte(patch)); err != nil {
136+
return fmt.Errorf("failed to write patch: %w", err)
137+
}
138+
}
139+
}
140+
141+
if err := fSys.WriteFile(filepath.Join(path, "kustomization.yaml"), buf.Bytes()); err != nil {
142+
return fmt.Errorf("failed to write kustomization.yaml: %w", err)
143+
}
144+
return nil
145+
}
146+
147+
func BenchmarkBuild(b *testing.B) {
148+
// This benchmark generates a kustomization tree with the following structure:
149+
genConfig := []GenConfig{
150+
{
151+
resources: 4, // four nested resources
152+
153+
// these operations should be very fast, so lets perform them on a *lot* of resources
154+
namePrefix: "foo-",
155+
nameSuffix: "-bar",
156+
commonLabels: map[string]string{
157+
"foo": "bar",
158+
},
159+
commonAnnotations: map[string]string{
160+
"baz": "blatti",
161+
},
162+
},
163+
{
164+
// test some more nesting (this could be `apps/` or `components/` directory with 100 apps or components)
165+
resources: 100,
166+
},
167+
{
168+
// this should be almost the same as using 300 above and skipping it, but it is currently not, so lets have some more nesting
169+
resources: 3,
170+
},
171+
{
172+
// here we have an actual component/app with lots or resources. Typically here we set namespace and have some patches
173+
resources: 1,
174+
namespaced: true,
175+
fileResources: 30,
176+
patches: 10,
177+
},
178+
{
179+
// we can also have a base/ or shared resources included into the namespace
180+
fileResources: 2,
181+
},
182+
}
183+
184+
fSys := filesys.MakeFsInMemory()
185+
if err := makeKustomization(genConfig, fSys, "testdata", "res", 0); err != nil {
186+
b.Fatal(err)
187+
}
188+
b.ResetTimer()
189+
for i := 0; i < b.N; i++ {
190+
buffy := new(bytes.Buffer)
191+
cmd := NewCmdBuild(fSys, MakeHelp("foo", "bar"), buffy)
192+
if err := cmd.RunE(cmd, []string{"./testdata"}); err != nil {
193+
b.Fatal(err)
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)