]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/DSdeflate.pm
imap+nntp: share COMPRESS implementation
[public-inbox.git] / lib / PublicInbox / DSdeflate.pm
1 # Copyright (C) all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # RFC 8054 NNTP COMPRESS DEFLATE implementation
5 # RFC 4978 IMAP COMPRESS=DEFLATE extension
6 #
7 # RSS usage for 10K idle-but-did-something NNTP clients on 64-bit:
8 #   TLS + DEFLATE[a] :  1.8 GB  (MemLevel=9, 1.2 GB with MemLevel=8)
9 #   TLS + DEFLATE[b] :  ~300MB
10 #   TLS only         :  <200MB
11 #   plain            :   <50MB
12 #
13 # [a] - initial implementation using per-client Deflate contexts and buffer
14 #
15 # [b] - memory-optimized implementation using a global deflate context.
16 #       It's less efficient in terms of compression, but way more
17 #       efficient in terms of server memory usage.
18 package PublicInbox::DSdeflate;
19 use strict;
20 use v5.10.1;
21 use Compress::Raw::Zlib;
22
23 my %IN_OPT = (
24         -Bufsize => 1024,
25         -WindowBits => -15, # RFC 1951
26         -AppendOutput => 1,
27 );
28
29 # global deflate context and buffer
30 my $zbuf = \(my $buf = '');
31 my $zout;
32 {
33         my $err;
34         ($zout, $err) = Compress::Raw::Zlib::Deflate->new(
35                 # nnrpd (INN) and Compress::Raw::Zlib favor MemLevel=9,
36                 # the zlib C library and git use MemLevel=8 as the default
37                 # -MemLevel => 9,
38                 -Bufsize => 65536, # same as nnrpd
39                 -WindowBits => -15, # RFC 1951
40                 -AppendOutput => 1,
41         );
42         $err == Z_OK or die "Failed to initialize zlib deflate stream: $err";
43 }
44
45 sub enable {
46         my ($class, $self) = @_;
47         my ($in, $err) = Compress::Raw::Zlib::Inflate->new(%IN_OPT);
48         if ($err != Z_OK) {
49                 $self->err("Inflate->new failed: $err");
50                 return;
51         }
52         bless $self, $class;
53         $self->{zin} = $in;
54 }
55
56 # overrides PublicInbox::DS::compressed
57 sub compressed { 1 }
58
59 sub do_read ($$$$) {
60         my ($self, $rbuf, $len, $off) = @_;
61
62         my $zin = $self->{zin} or return; # closed
63         my $doff;
64         my $dbuf = delete($self->{dbuf}) // '';
65         $doff = length($dbuf);
66         my $r = PublicInbox::DS::do_read($self, \$dbuf, $len, $doff) or return;
67
68         # Workaround inflate bug appending to OOK scalars:
69         # <https://rt.cpan.org/Ticket/Display.html?id=132734>
70         # We only have $off if the client is pipelining, and pipelining
71         # is where our substr() OOK optimization in event_step makes sense.
72         if ($off) {
73                 my $copy = $$rbuf;
74                 undef $$rbuf;
75                 $$rbuf = $copy;
76         }
77
78         # assert(length($$rbuf) == $off) as far as NNTP.pm is concerned
79         # -ConsumeInput is true, so $dbuf is automatically emptied
80         my $err = $zin->inflate($dbuf, $rbuf);
81         if ($err == Z_OK) {
82                 $self->{dbuf} = $dbuf if $dbuf ne '';
83                 $r = length($$rbuf) and return $r;
84                 # nothing ready, yet, get more, later
85                 $self->requeue;
86         } else {
87                 delete $self->{zin};
88                 $self->close;
89         }
90         0;
91 }
92
93 # override PublicInbox::DS::msg_more
94 sub msg_more ($$) {
95         my $self = $_[0];
96
97         # $_[1] may be a reference or not for ->deflate
98         my $err = $zout->deflate($_[1], $zbuf);
99         $err == Z_OK or die "->deflate failed $err";
100         1;
101 }
102
103 sub zflush ($) {
104         my ($self) = @_;
105
106         my $deflated = $zbuf;
107         $zbuf = \(my $next = '');
108
109         my $err = $zout->flush($deflated, Z_FULL_FLUSH);
110         $err == Z_OK or die "->flush failed $err";
111
112         # We can still let the lower socket layer do buffering:
113         PublicInbox::DS::msg_more($self, $$deflated);
114 }
115
116 # compatible with PublicInbox::DS::write, so $_[1] may be a reference or not
117 sub write ($$) {
118         my $self = $_[0];
119         return PublicInbox::DS::write($self, $_[1]) if ref($_[1]) eq 'CODE';
120
121         my $deflated = $zbuf;
122         $zbuf = \(my $next = '');
123
124         # $_[1] may be a reference or not for ->deflate
125         my $err = $zout->deflate($_[1], $deflated);
126         $err == Z_OK or die "->deflate failed $err";
127         $err = $zout->flush($deflated, Z_FULL_FLUSH);
128         $err == Z_OK or die "->flush failed $err";
129
130         # We can still let the socket layer do buffering:
131         PublicInbox::DS::write($self, $deflated);
132 }
133
134 1;