Skip to content

feat(embedded)!: no_std on bare metal, a small-metal geometry, and three reclamation fixes (v2.0.0) - #17

Merged
Ttimmahlax merged 15 commits into
mainfrom
feat/no-std-small-metal
Sep 8, 2026
Merged

feat(embedded)!: no_std on bare metal, a small-metal geometry, and three reclamation fixes (v2.0.0)#17
Ttimmahlax merged 15 commits into
mainfrom
feat/no-std-small-metal

Conversation

@Ttimmahlax

Copy link
Copy Markdown
Contributor

Puts rusty_alloc on a microcontroller, and fixes what running it there exposed.

Measured on hardware

Seeed XIAO ESP32-S3 Sense at 240 MHz, against esp-alloc 0.11. One firmware source with --cfg selecting the allocator, equal budgets, a subtracted harness floor (162 ns/op, identical in both arms) and checksums proving work parity.

workload esp-alloc rusty_alloc speedup
32 B alloc/free 1,638 640 2.56x
64 mixed blocks (8-512 B), batched 1,792 871 2.06x
churn: 64 live, random 8-512 B 3,987 1,069 3.73x
2048 B alloc/free 1,638 1,380 1.19x

It costs RAM: 68 KiB against esp-alloc's 8 KiB for the same workload. That gap is structural (bins x page size vs bytes + header), it is in both READMEs, and it is not sold as anything else.

Three reclamation defects, found by an adversarial stress battery

  • collect could not reclaim a bin's last empty page. It had borrowed mi_page_retire's keep-one-page rule; upstream frees an all-free page unconditionally. Harmless at 512 slices per segment, fatal at 16: 512 B capacity decayed 168 -> 8 blocks while 61,440 bytes sat free.
  • Nothing collected automaticallygeneric_collect was declared and read by nothing.
  • The generic path reported OOM while holding reclaimable memory. It now reclaims once and retries before returning null; free on the happy path.

Churn failures: 22,533 -> 357 per 50,000 allocations. Capacity no longer decays at all.

Plus, from the same campaign: an exclusive arena's huge path could escape to the OS; the arena bitmap scan stopped at word 0; stats.segments did not balance segments_freed; and the fixed-region backend's bottom-up placement cost a whole segment of reach per page-sized block.

Production gates, because none of this was covered

  • CI now runs the small-profile suite, clippy on the small profile and no_std, and both bare-metal RISC-V targets at both geometries.
  • tools/gate-selftest.sh reintroduces five real defects and requires the suite to go red for each. Four tests in this campaign passed under the exact bug they existed to catch.
  • no_std refuses to compile without --cfg ra_single_threaded — three things in it are sound only with one thread, and all three fail quietly.
  • A scheduled icount job regenerates the published instruction counts, which had aged silently.

Breaking

Neither affects a consumer on default features.

  • default-features = false now selects no_std (it used to be identical to the default). Add features = ["std"] to keep the old behaviour.
  • heap::Heap gained a field; all its fields are pub and it is not #[non_exhaustive], so a struct literal naming every field no longer compiles.

Known, and stated in the README

The host callgrind figures predate the reclamation fixes and are marked as a floor pending re-measurement — that needs Linux + valgrind, which the icount job now provides. The retire_expire per-page countdown remains unimplemented; a measured sweep-period default replaces it.

Verification: 106 tests / 33 suites default, 89 / 19 small profile, 0 failed; clippy clean on default / small profile / no_std; fmt clean; unsafe census RATCHET OK; gate selftest 5/5; both RISC-V targets at both geometries and wasm32 build; board kill test green at the 68 KiB floor.

Detail in docs/plans/small-metal.md and docs/LEDGER.md.

🤖 Generated with Claude Code

…ree reclamation fixes

Puts rusty_alloc on a microcontroller and fixes what running it there exposed.
Measured on a Seeed XIAO ESP32-S3 Sense at 240 MHz against esp-alloc 0.11, one
firmware source with --cfg selecting the allocator, equal budgets, a subtracted
harness floor and checksums proving work parity: 2.06-3.73x faster across four
allocate/free workloads.

Three reclamation defects, all found by an adversarial stress battery:

- `collect` borrowed `mi_page_retire`'s keep-one-page-per-bin rule, so no
  collect at any level could return a size class's page to a different class.
  Upstream frees an all-free page unconditionally ("this will free retired pages
  as well"). Harmless at 512 slices per segment; at 16 it decayed 512 B capacity
  from 168 to 8 blocks while 61,440 bytes sat free.
- `generic_collect` was declared with a default and read by nothing, so nothing
  ever collected automatically.
- The generic path reported OOM while still holding empty pages for classes
  nobody had asked for. It now reclaims once and retries before returning null,
  which costs nothing on the happy path. Churn failures fell 22,533 -> 357 per
  50,000 allocations.

Also fixed, from the same campaign: an exclusive arena's huge path could escape
to the OS instead of failing; the arena chunk bitmap scan stopped at word 0, so
an arena over 32 chunks could not allocate past chunk 31; `stats.segments` did
not balance `segments_freed`; and the fixed-region backend placed everything
bottom-up, so one page below a segment boundary cost a whole segment of reach.

Production gates, because none of the above was covered before:

- CI now runs the small-profile suite, clippy on the small profile and no_std,
  and both bare-metal RISC-V targets at both geometries.
- `tools/gate-selftest.sh` reintroduces five real defects and requires the suite
  to go red for each. Four tests in this campaign passed under the exact bug
  they existed to catch; this is what stops the fifth.
- no_std refuses to compile without `--cfg ra_single_threaded`, because three
  things in it are sound only with one thread and all three fail quietly.
- A scheduled `icount` job regenerates the published instruction counts, which
  had aged silently.

The host callgrind figures in the README predate the reclamation fixes and are
marked as a floor pending re-measurement.

BREAKING CHANGE: `default-features = false` now selects the no_std profile. The
crate previously had `default = []`, so `default-features = false` was identical
to the default; it now additionally requires `--cfg ra_single_threaded`. Set
`features = ["std"]` to keep the previous behaviour. Separately, `heap::Heap`
gained a field, and since all its fields are `pub` and it is not
`#[non_exhaustive]`, a struct literal naming every field no longer compiles.
Consumers on default features are unaffected by both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Ttimmahlax
Ttimmahlax force-pushed the feat/no-std-small-metal branch from b156a55 to 20723d0 Compare September 8, 2026 05:27
tim-almond-house and others added 14 commits September 7, 2026 22:44
CI's `no_std` clippy step runs on a unix HOST, and `random::os_entropy` and
`stats::process_info` both selected their unix arm on `cfg(unix)` alone — arms
that reach `/dev/urandom` and `/proc/self/statm` through `std::fs`. Three
E0433s, and only on unix: the local checks used the Windows target, whose arm
goes through `windows_sys` and needs no `std`.

This is the fifth-case defect shape a third time — P0 found it in
`prim/mod.rs`, P3 in `random.rs`, and here it is in both files again, because a
platform selection written when `std` was unconditional gains a new case the
moment `std` becomes a feature. Both unix arms now require `feature = "std"`,
and both fallbacks widen to cover unix-without-`std` alongside bare metal.

Also lists `riscv32imafc-unknown-none-elf` in `rust-toolchain.toml`: the
`embedded` job builds it, and a target the workflow names but the toolchain file
does not is one `rustup` will not have on a fresh machine.

Reproduced locally before the fix with
`cargo check -p rusty_alloc --no-default-features --target x86_64-unknown-linux-gnu`
(3 errors), which is the check that was missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…k on wasm

An integrator reported rusty_alloc adding ~12% to their gzipped wasm bundle.
Measured against the Rust default allocator (dlmalloc) on a minimal consumer
built distribution-shaped (opt-level="z", lto=fat, panic=abort, strip):

                        raw      gzip
  dlmalloc           15,536     6,706
  rusty_alloc        34,285    14,466   +7,760 gz of overhead
  after this         26,105    10,766   +4,060 gz

`options::get` was the LARGEST function in the module at 3,708 bytes — ahead of
anything in the allocator proper. `ensure_init` runs an environment pass that,
on `wasm32-unknown-unknown`, calls a `std::env::var` stub that always fails: 38
iterations, 76 `format!`s, 76 `String` allocations, `to_uppercase` on each of 38
option names, every startup, to read an environment the target does not have.
The `OPTION_NAMES` table and the formatting machinery came along with it.

Gated on `not(all(target_arch = "wasm32", target_os = "unknown"))` — the same
deletion the `no_std` arm already had, for the same reason. `wasm32-wasip1` has
a real environment and keeps the pass.

Also removes the two remaining formatters from `options`: `error` renders its
code into a stack buffer, and `out_fmt` writes bytes rather than `eprint!`-ing
an argument that is already a `&str`. Both measured ~0 on wasm size — the
`core::fmt` in that module turned out to be the consumer's, not ours, which only
the baseline profile could show — but they remove an allocation from an error
path and give `no_std` back the message P3 had to delete. No new `unsafe`:
`from_utf8` validates 30 ASCII bytes on a path that only runs after something
has already gone wrong.

Attribution came from a set difference against a baseline module, not a guess:
rusty_alloc adds 14,659 bytes of functions and removes 8,012 of dlmalloc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An integrator reported ~12% added to their gzipped bundle. Measured against the
Rust default allocator on a minimal consumer, built the way one ships:

                     raw      gzip   overhead
  dlmalloc        15,536     6,706          -
  before          34,285    14,466     +7,760
  after           25,734    10,535     +3,829

Second cause, after the option environment pass: `ra_thread_local!` expanded to
`std::thread_local!` on `wasm32-unknown-unknown`, linking lazy initialisation,
destructor registration and an "accessed during or after destruction" panic path
that can never run on a target `prim/wasm.rs` has assumed single-threaded since
it was written. It now takes the same single-`static` arm `no_std` uses, gated on
`not(target_feature = "atomics")` so a threaded wasm build keeps real TLS.
-371 raw, -231 gzipped, and the self-test's waste gate still passes in a VM.

`tools/wasm-size.sh` is the point of the commit. Nothing measured wasm size, so
a 3,700-byte-gzipped regression sat in the crate for its whole life and reached
a user before it reached us. The gate builds the fixture under `bench-dist`
(`release` keeps debug symbols; a 2 MB artifact hides a 4 KB regression), gzips
it, and fails past 3%. Poisoned by restoring the env pass, it fires.

Three things that were NOT the answer are recorded in docs/plans/wasm-size.md,
because each looked promising and each cost a measurement: `core::fmt` was
mostly the consumer's (the change made on the first reading saved exactly zero);
the data section is +4,124 raw but +535 gzipped and is the sentinel heap that
makes malloc's fast path branchless; and `wasm-opt -Oz` moves raw hard but gzip
barely, because gzip already does most of that work.

Also documents the integrator-facing recipe in the README, including that Rust
embeds absolute panic-location paths -- a published .wasm can carry the
developer's home directory and username -- and that `--remap-path-prefix` is the
stable fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… KiB loses

Two rounds of size work went by before anyone asked the other half of the
question: is rusty_alloc actually faster than the allocator it replaces on wasm?
If not, +3,829 gzipped bytes is indefensible at any size.

Measured in node, one source with `--features ra` swapping the allocator, a
FLOOR arm that allocates nothing, volatile touches folded into a checksum, and
seeded size sequences so both arms provably do identical work:

  churn, 64 live, random 8-512 B    ~71-78 ns  ->  ~10-15 ns   4.9-7.3x FASTER
  2048 B tight alloc/free            ~9-14 ns  ->  ~16-22 ns   0.55-0.83x
  32 B tight, and 64 mixed batched   straddle 1.0 across repeats -- no claim

Only two of four rows are claims. Between-process variance is +/-25%, which
cannot resolve a 10% effect, so the middle rows are reported as ranges rather
than dressed up as ratios.

The floor caught a broken first measurement: 5.2 ns/op for dlmalloc against
0.6 for rusty_alloc, on IDENTICAL code that allocates nothing. V8 tiers wasm up
per code path and only one branch had been warmed. The harness now warms every
branch and warns when the two floors disagree by more than 25%, because a floor
that differs is a harness measuring itself.

Why 2 KiB loses is instrumented, not guessed: exported `stats().generic` and
counted slow-path trips per op -- 0.008 for 32 B, 0.060 for churn, and 1.000 for
2048 B, every single allocation. `alloc.rs` already carries a dated REFUTED
2026-08-21 note for exactly this: a tight alloc/free loop frees into
`local_free`, so the medium bin's `free` list is always dry and the peek can
never hit. The repo had tried the fix and measured it worse. Recorded in
docs/plans/wasm-size.md so the third person to notice the row does not try it
a third time.

`bench/wasm-speed/` vendors the harness so the numbers are re-derivable rather
than asserted, with the floor/checksum/repeat discipline in its README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n on its evidence

Chasing the one row where rusty_alloc loses to dlmalloc on wasm (a 2 KiB tight
alloc/free loop, 0.55-0.83x). `alloc.rs` already documents a REFUTED attempt: a
peek at the medium bin's queue front, which cannot hit because a tight loop
frees onto `local_free` and leaves `free` permanently dry.

Tried the version that CAN hit -- add the collect, not just the peek. It is the
same `local_free` -> `free` swap `malloc_generic_walk` does a few lines later,
cheap in the common case, and guarded on `size <= MEDIUM_OBJ_SIZE_MAX` so it
cannot reproduce the refuted version's `big`/`large` +25 Ir/op.

Measured, both orders agreeing:

  2048 B tight loop    ~7% FASTER
  32 B tight loop      ~3-4% SLOWER
  churn, batched       orders disagree -- noise

REVERTED. 32 B is the commonest allocation there is, 7% does not flip the row it
helps (2 KiB still loses), and the native instruction-count cost is not
measurable from this machine. Recorded in docs/plans/wasm-size.md so the next
person sees the number rather than the idea.

The durable part is the harness. The first two A/B runs produced orderings that
disagreed in SIGN, because the runner measured one module to completion and then
the other, so drift -- another process waking, a thermal step, the scheduler --
landed entirely on one arm. `run.mjs` now interleaves the arms within each
repeat and takes the per-arm minimum, cancelling anything slower than one
repeat. That is what made a 7% effect resolvable, and what turned "the orders
disagree" into a decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o its band

A change with a + and a - outcome is an invitation to find the predicate that
separates them. The previous commit measured a collect-and-retry ahead of the
slow-path heartbeat at +7% on a 2 KiB tight loop and -3 to -4% on 32 B, and
reverted it as a bad trade. The predicate was already in the counters.

2 KiB reaches `malloc_generic` on 1.000 of its calls; 32 B on 0.008. That is not
list state -- which is what the two previous explanations assumed, including the
2026-08-21 refutation's -- it is ROUTING. `alloc::malloc` serves
`size <= SMALL_SIZE_MAX` from the direct table and tail-calls `malloc_slow`,
which goes straight to `malloc_generic`; `Heap::malloc`'s medium branch is on a
different entry point and is never reached through `GlobalAlloc`. Every medium
allocation arrives at the slow path by construction, and small ones almost never
do, which is exactly why the ungated version helped one and taxed the other.

Gated to `size > SMALL_SIZE_MAX && size <= MEDIUM_OBJ_SIZE_MAX`:

                        ungated        band-gated
  2048 B tight loop      +7%             +15%
  32 B tight loop        -3 to -4%       no effect
  churn, batched         noise           no effect

Three passes in each order through the interleaved harness. The win is LARGER
gated than ungated, because the check no longer runs on calls that cannot use
it. The band also excludes the `big`/`large` sizes the 2026-08-21 experiment
regressed by +25 Ir/op, and the small sizes that have their own fast path.

The row still loses: 2 KiB against dlmalloc goes ~0.65x -> ~0.72x. Kept on the
strength of costing nothing measurable elsewhere, not on winning that row.

UNVERIFIED ON NATIVE. The routing is the same on every target so the win should
carry, but instruction counts cannot be taken from this machine -- the scheduled
`icount` job confirms or refutes before a release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit shipped the collect-and-retry with "unverified on native;
the routing is the same so it should carry". That is a prediction, and this repo
does not ship predictions as results.

Measured with a temporary runtime toggle in `malloc_generic_once`, so one
process can time the retry on and off INTERLEAVED -- the same fix that made the
wasm A/B stop disagreeing in sign. Windows x86-64, four runs, best-of-15:

  2048 B tight loop   11.4-12.9 -> 9.6-11.2 ns    1.15-1.19x
  4096 B tight loop   10.4-13.4 -> 8.9-11.5 ns    1.14-1.19x
  32 B tight loop     no effect (0.97-1.05x)
  churn 8-512 B       no effect (1.00-1.04x)

Same shape as wasm and slightly larger, which is what the routing argument
predicted -- but it is worth more now that it is a number.

The toggle and its harness are removed again: a runtime branch in
`malloc_generic` to support an A/B is not something to ship. The method is
recorded instead.

Two things this still does not establish, both written down rather than glossed:
the repo's published figures are INSTRUCTION counts under callgrind and this is
wall-clock on one box; and it is single-threaded, so the `Acquire` load
`page_collect` performs on `xthread_free` -- a line other threads push to --
has had no contention measured on it by either wasm or this benchmark.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ad frees

The last two commits shipped a band-gated collect-and-retry ahead of the
slow-path heartbeat, measured at +15% on wasm and +15-19% on native for a 2 KiB
tight alloc/free loop, and closed with an explicit caveat: every one of those
numbers was single-threaded, and `page_collect` loads `xthread_free` -- the line
a remote `free` pushes onto. So that was measured.

A producer allocates a batch of 2 KiB blocks; a consumer thread frees THAT batch
while the producer is timed allocating another. Remote frees land on the
producer's own pages, and nothing in the timed loop synchronises with anything.

  producer alloc, consumer freeing its pages   ~51-75 -> ~71-77 ns   0.67-1.00x
  same, single-threaded control                no effect

Never better, usually 15-33% worse, across two harnesses and eight runs.
Reverted. Cross-thread frees are the producer/consumer shape -- thread pools,
channels, async runtimes -- and mimalloc's architecture exists to serve them. A
single-threaded tight-loop gain does not buy that.

THE OBVIOUS FIX DOES NOT WORK, which is the part worth keeping. Doing only the
LOCAL half of the collect -- swap `local_free` into `free`, touch no atomic --
measured WORSE (0.67-0.77x). `free`, `local_free` and `xthread_free` are
adjacent fields of a `#[repr(C)]` Page and share a cache line, so reading any of
them pulls the line the remote thread is invalidating. There is no cheap peek at
a page another core is freeing into. That is the mechanism the 2026-08-21 note
observed without naming, now named.

Recorded in docs/plans/wasm-size.md with the numbers, the cache-line mechanism,
the sticky per-heap predicate a next attempt should use instead, and why the
first contention harness (an SPSC ring, ~113 ns/op) was timing itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The revert two commits ago was right about the evidence and wrong about the
conclusion. A + and a - outcome is an invitation to find the predicate, and the
predicate here is not "is this program single-threaded" -- it is "does THIS heap
receive cross-thread frees", which is finer and more useful: a worker that owns
its allocations keeps the win however many threads the process has.

`page_collect` now reports whether it stole a cross-thread chain, and any steal
this heap observes latches a sticky `saw_remote_free`. The retry runs only while
that is clear.

  tight 2 KiB loop, 1 thread                    +14-16%  (1.08-1.23, 7 runs)
  tight 2 KiB loop, 2 threads, own frees        +12-26%
  producer alloc / consumer freeing its pages   neutral  (0.84-1.15, was 0.67-0.96)

The contended row straddles 1.0 with wide variance, so the claim is that the
regression is gone, not that contention got faster.

Two things this needed that the first attempt did not have. Latching on the
RETRY's own steal was not enough: the frees that hurt land on other pages of the
same heap, so the retried page never sees them and it never switched off -- the
latch is set from `malloc_generic_walk`'s collects too. And the harness had to
be told about the latch, because sticky is correct in production and useless in
an A/B: the contended case ran first in warmup and left the retry off for
everything after it, so the win read as 1.00x until the latch was reset between
timed runs.

Still unmeasured: host instruction counts under callgrind, and more than one
consumer thread. Recorded in docs/plans/wasm-size.md with the numbers, the
cache-line mechanism (`free`, `local_free` and `xthread_free` are adjacent in a
`#[repr(C)]` Page, so no variant of the peek is cheap under contention), and why
the local-only collect measured worse rather than better.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two anchors in the doc script used `--` where the file has an em-dash, so both
`assert`s failed, nothing was written, and the commit landed the self-disabling
retry while `docs/plans/wasm-size.md` still said the change was reverted and the
per-heap predicate was "not tried". The CHANGELOG had no entry for it at all.

Fixed line-based rather than by exact string match, which is what the dash
mismatch argued for. The doc now carries the predicate section, the seven-run
numbers, why latching on the retry's own steal was not enough, and why the
harness had to reset the latch between arms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It had been measured on native Windows and reasoned about everywhere else. So
it was run everywhere else, and the same one workload shape moves on all three
targets while nothing else moves anywhere.

  native x86-64 (Windows)                2 KiB tight loop  +14-16%
  wasm32 in V8 (node)                    2 KiB tight loop  +1-16%
  Xtensa ESP32-S3, no_std, small profile 2 KiB tight loop  +15% (1380 -> 1200 ns)

The board is the cleanest of the three. Its harness reports 0-1% spread, and at
the small profile `SMALL_SIZE_MAX` is 512 B, so its other three rows (32 B, a
mixed 8-512 B batch, churn over 8-511 B) sit BELOW the band and the retry
provably cannot touch them -- their 1-2% movement is layout, not effect.

"A win across the board" is the wrong phrase and the doc now says so: it is a
win on ONE workload shape that happens to hold on every target. Everything else
is unchanged and the contended case is neutral, not better.

Correctness on the same build: 109/33 default, 90/19 small profile, clippy on
three configs, riscv32imac and imafc at both geometries, rusty_alloc-api no_std,
the wasm waste gate inside a real VM, and on hardware both the 68 KiB footprint
kill test (used 69,632, PEAK 4,914, unchanged) and the eight-test stress battery
(capacity flat at 240, churn nulls 458 against a 334-575 range already seen).
Three ratchets green: unsafe census, gate selftest 5/5, wasm size.

Still not covered: host instruction counts under callgrind -- the instrument the
README's figures use, which no Windows box can run -- and contention with more
than one consumer thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fter 2.0.0

A major version is a promise consumers can keep compiling, and nothing inside
this repo can check it. 2.0.0 redefines `default-features = false` to mean
`no_std` and adds a field to `heap::Heap`; both are invisible here and land on
other people.

`tools/corpus/` builds each registered consumer twice: BASELINE as it sits on
disk with its pinned crates.io version, and CANDIDATE as a copy with every
`rusty_alloc*` requirement rewritten to this tree. `[patch.crates-io]` cannot do
this -- a patch must satisfy the original requirement and `=1.1.6` is not
satisfied by 2.0.0 -- so the upgrade is simulated by rewriting, in a COPY. The
script never writes inside a consumer's checkout.

First run:

  spacedb-sdk                PASS
  spacedb-sdk (secure)       PASS
  rusty_alloc_default        PASS
  rusty_zstd                 PASS
  rusty_maplibre             FAIL -- broken by 2.0.0

`rusty_maplibre` takes both `rusty_alloc` and `rusty_alloc-api` with
`default-features = false`, stops at the `compile_error!` 2.0.0 added, and is
fixed by one line per dependency -- verified by applying it to the copy and
watching the error go, not assumed. The CHANGELOG now carries that migration
with the corpus result behind it.

Four things the harness got wrong before it got anything right, all in its
README because a corpus that lies gets muted: tab is IFS whitespace, so an empty
`features` column shifted every field after it and fed each consumer's NOTE to
`--features` (all five baselines went red and it blamed the world); a missing
workspace root is a SKIP, not a red build, and for SpaceDB it is reconstructable
because those crates inherit only dependencies -- the harness synthesises one in
the copy, which is what made SpaceDB testable at all; a candidate whose error
never mentions rusty_alloc is UNRELATED, not FAIL; and a run that hits
`fork: retry: Resource temporarily unavailable` misclassifies, so an affected
consumer must be re-run alone before its row is believed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The corpus commit's own doc edit missed its anchor (the line wraps differently
than the script assumed) and the CHANGELOG shipped without the result. The
breaking-change entry now says which consumers were actually built against
2.0.0, which one it breaks, and that the one-line migration was tested by
applying it rather than reasoned about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The published table had a fresh rusty_alloc arm against an esp-alloc arm from
an earlier session, and the medium-band collect-and-retry had since moved the
2 KiB row. A table with one stale arm is not a measurement.

Both re-run in one session, same 162 ns/op floor in each, checksums matching:

                        esp-alloc   rusty_alloc   speedup
  32 B alloc/free            1638           647     2.53x
  64 mixed, batched          1792           881     2.03x
  churn 64 live              3987          1087     3.67x
  2048 B alloc/free          1638          1200     1.37x   (was 1.19x)

Headline moves 2.1-3.7x -> 2.0-3.7x, and the 2 KiB row improves because the
retry landed. Both READMEs updated.

Also demotes a `///` on the `no_std` `compile_error!` to a plain comment.
A doc comment on a macro INVOCATION documents nothing and warns
(`unused_doc_comments`); it only fires in the failing no_std build, which is why
every green gate walked past it. Found by building the board's esp-alloc arm
without `--cfg ra_single_threaded`.

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