Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ jobs:
run: cargo clippy -p rusty_alloc --no-default-features -- -D warnings
env:
RUSTFLAGS: --cfg ra_single_threaded
# The reentrancy detector only exists under `ra_single_threaded`, so the
# normal test run cannot see it. An allocating ISR is the one bare-metal
# failure that used to be an unbounded spin with no message.
- name: reentrancy detector (single-context builds)
run: cargo test -p rusty_alloc --lib prim::fixed
env:
RUSTFLAGS: --cfg ra_single_threaded
- name: prove the single-thread gate is not vacuous
run: |
if cargo check -p rusty_alloc --no-default-features \
Expand Down
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,49 @@ ESP32-S3. Everything below was measured on a **Seeed XIAO ESP32-S3 Sense at
240 MHz**, against **`esp-alloc` 0.11**, the standard allocator for the
`esp-hal` bare-metal track.

### What a firmware has to set

**All three of these, not two.** Every number in this section is *of this
configuration*; the 68 KiB floor below is meaningless without the geometry flag.

```toml
rusty_alloc-api = { version = "2", default-features = false }
```

```sh
RUSTFLAGS="--cfg ra_single_threaded --cfg ra_small_profile"
```

```rust
// A region the linker owns, aligned to a segment. Hand it over once, before
// the first allocation.
#[repr(align(65536))]
struct Region([u8; 68 * 1024]);
static mut REGION: Region = Region([0; 68 * 1024]);

// SAFETY: the only reference ever taken to REGION.
rusty_alloc::prim::fixed::init_region(unsafe { &mut (*(&raw mut REGION)).0 })
.expect("region is large enough and registered once");
```

| flag | what happens without it |
|---|---|
| `--cfg ra_single_threaded` | **build fails**, with a message telling you to set it |
| `--cfg ra_small_profile` | **builds and links clean, then nothing allocates** — `SEGMENT_SIZE` stays 32 MiB, a kilobyte-scale region yields zero segments, and the first `Vec` returns null |
| `init_region` | every allocation fails; the backend has no memory |

That middle row is the trap, and it was reported by the first outside firmware
to adopt 2.0.0 (`docs/plans/embedded-adoption.md`). `ra_single_threaded`
announces itself, so an integrator reasonably concludes the crate tells you what
it needs — and `ra_small_profile` did not. `init_region` now refuses a region
that cannot hold one segment at the active geometry, so the mistake is an `Err`
at startup rather than a wasted board run; **`--cfg ra_small_profile` is still
what you want to set**, because refusing early is a diagnosis, not a fix.

`ra_small_profile` is a `--cfg` and not a Cargo feature on purpose: it is
non-additive. Two crates in one graph cannot disagree about `SEGMENT_SIZE` the
way they can harmlessly disagree about `std`.

### Throughput — 2.0x to 3.7x faster

Nanoseconds per allocate/free pair, lower is better:
Expand Down Expand Up @@ -217,7 +260,7 @@ in a medium page.

| | `esp-alloc` | `rusty_alloc` |
|---|---:|---:|
| smallest heap that runs the same workload | **8 KiB** | 68 KiB |
| smallest heap that runs the same workload | **8 KiB** | 68 KiB *(needs `--cfg ra_small_profile`)* |
| peak live bytes (identical, the parity check) | 4,914 | 4,914 |
| app image | 116,032 B | 127,088 B (+9.5%) |

Expand Down
34 changes: 34 additions & 0 deletions crates/rusty_alloc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `prim::fixed::MIN_REGION` and `prim::fixed::usable_bytes(base, len)` — a
firmware can now size its heap region at COMPILE time
(`const _: () = assert!(N >= MIN_REGION)`) instead of discovering the answer
on silicon, and can log how much of a region can actually back segments. The
`k * SEGMENT_SIZE + FIXED_PAGE` rule previously existed only in a design
document; a 220 KiB region strands 24,576 bytes and nothing said so.
- Distinct `prim::fixed` error codes: `FERR_TOO_SMALL`, `FERR_GEOMETRY`,
`FERR_REGISTERED`. One sentinel covered three conditions with three different
fixes.

### Fixed

- **`init_region` accepted a region that could never yield a segment.** Setting
`--cfg ra_single_threaded` (which the crate demands loudly) without
`--cfg ra_small_profile` (which nothing demanded) left `SEGMENT_SIZE` at
32 MiB, so a kilobyte-scale region returned `Ok(())`, linked clean, and then
failed every allocation on the board with a backtrace pointing at whatever
allocated first. It now returns `FERR_GEOMETRY`, checked against the real base
address rather than the length alone — an unaligned base needs up to
`SEGMENT_SIZE - 1` more than a length test would demand.
- **An allocating interrupt handler hung the firmware silently.**
`prim::fixed`'s lock is not reentrant, and on a single-context target a lock
observed held can only mean reentrancy. That is now a panic naming the ISR
instead of an unbounded spin that surfaces as a watchdog reset. Confirmed with
a load, because `compare_exchange_weak` may fail spuriously and a bare CAS
failure would misfire.
- The README documents `--cfg ra_small_profile`, which it never mentioned, and
attaches it to the 68 KiB figure that is only true under it.

All four reported by the first outside firmware to adopt 2.0.0
(`docs/plans/embedded-adoption.md`).

## [2.0.0](https://github.com/Remade-With-Rust/rusty_alloc/compare/rusty_alloc-v1.1.6...rusty_alloc-v2.0.0) - 2026-09-07

### Breaking
Expand Down
9 changes: 8 additions & 1 deletion crates/rusty_alloc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,15 @@ realloc chains, zeroing over dirtied memory, fragmentation, exhaustion); it
found and fixed a real reclamation bug on the way, after which 512 B capacity no
longer decays and churn NULLs fell from 22,533 to 357 per 50,000.

**A firmware must set three things**, not two: `default-features = false`,
`--cfg ra_single_threaded` (the build fails without it, loudly) and
**`--cfg ra_small_profile`** (nothing tells you, and without it `SEGMENT_SIZE`
stays 32 MiB, a kilobyte-scale region yields zero segments and every allocation
fails) — then hand the backend its memory with
`prim::fixed::init_region`. The full recipe is in the repository README.

**It costs RAM to get that.** The smallest heap that runs the same workload is
**68 KiB for `rusty_alloc` against 8 KiB for `esp-alloc`** — a linked-list
**68 KiB for `rusty_alloc` (at that geometry) against 8 KiB for `esp-alloc`** — a linked-list
heap's floor is `bytes live + header`, while a size-class page allocator's is
`(classes touched) x (page size)`, independent of bytes requested. That floor is
roughly fixed, so it amortises as the working set grows. Reach for `esp-alloc`
Expand Down
Loading
Loading