Route zstd consumers through the compression-rs autogated router - #7157
Route zstd consumers through the compression-rs autogated router#7157guybedford wants to merge 1 commit into
Conversation
|
@guybedford Bonk workflow was cancelled. View workflow run · To retry, trigger Bonk again. |
The PR for brotli explicitly asks whether a slowdown is acceptable, but this is not being done here – people often choose zstd because of its high performance, IMHO a 3x slowdown on a compression primitive is a good reason to reconsider replacing a well-maintained C library with Rust. At least, there should be some discussion about this to consider this beforehand. |
Consider this one posted with the exact same stance as that one, I've added a clarification to the PR description to note that it is not going to land. |
The unprefixed ZSTD_* symbol names consumed in the build (the streaming compression/decompression context entry points and the error helpers) are now owned by a compiled routing layer, src/workerd/util/zstd-router.c++, mirroring the zlib router. It forwards each call to one of the two implementations linked into the binary: the C implementation (rebuilt with its routed entry points renamed to zstd_c_* via a patch to the BCR zstd module's BUILD, as @zstd//:zstd_impl) or a Rust implementation exported under zstd_rs_* prefixes by the new src/rust/zstd-rs crate. The branch is the same process-global atomic set from the compression-rs autogate as the zlib router, covering node:zlib's zstd modes universally. No credible pure-Rust zstd encoder exists (ruzstd's encoder only supports the fastest strategy, with no level selection, streaming flush control, or dictionaries), so only decompression is routed: the gate switches ZSTD_decompressStream and the DCtx entry points to ruzstd, while compression stays on the C implementation on both sides. The error helpers also stay on the C implementation; the Rust side returns errors in the standard (size_t)-ZSTD_ErrorCode encoding so they interpret results from either implementation. The zstd-rs crate implements the ZSTD_decompressStream contract over ruzstd's FrameDecoder: eager input buffering with push-style block decoding, frame-boundary and skippable-frame handling, windowLogMax enforcement, content checksum verification, and libzstd's hostage-byte protocol at frame end so node:zlib's availIn/availOut bookkeeping and truncation detection behave identically on both sides of the gate. A new kj_test verifies the router selects the matching implementation on both sides of the gate (observed via the experimental ZSTD_d_format parameter that only the C implementation accepts) and that both decode each other's output, including checksummed frames and corruption errors. A bench-zstd benchmark measures the streaming workload: ruzstd decompresses at roughly a third of C zstd's throughput (~445 MiB/s vs ~1.25 GiB/s on 1MB of compressible data in 16KB chunks).
fb2f509 to
bf72e5d
Compare
This extends the compression-rs autogated router introduced in #7132 to zstd.
As posted, this should not land, due to the 3x slowdown. But updating this PR as Rust zstd improves can allow us to track when this might become viable in future.
The unprefixed ZSTD_* symbol names consumed in the build (the streaming compression/decompression context entry points and the error helpers) are now owned by a compiled routing layer,
src/workerd/util/zstd-router.c++, which forwards each call to one of the two implementations linked into the binary, selected by the same process-global atomic set from the compression-rs autogate as the zlib router. This covers node:zlib's zstd modes universally (the only zstd consumer in the build).No credible pure-Rust zstd encoder exists today (ruzstd's encoder only supports the fastest strategy, with no level selection, streaming flush control, or dictionaries), so only decompression is routed: the gate switches
ZSTD_decompressStreamand the DCtx entry points to ruzstd, while compression stays on the C implementation on both sides of the gate. The error helpers also stay on the C implementation; the Rust side returns errors in the standard(size_t)-ZSTD_ErrorCodeencoding so they interpret results from either implementation.zstd_c_*as@zstd//:zstd_impl, via asingle_version_overridepatch to the BCR zstd module's BUILD (mirroring howbuild/BUILD.zlibdoeszlib_impl);@zstd//:zstdbecomes the standard headers over the routing layer. One source-line guard is needed inzstd_common.c, which#undefsZSTD_isError.src/rust/zstd-rscrate implements theZSTD_decompressStreamcontract over ruzstd'sFrameDecoder: push-style input buffering with bounded decode steps, frame-boundary and skippable-frame handling,ZSTD_d_windowLogMaxenforcement, content checksum verification, and libzstd's hostage-byte protocol at frame end so node:zlib's availIn/availOut bookkeeping and truncation detection behave identically on both sides of the gate.autogate.c++.ZSTD_d_formatparameter, which only the C implementation accepts) and that both implementations decode each other's output, including checksummed frames and corruption errors.bench-zstdbenchmark measures the node:zlib streaming workload (1MB compressible data, 16KB chunks) against both decoders.Benchmark results:
Compression output is gate-independent (always the C encoder); representative ratios with decode verified byte-exact through both decoders:
Test coverage: the new router kj_test, zlib-zstd-nodejs-test across all variants (
@all-autogatesexercises the ruzstd path end-to-end, including streaming, truncation, and maxOutputLength cases), and the full//src/workerd/...+//src/rust/...suites.