From b0e5c876369094b35b30250271a008bfe6e3387c Mon Sep 17 00:00:00 2001 From: Sergio Andres Virviescas Santana Date: Thu, 3 Sep 2020 12:28:04 +0200 Subject: [PATCH] Improve performance --- group_test.go | 3 +- radix/conts.go | 2 -- router.go | 86 ++++++++++++++++++++++++++++++++++++++++---------- router_test.go | 32 ++++++++++++++++--- types.go | 7 ++-- 5 files changed, 103 insertions(+), 27 deletions(-) diff --git a/group_test.go b/group_test.go index 8e6e79b..ec86c78 100644 --- a/group_test.go +++ b/group_test.go @@ -113,7 +113,8 @@ func TestGroup_shortcutsAndHandle(t *testing.T) { fn("/bar", func(_ *fasthttp.RequestCtx) {}) } - for _, method := range httpMethods { + methods := httpMethods[:len(httpMethods)-1] // Avoid customs methods + for _, method := range methods { h, _ := r.Lookup(method, "/v1/bar", nil) if h == nil { t.Errorf("Bad shorcurt") diff --git a/radix/conts.go b/radix/conts.go index 9022c0a..26bdbf1 100644 --- a/radix/conts.go +++ b/radix/conts.go @@ -1,7 +1,5 @@ package radix -const stackBufSize = 128 - const ( root nodeType = iota static diff --git a/router.go b/router.go index f8eb58f..7b8a840 100644 --- a/router.go +++ b/router.go @@ -26,7 +26,8 @@ var ( // Path auto-correction, including trailing slashes, is enabled by default. func New() *Router { return &Router{ - trees: make(map[string]*radix.Tree), + trees: make([]*radix.Tree, 10), + customMethodsIndex: make(map[string]int), registeredPaths: make(map[string][]string), RedirectTrailingSlash: true, RedirectFixedPath: true, @@ -51,6 +52,37 @@ func (r *Router) saveMatchedRoutePath(path string, handler fasthttp.RequestHandl } } +func (r *Router) methodIndexOf(method string) int { + switch method { + case fasthttp.MethodGet: + return 0 + case fasthttp.MethodHead: + return 1 + case fasthttp.MethodPost: + return 2 + case fasthttp.MethodPut: + return 3 + case fasthttp.MethodPatch: + return 4 + case fasthttp.MethodDelete: + return 5 + case fasthttp.MethodConnect: + return 6 + case fasthttp.MethodOptions: + return 7 + case fasthttp.MethodTrace: + return 8 + case MethodWild: + return 9 + } + + if i, ok := r.customMethodsIndex[method]; ok { + return i + } + + return -1 +} + // Mutable allows updating the route handler // // It's disabled by default @@ -59,8 +91,12 @@ func (r *Router) saveMatchedRoutePath(path string, handler fasthttp.RequestHandl func (r *Router) Mutable(v bool) { r.treeMutable = v - for method := range r.trees { - r.trees[method].Mutable = v + for i := range r.trees { + tree := r.trees[i] + + if tree != nil { + tree.Mutable = v + } } } @@ -175,12 +211,22 @@ func (r *Router) Handle(method, path string, handler fasthttp.RequestHandler) { r.registeredPaths[method] = append(r.registeredPaths[method], path) - tree := r.trees[method] + methodIndex := r.methodIndexOf(method) + if methodIndex == -1 { + tree := radix.New() + tree.Mutable = r.treeMutable + + r.trees = append(r.trees, tree) + methodIndex = len(r.trees) - 1 + r.customMethodsIndex[method] = methodIndex + } + + tree := r.trees[methodIndex] if tree == nil { tree = radix.New() tree.Mutable = r.treeMutable - r.trees[method] = tree + r.trees[methodIndex] = tree r.globalAllowed = r.allowed("*", "") } @@ -206,14 +252,19 @@ func (r *Router) Handle(method, path string, handler fasthttp.RequestHandler) { // values. Otherwise the third return value indicates whether a redirection to // the same path with an extra / without the trailing slash should be performed. func (r *Router) Lookup(method, path string, ctx *fasthttp.RequestCtx) (fasthttp.RequestHandler, bool) { - if tree := r.trees[method]; tree != nil { + methodIndex := r.methodIndexOf(method) + if methodIndex == -1 { + return nil, false + } + + if tree := r.trees[methodIndex]; tree != nil { handler, tsr := tree.Get(path, ctx) if handler != nil || tsr { return handler, tsr } } - if tree := r.trees[MethodWild]; tree != nil { + if tree := r.trees[r.methodIndexOf(MethodWild)]; tree != nil { return tree.Get(path, ctx) } @@ -243,13 +294,13 @@ func (r *Router) allowed(path, reqMethod string) (allow string) { return r.globalAllowed } } else { // specific path - for method := range r.trees { + for method := range r.registeredPaths { // Skip the requested method - we already tried this one if method == reqMethod || method == fasthttp.MethodOptions { continue } - handle, _ := r.trees[method].Get(path, nil) + handle, _ := r.trees[r.methodIndexOf(method)].Get(path, nil) if handle != nil { // Add request method to list of allowed methods allowed = append(allowed, method) @@ -342,20 +393,23 @@ func (r *Router) Handler(ctx *fasthttp.RequestCtx) { path := gotils.B2S(ctx.Request.URI().Path()) method := gotils.B2S(ctx.Request.Header.Method()) + methodIndex := r.methodIndexOf(method) - if tree := r.trees[method]; tree != nil { - if handler, tsr := tree.Get(path, ctx); handler != nil { - handler(ctx) - return - } else if method != fasthttp.MethodConnect && path != "/" { - if ok := r.tryRedirect(ctx, tree, tsr, method, path); ok { + if methodIndex > -1 { + if tree := r.trees[methodIndex]; tree != nil { + if handler, tsr := tree.Get(path, ctx); handler != nil { + handler(ctx) return + } else if method != fasthttp.MethodConnect && path != "/" { + if ok := r.tryRedirect(ctx, tree, tsr, method, path); ok { + return + } } } } // Try to search in the wild method tree - if tree := r.trees[MethodWild]; tree != nil { + if tree := r.trees[r.methodIndexOf(MethodWild)]; tree != nil { if handler, tsr := tree.Get(path, ctx); handler != nil { handler(ctx) return diff --git a/router_test.go b/router_test.go index 8f3921b..39a3a6b 100644 --- a/router_test.go +++ b/router_test.go @@ -33,6 +33,7 @@ var httpMethods = []string{ fasthttp.MethodOptions, fasthttp.MethodTrace, MethodWild, + "CUSTOM", } func randomHTTPMethod() string { @@ -156,10 +157,10 @@ func TestRouterAPI(t *testing.T) { router.GET("/GET", func(ctx *fasthttp.RequestCtx) { get = true }) - router.HEAD("/GET", func(ctx *fasthttp.RequestCtx) { + router.HEAD("/HEAD", func(ctx *fasthttp.RequestCtx) { head = true }) - router.OPTIONS("/GET", func(ctx *fasthttp.RequestCtx) { + router.OPTIONS("/OPTIONS", func(ctx *fasthttp.RequestCtx) { options = true }) router.POST("/POST", func(ctx *fasthttp.RequestCtx) { @@ -192,12 +193,12 @@ func TestRouterAPI(t *testing.T) { t.Error("routing GET failed") } - request(fasthttp.MethodHead, "/GET") + request(fasthttp.MethodHead, "/HEAD") if !head { t.Error("routing HEAD failed") } - request(fasthttp.MethodOptions, "/GET") + request(fasthttp.MethodOptions, "/OPTIONS") if !options { t.Error("routing OPTIONS failed") } @@ -335,7 +336,7 @@ func TestRouterMutable(t *testing.T) { for method := range router.trees { if !router.trees[method].Mutable { - t.Errorf("Method %s - Mutable == %v, want %v", method, router.trees[method].Mutable, true) + t.Errorf("Method %d - Mutable == %v, want %v", method, router.trees[method].Mutable, true) } } @@ -1072,6 +1073,27 @@ func BenchmarkRouterANY(b *testing.B) { } } +func BenchmarkRouterGet_ANY(b *testing.B) { + resp := []byte("Bench GET") + respANY := []byte("Bench GET (ANY)") + + r := New() + r.GET("/", func(ctx *fasthttp.RequestCtx) { + ctx.Success("text/plain", resp) + }) + r.ANY("/", func(ctx *fasthttp.RequestCtx) { + ctx.Success("text/plain", respANY) + }) + + ctx := new(fasthttp.RequestCtx) + ctx.Request.Header.SetMethod("UNICORN") + ctx.Request.SetRequestURI("/") + + for i := 0; i < b.N; i++ { + r.Handler(ctx) + } +} + func BenchmarkRouterNotFound(b *testing.B) { r := New() r.GET("/bench", func(ctx *fasthttp.RequestCtx) {}) diff --git a/types.go b/types.go index d58976b..a42ec18 100644 --- a/types.go +++ b/types.go @@ -8,9 +8,10 @@ import ( // Router is a fasthttp.RequestHandler which can be used to dispatch requests to different // handler functions via configurable routes type Router struct { - trees map[string]*radix.Tree - treeMutable bool - registeredPaths map[string][]string + trees []*radix.Tree + treeMutable bool + customMethodsIndex map[string]int + registeredPaths map[string][]string // If enabled, adds the matched route path onto the ctx.UserValue context // before invoking the handler.