reftable/block: reuse zstream when writing log blocks

While most reftable blocks are written to disk as-is, blocks for log records are compressed with zlib. To compress them we use `compress2()`, which is a simple wrapper around the more complex `zstream` interface that would require multiple function invocations. One downside of this interface is that `compress2()` will reallocate internal state of the `zstream` interface on every single invocation. Consequently, as we call `compress2()` for every single log block which we are about to write, this can lead to quite some memory allocation churn. Refactor the code so that the block writer reuses a `zstream`. This significantly reduces the number of bytes allocated when writing many refs in a single transaction, as demonstrated by the following benchmark that writes 100k refs in a single transaction. Before: HEAP SUMMARY: in use at exit: 671,931 bytes in 151 blocks total heap usage: 22,631,887 allocs, 22,631,736 frees, 1,854,670,793 bytes allocated After: HEAP SUMMARY: in use at exit: 671,931 bytes in 151 blocks total heap usage: 22,620,528 allocs, 22,620,377 frees, 1,245,549,984 bytes allocated Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Apr 8, 2024 at 14:24 UTC a155ab2bf4e16b8fe1e0644d9d57d2c62eecd452
2 files changed +53 -28
reftable/block.c
+52 -28
@@ -76,6 +76,10 @@ void block_writer_init(struct block_writer *bw, uint8_t typ, uint8_t *buf,
76 bw->entries = 0;
77 bw->restart_len = 0;
78 bw->last_key.len = 0;
79 + if (!bw->zstream) {
80 + REFTABLE_CALLOC_ARRAY(bw->zstream, 1);
81 + deflateInit(bw->zstream, 9);
82 + }
83 }
84
85 uint8_t block_writer_type(struct block_writer *bw)
@@ -139,39 +143,57 @@ int block_writer_finish(struct block_writer *w)
143 w->next += 2;
144 put_be24(w->buf + 1 + w->header_off, w->next);
145
146 + /*
147 + * Log records are stored zlib-compressed. Note that the compression
148 + * also spans over the restart points we have just written.
149 + */
150 if (block_writer_type(w) == BLOCK_TYPE_LOG) {
151 int block_header_skip = 4 + w->header_off;
144 - uLongf src_len = w->next - block_header_skip;
145 - uLongf dest_cap = src_len * 1.001 + 12;
146 - uint8_t *compressed;
147 -
148 - REFTABLE_ALLOC_ARRAY(compressed, dest_cap);
149 -
150 - while (1) {
151 - uLongf out_dest_len = dest_cap;
152 - int zresult = compress2(compressed, &out_dest_len,
153 - w->buf + block_header_skip,
154 - src_len, 9);
155 - if (zresult == Z_BUF_ERROR && dest_cap < LONG_MAX) {
156 - dest_cap *= 2;
157 - compressed =
158 - reftable_realloc(compressed, dest_cap);
159 - if (compressed)
160 - continue;
161 - }
162 -
163 - if (Z_OK != zresult) {
164 - reftable_free(compressed);
165 - return REFTABLE_ZLIB_ERROR;
166 - }
167 -
168 - memcpy(w->buf + block_header_skip, compressed,
169 - out_dest_len);
170 - w->next = out_dest_len + block_header_skip;
152 + uLongf src_len = w->next - block_header_skip, compressed_len;
153 + unsigned char *compressed;
154 + int ret;
155 +
156 + ret = deflateReset(w->zstream);
157 + if (ret != Z_OK)
158 + return REFTABLE_ZLIB_ERROR;
159 +
160 + /*
161 + * Precompute the upper bound of how many bytes the compressed
162 + * data may end up with. Combined with `Z_FINISH`, `deflate()`
163 + * is guaranteed to return `Z_STREAM_END`.
164 + */
165 + compressed_len = deflateBound(w->zstream, src_len);
166 + REFTABLE_ALLOC_ARRAY(compressed, compressed_len);
167 +
168 + w->zstream->next_out = compressed;
169 + w->zstream->avail_out = compressed_len;
170 + w->zstream->next_in = w->buf + block_header_skip;
171 + w->zstream->avail_in = src_len;
172 +
173 + /*
174 + * We want to perform all decompression in a single step, which
175 + * is why we can pass Z_FINISH here. As we have precomputed the
176 + * deflated buffer's size via `deflateBound()` this function is
177 + * guaranteed to succeed according to the zlib documentation.
178 + */
179 + ret = deflate(w->zstream, Z_FINISH);
180 + if (ret != Z_STREAM_END) {
181 reftable_free(compressed);
172 - break;
182 + return REFTABLE_ZLIB_ERROR;
183 }
184 +
185 + /*
186 + * Overwrite the uncompressed data we have already written and
187 + * adjust the `next` pointer to point right after the
188 + * compressed data.
189 + */
190 + memcpy(w->buf + block_header_skip, compressed,
191 + w->zstream->total_out);
192 + w->next = w->zstream->total_out + block_header_skip;
193 +
194 + reftable_free(compressed);
195 }
196 +
197 return w->next;
198 }
199
@@ -425,6 +447,8 @@ done:
447
448 void block_writer_release(struct block_writer *bw)
449 {
450 + deflateEnd(bw->zstream);
451 + FREE_AND_NULL(bw->zstream);
452 FREE_AND_NULL(bw->restarts);
453 strbuf_release(&bw->last_key);
454 /* the block is not owned. */
reftable/block.h
+1
@@ -18,6 +18,7 @@ https://developers.google.com/open-source/licenses/bsd
18 * allocation overhead.
19 */
20 struct block_writer {
21 + z_stream *zstream;
22 uint8_t *buf;
23 uint32_t block_size;
24