Skip to content

Commit 45cc4a3

Browse files
committed
✨ Add a fast path for client.Object in merge-patch
Most uses of merge-patch will be for built-in types or kubebuilder- generated custom resources which have accessors for ResourceVersion. Using these with DeepCopyObject() is simple and fast. name old time/op new time/op delta MergeFrom/NoOptions 91.6µs ± 4% 107.3µs ±26% ~ (p=0.417 n=7+10) MergeFrom/NoOptions-2 112µs ±18% 88µs ±11% -21.15% (p=0.000 n=10+9) MergeFrom/WithOptimisticLock 163µs ± 3% 121µs ± 3% -25.66% (p=0.000 n=10+8) MergeFrom/WithOptimisticLock-2 137µs ± 4% 101µs ± 4% -26.28% (p=0.000 n=10+10) name old alloc/op new alloc/op delta MergeFrom/NoOptions 20.3kB ± 0% 20.3kB ± 0% ~ (all equal) MergeFrom/NoOptions-2 20.3kB ± 0% 20.3kB ± 0% ~ (all equal) MergeFrom/WithOptimisticLock 34.1kB ± 0% 26.7kB ± 0% -21.89% (p=0.000 n=10+10) MergeFrom/WithOptimisticLock-2 34.2kB ± 0% 26.7kB ± 0% -21.89% (p=0.000 n=10+8) name old allocs/op new allocs/op delta MergeFrom/NoOptions 359 ± 0% 359 ± 0% ~ (all equal) MergeFrom/NoOptions-2 359 ± 0% 359 ± 0% ~ (all equal) MergeFrom/WithOptimisticLock 579 ± 0% 390 ± 0% -32.64% (p=0.000 n=10+10) MergeFrom/WithOptimisticLock-2 579 ± 0% 390 ± 0% -32.64% (p=0.000 n=10+10)
1 parent 5fd8414 commit 45cc4a3

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

pkg/client/patch.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,42 @@ func (s *mergeFromPatch) Type() types.PatchType {
9696
return types.MergePatchType
9797
}
9898

99+
func (*mergeFromPatch) data(original, modified Object, opts MergeFromOptions) ([]byte, error) {
100+
if opts.OptimisticLock {
101+
version := original.GetResourceVersion()
102+
if len(version) == 0 {
103+
return nil, fmt.Errorf("cannot use OptimisticLock, object %q does not have any resource version we can use", original)
104+
}
105+
106+
original = original.DeepCopyObject().(Object)
107+
original.SetResourceVersion("")
108+
109+
modified = modified.DeepCopyObject().(Object)
110+
modified.SetResourceVersion(version)
111+
}
112+
113+
originalJSON, err := json.Marshal(original)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
modifiedJSON, err := json.Marshal(modified)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
124+
}
125+
99126
// Data implements Patch.
100127
func (s *mergeFromPatch) Data(obj runtime.Object) ([]byte, error) {
128+
fromObject, fromOk := s.from.(Object)
129+
objObject, objOk := obj.(Object)
130+
131+
if fromOk && objOk {
132+
return s.data(fromObject, objObject, s.opts)
133+
}
134+
101135
originalJSON, err := json.Marshal(s.from)
102136
if err != nil {
103137
return nil, err

pkg/client/patch_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package client
18+
19+
import (
20+
"testing"
21+
22+
appsv1 "k8s.io/api/apps/v1"
23+
corev1 "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/api/resource"
25+
)
26+
27+
func BenchmarkMergeFrom(b *testing.B) {
28+
cm1 := &corev1.ConfigMap{}
29+
cm1.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap"))
30+
cm1.ResourceVersion = "anything"
31+
32+
cm2 := cm1.DeepCopy()
33+
cm2.Data = map[string]string{"key": "value"}
34+
35+
sts1 := &appsv1.StatefulSet{}
36+
sts1.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("StatefulSet"))
37+
sts1.ResourceVersion = "somesuch"
38+
39+
sts2 := sts1.DeepCopy()
40+
sts2.Spec.Template.Spec.Containers = []corev1.Container{{
41+
Resources: corev1.ResourceRequirements{
42+
Requests: map[corev1.ResourceName]resource.Quantity{
43+
corev1.ResourceCPU: resource.MustParse("1m"),
44+
corev1.ResourceMemory: resource.MustParse("1M"),
45+
},
46+
},
47+
ReadinessProbe: &corev1.Probe{
48+
Handler: corev1.Handler{
49+
HTTPGet: &corev1.HTTPGetAction{},
50+
},
51+
},
52+
Lifecycle: &corev1.Lifecycle{
53+
PreStop: &corev1.Handler{
54+
HTTPGet: &corev1.HTTPGetAction{},
55+
},
56+
},
57+
SecurityContext: &corev1.SecurityContext{},
58+
}}
59+
60+
b.Run("NoOptions", func(b *testing.B) {
61+
cmPatch := MergeFrom(cm1)
62+
if _, err := cmPatch.Data(cm2); err != nil {
63+
b.Fatalf("expected no error, got %v", err)
64+
}
65+
66+
stsPatch := MergeFrom(sts1)
67+
if _, err := stsPatch.Data(sts2); err != nil {
68+
b.Fatalf("expected no error, got %v", err)
69+
}
70+
71+
b.ResetTimer()
72+
for i := 0; i < b.N; i++ {
73+
_, _ = cmPatch.Data(cm2)
74+
_, _ = stsPatch.Data(sts2)
75+
}
76+
})
77+
78+
b.Run("WithOptimisticLock", func(b *testing.B) {
79+
cmPatch := MergeFromWithOptions(cm1, MergeFromWithOptimisticLock{})
80+
if _, err := cmPatch.Data(cm2); err != nil {
81+
b.Fatalf("expected no error, got %v", err)
82+
}
83+
84+
stsPatch := MergeFromWithOptions(sts1, MergeFromWithOptimisticLock{})
85+
if _, err := stsPatch.Data(sts2); err != nil {
86+
b.Fatalf("expected no error, got %v", err)
87+
}
88+
89+
b.ResetTimer()
90+
for i := 0; i < b.N; i++ {
91+
_, _ = cmPatch.Data(cm2)
92+
_, _ = stsPatch.Data(sts2)
93+
}
94+
})
95+
}

0 commit comments

Comments
 (0)