Bun.color: remove per-call mimalloc heap churn (3.5-7x faster, closes baseline gap) - #31494
Bun.color: remove per-call mimalloc heap churn (3.5-7x faster, closes baseline gap)#31494robobun wants to merge 1 commit into
Conversation
|
Status: perf change to
Waiting on CI. |
WalkthroughThis PR optimizes CSS color parsing and serialization by introducing a thread-local reusable arena. A new ChangesPer-thread arena reuse optimization
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 9:54 AM PT - Jul 9th, 2026
❌ @robobun, your commit 34be6bb has 1 failures in
🧪 To try this PR locally: bunx bun-pr 31494That installs a local version of the PR into your bun-31494 --bun |
There was a problem hiding this comment.
I didn't find any bugs and the safety reasoning looks sound, but this introduces a novel unsafe pattern (minting &'static Arena from a thread-local UnsafeCell with manually-argued non-reentrancy) that's worth a human sign-off.
Extended reasoning...
Overview
This PR eliminates per-call mi_heap_new/mi_heap_destroy churn in Bun.color() by (1) reusing a per-thread parse arena via a thread_local! UnsafeCell<Arena> that hands out a &'static Arena after a reset_retain_with_limit(64K), and (2) swapping the printer's Arena::new() for Arena::borrowing_default(). It also adds a 10k-iteration interleaved-reuse stress test and strides the 2²⁴ ansi256 fuzz on debug builds to avoid timeouts.
Security risks
None — Bun.color is a pure string/number → string/number/object utility with no I/O, auth, or filesystem surface.
Level of scrutiny
Elevated. The behavioral change is nil and the perf win is well-substantiated, but the implementation introduces a new unsafe block that extends a thread-local borrow to 'static and relies on hand-reasoned invariants: (a) CssColor carries no arena lifetime (verified — enum CssColor at src/css/values/color.rs:375 has no lifetime param), (b) the string-parse path never re-enters JS mid-parse, and (c) the previous call's &'static is dead before the next call's &mut reset. These all check out today, but this is the only UnsafeCell<Arena> in the codebase, so a maintainer should bless the pattern.
Other factors
borrowing_default()andreset_retain_with_limit()are established APIs already used for the same purpose inbunfig.rs,PackageInstall.rs,jsc_hooks.rs, etc., so the intent follows precedent — only the thread-local&'staticminting is new.- The printer change assumes
CssColor::to_cssnever allocates into the side arena (only into the global-heapdestvec); if that assumption were ever violated, allocations would land inmi_heap_main()rather than a destroyed-on-drop heap. The PR description states this was profiled, but it's an implicit coupling worth a reviewer's eye. - 917 existing tests pass unchanged and the new stress test specifically targets the reuse invariant; no CODEOWNERS cover these paths.
|
Addressed the review note about the novel Behavior and perf unchanged (baseline hsl 0.64µs, #f00 0.27µs; all 918 color tests pass on debug/ASAN). Pushed as 6e00223. |
There was a problem hiding this comment.
I didn't find any bugs and the reasoning checks out, but this changes arena lifecycle semantics (thread-local reuse + borrowing_default() for the printer) with correctness resting on allocation-behavior invariants in the CSS tokenizer/printer — worth a human sanity check from someone familiar with bun_alloc.
Extended reasoning...
Overview
This PR touches src/css_jsc/color_js.rs (the Bun.color() JS binding) and test/js/bun/css/color.test.ts. It replaces two per-call Arena::new() sites: the parse path now reuses a thread_local! { RefCell<Arena> } reset via reset_retain_with_limit(64 * 1024), and the CSS-format path borrows the process default heap via Arena::borrowing_default(). Tests add a 10k-iteration interleaved arena-reuse stress test and stride the 2²⁴ ansi256 fuzz on debug builds.
Security risks
None identified. No untrusted input handling changes, no auth/crypto/permissions. The change is allocation-strategy only; parse/format logic is untouched.
Level of scrutiny
This warrants human review despite being behavior-preserving. Correctness rests on several non-local invariants that I verified but a maintainer should confirm:
CssColor(src/css/values/color.rs:375) carries no arena lifetime, so returning it from thewith_parse_arenaclosure is sound — the borrow checker enforces this, and theRefCellmakes any (currently impossible) re-entry a safe panic rather than UB.Arena::borrowing_default()is an established pattern (bunfig.rs, PackageInstall.rs, jsc_hooks.rs), and the printer's arena-backedscratchbuf/indentation_buf(src/css/printer.rs:278 notes they rely on bulk free) appear to stay empty for a bareCssColor::to_css—scratchbufis never written in the Rust port andindentation_bufonly grows at indent > 0. If a futureto_csschange starts allocating into those, this becomes a per-call leak intomi_heap_mainrather than a crash, which is subtle.reset_retain_with_limit(src/bun_alloc/MimallocArena.rs:291) exists for exactly this hot-reuse pattern and bounds steady-state memory.
Other factors
The author already iterated once on reviewer feedback (dropped an unsafe &'static-minting approach for the safe RefCell scope), the bug-hunting system found nothing, 918 existing tests pass on debug/ASAN, and the new stress test specifically exercises the copy-on-write arena paths (CSS escapes, url() tokens). The isDebug stride on the fuzz test reduces debug coverage from 2²⁴ to 2¹⁵ values but keeps release exhaustive. The change is well-reasoned and likely correct; I'm deferring because memory-lifecycle changes in a hot path benefit from a maintainer's eyes on the borrowing_default() printer assumption specifically.
|
Addressed the follow-up review note about the The format path now shares the same reuse-per-call arena as the parser ( Perf and output unchanged (baseline: #f00 0.29µs, hsl 0.65µs; all 918 color tests pass on debug/ASAN). Pushed as f4bcbd4. |
There was a problem hiding this comment.
I didn't find any issues — the RefCell-scoped thread-local arena looks sound and the type system enforces that nothing arena-borrowed escapes with_color_arena — but introducing persistent per-thread allocator state in a runtime is the kind of change that deserves a human sign-off.
Extended reasoning...
Overview
This PR replaces two per-call Arena::new() constructions in Bun.color (one for the CSS parser, one for the CSS printer) with a single reused thread_local! { static ARENA: RefCell<Arena> } accessed via a with_color_arena helper that resets the heap with reset_retain_with_limit(64 * 1024) before each use. It also adds an interleaved-reuse stress test and strides the 2²⁴ fuzz ansi256 sweep on debug builds to avoid timeouts.
Security risks
None identified. No unsafe is introduced (an earlier revision had some, removed in 6e00223). The RefCell borrow is confined to the closure, and because with_color_arena<R>'s return type R has no lifetime relationship to the &Arena parameter, the borrow checker statically guarantees no arena-backed data escapes — so the second call's reset_retain_with_limit cannot invalidate live references from the first. Re-entry would be a safe RefCell double-borrow panic, not UB.
Level of scrutiny
Medium-high. The logic is correct as far as I can tell, reset_retain_with_limit is an established pattern used in ~9 other files, and the new stress test exercises the copy-on-write arena paths. But this introduces persistent per-thread allocator state with a retained mimalloc heap, and changes memory lifecycle semantics (the arena is reset between parse and serialize within the same call, relying on CssColor being fully owned). Allocator/lifetime changes in a runtime are not in the "simple, mechanical, obvious" bucket that warrants bot auto-approval.
Other factors
The PR already went through two review-driven revisions (dropping unsafe, then routing the printer through the shared arena instead of borrowing_default()), which suggests a human reviewer is already engaged and should give the final sign-off. The reported CI failures are infrastructure ("Failed to create agent" on Windows aarch64), not test failures. No CODEOWNERS entry covers these paths.
There was a problem hiding this comment.
I didn't find any issues, but this introduces thread-local arena reuse with reset-on-entry semantics in a runtime hot path — worth a human sign-off on the memory lifecycle even though it's safe Rust and matches the existing reset_retain_with_limit pattern elsewhere.
Extended reasoning...
Overview
This PR replaces per-call Arena::new() (which is mi_heap_new() + mi_heap_destroy() on drop) in Bun.color with a thread_local! { static ARENA: RefCell<Arena> } that is reset via reset_retain_with_limit(64 * 1024) at the top of each use. Both the CSS parse and the CSS-string serialization now route through the same with_color_arena(|arena| …) helper. The test file gains a 10k-iteration interleaved stress test exercising arena copy-on-write paths (CSS escapes, url() tokens) and strides the 2²⁴ ansi256 fuzz on debug builds to avoid timeouts.
Security risks
None identified. There is no unsafe — the closure signature FnOnce(&Arena) -> R with a free R statically prevents the arena borrow from escaping, so the parsed CssColor cannot carry arena-backed references past the reset. Re-entrancy would be a safe RefCell double-borrow panic rather than UB. Input is already bounded user-supplied color strings; no new attack surface.
Level of scrutiny
Moderate-to-high. While the diff is small and uses only safe Rust, it changes allocator lifecycle in a runtime hot path: a long-lived per-thread mimalloc heap now persists across Bun.color calls, with reset_retain_with_limit deciding when to retain vs. destroy+recreate. The pattern is well-established in this codebase (jsc_hooks.rs, FrameworkRouter.rs, renamer.rs, WorkspaceMap.rs, generateCompileResultForJSChunk.rs all use it), and the 64 KiB cap is conservative compared to the 8 MiB used elsewhere. Still, memory-lifecycle changes in shipped runtime code benefit from a human eye.
Other factors
- No CODEOWNERS match for
src/css_jsc/. - The bug-hunting system found nothing.
- The PR already went through two iterations responding to review notes (dropping an
unsafe/&'staticmint in favor ofRefCell, and replacingborrowing_default()for the printer with the shared reset-per-call arena), so the riskiest variants have already been removed. - 918 existing tests pass per the author; the new interleaved stress test specifically targets the reuse invariant.
- One subtlety I checked: within a single call, the second
with_color_arena(serialize) resets the arena that the first (parse) may have allocated into — but the type system guarantees the returnedCssColorholds no arena references, andreset_retain_with_limitunder the cap is a no-op retain anyway, so this is sound.
|
CI status: the diff is green. Build #58560 is 75 pass / 1 fail, and the one failure is I've already used my one retrigger (that's what produced #58560), so I won't re-roll again to avoid spamming builds. This needs a maintainer to merge or re-run the single expired macOS job. |
2b1af12 to
2421700
Compare
|
CI status after the rebase: the diff is green; the only failing lane is broken on main. Build 60644's sole lane failure is I've already used my one retrigger on this PR, so I'm not re-rolling again. Needs a maintainer to merge past the known-broken verify lane (or re-run it once main is fixed). |
There was a problem hiding this comment.
I didn't find any issues — the RefCell scoping is sound and reset_retain_with_limit matches established usage elsewhere — but introducing a persistent per-thread arena in a hot runtime path is a memory-lifecycle decision worth a maintainer's quick sign-off.
Extended reasoning...
Overview
This PR replaces two per-call Arena::new() constructions in Bun.color() (one for the CSS parser, one for the CSS printer) with a single reused thread_local! { static ARENA: RefCell<Arena> } accessed via a with_color_arena(|arena| …) helper that calls reset_retain_with_limit(64 * 1024) before each use. It also adds a 10k-iteration interleaved-input stress test and gates the existing 2²⁴ fuzz ansi256 sweep behind an isDebug stride. Two files touched: src/css_jsc/color_js.rs (~50 net lines, mostly doc comments) and test/js/bun/css/color.test.ts.
Security risks
None. Bun.color is a pure string→string conversion utility with no I/O, auth, crypto, or privilege boundaries. The change introduces no unsafe — the arena borrow is confined to a closure via RefCell::with_borrow_mut, and I verified CssColor / CssColorParseResult carry no lifetime parameter, so the type checker statically prevents arena-borrowed data from escaping. Re-entrancy (which the tokenizer/printer can't trigger anyway) would be a safe RefCell panic rather than UB.
Level of scrutiny
Moderate. The reset_retain_with_limit reuse pattern is well-established in this codebase (jsc_hooks.rs, generateCompileResultForJSChunk.rs, renamer.rs, FrameworkRouter.rs, install/lib.rs, WorkspaceMap.rs all use it), and this PR applies it cleanly with no novel mechanics after the earlier review rounds removed the unsafe and borrowing_default() approaches. That said, it does introduce a new persistent thread_local! heap (capped at 64 KiB per JS thread) — a small but real memory-lifecycle policy change that a maintainer should consciously accept rather than have auto-approved.
Other factors
- 917 existing tests pass unchanged plus a new dedicated arena-reuse stress test; CI green on all lanes (the one failure was an expired macOS runner, not a test).
- No CODEOWNERS for the touched paths.
- The PR already incorporated two rounds of review feedback (dropping
unsafe/&'static, then droppingborrowing_default()for the printer), which suggests the design has been iterated but also that it wasn't entirely mechanical to begin with. - The 64 KiB cap is more conservative than the 8 MiB used at most other
reset_retain_with_limitsites, which seems reasonable for color literals but is a tuning choice a maintainer might want to weigh in on.
|
Updated CI triage for build 60644 — every failure is CI-fleet noise, none touch this diff:
The lanes that exercise this change all pass: debian-13-x64-asan, debian-13-x64-baseline, debian-13 x64/aarch64, ubuntu-25.04 x64/baseline/aarch64, and all three Windows test lanes. I've already used my one retrigger on this PR, so I'm leaving it for a maintainer rather than pushing more retrigger commits. |
2421700 to
cb35b80
Compare
|
Rebased onto current main (cb35b80). Two main commits touched the same files since the last rebase:
926 color tests pass / 0 fail on the rebased branch; rustfmt clean. PR description's rebase notes updated. |
|
Addressed the review nit in 472c507: both CI triage for build 67031 (the run before this push): the only failed lane was |
There was a problem hiding this comment.
LGTM — nit addressed in 472c507; safe-Rust arena reuse following the established reset_retain_with_limit pattern, with a targeted stress test.
Extended reasoning...
Overview
This PR replaces per-call Arena::new() (which does mi_heap_new()/mi_heap_destroy()) in Bun.color()'s string parse and CSS serialize paths with a per-thread thread_local! { RefCell<Arena> } reused via reset_retain_with_limit(64 * 1024). The change is confined to src/css_jsc/color_js.rs (a ~40-line helper plus two call-site rewraps) and adds one stress test to test/js/bun/css/color.test.ts. Output is identical; this is a pure perf change (3.5–7× speedup, closes the baseline gap).
Security risks
None. No unsafe code is introduced — an earlier revision's unsafe lifetime extension was replaced with a RefCell-scoped closure per prior review feedback. CssColor (src/css/values/color.rs:370) carries no lifetime parameter, so the parse result is fully owned and cannot dangle past the arena borrow. Re-entry would be a safe RefCell double-borrow panic, not UB. Bun.color() takes no untrusted paths, does no I/O, and touches no auth/crypto/permissions surface.
Level of scrutiny
Moderate. Memory-lifecycle changes warrant care, but this one is mitigated on every axis: it's safe Rust; it follows an established codebase pattern (reset_retain_with_limit + reused per-thread arena is already used in src/install/lib.rs and src/install/npm.rs); the 64 KiB retain cap bounds steady-state memory; and the doc comment on with_color_arena spells out the invariants. Bun.color() is a leaf utility, not a critical path.
Other factors
- The PR has already been through three review iterations (drop
unsafe→RefCell; dropborrowing_default()→ shared arena; trim call-site comments per my prior nit), each addressed promptly. - My only prior finding was the comment-length nit, now resolved in 472c507 — both call sites are at the 2/3-line limit and match the suggested wording.
- The bug-hunting pass found nothing.
- 926 existing color tests pass unchanged, plus a new 10k-iteration interleaved stress test that mixes arena-allocating inputs (CSS escapes,
url()) with plain literals and invalid inputs to guard the reuse invariant. - CI green on all lanes that exercise this code (ASAN, baseline, x64/aarch64); the only failures across builds were known infra noise (expired macOS runners, fleet-wide alpine/windows-verify lane breakage on main).
|
CI status (build 67050, settled): 74 lanes pass; the single remaining lane failure is unrelated to this change.
Every lane that exercises this change passes (debian-13-x64-asan, debian-13-x64-baseline, debian/ubuntu/alpine x64+aarch64, darwin-14 x64, and all three Windows test lanes). Review is clean (claude bot LGTM, no unresolved threads). I've already used my one retrigger on this PR, so I'm leaving it here rather than pushing empty commits; this needs a maintainer to retry that one job or merge past it. |
Each Bun.color() string call created two fresh mimalloc heaps (Arena::new() = mi_heap_new() + mi_heap_destroy() on drop): one for the CSS parser and one for the CSS-string printer. That round-trip dominated the call: serializing an integer to css was ~1110ns vs ~218ns for the lean create_format path used by hex/rgb/rgba, pure heap churn. Reuse one warm per-thread heap for both via with_color_arena, a thread_local RefCell<Arena> reset at the top of each use with reset_retain_with_limit. The common color literal allocates nothing into it; escaped/non-ASCII idents allocate bounded copy-on-write buffers that the next reset reclaims. The borrow is confined to the closure so no unsafe is needed; re-entry would be a safe RefCell double-borrow panic. The parsed CssColor is fully owned and carries no arena lifetime. Output is unchanged; all existing color tests pass. Adds an interleaved arena-reuse stress test.
472c507 to
34be6bb
Compare
|
CI status (build 71065, finished): the diff is green; every failure is a GitHub download 504. Three 65 lanes pass in total, including every lane that exercises this change (debian-13-x64-asan, debian-13-x64-baseline, all linux/alpine/ubuntu/windows test lanes). Review is clean (claude bot LGTM, coderabbit no actionable comments, no unresolved threads). I've already used my one retrigger on this PR, so I'm not pushing empty commits at a CDN timeout. Needs a maintainer to retry those three build-cpp jobs or merge past them. |
There was a problem hiding this comment.
LGTM — the comment-length nit is addressed and the arena-reuse pattern matches established reset_retain_with_limit call sites elsewhere in the tree.
Extended reasoning...
Overview
This PR touches exactly two files: src/css_jsc/color_js.rs (the Bun.color() host function) and test/js/bun/css/color.test.ts (purely additive — one new stress test). The change replaces two per-call Arena::new() sites with a thread_local! { RefCell<Arena> } reused via a with_color_arena(|arena| ...) helper that calls reset_retain_with_limit(64 * 1024) before each use. Output is behaviorally identical; this is a pure perf change (3.5–7× faster, closes the baseline/haswell gap).
Security risks
None. Bun.color is a pure string→string color converter with no filesystem, network, auth, or privilege surface. The arena is a per-thread scratch buffer reset before every use, so there is no cross-call or cross-worker data bleed. No unsafe is introduced (an earlier revision's unsafe was replaced with the RefCell scoping per review feedback). The borrow checker statically prevents arena-lifetime data from escaping the closure (R in FnOnce(&Arena) -> R carries no arena lifetime).
Level of scrutiny
Low-to-moderate. Bun.color is a leaf utility API, not on any critical path (module loading, HTTP, install). The reset_retain_with_limit + reused-arena pattern is well-established across the codebase — I count ~10 existing call sites (jsc_hooks.rs, generateCompileResultForJSChunk.rs, install/lib.rs, WorkspaceMap.rs, renamer.rs, FrameworkRouter.rs, ast_memory_allocator.rs), so this is following convention rather than inventing a new mechanism. The 64KB cap (vs. the usual 8MB) is appropriately sized down for tiny color literals. The thread_local! concern from CLAUDE.md ("never back per-VM state with thread-locals") does not apply here: this is a reset-before-use scratch buffer, not semantic per-VM state, and each worker thread gets its own instance.
Other factors
- The bug-hunting system found no issues.
- I previously reviewed on 2026-06-30 with a single non-blocking nit (call-site comment length vs. CLAUDE.md rule 13), which was addressed in 472c507 and the thread resolved — the current diff shows the trimmed 2–3 line comments.
- 926 existing color tests pass unchanged; a new 10k-iteration interleaved stress test exercises the arena-allocating tokenizer paths (CSS escapes,
url()tokens) mixed with plain literals and invalid inputs to guard against reset/reuse state leakage. - CI is green on every lane that exercises this code (debian-asan, debian-baseline, all linux/windows/alpine); the residual failures documented in the timeline are known infra flakes (macOS agent expiry, an unrelated Bun.serve streaming test) with sibling passing attempts.
- The two
with_color_arenauses within one call are sequential (parse returns an ownedCssColor, then serialize borrows fresh), so theRefCellborrows never nest — and if they somehow did, it would be a safe panic, not UB.
What
Bun.color(input, "css")on thelinux-x64-baselinebuild was slower thanlinux-x64(from the canary comparison:hsl(0, 100%, 50%)1.84 → 2.01 µs). This PR makesBun.color()~3.5–7× faster across the board and closes the baseline-vs-native gap (they're now within noise).Investigation
Starting from the reported baseline regression, I built both
-march=nehalem(baseline) and-march=haswellbinaries and benchmarked. The baseline penalty was real but uniform across every color format (~5%), not concentrated inhsl()— which was the first clue that the hot cost wasn't the HSL→RGB float math (that only runs forhsl()), but the common parse/format path.Decomposing the per-call cost on baseline (ns/op):
Bun.color(int, "number")Bun.color(int, "css")Bun.color(int, "hex")BunString::create_formatBun.color("#abcdef", "number")The
"css"format was 5× slower than"hex"for the same kind of output. Both the parse and the"css"format path construct a freshbun_alloc::Arena(MimallocArena), andArena::new()=mi_heap_new()+ ami_heap_destroy()on drop. That heap create/destroy round-trip — two per call — was ~900ns each and dominated everything. (The same anti-pattern was already fixed inbunfig.rs, which attributed ~1.6% ofbun -e ''startup to exactly this.)So the maintainer's hunch about the layer was right (the hot path shouldn't depend on
-march), but the mechanism was heap churn, not vectorizable math — a Highway SIMD kernel wouldn't have touched the dominant cost. Removing the churn both cuts the absolute time and shrinks the fixed,-march-sensitive remainder (tokenizing + JS string creation) to a noise-level fraction, which is why the baseline gap closes.Fix
src/css_jsc/color_js.rs:Arena::new()per call, reset at the top of each call withreset_retain_with_limit. A color literal allocates nothing into the arena on the common path (idents/numbers/percentages are borrowed sub-slices of the input); only an escape sequence / NUL / non-ASCII byte forces a copy-on-writeArenaVec, which the per-call reset reclaims — so steady-state memory is bounded and nothing leaks.Bun.colorruns on the JS thread and the tokenizer doesn't re-enter it, and the parsedCssColoris fully owned (no arena lifetime), so the reused borrow never aliases a live&mut.Arena::borrowing_default()) for the CSS printer. A bare color value writes into the global-heapdestvec and leaves the printer'sscratchbuf/indentation_bufempty, so the side heap backed no allocations — it was pure create/destroy overhead.Behavior is identical — this is a pure performance change.
Results (mitata,
bench/snippets/color.mjs)#f00rgb(255, 0, 0)rgba(255, 0, 0, 1)hsl(0, 100%, 50%)Baseline is now within noise of the native build (was +4.4–5.9%), and every format is 3.5–7× faster in absolute terms.
Tests
test/js/bun/css/color.test.tscases pass unchanged (incl. adversarial inputs:url(#bad),calc(),var(--bad), escaped idents).reused parse arena stays correct across interleaved calls, which hammers the reused arena with 10k interleaved iterations mixing copy-on-write inputs (CSS escapes like\72\65\64== "red",url(...)function tokens), plain literals, and invalid inputs, asserting each call returns exactly what it returns in isolation. Guards against the reused arena leaking state between calls.As a pure perf change with identical output, there is no fail-before behavioral test. Correctness is covered by the full existing suite passing unchanged plus the new interleaved-reuse stress test.
Rebase notes
Rebased three times as main moved; the branch is squashed to one commit. Every main change in both files is kept:
BasicParseErrorKindmatch) and comment cleanups.fuzz ansi256debug timeout (viatest.skipIf(isDebug)) that an earlier revision of this PR had worked around with a strided sweep, so that test edit is dropped here and the test-file diff is purely additive.zero_if_nonehelper in the same spotwith_color_arenais inserted: both helpers kept side by side. Its changes to the ansi/hsl/lab output arms are untouched by this PR and merged cleanly. In the test file, color: ansi-16, ansi-256 and hsl/lab all produced unusable output #33328 (plus node: fix tty.WriteStream#getColorDepth reporting 256 colors for every TERM #33124, css: fix lab()/oklab() sRGB fallback desaturating boundary colors #33333, color: unmark the lab round-trip test as failing #33351 and make some tests faster #33622) appended ~200 lines of new tests where this PR appends its one; resolved by taking main's file verbatim and appending the arena-reuse test after them, so the test-file diff stays purely additive.973 color tests pass / 0 fail on the rebased branch.
[review] gate passed · iteration 12 · 2 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 12
evidence per changed file