Reduced memory usage

This commit is contained in:
Eduard Urbach 2023-09-02 09:19:11 +02:00
parent e5b0eb443a
commit 7070897e70
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
9 changed files with 95 additions and 84 deletions

View File

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

View File

@ -54,8 +54,11 @@ coverage: 76.9% of statements
## Benchmarks
```
BenchmarkLookup-12 6965749 171.2 ns/op 96 B/op 2 allocs/op
BenchmarkLookupNoAlloc-12 24243546 48.5 ns/op 0 B/op 0 allocs/op
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
```
## License

View File

@ -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.
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' {
return router.get.Lookup(path)
}

View File

@ -8,35 +8,35 @@ import (
)
func TestStatic(t *testing.T) {
router := router.New[string]()
router.Add("GET", "/hello", "Hello")
router.Add("GET", "/world", "World")
r := router.New[string]()
r.Add("GET", "/hello", "Hello")
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, data, "Hello")
data, params = router.Lookup("GET", "/world")
data, params = r.Lookup("GET", "/world")
assert.Equal(t, len(params), 0)
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, data, "")
}
func TestParameter(t *testing.T) {
router := router.New[string]()
router.Add("GET", "/blog/:slug", "Blog post")
router.Add("GET", "/blog/:slug/comments/:id", "Comment")
r := router.New[string]()
r.Add("GET", "/blog/:slug", "Blog post")
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, params[0].Key, "slug")
assert.Equal(t, params[0].Value, "hello-world")
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, params[0].Key, "slug")
assert.Equal(t, params[0].Value, "hello-world")
@ -46,29 +46,29 @@ func TestParameter(t *testing.T) {
}
func TestWildcard(t *testing.T) {
router := router.New[string]()
router.Add("GET", "/", "Front page")
router.Add("GET", "/:slug", "Blog post")
router.Add("GET", "/users/:id", "Parameter")
router.Add("GET", "/images/*path", "Wildcard")
r := router.New[string]()
r.Add("GET", "/", "Front page")
r.Add("GET", "/:slug", "Blog post")
r.Add("GET", "/users/:id", "Parameter")
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, 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, params[0].Key, "slug")
assert.Equal(t, params[0].Value, "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, params[0].Key, "id")
assert.Equal(t, params[0].Value, "42")
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, params[0].Key, "path")
assert.Equal(t, params[0].Value, "favicon/256.png")
@ -76,7 +76,6 @@ func TestWildcard(t *testing.T) {
}
func TestMethods(t *testing.T) {
router := router.New[string]()
methods := []string{
"GET",
"POST",
@ -89,26 +88,39 @@ func TestMethods(t *testing.T) {
"OPTIONS",
}
r := router.New[string]()
for _, method := range methods {
router.Add(method, "/", method)
r.Add(method, "/", method)
}
for _, method := range methods {
data, _ := router.Lookup(method, "/")
data, _ := r.Lookup(method, "/")
assert.Equal(t, data, method)
}
}
func TestGitHub(t *testing.T) {
router := router.New[string]()
routes := loadRoutes("testdata/github.txt")
r := router.New[string]()
for _, route := range routes {
router.Add(route.method, route.path, "octocat")
r.Add(route.method, route.path, "octocat")
}
for _, route := range routes {
data, _ := router.Lookup(route.method, route.path)
data, _ := r.Lookup(route.method, route.path)
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)
}

34
Tree.go
View File

@ -1,9 +1,5 @@
package router
import (
"strings"
)
// controlFlow tells the main loop what it should do next.
type controlFlow int
@ -16,23 +12,11 @@ const (
// Tree represents a radix tree.
type Tree[T any] struct {
static map[string]T
root treeNode[T]
canBeStatic [2048]bool
}
// Add adds a new element to the tree.
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
i := 0
offset := 0
@ -114,11 +98,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, []Parameter) {
var params []Parameter
func (tree *Tree[T]) Lookup(path string) (T, []keyValue) {
var params []keyValue
data := tree.LookupNoAlloc(path, func(key string, value string) {
params = append(params, Parameter{key, value})
params = append(params, keyValue{key, value})
})
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.
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 (
i uint
offset uint
@ -241,8 +217,4 @@ func (tree *Tree[T]) Bind(transform func(T) T) {
tree.root.each(func(node *treeNode[T]) {
node.data = transform(node.data)
})
for key, value := range tree.static {
tree.static[key] = transform(value)
}
}

View File

@ -6,33 +6,50 @@ import (
"git.akyoto.dev/go/router"
)
func BenchmarkLookup(b *testing.B) {
router := router.New[string]()
routes := loadRoutes("testdata/github.txt")
func BenchmarkBlog(b *testing.B) {
routes := loadRoutes("testdata/blog.txt")
r := router.New[string]()
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++ {
router.Lookup("GET", "/repos/:owner/:repo/issues")
r.LookupNoAlloc("GET", "/", noop)
}
})
b.Run("Len1-Param1", func(b *testing.B) {
for i := 0; i < b.N; i++ {
r.LookupNoAlloc("GET", "/:id", noop)
}
})
}
func BenchmarkLookupNoAlloc(b *testing.B) {
router := router.New[string]()
func BenchmarkGitHub(b *testing.B) {
routes := loadRoutes("testdata/github.txt")
addParameter := func(string, string) {}
r := router.New[string]()
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++ {
router.LookupNoAlloc("GET", "/repos/:owner/:repo/issues", addParameter)
r.LookupNoAlloc("GET", "/issues", noop)
}
})
b.Run("Len7-Param1", func(b *testing.B) {
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)
}
})
}

View File

@ -50,3 +50,6 @@ func linesInFile(fileName string) <-chan string {
return lines
}
// noop serves as an empty addParameter function.
func noop(string, string) {}

7
keyValue.go Normal file
View 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
View File

@ -0,0 +1,4 @@
GET /
GET /:slug
GET /tags
GET /tag/:tag