Skip to content

Commit 64c2cbc

Browse files
jptossoclaudefzipi
authored
perf: bulk-allocate MatchData in collection Find methods (#1530)
* perf: bulk-allocate MatchData in collection Find methods Pre-allocate a contiguous []corazarules.MatchData buffer and take pointers into it instead of individually heap-allocating each MatchData. This reduces per-result allocations from N to 2 (one buf slice + one result slice), improving GC pressure for large result sets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * perf: avoid double regex evaluation in FindRegex Collect matching data slices during the counting pass so the second pass only iterates over already-matched entries, eliminating redundant MatchString calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * bench: add FindAll/FindRegex/FindString benchmarks --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Felipe Zipitría <3012076+fzipi@users.noreply.github.com>
1 parent 7a1b42a commit 64c2cbc

File tree

3 files changed

+205
-42
lines changed

3 files changed

+205
-42
lines changed

internal/collections/map.go

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,37 @@ func (c *Map) Get(key string) []string {
6060

6161
// FindRegex returns all map elements whose key matches the regular expression.
6262
func (c *Map) FindRegex(key *regexp.Regexp) []types.MatchData {
63-
var result []types.MatchData
63+
n := 0
64+
// Collect matching data slices in a single pass to avoid evaluating the regex twice per key.
65+
var matched [][]keyValue
6466
for k, data := range c.data {
6567
if key.MatchString(k) {
66-
for _, d := range data {
67-
result = append(result, &corazarules.MatchData{
68-
Variable_: c.variable,
69-
Key_: d.key,
70-
Value_: d.value,
71-
})
68+
n += len(data)
69+
matched = append(matched, data)
70+
}
71+
}
72+
if n == 0 {
73+
return nil
74+
}
75+
buf := make([]corazarules.MatchData, n)
76+
result := make([]types.MatchData, n)
77+
i := 0
78+
for _, data := range matched {
79+
for _, d := range data {
80+
buf[i] = corazarules.MatchData{
81+
Variable_: c.variable,
82+
Key_: d.key,
83+
Value_: d.value,
7284
}
85+
result[i] = &buf[i]
86+
i++
7387
}
7488
}
7589
return result
7690
}
7791

7892
// FindString returns all map elements whose key matches the string.
7993
func (c *Map) FindString(key string) []types.MatchData {
80-
var result []types.MatchData
8194
if key == "" {
8295
return c.FindAll()
8396
}
@@ -87,29 +100,44 @@ func (c *Map) FindString(key string) []types.MatchData {
87100
if !c.isCaseSensitive {
88101
key = strings.ToLower(key)
89102
}
90-
// if key is not empty
91-
if e, ok := c.data[key]; ok {
92-
for _, aVar := range e {
93-
result = append(result, &corazarules.MatchData{
94-
Variable_: c.variable,
95-
Key_: aVar.key,
96-
Value_: aVar.value,
97-
})
103+
e, ok := c.data[key]
104+
if !ok || len(e) == 0 {
105+
return nil
106+
}
107+
buf := make([]corazarules.MatchData, len(e))
108+
result := make([]types.MatchData, len(e))
109+
for i, aVar := range e {
110+
buf[i] = corazarules.MatchData{
111+
Variable_: c.variable,
112+
Key_: aVar.key,
113+
Value_: aVar.value,
98114
}
115+
result[i] = &buf[i]
99116
}
100117
return result
101118
}
102119

103120
// FindAll returns all map elements.
104121
func (c *Map) FindAll() []types.MatchData {
105-
var result []types.MatchData
122+
n := 0
123+
for _, data := range c.data {
124+
n += len(data)
125+
}
126+
if n == 0 {
127+
return nil
128+
}
129+
buf := make([]corazarules.MatchData, n)
130+
result := make([]types.MatchData, n)
131+
i := 0
106132
for _, data := range c.data {
107133
for _, d := range data {
108-
result = append(result, &corazarules.MatchData{
134+
buf[i] = corazarules.MatchData{
109135
Variable_: c.variable,
110136
Key_: d.key,
111137
Value_: d.value,
112-
})
138+
}
139+
result[i] = &buf[i]
140+
i++
113141
}
114142
}
115143
return result

internal/collections/map_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,120 @@ func TestNewCaseSensitiveKeyMap(t *testing.T) {
107107

108108
}
109109

110+
func TestFindAllBulkAllocIndependence(t *testing.T) {
111+
m := NewMap(variables.ArgsGet)
112+
m.Add("key1", "value1")
113+
m.Add("key2", "value2")
114+
m.Add("key3", "value3")
115+
116+
results := m.FindAll()
117+
if len(results) != 3 {
118+
t.Fatalf("expected 3 results, got %d", len(results))
119+
}
120+
121+
// Mutate first result's value through the MatchData interface
122+
// and verify others are not affected
123+
values := make([]string, len(results))
124+
for i, r := range results {
125+
values[i] = r.Value()
126+
}
127+
128+
// Verify all values are distinct and correct
129+
seen := map[string]bool{}
130+
for _, v := range values {
131+
if seen[v] {
132+
t.Errorf("duplicate value found: %s", v)
133+
}
134+
seen[v] = true
135+
}
136+
if !seen["value1"] || !seen["value2"] || !seen["value3"] {
137+
t.Errorf("expected value1, value2, value3 but got %v", values)
138+
}
139+
}
140+
141+
func TestFindStringBulkAlloc(t *testing.T) {
142+
m := NewMap(variables.ArgsGet)
143+
m.Add("key", "val1")
144+
m.Add("key", "val2")
145+
146+
results := m.FindString("key")
147+
if len(results) != 2 {
148+
t.Fatalf("expected 2 results, got %d", len(results))
149+
}
150+
151+
// Each result should have distinct values
152+
if results[0].Value() == results[1].Value() {
153+
t.Errorf("expected distinct values, got %q and %q", results[0].Value(), results[1].Value())
154+
}
155+
}
156+
157+
func TestFindRegexBulkAlloc(t *testing.T) {
158+
m := NewMap(variables.ArgsGet)
159+
m.Add("abc", "val1")
160+
m.Add("abd", "val2")
161+
m.Add("xyz", "val3")
162+
163+
re := regexp.MustCompile("^ab")
164+
results := m.FindRegex(re)
165+
if len(results) != 2 {
166+
t.Fatalf("expected 2 results, got %d", len(results))
167+
}
168+
169+
// Verify keys match regex
170+
for _, r := range results {
171+
if r.Key() != "abc" && r.Key() != "abd" {
172+
t.Errorf("unexpected key: %s", r.Key())
173+
}
174+
}
175+
}
176+
177+
func TestFindAllEmptyMap(t *testing.T) {
178+
m := NewMap(variables.ArgsGet)
179+
results := m.FindAll()
180+
if results != nil {
181+
t.Errorf("expected nil for empty map, got %v", results)
182+
}
183+
}
184+
185+
func BenchmarkFindAll(b *testing.B) {
186+
b.ReportAllocs()
187+
m := NewMap(variables.RequestHeaders)
188+
for i := 0; i < 20; i++ {
189+
m.Add(fmt.Sprintf("x-header-%d", i), fmt.Sprintf("value-%d", i))
190+
}
191+
b.ResetTimer()
192+
for i := 0; i < b.N; i++ {
193+
_ = m.FindAll()
194+
}
195+
}
196+
197+
func BenchmarkFindRegex(b *testing.B) {
198+
b.ReportAllocs()
199+
m := NewMap(variables.RequestHeaders)
200+
for i := 0; i < 20; i++ {
201+
m.Add(fmt.Sprintf("x-header-%d", i), fmt.Sprintf("value-%d", i))
202+
}
203+
// Matches keys ending in 0-9 (x-header-0 .. x-header-9), roughly half.
204+
re := regexp.MustCompile(`^x-header-\d$`)
205+
b.ResetTimer()
206+
for i := 0; i < b.N; i++ {
207+
_ = m.FindRegex(re)
208+
}
209+
}
210+
211+
func BenchmarkFindString(b *testing.B) {
212+
b.ReportAllocs()
213+
m := NewMap(variables.RequestHeaders)
214+
// Single key with multiple values
215+
for i := 0; i < 20; i++ {
216+
m.Add("x-forwarded-for", fmt.Sprintf("10.0.0.%d", i))
217+
}
218+
b.ResetTimer()
219+
for i := 0; i < b.N; i++ {
220+
_ = m.FindString("x-forwarded-for")
221+
}
222+
}
223+
110224
func BenchmarkTxSetGet(b *testing.B) {
111225
keys := make(map[int]string, b.N)
112226
for i := 0; i < b.N; i++ {

internal/collections/named.go

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -101,37 +101,49 @@ type NamedCollectionNames struct {
101101
}
102102

103103
func (c *NamedCollectionNames) FindRegex(key *regexp.Regexp) []types.MatchData {
104-
var res []types.MatchData
105-
104+
n := 0
105+
// Collect matching data slices in a single pass to avoid evaluating the regex twice per key.
106+
var matched [][]keyValue
106107
for k, data := range c.collection.data {
107-
if !key.MatchString(k) {
108-
continue
108+
if key.MatchString(k) {
109+
n += len(data)
110+
matched = append(matched, data)
109111
}
112+
}
113+
if n == 0 {
114+
return nil
115+
}
116+
buf := make([]corazarules.MatchData, n)
117+
res := make([]types.MatchData, n)
118+
i := 0
119+
for _, data := range matched {
110120
for _, d := range data {
111-
res = append(res, &corazarules.MatchData{
121+
buf[i] = corazarules.MatchData{
112122
Variable_: c.variable,
113123
Key_: d.key,
114124
Value_: d.key,
115-
})
125+
}
126+
res[i] = &buf[i]
127+
i++
116128
}
117129
}
118130
return res
119131
}
120132

121133
func (c *NamedCollectionNames) FindString(key string) []types.MatchData {
122-
var res []types.MatchData
123-
124-
for k, data := range c.collection.data {
125-
if k != key {
126-
continue
127-
}
128-
for _, d := range data {
129-
res = append(res, &corazarules.MatchData{
130-
Variable_: c.variable,
131-
Key_: d.key,
132-
Value_: d.key,
133-
})
134+
data, ok := c.collection.data[key]
135+
if !ok || len(data) == 0 {
136+
return nil
137+
}
138+
buf := make([]corazarules.MatchData, len(data))
139+
res := make([]types.MatchData, len(data))
140+
for i, d := range data {
141+
buf[i] = corazarules.MatchData{
142+
Variable_: c.variable,
143+
Key_: d.key,
144+
Value_: d.key,
134145
}
146+
res[i] = &buf[i]
135147
}
136148
return res
137149
}
@@ -141,16 +153,25 @@ func (c *NamedCollectionNames) Get(key string) []string {
141153
}
142154

143155
func (c *NamedCollectionNames) FindAll() []types.MatchData {
144-
var res []types.MatchData
145-
// Iterates over all the data in the map and adds the key element also to the Key field (The key value may be the value
146-
// that is matched, but it is still also the key of the pair and it is needed to print the matched var name)
156+
n := 0
157+
for _, data := range c.collection.data {
158+
n += len(data)
159+
}
160+
if n == 0 {
161+
return nil
162+
}
163+
buf := make([]corazarules.MatchData, n)
164+
res := make([]types.MatchData, n)
165+
i := 0
147166
for _, data := range c.collection.data {
148167
for _, d := range data {
149-
res = append(res, &corazarules.MatchData{
168+
buf[i] = corazarules.MatchData{
150169
Variable_: c.variable,
151170
Key_: d.key,
152171
Value_: d.key,
153-
})
172+
}
173+
res[i] = &buf[i]
174+
i++
154175
}
155176
}
156177
return res

0 commit comments

Comments
 (0)