Skip to content

Snapshot support for embedders - #13

Open
Jarred-Sumner wants to merge 2 commits into
bun-dev3-v2from
claude/heap-image
Open

Snapshot support for embedders#13
Jarred-Sumner wants to merge 2 commits into
bun-dev3-v2from
claude/heap-image

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Allocator side of Bun's heap images (an embedder freezes a process's heap into a file and maps it into fresh processes). 240 lines; everything is behind new entry points or embedder-supplied defines, and the malloc/free fast paths are unchanged. Full rationale in the second commit's message.

Piece What Cost when unused
MI_HEAP_IMAGE_HOST_FN / MI_HEAP_IMAGE_BUILD_ENV, mi_os_hint_floor() Deterministic address hints in image-capable executables; a process that may map an image keeps its early heap above the image area none unless the define is set (Bun sets it; the predicate is one call per OS allocation)
MI_THREADID_FROZEN, mi_arenas_freeze_pages() Builder marks every page before writing it out; frees of image blocks then land on the existing cross-thread slow path and are dropped there; collects/sweeps skip them; abandoned maps no longer list them one compare on the cross-thread free path
mi_arenas_seal_existing(), mi_theap_freeze(), mi_arenas_visit_free_ranges() Restore-side: no allocation into image arenas, imaged main theap left alone, builder can omit free space none
mi_malloc_zone_process_owned_ranges() (macOS) The zone structs libsystem registered for this process must survive the image overlay none
Hole-sweep state moved from __thread variables onto the swept tld Standalone fix: under the macOS zone override, a new thread's first malloc that collected a purged page read a TLV, dyld allocated the TLV block with malloc, and the allocator re-entered itself without bound. The guard now reads the page's own tld none (the guard was already only reached for purged pages)
prof.c: mi_prof_reinit_lock, mi_prof_visit_live, mi_prof_lock_is_free Profiler lock is nobody's after fork/restore; live-sample visitor for attribution tooling none

Exercised by test/js/bun/heap-image on the Bun side (oven-sh/bun branch claude/lowmem-cc) and by an application that has been running from an image for a day. Companion: oven-sh/WebKit#397 (JSC side). Draft until the Bun side is up for review.

Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Aug 8, 2026
…les on this branch — the branch now builds from the download exactly like main does
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Aug 9, 2026
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/heap-image branch 2 times, most recently from aae392c to 7841fec Compare August 9, 2026 05:20
@Jarred-Sumner Jarred-Sumner changed the title Heap-image support for embedders Snapshot support for embedders Aug 9, 2026
…live samples; size the location table to the sample count

The thread that held the profiler lock in the parent (or in the process that built a snapshot) does not
exist in the child, so the lock is re-initialized there. mi_prof_visit_live() lets an embedder walk the
live sampled allocations, and mi_prof_lock_is_free() lets it assert the profiler is quiescent before
freezing memory. The location table used while aggregating grows with the sample count instead of being
fixed at 4096 entries.
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/heap-image branch 4 times, most recently from 018ddb2 to f593ef8 Compare August 9, 2026 10:22
An embedder can freeze the current heap into a snapshot and map it into later processes. This adds what
the allocator needs for that: pages captured in a snapshot get the thread id MI_THREADID_FROZEN, so frees
of their blocks are dropped on the (already slow) cross-thread path and collection, purging and the idle
hole sweep leave them alone; mi_theap_freeze() marks the frozen theaps; mi_arenas_seal_existing() stops
new allocation from landing in frozen arenas; mi_theap_adopt_current_thread() lets the resuming thread
take over the frozen main-thread state (otherwise every page it allocates carries a thread id nobody has
and all of its frees go cross-thread); mi_heap_snapshot_hints_enabled() exports the deterministic-placement
decision, made once, so the embedder's own reservations follow the same rule. Placement is deterministic
when the host executable says it can carry a snapshot (MI_HEAP_SNAPSHOT_HOST_FN) or
MIMALLOC_DETERMINISTIC_HINT is set; a capable process that is not taking a snapshot keeps its early
allocations above MI_HEAP_SNAPSHOT_RESTORER_FLOOR so they never sit where the snapshot will be mapped.

The hole-sweep bookkeeping moves from __thread variables onto the tld: on macOS the first touch of a
thread-local from inside the allocator makes dyld allocate the TLV block with malloc, which re-enters the
allocator before the variable exists.

Nothing here runs in a process that neither takes nor maps a snapshot, apart from the frozen-id compare
on the cross-thread free path.
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