Skip to content

Commit

Permalink
Merge pull request #33 from pressly/zeroalloc
Browse files Browse the repository at this point in the history
Reuse root context in a sync pool to reduce GC: zero-alloc routing
  • Loading branch information
Peter Kieltyka committed Mar 31, 2016
2 parents 3158fd2 + 93dc859 commit c420d7f
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 138 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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")`
50 changes: 26 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)))
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions _examples/rest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
49 changes: 0 additions & 49 deletions _examples/rest/render/render.go

This file was deleted.

2 changes: 1 addition & 1 deletion _examples/simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
Expand Down
17 changes: 13 additions & 4 deletions chi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}
60 changes: 60 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -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 = ""
}
44 changes: 23 additions & 21 deletions mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package chi
import (
"fmt"
"net/http"
"sync"

"golang.org/x/net/context"
)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
})

Expand All @@ -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) {
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand Down
Loading

0 comments on commit c420d7f

Please sign in to comment.