Skip to content

repr: back RowArena with a doubling region allocator#37114

Merged
frankmcsherry merged 2 commits into
MaterializeInc:mainfrom
frankmcsherry:rowarena-region
Jun 18, 2026
Merged

repr: back RowArena with a doubling region allocator#37114
frankmcsherry merged 2 commits into
MaterializeInc:mainfrom
frankmcsherry:rowarena-region

Conversation

@frankmcsherry

Copy link
Copy Markdown
Contributor

What

Reworks RowArena from "one Vec<u8> allocation per push_bytes, all freed on clear" into a bump allocator over a stack of byte regions:

  • push_bytes copies bytes into the active (last) region. When that region lacks spare capacity it allocates a new, larger region (doubling) rather than growing the current one — so a region that already holds data is never reallocated and references returned earlier stay valid. The outer Vec may reallocate as regions are added, but that only moves the Vec<u8> headers, not the heap buffers they own.
  • push_bytes now accepts any B: Deref<Target = [u8]> (e.g. Vec<u8>, &[u8]). Existing Vec<u8> callers are unchanged; borrowed sources no longer need a throwaway allocation just to hand bytes over.
  • clear retains only the single largest region (emptied) and drops the rest, right-sizing the arena. An arena reused across clear cycles — e.g. decoding arrangement rows one at a time — becomes allocation-free in steady state.
  • reserve now reserves bytes in the active region (was: slots in the outer vector); with_capacity sizes the initial region. Neither has load-bearing callers (the reserve users in expr pass size hints).

Why

Follows up the ExtendDatums arena work (#37110) and the per-row arena hoist (#37113). Once the arena is the decode target for compressed arrangement rows (per-column codecs, #37111), a fresh allocation per value per row is the dominant cost; a reused doubling region removes it. The pattern mirrors columnation's region allocator.

The trade-off is the standard bump-allocator one: a single very large row leaves the arena holding a large region until the next clear. For the per-row-cleared decode/eval paths the high-water mark is just the largest single row, so it stays bounded.

Tests

Adds mz_ore::tests in repr::row covering:

  • references from push_bytes remaining valid after later pushes force new regions,
  • push_unary_row reading a row back from a non-zero region offset (confirms decoding is position/alignment independent),
  • reuse across clear.

No behavior change for callers; no release note.

Comment thread src/repr/src/row.rs Outdated
Comment thread src/repr/src/row.rs
Comment thread src/repr/src/row.rs
Comment thread src/repr/src/row.rs
frankmcsherry added a commit to frankmcsherry/materialize that referenced this pull request Jun 17, 2026
Addresses review feedback on MaterializeInc#37114: when reserve() must stage a fresh region
(the active region holds live data), size it like push_bytes does — at least
double the current region — so a run of small reserve() calls yields at most
log-many regions instead of many small ones. Notes in clear() that region
capacities are therefore monotonic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@antiguru antiguru 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.

Seems good!

Comment thread src/repr/src/row.rs
frankmcsherry added a commit that referenced this pull request Jun 18, 2026
## What

`render_decode_chunk` (the oneshot/COPY decode operator) created one
`RowArena` in the operator body and reused it across every chunk and
every row **without ever clearing it**. The arena owns whatever is
pushed into it for its lifetime, so the MFP-evaluation temporaries for
the *entire source* accumulated until the COPY finished — an unbounded
arena proportional to the data volume.

This scopes the arena to each decoded chunk and clears it per row, so it
holds at most one row's worth of temporaries and is released when the
chunk is processed.

## Why

Found while auditing `RowArena` lifetimes (companion to the arena work
in #37114 / #37134). This was the only arena in the audit that was both
operator-lived **and** never cleared — i.e. a genuine unbounded
accumulation rather than a bounded high-water. It brings the operator in
line with the "arena per batch, cleared per row" idiom the other
operators use.

## Tests

No behavior change (the MFP output is identical; only the arena's
lifetime changes). Covered by the existing oneshot-source / COPY FROM
tests.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
frankmcsherry and others added 2 commits June 18, 2026 09:46
`RowArena` previously stored each `push_bytes` argument as its own `Vec<u8>`
allocation and freed them all on `clear`. Rework it as a bump allocator over a
stack of byte regions: `push_bytes` copies into the active region, allocating a
new (doubled) region only when the active one lacks spare capacity, and never
resizing a region that already holds data (so returned references stay valid).
`clear` now retains the single largest region, emptied, so an arena reused
across clears — e.g. decoding rows one at a time — becomes allocation-free in
steady state.

`push_bytes` now accepts any `Deref<Target = [u8]>` (e.g. `Vec<u8>`, `&[u8]`)
since it copies rather than taking ownership; existing `Vec<u8>` callers are
unaffected, and borrowed sources no longer need a throwaway allocation.
`reserve` reserves bytes in the active region rather than slots in the outer
vector, and `with_capacity` sizes the initial region.

Adds tests covering reference validity across region growth, reading a unary
row from a non-zero region offset, and reuse across `clear`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on MaterializeInc#37114: when reserve() must stage a fresh region
(the active region holds live data), size it like push_bytes does — at least
double the current region — so a run of small reserve() calls yields at most
log-many regions instead of many small ones. Notes in clear() that region
capacities are therefore monotonic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@frankmcsherry frankmcsherry enabled auto-merge (squash) June 18, 2026 14:16
@frankmcsherry frankmcsherry merged commit 1553727 into MaterializeInc:main Jun 18, 2026
117 checks passed
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