Improved performance

This commit is contained in:
Eduard Urbach 2024-03-14 23:26:59 +01:00
parent dd98b11eea
commit 99ad93e410
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
7 changed files with 77 additions and 60 deletions

7
Parameter.go Normal file
View File

@ -0,0 +1,7 @@
package router
// Parameter represents a URL parameter.
type Parameter struct {
Key string
Value string
}

View File

@ -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

View File

@ -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)
} }

View File

@ -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
View File

@ -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
View File

@ -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

View File

@ -1,7 +0,0 @@
package router
// keyValue represents a URL parameter.
type keyValue struct {
Key string
Value string
}