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
|
||||
|
||||
```
|
||||
BenchmarkBlog/Len1-Param0-12 182590244 6.57 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBlog/Len1-Param1-12 100000000 10.95 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-Param1-12 49371550 24.12 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGitHub/Len7-Param2-12 24562465 48.83 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 129550375 9.224 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 49366180 22.56 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGitHub/Len7-Param2-12 24332162 47.91 ns/op 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
## 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.
|
||||
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' {
|
||||
return router.get.Lookup(path)
|
||||
}
|
||||
|
@ -9,6 +9,20 @@ import (
|
||||
"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) {
|
||||
r := router.New[string]()
|
||||
r.Add("GET", "/hello", "Hello")
|
||||
@ -61,11 +75,12 @@ func TestParameter(t *testing.T) {
|
||||
func TestWildcard(t *testing.T) {
|
||||
r := router.New[string]()
|
||||
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", "/images/static", "Static")
|
||||
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", "/")
|
||||
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].Value, "favicon/256.png")
|
||||
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) {
|
||||
@ -115,6 +136,7 @@ func TestMap(t *testing.T) {
|
||||
r.Add("GET", "/world", "World")
|
||||
r.Add("GET", "/user/:user", "User")
|
||||
r.Add("GET", "/*path", "Path")
|
||||
r.Add("GET", "*root", "Root")
|
||||
|
||||
r.Map(func(data string) string {
|
||||
return strings.Repeat(data, 2)
|
||||
@ -135,6 +157,10 @@ func TestMap(t *testing.T) {
|
||||
data, params = r.Lookup("GET", "/test.txt")
|
||||
assert.Equal(t, len(params), 1)
|
||||
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) {
|
||||
|
79
Tree.go
79
Tree.go
@ -79,11 +79,11 @@ func (tree *Tree[T]) Add(path string, data T) {
|
||||
}
|
||||
|
||||
// Lookup finds the data for the given path.
|
||||
func (tree *Tree[T]) Lookup(path string) (T, []keyValue) {
|
||||
var params []keyValue
|
||||
func (tree *Tree[T]) Lookup(path string) (T, []Parameter) {
|
||||
var params []Parameter
|
||||
|
||||
data := tree.LookupNoAlloc(path, func(key string, value string) {
|
||||
params = append(params, keyValue{key, value})
|
||||
params = append(params, Parameter{key, value})
|
||||
})
|
||||
|
||||
return data, params
|
||||
@ -92,44 +92,28 @@ func (tree *Tree[T]) Lookup(path string) (T, []keyValue) {
|
||||
// LookupNoAlloc finds the data for the given path without using any memory allocations.
|
||||
func (tree *Tree[T]) LookupNoAlloc(path string, addParameter func(key string, value string)) T {
|
||||
var (
|
||||
i uint
|
||||
offset uint
|
||||
lastWildcardOffset uint
|
||||
lastWildcard *treeNode[T]
|
||||
empty T
|
||||
node = &tree.root
|
||||
i uint
|
||||
offset uint
|
||||
wildcardOffset uint
|
||||
wildcard *treeNode[T]
|
||||
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:
|
||||
// Search tree for equal parts until we can no longer proceed
|
||||
for {
|
||||
// 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
|
||||
}
|
||||
|
||||
for i < uint(len(path)) {
|
||||
// The node we just checked is entirely included in our path.
|
||||
// node: /|
|
||||
// path: /|blog
|
||||
if i-offset == uint(len(node.prefix)) {
|
||||
if node.wildcard != nil {
|
||||
lastWildcard = node.wildcard
|
||||
lastWildcardOffset = i
|
||||
wildcard = node.wildcard
|
||||
wildcardOffset = i
|
||||
}
|
||||
|
||||
char := path[i]
|
||||
@ -176,28 +160,35 @@ begin:
|
||||
|
||||
// node: /|*any
|
||||
// path: /|image.png
|
||||
if lastWildcard != nil {
|
||||
addParameter(lastWildcard.prefix, path[lastWildcardOffset:])
|
||||
return lastWildcard.data
|
||||
}
|
||||
|
||||
return empty
|
||||
goto notFound
|
||||
}
|
||||
|
||||
// We got a conflict.
|
||||
// node: /b|ag
|
||||
// path: /b|riefcase
|
||||
if path[i] != node.prefix[i-offset] {
|
||||
if lastWildcard != nil {
|
||||
addParameter(lastWildcard.prefix, path[lastWildcardOffset:])
|
||||
return lastWildcard.data
|
||||
}
|
||||
|
||||
return empty
|
||||
goto notFound
|
||||
}
|
||||
|
||||
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.
|
||||
|
2
go.mod
2
go.mod
@ -1,5 +1,5 @@
|
||||
module git.akyoto.dev/go/router
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
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…
Reference in New Issue
Block a user