Skip to content

Commit 75dcda0

Browse files
klauspostelithrar
authored andcommitted
perf: reduce allocations in (*routeRegexp).getURLQuery (#544)
A production server is seeing a significant amount of allocations in (*routeRegexp).getURLQuery Since it is only interested in a single value and only the first value we create a specialized function for that. Comparing a few parameter parsing scenarios: ``` Benchmark_findQueryKey/0-8 7184014 168 ns/op 0 B/op 0 allocs/op Benchmark_findQueryKey/1-8 5307873 227 ns/op 48 B/op 3 allocs/op Benchmark_findQueryKey/2-8 1560836 770 ns/op 483 B/op 10 allocs/op Benchmark_findQueryKey/3-8 1296200 931 ns/op 559 B/op 11 allocs/op Benchmark_findQueryKey/4-8 666502 1769 ns/op 3 B/op 1 allocs/op Benchmark_findQueryKeyGoLib/0-8 1740973 690 ns/op 864 B/op 8 allocs/op Benchmark_findQueryKeyGoLib/1-8 3029618 393 ns/op 432 B/op 4 allocs/op Benchmark_findQueryKeyGoLib/2-8 461427 2511 ns/op 1542 B/op 24 allocs/op Benchmark_findQueryKeyGoLib/3-8 324252 3804 ns/op 1984 B/op 28 allocs/op Benchmark_findQueryKeyGoLib/4-8 69348 14928 ns/op 12716 B/op 130 allocs/op ```
1 parent 49c0148 commit 75dcda0

File tree

2 files changed

+132
-4
lines changed

2 files changed

+132
-4
lines changed

regexp.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,51 @@ func (r *routeRegexp) getURLQuery(req *http.Request) string {
230230
return ""
231231
}
232232
templateKey := strings.SplitN(r.template, "=", 2)[0]
233-
for key, vals := range req.URL.Query() {
234-
if key == templateKey && len(vals) > 0 {
235-
return key + "=" + vals[0]
236-
}
233+
val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey)
234+
if ok {
235+
return templateKey + "=" + val
237236
}
238237
return ""
239238
}
240239

240+
// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0].
241+
// If key was not found, empty string and false is returned.
242+
func findFirstQueryKey(rawQuery, key string) (value string, ok bool) {
243+
query := []byte(rawQuery)
244+
for len(query) > 0 {
245+
foundKey := query
246+
if i := bytes.IndexAny(foundKey, "&;"); i >= 0 {
247+
foundKey, query = foundKey[:i], foundKey[i+1:]
248+
} else {
249+
query = query[:0]
250+
}
251+
if len(foundKey) == 0 {
252+
continue
253+
}
254+
var value []byte
255+
if i := bytes.IndexByte(foundKey, '='); i >= 0 {
256+
foundKey, value = foundKey[:i], foundKey[i+1:]
257+
}
258+
if len(foundKey) < len(key) {
259+
// Cannot possibly be key.
260+
continue
261+
}
262+
keyString, err := url.QueryUnescape(string(foundKey))
263+
if err != nil {
264+
continue
265+
}
266+
if keyString != key {
267+
continue
268+
}
269+
valueString, err := url.QueryUnescape(string(value))
270+
if err != nil {
271+
continue
272+
}
273+
return valueString, true
274+
}
275+
return "", false
276+
}
277+
241278
func (r *routeRegexp) matchQueryString(req *http.Request) bool {
242279
return r.regexp.MatchString(r.getURLQuery(req))
243280
}

regexp_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package mux
2+
3+
import (
4+
"net/url"
5+
"reflect"
6+
"strconv"
7+
"testing"
8+
)
9+
10+
func Test_findFirstQueryKey(t *testing.T) {
11+
tests := []string{
12+
"a=1&b=2",
13+
"a=1&a=2&a=banana",
14+
"ascii=%3Ckey%3A+0x90%3E",
15+
"a=1;b=2",
16+
"a=1&a=2;a=banana",
17+
"a==",
18+
"a=%2",
19+
"a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30",
20+
"a=1& ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;&a=5",
21+
"a=xxxxxxxxxxxxxxxx&b=YYYYYYYYYYYYYYY&c=ppppppppppppppppppp&f=ttttttttttttttttt&a=uuuuuuuuuuuuu",
22+
}
23+
for _, query := range tests {
24+
t.Run(query, func(t *testing.T) {
25+
// Check against url.ParseQuery, ignoring the error.
26+
all, _ := url.ParseQuery(query)
27+
for key, want := range all {
28+
t.Run(key, func(t *testing.T) {
29+
got, ok := findFirstQueryKey(query, key)
30+
if !ok {
31+
t.Error("Did not get expected key", key)
32+
}
33+
if !reflect.DeepEqual(got, want[0]) {
34+
t.Errorf("findFirstQueryKey(%s,%s) = %v, want %v", query, key, got, want[0])
35+
}
36+
})
37+
}
38+
})
39+
}
40+
}
41+
42+
func Benchmark_findQueryKey(b *testing.B) {
43+
tests := []string{
44+
"a=1&b=2",
45+
"ascii=%3Ckey%3A+0x90%3E",
46+
"a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30",
47+
"a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu",
48+
"a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=",
49+
}
50+
for i, query := range tests {
51+
b.Run(strconv.Itoa(i), func(b *testing.B) {
52+
// Check against url.ParseQuery, ignoring the error.
53+
all, _ := url.ParseQuery(query)
54+
b.ReportAllocs()
55+
b.ResetTimer()
56+
for i := 0; i < b.N; i++ {
57+
for key, _ := range all {
58+
_, _ = findFirstQueryKey(query, key)
59+
}
60+
}
61+
})
62+
}
63+
}
64+
65+
func Benchmark_findQueryKeyGoLib(b *testing.B) {
66+
tests := []string{
67+
"a=1&b=2",
68+
"ascii=%3Ckey%3A+0x90%3E",
69+
"a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30",
70+
"a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu",
71+
"a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=",
72+
}
73+
for i, query := range tests {
74+
b.Run(strconv.Itoa(i), func(b *testing.B) {
75+
// Check against url.ParseQuery, ignoring the error.
76+
all, _ := url.ParseQuery(query)
77+
var u url.URL
78+
u.RawQuery = query
79+
b.ReportAllocs()
80+
b.ResetTimer()
81+
for i := 0; i < b.N; i++ {
82+
for key, _ := range all {
83+
v := u.Query()[key]
84+
if len(v) > 0 {
85+
_ = v[0]
86+
}
87+
}
88+
}
89+
})
90+
}
91+
}

0 commit comments

Comments
 (0)