From 7070897e70b596e3c4870cecbb9e05d72b9f2e2a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 2 Sep 2023 09:19:11 +0200 Subject: [PATCH] Reduced memory usage --- Parameter.go | 7 ----- README.md | 7 +++-- Router.go | 2 +- Router_test.go | 64 +++++++++++++++++++++++++++------------------- Tree.go | 36 +++----------------------- benchmarks_test.go | 49 +++++++++++++++++++++++------------ helper_test.go | 3 +++ keyValue.go | 7 +++++ testdata/blog.txt | 4 +++ 9 files changed, 95 insertions(+), 84 deletions(-) delete mode 100644 Parameter.go create mode 100644 keyValue.go create mode 100644 testdata/blog.txt diff --git a/Parameter.go b/Parameter.go deleted file mode 100644 index b1c7cd2..0000000 --- a/Parameter.go +++ /dev/null @@ -1,7 +0,0 @@ -package router - -// Parameter is a URL parameter. -type Parameter struct { - Key string - Value string -} diff --git a/README.md b/README.md index 9854c92..03d3956 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Router.go b/Router.go index bbed698..df0ae32 100644 --- a/Router.go +++ b/Router.go @@ -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) } diff --git a/Router_test.go b/Router_test.go index e88afe6..2e192c1 100644 --- a/Router_test.go +++ b/Router_test.go @@ -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) +} diff --git a/Tree.go b/Tree.go index d091e31..9c3553b 100644 --- a/Tree.go +++ b/Tree.go @@ -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 + root treeNode[T] } // 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) - } } diff --git a/benchmarks_test.go b/benchmarks_test.go index 5182c3b..d72791e 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -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++ { + r.LookupNoAlloc("GET", "/", noop) + } + }) - for i := 0; i < b.N; i++ { - router.Lookup("GET", "/repos/:owner/:repo/issues") - } + 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++ { + r.LookupNoAlloc("GET", "/issues", noop) + } + }) - for i := 0; i < b.N; i++ { - router.LookupNoAlloc("GET", "/repos/:owner/:repo/issues", addParameter) - } + 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) + } + }) } diff --git a/helper_test.go b/helper_test.go index 1ad5d3c..92b14b1 100644 --- a/helper_test.go +++ b/helper_test.go @@ -50,3 +50,6 @@ func linesInFile(fileName string) <-chan string { return lines } + +// noop serves as an empty addParameter function. +func noop(string, string) {} diff --git a/keyValue.go b/keyValue.go new file mode 100644 index 0000000..def3969 --- /dev/null +++ b/keyValue.go @@ -0,0 +1,7 @@ +package router + +// keyValue represents a URL parameter. +type keyValue struct { + Key string + Value string +} diff --git a/testdata/blog.txt b/testdata/blog.txt new file mode 100644 index 0000000..432f70c --- /dev/null +++ b/testdata/blog.txt @@ -0,0 +1,4 @@ +GET / +GET /:slug +GET /tags +GET /tag/:tag