Willy, Aleks, List, this (absolutely non-ready-to-merge) patch adds support for brotli compression as suggested in issue #21: https://github.com/haproxy/haproxy/issues/21
It is tested on Ubuntu Xenial with libbrotli 1.0.3: [timwolla@~]apt-cache policy libbrotli-dev libbrotli-dev: Installed: 1.0.3-1ubuntu1~16.04.1 Candidate: 1.0.3-1ubuntu1~16.04.1 Version table: *** 1.0.3-1ubuntu1~16.04.1 500 500 http://de.archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages 100 /var/lib/dpkg/status [timwolla@~]apt-cache policy libbrotli1 libbrotli1: Installed: 1.0.3-1ubuntu1~16.04.1 Candidate: 1.0.3-1ubuntu1~16.04.1 Version table: *** 1.0.3-1ubuntu1~16.04.1 500 500 http://de.archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages 100 /var/lib/dpkg/status I am successfully able access brotli compressed URLs with Google Chrome, this requires me to disable `gzip` though (because haproxy prefers to select gzip, I suspect because `br` is last in Chrome's `Accept-Encoding` header). I also am able to sucessfully download and decompress URLs with `curl` and the `brotli` CLI utility. The server I use as the backend for these tests has about 45ms RTT to my machine. The HTML page I use is some random HTML page on the server, the noise file is 1 MiB of finest /dev/urandom. You'll notice that brotli compressed requests are both faster as well as smaller compared to gzip with the hardcoded brotli compression quality of 3. The default is 11, which is *way* slower than gzip. + curl localhost:8080/*snip*.html -H 'Accept-Encoding: gzip' % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 49280 0 49280 0 0 279k 0 --:--:-- --:--:-- --:--:-- 279k + curl localhost:8080/*snip*.html -H 'Accept-Encoding: br' % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 43401 0 43401 0 0 332k 0 --:--:-- --:--:-- --:--:-- 333k + curl localhost:8080/*snip*.html -H 'Accept-Encoding: identity' % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 127k 100 127k 0 0 441k 0 --:--:-- --:--:-- --:--:-- 441k + curl localhost:8080/noise -H 'Accept-Encoding: gzip' % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1025k 0 1025k 0 0 3330k 0 --:--:-- --:--:-- --:--:-- 3338k + curl localhost:8080/noise -H 'Accept-Encoding: br' % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1024k 0 1024k 0 0 3029k 0 --:--:-- --:--:-- --:--:-- 3030k + curl localhost:8080/noise -H 'Accept-Encoding: identity' % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1024k 100 1024k 0 0 3003k 0 --:--:-- --:--:-- --:--:-- 3002k + ls -al total 3384 drwxrwxr-x 2 timwolla timwolla 4096 Feb 13 17:30 . drwxrwxrwt 28 root root 69632 Feb 13 17:25 .. -rw-rw-r-- 1 timwolla timwolla 598 Feb 13 17:30 download -rw-rw-r-- 1 timwolla timwolla 43401 Feb 13 17:30 html-br -rw-rw-r-- 1 timwolla timwolla 49280 Feb 13 17:30 html-gz -rw-rw-r-- 1 timwolla timwolla 130334 Feb 13 17:30 html-id -rw-rw-r-- 1 timwolla timwolla 1048949 Feb 13 17:30 noise-br -rw-rw-r-- 1 timwolla timwolla 1049666 Feb 13 17:30 noise-gz -rw-rw-r-- 1 timwolla timwolla 1048576 Feb 13 17:30 noise-id ++ zcat html-gz + sha256sum html-id /dev/fd/63 /dev/fd/62 ++ brotli --decompress --stdout html-br 56f1664241b3dbb750f93b69570be76c6baccb8de4f3a62fb4fec0ce1bf440b5 html-id 56f1664241b3dbb750f93b69570be76c6baccb8de4f3a62fb4fec0ce1bf440b5 /dev/fd/63 56f1664241b3dbb750f93b69570be76c6baccb8de4f3a62fb4fec0ce1bf440b5 /dev/fd/62 ++ zcat noise-gz + sha256sum noise-id /dev/fd/63 /dev/fd/62 ++ brotli --decompress --stdout noise-br ab23236d9d4acecec239c3f0f9b59e59dd043267eeed9ed723da8b15f46bbf33 noise-id ab23236d9d4acecec239c3f0f9b59e59dd043267eeed9ed723da8b15f46bbf33 /dev/fd/63 ab23236d9d4acecec239c3f0f9b59e59dd043267eeed9ed723da8b15f46bbf33 /dev/fd/62 This patch still spits out a bunch of debug output to `stdout`, because I'm not sure about the do-while loops in `_flush` and `_finish`. I suppose they could lead to infinite loops if the encoded data exceeds the out buffer. It might be necessary to add a new return code that indicates that the `_flush` / `_finish` must be retried after making space in the output buffer, because the libbrotli API states: > Under some circumstances (e.g. lack of output stream capacity) this > operation would require several calls to BrotliEncoderCompressStream. > The method must be called again until both input stream is depleted > and encoder has no more output (see BrotliEncoderHasMoreOutput) after > the method is called. and > Warning: > When flushing and finishing, op should not change until operation is > complete; input stream should not be swapped, reduced or extended as well. Thus `_add_data` *must not* be called until the flush succeeded completely. One more thing: brotli theoretically supports passing a custom allocator. I attempted to use a pool for that, but `BrotliEncoderState` is an opaque struct. Best regards Tim Duesterhus Apply with `git am --scissors` to automatically cut the commit message. -- >8 -- see issue #21 --- Makefile | 9 +++ include/types/compression.h | 37 ++++++++---- src/compression.c | 113 +++++++++++++++++++++++++++++++++++- src/flt_http_comp.c | 2 +- 4 files changed, 149 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index e2c4d17a..f1818d01 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ # USE_MY_ACCEPT4 : use own implemention of accept4() if glibc < 2.10. # USE_ZLIB : enable zlib library support. # USE_SLZ : enable slz library instead of zlib (pick at most one). +# USE_BROTLI : enable brotli library support. # USE_CPU_AFFINITY : enable pinning processes to CPU on Linux. Automatic. # USE_TFO : enable TCP fast open. Supported on Linux >= 3.7. # USE_NS : enable network namespace support. Supported on Linux >= 2.6.24. @@ -555,6 +556,14 @@ BUILD_OPTIONS += $(call ignore_implicit,USE_ZLIB) OPTIONS_LDFLAGS += $(if $(ZLIB_LIB),-L$(ZLIB_LIB)) -lz endif +ifneq ($(USE_BROTLI),) +BROTLI_INC = +BROTLI_LIB = +OPTIONS_CFLAGS += -DUSE_BROTLI $(if $(BROTLI_INC),-I$(BROTLI_INC)) +BUILD_OPTIONS += $(call ignore_implicit,USE_BROTLI) +OPTIONS_LDFLAGS += $(if $(BROTLI_LIB),-L$(BROTLI_LIB)) -lbrotlienc +endif + ifneq ($(USE_POLL),) OPTIONS_CFLAGS += -DENABLE_POLL OPTIONS_OBJS += src/ev_poll.o diff --git a/include/types/compression.h b/include/types/compression.h index e515aadf..3c953e66 100644 --- a/include/types/compression.h +++ b/include/types/compression.h @@ -32,6 +32,10 @@ #include <zlib.h> #endif +#if defined(USE_BROTLI) +#include <brotli/encode.h> +#endif + #include <common/buffer.h> struct comp { @@ -40,22 +44,35 @@ struct comp { unsigned int offload; }; +#if defined(USE_SLZ) || defined(USE_ZLIB) || defined(USE_BROTLI) struct comp_ctx { + union { +#if defined(USE_SLZ) || defined(USE_ZLIB) + struct { #if defined(USE_SLZ) - struct slz_stream strm; - const void *direct_ptr; /* NULL or pointer to beginning of data */ - int direct_len; /* length of direct_ptr if not NULL */ - struct buffer queued; /* if not NULL, data already queued */ + struct slz_stream strm; + const void *direct_ptr; /* NULL or pointer to beginning of data */ + int direct_len; /* length of direct_ptr if not NULL */ + struct buffer queued; /* if not NULL, data already queued */ #elif defined(USE_ZLIB) - z_stream strm; /* zlib stream */ - void *zlib_deflate_state; - void *zlib_window; - void *zlib_prev; - void *zlib_pending_buf; - void *zlib_head; + z_stream strm; /* zlib stream */ + void *zlib_deflate_state; + void *zlib_window; + void *zlib_prev; + void *zlib_pending_buf; + void *zlib_head; +#endif + }; #endif +#if defined(USE_BROTLI) + struct { + BrotliEncoderState *brotli_state; + }; +#endif + }; int cur_lvl; }; +#endif /* Thanks to MSIE/IIS, the "deflate" name is ambigous, as according to the RFC * it's a zlib-wrapped deflate stream, but MSIE only understands a raw deflate diff --git a/src/compression.c b/src/compression.c index b0307b23..01d46945 100644 --- a/src/compression.c +++ b/src/compression.c @@ -26,6 +26,10 @@ #undef free_func #endif /* USE_ZLIB */ +#if defined(USE_BROTLI) +#include <brotli/encode.h> +#endif + #include <common/cfgparse.h> #include <common/compat.h> #include <common/hathreads.h> @@ -94,6 +98,14 @@ static int deflate_end(struct comp_ctx **comp_ctx); #endif /* USE_ZLIB */ +#if defined(USE_BROTLI) +static int brotli_init(struct comp_ctx **comp_ctx, int level); +static int brotli_add_data(struct comp_ctx *comp_ctx, const char *in_data, int in_len, struct buffer *out); +static int brotli_flush(struct comp_ctx *comp_ctx, struct buffer *out); +static int brotli_finish(struct comp_ctx *comp_ctx, struct buffer *out); +static int brotli_end(struct comp_ctx **comp_ctx); +#endif + const struct comp_algo comp_algos[] = { @@ -107,6 +119,9 @@ const struct comp_algo comp_algos[] = { "raw-deflate", 11, "deflate", 7, raw_def_init, deflate_add_data, deflate_flush, deflate_finish, deflate_end }, { "gzip", 4, "gzip", 4, gzip_init, deflate_add_data, deflate_flush, deflate_finish, deflate_end }, #endif /* USE_ZLIB */ +#if defined(USE_BROTLI) + { "brotli", 6, "br", 2, brotli_init, brotli_add_data, brotli_flush, brotli_finish, brotli_end }, +#endif { NULL, 0, NULL, 0, NULL , NULL, NULL, NULL, NULL } }; @@ -246,7 +261,6 @@ static int identity_end(struct comp_ctx **comp_ctx) return 0; } - #ifdef USE_SLZ /* SLZ's gzip format (RFC1952). Returns < 0 on error. */ @@ -691,6 +705,103 @@ static int zlib_parse_global_windowsize(char **args, int section_type, struct pr #endif /* USE_ZLIB */ +#ifdef USE_BROTLI + +static int brotli_init(struct comp_ctx **comp_ctx, int level) +{ + printf("brotli_init\n"); + BrotliEncoderState *brotli_state; + *comp_ctx = pool_alloc(pool_comp_ctx); + if (*comp_ctx == NULL) + return -1; + brotli_state = BrotliEncoderCreateInstance(NULL, NULL, NULL); + BrotliEncoderSetParameter(brotli_state, BROTLI_PARAM_QUALITY, 3); + if (brotli_state == NULL) { + pool_free(pool_comp_ctx, *comp_ctx); + *comp_ctx = NULL; + return -1; + } + (*comp_ctx)->brotli_state = brotli_state; + + return 0; +} + +static int brotli_add_data(struct comp_ctx *comp_ctx, const char *in_data, int in_len, struct buffer *out) +{ + printf("brotli_add_data\n"); + uint8_t *out_data = (uint8_t*)b_tail(out); + size_t out_len = b_room(out); + size_t out_len2 = out_len; + const uint8_t *in_data2 = (const uint8_t*)in_data; + size_t in_len2 = in_len; + + printf("BrotliEncoderCompressStream\n"); + printf("+in_len: %zu\n", in_len2); + printf("+out_len: %zu\n", out_len2); + if (BrotliEncoderCompressStream(comp_ctx->brotli_state, BROTLI_OPERATION_PROCESS, &in_len2, &in_data2, &out_len2, &out_data, NULL) == BROTLI_FALSE) { + return -1; + } + printf("-in_len: %zu\n", in_len2); + printf("-out_len: %zu = %zu\n", out_len2, out_len - out_len2); + b_add(out, out_len - out_len2); + + return in_len - in_len2; +} + +static int brotli_flush(struct comp_ctx *comp_ctx, struct buffer *out) +{ + printf("brotli_flush\n"); + uint8_t *out_data = (uint8_t*)b_tail(out); + size_t out_len = b_room(out); + size_t out_len2 = out_len; + size_t in_len2 = 0; + + do + { + printf("BrotliEncoderCompressStream\n"); + printf("+out_len: %zu\n", out_len2); + if (BrotliEncoderCompressStream(comp_ctx->brotli_state, BROTLI_OPERATION_FLUSH, &in_len2, NULL, &out_len2, &out_data, NULL) == BROTLI_FALSE) { + return -1; + } + printf("-out_len: %zu = %zu\n", out_len2, out_len - out_len2); + } + while (BrotliEncoderHasMoreOutput(comp_ctx->brotli_state) == BROTLI_TRUE); + b_add(out, out_len - out_len2); + printf("Done\n"); + return out_len - out_len2; +} +static int brotli_finish(struct comp_ctx *comp_ctx, struct buffer *out) +{ + printf("brotli_finish\n"); + uint8_t *out_data = (uint8_t*)b_tail(out); + size_t out_len = b_room(out); + size_t out_len2 = out_len; + size_t in_len = 0; + + do + { + printf("BrotliEncoderCompressStream\n"); + printf("+out_len: %zu\n", out_len2); + if (BrotliEncoderCompressStream(comp_ctx->brotli_state, BROTLI_OPERATION_FINISH, &in_len, NULL, &out_len2, &out_data, NULL) == BROTLI_FALSE) { + return -1; + } + printf("-out_len: %zu = %zu\n", out_len2, out_len - out_len2); + } + while (BrotliEncoderHasMoreOutput(comp_ctx->brotli_state) == BROTLI_TRUE); + b_add(out, out_len - out_len2); + printf("Done\n"); + return out_len - out_len2; +} +static int brotli_end(struct comp_ctx **comp_ctx) +{ + printf("brotli_end\n"); + BrotliEncoderDestroyInstance((*comp_ctx)->brotli_state); + pool_free(pool_comp_ctx, *comp_ctx); + *comp_ctx = NULL; + return 0; +} + +#endif /* config keyword parsers */ static struct cfg_kw_list cfg_kws = {ILH, { diff --git a/src/flt_http_comp.c b/src/flt_http_comp.c index 3c2ce365..bedc9e6f 100644 --- a/src/flt_http_comp.c +++ b/src/flt_http_comp.c @@ -1178,7 +1178,7 @@ http_compression_buffer_end(struct comp_state *st, struct stream *s, char *tail; int to_forward, left; -#if defined(USE_SLZ) || defined(USE_ZLIB) +#if defined(USE_SLZ) || defined(USE_ZLIB) || defined(USE_BROTLI) int ret; /* flush data here */ -- 2.20.1