Reduced memory usage
This commit is contained in:
parent
e5b0eb443a
commit
7070897e70
@ -1,7 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
// Parameter is a URL parameter.
|
|
||||||
type Parameter struct {
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
}
|
|
@ -54,8 +54,11 @@ coverage: 76.9% of statements
|
|||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
```
|
```
|
||||||
BenchmarkLookup-12 6965749 171.2 ns/op 96 B/op 2 allocs/op
|
BenchmarkBlog/Len1-Param0-12 182590244 6.57 ns/op 0 B/op 0 allocs/op
|
||||||
BenchmarkLookupNoAlloc-12 24243546 48.5 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -27,7 +27,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, []Parameter) {
|
func (router *Router[T]) Lookup(method string, path string) (T, []keyValue) {
|
||||||
if method[0] == 'G' {
|
if method[0] == 'G' {
|
||||||
return router.get.Lookup(path)
|
return router.get.Lookup(path)
|
||||||
}
|
}
|
||||||
|
@ -8,35 +8,35 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestStatic(t *testing.T) {
|
func TestStatic(t *testing.T) {
|
||||||
router := router.New[string]()
|
r := router.New[string]()
|
||||||
router.Add("GET", "/hello", "Hello")
|
r.Add("GET", "/hello", "Hello")
|
||||||
router.Add("GET", "/world", "World")
|
r.Add("GET", "/world", "World")
|
||||||
|
|
||||||
data, params := router.Lookup("GET", "/hello")
|
data, params := r.Lookup("GET", "/hello")
|
||||||
assert.Equal(t, len(params), 0)
|
assert.Equal(t, len(params), 0)
|
||||||
assert.Equal(t, data, "Hello")
|
assert.Equal(t, data, "Hello")
|
||||||
|
|
||||||
data, params = router.Lookup("GET", "/world")
|
data, params = r.Lookup("GET", "/world")
|
||||||
assert.Equal(t, len(params), 0)
|
assert.Equal(t, len(params), 0)
|
||||||
assert.Equal(t, data, "World")
|
assert.Equal(t, data, "World")
|
||||||
|
|
||||||
data, params = router.Lookup("GET", "/404")
|
data, params = r.Lookup("GET", "/404")
|
||||||
assert.Equal(t, len(params), 0)
|
assert.Equal(t, len(params), 0)
|
||||||
assert.Equal(t, data, "")
|
assert.Equal(t, data, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParameter(t *testing.T) {
|
func TestParameter(t *testing.T) {
|
||||||
router := router.New[string]()
|
r := router.New[string]()
|
||||||
router.Add("GET", "/blog/:slug", "Blog post")
|
r.Add("GET", "/blog/:slug", "Blog post")
|
||||||
router.Add("GET", "/blog/:slug/comments/:id", "Comment")
|
r.Add("GET", "/blog/:slug/comments/:id", "Comment")
|
||||||
|
|
||||||
data, params := router.Lookup("GET", "/blog/hello-world")
|
data, params := r.Lookup("GET", "/blog/hello-world")
|
||||||
assert.Equal(t, len(params), 1)
|
assert.Equal(t, len(params), 1)
|
||||||
assert.Equal(t, params[0].Key, "slug")
|
assert.Equal(t, params[0].Key, "slug")
|
||||||
assert.Equal(t, params[0].Value, "hello-world")
|
assert.Equal(t, params[0].Value, "hello-world")
|
||||||
assert.Equal(t, data, "Blog post")
|
assert.Equal(t, data, "Blog post")
|
||||||
|
|
||||||
data, params = router.Lookup("GET", "/blog/hello-world/comments/123")
|
data, params = r.Lookup("GET", "/blog/hello-world/comments/123")
|
||||||
assert.Equal(t, len(params), 2)
|
assert.Equal(t, len(params), 2)
|
||||||
assert.Equal(t, params[0].Key, "slug")
|
assert.Equal(t, params[0].Key, "slug")
|
||||||
assert.Equal(t, params[0].Value, "hello-world")
|
assert.Equal(t, params[0].Value, "hello-world")
|
||||||
@ -46,29 +46,29 @@ func TestParameter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWildcard(t *testing.T) {
|
func TestWildcard(t *testing.T) {
|
||||||
router := router.New[string]()
|
r := router.New[string]()
|
||||||
router.Add("GET", "/", "Front page")
|
r.Add("GET", "/", "Front page")
|
||||||
router.Add("GET", "/:slug", "Blog post")
|
r.Add("GET", "/:slug", "Blog post")
|
||||||
router.Add("GET", "/users/:id", "Parameter")
|
r.Add("GET", "/users/:id", "Parameter")
|
||||||
router.Add("GET", "/images/*path", "Wildcard")
|
r.Add("GET", "/images/*path", "Wildcard")
|
||||||
|
|
||||||
data, params := router.Lookup("GET", "/")
|
data, params := r.Lookup("GET", "/")
|
||||||
assert.Equal(t, len(params), 0)
|
assert.Equal(t, len(params), 0)
|
||||||
assert.Equal(t, data, "Front page")
|
assert.Equal(t, data, "Front page")
|
||||||
|
|
||||||
data, params = router.Lookup("GET", "/blog-post")
|
data, params = r.Lookup("GET", "/blog-post")
|
||||||
assert.Equal(t, len(params), 1)
|
assert.Equal(t, len(params), 1)
|
||||||
assert.Equal(t, params[0].Key, "slug")
|
assert.Equal(t, params[0].Key, "slug")
|
||||||
assert.Equal(t, params[0].Value, "blog-post")
|
assert.Equal(t, params[0].Value, "blog-post")
|
||||||
assert.Equal(t, data, "Blog post")
|
assert.Equal(t, data, "Blog post")
|
||||||
|
|
||||||
data, params = router.Lookup("GET", "/users/42")
|
data, params = r.Lookup("GET", "/users/42")
|
||||||
assert.Equal(t, len(params), 1)
|
assert.Equal(t, len(params), 1)
|
||||||
assert.Equal(t, params[0].Key, "id")
|
assert.Equal(t, params[0].Key, "id")
|
||||||
assert.Equal(t, params[0].Value, "42")
|
assert.Equal(t, params[0].Value, "42")
|
||||||
assert.Equal(t, data, "Parameter")
|
assert.Equal(t, data, "Parameter")
|
||||||
|
|
||||||
data, params = router.Lookup("GET", "/images/favicon/256.png")
|
data, params = r.Lookup("GET", "/images/favicon/256.png")
|
||||||
assert.Equal(t, len(params), 1)
|
assert.Equal(t, len(params), 1)
|
||||||
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")
|
||||||
@ -76,7 +76,6 @@ func TestWildcard(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMethods(t *testing.T) {
|
func TestMethods(t *testing.T) {
|
||||||
router := router.New[string]()
|
|
||||||
methods := []string{
|
methods := []string{
|
||||||
"GET",
|
"GET",
|
||||||
"POST",
|
"POST",
|
||||||
@ -89,26 +88,39 @@ func TestMethods(t *testing.T) {
|
|||||||
"OPTIONS",
|
"OPTIONS",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r := router.New[string]()
|
||||||
|
|
||||||
for _, method := range methods {
|
for _, method := range methods {
|
||||||
router.Add(method, "/", method)
|
r.Add(method, "/", method)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, method := range methods {
|
for _, method := range methods {
|
||||||
data, _ := router.Lookup(method, "/")
|
data, _ := r.Lookup(method, "/")
|
||||||
assert.Equal(t, data, method)
|
assert.Equal(t, data, method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGitHub(t *testing.T) {
|
func TestGitHub(t *testing.T) {
|
||||||
router := router.New[string]()
|
|
||||||
routes := loadRoutes("testdata/github.txt")
|
routes := loadRoutes("testdata/github.txt")
|
||||||
|
r := router.New[string]()
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
router.Add(route.method, route.path, "octocat")
|
r.Add(route.method, route.path, "octocat")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
data, _ := router.Lookup(route.method, route.path)
|
data, _ := r.Lookup(route.method, route.path)
|
||||||
assert.Equal(t, data, "octocat")
|
assert.Equal(t, data, "octocat")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMemoryUsage(t *testing.T) {
|
||||||
|
escape := func(a any) {}
|
||||||
|
|
||||||
|
result := testing.Benchmark(func(b *testing.B) {
|
||||||
|
r := router.New[string]()
|
||||||
|
escape(r)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Logf("%d bytes", result.MemBytes)
|
||||||
|
}
|
||||||
|
36
Tree.go
36
Tree.go
@ -1,9 +1,5 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// controlFlow tells the main loop what it should do next.
|
// controlFlow tells the main loop what it should do next.
|
||||||
type controlFlow int
|
type controlFlow int
|
||||||
|
|
||||||
@ -16,23 +12,11 @@ const (
|
|||||||
|
|
||||||
// Tree represents a radix tree.
|
// Tree represents a radix tree.
|
||||||
type Tree[T any] struct {
|
type Tree[T any] struct {
|
||||||
static map[string]T
|
root treeNode[T]
|
||||||
root treeNode[T]
|
|
||||||
canBeStatic [2048]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add adds a new element to the tree.
|
// Add adds a new element to the tree.
|
||||||
func (tree *Tree[T]) Add(path string, data T) {
|
func (tree *Tree[T]) Add(path string, data T) {
|
||||||
if !strings.Contains(path, ":") && !strings.Contains(path, "*") {
|
|
||||||
if tree.static == nil {
|
|
||||||
tree.static = map[string]T{}
|
|
||||||
}
|
|
||||||
|
|
||||||
tree.static[path] = data
|
|
||||||
tree.canBeStatic[len(path)] = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search tree for equal parts until we can no longer proceed
|
// Search tree for equal parts until we can no longer proceed
|
||||||
i := 0
|
i := 0
|
||||||
offset := 0
|
offset := 0
|
||||||
@ -114,11 +98,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, []Parameter) {
|
func (tree *Tree[T]) Lookup(path string) (T, []keyValue) {
|
||||||
var params []Parameter
|
var params []keyValue
|
||||||
|
|
||||||
data := tree.LookupNoAlloc(path, func(key string, value string) {
|
data := tree.LookupNoAlloc(path, func(key string, value string) {
|
||||||
params = append(params, Parameter{key, value})
|
params = append(params, keyValue{key, value})
|
||||||
})
|
})
|
||||||
|
|
||||||
return data, params
|
return data, params
|
||||||
@ -126,14 +110,6 @@ func (tree *Tree[T]) Lookup(path string) (T, []Parameter) {
|
|||||||
|
|
||||||
// LookupNoAlloc finds the data for the given path without using any memory allocations.
|
// 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 {
|
func (tree *Tree[T]) LookupNoAlloc(path string, addParameter func(key string, value string)) T {
|
||||||
if tree.canBeStatic[len(path)] {
|
|
||||||
handler, found := tree.static[path]
|
|
||||||
|
|
||||||
if found {
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
i uint
|
i uint
|
||||||
offset uint
|
offset uint
|
||||||
@ -241,8 +217,4 @@ func (tree *Tree[T]) Bind(transform func(T) T) {
|
|||||||
tree.root.each(func(node *treeNode[T]) {
|
tree.root.each(func(node *treeNode[T]) {
|
||||||
node.data = transform(node.data)
|
node.data = transform(node.data)
|
||||||
})
|
})
|
||||||
|
|
||||||
for key, value := range tree.static {
|
|
||||||
tree.static[key] = transform(value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,33 +6,50 @@ import (
|
|||||||
"git.akyoto.dev/go/router"
|
"git.akyoto.dev/go/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkLookup(b *testing.B) {
|
func BenchmarkBlog(b *testing.B) {
|
||||||
router := router.New[string]()
|
routes := loadRoutes("testdata/blog.txt")
|
||||||
routes := loadRoutes("testdata/github.txt")
|
r := router.New[string]()
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
router.Add(route.method, route.path, "")
|
r.Add(route.method, route.path, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.Run("Len1-Param0", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
r.LookupNoAlloc("GET", "/", noop)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
b.Run("Len1-Param1", func(b *testing.B) {
|
||||||
router.Lookup("GET", "/repos/:owner/:repo/issues")
|
for i := 0; i < b.N; i++ {
|
||||||
}
|
r.LookupNoAlloc("GET", "/:id", noop)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkLookupNoAlloc(b *testing.B) {
|
func BenchmarkGitHub(b *testing.B) {
|
||||||
router := router.New[string]()
|
|
||||||
routes := loadRoutes("testdata/github.txt")
|
routes := loadRoutes("testdata/github.txt")
|
||||||
addParameter := func(string, string) {}
|
r := router.New[string]()
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
router.Add(route.method, route.path, "")
|
r.Add(route.method, route.path, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.Run("Len7-Param0", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
r.LookupNoAlloc("GET", "/issues", noop)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
b.Run("Len7-Param1", func(b *testing.B) {
|
||||||
router.LookupNoAlloc("GET", "/repos/:owner/:repo/issues", addParameter)
|
for i := 0; i < b.N; i++ {
|
||||||
}
|
r.LookupNoAlloc("GET", "/gists/:id", noop)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Len7-Param2", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
r.LookupNoAlloc("GET", "/repos/:owner/:repo/issues", noop)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -50,3 +50,6 @@ func linesInFile(fileName string) <-chan string {
|
|||||||
|
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noop serves as an empty addParameter function.
|
||||||
|
func noop(string, string) {}
|
||||||
|
7
keyValue.go
Normal file
7
keyValue.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
// keyValue represents a URL parameter.
|
||||||
|
type keyValue struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
4
testdata/blog.txt
vendored
Normal file
4
testdata/blog.txt
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
GET /
|
||||||
|
GET /:slug
|
||||||
|
GET /tags
|
||||||
|
GET /tag/:tag
|
Loading…
x
Reference in New Issue
Block a user