FU::Util: Add brotli_compress() and use it for FU output compression

Seems to compresses and perform better than libdeflate at level 6, so
certainly worth using.
This commit is contained in:
Yorhel 2025-03-19 10:12:06 +01:00
parent bc33fe53f0
commit 6159b33950
5 changed files with 99 additions and 14 deletions

19
FU.pm
View file

@ -400,7 +400,7 @@ sub _do_req($c) {
($REQ->{trace_sqlexec}||0)*1000, ($REQ->{trace_sqlprep}||0)*1000, ($REQ->{trace_sqlexec}||0)*1000, ($REQ->{trace_sqlprep}||0)*1000,
$REQ->{trace_nsqldirect}||0, $REQ->{trace_nsqlprep}||0, $REQ->{trace_nsql} : '', $REQ->{trace_nsqldirect}||0, $REQ->{trace_nsqlprep}||0, $REQ->{trace_nsql} : '',
$REQ->{status}, ($REQ->{reshdr}{'content-type'}//'-') =~ s/;.+$//r, $REQ->{status}, ($REQ->{reshdr}{'content-type'}//'-') =~ s/;.+$//r,
length($REQ->{resbody}), substr($REQ->{reshdr}{'content-encoding'}//'bytes', 0, 1) length($REQ->{resbody}), substr($REQ->{reshdr}{'content-encoding'}//'r', 0, 1)
) if FU::debug || $proc_ms > (FU::log_slow_reqs||1e10); ) if FU::debug || $proc_ms > (FU::log_slow_reqs||1e10);
} }
@ -850,6 +850,8 @@ sub _error_page($, $code, $title, $msg) {
} }
sub _finalize { sub _finalize {
state $hasgzip = FU::Util::gzip_lib();
state $hasbrotli = eval { FU::Util::brotli_compress(6, ''); 1 };
my $r = $FU::REQ; my $r = $FU::REQ;
fu->add_header('set-cookie', $_) for $r->{rescookie} ? sort values $r->{rescookie}->%* : (); fu->add_header('set-cookie', $_) for $r->{rescookie} ? sort values $r->{rescookie}->%* : ();
@ -861,13 +863,19 @@ sub _finalize {
$r->{resbody} = ''; $r->{resbody} = '';
} else { } else {
if (FU::Util::gzip_lib() && length($r->{resbody}) > 256 if (($hasgzip || $hasbrotli) && length($r->{resbody}) > 256
&& !defined $r->{reshdr}{'content-encoding'} && FU::compress_mimes->{$r->{reshdr}{'content-type'}}) { && !defined $r->{reshdr}{'content-encoding'}
&& FU::compress_mimes->{$r->{reshdr}{'content-type'}}
) {
$r->{reshdr}{'vary'} = ($r->{reshdr}{'vary'} ? $r->{reshdr}{'vary'}.', ' : '').'accept-encoding' $r->{reshdr}{'vary'} = ($r->{reshdr}{'vary'} ? $r->{reshdr}{'vary'}.', ' : '').'accept-encoding'
if ($r->{reshdr}{'vary'}||'') !~ /accept-encoding/i; if ($r->{reshdr}{'vary'}||'') !~ /accept-encoding/i;
if (($r->{hdr}{'accept-encoding'}||'') =~ /gzip/) { if ($hasbrotli && ($r->{hdr}{'accept-encoding'}||'') =~ /\bbr\b/) {
$r->{resbody} = FU::Util::brotli_compress(6, $r->{resbody});
$r->{reshdr}{'content-encoding'} = 'br';
} elsif ($hasgzip && ($r->{hdr}{'accept-encoding'}||'') =~ /\bgzip\b/) {
$r->{resbody} = FU::Util::gzip_compress(6, $r->{resbody}); $r->{resbody} = FU::Util::gzip_compress(6, $r->{resbody});
$r->{reshdr}{'content-encoding'} = 'gzip'; $r->{reshdr}{'content-encoding'} = 'gzip';
} }
@ -991,6 +999,9 @@ C<dlopen()>.
=item * C<libdeflate.so> or C<libz-ng.so> or C<libz.so> - required for =item * C<libdeflate.so> or C<libz-ng.so> or C<libz.so> - required for
C<gzip_compress()> in L<FU::Util> and used for HTTP output compression. C<gzip_compress()> in L<FU::Util> and used for HTTP output compression.
=item * C<libbrotlienc.so> - required for C<brotli_compress()> in L<FU::Util>
and used for HTTP output compression.
=back =back

4
FU.xs
View file

@ -126,6 +126,10 @@ void gzip_compress(IV level, SV *in)
CODE: CODE:
ST(0) = fugz_compress(aTHX_ level, in); ST(0) = fugz_compress(aTHX_ level, in);
void brotli_compress(IV level, SV *in)
CODE:
ST(0) = fubr_compress(aTHX_ level, in);
void fdpass_send(int socket, int fd, SV *data) void fdpass_send(int socket, int fd, SV *data)
CODE: CODE:
STRLEN buflen; STRLEN buflen;

View file

@ -13,7 +13,7 @@ our @EXPORT_OK = qw/
utf8_decode uri_escape uri_unescape utf8_decode uri_escape uri_unescape
query_decode query_encode query_decode query_encode
httpdate_format httpdate_parse httpdate_format httpdate_parse
gzip_lib gzip_compress gzip_lib gzip_compress brotli_compress
fdpass_send fdpass_recv fdpass_send fdpass_recv
/; /;
@ -401,6 +401,8 @@ I<zlib(-ng)> the level is capped at 9. 6 is typically used as a default.
Throws an error if no suitable library was found. Throws an error if no suitable library was found.
This function is B<NOT> safe to use from multiple threads!
=back =back
This module does not currently implement decompression. If you need that, or This module does not currently implement decompression. If you need that, or
@ -409,6 +411,22 @@ L<Compress::Raw::Zlib> and L<Compress::Zlib> in the core Perl distribution and
L<Gzip::Deflate> on CPAN. L<Gzip::Deflate> on CPAN.
=head2 Brotli Compression
Just a small wrapper around C<libbrotlienc.so>'s one-shot compression
interface.
=over
=item brotli_compress($level, $data)
Returns a byte string with the brotli-compressed version of C<$data> at the
given quality C<$level> (between 0 and 11).
Throws an error if C<libbrotlienc.so> could not be found or loaded.
=back
=head2 File Descriptor Passing =head2 File Descriptor Passing

View file

@ -124,5 +124,38 @@ static SV *fugz_compress(pTHX_ IV level, SV *in) {
if (fugz_imp == 1) return fugz_compress_ld(aTHX_ level, bytes, inlen); if (fugz_imp == 1) return fugz_compress_ld(aTHX_ level, bytes, inlen);
else return fugz_compress_zlib(aTHX_ level, bytes, inlen); else return fugz_compress_zlib(aTHX_ level, bytes, inlen);
return &PL_sv_undef; }
/* Brotli */
typedef enum { BROTLI_MODE_GENERIC = 0, BROTLI_MODE_TEXT = 1, BROTLI_MODE_FONT = 2 } BrotliEncoderMode;
static size_t (*BrotliEncoderMaxCompressedSize)(size_t);
static int (*BrotliEncoderCompress)(int, int, BrotliEncoderMode, size_t, const char *, size_t *, char *);
static SV *fubr_compress(pTHX_ IV level, SV *in) {
if (!BrotliEncoderCompress) {
void *handle;
if (!(handle = dlopen("libbrotlienc.so", RTLD_LAZY))
|| !(BrotliEncoderMaxCompressedSize = dlsym(handle, "BrotliEncoderMaxCompressedSize"))
|| !(BrotliEncoderCompress = dlsym(handle, "BrotliEncoderCompress")))
fu_confess("Unable to load libbrotlienc.so: %s", dlerror());
}
if (level < 0 || level > 11) fu_confess("Invalid compression level: %"IVdf, level);
STRLEN inlen;
const char *bytes = SvPVbyte(in, inlen);
size_t outlen = BrotliEncoderMaxCompressedSize(inlen);
/* "Result is only valid if quality is at least 2", so let's use a (more conservative?) fallback */
if (level < 2 && outlen < inlen + 256) outlen = inlen + 256;
SV *out = sv_2mortal(newSV(outlen));
SvPOK_only(out);
if (!BrotliEncoderCompress(level, 22, BROTLI_MODE_GENERIC, inlen, bytes, &outlen, SvPVX(out)))
fu_confess("Brotli compression failed");
SvCUR_set(out, outlen);
return out;
} }

View file

@ -1,18 +1,34 @@
use v5.36; use v5.36;
use Test::More; use Test::More;
use FU::Util qw/gzip_lib gzip_compress/; use FU::Util qw/gzip_lib gzip_compress brotli_compress/;
like gzip_lib, qr/^(|libdeflate|zlib-ng|zlib)$/, gzip_lib; like gzip_lib, qr/^(|libdeflate|zlib-ng|zlib)$/, gzip_lib;
my $incompressible;
subtest 'gzip_compress', sub {
plan skip_all => 'No suitable gzip library found' if !gzip_lib; plan skip_all => 'No suitable gzip library found' if !gzip_lib;
plan skip_all => 'Compress::Zlib not found' if !eval { require Compress::Zlib }; plan skip_all => 'Compress::Zlib not found' if !eval { require Compress::Zlib };
my $incompressible = Compress::Zlib::memGzip(join '', map chr(rand 256), 0..93123); $incompressible = Compress::Zlib::memGzip(join '', map chr(rand 256), 0..93123);
for my $str ('', 'Hello world!', 'x'x4096, $incompressible) { for my $str ('', 'Hello world!', 'x'x4096, $incompressible) {
is Compress::Zlib::memGunzip(gzip_compress(0, $str)), $str; is Compress::Zlib::memGunzip(gzip_compress(0, $str)), $str;
is Compress::Zlib::memGunzip(gzip_compress(12, $str)), $str; is Compress::Zlib::memGunzip(gzip_compress(12, $str)), $str;
} }
};
subtest 'brotli_compress', sub {
plan skip_all => 'libbrotlienc not available'
if !eval { brotli_compress 6, '' } && $@ =~ /Unable to load/;
ok length(brotli_compress 0, '') > 0;
ok length(brotli_compress 11, '') > 0;
# '0' does not disable compression...
ok length(brotli_compress 0, 'Hello world!'x100) < 200;
ok length(brotli_compress 11, 'Hello world!'x100) < 100;
};
done_testing; done_testing;
@ -29,6 +45,8 @@ for (0..1000) {
local $_ = gzip_lib; local $_ = gzip_lib;
$_ = gzip_compress(0, $str); $_ = gzip_compress(0, $str);
$_ = gzip_compress(12, $str); $_ = gzip_compress(12, $str);
$_ = brotli_compress(0, $str);
$_ = brotli_compress(11, $str);
} }
} }
diag count_sv; diag count_sv;
@ -44,4 +62,5 @@ my $data = <$F>;
cmpthese -3, { cmpthese -3, {
memGzip => 'Compress::Zlib::memGzip($data)', memGzip => 'Compress::Zlib::memGzip($data)',
gzip_compress => 'gzip_compress(6, $data)', gzip_compress => 'gzip_compress(6, $data)',
brotli_compress => 'brotli_compress(6, $data)',
}; };