test(bare-metal): two allocator arms, and the on-chip heap budget - #16
Merged
Conversation
One firmware source, the allocator picked by a cargo feature, so the only
difference between the builds is which allocator serves the codec.
THE BUDGET, which is the number a firmware author actually needs:
workload peak heap smallest heap that round-trips
L1 only 112,468 B 112 KiB (110 KiB fails)
L1 + L3 + L5 175,832 B 176 KiB (172 KiB fails)
The jump is DFast, not depth: L1 peaks at 112,468 and L3 at 175,832, and L5
adds nothing over L3. The second hash table DFast introduces costs ~62 KiB
here, for 19.39x against L1's 17.65x on this corpus -- so a firmware that can
live with L1 saves a third of its heap. Everything is transient: `current`
after a round trip is 41,292 bytes.
THE rusty_alloc ARM DOES NOT WORK ON THIS PART, and not because of the codec.
It OOMs at 192 KiB (on a 1,536 B allocation) and at 256 KiB (on 656 B), and a
320 KiB region does not link at all ("Main stack is smaller than 8192 bytes"),
which is the RAM ceiling on an S3. Reduced to two lines with no zstd in them:
in a 256 KiB region it serves exactly ONE 64 KiB allocation, the second failing
with 192 KiB of the region still free. 32 KiB blocks pack fine, four of them.
The cliff is at the segment size. Written up for the allocator's maintainers in
rusty_alloc/docs/plans/esp32-large-alloc-ceiling.md, framed as evidence and a
hypothesis rather than a diagnosis, since we have not read their internals.
Static cost of the arm, both at 192 KiB, from the linked ELF: .bss +2,060,
.data -76, .stack -1,996, flash +7,536. The RAM columns sum to a constant
within 12 bytes, so the stack is what actually moves.
No timing is claimed and none should be: buffer placement alone moves compute
kernels on this part by up to 20%, so an allocator throughput comparison from
this rig would measure placement.
An instrument bug caught on the way, recorded because it nearly shipped as a
finding: a sweep script wrote escaped backslashes into the source, every build
after it failed, and three "identical peak" readings were the SAME STALE BINARY
reflashed. The real per-level numbers above are from a verified fresh build.
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.
One firmware source, the allocator picked by a cargo feature, so the only
difference between the builds is which allocator serves the codec.
The budget, which is the number a firmware author actually needs
The jump is DFast, not depth. L1 peaks at 112,468 and L3 at 175,832; L5 adds
nothing over L3. The second hash table DFast introduces costs about 62 KiB here,
buying 19.39x against L1's 17.65x on this corpus. A firmware that can live with
L1 saves a third of its heap. Everything is transient:
currentafter a roundtrip is 41,292 bytes, so this is a peak, not a resident cost.
The rusty_alloc arm does not work on this part
Not because of the codec:
Reduced to two lines with no zstd in them: in a 256 KiB region it serves
exactly one 64 KiB allocation, and the second fails with 192 KiB of the region
still free. Blocks of 32 KiB pack fine, four of them. The cliff is at the
segment size, and since RAM here is a fixed map, a larger region comes out of
.stackuntil the linker refuses.Written up for the allocator's maintainers in
rusty_alloc/docs/plans/esp32-large-alloc-ceiling.md, framed as evidence plus ahypothesis and two discriminating experiments rather than a diagnosis, since we
have not read their internals.
Static cost of the arm, both at 192 KiB, from the linked ELF:
.bss+2,060,.data−76,.stack−1,996, flash +7,536. The RAM columns sum to a constantwithin 12 bytes, so the stack is what actually moves.
What is deliberately not claimed
No timing. Buffer placement alone moves compute kernels on this part by up to
20%, so an allocator throughput comparison from this rig would be measuring
placement, not the allocator.
An instrument bug, recorded because it nearly shipped as a finding
A sweep script wrote escaped backslashes into the source. Every build after it
failed, and three "identical peak" readings across L1/L3/L5 were the same
stale binary reflashed. The per-level numbers above are from a verified fresh
build, and the identical-looking result was the tell.
🤖 Generated with Claude Code