Skip to content

Hash compressed template input off the decoder thread - #778

Open
zibi-pplx wants to merge 1 commit into
mainfrom
pr778
Open

zibi-pplx wants to merge 1 commit into
mainfrom
pr778

Conversation

@zibi-pplx

@zibi-pplx zibi-pplx commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Template materialization computes the compressed rootfs SHA-256 inline in the zstd decoder's reads. Once the expanded image is hashed with BLAKE3, the decoder thread is the slowest stage: on the Oct 20 r6id.metal fixture (6.44 GB compressed, 24 GiB logical) it used 12.6 CPU-seconds of a 16.0-second materialization and never waited for a free buffer, and the compressed SHA-256 accounted for about 4.75 of those seconds. The sparse writer, the next stage, used 7.5.

The decoder now reads the compressed file into a pool of three 8 MiB buffers. Each filled buffer is shared with a dedicated thread that computes the same SHA-256 and returns the buffer to the pool, so hashing overlaps decoding, and a slow hash makes reads wait rather than grow memory. The digest, its format, where it is compared and the mismatch handling are unchanged. The input pool uses 24 MiB per materialization instead of one 8 MiB read buffer.

Expected effect, projected from the stage CPU above rather than measured on that worker:

  • Releases with BLAKE3 identities: materialization of about 16 s falls to about 11-12 s, limited by the decoder without the hash (about 7.8 s) and the sparse writer (about 7.5 s). That is roughly 0.75 s per compressed GB on cold template pulls, prefetches and compressed imports.
  • Releases with SHA-256 identities, which today are all of them: little or no change, because the expanded SHA-256 thread is the bottleneck there and the decoder already waited 7.7 s for buffers.
  • Sandbox snapshot restores are unaffected; they do not use this path.
    Total CPU per materialization is unchanged; one more thread runs during it.

Validation: the 255 template-manager tests pass, including a new test that reads input through one-byte to larger-than-input buffers, with short reads and recycled pools, and checks the delivered bytes and digest against SHA-256 of the whole input. Clippy is clean. A local interleaved A/B on Apple silicon (12 GiB logical, 3.2 GB compressed, four runs each) produced identical, independently verified compressed digests, with materialization 4.5% faster for SHA-256 identities and 0.9% faster for BLAKE3 identities. That machine's SHA-256 hardware leaves the expanded hash as its bottleneck in both cases, so it confirms correctness and no regression, not the projected x86 gain.

🤖 Generated with Claude Code

Template materialization computes the compressed rootfs SHA-256 inline in the zstd decoder's reads. Once the expanded image is hashed with BLAKE3, the decoder thread is the slowest stage: on the Oct 20 r6id.metal fixture (6.44 GB compressed, 24 GiB logical) it used 12.6 CPU-seconds of a 16.0-second materialization and never waited for a free buffer, and the compressed SHA-256 accounted for about 4.75 of those seconds. The sparse writer, the next stage, used 7.5.

The decoder now reads the compressed file into a pool of three 8 MiB buffers. Each filled buffer is shared with a dedicated thread that computes the same SHA-256 and returns the buffer to the pool, so hashing overlaps decoding, and a slow hash makes reads wait rather than grow memory. The digest, its format, where it is compared and the mismatch handling are unchanged. The input pool uses 24 MiB per materialization instead of one 8 MiB read buffer.

Expected effect, projected from the stage CPU above rather than measured on that worker:
- Releases with BLAKE3 identities: materialization of about 16 s falls to about 11-12 s, limited by the decoder without the hash (about 7.8 s) and the sparse writer (about 7.5 s). That is roughly 0.75 s per compressed GB on cold template pulls, prefetches and compressed imports.
- Releases with SHA-256 identities, which today are all of them: little or no change, because the expanded SHA-256 thread is the bottleneck there and the decoder already waited 7.7 s for buffers.
- Sandbox snapshot restores are unaffected; they do not use this path.
Total CPU per materialization is unchanged; one more thread runs during it.

Validation: the 255 template-manager tests pass, including a new test that reads input through one-byte to larger-than-input buffers, with short reads and recycled pools, and checks the delivered bytes and digest against SHA-256 of the whole input. Clippy is clean. A local interleaved A/B on Apple silicon (12 GiB logical, 3.2 GB compressed, four runs each) produced identical, independently verified compressed digests, with materialization 4.5% faster for SHA-256 identities and 0.9% faster for BLAKE3 identities. That machine's SHA-256 hardware leaves the expanded hash as its bottleneck in both cases, so it confirms correctness and no regression, not the projected x86 gain.

@nickdujay-pplx nickdujay-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed buffer ownership, backpressure, thread shutdown, digest verification, publication order, and rollback compatibility. The input pool is bounded, chunks are shared without another byte copy, and existing artifact formats and verification remain unchanged.

All 255 template-manager tests passed locally. Additional checks of the extracted reader verified bytes/digests across pool depths and 150 early drops. Approving with two non-blocking follow-ups: measure the target-architecture benefit under concurrent materialization, and retain EOF state in the private reader. The repeated-EOF issue is not reached by the current zstd call path.


const ROOTFS_SPARSE_BLOCK_BYTES: usize = 64 * 1024;
const COMPRESSED_INPUT_BUFFER_SIZE: usize = 8 * 1024 * 1024;
const COMPRESSED_INPUT_PIPELINE_DEPTH: usize = 3;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking performance follow-up: please add an interleaved before/after benchmark on the target CPU architecture, using a synthetic representative image and concurrent distinct-template pulls. Record wall time, CPU, and peak RSS for both digest algorithms. The fixed pool bounds this stage to 24 MiB, but the extra 16 MiB and hash thread apply per materialization; the call site also decompresses manifest entries concurrently. The overlap looks sound, but the projected decoder improvement needs that measurement before we rely on it for capacity or latency expectations.

.as_ref()
.is_none_or(|chunk| self.position >= chunk.len);
if exhausted {
self.next_chunk()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking reader robustness follow-up: preserve EOF without checking out another pool buffer, and add repeated-EOF coverage. I reproduced this with an empty input and depth 3: three reads return 0; the fourth blocks in free_rx.recv(). An empty chunk is not sent to the hash worker, and the next fill_buf drops it, so each EOF read removes a buffer from the pool. The current zstd reader switches to its own EOF state after the first empty buffer, so this does not block the materialization path reviewed here. An EOF flag or retaining the empty current chunk would make the reader safe to reuse.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants