diff --git a/go.mod b/go.mod index 2fd25e8..f7a9b6e 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,38 @@ module github.com/tigerwill90/fox go 1.19 require ( + github.com/gin-gonic/gin v1.9.0 github.com/google/gofuzz v1.2.0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.2 ) require ( + github.com/bytedance/sonic v1.8.6 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.12.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/kr/pretty v0.3.0 // indirect + github.com/leodido/go-urn v1.2.2 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a26abda..bc886df 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,37 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.8.6 h1:aUgO9S8gvdN6SyW2EhIpAw5E4ChworywIEndZCkCVXk= +github.com/bytedance/sonic v1.8.6/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= +github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= +github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -12,17 +40,55 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4= +github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= +github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -31,3 +97,4 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/node.go b/node.go index 2e29ff9..1022750 100644 --- a/node.go +++ b/node.go @@ -2,6 +2,7 @@ package fox import ( "sort" + "strconv" "strings" "sync/atomic" ) @@ -31,35 +32,38 @@ type node struct { // each pointer reference to a new child node starting with the same character. children []atomic.Pointer[node] - // Indicate whether its child node is a param node type. If true, len(children) == 1. - // Once assigned, paramChild is immutable. - paramChild bool + // The index of a paramChild if any, -1 if none (per rules, only one paramChildren is allowed). + paramChildIndex int } -func newNode(key string, handler Handler, children []*node, catchAllKey string, paramChild bool, path string) *node { +func newNode(key string, handler Handler, children []*node, catchAllKey string, path string) *node { sort.Slice(children, func(i, j int) bool { return children[i].key < children[j].key }) nds := make([]atomic.Pointer[node], len(children)) childKeys := make([]byte, len(children)) + childIndex := -1 for i := range children { assertNotNil(children[i]) childKeys[i] = children[i].key[0] nds[i].Store(children[i]) + if strings.HasPrefix(children[i].key, "{") { + childIndex = i + } } - return newNodeFromRef(key, handler, nds, childKeys, catchAllKey, paramChild, path) + return newNodeFromRef(key, handler, nds, childKeys, catchAllKey, childIndex, path) } -func newNodeFromRef(key string, handler Handler, children []atomic.Pointer[node], childKeys []byte, catchAllKey string, paramChild bool, path string) *node { +func newNodeFromRef(key string, handler Handler, children []atomic.Pointer[node], childKeys []byte, catchAllKey string, childIndex int, path string) *node { n := &node{ - key: key, - childKeys: childKeys, - children: children, - handler: handler, - catchAllKey: catchAllKey, - path: path, - paramChild: paramChild, + key: key, + childKeys: childKeys, + children: children, + handler: handler, + catchAllKey: catchAllKey, + path: path, + paramChildIndex: childIndex, } // TODO find a better way if catchAllKey != "" { @@ -178,8 +182,11 @@ func (n *node) string(space int) string { sb.WriteString(strings.Repeat(" ", space)) sb.WriteString("path: ") sb.WriteString(n.key) - if n.paramChild { - sb.WriteString(" [paramChild]") + + if n.paramChildIndex >= 0 { + sb.WriteString(" [paramIdx=") + sb.WriteString(strconv.Itoa(n.paramChildIndex)) + sb.WriteString("]") } if n.isCatchAll() { @@ -195,7 +202,7 @@ func (n *node) string(space int) string { children := n.getEdgesShallowCopy() for _, child := range children { sb.WriteString(" ") - sb.WriteString(child.string(space + 2)) + sb.WriteString(child.string(space + 4)) } return sb.String() } diff --git a/params.go b/params.go index a7dacb9..6715b12 100644 --- a/params.go +++ b/params.go @@ -41,7 +41,7 @@ func (p *Params) Free(t *Tree) { return } *p = (*p)[:0] - t.p.Put(p) + t.pp.Put(p) } // ParamsFromContext is a helper function to retrieve parameters from the request context. diff --git a/router.go b/router.go index 7892641..7a56e6f 100644 --- a/router.go +++ b/router.go @@ -95,16 +95,24 @@ func (fox *Router) NewTree() *Tree { for i := range commonVerbs { nds[i] = new(node) nds[i].key = commonVerbs[i] + nds[i].paramChildIndex = -1 } tree.nodes.Store(&nds) - tree.p = sync.Pool{ + tree.pp = sync.Pool{ New: func() any { params := make(Params, 0, tree.maxParams.Load()) return ¶ms }, } + tree.np = sync.Pool{ + New: func() any { + skippedNodes := make([]skippedNode, 0, tree.maxDepth.Load()) + return &skippedNodes + }, + } + return tree } @@ -386,6 +394,7 @@ type searchResult struct { path string charsMatched int charsMatchedInNodeFound int + depth uint32 } func min(a, b int) int { diff --git a/router_test.go b/router_test.go index 56fe787..f147a7d 100644 --- a/router_test.go +++ b/router_test.go @@ -2,6 +2,7 @@ package fox import ( "fmt" + "github.com/gin-gonic/gin" fuzz "github.com/google/gofuzz" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -40,6 +41,212 @@ type route struct { path string } +var ginOverlappingRoutes = []route{ + {"GET", "/foo/abc/id_:id/xyz"}, + {"GET", "/foo/:name/id_:id/:name"}, + {"GET", "/foo/:name/id_:id/xyz"}, +} + +var overlappingRoutes = []route{ + {"GET", "/foo/abc/id:{id}/xyz"}, + {"GET", "/foo/{name}/id:{id}/{name}"}, + {"GET", "/foo/{name}/id:{id}/xyz"}, +} + +var ginGithubRoutes = []route{ + {"GET", "/repos/:owner/:repo/subscription"}, + {"PUT", "/repos/:owner/:repo/subscription"}, + {"DELETE", "/repos/:owner/:repo/subscription"}, + {"GET", "/user/subscriptions/:owner/:repo"}, + {"PUT", "/user/subscriptions/:owner/:repo"}, + {"DELETE", "/user/subscriptions/:owner/:repo"}, + + // Gists + {"GET", "/users/:user/gists"}, + {"GET", "/gists"}, + {"GET", "/gists/:id"}, + {"POST", "/gists"}, + {"PUT", "/gists/:id/star"}, + {"DELETE", "/gists/:id/star"}, + {"GET", "/gists/:id/star"}, + {"POST", "/gists/:id/forks"}, + {"DELETE", "/gists/:id"}, + + // Git Data + {"GET", "/repos/:owner/:repo/git/blobs/:sha"}, + {"POST", "/repos/:owner/:repo/git/blobs"}, + {"GET", "/repos/:owner/:repo/git/commits/:sha"}, + {"POST", "/repos/:owner/:repo/git/commits"}, + {"GET", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/refs"}, + {"POST", "/repos/:owner/:repo/git/refs"}, + {"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/tags/:sha"}, + {"POST", "/repos/:owner/:repo/git/tags"}, + {"GET", "/repos/:owner/:repo/git/trees/:sha"}, + {"POST", "/repos/:owner/:repo/git/trees"}, + + // Issues + {"GET", "/issues"}, + {"GET", "/user/issues"}, + {"GET", "/orgs/:org/issues"}, + {"GET", "/repos/:owner/:repo/issues"}, + {"GET", "/repos/:owner/:repo/issues/:number"}, + {"POST", "/repos/:owner/:repo/issues"}, + {"GET", "/repos/:owner/:repo/assignees"}, + {"GET", "/repos/:owner/:repo/assignees/:assignee"}, + {"GET", "/repos/:owner/:repo/issues/:number/comments"}, + {"POST", "/repos/:owner/:repo/issues/:number/comments"}, + {"GET", "/repos/:owner/:repo/issues/:number/events"}, + {"GET", "/repos/:owner/:repo/labels"}, + {"GET", "/repos/:owner/:repo/labels/:name"}, + {"POST", "/repos/:owner/:repo/labels"}, + {"DELETE", "/repos/:owner/:repo/labels/:name"}, + {"GET", "/repos/:owner/:repo/issues/:number/labels"}, + {"POST", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, + {"PUT", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones"}, + {"GET", "/repos/:owner/:repo/milestones/:number"}, + {"POST", "/repos/:owner/:repo/milestones"}, + {"DELETE", "/repos/:owner/:repo/milestones/:number"}, + + // Miscellaneous + {"GET", "/emojis"}, + {"GET", "/gitignore/templates"}, + {"GET", "/gitignore/templates/:name"}, + {"POST", "/markdown"}, + {"POST", "/markdown/raw"}, + {"GET", "/meta"}, + {"GET", "/rate_limit"}, + + // Organizations + {"GET", "/users/:user/orgs"}, + {"GET", "/user/orgs"}, + {"GET", "/orgs/:org"}, + {"GET", "/orgs/:org/members"}, + {"GET", "/orgs/:org/members/:user"}, + {"DELETE", "/orgs/:org/members/:user"}, + {"GET", "/orgs/:org/public_members"}, + {"GET", "/orgs/:org/public_members/:user"}, + {"PUT", "/orgs/:org/public_members/:user"}, + {"DELETE", "/orgs/:org/public_members/:user"}, + {"GET", "/orgs/:org/teams"}, + {"GET", "/teams/:id"}, + {"POST", "/orgs/:org/teams"}, + {"DELETE", "/teams/:id"}, + {"GET", "/teams/:id/members"}, + {"GET", "/teams/:id/members/:user"}, + {"PUT", "/teams/:id/members/:user"}, + {"DELETE", "/teams/:id/members/:user"}, + {"GET", "/teams/:id/repos"}, + {"GET", "/teams/:id/repos/:owner/:repo"}, + {"PUT", "/teams/:id/repos/:owner/:repo"}, + {"DELETE", "/teams/:id/repos/:owner/:repo"}, + {"GET", "/user/teams"}, + + // Pull Requests + {"GET", "/repos/:owner/:repo/pulls"}, + {"GET", "/repos/:owner/:repo/pulls/:number"}, + {"POST", "/repos/:owner/:repo/pulls"}, + {"GET", "/repos/:owner/:repo/pulls/:number/commits"}, + {"GET", "/repos/:owner/:repo/pulls/:number/files"}, + {"GET", "/repos/:owner/:repo/pulls/:number/merge"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, + {"GET", "/repos/:owner/:repo/pulls/:number/comments"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, + + // Repositories + {"GET", "/user/repos"}, + {"GET", "/users/:user/repos"}, + {"GET", "/orgs/:org/repos"}, + {"GET", "/repositories"}, + {"POST", "/user/repos"}, + {"POST", "/orgs/:org/repos"}, + {"GET", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/contributors"}, + {"GET", "/repos/:owner/:repo/languages"}, + {"GET", "/repos/:owner/:repo/teams"}, + {"GET", "/repos/:owner/:repo/tags"}, + {"GET", "/repos/:owner/:repo/branches"}, + {"GET", "/repos/:owner/:repo/branches/:branch"}, + {"DELETE", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/collaborators"}, + {"GET", "/repos/:owner/:repo/collaborators/:user"}, + {"PUT", "/repos/:owner/:repo/collaborators/:user"}, + {"DELETE", "/repos/:owner/:repo/collaborators/:user"}, + {"GET", "/repos/:owner/:repo/comments"}, + {"GET", "/repos/:owner/:repo/commits/:sha/comments"}, + {"POST", "/repos/:owner/:repo/commits/:sha/comments"}, + {"GET", "/repos/:owner/:repo/comments/:id"}, + {"DELETE", "/repos/:owner/:repo/comments/:id"}, + {"GET", "/repos/:owner/:repo/commits"}, + {"GET", "/repos/:owner/:repo/commits/:sha"}, + {"GET", "/repos/:owner/:repo/readme"}, + {"GET", "/repos/:owner/:repo/contents/*path"}, + {"DELETE", "/repos/:owner/:repo/contents/*path"}, + {"GET", "/repos/:owner/:repo/keys"}, + {"GET", "/repos/:owner/:repo/keys/:id"}, + {"POST", "/repos/:owner/:repo/keys"}, + {"DELETE", "/repos/:owner/:repo/keys/:id"}, + {"GET", "/repos/:owner/:repo/downloads"}, + {"GET", "/repos/:owner/:repo/downloads/:id"}, + {"DELETE", "/repos/:owner/:repo/downloads/:id"}, + {"GET", "/repos/:owner/:repo/forks"}, + {"POST", "/repos/:owner/:repo/forks"}, + {"GET", "/repos/:owner/:repo/hooks"}, + {"GET", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/hooks"}, + {"POST", "/repos/:owner/:repo/hooks/:id/tests"}, + {"DELETE", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/merges"}, + {"GET", "/repos/:owner/:repo/releases"}, + {"GET", "/repos/:owner/:repo/releases/:id"}, + {"POST", "/repos/:owner/:repo/releases"}, + {"DELETE", "/repos/:owner/:repo/releases/:id"}, + {"GET", "/repos/:owner/:repo/releases/:id/assets"}, + {"GET", "/repos/:owner/:repo/stats/contributors"}, + {"GET", "/repos/:owner/:repo/stats/commit_activity"}, + {"GET", "/repos/:owner/:repo/stats/code_frequency"}, + {"GET", "/repos/:owner/:repo/stats/participation"}, + {"GET", "/repos/:owner/:repo/stats/punch_card"}, + {"GET", "/repos/:owner/:repo/statuses/:ref"}, + {"POST", "/repos/:owner/:repo/statuses/:ref"}, + + // Search + {"GET", "/search/repositories"}, + {"GET", "/search/code"}, + {"GET", "/search/issues"}, + {"GET", "/search/users"}, + {"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, + {"GET", "/legacy/repos/search/:keyword"}, + {"GET", "/legacy/user/search/:keyword"}, + {"GET", "/legacy/user/email/:email"}, + + // Users + {"GET", "/users/:user"}, + {"GET", "/user"}, + {"GET", "/users"}, + {"GET", "/user/emails"}, + {"POST", "/user/emails"}, + {"DELETE", "/user/emails"}, + {"GET", "/users/:user/followers"}, + {"GET", "/user/followers"}, + {"GET", "/users/:user/following"}, + {"GET", "/user/following"}, + {"GET", "/user/following/:user"}, + {"GET", "/users/:user/following/:target_user"}, + {"PUT", "/user/following/:user"}, + {"DELETE", "/user/following/:user"}, + {"GET", "/users/:user/keys"}, + {"GET", "/user/keys"}, + {"GET", "/user/keys/:id"}, + {"POST", "/user/keys"}, + {"DELETE", "/user/keys/:id"}, +} + // From https://github.com/julienschmidt/go-http-routing-benchmark var staticRoutes = []route{ {"GET", "/"}, @@ -476,6 +683,16 @@ func BenchmarkStaticAll(b *testing.B) { benchRoutes(b, r, staticRoutes) } +func BenchmarkGinStaticAll(b *testing.B) { + gin.SetMode(gin.ReleaseMode) + r := gin.New() + for _, route := range staticRoutes { + r.GET(route.path, func(context *gin.Context) {}) + } + + benchRoutes(b, r, staticRoutes) +} + func BenchmarkLookup(b *testing.B) { r := New() for _, route := range staticRoutes { @@ -513,6 +730,60 @@ func BenchmarkGithubParamsAll(b *testing.B) { } } +func BenchmarkGinGithubParamsAll(b *testing.B) { + gin.SetMode(gin.ReleaseMode) + r := gin.New() + for _, route := range ginGithubRoutes { + r.Handle(route.method, route.path, func(context *gin.Context) {}) + } + + req := httptest.NewRequest("GET", "/repos/sylvain/fox/hooks/1500", 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 { + require.NoError(b, r.Tree().Handler(route.method, route.path, HandlerFunc(func(w http.ResponseWriter, r *http.Request, p Params) {}))) + } + + req := httptest.NewRequest("GET", "/foo/abc/id:123/xy", nil) + w := new(mockResponseWriter) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + r.ServeHTTP(w, req) + } +} + +func BenchmarkGinOverlappingRoute(b *testing.B) { + gin.SetMode(gin.ReleaseMode) + r := gin.New() + + for _, route := range ginOverlappingRoutes { + r.Handle(route.method, route.path, func(context *gin.Context) {}) + } + + req := httptest.NewRequest("GET", "/foo/abc/id_123/xy", nil) + w := new(mockResponseWriter) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + r.ServeHTTP(w, req) + } +} + func BenchmarkStaticParallel(b *testing.B) { r := New() for _, route := range staticRoutes { diff --git a/tree.go b/tree.go index 1529f3f..a21e99b 100644 --- a/tree.go +++ b/tree.go @@ -29,10 +29,12 @@ import ( // calling params.Free(tree). Always ensure that the Tree pointer passed as a parameter to params.Free is the same // as the one passed to the Lookup function. type Tree struct { - p sync.Pool + pp sync.Pool + np sync.Pool nodes atomic.Pointer[[]*node] sync.Mutex maxParams atomic.Uint32 + maxDepth atomic.Uint32 saveRoute bool } @@ -119,7 +121,7 @@ func (t *Tree) insert(method, path, catchAllKey string, paramsN uint32, handler } // We are updating an existing node. We only need to create a new node from // the matched one with the updated/added value (handler and wildcard). - n := newNodeFromRef(result.matched.key, handler, result.matched.children, result.matched.childKeys, catchAllKey, result.matched.paramChild, path) + n := newNodeFromRef(result.matched.key, handler, result.matched.children, result.matched.childKeys, catchAllKey, result.matched.paramChildIndex, path) t.updateMaxParams(paramsN) result.p.updateEdge(n) @@ -154,7 +156,7 @@ func (t *Tree) insert(method, path, catchAllKey string, paramsN uint32, handler result.matched.children, result.matched.childKeys, result.matched.catchAllKey, - result.matched.paramChild, + result.matched.paramChildIndex, result.matched.path, ) @@ -163,18 +165,11 @@ func (t *Tree) insert(method, path, catchAllKey string, paramsN uint32, handler handler, []*node{child}, catchAllKey, - // e.g. tree encode /tes/{t} and insert /tes/ - // /tes/ (paramChild) - // ├── {t} - // since /tes/xyz will match until /tes/ and when looking for next child, 'x' will match nothing - // if paramChild == true { - // next = current.get(0) - // } - strings.HasPrefix(suffixFromExistingEdge, "{"), path, ) t.updateMaxParams(paramsN) + t.updateMaxDepth(result.depth + 1) result.p.updateEdge(parent) case incompleteMatchToEndOfEdge: // e.g. matched until "st" for "st" node but still have remaining char (ify) when inserting "testify" key. @@ -198,7 +193,7 @@ func (t *Tree) insert(method, path, catchAllKey string, paramsN uint32, handler keySuffix := path[result.charsMatched:] // No children, so no paramChild - child := newNode(keySuffix, handler, nil, catchAllKey, false, path) + child := newNode(keySuffix, handler, nil, catchAllKey, path) edges := result.matched.getEdgesShallowCopy() edges = append(edges, child) n := newNode( @@ -206,20 +201,15 @@ func (t *Tree) insert(method, path, catchAllKey string, paramsN uint32, handler result.matched.handler, edges, result.matched.catchAllKey, - // e.g. tree encode /tes/ and insert /tes/{t} - // /tes/ (paramChild) - // ├── {t} - // since /tes/xyz will match until /tes/ and when looking for next child, 'x' will match nothing - // if paramChild == true { - // next = current.get(0) - // } - strings.HasPrefix(keySuffix, "{"), result.matched.path, ) + t.updateMaxDepth(result.depth + 1) t.updateMaxParams(paramsN) + if result.matched == rootNode { n.key = method + n.paramChildIndex = -1 t.updateRoot(n) break } @@ -259,31 +249,32 @@ func (t *Tree) insert(method, path, catchAllKey string, paramsN uint32, handler suffixFromExistingEdge := strings.TrimPrefix(result.matched.key, cPrefix) // Rule: parent's of a node with {param} have only one node or are prefixed by a char (e.g /{param}) - if strings.HasPrefix(suffixFromExistingEdge, "{") { - return newConflictErr(method, path, catchAllKey, getRouteConflict(result.matched)) - } + /* if strings.HasPrefix(suffixFromExistingEdge, "{") { + return newConflictErr(method, path, catchAllKey, getRouteConflict(result.matched)) + }*/ keySuffix := path[result.charsMatched:] // Rule: parent's of a node with {param} have only one node or are prefixed by a char (e.g /{param}) - if strings.HasPrefix(keySuffix, "{") { - return newConflictErr(method, path, catchAllKey, getRouteConflict(result.matched)) - } + /* if strings.HasPrefix(keySuffix, "{") { + return newConflictErr(method, path, catchAllKey, getRouteConflict(result.matched)) + }*/ // No children, so no paramChild - n1 := newNodeFromRef(keySuffix, handler, nil, nil, catchAllKey, false, path) // inserted node + n1 := newNodeFromRef(keySuffix, handler, nil, nil, catchAllKey, -1, path) // inserted node n2 := newNodeFromRef( suffixFromExistingEdge, result.matched.handler, result.matched.children, result.matched.childKeys, result.matched.catchAllKey, - result.matched.paramChild, + result.matched.paramChildIndex, result.matched.path, ) // previous matched node // n3 children never start with a param - n3 := newNode(cPrefix, nil, []*node{n1, n2}, "", false, "") // intermediary node + n3 := newNode(cPrefix, nil, []*node{n1, n2}, "", "") // intermediary node + t.updateMaxDepth(result.depth + 1) t.updateMaxParams(paramsN) result.p.updateEdge(n3) default: @@ -319,7 +310,7 @@ func (t *Tree) update(method string, path, catchAllKey string, handler Handler) result.matched.children, result.matched.childKeys, catchAllKey, - result.matched.paramChild, + result.matched.paramChildIndex, path, ) result.p.updateEdge(n) @@ -352,7 +343,7 @@ func (t *Tree) remove(method, path string) bool { result.matched.children, result.matched.childKeys, "", - result.matched.paramChild, + result.matched.paramChildIndex, "", ) result.p.updateEdge(n) @@ -368,7 +359,7 @@ func (t *Tree) remove(method, path string) bool { child.children, child.childKeys, child.catchAllKey, - child.paramChild, + child.paramChildIndex, child.path, ) result.p.updateEdge(n) @@ -397,7 +388,7 @@ func (t *Tree) remove(method, path string) bool { child.children, child.childKeys, child.catchAllKey, - child.paramChild, + child.paramChildIndex, child.path, ) } else { @@ -406,7 +397,6 @@ func (t *Tree) remove(method, path string) bool { result.p.handler, parentEdges, result.p.catchAllKey, - result.p.paramChild, result.p.path, ) } @@ -416,6 +406,7 @@ func (t *Tree) remove(method, path string) bool { return t.removeRoot(method) } parent.key = method + parent.paramChildIndex = -1 t.updateRoot(parent) return true } @@ -424,30 +415,44 @@ func (t *Tree) remove(method, path string) bool { return true } +type skippedNode struct { + node *node + pathIndex int +} + func (t *Tree) lookup(rootNode *node, path string, lazy bool) (n *node, params *Params, tsr bool) { + if len(rootNode.children) == 0 { + return nil, nil, false + } + var ( charsMatched int charsMatchedInNodeFound int + paramCnt int + skippedNodes *[]skippedNode ) - current := rootNode -STOP: - for charsMatched < len(path) { - idx := linearSearch(current.childKeys, path[charsMatched]) - if idx < 0 { - if !current.paramChild { - break - } - idx = 0 - } + current := rootNode.children[0].Load() - current = current.children[idx].Load() +walk: + for charsMatched < len(path) { charsMatchedInNodeFound = 0 for i := 0; charsMatched < len(path); i++ { if i >= len(current.key) { break } + /* tmp1 := string(path[charsMatched]) + _ = tmp1 + tmp2 := string(current.key[i]) + _ = tmp2 + + tmp3 := charsMatchedInNodeFound + _ = tmp3 + + tmp4 := charsMatched + _ = tmp4*/ + if current.key[i] != path[charsMatched] || path[charsMatched] == '{' { if current.key[i] == '{' { startPath := charsMatched @@ -460,8 +465,9 @@ STOP: charsMatched += len(path[charsMatched:]) } else { // segment is empty - break STOP + break walk } + startKey := charsMatchedInNodeFound idx = strings.IndexByte(current.key[startKey:], '/') if idx >= 0 { @@ -473,25 +479,83 @@ STOP: i += len(current.key[charsMatchedInNodeFound:]) - 1 charsMatchedInNodeFound += len(current.key[charsMatchedInNodeFound:]) } + if !lazy { if params == nil { params = t.newParams() } + paramCnt++ // :n where n > 0 *params = append(*params, Param{Key: current.key[startKey+1 : charsMatchedInNodeFound-1], Value: path[startPath:charsMatched]}) } + continue } - break STOP + + break walk } charsMatched++ charsMatchedInNodeFound++ } + + if charsMatched < len(path) { + // linear search + idx := -1 + for i := 0; i < len(current.childKeys); i++ { + if current.childKeys[i] == path[charsMatched] { + idx = i + break + } + } + + if idx < 0 { + if current.paramChildIndex < 0 { + break + } + // child param: go deeper and since the child param is evaluated + // now, no need to backtrack later. + idx = current.paramChildIndex + current = current.children[idx].Load() + continue + } + + if current.paramChildIndex >= 0 && len(current.children) > 1 { + if skippedNodes == nil { + skippedNodes = t.newSkippedNods() + } + *skippedNodes = append(*skippedNodes, skippedNode{current, charsMatched}) + paramCnt = 0 + } + current = current.children[idx].Load() + } } if !current.isLeaf() { - return nil, params, false + if skippedNodes != nil && len(*skippedNodes) > 0 { + skipped := (*skippedNodes)[len(*skippedNodes)-1] + current = skipped.node.children[skipped.node.paramChildIndex].Load() + // pop + *skippedNodes = (*skippedNodes)[:len(*skippedNodes)-1] + + /* pp := path[skipped.pathIndex:] + _ = pp + */ + if params != nil { + *params = (*params)[:len(*params)-paramCnt] + } + charsMatched = skipped.pathIndex + paramCnt = 0 + goto walk + } + + if params != nil { + params.Free(t) + } + if skippedNodes != nil { + t.np.Put(skippedNodes) + } + return nil, nil, false } if charsMatched == len(path) { @@ -510,14 +574,49 @@ STOP: *params = append(*params, Param{Key: current.catchAllKey, Value: path[charsMatched:]}) } + if skippedNodes != nil { + t.np.Put(skippedNodes) + } return current, params, false } + + if skippedNodes != nil { + t.np.Put(skippedNodes) + } return current, params, false } else if charsMatchedInNodeFound < len(current.key) { // Key end mid-edge // Tsr recommendation: add an extra trailing slash (got an exact match) - remainingSuffix := current.key[charsMatchedInNodeFound:] - return nil, nil, len(remainingSuffix) == 1 && remainingSuffix[0] == '/' + + if !tsr { + remainingSuffix := current.key[charsMatchedInNodeFound:] + tsr = len(remainingSuffix) == 1 && remainingSuffix[0] == '/' + } + + if skippedNodes != nil && len(*skippedNodes) > 0 { + skipped := (*skippedNodes)[len(*skippedNodes)-1] + current = skipped.node.children[skipped.node.paramChildIndex].Load() + // pop + *skippedNodes = (*skippedNodes)[:len(*skippedNodes)-1] + + /* pp := path[skipped.pathIndex:] + _ = pp*/ + + if params != nil { + *params = (*params)[:len(*params)-paramCnt] + } + charsMatched = skipped.pathIndex + paramCnt = 0 + goto walk + } + + if params != nil { + params.Free(t) + } + if skippedNodes != nil { + t.np.Put(skippedNodes) + } + return nil, nil, tsr } } @@ -537,12 +636,62 @@ STOP: // Same as exact match, no tsr recommendation return current, params, false } + // Tsr recommendation: remove the extra trailing slash (got an exact match) - remainingKeySuffix := path[charsMatched:] - return nil, nil, len(remainingKeySuffix) == 1 && remainingKeySuffix[0] == '/' + if !tsr { + remainingKeySuffix := path[charsMatched:] + tsr = len(remainingKeySuffix) == 1 && remainingKeySuffix[0] == '/' + } + + if skippedNodes != nil && len(*skippedNodes) > 0 { + skipped := (*skippedNodes)[len(*skippedNodes)-1] + current = skipped.node.children[skipped.node.paramChildIndex].Load() + // pop + *skippedNodes = (*skippedNodes)[:len(*skippedNodes)-1] + + /* pp := path[skipped.pathIndex:] + _ = pp*/ + + if params != nil { + *params = (*params)[:len(*params)-paramCnt] + } + charsMatched = skipped.pathIndex + paramCnt = 0 + goto walk + } + + if skippedNodes != nil { + t.np.Put(skippedNodes) + } + return nil, nil, tsr + } + + if skippedNodes != nil && len(*skippedNodes) > 0 { + skipped := (*skippedNodes)[len(*skippedNodes)-1] + current = skipped.node.children[skipped.node.paramChildIndex].Load() + // pop + *skippedNodes = (*skippedNodes)[:len(*skippedNodes)-1] + + /* pp := path[skipped.pathIndex:] + _ = pp*/ + + if params != nil { + *params = (*params)[:len(*params)-paramCnt] + } + charsMatched = skipped.pathIndex + paramCnt = 0 + goto walk } - return nil, nil, false + if params != nil { + params.Free(t) + } + + if skippedNodes != nil { + t.np.Put(skippedNodes) + } + + return nil, nil, tsr } func (t *Tree) search(rootNode *node, path string) searchResult { @@ -553,6 +702,7 @@ func (t *Tree) search(rootNode *node, path string) searchResult { p *node charsMatched int charsMatchedInNodeFound int + depth uint32 ) STOP: @@ -562,6 +712,7 @@ STOP: break STOP } + depth++ pp = p p = current current = next @@ -587,6 +738,7 @@ STOP: charsMatchedInNodeFound: charsMatchedInNodeFound, p: p, pp: pp, + depth: depth, } } @@ -636,10 +788,22 @@ func (t *Tree) updateMaxParams(max uint32) { } } +// updateMaxDepth perform an update only if max is greater than the current +// max depth. This function should be guarded my mutex. +func (t *Tree) updateMaxDepth(max uint32) { + if max > t.maxDepth.Load() { + t.maxDepth.Store(max) + } +} + func (t *Tree) load() []*node { return *t.nodes.Load() } func (t *Tree) newParams() *Params { - return t.p.Get().(*Params) + return t.pp.Get().(*Params) +} + +func (t *Tree) newSkippedNods() *[]skippedNode { + return t.np.Get().(*[]skippedNode) } diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 0000000..a3293d9 --- /dev/null +++ b/tree_test.go @@ -0,0 +1,173 @@ +package fox + +import ( + "fmt" + "github.com/stretchr/testify/require" + "net/http" + "testing" +) + +func TestX(t *testing.T) { + tree := New().Tree() + + /* + path: GET + path: /foo/ [paramIdx=1] + path: abc/{yolo} [leaf=/foo/abc/{yolo}] + path: /boom [leaf=/foo/abc/{yolo}/boom] + path: {yo}/{yolo} [leaf=/foo/{yo}/{yolo}] + path: /{id} [leaf=/foo/{yo}/{yolo}/{id}] + */ + + require.NoError(t, tree.insert("GET", "/foo/abc", "", 1, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/abc/{yolo}", "", 1, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/{yo}/{yolo}", "", 1, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/abc/{yolo}/boom", "", 1, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/{yo}/{yolo}/{id}", "", 1, emptyHandler)) + + nds := tree.load() + fmt.Println(nds[0]) + // barr/ + n, ps, tsr := tree.lookup(nds[0], "/foo/abc/123/boom/", false) + fmt.Println("matched") + fmt.Println(n) + fmt.Println(ps) + fmt.Println(tsr) +} + +func TestY(t *testing.T) { + tree := New().Tree() + + /* + path: GET + path: /foo/{ab} [leaf=/foo/{ab}] + path: /{bc} [leaf=/foo/{ab}/{bc}] + path: /{de} [leaf=/foo/{ab}/{bc}/{de}] + path: / [paramIdx=1] + path: boom [leaf=/foo/{ab}/{bc}/{de}/boom] + path: {fg} [leaf=/foo/{ab}/{bc}/{de}/{fg}] + */ + + require.NoError(t, tree.insert("GET", "/foo/{ab}", "", 1, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/{ab}/{bc}", "", 1, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/{ab}/{bc}/{de}", "", 3, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/{ab}/{bc}/{de}/boom", "", 3, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/{ab}/{bc}/{de}/{fg}", "", 1, emptyHandler)) + + nds := tree.load() + fmt.Println(nds[0]) + + fmt.Println("depth =", tree.maxDepth.Load()) + + // barr/ + n, ps, tsr := tree.lookup(nds[0], "/foo/ab/bc/de/boom", false) + fmt.Println("matched") + fmt.Println(n) + fmt.Println(ps) + fmt.Println(tsr) +} + +func TestZ(t *testing.T) { + tree := New().Tree() + + /* + path: GET + path: /foo/ [paramIdx=1] + path: eee/ [paramIdx=1] [leaf=/foo/eee/] + path: baz/bar [leaf=/foo/eee/baz/bar] + path: {aa}/{yolo} [leaf=/foo/eee/{aa}/{yolo}] + path: {aa}/abc/foo [leaf=/foo/{aa}/abc/foo] + */ + + require.NoError(t, tree.insert("GET", "/foo/{aa}/abc/foo", "", 1, emptyHandler)) + // require.NoError(t, tree.insert("GET", "/foo/eee/{aa}/yolo", "", 1, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/eee/{aa}/{yolo}", "", 1, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/eee/baz/bar/", "", 1, emptyHandler)) + require.NoError(t, tree.insert("GET", "/foo/eee/", "", 1, emptyHandler)) + + nds := tree.load() + fmt.Println(nds[0]) + + fmt.Println("depth =", tree.maxDepth.Load()) + + n, ps, tsr := tree.lookup(nds[0], "/foo/er/abc/foo", false) + fmt.Println("matched") + fmt.Println(n) + fmt.Println(ps) + fmt.Println(tsr) +} + +func TestA(t *testing.T) { + tree := New().Tree() + for _, route := range overlappingRoutes { + require.NoError(t, tree.Handler(route.method, route.path, HandlerFunc(func(w http.ResponseWriter, r *http.Request, p Params) {}))) + } + + /** + path: GET + path: /foo/ [paramIdx=1] + path: abc/id:{id}/xyz [leaf=/foo/abc/id:{id}/xyz] + path: {name}/id:{id}/ [paramIdx=1] + path: xyz [leaf=/foo/{name}/id:{id}/xyz] + path: {name} [leaf=/foo/{name}/id:{id}/{name}] + */ + + nds := tree.load() + fmt.Println(nds[0]) + n, ps, tsr := tree.lookup(nds[0], "/foo/ab/id:123/xyz", false) + fmt.Println("matched") + fmt.Println(n) + fmt.Println(ps) + fmt.Println(tsr) + +} + +func TestO(t *testing.T) { + tree := New().Tree() + + /* + path: GET + path: /{foo}/ [paramIdx=1] + path: eee/ [paramIdx=0] [leaf=/{foo}/eee/] + path: {aa}/foo/ba + path: r [leaf=/{foo}/eee/{aa}/foo/bar] + path: z [leaf=/{foo}/eee/{aa}/foo/baz] + path: {aa}/abc/foo [leaf=/{foo}/{aa}/abc/foo] + */ + + require.NoError(t, tree.Handler("GET", "/{foo}/{aa}/abc/foo", emptyHandler)) + // require.NoError(t, tree.insert("GET", "/foo/eee/{aa}/yolo", "", 1, emptyHandler)) + require.NoError(t, tree.Handler("GET", "/{foo}/eee/{aa}/foo/bar", emptyHandler)) + require.NoError(t, tree.Handler("GET", "/{foo}/eee/{aa}/foo/baz", emptyHandler)) + require.NoError(t, tree.Handler("GET", "/{foo}/eee/", emptyHandler)) + + nds := tree.load() + fmt.Println(nds[0]) + + fmt.Println("depth =", tree.maxDepth.Load()) + + n, ps, tsr := tree.lookup(nds[0], "/foo/eee/abc/foo", false) + fmt.Println("matched") + fmt.Println(n) + fmt.Println(ps) + fmt.Println(tsr) +} + +func BenchmarkO(b *testing.B) { + r := New() + require.NoError(b, r.Tree().Handler("GET", "/{foo}/{aa}/abc/foo", emptyHandler)) + require.NoError(b, r.Tree().Handler("GET", "/{foo}/eee/{aa}/yep", emptyHandler)) + require.NoError(b, r.Tree().Handler("GET", "/{foo}/eee/baz/bar/", emptyHandler)) + require.NoError(b, r.Tree().Handler("GET", "/{foo}/eee/", emptyHandler)) + + b.ReportAllocs() + b.ResetTimer() + + tree := r.Tree() + for i := 0; i < b.N; i++ { + _, ps, _ := Lookup(tree, "GET", "/foo/eee/abc/foo", false) + if ps != nil { + ps.Free(tree) + } + } +}