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

perf: cut allocations #691

Merged
merged 7 commits into from
Dec 7, 2023
Merged
32 changes: 32 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,38 @@ func BenchmarkMux(b *testing.B) {
}
}

func BenchmarkMuxSimple(b *testing.B) {
router := new(Router)
handler := func(w http.ResponseWriter, r *http.Request) {}
router.HandleFunc("/status", handler)

testCases := []struct {
name string
omitRouteFromContext bool
}{
{
name: "default",
omitRouteFromContext: false,
},
{
name: "omit route from ctx",
omitRouteFromContext: true,
},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
router.OmitRouteFromContext(tc.omitRouteFromContext)

request, _ := http.NewRequest("GET", "/status", nil)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
router.ServeHTTP(nil, request)
}
})
}
}

func BenchmarkMuxAlternativeInRegexp(b *testing.B) {
router := new(Router)
handler := func(w http.ResponseWriter, r *http.Request) {}
Expand Down
57 changes: 45 additions & 12 deletions mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"path"
"regexp"
)
Expand Down Expand Up @@ -84,6 +85,9 @@ type routeConf struct {
// will not redirect
skipClean bool

// If true, the http.Request context will not contain the Route.
omitRouteFromContext bool

// Manager for the variables from host and path.
regexp routeRegexpGroup

Expand Down Expand Up @@ -180,15 +184,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
// Clean path to canonical form and redirect.
if p := cleanPath(path); p != path {

// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
// http://code.google.com/p/go/issues/detail?id=5252
url := *req.URL
url.Path = p
p = url.String()

w.Header().Set("Location", p)
w.Header().Set("Location", replaceURLPath(req.URL, p))
w.WriteHeader(http.StatusMovedPermanently)
return
}
Expand All @@ -197,8 +193,15 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var handler http.Handler
if r.Match(req, &match) {
handler = match.Handler
req = requestWithVars(req, match.Vars)
req = requestWithRoute(req, match.Route)
if handler != nil {
// Populate context for custom handlers
if r.omitRouteFromContext {
// Only populate the match vars (if any) into the context.
req = requestWithVars(req, match.Vars)
} else {
req = requestWithRouteAndVars(req, match.Route, match.Vars)
}
}
}

if handler == nil && match.MatchErr == ErrMethodMismatch {
Expand Down Expand Up @@ -260,6 +263,16 @@ func (r *Router) SkipClean(value bool) *Router {
return r
}

// OmitRouteFromContext defines the behavior of omitting the Route from the
//
// http.Request context.
//
// CurrentRoute will yield nil with this option.
func (r *Router) OmitRouteFromContext(value bool) *Router {
AlexVulaj marked this conversation as resolved.
Show resolved Hide resolved
r.omitRouteFromContext = value
return r
}

// UseEncodedPath tells the router to match the encoded original path
// to the routes.
// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to".
Expand Down Expand Up @@ -445,13 +458,25 @@ func CurrentRoute(r *http.Request) *Route {
return nil
}

// requestWithRouteAndVars adds the matched vars to the request ctx.
// It shortcuts the operation when the vars are empty.
func requestWithVars(r *http.Request, vars map[string]string) *http.Request {
if len(vars) == 0 {
return r
}
ctx := context.WithValue(r.Context(), varsKey, vars)
return r.WithContext(ctx)
}

func requestWithRoute(r *http.Request, route *Route) *http.Request {
// requestWithRouteAndVars adds the matched route and vars to the request ctx.
// It saves extra allocations in cloning the request once and skipping the
//
// population of empty vars, which in turn mux.Vars can handle gracefully.
func requestWithRouteAndVars(r *http.Request, route *Route, vars map[string]string) *http.Request {
ctx := context.WithValue(r.Context(), routeKey, route)
if len(vars) > 0 {
ctx = context.WithValue(ctx, varsKey, vars)
}
return r.WithContext(ctx)
}

Expand All @@ -478,6 +503,14 @@ func cleanPath(p string) string {
return np
}

// replaceURLPath prints an url.URL with a different path.
func replaceURLPath(u *url.URL, p string) string {
// Operate on a copy of the request url.
u2 := *u
u2.Path = p
return u2.String()
}

// uniqueVars returns an error if two slices contain duplicated strings.
func uniqueVars(s1, s2 []string) error {
for _, v1 := range s1 {
Expand Down
Loading
Loading