From 482e5fb4892587650020578d98863e6bba221dd9 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Fri, 27 Nov 2020 06:30:34 -0800 Subject: [PATCH 1/2] Switch compression package (3x faster) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use `github.com/klauspost/compress` package for compression. Benchmarks did not actually compress since the header was carried over between calls. Before/after with fix in place. ``` λ benchcmp before.txt after.txt benchmark old ns/op new ns/op delta BenchmarkSoleGin_SmallPayload-12 279 284 +1.79% BenchmarkGinWithDefaultHandler_SmallPayload-12 653 662 +1.38% BenchmarkSoleGin_BigPayload-12 271 284 +4.80% BenchmarkGinWithDefaultHandler_BigPayload-12 83649 25616 -69.38% ``` I believe the last benchmark is the only one actually to be compressing, correct? I didn't go out of my way to clean up the benchmark since the difference is rather obvious. The compression ratio is likely a bit less, but among many other changes the default settings have been tweaked to give the most reasonable speed/compression tradeoff. --- go.mod | 1 + go.sum | 2 ++ handler.go | 2 +- handler_test.go | 20 ++++++++++++++++++++ writerwrapper.go | 13 ++++++++----- writerwrapper_test.go | 2 +- 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 6b9a8ca..3b4039d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/gin-gonic/gin v1.5.0 + github.com/klauspost/compress v1.11.3 github.com/signalsciences/ac v1.1.0 github.com/stretchr/testify v1.4.0 ) diff --git a/go.sum b/go.sum index e402237..49d41ca 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= diff --git a/handler.go b/handler.go index fe5408b..a595bd2 100644 --- a/handler.go +++ b/handler.go @@ -2,7 +2,6 @@ package gzip import ( "bufio" - "compress/gzip" "fmt" "io/ioutil" "net" @@ -10,6 +9,7 @@ import ( "sync" "github.com/gin-gonic/gin" + "github.com/klauspost/compress/gzip" ) // These constants are copied from the gzip package diff --git a/handler_test.go b/handler_test.go index cc6fc63..436b9ec 100644 --- a/handler_test.go +++ b/handler_test.go @@ -150,7 +150,12 @@ func BenchmarkSoleGin_SmallPayload(b *testing.B) { r.Header.Set("Accept-Encoding", "gzip") b.ResetTimer() + h := map[string][]string(w.header) for i := 0; i < b.N; i++ { + // Delete header between calls. + for k := range h { + delete(h, k) + } g.ServeHTTP(w, r) } @@ -170,7 +175,12 @@ func BenchmarkGinWithDefaultHandler_SmallPayload(b *testing.B) { r.Header.Set("Accept-Encoding", "gzip") b.ResetTimer() + h := map[string][]string(w.header) for i := 0; i < b.N; i++ { + // Delete header between calls. + for k := range h { + delete(h, k) + } g.ServeHTTP(w, r) } @@ -190,7 +200,12 @@ func BenchmarkSoleGin_BigPayload(b *testing.B) { r.Header.Set("Accept-Encoding", "gzip") b.ResetTimer() + h := map[string][]string(w.header) for i := 0; i < b.N; i++ { + // Delete header between calls. + for k := range h { + delete(h, k) + } g.ServeHTTP(w, r) } @@ -210,7 +225,12 @@ func BenchmarkGinWithDefaultHandler_BigPayload(b *testing.B) { r.Header.Set("Accept-Encoding", "gzip") b.ResetTimer() + h := map[string][]string(w.header) for i := 0; i < b.N; i++ { + // Delete header between calls. + for k := range h { + delete(h, k) + } g.ServeHTTP(w, r) } diff --git a/writerwrapper.go b/writerwrapper.go index 03dfd33..9559d3c 100644 --- a/writerwrapper.go +++ b/writerwrapper.go @@ -1,11 +1,12 @@ package gzip import ( - "compress/gzip" "fmt" "net/http" "strconv" "strings" + + "github.com/klauspost/compress/gzip" ) // writerWrapper wraps the originalHandler @@ -154,10 +155,12 @@ func (w *writerWrapper) Write(data []byte) (int, error) { w.WriteHeaderNow() w.initGzipWriter() - written, err := w.gzipWriter.Write(w.bodyBuffer) - if err != nil { - err = fmt.Errorf("w.gzipWriter.Write: %w", err) - return written, err + if len(w.bodyBuffer) > 0 { + written, err := w.gzipWriter.Write(w.bodyBuffer) + if err != nil { + err = fmt.Errorf("w.gzipWriter.Write: %w", err) + return written, err + } } return w.gzipWriter.Write(data) } diff --git a/writerwrapper_test.go b/writerwrapper_test.go index dd7ca3f..2e6f288 100644 --- a/writerwrapper_test.go +++ b/writerwrapper_test.go @@ -1,7 +1,6 @@ package gzip import ( - "compress/gzip" "io/ioutil" "net/http" "net/http/httptest" @@ -9,6 +8,7 @@ import ( "sync" "testing" + "github.com/klauspost/compress/gzip" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) From d5b52398f549ebb80d42032132a47e1effde1ba6 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Fri, 27 Nov 2020 23:28:57 +0100 Subject: [PATCH 2/2] Add stateless compression and tests. --- handler.go | 10 ++++++++-- handler_test.go | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/handler.go b/handler.go index a595bd2..64a391d 100644 --- a/handler.go +++ b/handler.go @@ -19,12 +19,18 @@ const ( BestCompression = gzip.BestCompression DefaultCompression = gzip.DefaultCompression HuffmanOnly = gzip.HuffmanOnly + // Stateless will do compression but without maintaining any state + // between Write calls, so long running responses will not take memory. + // There will be no memory kept between Write calls, + // but compression and speed will be suboptimal. + // Because of this, the size of actual Write calls will affect output size. + Stateless = gzip.StatelessCompression ) // Config is used in Handler initialization type Config struct { // gzip compression level to use, - // valid value: -2 ~ 9. + // valid value: -3 => 9. // // see https://golang.org/pkg/compress/gzip/#NewWriterLevel CompressionLevel int @@ -58,7 +64,7 @@ type Handler struct { // // config must not be modified after calling on NewHandler() func NewHandler(config Config) *Handler { - if config.CompressionLevel < HuffmanOnly || config.CompressionLevel > BestCompression { + if config.CompressionLevel < Stateless || config.CompressionLevel > BestCompression { panic(fmt.Sprintf("gzip: invalid CompressionLevel: %d", config.CompressionLevel)) } if config.MinContentLength <= 0 { diff --git a/handler_test.go b/handler_test.go index 436b9ec..e23ba93 100644 --- a/handler_test.go +++ b/handler_test.go @@ -113,7 +113,7 @@ func TestNewHandler_Checks(t *testing.T) { assert.Panics(t, func() { NewHandler(Config{ - CompressionLevel: -3, + CompressionLevel: -4, MinContentLength: 100, }) }) @@ -285,6 +285,39 @@ func TestGinWithDefaultHandler(t *testing.T) { } } +func TestGinWithLevelsHandler(t *testing.T) { + for i := Stateless; i < 10; i++ { + var seq = "level_" + strconv.Itoa(i) + i := i + t.Run(seq, func(t *testing.T) { + g := newEchoGinInstance(bigPayload, NewHandler(Config{ + CompressionLevel: i, + MinContentLength: 1, + }).Gin) + + var ( + w = httptest.NewRecorder() + r = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(seq)) + ) + + r.Header.Set("Accept-Encoding", "gzip") + g.ServeHTTP(w, r) + + result := w.Result() + require.EqualValues(t, http.StatusOK, result.StatusCode) + require.Equal(t, "gzip", result.Header.Get("Content-Encoding")) + comp, err := ioutil.ReadAll(result.Body) + require.NoError(t, err) + reader, err := gzip.NewReader(bytes.NewReader(comp)) + require.NoError(t, err) + body, err := ioutil.ReadAll(reader) + require.NoError(t, err) + require.True(t, bytes.HasPrefix(body, []byte(seq))) + t.Logf("%s: compressed %d => %d", seq, len(body), len(comp)) + }) + } +} + func TestGinWithDefaultHandler_404(t *testing.T) { var ( g = newGinInstance(bigPayload, DefaultHandler().Gin)