Skip to content

Commit 6a00bf3

Browse files
author
Sergio Andres Virviescas Santana
committed
Improve performance of radix.FindCaseInsensitivePath
1 parent 510879a commit 6a00bf3

File tree

6 files changed

+97
-69
lines changed

6 files changed

+97
-69
lines changed

radix/node.go

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"sort"
55
"strings"
66

7+
"github.com/valyala/bytebufferpool"
78
"github.com/valyala/fasthttp"
89
)
910

@@ -322,49 +323,53 @@ walk:
322323
}
323324
}
324325

325-
func (n *node) find(path string, buf []byte) ([]byte, bool) {
326+
func (n *node) find(path string, buf *bytebufferpool.ByteBuffer) (bool, bool) {
326327
if len(path) > len(n.path) {
327328
if strings.EqualFold(path[:len(n.path)], n.path) {
328329

329330
path = path[len(n.path):]
330-
buf = append(buf, n.path...)
331+
buf.WriteString(n.path)
331332

332333
if len(path) == 1 {
333334
if path == "/" && n.handler != nil {
334335
if n.tsr {
335-
buf = append(buf, '/')
336+
buf.WriteByte('/')
336337

337-
return buf, false
338+
return true, false
338339
}
339340

340-
return buf, true
341+
return true, true
341342
}
342343
}
343344

344-
return n.findChild(path, buf)
345+
found, tsr := n.findChild(path, buf)
346+
if found {
347+
return found, tsr
348+
}
349+
350+
bufferRemoveString(buf, n.path)
345351
}
346352
} else if strings.EqualFold(path, n.path) && n.handler != nil {
347-
buf = append(buf, n.path...)
353+
buf.WriteString(n.path)
348354

349355
if n.tsr {
350-
buf = append(buf, '/')
351-
352-
return buf, true
356+
buf.WriteByte('/')
357+
return true, true
353358
}
354359

355-
return buf, false
360+
return true, false
356361
}
357362

358-
return nil, false
363+
return false, false
359364
}
360365

361-
func (n *node) findChild(path string, buf []byte) ([]byte, bool) {
366+
func (n *node) findChild(path string, buf *bytebufferpool.ByteBuffer) (bool, bool) {
362367
for _, child := range n.children {
363368
switch child.nType {
364369
case static:
365-
buf2, tsr := child.find(path, buf)
366-
if buf2 != nil || tsr {
367-
return buf2, tsr
370+
found, tsr := child.find(path, buf)
371+
if found {
372+
return found, tsr
368373
}
369374

370375
case param:
@@ -377,49 +382,52 @@ func (n *node) findChild(path string, buf []byte) ([]byte, bool) {
377382
}
378383
}
379384

385+
buf.WriteString(path[:end])
386+
380387
if child.handler != nil {
381388
if end == len(path) {
382-
buf = append(buf, path...)
383-
384389
if child.tsr {
385-
buf = append(buf, '/')
390+
buf.WriteByte('/')
386391

387-
return buf, true
392+
return true, true
388393
}
389394

390-
return buf, false
391-
} else if path[end:] == "/" {
392-
buf = append(buf, path[:end]...)
395+
return true, false
393396

397+
} else if path[end:] == "/" {
394398
if child.tsr {
395-
buf = append(buf, '/')
399+
buf.WriteByte('/')
396400

397-
return buf, false
401+
return true, false
398402
}
399403

400-
return buf, true
404+
return true, true
401405
}
402406
} else if len(path[end:]) == 0 {
403-
return nil, false
407+
bufferRemoveString(buf, path[:end])
408+
409+
return false, false
404410
}
405411

406-
buf2, tsr := child.findChild(path[end:], append(buf, path[:end]...))
407-
if buf2 != nil || tsr {
408-
return buf2, tsr
412+
found, tsr := child.findChild(path[end:], buf)
413+
if found {
414+
return found, tsr
409415
}
410416

417+
bufferRemoveString(buf, path[:end])
418+
411419
default:
412420
panic("invalid node type")
413421
}
414422
}
415423

416424
if n.wildcard != nil {
417-
buf = append(buf, path...)
425+
buf.WriteString(path)
418426

419-
return buf, false
427+
return true, false
420428
}
421429

422-
return nil, false
430+
return false, false
423431
}
424432

425433
// clone clones the current node in a new pointer

radix/node_test.go

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77
"testing"
88

9+
"github.com/valyala/bytebufferpool"
910
"github.com/valyala/fasthttp"
1011
)
1112

@@ -469,24 +470,30 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
469470
}
470471
}
471472

473+
buf := bytebufferpool.Get()
474+
472475
// Check out == in for all registered routes
473476
// With fixTrailingSlash = true
474477
for _, route := range routes {
475-
out, found := tree.FindCaseInsensitivePath(route, true)
478+
found := tree.FindCaseInsensitivePath(route, true, buf)
476479
if !found {
477480
t.Errorf("Route '%s' not found!", route)
478-
} else if string(out) != route {
479-
t.Errorf("Wrong result for route '%s': %s", route, string(out))
481+
} else if out := buf.String(); out != route {
482+
t.Errorf("Wrong result for route '%s': %s", route, out)
480483
}
484+
485+
buf.Reset()
481486
}
482487
// With fixTrailingSlash = false
483488
for _, route := range routes {
484-
out, found := tree.FindCaseInsensitivePath(route, false)
489+
found := tree.FindCaseInsensitivePath(route, false, buf)
485490
if !found {
486491
t.Errorf("Route '%s' not found!", route)
487-
} else if string(out) != route {
488-
t.Errorf("Wrong result for route '%s': %s", route, string(out))
492+
} else if out := buf.String(); out != route {
493+
t.Errorf("Wrong result for route '%s': %s", route, out)
489494
}
495+
496+
buf.Reset()
490497
}
491498

492499
tests := []struct {
@@ -514,6 +521,7 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
514521
{"/RegEx/a1b2_test/DaTA", "/regex/a1b2_test/data", true, false},
515522
{"/RegEx/A1B2_test/DaTA/", "/regex/A1B2_test/data", true, true},
516523
{"/RegEx/blabla/DaTA/", "", false, false},
524+
{"/RegEx/blabla_test/fail", "", false, false},
517525
{"/x/Y", "/x/y", true, false},
518526
{"/x/Y/", "/x/y", true, true},
519527
{"/X/y", "/x/y", true, false},
@@ -559,25 +567,29 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
559567
}
560568
// With fixTrailingSlash = true
561569
for _, test := range tests {
562-
out, found := tree.FindCaseInsensitivePath(test.in, true)
563-
if found != test.found || (found && (string(out) != test.out)) {
570+
found := tree.FindCaseInsensitivePath(test.in, true, buf)
571+
if out := buf.String(); found != test.found || (found && (out != test.out)) {
564572
t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
565573
test.in, string(out), found, test.out, test.found)
566574
}
575+
576+
buf.Reset()
567577
}
568578
// With fixTrailingSlash = false
569579
for _, test := range tests {
570-
out, found := tree.FindCaseInsensitivePath(test.in, false)
580+
found := tree.FindCaseInsensitivePath(test.in, false, buf)
571581
if test.slash {
572582
if found { // test needs a trailingSlash fix. It must not be found!
573-
t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out))
583+
t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, buf.String())
574584
}
575585
} else {
576-
if found != test.found || (found && (string(out) != test.out)) {
586+
if out := buf.String(); found != test.found || (found && (out != test.out)) {
577587
t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
578-
test.in, string(out), found, test.out, test.found)
588+
test.in, out, found, test.out, test.found)
579589
}
580590
}
591+
592+
buf.Reset()
581593
}
582594
}
583595

@@ -599,9 +611,11 @@ func TestTreeInvalidNodeType(t *testing.T) {
599611
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
600612
}
601613

614+
buf := bytebufferpool.Get()
615+
602616
// case-insensitive lookup
603617
recv = catchPanic(func() {
604-
tree.FindCaseInsensitivePath("/test", true)
618+
tree.FindCaseInsensitivePath("/test", true, buf)
605619
})
606620
if rs, ok := recv.(string); !ok || rs != panicMsg {
607621
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)

radix/tree.go

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package radix
33
import (
44
"strings"
55

6+
"github.com/valyala/bytebufferpool"
67
"github.com/valyala/fasthttp"
78
)
89

@@ -111,24 +112,14 @@ func (t *Tree) Get(path string, ctx *fasthttp.RequestCtx) (fasthttp.RequestHandl
111112
// It can optionally also fix trailing slashes.
112113
// It returns the case-corrected path and a bool indicating whether the lookup
113114
// was successful.
114-
func (t *Tree) FindCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
115-
// Use a static sized buffer on the stack in the common case.
116-
// If the path is too long, allocate a buffer on the heap instead.
117-
buf := make([]byte, 0, stackBufSize)
118-
if l := len(path) + 1; l > stackBufSize {
119-
buf = make([]byte, 0, l)
120-
}
121-
122-
tsr := false
115+
func (t *Tree) FindCaseInsensitivePath(path string, fixTrailingSlash bool, buf *bytebufferpool.ByteBuffer) bool {
116+
found, tsr := t.root.find(path, buf)
123117

124-
buf, tsr = t.root.find(path, buf)
118+
if !found || (tsr && !fixTrailingSlash) {
119+
buf.Reset()
125120

126-
switch {
127-
case buf == nil:
128-
return nil, false
129-
case tsr && !fixTrailingSlash:
130-
return nil, false
121+
return false
131122
}
132123

133-
return buf, true
124+
return true
134125
}

radix/tree_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/savsgio/gotils"
8+
"github.com/valyala/bytebufferpool"
89
"github.com/valyala/fasthttp"
910
)
1011

@@ -283,9 +284,11 @@ func Benchmark_FindCaseInsensitivePath(b *testing.B) {
283284
tree := New()
284285
tree.Add("/endpoint", generateHandler())
285286

287+
buf := bytebufferpool.Get()
288+
286289
b.ResetTimer()
287290

288291
for i := 0; i < b.N; i++ {
289-
tree.FindCaseInsensitivePath("/ENdpOiNT", false)
292+
tree.FindCaseInsensitivePath("/ENdpOiNT", false, buf)
290293
}
291294
}

radix/utils.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"regexp"
55
"strings"
66
"unicode/utf8"
7+
8+
"github.com/valyala/bytebufferpool"
79
)
810

911
func min(a, b int) int {
@@ -13,6 +15,10 @@ func min(a, b int) int {
1315
return b
1416
}
1517

18+
func bufferRemoveString(buf *bytebufferpool.ByteBuffer, s string) {
19+
buf.B = buf.B[:len(buf.B)-len(s)]
20+
}
21+
1622
// func isIndexEqual(a, b string) bool {
1723
// ra, _ := utf8.DecodeRuneInString(a)
1824
// rb, _ := utf8.DecodeRuneInString(b)

router.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ import (
8383

8484
var (
8585
defaultContentType = []byte("text/plain; charset=utf-8")
86-
questionMark = []byte("?")
86+
questionMark = byte('?')
8787
)
8888

8989
// MatchedRoutePathParam is the param name under which the path of the matched
@@ -462,7 +462,7 @@ func (r *Router) Handler(ctx *fasthttp.RequestCtx) {
462462

463463
queryBuf := ctx.URI().QueryString()
464464
if len(queryBuf) > 0 {
465-
uri.Write(questionMark)
465+
uri.WriteByte(questionMark)
466466
uri.Write(queryBuf)
467467
}
468468

@@ -474,18 +474,24 @@ func (r *Router) Handler(ctx *fasthttp.RequestCtx) {
474474

475475
// Try to fix the request path
476476
if r.RedirectFixedPath {
477-
fixedPath, found := root.FindCaseInsensitivePath(
477+
uri := bytebufferpool.Get()
478+
found := root.FindCaseInsensitivePath(
478479
CleanPath(path),
479480
r.RedirectTrailingSlash,
481+
uri,
480482
)
483+
481484
if found {
482485
queryBuf := ctx.URI().QueryString()
483486
if len(queryBuf) > 0 {
484-
fixedPath = append(fixedPath, questionMark...)
485-
fixedPath = append(fixedPath, queryBuf...)
487+
uri.WriteByte(questionMark)
488+
uri.Write(queryBuf)
486489
}
487490

488-
ctx.RedirectBytes(fixedPath, code)
491+
ctx.RedirectBytes(uri.Bytes(), code)
492+
493+
bytebufferpool.Put(uri)
494+
489495
return
490496
}
491497
}

0 commit comments

Comments
 (0)