diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_benchmark_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_benchmark_test.go new file mode 100644 index 00000000000..812c181627c --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_benchmark_test.go @@ -0,0 +1,118 @@ +/* +Copyright 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 v1_test + +import ( + "encoding/json" + "fmt" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var benchmarkPayloads = []string{ + // Small managed fields payload + `{"f:metadata":{"f:labels":{"f:app":{}},"f:annotations":{"f:revision":{}}},"f:spec":{"f:replicas":{},"f:template":{"f:metadata":{"f:labels":{"f:app":{}}},"f:spec":{"f:containers":{"k:{\"name\":\"nginx\"}":{".":{},"f:image":{},"f:name":{}}}}}}}`, + // Larger, deeply nested valid JSON payload (representing complex managedFields) + `{"f:metadata":{"f:annotations":{".":{},"f:kubectl.kubernetes.io/last-applied-configuration":{}},"f:labels":{".":{},"f:app.kubernetes.io/name":{}}},"f:spec":{"f:replicas":{},"f:selector":{},"f:template":{"f:metadata":{"f:labels":{".":{},"f:app.kubernetes.io/name":{}}},"f:spec":{"f:containers":{"k:{\"name\":\"app\"}":{".":{},"f:image":{},"f:name":{},"f:ports":{".":{},"k:{\"containerPort\":8080,\"protocol\":\"TCP\"}":{".":{},"f:containerPort":{},"f:protocol":{}}},"f:resources":{".":{},"f:limits":{".":{},"f:cpu":{},"f:memory":{}},"f:requests":{".":{},"f:cpu":{},"f:memory":{}}}}}}}}}`, +} + +// BenchmarkDecodeDuplicate measures the allocation and speed of unmarshaling +// exactly identical payloads repeatedly, which is the most common case for +// heavily replicated resources (DaemonSets, ReplicaSets) in the API server. +func BenchmarkDecodeDuplicate(b *testing.B) { + for _, payload := range benchmarkPayloads { + b.Run(fmt.Sprintf("Size%d", len(payload)), func(b *testing.B) { + rawJSON := []byte(payload) + b.ResetTimer() + b.ReportAllocs() + var retained []metav1.FieldsV1 + for j := 0; j < b.N; j++ { + var f metav1.FieldsV1 + if err := json.Unmarshal(rawJSON, &f); err != nil { + b.Fatal(err) + } + retained = append(retained, f) + } + _ = retained + }) + } +} + +// BenchmarkDecodeUnique measures the allocation and speed of unmarshaling +// completely unique payloads to measure the worst-case interning overhead. +func BenchmarkDecodeUnique(b *testing.B) { + for _, payload := range benchmarkPayloads { + b.Run(fmt.Sprintf("Size%d", len(payload)), func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + var retained []metav1.FieldsV1 + for j := 0; j < b.N; j++ { + // Inject the iteration counter to guarantee the string is unique + novelPayload := fmt.Appendf(nil, `{"f:iter":%d,%s`, j, payload[1:]) + var f metav1.FieldsV1 + if err := json.Unmarshal(novelPayload, &f); err != nil { + b.Fatal(err) + } + retained = append(retained, f) + } + _ = retained + }) + } +} + +// BenchmarkParallelDecode tests parallel deserialization of duplicate strings +// to verify lock contention behavior in highly concurrent environments. +func BenchmarkParallelDecode(b *testing.B) { + for _, payload := range benchmarkPayloads { + b.Run(fmt.Sprintf("Size%d", len(payload)), func(b *testing.B) { + rawJSON := []byte(payload) + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var f metav1.FieldsV1 + if err := json.Unmarshal(rawJSON, &f); err != nil { + b.Fatal(err) + } + } + }) + }) + } +} + +// BenchmarkEqual_Same measures the fast-path equality check for identical structs. +func BenchmarkEqual_Same(b *testing.B) { + f1 := metav1.NewFieldsV1(benchmarkPayloads[1]) + f2 := metav1.NewFieldsV1(benchmarkPayloads[1]) + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = f1.Equal(*f2) + } +} + +// BenchmarkEqual_Different measures the fallback-path equality check for differing structs. +func BenchmarkEqual_Different(b *testing.B) { + f1 := metav1.NewFieldsV1(benchmarkPayloads[1]) + f2 := metav1.NewFieldsV1(benchmarkPayloads[0]) + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = f1.Equal(*f2) + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_byte.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_byte.go index 8adb21e5c87..66f28e1d9ff 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_byte.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_byte.go @@ -1,3 +1,5 @@ +//go:build !fieldsv1string + /* Copyright The Kubernetes Authors. diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_string.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_string.go new file mode 100644 index 00000000000..679452ae17d --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_string.go @@ -0,0 +1,113 @@ +//go:build fieldsv1string + +/* +Copyright 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 v1 + +import ( + "strings" + "unique" +) + +// FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format. +// +// Each key is either a '.' representing the field itself, and will always map to an empty set, +// or a string representing a sub-field or item. The string will follow one of these four formats: +// 'f:', where is the name of a field in a struct, or key in a map +// 'v:', where is the exact json formatted value of a list item +// 'i:', where is position of a item in a list +// 'k:', where is a map of a list item's key fields to their unique values +// If a key maps to an empty Fields value, the field that key represents is part of the set. +// +// The exact format is defined in sigs.k8s.io/structured-merge-diff +// +k8s:deepcopy-gen=false +// +protobuf.options.marshal=false +// +protobuf.options.(gogoproto.goproto_stringer)=false +type FieldsV1 struct { + // The zero value of a unique.Handle[string] has an uninitialized underlying pointer. + // Calling .Value() on it panics. We must explicitly check for this uninitialized + // state (f.handle == unique.Handle[string]{}) across accessors to safely support + // uninitialized metav1.FieldsV1{} objects. + // See ongoing golang discussion related to this here: https://github.com/golang/go/issues/73344 + handle unique.Handle[string] +} + +func (f FieldsV1) String() string { + if f.handle == (unique.Handle[string]{}) { + return "" + } + return f.handle.Value() +} + +func (f FieldsV1) Equal(f2 FieldsV1) bool { + if f.handle == f2.handle { + return true + } + // An uninitialized FieldsV1 compared to an explicitly empty + // FieldsV1 (unique.Make("") will fail the handle check above. + // Evaluate string contents directly as well to maintain parity with legacy + // bytes.Equal(nil, []byte{}) == true behavior. + return f.GetRawString() == f2.GetRawString() +} + +func (f *FieldsV1) GetRawReader() FieldsV1Reader { + if f == nil || f.handle == (unique.Handle[string]{}) { + return strings.NewReader("") + } + return strings.NewReader(f.handle.Value()) +} + +// GetRawBytes returns the raw bytes. +// These may or may not be a copy of the underlying bytes. +// If mutating the underlying bytes is desired, the returned bytes may be mutated and then passed to SetRawBytes(). +// If mutating the underlying bytes is not desired, make a copy of the returned bytes. +func (f *FieldsV1) GetRawBytes() []byte { + if f == nil || f.handle == (unique.Handle[string]{}) { + return nil + } + return []byte(f.handle.Value()) +} + +// GetRawString returns the raw data as a string. +func (f *FieldsV1) GetRawString() string { + if f == nil || f.handle == (unique.Handle[string]{}) { + return "" + } + return f.handle.Value() +} + +// SetRawBytes sets the raw bytes. It does not retain the passed-in byte slice. +func (f *FieldsV1) SetRawBytes(b []byte) { + if f != nil { + f.handle = unique.Make(string(b)) + } +} + +// SetRawString sets the raw data from a string. +func (f *FieldsV1) SetRawString(s string) { + if f != nil { + f.handle = unique.Make(s) + } +} + +func NewFieldsV1(raw string) *FieldsV1 { + return &FieldsV1{handle: unique.Make(raw)} +} + +func (f *FieldsV1) DeepCopyInto(out *FieldsV1) { + *out = *f +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_string_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_string_test.go new file mode 100644 index 00000000000..384951673aa --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/fieldsv1_string_test.go @@ -0,0 +1,362 @@ +//go:build fieldsv1string + +/* +Copyright 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 v1_test + +import ( + "bytes" + "encoding/json" + "io" + "testing" + "unsafe" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// requireInterned fails the test if the two strings do not point to the exact same memory address. +func requireInterned(t *testing.T, a, b string) { + t.Helper() + ptrA := unsafe.StringData(a) + ptrB := unsafe.StringData(b) + if ptrA != ptrB { + t.Fatalf("Expected strings to be interned (same memory address) but pointers differ: %p != %p", ptrA, ptrB) + } +} + +func TestFieldsV1_String(t *testing.T) { + for _, tc := range []struct { + name string + f metav1.FieldsV1 + expected string + }{ + { + name: "zero value handle", + f: metav1.FieldsV1{}, + expected: "", + }, + { + name: "initialized empty handle", + f: *metav1.NewFieldsV1(""), + expected: "", + }, + { + name: "valid payload", + f: *metav1.NewFieldsV1(`{"f:app":{}}`), + expected: `{"f:app":{}}`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + if got := tc.f.String(); got != tc.expected { + t.Errorf("expected %q, got %q", tc.expected, got) + } + }) + } +} + +func TestFieldsV1_Equal(t *testing.T) { + valid := *metav1.NewFieldsV1(`{"f:app":{}}`) + validClone := *metav1.NewFieldsV1(`{"f:app":{}}`) + different := *metav1.NewFieldsV1(`{"f:other":{}}`) + + for _, tc := range []struct { + name string + a metav1.FieldsV1 + b metav1.FieldsV1 + expected bool + }{ + {"both zero value", metav1.FieldsV1{}, metav1.FieldsV1{}, true}, + {"zero value and initialized empty handle", metav1.FieldsV1{}, *metav1.NewFieldsV1(""), true}, + {"identical valid payloads", valid, validClone, true}, + {"different payloads", valid, different, false}, + {"zero value and valid payload", metav1.FieldsV1{}, valid, false}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := tc.a.Equal(tc.b); got != tc.expected { + t.Errorf("expected Equal() to be %v, got %v", tc.expected, got) + } + // Commutative property + if got := tc.b.Equal(tc.a); got != tc.expected { + t.Errorf("expected Equal() commutative to be %v, got %v", tc.expected, got) + } + }) + } +} + +func TestFieldsV1_GetRawReader(t *testing.T) { + for _, tc := range []struct { + name string + f *metav1.FieldsV1 + expected []byte + }{ + {"nil receiver", nil, []byte("")}, + {"zero value handle", &metav1.FieldsV1{}, []byte("")}, + {"initialized empty handle", metav1.NewFieldsV1(""), []byte("")}, + {"valid payload", metav1.NewFieldsV1(`{"f:app":{}}`), []byte(`{"f:app":{}}`)}, + } { + t.Run(tc.name, func(t *testing.T) { + reader := tc.f.GetRawReader() + got, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("unexpected error reading from RawReader: %v", err) + } + if !bytes.Equal(got, tc.expected) { + t.Errorf("expected %q, got %q", tc.expected, got) + } + }) + } +} + +func TestFieldsV1_GetRawBytes(t *testing.T) { + for _, tc := range []struct { + name string + f *metav1.FieldsV1 + expected []byte + }{ + {"nil receiver", nil, nil}, + {"zero value handle", &metav1.FieldsV1{}, nil}, + {"initialized empty handle", metav1.NewFieldsV1(""), []byte{}}, + {"valid payload", metav1.NewFieldsV1(`{"f:app":{}}`), []byte(`{"f:app":{}}`)}, + } { + t.Run(tc.name, func(t *testing.T) { + got := tc.f.GetRawBytes() + if !bytes.Equal(got, tc.expected) { + t.Errorf("expected %v, got %v", tc.expected, got) + } + // Explicitly check for nil + if tc.expected == nil && got != nil { + t.Errorf("expected strict nil, got %v", got) + } + }) + } + + t.Run("mutation safety", func(t *testing.T) { + f := metav1.NewFieldsV1(`{"f:app":{}}`) + b := f.GetRawBytes() + + // Mutate the returned bytes + b[2] = 'X' + + // The original interned string must remain unchanged + if got := f.GetRawString(); got != `{"f:app":{}}` { + t.Errorf("GetRawBytes returned an unisolated slice! Mutating it corrupted the handle. Got: %s", got) + } + }) +} + +func TestFieldsV1_GetRawString(t *testing.T) { + for _, tc := range []struct { + name string + f *metav1.FieldsV1 + expected string + }{ + {"nil receiver", nil, ""}, + {"zero value handle", &metav1.FieldsV1{}, ""}, + {"initialized empty handle", metav1.NewFieldsV1(""), ""}, + {"valid payload", metav1.NewFieldsV1(`{"f:app":{}}`), `{"f:app":{}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := tc.f.GetRawString(); got != tc.expected { + t.Errorf("expected %q, got %q", tc.expected, got) + } + }) + } +} + +func TestFieldsV1_SetRawBytes(t *testing.T) { + t.Run("nil receiver", func(t *testing.T) { + var f *metav1.FieldsV1 // nil + f.SetRawBytes([]byte("test")) // Should not panic + }) + + t.Run("empty slice input", func(t *testing.T) { + f := &metav1.FieldsV1{} + f.SetRawBytes([]byte{}) + if f.GetRawString() != "" { + t.Errorf("Expected empty string, got %q", f.GetRawString()) + } + }) + + t.Run("nil slice input", func(t *testing.T) { + f := &metav1.FieldsV1{} + f.SetRawBytes(nil) + if f.GetRawString() != "" { + t.Errorf("Expected empty string, got %q", f.GetRawString()) + } + }) + + t.Run("interning behavior", func(t *testing.T) { + payload := []byte(`{"f:app":{}}`) + + // Two distinct slices with identical content + b1 := append([]byte(nil), payload...) + b2 := append([]byte(nil), payload...) + + f1 := &metav1.FieldsV1{} + f1.SetRawBytes(b1) + + f2 := &metav1.FieldsV1{} + f2.SetRawBytes(b2) + + if f1.GetRawString() != string(payload) { + t.Errorf("Expected GetRawString to match payload, got %q", f1.GetRawString()) + } + + requireInterned(t, f1.GetRawString(), f2.GetRawString()) + }) +} + +func TestFieldsV1_SetRawString(t *testing.T) { + t.Run("nil receiver", func(t *testing.T) { + var f *metav1.FieldsV1 // nil + f.SetRawString("test") // Should not panic + }) + + t.Run("interning behavior", func(t *testing.T) { + payload := `{"f:app":{}}` + + // Force string copy to avoid compiler static string optimization if possible + s1 := string(append([]byte(nil), payload...)) + s2 := string(append([]byte(nil), payload...)) + + f1 := &metav1.FieldsV1{} + f1.SetRawString(s1) + + f2 := &metav1.FieldsV1{} + f2.SetRawString(s2) + + requireInterned(t, f1.GetRawString(), f2.GetRawString()) + }) +} + +func TestFieldsV1_NewFieldsV1(t *testing.T) { + t.Run("interning behavior", func(t *testing.T) { + payload := `{"f:app":{}}` + s1 := string(append([]byte(nil), payload...)) + s2 := string(append([]byte(nil), payload...)) + + f1 := metav1.NewFieldsV1(s1) + f2 := metav1.NewFieldsV1(s2) + + requireInterned(t, f1.GetRawString(), f2.GetRawString()) + }) +} + +func TestFieldsV1_DeepCopyInto(t *testing.T) { + t.Run("preserves interning", func(t *testing.T) { + orig := metav1.NewFieldsV1(`{"f:app":{}}`) + var clone metav1.FieldsV1 + orig.DeepCopyInto(&clone) + + if orig.GetRawString() != clone.GetRawString() { + t.Fatalf("Expected strings to match after DeepCopyInto, got %q and %q", orig.GetRawString(), clone.GetRawString()) + } + + requireInterned(t, orig.GetRawString(), clone.GetRawString()) + }) + + t.Run("zero value deep copy", func(t *testing.T) { + var orig metav1.FieldsV1 + var clone metav1.FieldsV1 + orig.DeepCopyInto(&clone) + + if clone.GetRawString() != "" { + t.Errorf("Expected empty string from cloned zero-value, got %q", clone.GetRawString()) + } + if !orig.Equal(clone) { + t.Errorf("Expected original zero-value and its clone to be equal") + } + }) + + t.Run("independence after deep copy", func(t *testing.T) { + orig := metav1.NewFieldsV1(`{"f:app":{}}`) + var clone metav1.FieldsV1 + orig.DeepCopyInto(&clone) + + // Modify the clone + clone.SetRawString(`{"f:new":{}}`) + + // The original should be completely untouched + if got := orig.GetRawString(); got != `{"f:app":{}}` { + t.Errorf("DeepCopyInto failed to isolate clone! Modifying clone corrupted original. Got: %s", got) + } + }) +} +func TestFieldsV1_UnmarshalInterning(t *testing.T) { + jsonPayload := []byte(`{"f:metadata":{"f:labels":{"f:app":{}}}}`) + orig := metav1.NewFieldsV1(string(jsonPayload)) + + protoPayload, err := orig.Marshal() + if err != nil { + t.Fatalf("Failed to marshal protobuf payload during setup: %v", err) + } + + cborPayload, err := orig.MarshalCBOR() + if err != nil { + t.Fatalf("Failed to marshal CBOR payload during setup: %v", err) + } + + tests := []struct { + name string + payload []byte + unmarshal func(t *testing.T, f *metav1.FieldsV1, payload []byte) + }{ + { + name: "json unmarshal", + payload: jsonPayload, + unmarshal: func(t *testing.T, f *metav1.FieldsV1, payload []byte) { + if err := json.Unmarshal(payload, f); err != nil { + t.Fatalf("JSON Unmarshal failed: %v", err) + } + }, + }, + { + name: "protobuf unmarshal", + payload: protoPayload, + unmarshal: func(t *testing.T, f *metav1.FieldsV1, payload []byte) { + if err := f.Unmarshal(payload); err != nil { + t.Fatalf("Protobuf Unmarshal failed: %v", err) + } + }, + }, + { + name: "cbor unmarshal", + payload: cborPayload, + unmarshal: func(t *testing.T, f *metav1.FieldsV1, payload []byte) { + if err := f.UnmarshalCBOR(payload); err != nil { + t.Fatalf("CBOR Unmarshal failed: %v", err) + } + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Ensure independent slice memory + b1 := append([]byte(nil), tc.payload...) + b2 := append([]byte(nil), tc.payload...) + + var f1 metav1.FieldsV1 + var f2 metav1.FieldsV1 + + tc.unmarshal(t, &f1, b1) + tc.unmarshal(t, &f2, b2) + + requireInterned(t, f1.GetRawString(), f2.GetRawString()) + }) + } +}