Skip to content

Commit

Permalink
✨ Add a fast path for client.Object in merge-patch
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
cbandy committed Feb 24, 2021
1 parent 5fd8414 commit 45cc4a3
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 0 deletions.
34 changes: 34 additions & 0 deletions pkg/client/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,42 @@ func (s *mergeFromPatch) Type() types.PatchType {
return types.MergePatchType
}

func (*mergeFromPatch) data(original, modified Object, opts MergeFromOptions) ([]byte, error) {
if opts.OptimisticLock {
version := original.GetResourceVersion()
if len(version) == 0 {
return nil, fmt.Errorf("cannot use OptimisticLock, object %q does not have any resource version we can use", original)
}

original = original.DeepCopyObject().(Object)
original.SetResourceVersion("")

modified = modified.DeepCopyObject().(Object)
modified.SetResourceVersion(version)
}

originalJSON, err := json.Marshal(original)
if err != nil {
return nil, err
}

modifiedJSON, err := json.Marshal(modified)
if err != nil {
return nil, err
}

return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
}

// Data implements Patch.
func (s *mergeFromPatch) Data(obj runtime.Object) ([]byte, error) {
fromObject, fromOk := s.from.(Object)
objObject, objOk := obj.(Object)

if fromOk && objOk {
return s.data(fromObject, objObject, s.opts)
}

originalJSON, err := json.Marshal(s.from)
if err != nil {
return nil, err
Expand Down
95 changes: 95 additions & 0 deletions pkg/client/patch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
Copyright 2021 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 client

import (
"testing"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

func BenchmarkMergeFrom(b *testing.B) {
cm1 := &corev1.ConfigMap{}
cm1.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap"))
cm1.ResourceVersion = "anything"

cm2 := cm1.DeepCopy()
cm2.Data = map[string]string{"key": "value"}

sts1 := &appsv1.StatefulSet{}
sts1.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("StatefulSet"))
sts1.ResourceVersion = "somesuch"

sts2 := sts1.DeepCopy()
sts2.Spec.Template.Spec.Containers = []corev1.Container{{
Resources: corev1.ResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceCPU: resource.MustParse("1m"),
corev1.ResourceMemory: resource.MustParse("1M"),
},
},
ReadinessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{},
},
},
Lifecycle: &corev1.Lifecycle{
PreStop: &corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{},
},
},
SecurityContext: &corev1.SecurityContext{},
}}

b.Run("NoOptions", func(b *testing.B) {
cmPatch := MergeFrom(cm1)
if _, err := cmPatch.Data(cm2); err != nil {
b.Fatalf("expected no error, got %v", err)
}

stsPatch := MergeFrom(sts1)
if _, err := stsPatch.Data(sts2); err != nil {
b.Fatalf("expected no error, got %v", err)
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = cmPatch.Data(cm2)
_, _ = stsPatch.Data(sts2)
}
})

b.Run("WithOptimisticLock", func(b *testing.B) {
cmPatch := MergeFromWithOptions(cm1, MergeFromWithOptimisticLock{})
if _, err := cmPatch.Data(cm2); err != nil {
b.Fatalf("expected no error, got %v", err)
}

stsPatch := MergeFromWithOptions(sts1, MergeFromWithOptimisticLock{})
if _, err := stsPatch.Data(sts2); err != nil {
b.Fatalf("expected no error, got %v", err)
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = cmPatch.Data(cm2)
_, _ = stsPatch.Data(sts2)
}
})
}

0 comments on commit 45cc4a3

Please sign in to comment.