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
5 files changed
tree: a5d69eea8d611b15c64e0b48d7630dce33365142
  1. .bazelci/
  2. .devcontainer/
  3. .gemini/
  4. .github/
  5. docs/
  6. examples/
  7. scripts/
  8. src/
  9. third_party/
  10. tools/
  11. .bazelrc
  12. .bazelversion
  13. .gitattributes
  14. .gitignore
  15. AGENTS.md
  16. AUTHORS
  17. bazel_downloader.cfg
  18. BUILD
  19. CHANGELOG.md
  20. CODE_OF_CONDUCT.md
  21. CODEOWNERS
  22. combine_distfiles.py
  23. combine_distfiles_to_tar.sh
  24. compile.sh
  25. CONTRIBUTING.md
  26. CONTRIBUTORS
  27. distdir.bzl
  28. extensions.bzl
  29. LICENSE
  30. maven_install.json
  31. MODULE.bazel
  32. MODULE.bazel.lock
  33. oneversion_allowlist.csv
  34. oneversion_allowlist_for_tests.csv
  35. pyproject.toml
  36. README.md
  37. repositories.bzl
  38. requirements.txt
  39. SECURITY.md
  40. verify_module_bazel_lock.sh
README.md

Bazel

{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.

Getting Started

Documentation

Reporting a Vulnerability

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.

Contributing to Bazel

See CONTRIBUTING.md

Build status