Improved performance
This commit is contained in:
parent
dd98b11eea
commit
99ad93e410
7
Parameter.go
Normal file
7
Parameter.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
// Parameter represents a URL parameter.
|
||||||
|
type Parameter struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
10
README.md
10
README.md
@ -59,11 +59,11 @@ coverage: 100.0% of statements
|
|||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
```
|
```
|
||||||
BenchmarkBlog/Len1-Param0-12 182590244 6.57 ns/op 0 B/op 0 allocs/op
|
BenchmarkBlog/Len1-Param0-12 200621386 5.963 ns/op 0 B/op 0 allocs/op
|
||||||
BenchmarkBlog/Len1-Param1-12 100000000 10.95 ns/op 0 B/op 0 allocs/op
|
BenchmarkBlog/Len1-Param1-12 129550375 9.224 ns/op 0 B/op 0 allocs/op
|
||||||
BenchmarkGitHub/Len7-Param0-12 67053636 17.95 ns/op 0 B/op 0 allocs/op
|
BenchmarkGitHub/Len7-Param0-12 70562060 16.98 ns/op 0 B/op 0 allocs/op
|
||||||
BenchmarkGitHub/Len7-Param1-12 49371550 24.12 ns/op 0 B/op 0 allocs/op
|
BenchmarkGitHub/Len7-Param1-12 49366180 22.56 ns/op 0 B/op 0 allocs/op
|
||||||
BenchmarkGitHub/Len7-Param2-12 24562465 48.83 ns/op 0 B/op 0 allocs/op
|
BenchmarkGitHub/Len7-Param2-12 24332162 47.91 ns/op 0 B/op 0 allocs/op
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -25,7 +25,7 @@ func (router *Router[T]) Add(method string, path string, handler T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lookup finds the handler and parameters for the given route.
|
// Lookup finds the handler and parameters for the given route.
|
||||||
func (router *Router[T]) Lookup(method string, path string) (T, []keyValue) {
|
func (router *Router[T]) Lookup(method string, path string) (T, []Parameter) {
|
||||||
if method[0] == 'G' {
|
if method[0] == 'G' {
|
||||||
return router.get.Lookup(path)
|
return router.get.Lookup(path)
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,20 @@ import (
|
|||||||
"git.akyoto.dev/go/router/testdata"
|
"git.akyoto.dev/go/router/testdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestHello(t *testing.T) {
|
||||||
|
r := router.New[string]()
|
||||||
|
r.Add("GET", "/blog", "Blog")
|
||||||
|
r.Add("GET", "/blog/post", "Blog post")
|
||||||
|
|
||||||
|
data, params := r.Lookup("GET", "/blog")
|
||||||
|
assert.Equal(t, len(params), 0)
|
||||||
|
assert.Equal(t, data, "Blog")
|
||||||
|
|
||||||
|
data, params = r.Lookup("GET", "/blog/post")
|
||||||
|
assert.Equal(t, len(params), 0)
|
||||||
|
assert.Equal(t, data, "Blog post")
|
||||||
|
}
|
||||||
|
|
||||||
func TestStatic(t *testing.T) {
|
func TestStatic(t *testing.T) {
|
||||||
r := router.New[string]()
|
r := router.New[string]()
|
||||||
r.Add("GET", "/hello", "Hello")
|
r.Add("GET", "/hello", "Hello")
|
||||||
@ -61,11 +75,12 @@ func TestParameter(t *testing.T) {
|
|||||||
func TestWildcard(t *testing.T) {
|
func TestWildcard(t *testing.T) {
|
||||||
r := router.New[string]()
|
r := router.New[string]()
|
||||||
r.Add("GET", "/", "Front page")
|
r.Add("GET", "/", "Front page")
|
||||||
r.Add("GET", "/:post", "Blog post")
|
|
||||||
r.Add("GET", "/*any", "Wildcard")
|
|
||||||
r.Add("GET", "/users/:id", "Parameter")
|
r.Add("GET", "/users/:id", "Parameter")
|
||||||
r.Add("GET", "/images/static", "Static")
|
r.Add("GET", "/images/static", "Static")
|
||||||
r.Add("GET", "/images/*path", "Wildcard")
|
r.Add("GET", "/images/*path", "Wildcard")
|
||||||
|
r.Add("GET", "/:post", "Blog post")
|
||||||
|
r.Add("GET", "/*any", "Wildcard")
|
||||||
|
r.Add("GET", "*root", "Root wildcard")
|
||||||
|
|
||||||
data, params := r.Lookup("GET", "/")
|
data, params := r.Lookup("GET", "/")
|
||||||
assert.Equal(t, len(params), 0)
|
assert.Equal(t, len(params), 0)
|
||||||
@ -107,6 +122,12 @@ func TestWildcard(t *testing.T) {
|
|||||||
assert.Equal(t, params[0].Key, "path")
|
assert.Equal(t, params[0].Key, "path")
|
||||||
assert.Equal(t, params[0].Value, "favicon/256.png")
|
assert.Equal(t, params[0].Value, "favicon/256.png")
|
||||||
assert.Equal(t, data, "Wildcard")
|
assert.Equal(t, data, "Wildcard")
|
||||||
|
|
||||||
|
data, params = r.Lookup("GET", "not-a-path")
|
||||||
|
assert.Equal(t, len(params), 1)
|
||||||
|
assert.Equal(t, params[0].Key, "root")
|
||||||
|
assert.Equal(t, params[0].Value, "not-a-path")
|
||||||
|
assert.Equal(t, data, "Root wildcard")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMap(t *testing.T) {
|
func TestMap(t *testing.T) {
|
||||||
@ -115,6 +136,7 @@ func TestMap(t *testing.T) {
|
|||||||
r.Add("GET", "/world", "World")
|
r.Add("GET", "/world", "World")
|
||||||
r.Add("GET", "/user/:user", "User")
|
r.Add("GET", "/user/:user", "User")
|
||||||
r.Add("GET", "/*path", "Path")
|
r.Add("GET", "/*path", "Path")
|
||||||
|
r.Add("GET", "*root", "Root")
|
||||||
|
|
||||||
r.Map(func(data string) string {
|
r.Map(func(data string) string {
|
||||||
return strings.Repeat(data, 2)
|
return strings.Repeat(data, 2)
|
||||||
@ -135,6 +157,10 @@ func TestMap(t *testing.T) {
|
|||||||
data, params = r.Lookup("GET", "/test.txt")
|
data, params = r.Lookup("GET", "/test.txt")
|
||||||
assert.Equal(t, len(params), 1)
|
assert.Equal(t, len(params), 1)
|
||||||
assert.Equal(t, data, "PathPath")
|
assert.Equal(t, data, "PathPath")
|
||||||
|
|
||||||
|
data, params = r.Lookup("GET", "test.txt")
|
||||||
|
assert.Equal(t, len(params), 1)
|
||||||
|
assert.Equal(t, data, "RootRoot")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMethods(t *testing.T) {
|
func TestMethods(t *testing.T) {
|
||||||
|
73
Tree.go
73
Tree.go
@ -79,11 +79,11 @@ func (tree *Tree[T]) Add(path string, data T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lookup finds the data for the given path.
|
// Lookup finds the data for the given path.
|
||||||
func (tree *Tree[T]) Lookup(path string) (T, []keyValue) {
|
func (tree *Tree[T]) Lookup(path string) (T, []Parameter) {
|
||||||
var params []keyValue
|
var params []Parameter
|
||||||
|
|
||||||
data := tree.LookupNoAlloc(path, func(key string, value string) {
|
data := tree.LookupNoAlloc(path, func(key string, value string) {
|
||||||
params = append(params, keyValue{key, value})
|
params = append(params, Parameter{key, value})
|
||||||
})
|
})
|
||||||
|
|
||||||
return data, params
|
return data, params
|
||||||
@ -94,42 +94,26 @@ func (tree *Tree[T]) LookupNoAlloc(path string, addParameter func(key string, va
|
|||||||
var (
|
var (
|
||||||
i uint
|
i uint
|
||||||
offset uint
|
offset uint
|
||||||
lastWildcardOffset uint
|
wildcardOffset uint
|
||||||
lastWildcard *treeNode[T]
|
wildcard *treeNode[T]
|
||||||
empty T
|
|
||||||
node = &tree.root
|
node = &tree.root
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Skip the first loop iteration if the starting characters are equal
|
||||||
|
if len(path) > 0 && len(node.prefix) > 0 && path[0] == node.prefix[0] {
|
||||||
|
i = 1
|
||||||
|
}
|
||||||
|
|
||||||
begin:
|
begin:
|
||||||
// Search tree for equal parts until we can no longer proceed
|
// Search tree for equal parts until we can no longer proceed
|
||||||
for {
|
for i < uint(len(path)) {
|
||||||
// We reached the end.
|
|
||||||
if i == uint(len(path)) {
|
|
||||||
// node: /blog|
|
|
||||||
// path: /blog|
|
|
||||||
if i-offset == uint(len(node.prefix)) {
|
|
||||||
return node.data
|
|
||||||
}
|
|
||||||
|
|
||||||
// node: /|*any
|
|
||||||
// path: /|image.png
|
|
||||||
if lastWildcard != nil {
|
|
||||||
addParameter(lastWildcard.prefix, path[lastWildcardOffset:])
|
|
||||||
return lastWildcard.data
|
|
||||||
}
|
|
||||||
|
|
||||||
// node: /blog|feed
|
|
||||||
// path: /blog|
|
|
||||||
return empty
|
|
||||||
}
|
|
||||||
|
|
||||||
// The node we just checked is entirely included in our path.
|
// The node we just checked is entirely included in our path.
|
||||||
// node: /|
|
// node: /|
|
||||||
// path: /|blog
|
// path: /|blog
|
||||||
if i-offset == uint(len(node.prefix)) {
|
if i-offset == uint(len(node.prefix)) {
|
||||||
if node.wildcard != nil {
|
if node.wildcard != nil {
|
||||||
lastWildcard = node.wildcard
|
wildcard = node.wildcard
|
||||||
lastWildcardOffset = i
|
wildcardOffset = i
|
||||||
}
|
}
|
||||||
|
|
||||||
char := path[i]
|
char := path[i]
|
||||||
@ -176,28 +160,35 @@ begin:
|
|||||||
|
|
||||||
// node: /|*any
|
// node: /|*any
|
||||||
// path: /|image.png
|
// path: /|image.png
|
||||||
if lastWildcard != nil {
|
goto notFound
|
||||||
addParameter(lastWildcard.prefix, path[lastWildcardOffset:])
|
|
||||||
return lastWildcard.data
|
|
||||||
}
|
|
||||||
|
|
||||||
return empty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We got a conflict.
|
// We got a conflict.
|
||||||
// node: /b|ag
|
// node: /b|ag
|
||||||
// path: /b|riefcase
|
// path: /b|riefcase
|
||||||
if path[i] != node.prefix[i-offset] {
|
if path[i] != node.prefix[i-offset] {
|
||||||
if lastWildcard != nil {
|
goto notFound
|
||||||
addParameter(lastWildcard.prefix, path[lastWildcardOffset:])
|
|
||||||
return lastWildcard.data
|
|
||||||
}
|
|
||||||
|
|
||||||
return empty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// node: /blog|
|
||||||
|
// path: /blog|
|
||||||
|
if i-offset == uint(len(node.prefix)) {
|
||||||
|
return node.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// node: /|*any
|
||||||
|
// path: /|image.png
|
||||||
|
notFound:
|
||||||
|
if wildcard != nil {
|
||||||
|
addParameter(wildcard.prefix, path[wildcardOffset:])
|
||||||
|
return wildcard.data
|
||||||
|
}
|
||||||
|
|
||||||
|
var empty T
|
||||||
|
return empty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map binds all handlers to a new one provided by the callback.
|
// Map binds all handlers to a new one provided by the callback.
|
||||||
|
2
go.mod
2
go.mod
@ -1,5 +1,5 @@
|
|||||||
module git.akyoto.dev/go/router
|
module git.akyoto.dev/go/router
|
||||||
|
|
||||||
go 1.21
|
go 1.22
|
||||||
|
|
||||||
require git.akyoto.dev/go/assert v0.1.3
|
require git.akyoto.dev/go/assert v0.1.3
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
// keyValue represents a URL parameter.
|
|
||||||
type keyValue struct {
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user