diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..2d261e87 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## 0.9.0 (2016-03-31) + +- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/pressly/chi/pull/33) +- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters + has changed to: `chi.URLParam(ctx, "id")` diff --git a/README.md b/README.md index 6bb1e87e..7cfab653 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,12 @@ scaled very well. ## Features -* Lightweight - cloc`d in ~600 LOC for the chi router -* Fast - yes, see [benchmarks](#benchmarks) -* Expressive routing - middlewares, inline middleware groups/chains, and subrouter mounting -* Request context control (value chaining, deadlines and timeouts) - built on `net/context` -* Robust (tested, used in production) +* **Lightweight** - cloc'd in <1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](#benchmarks) +* **Zero allocations** - no GC pressure during routing +* **Designed for modular/composable APIs** - middlewares, inline middleware groups/chains, and subrouter mounting +* **Context control** - built on `net/context` with value chaining, deadlines and timeouts +* **Robust** - tested / used in production ## Router design @@ -122,7 +123,7 @@ func StdHandler(w http.ResponseWriter, r *http.Request) { ```go // net/context HTTP request handler func CtxHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { - userID := chi.URLParams(ctx)["userID"] // from a route like /users/:userID + userID := chi.URLParam(ctx, "userID") // from a route like /users/:userID key := ctx.Value("key").(string) w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) } @@ -200,7 +201,7 @@ func main() { func ArticleCtx(next chi.Handler) chi.Handler { return chi.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - articleID := chi.URLParams(ctx)["articleID"] + articleID := chi.URLParam(ctx, "articleID") article, err := dbGetArticle(articleID) if err != nil { http.Error(w, http.StatusText(404), 404) @@ -284,11 +285,24 @@ See discussions: The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark -The results as of Nov. 6, 2015 - https://gist.github.com/pkieltyka/505b07b09f5c63e36ef5 - -Note: by design, chi allocates new routing URLParams map for each request, as opposed -to reusing URLParams from a pool. - +```shell +BenchmarkChi_Param 10000000 181 ns/op 0 B/op 0 allocs/op +BenchmarkChi_Param5 3000000 570 ns/op 0 B/op 0 allocs/op +BenchmarkChi_Param20 1000000 2057 ns/op 0 B/op 0 allocs/op +BenchmarkChi_ParamWrite 5000000 245 ns/op 0 B/op 0 allocs/op +BenchmarkChi_GithubStatic 5000000 250 ns/op 0 B/op 0 allocs/op +BenchmarkChi_GithubParam 2000000 589 ns/op 0 B/op 0 allocs/op +BenchmarkChi_GithubAll 10000 102664 ns/op 0 B/op 0 allocs/op +BenchmarkChi_GPlusStatic 10000000 161 ns/op 0 B/op 0 allocs/op +BenchmarkChi_GPlusParam 5000000 291 ns/op 0 B/op 0 allocs/op +BenchmarkChi_GPlus2Params 5000000 393 ns/op 0 B/op 0 allocs/op +BenchmarkChi_GPlusAll 300000 4335 ns/op 0 B/op 0 allocs/op +BenchmarkChi_ParseStatic 10000000 162 ns/op 0 B/op 0 allocs/op +BenchmarkChi_ParseParam 10000000 227 ns/op 0 B/op 0 allocs/op +BenchmarkChi_Parse2Params 5000000 327 ns/op 0 B/op 0 allocs/op +BenchmarkChi_ParseAll 200000 7368 ns/op 0 B/op 0 allocs/op +BenchmarkChi_StaticAll 30000 57990 ns/op 0 B/op 0 allocs/op +``` ## Credits @@ -298,18 +312,6 @@ to reusing URLParams from a pool. * Armon Dadgar for https://github.com/armon/go-radix * Contributions: [@VojtechVitek](https://github.com/VojtechVitek) - -## TODO - -* Mux options - * Trailing slash? - * Case insensitive paths? - * GET for HEAD requests (auto fallback)? -* Register error handler (500's), ServerError() handler? -* HTTP2 example - * both http 1.1 and http2 automatically.. just turn it on :) -* Regexp support in router "/:id([0-9]+)" or "#id^[0-9]+$" or .. - We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! ## License diff --git a/_examples/rest/main.go b/_examples/rest/main.go index 5e584ea0..34afb6c2 100644 --- a/_examples/rest/main.go +++ b/_examples/rest/main.go @@ -9,8 +9,8 @@ import ( "time" "github.com/pressly/chi" - "github.com/pressly/chi/_examples/rest/render" "github.com/pressly/chi/middleware" + "github.com/pressly/chi/render" "golang.org/x/net/context" ) @@ -113,7 +113,7 @@ type Article struct { func ArticleCtx(next chi.Handler) chi.Handler { return chi.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - articleID := chi.URLParams(ctx)["articleID"] + articleID := chi.URLParam(ctx, "articleID") article, err := dbGetArticle(articleID) if err != nil { http.Error(w, http.StatusText(404), 404) @@ -229,7 +229,7 @@ func adminRouter() http.Handler { // or chi.Router { w.Write([]byte("admin: list accounts..")) }) r.Get("/users/:userId", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - w.Write([]byte(fmt.Sprintf("admin: view user id %v", chi.URLParams(ctx)["userId"]))) + w.Write([]byte(fmt.Sprintf("admin: view user id %v", chi.URLParam(ctx, "userId")))) }) return r } diff --git a/_examples/rest/render/render.go b/_examples/rest/render/render.go deleted file mode 100644 index 2535818a..00000000 --- a/_examples/rest/render/render.go +++ /dev/null @@ -1,49 +0,0 @@ -// Responder as a wrapper of https://github.com/unrolled/render -// -- -// This is a good base to work off and extend for your own uses, or build your own. -// For example, adding pagination to JSON() where you add a "Link" header that is -// a cursor URL to the next page of results (check how GitHubb does it in their API docs). -// The power is yours. -package render - -import ( - "net/http" - - renderer "github.com/unrolled/render" -) - -var ( - Renderer *renderer.Render -) - -func init() { - Renderer = renderer.New() -} - -func Render(w http.ResponseWriter, e renderer.Engine, data interface{}) error { - return Renderer.Render(w, e, data) -} - -func Data(w http.ResponseWriter, status int, v []byte) error { - return Renderer.Data(w, status, v) -} - -func HTML(w http.ResponseWriter, status int, name string, binding interface{}, htmlOpt ...renderer.HTMLOptions) error { - return Renderer.HTML(w, status, name, binding, htmlOpt...) -} - -func JSON(w http.ResponseWriter, status int, v interface{}) error { - return Renderer.JSON(w, status, v) -} - -func JSONP(w http.ResponseWriter, status int, callback string, v interface{}) error { - return Renderer.JSONP(w, status, callback, v) -} - -func Text(w http.ResponseWriter, status int, v string) error { - return Renderer.Text(w, status, v) -} - -func XML(w http.ResponseWriter, status int, v interface{}) error { - return Renderer.XML(w, status, v) -} diff --git a/_examples/simple/main.go b/_examples/simple/main.go index f6cb00b2..cc6bec83 100644 --- a/_examples/simple/main.go +++ b/_examples/simple/main.go @@ -108,7 +108,7 @@ func createAccount(w http.ResponseWriter, r *http.Request) { } func getAccount(ctx context.Context, w http.ResponseWriter, r *http.Request) { - accountID := chi.URLParams(ctx)["accountID"] + accountID := chi.URLParam(ctx, "accountID") account := ctx.Value("account").(string) w.Write([]byte(fmt.Sprintf("get account id:%s details:%s", accountID, account))) } diff --git a/chi.go b/chi.go index f0adad79..31203296 100644 --- a/chi.go +++ b/chi.go @@ -58,9 +58,18 @@ func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { h(context.Background(), w, r) } -func URLParams(ctx context.Context) map[string]string { - if urlParams, ok := ctx.Value(URLParamsCtxKey).(map[string]string); ok { - return urlParams +// Returns the root level chi Context object +func RootContext(ctx context.Context) *Context { + rctx, _ := ctx.(*Context) + if rctx == nil { + rctx = ctx.Value(rootCtxKey).(*Context) } - return nil + return rctx +} + +func URLParam(ctx context.Context, key string) string { + if rctx := RootContext(ctx); rctx != nil { + return rctx.Param(key) + } + return "" } diff --git a/context.go b/context.go new file mode 100644 index 00000000..a132fa79 --- /dev/null +++ b/context.go @@ -0,0 +1,60 @@ +package chi + +import "golang.org/x/net/context" + +var _ context.Context = &Context{} + +type ctxKey int + +const ( + rootCtxKey ctxKey = iota +) + +type Context struct { + context.Context + + // URL parameter key and values + pkeys, pvalues []string + + // Routing path override + routePath string +} + +func newContext() *Context { + rctx := &Context{} + ctx := context.WithValue(context.Background(), rootCtxKey, rctx) + rctx.Context = ctx + return rctx +} + +func (x *Context) Param(key string) string { + for i, k := range x.pkeys { + if k == key { + return x.pvalues[i] + } + } + return "" +} + +func (x *Context) addParam(key string, value string) { + x.pkeys = append(x.pkeys, key) + x.pvalues = append(x.pvalues, value) +} + +func (x *Context) delParam(key string) string { + for i, k := range x.pkeys { + if k == key { + v := x.pvalues[i] + x.pkeys = append(x.pkeys[:i], x.pkeys[i+1:]...) + x.pvalues = append(x.pvalues[:i], x.pvalues[i+1:]...) + return v + } + } + return "" +} + +func (x *Context) reset() { + x.pkeys = x.pkeys[:0] + x.pvalues = x.pvalues[:0] + x.routePath = "" +} diff --git a/mux.go b/mux.go index 3f543009..b71acb0a 100644 --- a/mux.go +++ b/mux.go @@ -3,6 +3,7 @@ package chi import ( "fmt" "net/http" + "sync" "golang.org/x/net/context" ) @@ -23,6 +24,9 @@ type Mux struct { // Controls the behaviour of middleware chain generation when a mux // is registered as an inline group inside another mux. inline bool + + // Routing context pool + pool sync.Pool } type methodTyp int @@ -54,15 +58,12 @@ var methodMap = map[string]methodTyp{ "TRACE": mTRACE, } -type ctxKey int - -const ( - URLParamsCtxKey ctxKey = iota - SubRouterCtxKey -) - func NewMux() *Mux { - return &Mux{router: newTreeRouter(), handler: nil} + mux := &Mux{router: newTreeRouter(), handler: nil} + mux.pool.New = func() interface{} { + return newContext() + } + return mux } // Append to the middleware stack @@ -188,8 +189,8 @@ func (mx *Mux) Mount(path string, handlers ...interface{}) { // Wrap the sub-router in a handlerFunc to scope the request path for routing. subHandler := HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - path := URLParams(ctx)["*"] - ctx = context.WithValue(ctx, SubRouterCtxKey, "/"+path) + rctx := RootContext(ctx) + rctx.routePath = "/" + rctx.delParam("*") h.ServeHTTPC(ctx, w, r) }) @@ -202,7 +203,10 @@ func (mx *Mux) Mount(path string, handlers ...interface{}) { } func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - mx.ServeHTTPC(context.Background(), w, r) + ctx := mx.pool.Get().(*Context) + mx.ServeHTTPC(ctx, w, r) + ctx.reset() + mx.pool.Put(ctx) } func (mx *Mux) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) { @@ -238,18 +242,15 @@ func (tr treeRouter) NotFoundHandlerFn() HandlerFunc { } func (tr treeRouter) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) { - // Allocate a new url params map at the start of each request. - params, ok := ctx.Value(URLParamsCtxKey).(map[string]string) - if !ok || params == nil { - params = make(map[string]string, 0) - ctx = context.WithValue(ctx, URLParamsCtxKey, params) + // Grab the root context object + rctx, _ := ctx.(*Context) + if rctx == nil { + rctx = ctx.Value(rootCtxKey).(*Context) } // The request path - routePath, ok := ctx.Value(SubRouterCtxKey).(string) - if ok { - delete(params, "*") - } else { + routePath := rctx.routePath + if routePath == "" { routePath = r.URL.Path } @@ -261,7 +262,8 @@ func (tr treeRouter) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *h } // Find the handler in the router - cxh := tr.routes[method].Find(routePath, params) + cxh := tr.routes[method].Find(rctx, routePath) + if cxh == nil { tr.NotFoundHandlerFn().ServeHTTPC(ctx, w, r) return diff --git a/mux_test.go b/mux_test.go index 05da20d8..a2e8ac0c 100644 --- a/mux_test.go +++ b/mux_test.go @@ -80,7 +80,7 @@ func TestMux(t *testing.T) { _ = pingAll2 pingOne := func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - idParam := URLParams(ctx)["id"] // from outside: chi.URLParams(ctx) + idParam := URLParam(ctx, "id") w.WriteHeader(200) w.Write([]byte(fmt.Sprintf("ping one id: %s", idParam))) @@ -436,11 +436,11 @@ func TestMuxBig(t *testing.T) { w.Write([]byte("fav")) }) r.Get("/hubs/:hubID/view", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - s := fmt.Sprintf("/hubs/%s/view reqid:%s", URLParams(ctx)["hubID"], ctx.Value("requestID")) + s := fmt.Sprintf("/hubs/%s/view reqid:%s", URLParam(ctx, "hubID"), ctx.Value("requestID")) w.Write([]byte(s)) }) r.Get("/hubs/:hubID/view/*", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - s := fmt.Sprintf("/hubs/%s/view/%s reqid:%s", URLParams(ctx)["hubID"], URLParams(ctx)["*"], + s := fmt.Sprintf("/hubs/%s/view/%s reqid:%s", URLParam(ctx, "hubID"), URLParam(ctx, "*"), ctx.Value("requestID")) w.Write([]byte(s)) }) @@ -462,7 +462,7 @@ func TestMuxBig(t *testing.T) { }) r.Get("/woot/:wootID/*", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - s := fmt.Sprintf("/woot/%s/%s", URLParams(ctx)["wootID"], URLParams(ctx)["*"]) + s := fmt.Sprintf("/woot/%s/%s", URLParam(ctx, "wootID"), URLParam(ctx, "*")) w.Write([]byte(s)) }) @@ -472,26 +472,26 @@ func TestMuxBig(t *testing.T) { sr2 = r.(*Mux) r.Get("/", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { s := fmt.Sprintf("/hubs/%s reqid:%s session:%s", - URLParams(ctx)["hubID"], ctx.Value("requestID"), ctx.Value("session.user")) + URLParam(ctx, "hubID"), ctx.Value("requestID"), ctx.Value("session.user")) w.Write([]byte(s)) }) r.Get("/touch", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - s := fmt.Sprintf("/hubs/%s/touch reqid:%s session:%s", URLParams(ctx)["hubID"], + s := fmt.Sprintf("/hubs/%s/touch reqid:%s session:%s", URLParam(ctx, "hubID"), ctx.Value("requestID"), ctx.Value("session.user")) w.Write([]byte(s)) }) sr3 = NewRouter() sr3.Get("/", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - s := fmt.Sprintf("/hubs/%s/webhooks reqid:%s session:%s", URLParams(ctx)["hubID"], + s := fmt.Sprintf("/hubs/%s/webhooks reqid:%s session:%s", URLParam(ctx, "hubID"), ctx.Value("requestID"), ctx.Value("session.user")) w.Write([]byte(s)) }) sr3.Route("/:webhookID", func(r Router) { sr4 = r.(*Mux) r.Get("/", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - s := fmt.Sprintf("/hubs/%s/webhooks/%s reqid:%s session:%s", URLParams(ctx)["hubID"], - URLParams(ctx)["webhookID"], ctx.Value("requestID"), ctx.Value("session.user")) + s := fmt.Sprintf("/hubs/%s/webhooks/%s reqid:%s session:%s", URLParam(ctx, "hubID"), + URLParam(ctx, "webhookID"), ctx.Value("requestID"), ctx.Value("session.user")) w.Write([]byte(s)) }) }) @@ -500,7 +500,7 @@ func TestMuxBig(t *testing.T) { r.Route("/posts", func(r Router) { sr5 = r.(*Mux) r.Get("/", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - s := fmt.Sprintf("/hubs/%s/posts reqid:%s session:%s", URLParams(ctx)["hubID"], + s := fmt.Sprintf("/hubs/%s/posts reqid:%s session:%s", URLParam(ctx, "hubID"), ctx.Value("requestID"), ctx.Value("session.user")) w.Write([]byte(s)) }) @@ -556,7 +556,7 @@ func TestMuxBig(t *testing.T) { ts := httptest.NewServer(r) defer ts.Close() - var resp string + var resp, expected string resp = testRequest(t, ts, "GET", "/favicon.ico", nil) if resp != "fav" { @@ -583,8 +583,9 @@ func TestMuxBig(t *testing.T) { t.Fatalf("got '%s'", resp) } resp = testRequest(t, ts, "GET", "/hubs/123", nil) - if resp != "/hubs/123 reqid:1 session:elvis" { - t.Fatalf("got '%s'", resp) + expected = "/hubs/123 reqid:1 session:elvis" + if resp != expected { + t.Fatalf("expected:%s got:%s", expected, resp) } resp = testRequest(t, ts, "GET", "/hubs/123/touch", nil) if resp != "/hubs/123/touch reqid:1 session:elvis" { @@ -681,28 +682,44 @@ func TestMuxSubroutes(t *testing.T) { ts := httptest.NewServer(r) defer ts.Close() - var resp string + var resp, expected string resp = testRequest(t, ts, "GET", "/hubs/123/view", nil) - if resp != "hub1" { - t.Fatalf("got '%s'", resp) + expected = "hub1" + if resp != expected { + t.Fatalf("expected:%s got:%s", expected, resp) } resp = testRequest(t, ts, "GET", "/hubs/123/view/index.html", nil) - if resp != "hub2" { - t.Fatalf("got '%s'", resp) + expected = "hub2" + if resp != expected { + t.Fatalf("expected:%s got:%s", expected, resp) } resp = testRequest(t, ts, "GET", "/hubs/123/users", nil) - if resp != "hub3" { - t.Fatalf("got '%s'", resp) + expected = "hub3" + if resp != expected { + t.Fatalf("expected:%s got:%s", expected, resp) } resp = testRequest(t, ts, "GET", "/accounts/44", nil) - if resp != "account1" { - t.Fatalf("got '%s'", resp) + expected = "account1" + if resp != expected { + t.Fatalf("request:%s expected:%s got:%s", "GET /accounts/44", expected, resp) } resp = testRequest(t, ts, "GET", "/accounts/44/hi", nil) - if resp != "account2" { - t.Fatalf("got '%s'", resp) + expected = "account2" + if resp != expected { + t.Fatalf("expected:%s got:%s", expected, resp) + } +} + +func urlParams(ctx context.Context) map[string]string { + if rctx := RootContext(ctx); rctx != nil { + m := make(map[string]string, 0) + for i, k := range rctx.pkeys { + m[k] = rctx.pvalues[i] + } + return m } + return nil } func testRequest(t *testing.T, ts *httptest.Server, method, path string, body io.Reader) string { diff --git a/tree.go b/tree.go index 2ad82d28..7e19b017 100644 --- a/tree.go +++ b/tree.go @@ -180,7 +180,7 @@ func (n *node) findEdge(ntyp nodeTyp, label byte) *node { // Recursive edge traversal by checking all nodeTyp groups along the way. // It's like searching through a three-dimensional radix trie. -func (n *node) findNode(path string, params map[string]string) *node { +func (n *node) findNode(ctx *Context, path string) *node { nn := n search := path @@ -213,9 +213,9 @@ func (n *node) findNode(path string, params map[string]string) *node { } if xn.typ == ntCatchAll { - params["*"] = xsearch + ctx.addParam("*", xsearch) } else { - params[xn.prefix[1:]] = xsearch[:p] + ctx.addParam(xn.prefix[1:], xsearch[:p]) } xsearch = xsearch[p:] @@ -233,7 +233,7 @@ func (n *node) findNode(path string, params map[string]string) *node { } // recursively find the next node.. - fin := xn.findNode(xsearch, params) + fin := xn.findNode(ctx, xsearch) if fin != nil { // found a node, return it return fin @@ -241,9 +241,9 @@ func (n *node) findNode(path string, params map[string]string) *node { // let's remove the param here if it was set if xn.typ > ntStatic { if xn.typ == ntCatchAll { - delete(params, "*") + ctx.delParam("*") } else { - delete(params, xn.prefix[1:]) + ctx.delParam(xn.prefix[1:]) } } } @@ -368,8 +368,8 @@ func (t *tree) Insert(pattern string, handler Handler) { return } -func (t *tree) Find(path string, params map[string]string) Handler { - node := t.root.findNode(path, params) +func (t *tree) Find(ctx *Context, path string) Handler { + node := t.root.findNode(ctx, path) if node == nil { return nil } diff --git a/tree_test.go b/tree_test.go index 06035c5b..fda63e59 100644 --- a/tree_test.go +++ b/tree_test.go @@ -141,8 +141,10 @@ func TestTree(t *testing.T) { // log.Println("~~~~~~~~~") for i, tt := range tests { - params := make(map[string]string, 0) - handler := tr.Find(tt.r, params) + // params := make(map[string]string, 0) + rctx := newContext() + handler := tr.Find(rctx, tt.r) //, params) + params := urlParams(rctx) if fmt.Sprintf("%v", tt.h) != fmt.Sprintf("%v", handler) { t.Errorf("input [%d]: find '%s' expecting handler:%v , got:%v", i, tt.r, tt.h, handler) } @@ -194,8 +196,9 @@ func BenchmarkTreeGet(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - params := map[string]string{} - tr.Find("/ping/123/456", params) + // params := map[string]string{} + mctx := newContext() + tr.Find(mctx, "/ping/123/456") // tr.Find("/pingggg", params) } }