net/http: speed up ServeMux matching
Scanning through all path patterns is not necessary, since the
paths do not change frequently. Instead, maintain a sorted list
of path prefixes and return the first match.
name old time/op new time/op delta
ServerMatch-12 134ns ± 3% 17ns ± 4% -86.95% (p=0.000 n=19+20)
Change-Id: I15b4483dc30db413321435ee6815fc9bf2bcc546
Reviewed-on: https://go-review.googlesource.com/c/144937
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/src/net/http/server.go b/src/net/http/server.go
index 6e1ccff..a7e79c2d 100644
--- a/src/net/http/server.go
+++ b/src/net/http/server.go
@@ -22,6 +22,7 @@
"os"
"path"
"runtime"
+ "sort"
"strconv"
"strings"
"sync"
@@ -2179,7 +2180,8 @@
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
- hosts bool // whether any patterns contain hostnames
+ es []muxEntry // slice of entries sorted from longest to shortest.
+ hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
@@ -2195,19 +2197,6 @@
var defaultServeMux ServeMux
-// Does path match pattern?
-func pathMatch(pattern, path string) bool {
- if len(pattern) == 0 {
- // should not happen
- return false
- }
- n := len(pattern)
- if pattern[n-1] != '/' {
- return pattern == path
- }
- return len(path) >= n && path[0:n] == pattern
-}
-
// cleanPath returns the canonical path for p, eliminating . and .. elements.
func cleanPath(p string) string {
if p == "" {
@@ -2252,19 +2241,14 @@
return v.h, v.pattern
}
- // Check for longest valid match.
- var n = 0
- for k, v := range mux.m {
- if !pathMatch(k, path) {
- continue
- }
- if h == nil || len(k) > n {
- n = len(k)
- h = v.h
- pattern = v.pattern
+ // Check for longest valid match. mux.es contains all patterns
+ // that end in / sorted from longest to shortest.
+ for _, e := range mux.es {
+ if strings.HasPrefix(path, e.pattern) {
+ return e.h, e.pattern
}
}
- return
+ return nil, ""
}
// redirectToPathSlash determines if the given path needs appending "/" to it.
@@ -2410,13 +2394,32 @@
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
- mux.m[pattern] = muxEntry{h: handler, pattern: pattern}
+ e := muxEntry{h: handler, pattern: pattern}
+ mux.m[pattern] = e
+ if pattern[len(pattern)-1] == '/' {
+ mux.es = appendSorted(mux.es, e)
+ }
if pattern[0] != '/' {
mux.hosts = true
}
}
+func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
+ n := len(es)
+ i := sort.Search(n, func(i int) bool {
+ return len(es[i].pattern) < len(e.pattern)
+ })
+ if i == n {
+ return append(es, e)
+ }
+ // we now know that i points at where we want to insert
+ es = append(es, muxEntry{}) // try to grow the slice in place, any entry works.
+ copy(es[i+1:], es[i:]) // Move shorter entries down
+ es[i] = e
+ return es
+}
+
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
diff --git a/src/net/http/server_test.go b/src/net/http/server_test.go
new file mode 100644
index 0000000..0132f3b
--- /dev/null
+++ b/src/net/http/server_test.go
@@ -0,0 +1,45 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Server unit tests
+
+package http
+
+import (
+ "fmt"
+ "testing"
+)
+
+func BenchmarkServerMatch(b *testing.B) {
+ fn := func(w ResponseWriter, r *Request) {
+ fmt.Fprintf(w, "OK")
+ }
+ mux := NewServeMux()
+ mux.HandleFunc("/", fn)
+ mux.HandleFunc("/index", fn)
+ mux.HandleFunc("/home", fn)
+ mux.HandleFunc("/about", fn)
+ mux.HandleFunc("/contact", fn)
+ mux.HandleFunc("/robots.txt", fn)
+ mux.HandleFunc("/products/", fn)
+ mux.HandleFunc("/products/1", fn)
+ mux.HandleFunc("/products/2", fn)
+ mux.HandleFunc("/products/3", fn)
+ mux.HandleFunc("/products/3/image.jpg", fn)
+ mux.HandleFunc("/admin", fn)
+ mux.HandleFunc("/admin/products/", fn)
+ mux.HandleFunc("/admin/products/create", fn)
+ mux.HandleFunc("/admin/products/update", fn)
+ mux.HandleFunc("/admin/products/delete", fn)
+
+ paths := []string{"/", "/notfound", "/admin/", "/admin/foo", "/contact", "/products",
+ "/products/", "/products/3/image.jpg"}
+ b.StartTimer()
+ for i := 0; i < b.N; i++ {
+ if h, p := mux.match(paths[i%len(paths)]); h != nil && p == "" {
+ b.Error("impossible")
+ }
+ }
+ b.StopTimer()
+}