From 8ec458d30b7b27423513fd64c02039f5c10e3b15 Mon Sep 17 00:00:00 2001 From: tigerwill90 Date: Sun, 7 Jul 2024 16:11:04 +0200 Subject: [PATCH] feat: improve lookup speed for wildcard route --- fox_test.go | 14 +++++++ node.go | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++-- tree.go | 11 ++++-- 3 files changed, 128 insertions(+), 8 deletions(-) diff --git a/fox_test.go b/fox_test.go index 450ace3..6ed42ba 100644 --- a/fox_test.go +++ b/fox_test.go @@ -507,6 +507,20 @@ func BenchmarkGithubParamsAll(b *testing.B) { } } +func BenchmarkLongParam(b *testing.B) { + r := New() + r.MustHandle(http.MethodGet, "/foo/{very_very_very_very_very_long_param}", emptyHandler) + req := httptest.NewRequest(http.MethodGet, "/foo/bar", nil) + w := new(mockResponseWriter) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + r.ServeHTTP(w, req) + } +} + func BenchmarkOverlappingRoute(b *testing.B) { r := New() for _, route := range overlappingRoutes { diff --git a/node.go b/node.go index 9927fec..211ee1f 100644 --- a/node.go +++ b/node.go @@ -36,6 +36,8 @@ type node struct { // each pointer reference to a new child node starting with the same character. children []atomic.Pointer[node] + params []param + // The index of a paramChild if any, -1 if none (per rules, only one paramChildren is allowed). paramChildIndex int } @@ -60,7 +62,7 @@ func newNode(key string, handler HandlerFunc, children []*node, catchAllKey stri } func newNodeFromRef(key string, handler HandlerFunc, children []atomic.Pointer[node], childKeys []byte, catchAllKey string, childIndex int, path string) *node { - n := &node{ + return &node{ key: key, childKeys: childKeys, children: children, @@ -68,9 +70,8 @@ func newNodeFromRef(key string, handler HandlerFunc, children []atomic.Pointer[n catchAllKey: catchAllKey, path: appendCatchAll(path, catchAllKey), paramChildIndex: childIndex, + params: parseWildcard(key), } - - return n } func (n *node) isLeaf() bool { @@ -81,6 +82,10 @@ func (n *node) isCatchAll() bool { return n.catchAllKey != "" } +func (n *node) hasWildcard() bool { + return len(n.params) > 0 +} + func (n *node) getEdge(s byte) *node { if len(n.children) <= 50 { id := linearSearch(n.childKeys, s) @@ -184,7 +189,21 @@ func (n *node) string(space int) string { if n.paramChildIndex >= 0 { sb.WriteString(" [paramIdx=") sb.WriteString(strconv.Itoa(n.paramChildIndex)) - sb.WriteString("]") + sb.WriteByte(']') + if n.hasWildcard() { + sb.WriteString(" [") + for i, param := range n.params { + if i > 0 { + sb.WriteByte(',') + } + sb.WriteString(param.key) + sb.WriteString(" (") + sb.WriteString(strconv.Itoa(param.end)) + sb.WriteString(")") + } + sb.WriteString("]") + } + } if n.isCatchAll() { @@ -195,6 +214,19 @@ func (n *node) string(space int) string { sb.WriteString(n.path) sb.WriteString("]") } + if n.hasWildcard() { + sb.WriteString(" [") + for i, param := range n.params { + if i > 0 { + sb.WriteByte(',') + } + sb.WriteString(param.key) + sb.WriteString(" (") + sb.WriteString(strconv.Itoa(param.end)) + sb.WriteString(")") + } + sb.WriteByte(']') + } sb.WriteByte('\n') children := n.getEdgesShallowCopy() @@ -229,3 +261,74 @@ func appendCatchAll(path, catchAllKey string) string { } return path } + +// param represents a parsed parameter and its end position in the path. +type param struct { + key string + end int // -1 if end with {a}, else pos of the next char + catchAll bool +} + +func parseWildcard(segment string) []param { + var params []param + + state := stateDefault + start := 0 + i := 0 + for i < len(segment) { + switch state { + case stateParam: + seg := string(segment[i]) + _ = seg + if segment[i] == '}' { + end := -1 + if len(segment[i+1:]) > 0 { + end = i + 1 + } + params = append(params, param{ + key: segment[start:i], + end: end, + }) + start = 0 + state = stateDefault + } + i++ + case stateCatchAll: + seg := string(segment[i]) + _ = seg + if segment[i] == '}' { + end := -1 + if len(segment[i+1:]) > 0 { + end = i + 1 + } + params = append(params, param{ + key: segment[start:i], + end: end, + catchAll: true, + }) + start = 0 + state = stateDefault + } + i++ + default: + seg := string(segment[i]) + _ = seg + if segment[i] == '*' { + state = stateCatchAll + i += 2 + start = i + continue + } + + if segment[i] == '{' { + state = stateParam + i++ + start = i + continue + } + i++ + } + } + + return params +} diff --git a/tree.go b/tree.go index eb8033e..77d4d18 100644 --- a/tree.go +++ b/tree.go @@ -535,6 +535,7 @@ func (t *Tree) lookup(rootNode *node, path string, c *cTx, lazy bool) (n *node, charsMatched int charsMatchedInNodeFound int paramCnt uint32 + paramKeyCnt uint32 parent *node ) @@ -564,8 +565,7 @@ Walk: break Walk } - startKey := charsMatchedInNodeFound - idx = strings.IndexByte(current.key[startKey:], slashDelim) + idx = current.params[paramKeyCnt].end - charsMatchedInNodeFound if idx >= 0 { // -1 since on the next incrementation, if any, 'i' are going to be incremented i += idx - 1 @@ -578,9 +578,9 @@ Walk: if !lazy { paramCnt++ - *c.params = append(*c.params, Param{Key: current.key[startKey+1 : charsMatchedInNodeFound-1], Value: path[startPath:charsMatched]}) + *c.params = append(*c.params, Param{Key: current.params[paramKeyCnt].key, Value: path[startPath:charsMatched]}) } - + paramKeyCnt++ continue } @@ -615,6 +615,7 @@ Walk: idx = current.paramChildIndex parent = current current = current.children[idx].Load() + paramKeyCnt = 0 continue } @@ -624,10 +625,12 @@ Walk: } parent = current current = current.children[idx].Load() + paramKeyCnt = 0 } } paramCnt = 0 + paramKeyCnt = 0 hasSkpNds := len(*c.skipNds) > 0 if !current.isLeaf() {