Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃殌 compress same route's handler and sort routes when registering #651

Merged
merged 7 commits into from
Jul 21, 2020
40 changes: 7 additions & 33 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"os"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -52,8 +51,8 @@ type App struct {
mutex sync.Mutex
// Route stack divided by HTTP methods
stack [][]*Route
// Amount of registered routes
routes int
// A sorted route slice for app.Routes()
routes []*Route
// Ctx pool
pool sync.Pool
// Fasthttp server
Expand Down Expand Up @@ -372,7 +371,10 @@ func (app *App) Static(prefix, root string, config ...Static) Router {
// All ...
func (app *App) All(path string, handlers ...Handler) Router {
for _, method := range intMethod {
_ = app.Add(method, path, handlers...)
// MethodHead will be added by MethodGet
if method != MethodHead {
_ = app.Add(method, path, handlers...)
}
}
return app
}
Expand Down Expand Up @@ -404,35 +406,7 @@ func NewError(code int, message ...string) *Error {
// fmt.Printf("%s\t%s\n", r.Method, r.Path)
// }
func (app *App) Routes() []*Route {
routes := make([]*Route, 0)
for m := range app.stack {
for r := range app.stack[m] {
// Ignore HEAD routes handling GET routes
if m == 1 && app.stack[m][r].Method == MethodGet {
continue
}
// Don't duplicate USE routes
if app.stack[m][r].Method == methodUse {
duplicate := false
for i := range routes {
if routes[i].Method == methodUse && routes[i].Name == app.stack[m][r].Name {
duplicate = true
break
}
}
if !duplicate {
routes = append(routes, app.stack[m][r])
}
} else {
routes = append(routes, app.stack[m][r])
}
}
}
// Sort routes by stack position
sort.Slice(routes, func(i, k int) bool {
return routes[i].pos < routes[k].pos
})
return routes
return app.routes
}

// Serve is deprecated, please use app.Listener()
Expand Down
32 changes: 32 additions & 0 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,38 @@ func Test_App_Routes(t *testing.T) {
utils.AssertEqual(t, 4, len(app.Routes()))
}

// go test -v -run=^$ -bench=Benchmark_App_Routes -benchmem -count=4
func Benchmark_App_Routes(b *testing.B) {
app := New()
h := func(c *Ctx) {}
app.Use("/", h)
app.Use("/", h)
app.Get("/Get", h)
app.Head("/Head", h)
app.Post("/post", h)

b.ReportAllocs()
b.ResetTimer()

for n := 0; n < b.N; n++ {
app.Routes()
}
utils.AssertEqual(b, 5, len(app.Routes()))
}

func Test_App_Router_Compress(t *testing.T) {
app := New()
h := func(c *Ctx) { c.Next() }
app.Use("/", h)
app.Use("/", h)
app.Use("/", h)
app.Get("/:a/:b/:c", h)
app.Get("/:a/:b/:c", h)
app.Get("/:a/:b/:c", h)

utils.AssertEqual(t, 2, len(app.stack[methodInt(MethodGet)]))
}

func Test_App_ServerErrorHandler_SmallReadBuffer(t *testing.T) {
expectedError := regexp.MustCompile(
`error when reading request headers: small read buffer\. Increase ReadBufferSize\. Buffer size=4096, contents: "GET / HTTP/1.1\\r\\nHost: example\.com\\r\\nVery-Long-Header: -+`,
Expand Down
5 changes: 4 additions & 1 deletion group.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ func (grp *Group) Static(prefix, root string, config ...Static) Router {
// All ...
func (grp *Group) All(path string, handlers ...Handler) Router {
for _, method := range intMethod {
_ = grp.Add(method, path, handlers...)
// MethodHead will be added by MethodGet
if method != MethodHead {
_ = grp.Add(method, path, handlers...)
}
}
return grp
}
Expand Down
100 changes: 68 additions & 32 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ type Router interface {
// Route is a struct that holds all metadata for each registered handler
type Route struct {
// Data for routing
pos int // Position in stack
use bool // USE matches path prefixes
star bool // Path equals '*'
root bool // Path equals '/'
Expand Down Expand Up @@ -181,14 +180,9 @@ func (app *App) register(method, pathRaw string, handlers ...Handler) {
var parsedRaw = parseRoute(pathRaw)
var parsedPretty = parseRoute(pathPretty)

// Increment global route position
app.mutex.Lock()
app.routes++
app.mutex.Unlock()
// Create route metadata
route := &Route{
// Router booleans
pos: app.routes,
use: isUse,
star: isStar,
root: isRoot,
Expand All @@ -202,22 +196,10 @@ func (app *App) register(method, pathRaw string, handlers ...Handler) {
Method: method,
Handlers: handlers,
}
// Middleware route matches all HTTP methods
if isUse {
// Add route to all HTTP methods stack
for _, m := range intMethod {
app.addRoute(m, route)
}
return
}

// Handle GET routes on HEAD requests
if method == MethodGet {
app.addRoute(MethodHead, route)
}

// Add route to stack
app.mutex.Lock()
app.addRoute(method, route)
app.mutex.Unlock()
}

func (app *App) registerStatic(prefix, root string, config ...Static) {
Expand Down Expand Up @@ -305,12 +287,8 @@ func (app *App) registerStatic(prefix, root string, config ...Static) {
// Next middleware
c.Next()
}
// Increment global route position
app.mutex.Lock()
app.routes++
app.mutex.Unlock()

route := &Route{
pos: app.routes,
use: true,
root: isRoot,
path: prefix,
Expand All @@ -319,17 +297,75 @@ func (app *App) registerStatic(prefix, root string, config ...Static) {
}
route.Handlers = append(route.Handlers, handler)
// Add route to stack
app.addRoute(MethodGet, route)
app.addRoute(MethodHead, route)
app.mutex.Lock()
app.addRoute(MethodGet, route, true)
app.mutex.Unlock()
}

func (app *App) addRoute(method string, route *Route) {
func (app *App) compressed(route *Route, isStatic ...bool) bool {
if route.Method == methodUse && len(isStatic) == 0 {
// Check if stack tail is the same use route
for m := range intMethod {
end := len(app.stack[m]) - 1
if end == -1 {
return false
}
if app.stack[m][end].Path != route.Path {
return false
}
}

// Append handlers directly
for m := range intMethod {
end := len(app.stack[m]) - 1
app.stack[m][end].Handlers = append(app.stack[m][end].Handlers, route.Handlers...)
}
route.Handlers = nil
return true
}

m := methodInt(route.Method)
end := len(app.stack[m]) - 1
if end == -1 {
return false
}

if app.stack[m][end].Method != route.Method || app.stack[m][end].Path != route.Path {
return false
}

app.stack[m][end].Handlers = append(app.stack[m][end].Handlers, route.Handlers...)
route.Handlers = nil

return true
}

func (app *App) addRoute(method string, route *Route, isStatic ...bool) {
// Give name to route if not defined
if route.Name == "" && len(route.Handlers) > 0 {
route.Name = utils.FunctionName(route.Handlers[0])
}
// Get unique HTTP method indentifier
m := methodInt(method)
// Add route to the stack
app.stack[m] = append(app.stack[m], route)

if !app.compressed(route, isStatic...) {
// Get unique HTTP method indentifier
m := methodInt(method)

if route.use && len(isStatic) == 0 {
// Add route to all methods' stack
for m := range intMethod {
app.stack[m] = append(app.stack[m], route)
}
} else {
// Add route to the specific method's stack
app.stack[m] = append(app.stack[m], route)

// Handle GET routes on HEAD requests
if method == MethodGet {
app.stack[1] = append(app.stack[1], route)
}
}
}

// Add route to routes slice
app.routes = append(app.routes, route)
}
7 changes: 6 additions & 1 deletion router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,12 @@ func Benchmark_Router_Chain(b *testing.B) {
handler := func(c *Ctx) {
c.Next()
}
app.Get("/", handler, handler, handler, handler, handler, handler)
app.Get("/", handler)
Fenny marked this conversation as resolved.
Show resolved Hide resolved
app.Get("/", handler)
app.Get("/", handler)
app.Get("/", handler)
app.Get("/", handler)
app.Get("/", handler)

c := &fasthttp.RequestCtx{}

Expand Down