diff --git a/mux.go b/mux.go index 1e089906..6f45b998 100644 --- a/mux.go +++ b/mux.go @@ -19,6 +19,10 @@ var ( ErrMethodMismatch = errors.New("method is not allowed") // ErrNotFound is returned when no route match is found. ErrNotFound = errors.New("no matching route was found") + // RegexpCompileFunc aliases regexp.Compile and enables overriding it. + // Do not run this function from `init()` in importable packages. + // Changing this value is not safe for concurrent use. + RegexpCompileFunc = regexp.Compile ) // NewRouter returns a new router instance. @@ -524,7 +528,7 @@ func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { } m := make(map[string]*regexp.Regexp, length/2) for i := 0; i < length; i += 2 { - regex, err := regexp.Compile(pairs[i+1]) + regex, err := RegexpCompileFunc(pairs[i+1]) if err != nil { return nil, err } diff --git a/regexp.go b/regexp.go index 5d05cfa0..0baba6a4 100644 --- a/regexp.go +++ b/regexp.go @@ -93,7 +93,7 @@ func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*ro // Append variable name and compiled pattern. varsN[i/2] = name - varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + varsR[i/2], err = RegexpCompileFunc(fmt.Sprintf("^%s$", patt)) if err != nil { return nil, err } @@ -125,7 +125,7 @@ func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*ro reverse.WriteByte('/') } // Compile full regexp. - reg, errCompile := regexp.Compile(pattern.String()) + reg, errCompile := RegexpCompileFunc(pattern.String()) if errCompile != nil { return nil, errCompile } diff --git a/route_test.go b/route_test.go new file mode 100644 index 00000000..5aa35d11 --- /dev/null +++ b/route_test.go @@ -0,0 +1,66 @@ +package mux + +import ( + "net/http" + "regexp" + "sync" + "testing" +) + +var testNewRouterMu sync.Mutex +var testHandler = http.NotFoundHandler() + +func BenchmarkNewRouter(b *testing.B) { + testNewRouterMu.Lock() + defer testNewRouterMu.Unlock() + + // Set the RegexpCompileFunc to the default regexp.Compile. + RegexpCompileFunc = regexp.Compile + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + testNewRouter(b, testHandler) + } +} + +func BenchmarkNewRouterRegexpFunc(b *testing.B) { + testNewRouterMu.Lock() + defer testNewRouterMu.Unlock() + + // We preallocate the size to 8. + cache := make(map[string]*regexp.Regexp, 8) + + // Override the RegexpCompileFunc to reuse compiled expressions + // from the `cache` map. Real world caches should have eviction + // policies or some sort of approach to limit memory use. + RegexpCompileFunc = func(expr string) (*regexp.Regexp, error) { + if regex, ok := cache[expr]; ok { + return regex, nil + } + + regex, err := regexp.Compile(expr) + if err != nil { + return nil, err + } + + cache[expr] = regex + return regex, nil + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + testNewRouter(b, testHandler) + } +} + +func testNewRouter(_ testing.TB, handler http.Handler) { + r := NewRouter() + // A route with a route variable: + r.Handle("/metrics/{type}", handler) + r.Queries("orgID", "{orgID:[0-9]*?}") + r.Host("{subdomain}.domain.com") +}