Skip to content

feat(types): ra_segment_size="256k" for large-allocation consumers; name the ceiling in the API - #31

Merged
Ttimmahlax merged 3 commits into
mainfrom
esp32-large-alloc-ceiling
Sep 10, 2026
Merged

feat(types): ra_segment_size="256k" for large-allocation consumers; name the ceiling in the API#31
Ttimmahlax merged 3 commits into
mainfrom
esp32-large-alloc-ceiling

Conversation

@Ttimmahlax

Copy link
Copy Markdown
Contributor

Closes docs/plans/esp32-large-alloc-ceiling.md (now in finished/, §9), reported from the rusty_zstd bare-metal work.

The report, confirmed on silicon

In a 256 KiB region, rusty_alloc serves exactly one 64 KiB allocation. The second fails, with 192 KiB of the region unused.

Reproduced on a XIAO ESP32-S3 with their shape, and their hypothesis was right:

[big] geometry: SEGMENT_SIZE=65536 LARGEST_SHARED_ALLOC=61440 dedicated_segments(65536)=2
[big] before #0: used=0 free=262144 total=262144 | free_segments=4 largest_servable=258048
[big] block #0 OK (live 64 KiB)
[big] before #1: used=135168 free=126976 total=262144 | free_segments=1 largest_servable=61440
[big] block #1 REFUSED
[big] served 1 blocks of 65536

A segment's slice 0 is its header, so LARGE_OBJ_SIZE_MAX = SEGMENT_SIZE - SEGMENT_SLICE_SIZE = 61,440. One byte over and huge_alloc reserves 4 KiB + size on a SEGMENT_SIZE stride, spanning two segments. Structural: no allocation of SEGMENT_SIZE can share a segment with its own metadata. Their sweep request puts the cliff at 61,440 exactly.

Their "roughly 50 %" read 25 % because of a cost they could not see: the first small allocation claims a whole segment. used=135168 is block #0's 69,632 plus 65,536 for a Vec spine.

The fix, measured on the same board

--cfg ra_segment_size="256k" → 8 KiB slice × 32, so LARGEST_SHARED_ALLOC is 253,952 and a 64 KiB request is a span that packs three to a segment.

geometry 64 KiB blocks served payload live region used
default 1 64 KiB 25 %
ra_segment_size="256k" 3 192 KiB 75 %

Their kill test — "PASSES WHEN: at least 3 succeed"passes, and "no region size works on an S3" is no longer true: 256 KiB serves three 64 KiB blocks with 56 KiB of slices left over, inside their 320 KiB ceiling. Opt-in, because it doubles the page floor a small-object workload pays.

Their second ask: report the real constraint

  • region_capacity() -> (free_segments, largest_servable). Free bytes hide this; the refused call read (1, 61440) against 126,976 free.
  • LARGEST_SHARED_ALLOC, dedicated_segments(size), region_for_allocs(size, count) make the ceiling a compile-time answer. region_for_allocs counts the small-allocation segment above.

Against the README

Their last point is taken. The floor section described (classes touched) × (page size) as "independent of bytes requested" and amortising with the working set. True for small objects, and it inverts for segment-sized ones, where the cost is a granularity tax scaling with how many are live. Both READMEs now say so with these numbers.

A rung built and withdrawn

4 KiB × 32 (128 KiB) buys a 64 KiB consumer nothing — one 16-slice span in a 31-slice segment is still 128 KiB per block, the default's cost by another route. The lever is LARGEST_SHARED_ALLOC / size, not SEGMENT_SIZE. It also segfaulted 11 runs in 12 under the concurrent host battery where the default and 256k are 0/40 and 0/12. Real, geometry-specific, passes single-threaded, not root-caused — kept out of the shipped set and written up in §9.5 in case it is latent rather than local.

Gates

  • 20 suites green at both shipped geometries, plus the hosted default.
  • CI runs the whole suite at 256k, not just a build.
  • gate-selftest 11/11: new mutation drops the header slice from dedicated_segments and the sizing test goes red.
  • The reproduction is a permanent property-based test against the real extent allocator.
  • wasm 20,168 B (baseline 20,169). Unsafe census +4, all #[cfg(test)]; the fix adds none to shipped code.
  • Semver: additions only. No existing signature or value moves at the default geometry.

🤖 Generated with Claude Code

tim-almond-house and others added 3 commits September 9, 2026 18:26
…ame the ceiling in the API

Reported from the rusty_zstd bare-metal work: in a 256 KiB region rusty_alloc
served ONE 64 KiB allocation and refused the second with 192 KiB free.
Reproduced on a XIAO ESP32-S3, and the reporter's hypothesis was right.

A segment's slice 0 holds its header, so LARGE_OBJ_SIZE_MAX is
SEGMENT_SIZE - SEGMENT_SLICE_SIZE = 61,440. One byte over and `huge_alloc`
reserves 4 KiB + size on a SEGMENT_SIZE stride, spanning two segments. That is
structural: no allocation of SEGMENT_SIZE can share a segment with the metadata
describing it. Their 50% estimate measured 25% because of a cost they could not
see -- the first small allocation claims a whole segment.

`--cfg ra_segment_size="256k"` moves the small profile to an 8 KiB slice x 32,
raising LARGEST_SHARED_ALLOC to 253,952 so a 64 KiB request becomes a span that
packs three-to-a-segment. Same board, same 256 KiB region: 1 block -> 3, 25% ->
75% utilisation. Their kill test ("at least 3 succeed") passes. Opt-in, because
it doubles the page floor a small-object workload pays.

Also: LARGEST_SHARED_ALLOC, dedicated_segments(size) and region_for_allocs(size,
count) make the ceiling a compile-time answer; region_capacity() reports
(free_segments, largest_servable) because free BYTES hide this failure -- the
refused call had 126,976 free and read (1, 61440). The README's floor model
claimed the cost amortises as the working set grows, which is true for small
objects and inverts for segment-sized ones; it now says so.

A 4 KiB x 32 (128 KiB) rung was built and withdrawn: it buys a 64 KiB consumer
nothing (one span per segment is still 128 KiB per block), and it segfaulted
11/12 under the concurrent host battery where the shipped geometries are 0/40
and 0/12. Real, geometry-specific, not root-caused, recorded in the plan.

Gates: 20 suites green at both shipped geometries, CI runs the whole suite at
256k, gate-selftest 11/11 with a new mutation on the header slice, and the
reproduction is a permanent property-based test. Unsafe +4, all cfg(test).

Plan closed: docs/plans/finished/esp32-large-alloc-ceiling.md (section 9).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The kill test passes on the board. rusty_zstd's real workload was not run here
-- 64 KiB blocks in a loop is not match tables through a round trip. The
arithmetic says their 175,832-byte peak fits at this geometry; packing depends
on the live set, and only that firmware can settle it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fixed-region recipe was reachable from one path except for the type its
fallible entry points return. `PrimError` has always been public, but as
`prim::PrimError` -- so a seam re-exporting this API in a single `pub use`
could name `Region`, `good_region_size`, `init_region` and every `FERR_*`
value, and not the error they produce.

The Kairos RTOS allocator seam hit exactly that and carries a comment saying a
public error type "arrives with the next rusty_alloc release"
(rusty_RTOS/docs/plans/build-me-bare.md, brick B3). Verified against the
PUBLISHED 2.0.5: `prim::fixed::PrimError` is a private `use` and fails to
resolve, while `prim::PrimError` works. Additive; same type, one more path.

Checked by building the seam's exact re-export list, plus REGION_ALIGN and the
four FERR_* values, on riscv32imac-unknown-none-elf and thumbv7em-none-eabihf
under --cfg ra_single_threaded --cfg ra_small_profile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Ttimmahlax
Ttimmahlax merged commit 1d8d819 into main Sep 10, 2026
9 of 12 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