Skip to content

Commit 2a83281

Browse files
authored
[shell-operator] chore: reduce memory consumption (#871)
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
1 parent 7ecddf2 commit 2a83281

9 files changed

Lines changed: 499 additions & 529 deletions

File tree

pkg/executor/executor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ func (pl *proxyLogger) Write(p []byte) (int, error) {
215215

216216
logMap, ok := line.(map[string]interface{})
217217
defer func() {
218-
pl.buf = []byte{}
218+
pl.buf = pl.buf[:0]
219219
}()
220220

221221
if !ok {

pkg/filter/jq/apply.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package jq
33
import (
44
"encoding/json"
55
"errors"
6+
"fmt"
67

78
"github.com/itchyny/gojq"
89

@@ -91,19 +92,48 @@ func (c *CompiledJqFilter) String() string {
9192
return c.originalStr
9293
}
9394

95+
// deepCopyAny recursively copies JSON-compatible values (maps, slices, and
96+
// primitives) without going through json.Marshal/Unmarshal. This is
97+
// significantly faster and allocates only the final structure.
9498
func deepCopyAny(input any) (any, error) {
9599
if input == nil {
96100
return nil, nil
97101
}
98-
data, err := json.Marshal(input)
99-
if err != nil {
100-
return nil, err
101-
}
102-
var output any
103-
if err := json.Unmarshal(data, &output); err != nil {
104-
return nil, err
102+
return deepCopyValue(input)
103+
}
104+
105+
func deepCopyValue(v any) (any, error) {
106+
switch val := v.(type) {
107+
case map[string]any:
108+
m := make(map[string]any, len(val))
109+
for k, v := range val {
110+
copied, err := deepCopyValue(v)
111+
if err != nil {
112+
return nil, err
113+
}
114+
m[k] = copied
115+
}
116+
return m, nil
117+
case []any:
118+
s := make([]any, len(val))
119+
for i, v := range val {
120+
copied, err := deepCopyValue(v)
121+
if err != nil {
122+
return nil, err
123+
}
124+
s[i] = copied
125+
}
126+
return s, nil
127+
case string, bool, json.Number,
128+
int, int8, int16, int32, int64,
129+
uint, uint8, uint16, uint32, uint64,
130+
float32, float64:
131+
return val, nil
132+
case nil:
133+
return nil, nil
134+
default:
135+
return nil, fmt.Errorf("deepCopyValue: unsupported type %T", v)
105136
}
106-
return output, nil
107137
}
108138

109139
// collectResults drains a gojq iterator and serialises the results to JSON.

pkg/filter/jq/apply_test.go

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,32 +175,114 @@ func Test_ApplyFilter_PanicSafety(t *testing.T) {
175175
func Test_deepCopyAny(t *testing.T) {
176176
g := NewWithT(t)
177177

178-
// Test copying a map
178+
// Test copying a map preserves types
179179
inputMap := map[string]any{"foo": "bar", "num": 42}
180180
copyMap, err := deepCopyAny(inputMap)
181181
g.Expect(err).Should(BeNil())
182-
g.Expect(copyMap).Should(Equal(map[string]any{"foo": "bar", "num": float64(42)}))
182+
g.Expect(copyMap).Should(Equal(map[string]any{"foo": "bar", "num": 42}))
183183
g.Expect(copyMap).ShouldNot(BeIdenticalTo(inputMap))
184184

185-
// Test copying a slice
185+
// Test copying a slice preserves types
186186
inputSlice := []any{"a", 1, true}
187187
copySlice, err := deepCopyAny(inputSlice)
188188
g.Expect(err).Should(BeNil())
189-
g.Expect(copySlice).Should(Equal([]any{"a", float64(1), true}))
189+
g.Expect(copySlice).Should(Equal([]any{"a", 1, true}))
190190
g.Expect(copySlice).ShouldNot(BeIdenticalTo(inputSlice))
191191

192192
// Test copying nil
193193
copyNil, err := deepCopyAny(nil)
194194
g.Expect(err).Should(BeNil())
195195
g.Expect(copyNil).Should(BeNil())
196196

197-
// Test copying a value that cannot be marshaled to JSON
197+
// Test copying a value with unsupported type
198198
inputInvalid := map[string]any{"ch": make(chan int)}
199199
copyInvalid, err := deepCopyAny(inputInvalid)
200200
g.Expect(err).ShouldNot(BeNil())
201201
g.Expect(copyInvalid).Should(BeNil())
202202
}
203203

204+
func Test_deepCopyAny_NestedMap(t *testing.T) {
205+
g := NewWithT(t)
206+
207+
input := map[string]any{
208+
"metadata": map[string]any{
209+
"name": "my-pod",
210+
"namespace": "default",
211+
"labels": map[string]any{"app": "test"},
212+
},
213+
"spec": map[string]any{
214+
"replicas": float64(3),
215+
},
216+
}
217+
218+
result, err := deepCopyAny(input)
219+
g.Expect(err).Should(BeNil())
220+
221+
resultMap := result.(map[string]any)
222+
g.Expect(resultMap["metadata"]).Should(Equal(input["metadata"]))
223+
224+
// Verify it's a true deep copy: mutating the copy must not affect the original.
225+
resultMeta := resultMap["metadata"].(map[string]any)
226+
resultMeta["name"] = "mutated"
227+
g.Expect(input["metadata"].(map[string]any)["name"]).Should(Equal("my-pod"))
228+
}
229+
230+
func Test_deepCopyAny_NestedSlice(t *testing.T) {
231+
g := NewWithT(t)
232+
233+
input := []any{
234+
map[string]any{"name": "a"},
235+
map[string]any{"name": "b"},
236+
}
237+
238+
result, err := deepCopyAny(input)
239+
g.Expect(err).Should(BeNil())
240+
241+
resultSlice := result.([]any)
242+
g.Expect(resultSlice).Should(HaveLen(2))
243+
244+
// Mutate copy, verify original is untouched.
245+
resultSlice[0].(map[string]any)["name"] = "mutated"
246+
g.Expect(input[0].(map[string]any)["name"]).Should(Equal("a"))
247+
}
248+
249+
func Test_deepCopyAny_NumericTypes(t *testing.T) {
250+
g := NewWithT(t)
251+
252+
input := map[string]any{
253+
"int": 42,
254+
"int64": int64(100),
255+
"float64": 3.14,
256+
"bool": true,
257+
"string": "hello",
258+
}
259+
260+
result, err := deepCopyAny(input)
261+
g.Expect(err).Should(BeNil())
262+
g.Expect(result).Should(Equal(input))
263+
}
264+
265+
func BenchmarkDeepCopyAny(b *testing.B) {
266+
input := map[string]any{
267+
"metadata": map[string]any{
268+
"name": "my-pod",
269+
"namespace": "default",
270+
"labels": map[string]any{"app": "test", "env": "prod"},
271+
},
272+
"spec": map[string]any{
273+
"containers": []any{
274+
map[string]any{"name": "main", "image": "nginx:latest"},
275+
map[string]any{"name": "sidecar", "image": "envoy:latest"},
276+
},
277+
"replicas": float64(3),
278+
},
279+
}
280+
b.ResetTimer()
281+
for range b.N {
282+
_, _ = deepCopyAny(input)
283+
}
284+
}
285+
204286
// ---- Compile / CompiledJqFilter tests ----
205287

206288
func Test_Compile_ValidExpression(t *testing.T) {

pkg/hook/binding_context/binding_context.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package bindingcontext
22

33
import (
44
"encoding/json"
5+
"io"
56

67
"github.com/deckhouse/deckhouse/pkg/log"
78
v1 "k8s.io/api/admission/v1"
@@ -182,6 +183,13 @@ func ConvertBindingContextList(version string, contexts []BindingContext) Bindin
182183
}
183184

184185
func (b BindingContextList) Json() ([]byte, error) {
185-
data, err := json.MarshalIndent(b, "", " ")
186-
return data, err
186+
return json.Marshal(b)
187+
}
188+
189+
// WriteJson streams the JSON-encoded binding context list directly to w,
190+
// avoiding an intermediate []byte allocation that can be very large for
191+
// synchronization events with many objects.
192+
func (b BindingContextList) WriteJson(w io.Writer) error {
193+
enc := json.NewEncoder(w)
194+
return enc.Encode(b)
187195
}

0 commit comments

Comments
 (0)