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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧹 chore: Improve Performance of Fiber Router #3261

Merged
merged 14 commits into from
Dec 29, 2024
12 changes: 9 additions & 3 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,10 +620,16 @@ func GetTrimmedParam(param string) string {

// RemoveEscapeChar remove escape characters
func RemoveEscapeChar(word string) string {
if strings.IndexByte(word, escapeChar) != -1 {
gaby marked this conversation as resolved.
Show resolved Hide resolved
return strings.ReplaceAll(word, string(escapeChar), "")
b := []byte(word)
dst := 0
for src := 0; src < len(b); src++ {
if b[src] == '\\' {
continue
}
b[dst] = b[src]
dst++
}
return word
return string(b[:dst])
}

func getParamConstraintType(constraintPart string) TypeConstraint {
Expand Down
123 changes: 60 additions & 63 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
package fiber

import (
"bytes"
"errors"
"fmt"
"html"
"sort"
"strings"
"sync/atomic"

"github.com/gofiber/utils/v2"
Expand Down Expand Up @@ -65,35 +65,48 @@

func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool {
// root detectionPath check
if r.root && detectionPath == "/" {
if r.root && len(detectionPath) == 1 && detectionPath[0] == '/' {
return true
// '*' wildcard matches any detectionPath
} else if r.star {
}

// '*' wildcard matches any detectionPath
if r.star {
if len(path) > 1 {
params[0] = path[1:]
} else {
params[0] = ""
}
return true
}
// Does this route have parameters

// Does this route have parameters?
if len(r.Params) > 0 {
// Match params
if match := r.routeParser.getMatch(detectionPath, path, params, r.use); match {
// Get params from the path detectionPath
return match
// Match params using precomputed routeParser
if r.routeParser.getMatch(detectionPath, path, params, r.use) {
return true
}
}
// Is this route a Middleware?

// Middleware route?
if r.use {
// Single slash will match or detectionPath prefix
if r.root || strings.HasPrefix(detectionPath, r.path) {
// Single slash or prefix match
plen := len(r.path)
if r.root {
// If r.root is '/', it matches everything starting at '/'
// Actually, if it's a middleware root, it should match always at '/'
if len(detectionPath) > 0 && detectionPath[0] == '/' {
return true
}
} else if len(detectionPath) >= plen && detectionPath[:plen] == r.path {
return true
}
} else {

Check failure on line 103 in router.go

View workflow job for this annotation

GitHub Actions / lint

elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic)
// Check exact match
if len(r.path) == len(detectionPath) && detectionPath == r.path {
return true
}
// Check for a simple detectionPath match
} else if len(r.path) == len(detectionPath) && r.path == detectionPath {
return true
}

// No match
return false
}
Expand Down Expand Up @@ -202,7 +215,7 @@
}

func (app *App) requestHandler(rctx *fasthttp.RequestCtx) {
// Handler for default ctxs
// Acquire context
var c CustomCtx
var ok bool
if app.newCtxFunc != nil {
Expand All @@ -224,8 +237,9 @@
return
}

// check flash messages
if strings.Contains(utils.UnsafeString(c.Request().Header.RawHeaders()), FlashCookieName) {
// Efficient flash cookie check using bytes.Index
rawHeaders := c.Request().Header.RawHeaders()
if len(rawHeaders) > 0 && bytes.Index(rawHeaders, []byte(FlashCookieName)) != -1 {

Check failure on line 242 in router.go

View workflow job for this annotation

GitHub Actions / lint

wrapperFunc: suggestion: bytes.Contains(rawHeaders, []byte(FlashCookieName)) (gocritic)

Check failure on line 242 in router.go

View workflow job for this annotation

GitHub Actions / lint

S1003: should use bytes.Contains(rawHeaders, []byte(FlashCookieName)) instead (gosimple)
c.Redirect().parseAndClearFlashMessages()
}

Expand All @@ -236,6 +250,7 @@
} else {
_, err = app.next(c.(*DefaultCtx)) //nolint:errcheck // It is fine to ignore the error here
}

if err != nil {
if catch := c.App().ErrorHandler(c, err); catch != nil {
_ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here
Expand Down Expand Up @@ -295,81 +310,63 @@
handlers = append(handlers, handler)
}

// Precompute path normalization ONCE
if pathRaw == "" {
pathRaw = "/"
}
if pathRaw[0] != '/' {
pathRaw = "/" + pathRaw
}
pathPretty := pathRaw
if !app.config.CaseSensitive {
pathPretty = utils.ToLower(pathPretty)
}
if !app.config.StrictRouting && len(pathPretty) > 1 {
pathPretty = utils.TrimRight(pathPretty, '/')
}
pathClean := RemoveEscapeChar(pathPretty)

parsedRaw := parseRoute(pathRaw, app.customConstraints...)
parsedPretty := parseRoute(pathPretty, app.customConstraints...)

for _, method := range methods {
// Uppercase HTTP methods
method = utils.ToUpper(method)
// Check if the HTTP method is valid unless it's USE
if method != methodUse && app.methodInt(method) == -1 {
panic(fmt.Sprintf("add: invalid http method %s\n", method))
}
// is mounted app

isMount := group != nil && group.app != app
// A route requires atleast one ctx handler
if len(handlers) == 0 && !isMount {
panic(fmt.Sprintf("missing handler/middleware in route: %s\n", pathRaw))
}
// Cannot have an empty path
if pathRaw == "" {
pathRaw = "/"
}
// Path always start with a '/'
if pathRaw[0] != '/' {
pathRaw = "/" + pathRaw
}
// Create a stripped path in case-sensitive / trailing slashes
pathPretty := pathRaw
// Case-sensitive routing, all to lowercase
if !app.config.CaseSensitive {
pathPretty = utils.ToLower(pathPretty)
}
// Strict routing, remove trailing slashes
if !app.config.StrictRouting && len(pathPretty) > 1 {
pathPretty = utils.TrimRight(pathPretty, '/')
}
// Is layer a middleware?

isUse := method == methodUse
// Is path a direct wildcard?
isStar := pathPretty == "/*"
// Is path a root slash?
isRoot := pathPretty == "/"
// Parse path parameters
parsedRaw := parseRoute(pathRaw, app.customConstraints...)
parsedPretty := parseRoute(pathPretty, app.customConstraints...)

// Create route metadata without pointer
isStar := pathClean == "/*"
isRoot := pathClean == "/"

route := Route{
// Router booleans
use: isUse,
mount: isMount,
star: isStar,
root: isRoot,

// Path data
path: RemoveEscapeChar(pathPretty),
path: pathClean,
routeParser: parsedPretty,
Params: parsedRaw.params,
group: group,

// Group data
group: group,

// Public data
Path: pathRaw,
Method: method,
Handlers: handlers,
}
// Increment global handler count
atomic.AddUint32(&app.handlersCount, uint32(len(handlers))) //nolint:gosec // Not a concern

// Middleware route matches all HTTP methods
atomic.AddUint32(&app.handlersCount, uint32(len(handlers)))

Check failure on line 363 in router.go

View workflow job for this annotation

GitHub Actions / lint

G115: integer overflow conversion int -> uint32 (gosec)
if isUse {
// Add route to all HTTP methods stack
for _, m := range app.config.RequestMethods {
// Create a route copy to avoid duplicates during compression
r := route
app.addRoute(m, &r, isMount)
}
} else {
// Add route to stack
app.addRoute(method, &route, isMount)
}
}
Expand Down
23 changes: 23 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,29 @@ func Benchmark_Router_Next_Default(b *testing.B) {
}
}

// go test -benchmem -run=^$ -bench ^Benchmark_Router_Next_Default_Parallel$ github.com/gofiber/fiber/v3 -count=1
func Benchmark_Router_Next_Default_Parallel(b *testing.B) {
app := New()
app.Get("/", func(_ Ctx) error {
return nil
})

h := app.Handler()

b.ReportAllocs()
b.ResetTimer()

b.RunParallel(func(pb *testing.PB) {
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(MethodGet)
fctx.Request.SetRequestURI("/")

for pb.Next() {
h(fctx)
}
})
}

// go test -v ./... -run=^$ -bench=Benchmark_Route_Match -benchmem -count=4
func Benchmark_Route_Match(b *testing.B) {
var match bool
Expand Down
Loading