Skip to content

profile: MI_PROFILE=1 without a profiler costs what MI_PROFILE=0 costs (but for 4 instructions in the generic path); mi_profiler_start reaches the running threads - #44

Merged
Jarred-Sumner merged 1 commit into
bun-dev3-v2from
claude/profile-unattached-cost
Sep 14, 2026
Merged

Jarred-Sumner merged 1 commit into
bun-dev3-v2from
claude/profile-unattached-cost

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Where MI_PROFILE=1 costs something without a profiler

Bun's flags (C++20, -O3 -march=nehalem, MI_FREE_USE_PAGEMAP=1), clang 21, a malloc/free micro-benchmark (50 M iterations over 256 slots, 16 to 1039 bytes, 25 M mi_malloc of which 1.23 M reach _mi_malloc_generic), user instructions:

MI_PROFILE=0 MI_PROFILE=1, no profiler
bun-dev3-v2 2.25417 G 2.28706 G (+1.46%)
this branch 2.25419 G 2.25916 G (+0.22%)

mi_malloc and mi_free compile to the same instructions with and without MI_PROFILE (only the address of __mi_page_map differs), and callgrind counts the same for them and for main: 421,582,951 and 576,011,818 in both builds. All of the 32.9 M instructions are in the generic path:

  • mi_page_update_sample_countdown: 25.3 M. It runs when a free list is refilled (898,081 times, 28 instructions): find the theap of the page, leave if sample_rate == 0, move last_alloc up.
  • _mi_malloc_generic: 7.6 M, 6 instructions per call: the check of the countdown before, and of sample_rate and the countdown after mi_page_queue_find_free.

What a theap that does not sample runs now

  • mi_page_update_sample_countdown is what it is with MI_SAMPLE==0: the reset when alloc_count gets to 0x8000. The blocks that the fast path took from a page are charged to the sample countdown by mi_theap_count_page_allocs, which only a theap that samples calls: for the first page of the size class before mi_page_queue_find_free refills it (mi_theap_refill_is_sample, out of line), and as before in mi_theap_page_update_stats. (At the end of mi_malloc_generic_fallback every theap calls it: that is where the block of a sample comes from, with sample_rate set aside by _mi_malloc_generic_no_sample; last_alloc has to move up there or the block is counted a second time.) If that uses up the countdown, or it was used up already, the allocation in progress goes to mi_malloc_generic_fallback, which takes the sample as it did (or returns NULL for MI_MALLOC_GENERIC_BLOCK_START).
  • In _mi_malloc_generic that is one test, theap->sample_rate != 0, before mi_page_queue_find_free.
  • A theap that does not sample does not move last_alloc up either. When its rate goes from 0 to something (_mi_theap_update_sample_rate), _mi_theap_sync_sample_counts sets last_alloc = alloc_count on its pages: what they handed out before is not for the first period.
  • Its countdown is MI_SAMPLE_COUNTDOWN_MAX (_mi_theap_init, and _mi_theap_update_sample_rate when the rate goes to 0), not -1 or 0: nothing in the generic path puts it right any more, and alloc-aligned.c asks mi_theap_should_sample before its naturally aligned path.

The 4 instructions per generic call that are left: that test (2), the limit below (1, clang does not fold the relaxed atomic load into the compare), and one that the compiler adds around them. Without any: a theap that samples has a limit of 0 and takes another path than the fast one of _mi_malloc_generic, which then has no code for sampling at all (+0.056%, the load of the limit). That path runs the head of _mi_malloc_generic for nothing, and an attached profiler cost 2% more in an allocation benchmark of Bun: not taken.

mi_profiler_start reaches the threads that are running

A theap looked at its profiler every 1000 generic allocations (mi_malloc_generic_admin): with small blocks that is many megabytes after mi_profiler_start. Now:

  • _mi_malloc_generic takes its fast path while ++generic_count < theap->generic_fast_limit (a field, MI_GENERIC_FAST_LIMIT = 1000, instead of the constant).
  • mi_profiler_start stores -1 there for the theaps (not the detached ones, which hold meta data) of the heaps that have the profiler, in the current and the main sub-process, under heaps_lock and theaps_lock. Their next generic allocation goes to mi_malloc_generic_fallback, which calls _mi_theap_update_profiling (the code that was in mi_malloc_generic_admin, which calls it too). That sets the limit to 0 while it looks and back to 1000 with a compare-and-swap: a request that comes in while it looks stays.
  • A theap that is made while mi_profiler_start runs leaves the request with itself if the profiler is enabled once it is on the list of its heap and its own look did not see that.
  • It starts with a full period for the profiler as well (profile_sample_countdown), as _mi_theap_init does: with guarded sampling on, the first guarded sample point was a profiler sample for a period that nobody requested.
  • _mi_theap_update_profiling does not start a detached theap: mi_process_theap_meta is on the list of the main heap, and mi_malloc_generic_admin would have started to sample it after 1000 generic allocations ("no sampling for meta data", init.c).
  • Inside _mi_malloc_generic_no_sample (which sets sample_rate aside and puts it back) a start or stop is put off to the next generic allocation (it leaves the request for it: when every allocation is a sample, mi_malloc_generic_admin can get there inside of it every time, and a mi_profiler_stop was never seen). _mi_theap_malloc_profiled looks as well when it finds the profiler stopped. _mi_malloc_generic_no_sample on a theap that does not sample sets nothing aside and just allocates.
  • The meta-data theaps get a countdown of MI_SAMPLE_COUNTDOWN_MAX instead of 0.

With a profiler attached

A running profiler costs a bit more than before. Same micro-benchmark with a profiler at 512 KiB (26,900 samples): 2.87383 G before, 2.88457 G (+0.37%). An allocation benchmark of Bun (3 M ArrayBuffers, JIT on, user instructions, average of 5): without a profile 6.0253 G to 6.0138 G; with one at 512 KiB 6.1229 G to 6.1653 G, which is 2.5% over a run without a profile where it was 1.6%. That is paid only while a profile runs; what is saved is paid by everyone all the time. Sampled over requested bytes for allocations of 12,000 to 200,000 bytes at 128 KiB: 1.021, 1.022, 1.098, 1.012, 1.000, as before. test-profile prints the same figures as before within a few percent (release: small share 0.585 to 0.620 for 0.699, 511 to 493 samples of aligned allocations for 553).

Tests

test_profiler_start_with_running_threads: four threads allocate (250,000 mi_malloc each, 16 to 1039 bytes, 256 slots), a fifth starts a profiler at 256 KiB; each thread gets its first sample after 264 K to 333 K bytes of what it requests from then on (bounds: half a period to two). Without the request: none within 16 periods on two threads, 3 and 4 MiB on the others. Without _mi_theap_sync_sample_counts: after 546 to 14,439 bytes.

test_profiler_aligned_late_start now gets all 3000 samples (2002 before: the first 998 allocations were before the theap looked).

ctest with the flags and environment of .github/workflows/test.yaml: Release, Debug MI_DEBUG=FULL, Secure, Debug and Release as C++, ASAN, UBSAN, TSAN, Debug and Release MI_GUARDED=FULL, Debug and Release MI_FREE_USE_PAGEMAP=ON, Debug and Release MI_SECURE=FULL, TLS pthreads, SIMD, Release MI_PROFILE=OFF, Debug MI_DEBUG=FULL with MI_GUARDED=OFF (coarse sampling with asserts).

Seen on the way, not changed

test_profiler_start_with_running_threads fails when it is run by hand with MIMALLOC_GUARDED_SAMPLE_RATE below its rate in a MI_GUARDED build (ctest sets 0 for test-profile): the profiler gets its samples early there, before and after this change (_mi_theap_malloc_sampled takes all bytes since the last profiler sample off profile_sample_countdown at every guarded sample point, see the notes of #43).

The block of a sample below MI_SMALL_MAX_OBJ_SIZE is counted a second time when its page is next counted (it comes from the fast path of _mi_malloc_generic inside _mi_malloc_generic_no_sample, where last_alloc does not move): 3% for allocations of 2000 bytes at 128 KiB, as before.

… generic path, and little

mi_malloc and mi_free compile to the same code with MI_PROFILE=0 and 1.
What MI_PROFILE=1 cost without a profiler (1.46% of the instructions of
a malloc/free micro-benchmark) was all in the generic path: the count of
the blocks that a page handed out, at every refill of a free list (28
instructions to find out that the theap does not sample), and the checks
of the sample countdown in _mi_malloc_generic.

The refill code is that of MI_SAMPLE==0 again. A theap that samples
counts the first page of a size class before it refills it, from
_mi_malloc_generic behind one test of sample_rate. A theap that does not
sample counts nothing; when it starts to, last_alloc is brought up to
alloc_count on its pages so that nothing from before is counted.
0.22% is left (4 instructions per call of _mi_malloc_generic).

mi_profiler_start now asks the theaps of the running threads to look at
the profiler at their next generic allocation, through the limit of
generic_count, which is a field of the theap now: they did so only every
1000 generic allocations, which is many megabytes of small blocks.
@Jarred-Sumner
Jarred-Sumner merged commit 9902215 into bun-dev3-v2 Sep 14, 2026
23 checks passed
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Sep 14, 2026
Pins 58460467413a, the merge of oven-sh/mimalloc#45 on top of #44, on top of #40, #43 and #42.

#44: with the profiler hooks compiled in (MI_PROFILE=1, the default) and no profiler attached, a thread paid for them in the
shared page collect and extend code. A theap that does not sample now pays in the generic allocation path only, and little:
hooks compiled in cost +0.2% of instructions on the allocator micro benchmark where they cost +1.5%. mi_profiler_start reaches
the threads that are already running through theap->generic_fast_limit, at their next allocation that takes the generic path.
#45 drops a debug assertion that raced with mi_profiler_stop.

25 M malloc + 25 M free with bun's flags: 2.2403 G instructions with main's pin, 2.2384 G with the previous commit, 2.2073 G now
(-1.5% against main's pin). No header that bun includes changes.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Sep 15, 2026
Pins 58460467413a, the merge of oven-sh/mimalloc#45 on top of #44, on top of #40, #43 and #42.

#44: with the profiler hooks compiled in (MI_PROFILE=1, the default) and no profiler attached, a thread paid for them in the
shared page collect and extend code. A theap that does not sample now pays in the generic allocation path only, and little:
hooks compiled in cost +0.2% of instructions on the allocator micro benchmark where they cost +1.5%. mi_profiler_start reaches
the threads that are already running through theap->generic_fast_limit, at their next allocation that takes the generic path.
#45 drops a debug assertion that raced with mi_profiler_stop.

25 M malloc + 25 M free with bun's flags: 2.2403 G instructions with main's pin, 2.2384 G with the previous commit, 2.2073 G now
(-1.5% against main's pin). No header that bun includes changes.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Sep 15, 2026
Pins 58460467413a, the merge of oven-sh/mimalloc#45 on top of #44, on top of #40, #43 and #42.

#44: with the profiler hooks compiled in (MI_PROFILE=1, the default) and no profiler attached, a thread paid for them in the
shared page collect and extend code. A theap that does not sample now pays in the generic allocation path only, and little:
hooks compiled in cost +0.2% of instructions on the allocator micro benchmark where they cost +1.5%. mi_profiler_start reaches
the threads that are already running through theap->generic_fast_limit, at their next allocation that takes the generic path.
#45 drops a debug assertion that raced with mi_profiler_stop.

25 M malloc + 25 M free with bun's flags: 2.2403 G instructions with main's pin, 2.2384 G with the previous commit, 2.2073 G now
(-1.5% against main's pin). No header that bun includes changes.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Sep 15, 2026
, #44, #45, #46) (#42668)

### What does this PR do?

Moves `MIMALLOC_COMMIT` from `ab13501334a8` to
`69050cf3ec14be4847ca3ba6c840da821fa283b0`, the head of `bun-dev3-v2`:
main's pin plus six merged PRs of the fork and nothing else, and changes
the two callers in bun that the last of them asks to change.
`MI_FREE_USE_PAGEMAP=1` stays.

- oven-sh/mimalloc#40: the sampled profiler hooks (`mi_profiler_t`)
attribute a sample to the small allocation that refilled its page, keep
`bytes_since_last_sample` right when `on_alloc` raises the rate, sample
a new thread's heap from its first allocation, report the frees of
`mi_heap_destroy`, and give `on_free` of an aligned block the pointer
that `on_alloc` got. What the heap profiler in #42626 needs.
- oven-sh/mimalloc#43: an aligned request only becomes a sample through
the over-allocating path (with #40 alone `mi_malloc_aligned(64, 64)`
could free a sampled block inside the allocator; `MI_DEBUG` asserted).
- oven-sh/mimalloc#42: the purge pull-forward of an idle thread
(`mi_on_thread_idle`) goes through compare-and-swap, so it cannot undo
the reset that a purge pass does first thing. 12 lines in `src/arena.c`.
- oven-sh/mimalloc#44: a thread that does not sample pays for the
profiler hooks in the generic path only: +0.2% of instructions on the
allocator micro benchmark where they cost +1.5%.
- oven-sh/mimalloc#45: a debug assertion that raced with
`mi_profiler_stop` is gone.
- oven-sh/mimalloc#46: **free blocks in the 4 MiB pages (blocks of 96 to
512 KiB) are handed back to the OS.** Hole purging skipped these pages
(their 1024 OS pages do not fit the 256-bit purge bitmap), so what a
process freed in that range stayed resident for as long as one block of
the page was alive: 97% of it by `mincore`. The bitmap's unit is now per
page (16 KiB for a 4 MiB page), and the free blocks of a large page stay
while the page is in use, which the allocator tells without a clock on
the allocating side: the idle sweep counts epochs of 100 ms or more, an
allocation from a large page leaves the current epoch in the page (a
load, a compare, and a store once per epoch), and the blocks go when
that is two epochs ago. Without that hold a `Bun.serve` with 256 KiB
bodies discarded and refaulted 3 GB in 20 s.

Nothing in bun attaches a profiler yet. The fast paths of `mi_malloc`
and `mi_free` are untouched by all six.

`include/mimalloc.h` gains one function, `mi_on_thread_idle_pending()`,
and comments; `mimalloc-stats.h` is identical, `mimalloc-profile.h`
differs in comments, `mimalloc/types.h` gains fields of the internal
`mi_theap_t` and `mi_tld_t`. No struct or enum that bun binds changed
(`src/mimalloc_sys/mimalloc.rs`, the declarations in
`BunJSCEventLoop.cpp` and `epoll_kqueue.c`).

#### The callers that sweep once and then block

With oven-sh/mimalloc#46 ONE call of `mi_on_thread_idle()` never takes
the free blocks of a large page that the thread allocated from since its
previous sweep: a sweep ends one epoch at the most. The event loop is
fine: `us_loop_run_bun_tick` hands the thread's heaps to mimalloc's
scavenger thread across the poll (`mi_on_thread_idle_start()` /
`_end()`), and the scavenger comes back for them by itself, two or three
sweeps an interval apart. Two callers swept inline once and then blocked
for good, and would have kept such blocks, as they do on main:

- **`src/threading/ThreadPool.rs`**: a worker swept once, after its
futex wait timed out at 100 ms, and then waited without a timeout. It
now brackets that second wait with `mi_on_thread_idle_start()` /
`mi_on_thread_idle_end()`, and sweeps inline (once, as before) only when
`_start` says there is no scavenger to hand off to. Nothing runs between
the two calls but `Futex::wait`, which allocates nothing;
`drain_idle_events` runs before `_start`.
Only the wait that follows a timed-out one is bracketed. Bracketing
every park was measured and is slower (table below: `_start` is two
read-modify-writes on cache lines that every parking thread shares, and
it wakes the scavenger, which costs half a context switch per task), and
sweeping on every park is what the comment there warns about (13% of
`vite preview` requests per second). As it is, a worker that parks
between tasks never reaches `_start`, and `notify()` is what it was.
- **The libuv loop on Windows** (`Bun__JSC_onBeforeWait` in
`BunJSCEventLoop.cpp`, from `us_loop_run` in `libuv.c`) sweeps inline at
most every 100 ms and then `uv_run(UV_RUN_ONCE)` blocks. It cannot hand
off: `uv_run` dispatches the completions right after its poll inside the
same call (`uv__poll` at `src/win/core.c:732`, `uv__process_reqs` at
`:737`), and libuv allocates from mimalloc there
(`uv_replace_allocator`), so there is no point between "about to block"
and "awake" that bun's code sees without a patch to libuv. It keeps the
inline sweep, and while `mi_on_thread_idle_pending()` says blocks were
left, `Bun__JSC_onBeforeWait` returns 100 (ms) and `us_loop_run` arms an
unref'd timer for that long after the sweep, so the hook runs again:
three times at the most after JS last ran, which is the bound the fork
gives. The timer does not keep the loop alive.

Not changed: when no scavenger runs (`MIMALLOC_SCAVENGER=0` or a purge
delay of 0; bun sets neither) the fallbacks in `us_loop_run_bun_tick`
and in the pool sweep inline once, as on main, and the free blocks of
large pages stay.

### How did you verify your code works?

Release builds of one tree (this branch on `04823d8fad67`), Linux x64:
**base** = main's pin, **pin** = this pin with the callers unchanged
(the first commit), **this PR** = both commits.

**Test.** `test/js/bun/jsc/heapStats-mimalloc.test.ts` gains "an idle
work pool thread hands back the free blocks of its large pages": a
subprocess with 4 pool threads reads 480 files of 96 to 512 KiB with
`fs.promises.readFile` (the buffers are allocated on the pool threads),
keeps 1 in 12, collects the rest, and polls `RssAnon` while nothing
gives the pool work. Anonymous memory beyond what it started with and
what it keeps has to come down to 12 MB within 2.5 s.

| | free and resident at the end |
| --- | --- |
| base | 54 to 67 MB: fails |
| pin, callers unchanged | 38 to 47 MB: fails (also with
`BUN_GARBAGE_COLLECTOR_LEVEL=1` and `2`, and with the system bun 1.4.1:
50 MB) |
| this PR | -1 MB, reached 350 ms into the idle: passes, 10 of 10 runs |

**The park and notify path of the pool** (what the `has_swept` comment
protects). 100000 `await fs.promises.stat("/")` one after the other
(every call notifies a worker, which runs one task and parks), the same
in batches of 8, and `await randomBytes(16)`; `perf stat` of the whole
process, 8 cores, 5 interleaved runs, medians (range):

| user instructions per call | base | pin | this PR |
| --- | --- | --- | --- |
| `stat`, sequential | 8794 (8784..8828) | 8792 (8716..8827) | 8797
(8780..8816) |
| `stat`, 8 at a time | 6027 (6014..6131) | 6025 (6004..6080) | 6034
(6008..6102) |
| `randomBytes(16)`, sequential | 12608 (12513..12629) | 12581
(12522..12609) | 12589 (12582..12627) |

Context switches per call are 3.96 to 3.97, 0.51 to 0.53 and 3.97 in all
three, kernel instructions per call overlap (41.6 k..49.9 k sequential),
wall time is within the spread of the machine.

Bracketing EVERY park instead (a build with a switch for it, 200000
calls, 4 interleaved runs), against the same build with the handoff
after a timed-out wait only:

| | after a timed-out wait (this PR) | every park |
| --- | --- | --- |
| `stat` sequential: user / kernel instructions, context switches per
call | 8565..8601 / 45.6 k..46.7 k / 3.95..3.99 | 9193..9335 (+7%) /
49.6 k..50.7 k (+9%) / 4.2..4.55 |
| `stat` 8 at a time | 5635..5659 / 8.8 k..9.3 k / 0.53..0.55 |
5771..5960 (+3%) / 10.1 k..11.6 k (+13%) / 0.63..0.75 |
| `randomBytes(16)` sequential | 12145..12283 / 43.2 k..45.5 k /
3.97..3.99 | 12815..12893 (+5%) / 48.2 k..51.2 k (+11%) / 4.41..4.5 |
| wall time, `stat` sequential | 11.6 s..12.7 s | 12.4 s..13.5 s (+6 to
8%) |

**Allocator micro benchmarks** (mimalloc alone with bun's flags: clang
21, `-O3 -march=nehalem`, C++, `MI_FREE_USE_PAGEMAP`; user-mode
instructions, the runs of one build equal to within 100):

| 25 M `mi_malloc` + 25 M `mi_free` of 16..1039 bytes | instructions |
| --- | --- |
| before the upstream sync (`707d90be`) | 2.0731 G |
| main's pin (`ab135013`) | 2.0568 G |
| with oven-sh/mimalloc#40, #43, #42 (`3aa0ac9b`) | 2.0556 G (-0.06%) |
| with #44, #45 (`58460467`) | 2.0305 G (-1.28%) |
| this pin (`69050cf3`) | **2.0305 G (-1.28%)**, the same as `58460467`
to within 400 instructions |
| this pin with `MI_PROFILE=0` (not what bun builds) | 2.0244 G |

(The absolute numbers are 8% below the ones this description had before:
the benchmark's own object file is built differently here. All rows are
from one recipe.)

| 20 M `mi_malloc(128 KiB)` + `mi_free`, the generic path into a large
page | instructions per pair |
| --- | --- |
| main's pin | 230.6 |
| `58460467` | 193.6 |
| this pin | 206.6 (+13.0 for the epoch stamp and its gate; -10% against
main's pin) |

**bun** (5 interleaved runs pinned to 8 cores; user-mode instructions
and peak RSS, medians):

| | base | pin | this PR |
| --- | --- | --- | --- |
| `bun -e 1` | 10.405 M, 27.0 MB | 10.410 M (+0.04%), 27.0 MB | 10.412 M
(+0.06%), 27.0 MB |
| `Bun.serve` hello, per request (20000 sequential `fetch`) | 52071,
49.0 MB | 51129 (-1.8%), 50.6 MB | 51361 (-1.4%; the spread between runs
is 3%), 50.1 MB |
| `Bun.serve` start and stop alone | 12.180 M | 12.179 M | 12.182 M
(+0.01%) |
| `JSON.stringify` + `JSON.parse` of a 200-user object, 3000 times |
5105.20 M, 44.0 MB | 5104.58 M, 44.0 MB | 5104.00 M (-0.02%), 43.5 MB |
| string concat + `Buffer.from` + `split`, 300 x 5000 | 1900 M, 59.6 MB
| 1912 M (+0.6%; the spread between its runs is 6.5%) , 60.5 MB | 1897 M
(-0.2%), 59.7 MB |
| idle after start (`Bun.sleep`), RSS / anonymous from `smaps_rollup`,
binaries read into the page cache first | 28.42 MB / 3.14 | 28.45 MB /
3.05 | 28.45 MB / 3.05 |

Nothing is slower by more than 0.1% outside the spread of its own runs.

**The large-buffer server of oven-sh/mimalloc#46 at bun level**
(`Bun.serve` that reads 256 KiB request bodies and answers 90 to 270 KB
of JSON; 16 connections for 15 s, then idle; RSS / anonymous from
`smaps_rollup`):

| at idle | base | pin | this PR |
| --- | --- | --- | --- |
| 40 bodies kept, 2 s idle | 74.7 / 36.2 MB | 54.3 / 15.9 MB | 54.3 /
15.9 MB (**-20.4 MB**) |
| 40 bodies kept, 15 s idle | 74.7 / 36.2 MB | 54.2 / 15.8 MB | 54.2 /
15.9 MB |
| nothing kept, 15 s idle | 47.0 / 8.6 MB | 43.6 / 5.3 MB | 43.7 / 5.5
MB (-3.3 MB) |
| at the end of the load, before the idle (40 kept) | 74.4 / 36.4 MB |
73.7 / 35.7 MB | 73.9 / 35.9 MB |

Under sustained load (the same server, 20 s, 3 interleaved rounds;
instructions and page faults of the server process from `perf stat -p`):

| | requests | user instr / request | kernel instr / request | page
faults / request |
| --- | --- | --- | --- | --- |
| base | 83.6 k .. 92.7 k | 283.9 k .. 284.5 k | 119.4 k .. 126.6 k |
0.34 .. 0.53 |
| pin | 87.0 k .. 91.4 k | 284.0 k .. 284.1 k | 118.8 k .. 127.4 k |
0.50 .. 0.64 |
| this PR | 80.4 k .. 91.4 k | 284.0 k .. 284.7 k | 120.9 k .. 125.3 k |
0.51 .. 0.86 |

Requests and instructions per request are the same; the buffers stay
while the server is busy and go when it is not.

**Tests with the release build of this PR**
- `test/js/bun/spawn/spawn-pipe-leak.test.ts` 25 of 25 runs,
`test/js/bun/jsc/heapStats-mimalloc.test.ts` (with the new test) and
`test/js/bun/gc/gc-controller-cadence.test.ts` 10 of 10 each.
- 66 test files, 2165 tests pass: `require-cache`, `serve.test.ts`, all
of `test/js/web/workers`, `test/js/node/worker_threads`,
`test/js/node/child_process` and `test/js/node/fs` (the pool-heavy
ones), `spawn-noread-leak`, `bun-file`, `bun-write`, `bundler_edgecase`,
`esbuild/splitting`. The two failures need a non-root user (the
root-range port in `serve.test.ts`, EPERM after dropping privileges in
`child_process.test.ts`) and fail the same way with the base build.
- The wider list of 113 files (bake dev, bundler, fetch, http, streams,
sqlite, install, resolve, shell and others): 2485 tests pass; the two
failures are in `resolve.test.ts`, want a directory that root cannot
read, and fail the same way with the base build.
- The new test fails with `USE_SYSTEM_BUN=1` (bun 1.4.1: 50 MB) and with
the first commit alone (38 to 47 MB), and passes with both.
- No debug or ASAN build was made for this revision of the PR (the
earlier revisions ran `heapStats-mimalloc`, `gc-controller-cadence` and
`worker.test.ts` on one at `58460467`); the new test skips itself under
ASAN, where malloc is not mimalloc.

**Not run here:** macOS and Windows. The Windows half of the second
commit (`BunJSCEventLoop.cpp` under `OS(WINDOWS)`, `libuv.c`, `libuv.h`)
was compiled by nothing on this machine: the C++ function was
syntax-checked with `OS(WINDOWS)` forced on against stubs, `cargo check
--target x86_64-pc-windows-msvc` passes for `bun_uws_sys`,
`bun_threading` and `bun_mimalloc_sys` (the `WindowsLoop` mirror gained
the field), and the rest is for CI. The fork's CI passed 23 of 23 jobs
at each of the six merges (Linux x64/arm64 with ASAN, UBSAN, TSAN and
guarded builds, the `MI_FREE_USE_PAGEMAP` build that matches bun's
configuration, macOS, Windows x64/arm64, FreeBSD).
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