Skip to content

timely-util: split the chunk body off the edge column - #39159

Merged
antiguru merged 2 commits into
mainfrom
moritz/column-body-split
Oct 1, 2026
Merged

antiguru merged 2 commits into
mainfrom
moritz/column-body-split

Conversation

@antiguru

@antiguru antiguru commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Column served two roles. On a dataflow edge it is built by pushing, crosses an exchange as channel bytes, and is read on arrival, which is what its Typed, Bytes, and Align variants are for. Behind an edge, in a merge batcher's chains, a spill-backed ColumnChunk, and a batch builder's seal, the same type held sorted, consolidated bodies that were only ever typed or, when read back from a store, serialized words. That code never constructed Bytes, and it asserted the variant it expected at seven sites with unreachable!.

ColumnBody<C> is the type at rest: Typed(C::Container) while a body is written, Words(Vec<u64>) once a store handed it back, and nothing else. Writing to a serialized body materializes it by a bulk per-leaf copy, so the merge, extract, advance, and settle bodies address the typed containers without matching on a variant. ColumnChunk, ColumnChunker, ColumnMerger, UnchunkBuilder, RowRowColPagedBuilder, and the correction buffer's chunks and builder now hold bodies. The edge container keeps its name and every operator that names it is untouched.

The boundaries are moves. A chunker takes whatever the edge delivered and emits bodies, and a body goes back onto an edge through From, typed staying typed and words becoming Align. The column pager's batcher and ValRowColPagedBuilder keep Column chains until the pager is retired, so the merge and extract bodies are generic over a MergeChunk trait both types implement, and PagedChunker wraps the chunker for that batcher.

Fifth in the stack, on top of #38897. The chunked consolidation from #38900 hands its bodies back onto the edge through From. #38334 touches the same files and will need rebasing onto this, since edge paging lands on Column alone, which is the point of the split.

Part of CPU-279.

🤖 Generated with Claude Code

@antiguru
antiguru added this pull request to stack #38898 September 25, 2026 14:04
@antiguru antiguru changed the title moritz/column body split timely-util: split the chunk body off the edge column Sep 25, 2026
@antiguru
antiguru requested a review from DAlperin September 30, 2026 12:02
@antiguru
antiguru marked this pull request as ready for review September 30, 2026 12:02
@antiguru
antiguru requested review from a team as code owners September 30, 2026 12:02

@antiguru antiguru left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Self-review, read against the chunk-native arrangements design in #39072.

  • CI: both rust-build jobs failed at docker login ghcr.io with "denied". That is an infrastructure failure, not the code.
  • Fit with #39072: ColumnBody is the seam that design needs. An encoded chunk payload becomes a body form, and Column remains the edge container. MergeChunk should stay minimal, because the design's range builder replaces it.
  • Naming: ColumnBody::Words is specifically the flat indexed encoding. The doc should say so, so that nobody reads it as the general serialized case once a second encoding exists.
  • Copy: into_body on a shared resident body that is already Words decodes it into typed containers. Cloning the words is cheaper and keeps the form.
  • Transitional code: the duplicated empty_chunk and recycle_chunk in merge_batcher.rs should say at their definitions that they exist until the pager is retired.
  • Not a pattern to copy: correction Chunk::body takes a body out of the pool permanently on first read. That suits the correction buffer, but the design's bounded read windows avoid exactly this, so arrangements should not copy it.

🤖 Posted by Claude Code

@antiguru
antiguru requested a review from a team as a code owner September 30, 2026 13:12
@antiguru
antiguru force-pushed the moritz/column-body-split branch from b4afc94 to 31c2f68 Compare September 30, 2026 13:12
@antiguru

Copy link
Copy Markdown
Member Author

Addressed and rebased onto #38900, whose chunked consolidation now hands bodies back onto the edge through From. The Words doc now names the flat indexed encoding. A shared body is copied in its current form through duplicate, which is also used in settle. The pager-only helpers state why they exist.

🤖 Posted by Claude Code

@antiguru
antiguru force-pushed the moritz/column-body-split branch from 31c2f68 to 0b673aa Compare September 30, 2026 15:44
@antiguru
antiguru force-pushed the moritz/column-body-split branch from 0b673aa to 55a8442 Compare September 30, 2026 18:07
Base automatically changed from moritz/correction-pool-substrate to main September 30, 2026 19:45
`Column` served two roles. On a dataflow edge it is built by pushing, crosses an exchange as channel bytes, and is read on arrival, which is what its `Typed`, `Bytes`, and `Align` variants are for. Behind an edge, in a merge batcher's chains, a spill-backed `ColumnChunk`, and a batch builder's seal, the same type held sorted, consolidated bodies that were only ever typed or, when read back from a store, serialized words. That code never constructed `Bytes`, and it asserted the variant it expected at seven sites with `unreachable!`.

`ColumnBody<C>` is the type at rest: `Typed(C::Container)` while a body is written, `Words(Vec<u64>)` once a store handed it back, and nothing else. Writing to a serialized body materializes it by a bulk per-leaf copy (`typed_mut`), so the merge, extract, advance, and settle bodies address the typed containers without matching on a variant. `ColumnChunk`, `ColumnChunker`, `ColumnMerger`, `UnchunkBuilder`, `RowRowColPagedBuilder`, and the correction buffer's chunks and builder now hold bodies. The edge container keeps its name and every operator that names it is untouched.

The boundaries are moves. A chunker takes whatever the edge delivered and emits bodies. A body goes back onto an edge through `From`, typed staying typed and words becoming `Align`, which is how the temporal bucket's peeled chunks, the chunked consolidation's output, and the pager batcher's input travel. The column pager's batcher and `ValRowColPagedBuilder` keep `Column` chains until the pager is retired, so the merge and extract bodies are generic over a `MergeChunk` trait both types implement, and `PagedChunker` wraps the chunker for that batcher.

A shared resident body is copied in its current form when a chunk is unwrapped, so a serialized body is cloned as words rather than decoded into typed containers.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@antiguru

Copy link
Copy Markdown
Member Author

Staging results for the stack, which includes this PR, are posted on #38897: #38897 (comment). They cover V1 against V2 and spill off against spill on, from 200cc down to 50cc.

🤖 Posted by Claude Code

@linear-code

linear-code Bot commented Oct 1, 2026

Copy link
Copy Markdown

CPU-279

@DAlperin DAlperin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but could we have some tests covering merge and extract in the Words variant? Seems uncovered

@antiguru

antiguru commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

@DAlperin Added in 6c6f881. The merge_from and extract proptests now draw each input, and the merge target in the one-input case, in either the typed or the Words form, so serialized sources and a serialized target that has to materialize are both covered.

🤖 Posted by Claude Code

@antiguru
antiguru merged commit 241f928 into main Oct 1, 2026
82 checks passed
@antiguru
antiguru deleted the moritz/column-body-split branch October 1, 2026 17:55
antiguru added a commit that referenced this pull request Oct 1, 2026
The correction buffer spilled every chunk at depth 0, which put all of
them in the buffer pool's hottest eviction band and kept them out of
compression. The merge batcher instead tags chunks with a generational
depth that grows with each merge they survive, and the pool evicts
deeper chunks first and compresses them past
`column_chunk_compress_min_depth`. This change gives correction chunks
the same depth.

A bucket's chain merge writes its output one generation deeper than its
deepest input. The bucket's geometric chain invariant is the same
structure the merge batcher's depth describes: a merged chain waits
about `chain_proportionality` times longer for its next merge. Chunks
that a read writes into `emitted` stay at depth 0, because the caller
iterates them next and the next consolidation merges them with the
feedback retractions. Chunks built from staged updates also start at
depth 0, and a split keeps the depth of the chunk it copies.

On a CPU-bound M.1-nano, compressing every spilled correction chunk was
about 12% slower than not compressing
([CPU-279](https://linear.app/materializeinc/issue/CPU-279)). With the
default floor of 1, this change compresses the chain-merge generations
of a hydration snapshot, so it trades CPU for memory on such replicas.
The floor stays tunable per replica.

On top of #39159. Part of
[CPU-279](https://linear.app/materializeinc/issue/CPU-279).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <[email protected]>
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