remote: avoid tiny zstd upload blocks (#31528)
### Description
Bazel's zstd upload adapter uses a 512-byte pipe and flushes after
at most 491 source bytes. This forces short compressed blocks and
adds encoding and pipe overhead.
Replace the pipe with a reusable 16 KiB input buffer and a bounded
output collector. Let zstd choose when to emit blocks, drain that
output before reading more input, and finish the frame at EOF. Keep
source cleanup and retries from an uncompressed resume offset unchanged.
Limit each upload chunk to eight compressed reads, plus EOF lookahead.
This avoids reading a large, highly compressible blob just to fill
one chunk before checking upload readiness. Returning partly filled
chunks can add WriteRequest messages for such inputs.
Two matched benchmarks compare this change with the old implementation.
The tables show medians; change is (after / before - 1) * 100.
The compression-and-chunking benchmark uses 1,348 historical source
inputs totaling 35,742,901 raw bytes, with zstd-jni 1.5.6-9 at level 3:
Metric Before After Change %
------------------------- ----------- ----------- --------
Encoded bytes 10,429,640 7,893,477 -24.3%
Elapsed time, ms 170.257 74.355 -56.3%
Thread CPU, ms 169.217 73.350 -56.7%
Java allocation, MB 250.704 254.813 +1.6%
Each arm uses three JVMs, with seven measured rounds after two warmups
per JVM. Times cover resident inputs, normalized to one encoding per
input, and include compressor creation, encoding, chunking and cleanup.
They exclude loading and verification. This uses JDK 27 on a shared AMD
Ryzen AI MAX+ 395 host; MB is decimal. One-shot compression produces
7,899,530 bytes as a control, not as a bounded streaming fix.
The end-to-end benchmark builds //src:bazel from the same fixed
source with the before and after clients. Each run starts with a fresh
output base, empty remote blob/action caches and fresh executor file
cache. Disk cache is disabled; repository download/content caches
and image layers are warm. A local BuildBuddy server and executor
(cbc28103a282) run on the same shared host, using a pinned Ubuntu
24.04/GCC 13.3 image. Both clients embed JDK 26.0.1. Builds use 32
jobs; the executor advertises 16 CPUs.
Build metric Before After Change %
------------------------- ----------- ----------- --------
Command wall, s 517.537 516.352 -0.2%
Client CPU, s 224.269 205.670 -8.3%
Client peak memory, MiB 3,437.117 3,378.918 -1.7%
Upload payload, bytes 571,625,818 535,215,489 -6.4%
Same-blob payload, bytes 568,918,992 528,362,873 -7.1%
WriteRequest messages 117,089 115,263 -1.6%
There are three runs per arm, ordered before/after, after/before, then
before/after. All six request the same 4,699 remote action digests
and produce identical binaries, with no remote/disk cache hits,
unexpected RPC failures or retries. Paired wall changes are -4.6%,
-3.5% and +3.7%. Median wall time is essentially flat; these runs
do not establish a build-speed improvement.
Upload bytes count client Write payload, excluding network framing
and executor transfers. Uploaded sets differ slightly, so the
same-blob row compares only the 51,539 compressed blobs uploaded
in every run (918,011,952 raw bytes). Most small blobs still fit in
one message, so message count falls less than payload bytes in this
build. Client CPU includes the batch JVM and waited local children,
not server/executor CPU or encode-only work. Peak memory is the largest
process resident-memory high-water mark, not the sum across processes.
The tradeoff is more retained memory per compressor. Including the
unchanged JNI destination array, Java buffer payload rises from about
129.0 KiB to 160.5 KiB initially, and to 272.5 KiB at the collector
capacity observed for incompressible inputs: 31.5-143.5 KiB extra per
stream. These observed capacities are not strict maxima. They exclude
objects, native zstd memory, upload messages and transient growth.
Buffering remains independent of blob size; generated 64 and 512
MiB inputs complete under a 32 MiB Java heap. Lower measured process
memory does not negate the higher per-stream cost.
At 10,000 initialized compressors, the observed larger-capacity case
adds about 1.37 GiB of Java-array payload, not a total-memory bound.
Jobs limit actions, not blob fanout; asynchronous uploads can outlive
job slots. RPC slots do not cap compressors retained during retry
backoff, so those limits alone do not bound total compressor memory.
### Motivation
Reduce compressed upload size and client encoding work without buffering whole blobs.
### Build API Changes
No.
### Checklist
- [x] I have added tests for the new use cases.
- [x] Documentation is not applicable (no setup or API changes).
### Release Notes
RELNOTES: Reduce zstd-compressed remote upload size and encoding overhead.
Closes #31528
COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/bazel/pull/31528 from sluongng:sluongng/zstd-streaming-upload-perf 87009db24809f51f9c12b53a555dfa96751a594e
PiperOrigin-RevId: 1007356225
Change-Id: I695120fa944ea95d83ae25ac7e1da07557fbf8b0
{Fast, Correct} - Choose two
Build and test software of any size, quickly and reliably.
Speed up your builds and tests: Bazel rebuilds only what is necessary. With advanced local and distributed caching, optimized dependency analysis and parallel execution, you get fast and incremental builds.
One tool, multiple languages: Build and test Java, C++, Android, iOS, Go, and a wide variety of other language platforms. Bazel runs on Windows, macOS, and Linux.
Scalable: Bazel helps you scale your organization, codebase, and continuous integration solution. It handles codebases of any size, in multiple repositories or a huge monorepo.
Extensible to your needs: Easily add support for new languages and platforms with Bazel's familiar extension language. Share and re-use language rules written by the growing Bazel community.
To report a security issue, please email security@bazel.build with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. Our vulnerability management team will respond within 3 working days of your email. If the issue is confirmed as a vulnerability, we will open a Security Advisory. This project follows a 90 day disclosure timeline.
See CONTRIBUTING.md