feat(types): ra_segment_size="256k" for large-allocation consumers; name the ceiling in the API - #31
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes
docs/plans/esp32-large-alloc-ceiling.md(now infinished/, §9), reported from therusty_zstdbare-metal work.The report, confirmed on silicon
Reproduced on a XIAO ESP32-S3 with their shape, and their hypothesis was right:
A segment's slice 0 is its header, so
LARGE_OBJ_SIZE_MAX = SEGMENT_SIZE - SEGMENT_SLICE_SIZE= 61,440. One byte over andhuge_allocreserves4 KiB + sizeon aSEGMENT_SIZEstride, spanning two segments. Structural: no allocation ofSEGMENT_SIZEcan 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=135168is block #0's 69,632 plus 65,536 for aVecspine.The fix, measured on the same board
--cfg ra_segment_size="256k"→ 8 KiB slice × 32, soLARGEST_SHARED_ALLOCis 253,952 and a 64 KiB request is a span that packs three to a segment.ra_segment_size="256k"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_allocscounts 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, notSEGMENT_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
gate-selftest11/11: new mutation drops the header slice fromdedicated_segmentsand the sizing test goes red.#[cfg(test)]; the fix adds none to shipped code.🤖 Generated with Claude Code