Skip to content

Commit

Permalink
feat: improve lookup speed for wildcard route
Browse files Browse the repository at this point in the history
  • Loading branch information
tigerwill90 committed Jul 7, 2024
1 parent 8fcce12 commit 8ec458d
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 8 deletions.
14 changes: 14 additions & 0 deletions fox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
111 changes: 107 additions & 4 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -60,17 +62,16 @@ 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,
handler: handler,
catchAllKey: catchAllKey,
path: appendCatchAll(path, catchAllKey),
paramChildIndex: childIndex,
params: parseWildcard(key),
}

return n
}

func (n *node) isLeaf() bool {
Expand All @@ -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)
Expand Down Expand Up @@ -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() {
Expand All @@ -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()
Expand Down Expand Up @@ -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
}
11 changes: 7 additions & 4 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -615,6 +615,7 @@ Walk:
idx = current.paramChildIndex
parent = current
current = current.children[idx].Load()
paramKeyCnt = 0
continue
}

Expand All @@ -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() {
Expand Down

0 comments on commit 8ec458d

Please sign in to comment.