From cad89a07e0fe6359870537acc43d8d52b726cdd3 Mon Sep 17 00:00:00 2001 From: Tw Date: Wed, 26 Apr 2017 01:23:50 -0500 Subject: [PATCH] gzip: pool gzip.Writer to reduce allocation (#1618) * gzip: add benchmark Signed-off-by: Tw * gzip: pool gzip.Writer to reduce allocation Signed-off-by: Tw --- caddyhttp/gzip/gzip.go | 22 +++------------- caddyhttp/gzip/gzip_test.go | 51 ++++++++++++++++++++++++++++++++++++ caddyhttp/gzip/setup.go | 52 +++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 18 deletions(-) diff --git a/caddyhttp/gzip/gzip.go b/caddyhttp/gzip/gzip.go index 2389fd99..3d34ca27 100644 --- a/caddyhttp/gzip/gzip.go +++ b/caddyhttp/gzip/gzip.go @@ -4,9 +4,7 @@ package gzip import ( "bufio" - "compress/gzip" "io" - "io/ioutil" "net" "net/http" "strings" @@ -22,6 +20,8 @@ func init() { ServerType: "http", Action: setup, }) + + initWriterPool() } // Gzip is a middleware type which gzips HTTP responses. It is @@ -58,12 +58,8 @@ outer: // gzipWriter modifies underlying writer at init, // use a discard writer instead to leave ResponseWriter in // original form. - gzipWriter, err := newWriter(c, ioutil.Discard) - if err != nil { - // should not happen - return http.StatusInternalServerError, err - } - defer gzipWriter.Close() + gzipWriter := getWriter(c.Level) + defer putWriter(c.Level, gzipWriter) gz := &gzipResponseWriter{Writer: gzipWriter, ResponseWriter: w} var rw http.ResponseWriter @@ -94,16 +90,6 @@ outer: return g.Next.ServeHTTP(w, r) } -// newWriter create a new Gzip Writer based on the compression level. -// If the level is valid (i.e. between 1 and 9), it uses the level. -// Otherwise, it uses default compression level. -func newWriter(c Config, w io.Writer) (*gzip.Writer, error) { - if c.Level >= gzip.BestSpeed && c.Level <= gzip.BestCompression { - return gzip.NewWriterLevel(w, c.Level) - } - return gzip.NewWriter(w), nil -} - // gzipResponeWriter wraps the underlying Write method // with a gzip.Writer to compress the output. type gzipResponseWriter struct { diff --git a/caddyhttp/gzip/gzip_test.go b/caddyhttp/gzip/gzip_test.go index 5c57d931..ff94edb8 100644 --- a/caddyhttp/gzip/gzip_test.go +++ b/caddyhttp/gzip/gzip_test.go @@ -1,6 +1,7 @@ package gzip import ( + "compress/gzip" "fmt" "io/ioutil" "net/http" @@ -77,6 +78,22 @@ func TestGzipHandler(t *testing.T) { t.Error(err) } } + + // test all levels + w = httptest.NewRecorder() + gz.Next = nextFunc(true) + for i := 0; i <= gzip.BestCompression; i++ { + gz.Configs[0].Level = i + r, err := http.NewRequest("GET", "/file.txt", nil) + if err != nil { + t.Error(err) + } + r.Header.Set("Accept-Encoding", "gzip") + _, err = gz.ServeHTTP(w, r) + if err != nil { + t.Error(err) + } + } } func nextFunc(shouldGzip bool) httpserver.Handler { @@ -117,3 +134,37 @@ func nextFunc(shouldGzip bool) httpserver.Handler { return 0, nil }) } + +func BenchmarkGzip(b *testing.B) { + pathFilter := PathFilter{make(Set)} + badPaths := []string{"/bad", "/nogzip", "/nongzip"} + for _, p := range badPaths { + pathFilter.IgnoredPaths.Add(p) + } + extFilter := ExtFilter{make(Set)} + for _, e := range []string{".txt", ".html", ".css", ".md"} { + extFilter.Exts.Add(e) + } + gz := Gzip{Configs: []Config{ + { + RequestFilters: []RequestFilter{pathFilter, extFilter}, + }, + }} + + w := httptest.NewRecorder() + gz.Next = nextFunc(true) + url := "/file.txt" + r, err := http.NewRequest("GET", url, nil) + if err != nil { + b.Fatal(err) + } + r.Header.Set("Accept-Encoding", "gzip") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err = gz.ServeHTTP(w, r) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/caddyhttp/gzip/setup.go b/caddyhttp/gzip/setup.go index 613b6497..f86d89c5 100644 --- a/caddyhttp/gzip/setup.go +++ b/caddyhttp/gzip/setup.go @@ -1,9 +1,12 @@ package gzip import ( + "compress/gzip" "fmt" + "io/ioutil" "strconv" "strings" + "sync" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" @@ -119,3 +122,52 @@ func gzipParse(c *caddy.Controller) ([]Config, error) { return configs, nil } + +// pool gzip.Writer according to compress level +// so we can reuse allocations over time +var ( + writerPool = map[int]*sync.Pool{} + defaultWriterPoolIndex int +) + +func initWriterPool() { + var i int + newWriterPool := func(level int) *sync.Pool { + return &sync.Pool{ + New: func() interface{} { + w, _ := gzip.NewWriterLevel(ioutil.Discard, level) + return w + }, + } + } + for i = gzip.BestSpeed; i <= gzip.BestCompression; i++ { + writerPool[i] = newWriterPool(i) + } + + // add default writer pool + defaultWriterPoolIndex = i + writerPool[defaultWriterPoolIndex] = &sync.Pool{ + New: func() interface{} { + return gzip.NewWriter(ioutil.Discard) + }, + } +} + +func getWriter(level int) *gzip.Writer { + index := defaultWriterPoolIndex + if level >= gzip.BestSpeed && level <= gzip.BestCompression { + index = level + } + w := writerPool[index].Get().(*gzip.Writer) + w.Reset(ioutil.Discard) + return w +} + +func putWriter(level int, w *gzip.Writer) { + index := defaultWriterPoolIndex + if level >= gzip.BestSpeed && level <= gzip.BestCompression { + index = level + } + w.Close() + writerPool[index].Put(w) +}