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
|
||||
|
||||
```
|
||||
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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
34
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
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