Skip to content

Commit 1514bb2

Browse files
authored
Merge pull request kubernetes#75699 from smarterclayton/fast_encode
Avoid allocations when building SelfLinks and fast path escape
2 parents 2086f81 + 389a843 commit 1514bb2

File tree

4 files changed

+116
-8
lines changed

4 files changed

+116
-8
lines changed

staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ go_test(
3434
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
3535
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
3636
"//vendor/github.com/evanphx/json-patch:go_default_library",
37+
"//vendor/github.com/google/gofuzz:go_default_library",
3738
"//vendor/k8s.io/utils/trace:go_default_library",
3839
],
3940
)

staging/src/k8s.io/apiserver/pkg/endpoints/handlers/namer.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"net/http"
2222
"net/url"
23+
"strings"
2324

2425
"k8s.io/apimachinery/pkg/api/errors"
2526
"k8s.io/apimachinery/pkg/runtime"
@@ -86,6 +87,21 @@ func (n ContextBasedNaming) Name(req *http.Request) (namespace, name string, err
8687
return ns, requestInfo.Name, nil
8788
}
8889

90+
// fastURLPathEncode encodes the provided path as a URL path
91+
func fastURLPathEncode(path string) string {
92+
for _, r := range []byte(path) {
93+
switch {
94+
case r >= '-' && r <= '9', r >= 'A' && r <= 'Z', r >= 'a' && r <= 'z':
95+
// characters within this range do not require escaping
96+
default:
97+
var u url.URL
98+
u.Path = path
99+
return u.EscapedPath()
100+
}
101+
}
102+
return path
103+
}
104+
89105
func (n ContextBasedNaming) GenerateLink(requestInfo *request.RequestInfo, obj runtime.Object) (uri string, err error) {
90106
namespace, name, err := n.ObjectName(obj)
91107
if err == errEmptyName && len(requestInfo.Name) > 0 {
@@ -101,19 +117,23 @@ func (n ContextBasedNaming) GenerateLink(requestInfo *request.RequestInfo, obj r
101117
return n.SelfLinkPathPrefix + url.QueryEscape(name) + n.SelfLinkPathSuffix, nil
102118
}
103119

104-
return n.SelfLinkPathPrefix +
105-
url.QueryEscape(namespace) +
106-
"/" + url.QueryEscape(requestInfo.Resource) + "/" +
107-
url.QueryEscape(name) +
108-
n.SelfLinkPathSuffix,
109-
nil
120+
builder := strings.Builder{}
121+
builder.Grow(len(n.SelfLinkPathPrefix) + len(namespace) + len(requestInfo.Resource) + len(name) + len(n.SelfLinkPathSuffix) + 8)
122+
builder.WriteString(n.SelfLinkPathPrefix)
123+
builder.WriteString(namespace)
124+
builder.WriteByte('/')
125+
builder.WriteString(requestInfo.Resource)
126+
builder.WriteByte('/')
127+
builder.WriteString(name)
128+
builder.WriteString(n.SelfLinkPathSuffix)
129+
return fastURLPathEncode(builder.String()), nil
110130
}
111131

112132
func (n ContextBasedNaming) GenerateListLink(req *http.Request) (uri string, err error) {
113133
if len(req.URL.RawPath) > 0 {
114134
return req.URL.RawPath, nil
115135
}
116-
return req.URL.EscapedPath(), nil
136+
return fastURLPathEncode(req.URL.Path), nil
117137
}
118138

119139
func (n ContextBasedNaming) ObjectName(obj runtime.Object) (namespace, name string, err error) {

staging/src/k8s.io/apiserver/pkg/endpoints/handlers/namer_test.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ limitations under the License.
1717
package handlers
1818

1919
import (
20+
"math/rand"
21+
"net/url"
2022
"testing"
2123

22-
"k8s.io/api/core/v1"
24+
fuzz "github.com/google/gofuzz"
25+
26+
v1 "k8s.io/api/core/v1"
2327
"k8s.io/apimachinery/pkg/api/meta"
2428
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2529
"k8s.io/apimachinery/pkg/runtime"
@@ -114,3 +118,62 @@ func TestGenerateLink(t *testing.T) {
114118
}
115119
}
116120
}
121+
122+
func Test_fastURLPathEncode_fuzz(t *testing.T) {
123+
specialCases := []string{"/", "//", ".", "*", "/abc%"}
124+
for _, test := range specialCases {
125+
got := fastURLPathEncode(test)
126+
u := url.URL{Path: test}
127+
expected := u.EscapedPath()
128+
if got != expected {
129+
t.Errorf("%q did not match %q", got, expected)
130+
}
131+
}
132+
f := fuzz.New().Funcs(
133+
func(s *string, c fuzz.Continue) {
134+
*s = randString(c.Rand)
135+
},
136+
)
137+
for i := 0; i < 2000; i++ {
138+
var test string
139+
f.Fuzz(&test)
140+
141+
got := fastURLPathEncode(test)
142+
u := url.URL{Path: test}
143+
expected := u.EscapedPath()
144+
if got != expected {
145+
t.Errorf("%q did not match %q", got, expected)
146+
}
147+
}
148+
}
149+
150+
// Unicode range fuzzer from github.com/google/gofuzz/fuzz.go
151+
152+
type charRange struct {
153+
first, last rune
154+
}
155+
156+
var unicodeRanges = []charRange{
157+
{0x00, 0x255},
158+
{' ', '~'}, // ASCII characters
159+
{'\u00a0', '\u02af'}, // Multi-byte encoded characters
160+
{'\u4e00', '\u9fff'}, // Common CJK (even longer encodings)
161+
}
162+
163+
// randString makes a random string up to 20 characters long. The returned string
164+
// may include a variety of (valid) UTF-8 encodings.
165+
func randString(r *rand.Rand) string {
166+
n := r.Intn(20)
167+
runes := make([]rune, n)
168+
for i := range runes {
169+
runes[i] = unicodeRanges[r.Intn(len(unicodeRanges))].choose(r)
170+
}
171+
return string(runes)
172+
}
173+
174+
// choose returns a random unicode character from the given range, using the
175+
// given randomness source.
176+
func (r *charRange) choose(rand *rand.Rand) rune {
177+
count := int64(r.last - r.first)
178+
return r.first + rune(rand.Int63n(count))
179+
}

staging/src/k8s.io/apiserver/pkg/endpoints/watch_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,30 @@ func TestWatchHTTPTimeout(t *testing.T) {
757757
func BenchmarkWatchHTTP(b *testing.B) {
758758
items := benchmarkItems(b)
759759

760+
// use ASCII names to capture the cost of handling ASCII only self-links
761+
for i := range items {
762+
item := &items[i]
763+
item.Namespace = fmt.Sprintf("namespace-%d", i)
764+
item.Name = fmt.Sprintf("reasonable-name-%d", i)
765+
}
766+
767+
runWatchHTTPBenchmark(b, items)
768+
}
769+
770+
func BenchmarkWatchHTTP_UTF8(b *testing.B) {
771+
items := benchmarkItems(b)
772+
773+
// use UTF names to capture the cost of handling UTF-8 escaping in self-links
774+
for i := range items {
775+
item := &items[i]
776+
item.Namespace = fmt.Sprintf("躀痢疈蜧í柢-%d", i)
777+
item.Name = fmt.Sprintf("翏Ŏ熡韐-%d", i)
778+
}
779+
780+
runWatchHTTPBenchmark(b, items)
781+
}
782+
783+
func runWatchHTTPBenchmark(b *testing.B, items []example.Pod) {
760784
simpleStorage := &SimpleRESTStorage{}
761785
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
762786
server := httptest.NewServer(handler)

0 commit comments

Comments
 (0)