Skip to content

perf(cli): use mimalloc as the global allocator - #1355

Merged
gregmagolan merged 1 commit into
mainfrom
mimalloc-global
Jul 28, 2026
Merged

perf(cli): use mimalloc as the global allocator#1355
gregmagolan merged 1 commit into
mainfrom
mimalloc-global

Conversation

@gregmagolan

@gregmagolan gregmagolan commented Jul 28, 2026

Copy link
Copy Markdown
Member

Sets mimalloc as the global allocator for aspect-cli.

The Linux release binaries are static-musl. musl's mallocng serializes every allocation in the process behind a single global lock, and is substantially slower than mimalloc under this CLI's allocation patterns — parsing tens of thousands of build events, building large Starlark heaps, streaming lint outputs — with the BES, sink, and file-probe threads all contending with the Starlark thread on every malloc/free. mimalloc keeps per-thread free lists, which removes that contention.

It builds through the crate universe for every release target with no annotation — the same path ring's C/asm already takes — so no gen_build_script workaround or BCR dependency is needed. Binary grows ~2MB (51MB → 53MB on x86_64 musl).


Changes are visible to end-users: yes

  • Searched for relevant documentation and updated as needed: no (no user-facing allocator behavior is documented)

  • Breaking change (forces users to change their own code or config): no

  • Suggested release notes appear below: yes

  • perf: aspect-cli now uses mimalloc as its allocator, removing global allocator-lock contention on Linux (static-musl) builds where the BES, sink, and file-read threads previously serialized against the Starlark thread on every allocation.

Test plan

  • Covered by existing test cases (full AXL suite: 910 passing; crate tests).
  • Manual: the x86_64-unknown-linux-musl and aarch64-apple-darwin release targets both build via --config=release; the musl binary links mimalloc (21 symbol matches), runs (aspect 2026.31.6), and the fatal-signal crash handler still reports correctly under the new allocator.

Note for the ongoing lint-segfault investigation

Landing this also acts as a diagnostic. That crash is musl's mallocng aborting from a_crash() inside enframe — its check that the byte preceding a slot's user pointer is clear — which means something wrote out of bounds. Auditing has not found the writer, and ASan is unavailable on this target (sanitizer is incompatible with statically linked libc), so swapping allocators is the available experiment:

  • crash persists → confirms an out-of-bounds write in our own code, and rules the allocator out;
  • crash disappears → the fault depends on mallocng's specific heap layout/timing, which would most likely mean a latent bug being masked rather than fixed.

There is no known mallocng defect matching this signature. The one recent mallocng correctness fix (musl bf96b52a, 2026-05-12) traps in get_meta on free/realloc of allocations with gigabyte-scale alignment — a different function, a different assert, and alignments nothing here requests — so it is not a candidate explanation.

The Linux release binaries are static-musl. musl's mallocng serializes
every allocation in the process behind one global lock and is
substantially slower than mimalloc under this CLI's allocation patterns —
parsing tens of thousands of build events, building large Starlark heaps,
and streaming lint outputs — with the BES, sink, and file-probe threads
all contending with the Starlark thread.

mimalloc keeps per-thread free lists, removing that contention. It builds
through the crate universe for every release target with no annotation
(the same path ring's C/asm already takes), adds ~2MB, and needs no
build-script workaround.

This also settles an open question on the intermittent lint segfault: the
crash is musl mallocng detecting corrupted heap metadata (a_crash in
enframe). Running on a different allocator tells us whether the fault
tracks the allocator or our own writes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aspect-workflows

aspect-workflows Bot commented Jul 28, 2026

Copy link
Copy Markdown

✨ Aspect Workflows Tasks

📅 Tue Jul 28 23:29:47 UTC 2026

❌ 1 failed task

  • ❌ delivery-uncacheable [delivery] · ⏱ 36s · 🐙 GitHub Actions
    💬 failed in deliver · Delivery failed (1 delivery fail)

⚠️ 3 flagged tasks

  • ⚠️ delivery-gha-debug [delivery] · ⏱ 51.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Delivery complete (1 delivered · 2 warn · 3 skipped)
  • ⚠️ delivery-gha [delivery] · ⏱ 53.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Delivery complete (1 delivered · 2 warn · 3 skipped)
  • ⚠️ delivery-uncacheable-warn [delivery] · ⏱ 12.3s · 🐙 GitHub Actions
    💬 Delivery complete (1 warn)

✅ 27 successful tasks

  • ✅ axl-smoke-gha-bootstrap [build] · ⏱ 1m 12s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (1 built)
  • ✅ run-axl-smoke [run] · ⏱ 24.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Ran //examples/deliverable:py_deliverable
  • ✅ run-axl-smoke-2 [run] · ⏱ 14.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Ran //examples/deliverable:sh_deliverable
  • ✅ axl-tests-gha-bootstrap [build] · ⏱ 1m 14s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (1 built)
  • ✅ build-gha-debug [build] · ⏱ 13m 14s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (166 built)
  • ✅ build-gha [build] · ⏱ 14m 53s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (166 built)
  • ✅ build-gha-ephemeral [build] · ⏱ 47.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (9 built)
  • ✅ buildifier-gha-debug [buildifier] · ⏱ 1m · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ buildifier-gha [buildifier] · ⏱ 46.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-gha-debug [format] · ⏱ 1m 32s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task [format] · ⏱ 1m 24s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-2 [format] · ⏱ 10.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-3 [format] · ⏱ 9.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-4 [format] · ⏱ 10s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-gha [format] · ⏱ 1m · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ gazelle-gha-debug [gazelle] · ⏱ 1m 27s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-from-source-gha-debug [gazelle] · ⏱ 2m 7s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-from-source-gha [gazelle] · ⏱ 1m 33s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-gha [gazelle] · ⏱ 36.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ init-shell [build] · ⏱ 1m 18s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (10 built)
  • ✅ lint-gha-debug [lint] · ⏱ 40.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Lint complete (clean)
  • ✅ lint-gha [lint] · ⏱ 40.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Lint complete (clean)
  • ✅ test-gha-debug [test] · ⏱ 15m 1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (26/26 passed · 25 cached)
  • ✅ test-gha-coverage [test] · ⏱ 31.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-gha-target-pattern-file [test] · ⏱ 25.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-gha [test] · ⏱ 6m 41s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (26/26 passed · 26 cached)
  • ✅ test-gha-ephemeral [test] · ⏱ 43.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)

🔁 Reproduce

❌ delivery (delivery-uncacheable · delivery-gha-debug · delivery-gha · delivery-uncacheable-warn)

# --mode=always --track-state=false for off-runner with no state backend.
aspect delivery \
  --commit-sha=ebd633c6b821f39e7471147bd0dd626a605c4bb2 \
  --mode=always \
  --track-state=false \
  --dry-run=true

Install aspect: aspect.build/docs/cli/install


⏱ Last updated Tue Jul 28 23:46:30 UTC 2026 · 📊 GitHub API quota 2,275/15,000 (15% used, resets in 10m)
🚀 Powered by Aspect CLI (v0.0.0-dev)  |  Aspect Build · X · LinkedIn · YouTube

@gregmagolan
gregmagolan merged commit 166a057 into main Jul 28, 2026
71 checks passed
@gregmagolan
gregmagolan deleted the mimalloc-global branch July 28, 2026 23:50
gregmagolan added a commit that referenced this pull request Jul 29, 2026
Follow-up to #1355 (mimalloc). Builds mimalloc with its `secure` feature
and makes detected heap corruption fatal.

Moving off musl's mallocng loses a diagnostic property we were relying
on. mallocng validates a check byte on every allocation, so heap
corruption aborted the process by itself — that is exactly what produced
the `a_crash`/`enframe` abort used to identify the intermittent lint
segfault as heap corruption in the first place. A default mimalloc build
performs no equivalent check, so the same corruption would now pass
unnoticed and resurface later as an unrelated failure.

**Secure build.** `MI_SECURE=4` gives guard pages around metadata,
encoded free lists, randomized placement, and double-free detection.
Verified present in the built `x86_64-unknown-linux-musl` release
binary: `corrupted free list entry of size %zub at %p`, `double free
detected of block %p with size %zu`, and `corrupted meta-data in
thread-free list`.

**Abort on detection.** Detection alone is not enough: mimalloc's
default handler aborts only on `EFAULT` (corrupted metadata, corrupted
thread-free list, and — under `secure` — a detected buffer overflow),
while a double free (`EAGAIN`) or a free of an invalid pointer
(`EINVAL`) is reported and then execution *continues*. Continuing on a
corrupted heap is what makes this class of bug so hard to trace: the
eventual crash lands somewhere unrelated, long after the write that
caused it. This registers an error handler via `mi_register_error` that
aborts on every corruption code, so a detection is fatal where it
happens. The SIGABRT is caught by the crash handler, which reports it
with a resolvable address.

Setting `MIMALLOC_SHOW_ERRORS=1` on a diagnostic run additionally prints
the message saying *what* was detected (`show_errors` defaults off in a
release build). The abort happens either way.

---

### Changes are visible to end-users: yes

- Searched for relevant documentation and updated as needed: no
(behavior is documented on the allocator item in `main.rs`)
- Breaking change (forces users to change their own code or config): no
- Suggested release notes appear below: yes

- chore: the allocator is now built in secure mode and aborts when it
detects heap corruption (a smashed free list, a double free, corrupted
metadata) rather than continuing on a corrupted heap. Set
`MIMALLOC_SHOW_ERRORS=1` to also print what was detected.

### Test plan

- New test case added: an end-to-end test drives a deliberate double
free through the internal crash-test hook and asserts the allocator
reports it (`double free detected`) and the process dies by SIGABRT with
a crash report — the path that was previously silent.
- Covered by existing test cases (full AXL suite: 910 passing;
crash-handler suite: 6 passing).
- Manual: the `x86_64-unknown-linux-musl` release target builds via
`--config=release`; `mi_register_error` and all three secure detectors
are linked into the binary, and it runs normally (`aspect --version`).

### Note on cost

`MI_SECURE=4` adds guard pages and free-list encoding, which is not
free. If the perf hit is material we can land this as a diagnostic-only
release and revert, the same way #1352 was scoped.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant