Skip to content

test(bare-metal): two allocator arms, and the on-chip heap budget - #16

Merged
Ttimmahlax merged 1 commit into
mainfrom
test/esp32s3-allocator-arms
Sep 10, 2026
Merged

test(bare-metal): two allocator arms, and the on-chip heap budget#16
Ttimmahlax merged 1 commit into
mainfrom
test/esp32s3-allocator-arms

Conversation

@Ttimmahlax

Copy link
Copy Markdown
Contributor

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; 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: current after a round
trip 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:

esp-alloc 0.11 rusty_alloc 2.0.5
round trip at 192 KiB PASS OOM on a 1,536 B allocation
round trip at 256 KiB n/a OOM on a 656 B allocation
320 KiB region n/a does not link, "Main stack is smaller than 8192 bytes"

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
.stack until the linker refuses.

Written up for the allocator's maintainers in
rusty_alloc/docs/plans/esp32-large-alloc-ceiling.md, framed as evidence plus a
hypothesis 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 constant
within 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

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>
@Ttimmahlax
Ttimmahlax merged commit 5293a85 into main Sep 10, 2026
8 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