Repository navigation
perf(jit): V8/Bun roadmap and P0-P4 implementation - #30
Merged
Merged
Conversation
Every engine now times K consecutive batches (default 16, JIT_BENCH_TIMED_BATCHES) after the 64 warmups. elapsed_ns is the upper median of those batches, all raw values are kept in protocol.timed_batch_ns, the old single fixed batch is kept as the fixed_batch_ns diagnostic, and batches slower than 5x the median are counted in outlier_batches (never dropped). Bun now runs the wrapper from a temporary .mjs file, recording its SHA-256, instead of bun -e. Under -e, batch index 64 (the only batch v2 timed) hit a one-off JSC stall that inflated Bun latency by 1.2x-32x on two-argument kernels. JIT_BENCH_BUN_LAUNCH=eval keeps -e as a control. The legacy reporter still rejects protocol-tagged samples and names the protocol. paired.py and summarize_paired.py share protocol_check.py, which validates v3 invariants and refuses mixed v2/v3 evidence. Adds docs/roadmap/p0-bench-protocol.md with the remaining P0 work and a JSC late-tier-up caveat for the file launch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add QuickJS patch 0021-helper-boundary: - Replace the per-helper linear bytecode scan in qjsjit_valid_pc with a lazily built, bytecode-owned instruction-boundary bitmap that uses the scan's exact decoding rules (scan fallback on allocation failure; freed with the bytecode and counted in JS_ComputeMemoryUsage). - Keep every helper-frame check but inline the pending-exception test and move rejection out of line so the validated fast path stays compact. - JS_JitInlineEnter: drop the redundant call/continuation PC scans and reserve shadow frames from a runtime-owned LIFO arena with heap fallback (heap-only under ASan/MSan). Tests cover mid-instruction and past-end frame PCs in helper validation and LIFO arena accounting for nested inline frames. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Extend the target-only linked leaf ABI to bounded, acyclic callees that read and write guarded own data fields of borrowed HeapRef arguments (e.g. generic-call-fallback's incrementAndRecord). Every speculation (Int32 overflow, shape identity/generation, field tag) runs before any write; field stores are buffered in SSA and committed in program order only at return, so status 1 still means an untouched frame and heap and the caller's exact generic-CALL recovery is unchanged. A direct target now always wins over frame inlining at its call PC. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Linked entries need property feedback, which only the callee's optimizing compile sees. A caller queued in the same scan (or before that install) captured a frame inline of the Baseline body and never re-linked, leaving generic-call-fallback on the ~9 ms JS_JitInlineEnter path in some processes. Queue callees before callers within a scan and defer a frame-inline-ready caller while its callee's optimizing compile is queued, compiling or ready. The wait ends on install or failure. Documents the slice and remaining work in docs/roadmap/p3b-effectful-direct.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tier 2 only specialized typed-array length/element/store sites whose receiver was an entry argument. float64array-traversal loads both views from buffers.ints/buffers.floats into const locals, so every iteration ran ints.length through the generic GET_PROPERTY bridge and its getter. - ArrayPlan identifies a receiver as an entry argument or as the unique local holding the operand's SSA value (KnownFacts now tracks value identity through agreeing Phis and preserved frame reads); a loop that writes that local is rejected. - Planned typed get_length lowers to the versioned leaf query (exit on miss) instead of the accessor-running bridge. - Lowering keeps up to four guarded metadata tuples keyed by frame slot, carries them across forward joins where every predecessor agrees, and hoists storage-only typed receivers (store destinations) to the loop preheader with poll revalidation. - Numeric constant pushes and guarded to_propkey are certified leaf sites; poll amortization additionally accepts owned-local stores outside every loop and proves frame reads across shape-guarded get_field leaves. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
QuickJS patch 0023 (ABI 1.25) lets the backend grant a bounded budget to the idle pc=0 handle it caches after a DONE exit. While the budget and a shared epoch hold, calls of that function (including C->JS callbacks such as Array.prototype.sort comparators) reuse the handle without call feedback, record_hot, entry_cache_epoch, native_enter or native_exit. Executions are counted in backend-owned shared state and marked JS_JIT_FRAME_FAST_ENTRY; non-DONE exits still report native_exit tagged JS_JIT_EXIT_FAST_UNPAIRED and are never re-cached. Calls that never reach native code no longer evict another function's admissible idle handle. The production backend grants 255 calls only to steady entries: an installed Tier 2 artifact whose profitability trial is decided, or an installed baseline whose optimizing tier is terminally blacklisted. Each renewal is a full call, so hot counts, call/return feedback, timing and maintenance are sampled instead of paid per call. Jit::metrics adds the shared counters, so native entries/exits and Tier 2 entries stay exact. Maintenance now skips re-evaluating deferred Tier 2 candidates while their inputs (feedback and shape-feedback versions, installed count, element loop samples) are unchanged, instead of re-snapshotting feedback on every pass. Shape feedback gains a version that ignores repeats and reordering. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rty loops - Guarded property leaves no longer republish operand-stack memory or stack_top; every observer (deopt, ownership helpers, generic bridges) already republishes the live prefix from SSA, and can_emit_access admits only borrowed/primitive operands. - After the final property plan, select checked Int32 updates and recompute numeric facts with the planned guarded leaves (and polls) as preserved frame reads, so loop counters crossing field accesses stay Int32 (integer compare, no Float64 fallback). Overflow and operand checks still deopt. - Cached field values carry their checked constant tag; proven inputs to a cached store skip the input tag check. - emit_opt_guard_branch and the generic if_* truthiness guard omit branches whose condition is statically true from emitted constants. - Canonicalize Tier 2 iconsts into the entry block so Cranelift's identity-based constant-phi pass removes loop-carried constant tags/flags. Noisy diagnostic (P-core pinned, 9 alternating runs, median of last 16 warmup batches): property-heavy 2.47x the 82d3808 JIT speed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds an interpreter-differential regression for packed-array receivers held in locals and records the noisy float64array-traversal/arrays-typed diagnostic plus remaining work in docs/roadmap. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…et_name) Add ABI 1.25 (runtime API 1.10) append-only closure helpers in sys/patches/0025-tier1-closures.patch: FCLOSURE, GET_VAR_REF, PUT_VAR_REF (plain/check modes), CLOSE_LOC and SET_NAME. Each mirrors its interpreter opcode on the validated current frame, including the exact TDZ ReferenceError, set_value ownership and close_lexical_var detaching. Bundled bindings, patch digests, fingerprints and ABI tests are updated. Tier 1 now admits fclosure/fclosure8, get_var_ref0-3/get_var_ref/ get_var_ref_check, put_var_ref0-3/put_var_ref/set_var_ref0-3/set_var_ref/ put_var_ref_check, close_loc and set_name. Attached var refs alias the interpreter's var_buf/arg_buf, so a function that creates closures reloads its native argument/local copies after every helper and poll (a closure may have written them during a call). Functions combining fclosure with put_loc_check_init stay rejected; put_var_ref_check_init and make_var_ref* remain ClosureFrame rejects. Tier 2 and frame inlining keep rejecting closure-creating callees. Adds opcode-case manifest entries for all 21 newly advertised opcodes and tier1_closures regression tests (captured local/argument reload, type-change deopt, stress-GC ownership, per-iteration close_loc, TDZ get/put errors, shared var refs, set_var_ref keep, function names). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Record up to four own-data shapes per property site (POLYMORPHIC_PROPERTY_LIMIT) and use them in both native tiers: - Tier 1 dispatches inline on the receiver's shape pointer and layout generation (the SHAPE_GUARD predicate) with per-shape slot offsets, dropping the per-site cache and the helper crossing; misses keep the exact GET/SET_PROPERTY helper edge. - Automatic tiering refreshes a baseline once with property feedback when the function is statically outside the Tier-2 vocabulary, so terminal baselines (objects-polymorphic, mixed-quotes workload) get the ICs. - Tier 2 accepts four-shape chains, compares shapes inline for frame-materialized receivers too, lowers megamorphic get/put sites to the owning generic helpers instead of failing (no deopt loop), and rejects shape-guarded receivers that are optimized element loads, which could only deopt on every execution. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add a non-designated diagnostic workload whose two read sites each see four stable shapes (one prototype-less) on the Tier-2 IC path, and document the first P4c slice, noisy diagnostics and remaining work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Admit throw, throw_error, catch, nip_catch, gosub and ret in Tier 1. - Verifier: model catch handler entries (caught value replaces the catch offset; locals joined over the whole try region), gosub return offsets and ret successors resolved to the owning finally block's return points; record the innermost live catch offset per PC. - Tier 1: every helper exception edge inside a try region publishes the exact owned stack and enters a per-handler landing pad that calls JS_JitCatchException (interpreter exception prologue incl. backtrace); a caught exception resumes the handler natively, anything else (uncatchable, invariant failure) exits for exact interpreter unwinding. throw/throw_error use JS_JitThrowValue/JS_JitThrowError. - Patch 0027 (ABI 1.25): the three exception entry points, plus generator/async/eval/with snapshot refusals now disable hot probes locally instead of re-requesting a snapshot at every call/resume/loop poll (the exceptions-promises-async regression). - Tier 2, inlining and direct leaf links reject exception regions up front; OSR validation checks catch offsets by their real tag. - Add the exceptions-sync benchmark scenario and regression tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add QuickJS patch 0028 (ABI 1.25, runtime API 1.10): an append-only ITERATOR_OP helper that runs the interpreter's exact stack effect for for_of_start/for_of_next/for_in_start/for_in_next/iterator_close on the materialized frame, plus a versioned non-allocating Array values leaf (JS_JitGetIteratorAPI) that revalidates the live iterator, the exact built-in next method, a fast Array and an in-bounds dense index on every step. Tier 1 now admits the five opcodes; for_of_next tries the leaf first and falls back to the helper, so iterator close on break/throw and all protocol errors keep interpreter semantics (the catch offset stays in the frame for the interpreter unwinder). The verifier keeps enumeration-record slot kinds across for_of_next/for_in_next, joins loop-scoped TDZ locals as Tagged, and OSR validates catch offsets by their real tag. Adds tier1_iteration regression tests, opcode manifest cases, updated bundled bindings for all JIT targets, and a for-of-array scenario. Noisy diagnostic: for-of-array 2.97x the 82d3808 automatic speed; collections/map-set-bigint/strings-json/json-codec statistically unchanged (see docs/roadmap/p2d-iteration.md). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add the ABI 1.25 versioned JSJitObjectAPI (patch 0032). Each leaf either
completes the interpreter's exact effect or misses without an observable
effect; none throws, runs user code or reenters the VM, so generated code
keeps the generic helper sequence as the fallback.
- literal: build {a: v0, ...} from the existing final hashed shape,
found directly from the (Object.prototype, atoms, C_W_E) hash chain,
with non-throwing allocation and the JS_NewObjectFromShape GC trigger.
- retain_shape: keep generically built literal shapes in the bounded,
GC-traced 0012 shape cache so later executions hit.
- array_method / array_push / array_push_method: exact get_field2 lookup
of Array.prototype data properties on fast arrays and one-argument
built-in push (stack check, fast-array path, non-throwing growth).
- Fix the upstream js_array_push fast path that appended at `length`
when a fast array kept count < length, leaving uninitialized slots
(the unmodified interpreter crashes on it).
Tier 1 fuses complete pure object literals and `x.push(v)` statements
with pure operands into one leaf call each, and tries the lookup/push
leaves before GET_PROPERTY/CALL on Array receivers.
Noisy diagnostic (loaded shared host, 9 alternating runs, automatic):
objects-polymorphic 1.21x the 82d3808 speed; mixed-quotes and
collections statistically tied. Remaining work and a pre-existing
define_field set-semantics bug are recorded in
docs/roadmap/p4e-alloc-fastpath.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reduce per-iteration overhead in Tier 2 loops without changing semantics: - keep the amortized poll countdown in an SSA variable instead of a stack slot; - amortize the header poll of each pure natural loop (same permits-amortized-poll proof, restricted to the loop's nodes) even when calls elsewhere rule out whole-function amortization; locals, numeric guards and revalidation still run every iteration; - share one definition per integer constant so loop-invariant tags stop becoming block parameters, then fold branches on constant conditions (after Cranelift constant-phi removal) and drop the dead exits; - forward scalar bitcast round trips on Float64 payloads; - reload frame buffers and the exit record in exit blocks instead of keeping prologue values live across loops; on x86-64 System V pass the exit record as a plain pointer and return it explicitly, which keeps the ABI contract while ending the sret live range at entry; - lower `x >>> c` with a non-zero masked constant count as Int32. RelocatableCode::clif() keeps the lowering output; a test-support-only QJSJIT_DUMP_CODE directory dumps final CLIF and machine code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ions - Production tiering runs a hot try/catch/finally loop natively and an OSR entry lands on a loop header inside a try region. - Refused async/generator snapshots are no longer re-requested. - sys patch tests cover patch 0027 and the ABI 1.25 bindings. - Document patch 0027 and the p2c-exceptions slice, noisy diagnostics and remaining work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Tier 2 Bounds-check elimination and metadata hoisting for packed Array, Int32Array and Float64Array loops: - Fold literal-constant guard conditions (iconst tags through icmp/band/ bor/extends) so representation checks on hoisted Int32 lengths, loaded typed elements and proven induction variables emit no branch; compare literal-Int32 operands with integer icmp; branch directly on literal Bool/Int32 conditions. - Emit only the statically known storage kind for cached element loads and drop the redundant receiver tag check on the cached path. - Seal IR blocks whose predecessors are all lowered and local guard diamonds, and restate literal tags after the amortized poll join, so variable reads keep dominating literal definitions. - Track guarded element metadata per frame-slot provenance instead of one slot, carry loop-hoisted sources across in-loop diamonds when every incoming edge still holds the identical hoisted source, and hoist storage-only metadata for typed candidates without a length read (polls still revalidate data/count; bounds checks stay). - Treat numeric push_const sites as non-reentrant pure literals for array plans, amortized polls and representation proofs. - Restate a hoisted, never-rebound argument receiver's object tag at the loop header. - De-race automatic_call_heavy_promotes_the_direct_edge_caller by waiting for the leaf's five paced profitability retries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add sys/patches/0022-inline-refcount.patch. Static assertions require every payload a JS_VALUE_HAS_REF_COUNT value can address (objects, bytecode, strings, ropes, symbols, BigInts, modules) to start with the C int count, and the ABI value-layout fingerprint now also hashes its offset and width. The Rust side pins REF_COUNT_OFFSET/REF_COUNT_BYTES into the same fingerprint, so a QuickJS build without the patch fails ABI validation and the JIT stays closed. No ABI table or structure changes; ABI minor and bindings unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Generated code now increments ref_count for DUP and decrements it for FREE while other references remain; JS_JitHelperFree still runs for the last reference (finalizers, GC lists, reentrancy) and every stress-GC frame keeps the helpers and their collection points. - Tier 1: argument/local/stack dups, put/drop/nip/is_undefined frees, property receivers and consumed generic CALL operands. Inline releases write undefined to the consumed slot; CALL/GET_PROPERTY scratch outputs are cleared after their SSA reload because a following FREE no longer rematerializes the frame (exits reject owned scratch). - Tier 2: owned stack/local frees, owned-slot dup, shadow-frame dup/free and owner materialization on helper-bound paths (MATERIALIZE_OWNER is a js_dup); deopt/exception bridges keep the helper. The direct-call signature deopt block is cold and helper continuations inherit coldness. - Tests: new differential/ownership suite (tiers x stress GC, helper-count evidence that last references still reach FREE); helper-count expectations updated where shared references are now released inline. Noisy diagnostic vs 82d3808 (automatic tiering, P-cores, 9 paired runs): objects-polymorphic 1.23x, generic-call-fallback 1.38x, arrays-typed 1.06x, call-heavy 1.35x, float64array-traversal 1.27x, property-heavy 1.00x, mixed-quotes 0.98x. Not publishable evidence. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The interpreter's exception label builds a backtrace for every caught primitive or non-Error value only to store it in ctx->error_back_trace, which the catch then releases unread. JS_JitCatchException now skips that build when it cannot run user code (Error.prepareStackTrace unset, numeric Error.stackTraceLimit) and cannot define `stack` on the caught value. A regression test checks both hooks still fire exactly as in the interpreter for natively caught throws. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The arrays-typed scenario buries its two-receiver typed loop under interpreter-only push/construction work. typed-convert-traversal passes a stable Int32Array and Float64Array as arguments to one callee so the multi-source, metadata-only-hoist path is measured directly. Registered in the array traversal diagnostics suite with a hand-checked QuickJS checksum and the Bun checksum-parity list. docs/roadmap/p4b-bce.md records the slice, noisy diagnostics and the remaining work (non-argument receivers, poll countdown register, wrapping add under |0, cross-length range facts). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…helper Add patch 0026 (ABI 1.25, runtime API 1.10) with one append-only helper, GENERIC_OP (ID 24, MAP_OUT_TWO_OP), that runs the interpreter's own C routines for push_this, special_object (arguments, this function, new.target, home object, variable object, null prototype), get_var_undef, delete_var, put_var, typeof, typeof_is_undefined, typeof_is_function, to_object, to_propkey2, in, instanceof and delete. Root native entries publish a stack-allocated receiver/new.target/argv context that is saved and restored around the pinned entry; push_this and special_object reject any other frame. Mapped (sloppy) arguments and import.meta stay interpreter-only through an operand-sensitive Tier 1 rejection. Tier 1 lowers the new IrOp::Generic family with the same slot/ownership contract as the other exact slow paths, adds native fast paths for typeof_is_* on non-object primitives and for to_propkey2 with ready keys, routes pow through the existing BINARY_ARITH_SLOW helper, and admits the reachable stack shuffles (dup1, dup2, perm4, swap2, rot3l, rot3r). Unreachable shuffles, set_name*, and the global declaration family stay rejected. Every admitted opcode has an opcode-cases.json entry, and tests/tier1_generic_ops.rs covers exceptions, strict/sloppy receivers, proxies, getters, coercion and stress GC against the interpreter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Maintenance runs every 32 calls in callback-heavy code, so checking every optimizing snapshot for exception regions measurably slowed mixed-quotes (noisy diagnostic: 0.98x of 82d3808). Compute the flag once at verification and consult it only for candidates that are otherwise ready for Tier 2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Describe the target JIT->JIT call ABI (monomorphic call-target IC with identity, generation and publication epoch; native callee frames; lazy JSStackFrame materialization on deopt, exception, GC stack walk and backtrace; interaction with the stack walker, interrupts, stack-overflow checks and GC roots) and the first fail-closed slice implemented on this branch, with noisy diagnostics and remaining work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolve the lower_optimized_machine conflict by keeping the integrated amortization proof (amortized_numeric with preserved frame reads, plus numeric constant pushes, guarded property keys and owned-local stores outside loops as guarded heap sites) inside p4d's shared guarded_heap closure, which now feeds both whole-function and per-loop poll amortization. The per-loop proof uses amortized_numeric, a superset of scalar_numeric that is sound a fortiori because per-loop mode keeps the header numeric guard and immediate frame stores. The scalar_codegen countdown counter now ignores inline refcount releases (also iadd_imm -1, but stored rather than compared with zero). Add regression tests for per-loop countdown polls around shape-guarded property reads of a refcounted local, with and without GC stress, across a shape-change deopt and recompilation, including interrupt service. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in lower_optimized_machine's post-lowering tail: keep p4d's lowered-text capture and exit rematerialization, then run p4a's bounded const_canon pre-pass. The finalizer's own canonicalization/constant-branch folding (p4d) still runs afterwards; both are idempotent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolve against P1-6 (typed receivers in locals), which already provided multiple live tuples, forward-join inheritance and storage-only hoists: - Keep P1-6's ElementSources/at_block_entry (subsumes p4b's per-provenance set and hoisted-source diamond inheritance) and its emitter interfaces; port p4b's static storage kind, exact_length-gated bounds deletion, literal-tag folding (opt_known_const alongside P4a's opt_known_condition), early block sealing with opt_ir_edge, post-poll literal tag restatement and header object-tag restatement (length hoists of argument receivers). - p4b's metadata-only `length: None` hoist is carried by P1-6's storage_hoists (latch-dominance admission kept). Storage hoists now use p4b's non-exiting query: a miss publishes an empty `unguarded` view and poll revalidation skips count == 0, so zero-trip loops with a wrong-kind local receiver no longer deopt in the preheader. Regression test: array_bce::local_storage_hoists_never_deopt_for_accesses_that_do_not_run (fails with the side-exiting storage query). - Seed amortized_numeric with the same numeric constant-pool literals as scalar_numeric so the amortization proof stays a superset of lowering. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Attribute the remaining quickjs-jit gap to Bun/V8 on 82d3808 and plan staged, acceptance-gated work: - The Bun worker (`bun -e`) times only batch 64, which hits a one-off JSC stall; two-argument scenarios are inflated 1.2x-32x, so the README "faster than Bun" scalar rows are 0.15x-0.51x in steady state. - Diagnostic steady-state three-engine table (median of the last 16 warmup batches); every scenario is slower than Bun. - gdb signal-sampling profiles: linear PC rescans in helper frame validation (63% self time in objects-polymorphic), per-call JIT bookkeeping, 124/255 rejected opcodes, no general native call ABI. - Capability map against V8 and JSC and phased roadmap P0-P5. Diagnostics only; no runtime change and no README matrix change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Textually clean. Interaction fixes and review: - P1b inline refcount x P4c megamorphic Tier-2 store: the new emit_opt_owned_property_store released its receiver through the FREE helper unconditionally. Release it with emit_opt_free_slot (inline decrement for shared owners, FREE only for the last reference and stress-GC frames), the same contract P1b gave the sibling generic GET_PROPERTY replace path. Regression test: polymorphic_property_ic::production_tier2_megamorphic_store_releases_receivers_inline_without_leaks (non-stress Tier 2, shared argument receivers plus last-reference call results; obj_count must not change; dropping the release aborts at JS_FreeRuntime). - P4a/P4b: megamorphic get_field/put_field sites are not in specialization.properties, so they are neither guarded_heap leaves nor preserved frame reads, and they remain Reentrant/invalidating for the array metadata plan and property cache (which p4c flushes/invalidates before the helper). - P1c fast entry: the automatic terminal baseline refresh runs inside maintenance(), which invalidates the fast-entry epoch before queueing, installing (drain_completions) or reclaiming anything, and entry_cache_epoch() reports 0 while work is pending or the installed count changed, so no granted call can reuse a replaced artifact. - No helper IDs, JSJitRuntimeAPI slots, vtable/layout changes, patches or digest changes. QJSJIT_ABI_MINOR stays 25; runtime API minor stays 9. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
huacnlee
marked this pull request as draft
September 27, 2026 12:16
The p3b caller wait depends on the callee's optimizing tier state, which the p1c Tier2ScanInputs scan cache does not capture. A callee compile that fails or goes stale changes no scan input, so a cached deferral could keep the caller at Baseline until unrelated feedback or an install happened. The wait is transient and bounded by the in-flight compile, so it is rescanned instead of cached. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integrates Tier 1 closures (fclosure, var refs, close_loc, set_name). - Regenerated sys/patches/0025-tier1-closures.patch against 0001..0023 (logically identical; the ABI minor 24->25 hunk is dropped because 0023 already made the integration's single ABI bump). Updated its digest and the quickjs.c/quickjs-jit.h/quickjs-jit-helpers.h fingerprints. - Runtime API minor 9 -> 10 (the integration's single bump); closure helpers take IDs 24-28 and JSJitRuntimeAPI offsets 200-232 (size 240) as the first helper/API tail appended in the integration. - Bindings merged with the 0023 fast-entry additions; the host binding matches a fresh bindgen regeneration byte for byte. - Adapted the set_var_ref keep duplicate to P1b's inline-refcount lower_dup_if_refcounted signature (stress_primitives = false, matching the pre-P1b dup semantics) and added regression tests for inline refcounting of captured heap values. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integrates the Tier 1 GENERIC_OP helper family and the untranslatable settle. - Regenerated sys/patches/0026-tier1-generic-ops.patch against 0001..0025 (logically identical): GENERIC_OP is appended after the 0025 closure helpers in QJSJIT_HELPER_LIST, the root-call wrapper replaces the (0023-updated) pinned entry call, and the ABI/runtime-API minor hunks are dropped because 0023 and 0025 already made the integration's single bumps. Updated its digest and the quickjs.c / quickjs-jit-helpers.h fingerprints. - Renumbering: GENERIC_OP helper ID 24 -> 29 (COUNT 30); JSJitRuntimeAPI generic_op at offset 240, size 248 (wasm32: 124 / 128). Updated build.rs, abi.rs, tests/abi.rs, jit_patch.rs and all 10 bundled bindings; the host binding matches a fresh bindgen run byte for byte. - Kept both "Tier 2 can never translate" predicates (P4c's reachable-block optimized_vocabulary_admits for the terminal baseline refresh, P2b's optimized_opcodes_supported for the settle). The settle check runs before the Tier2ScanInputs deferral cache. - P2b x P4c: settle candidates never take the terminal baseline property refresh. An in-flight refresh left no installed baseline to demote, so the settled generation kept entering native code; under forced trials the refreshed accessor left a frame-inlined caller deoptimizing until its side-path trial failed (frame_inline flaked ~40%). Regression test untranslatable_property_loop_settles_instead_of_taking_a_terminal_baseline_refresh. - P2a+P2b x Tier 2 deopt validation: newly Tier 1 callers call zero-argument callees through the baseline CALL helper with a null argv, so the callee frame's arg_buf is null and a Tier 2 deopt failed with an uncatchable "invalid native deoptimization metadata" (jit-bench tier2 mode failed on 9 scripts). Empty argument/local areas may now be null in deopt and OSR frame validation, and observed_deopt_type never builds a slice from a null pointer. Regression test zero_argument_tier2_callee_deopts_under_a_tier1_generic_op_caller. - opcodes test: set_name is admitted by P2a's SET_NAME helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integrates Tier 1 exception regions (throw, throw_error, catch, nip_catch, gosub, ret) with native landing pads. - Regenerated sys/patches/0027-tier1-exception-regions.patch against 0001..0026 (logically identical; the ABI minor 24->25 hunk is dropped because 0023 already made the integration's single ABI bump). Updated its digest and the quickjs.c / quickjs-jit.h fingerprints. - Renumbering: none. 0027 adds three exported entry points (JS_JitThrowValue, JS_JitThrowError, JS_JitCatchException), no helper IDs, no JSJitRuntimeAPI slots and no layout changes; ABI minor stays 25 and runtime API minor stays 10. The bundled bindings carry the three declarations in fresh-bindgen formatting (the host binding matches a fresh bindgen run byte for byte); jit_patch.rs checks them. - P2a x P2c (real bug, fixed): in functions that create closures, the throwing helper (or the catch prologue's Error.prepareStackTrace hook) can write argument/local slots through an attached var ref and release the old value. Landing pads resumed the handler from the stale native copies (SIGSEGV on a freed object). The landing pad now reloads the captured slots from the frame before entering the handler. Regression test handlers_resume_with_locals_written_by_a_throwing_closure. - P2b x P2c: the untranslatable settle runs before the exception-region skip in the Tier 2 scan, so a region that also uses a newly admitted generic opcode still settles to the interpreter as P2b documents; other regions stay on Tier 1. - tagged_call_link: kept the multi-block linked leaf and added the has_exception_regions rejection. IrOp variant count 44; the opcode fixture keeps both sides' cases; methods-dynamic and exceptions-sync workloads both registered. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integrates the Tier 1 iteration opcodes (for_of_*, for_in_*, iterator_close) with the ITERATOR_OP helper and the Array values leaf, plus the Tier 2 lexical TDZ proof. - Regenerated sys/patches/0028-tier1-iteration.patch against 0001..0027 (logically identical): ITERATOR_OP is appended after GENERIC_OP in QJSJIT_HELPER_LIST, JS_JitHelperIteratorOp follows the 0026 root-call wrapper, and the ABI/runtime-API minor hunks are dropped because 0023 and 0025 already made the integration's single bumps. Updated its digest and the quickjs.c / quickjs-jit.h / quickjs-jit-helpers.h fingerprints. - Renumbering: ITERATOR_OP helper ID 24 -> 30 (COUNT 31); JSJitRuntimeAPI iterator_op at offset 248, size 256 (wasm32: 128 / 132). Updated build.rs, abi.rs, tests/abi.rs, jit_patch.rs and all 10 bundled bindings; the host binding matches a fresh bindgen run byte for byte. - P2c x P2d (real bug, fixed): P2c's verifier trimmed every catch offset inside an instruction's nominal pop window, so for_of_next dropped the live for-of close offset and every for-of loop failed verification at its header merge (UnsupportedExceptionRegion). for_of_next/for_in_next now keep their window before the trim. - P2c x P2d: a for-of close offset alone is no longer an exception region. Previously every for-of function would have been refused by the Tier 1 IR (no in-frame handler) and skipped by the Tier 2 scan, which also bypasses the profitability demotion P2d relies on for `collections`. Exceptions under a close offset take the ordinary native exception exit, where the interpreter closes the iterator and then resumes any enclosing handler, so for-of inside try also stays native. nip_catch's direct-form check reads the innermost catch offset (including a close offset), so `return` inside for-of now runs in Tier 1. - Tests: return_inside_for_of_stays_in_the_interpreter became return_inside_for_of_closes_the_iterator_in_tier1; added try_catch_inside_a_for_of_body_resumes_its_handler_natively, for_of_inside_a_try_region_closes_the_iterator_before_the_handler (asserts native entries) and for_of_close_offsets_alone_are_not_exception_regions. Opcode fixture keeps both sides' cases (p2d cases appended). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integrates the P3a native-to-native call convention: pure self-recursive Int32/Float64 Tier 2 functions publish a native call entry, run their recursion as direct native calls, and link monomorphic callers to it. - Regenerated sys/patches/0029-native-call-convention.patch against 0001..0028 (logically identical; only the hunk line numbers moved: the stack-overflow and interrupt-poll hunks by +53, the appended support functions by +1074). Updated its digest (0xb9cf_65e5_3bca_852e) and the quickjs.c fingerprint (0xaf4b_1500_a0d1_1bc1); quickjs-jit.h and quickjs-jit-helpers.h are unchanged. jit_patch.rs copies 0029. - Renumbering: none. 0029 adds plain exported functions and internal JSRuntime fields only: no helper IDs, JSJitRuntimeAPI slots, vtable or binding-visible layout changes. QJSJIT_ABI_MINOR stays 25 (0023) and the runtime API minor stays 10 (0025). - lib.rs Tier 2 scan: kept the p3b/p1c frame-ready wait for an in-flight linked callee and added P3a's native_ready skip and the wait for an admitted native-recursive callee. That wait also depends on the callee's optimizing state, which Tier2ScanInputs does not capture, so it marks callee_compile_wait and is rescanned instead of cached (same reasoning as 4c32486). Kept both tier2_untranslatable and the native_recursive / native_call_plans fields, and both the closure-creation gate and the native recursion constants. - P2c x P3a (test): native_recursion_resumes_after_a_caught_range_error wraps the recursion in a try/catch function, which now runs on Tier 1 (P2c) and enters `depth` through one generic CALL per evaluation instead of the interpreter. Added warm_native_from, which allows those outside calls while still requiring every recursive call to stay native. - P1c x P3a (new regression test): a native-recursive root's trial is decided on its first optimized exit, so it becomes fast-entry steady. granted_fast_entries_keep_native_chain_failures_exact checks granted callback-free entries, stack exhaustion and a due interrupt under them, and native_entries == native_exits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integrates the Tier 1 object-literal and Array.prototype.push leaf fast paths (versioned JSJitObjectAPI table: literal, retain_shape, array_method, array_push, array_push_method) plus the upstream js_array_push count < length fix. - Regenerated sys/patches/0032-object-fast-paths.patch against 0001..0029 (logically identical): the QJSJIT_ABI_MINOR 24 -> 25 hunk is dropped because 0023 already made the integration's single ABI bump, and the quickjs.c hunks moved by +1048 lines. Digest 0x3104_b683_7243_9d7e; quickjs.c fingerprint 0xa291_a4b4_7e88_fa66, quickjs-jit.h 0xcb77_8ada_a891_dd38 (quickjs-jit-helpers.h unchanged). jit_patch.rs copies 0032 after 0029. - Renumbering: none. JSJitObjectAPI is its own versioned table, so no helper IDs, JSJitRuntimeAPI slots or vtable changes. QJSJIT_ABI_MINOR stays 25 (0023) and the runtime API minor stays 10 (0025). The object table is declared right after JS_JitGetArrayAPI, before the P2d iterator table; the 10 bundled bindings insert the object block there and the host binding matches a fresh bindgen run byte for byte. - abi.rs: AbiInfo carries both iterator_api (P2d) and object_api; validate() checks both; AbiStructure/AbiMismatchFixture gain ObjectApi after IteratorApi (fixture count 21). - baseline.rs: HelperLowering keeps P2a reload_captured_slots and P2c exception_target and adds element_layout/object_fast. In CALL, P1b's clear_reloaded_output stays on the generic path before the push-leaf join (the hit path defines its output only in SSA, never frame memory). - Interactions (tests added): fused literal/push leaves inside a P2c try region whose generic fallback throws into the in-frame handler, fused operands that a closure (P2a) rewrites between executions, and a for-of (P2d) binding pushed and wrapped in a literal; differential plus stress GC, and helper counts proving the leaves still hit in those functions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
huacnlee
marked this pull request as ready for review
September 27, 2026 15:37
…er 2 callers - Import the fast-entry atomics unconditionally and gate compiler-only re-exports and the closure-profitability helper, so builds without the `compiler` feature pass under `-D warnings`. - Unpoison the receiver and metadata stack slots passed to the typed-array guard query and route it through `emit_external_call`; MSan cannot observe Cranelift stores. - Treat every call opcode (including `call0`..`call3`) as disqualifying a pure numeric candidate; callers with object/function arguments otherwise waited forever for a bounded numeric signature. - Refuse non-numeric side-path profiles at queue time. Side-path lowering only accepts Int32/Float64 exits, so an Object exit spent a compile and recorded a failure that stranded the installed caller. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…est deadline - Object literal, array method/push and shape-retain leaves read stack slots that Cranelift stored; MSan cannot observe those stores, so js_dup in qjsjit_object_literal reported use-of-uninitialized-value. Unpoison every input slot before the leaf call (no-op outside MSan builds). - inline_refcount's 60s deadline only bounds waiting for native code. Under coverage with stress GC, native code ran but 40 rounds exceeded 60s; stop early once the native requirement holds instead of failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Array iterator next leaf reads the iterator/next-method stack slot that Cranelift stored; unpoison it for MSan like the object leaves. A full local MSan run of the CI command now reports no findings. - native_calls: serialize the file (automatic tiering must profile the installed baseline), use recursion depths that fit debug/coverage C frames, and skip it under sanitizers, where one interpreter level takes tens of KiB and the 1 MiB JS stack overflows within ~40 levels. - tier1_exceptions: keep evaluating until a queued compile installs and keep the try/catch recursion shallow. - typed_receivers, untranslatable_settle, inline_refcount: tolerate slow Tier 1/Tier 2 compilation in debug, coverage and sanitizer builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntranslatable baselines - FeedbackTable never dropped entries of freed generations, so one-shot eval scripts filled its 256-entry capacity and later functions lost the return feedback that call links and native calls need. Forget a function's feedback when its generation retires (tagged_call_link failed under coverage). - Retiring a generation now flags its dispatched compile request; workers skip flagged jobs. Dead one-shot scripts otherwise filled the bounded worker queue and live functions were rolled back until they starved in Compiling(Baseline). - The optimizing scan no longer starts Tier 2 trials for generations whose reachable opcodes Tier 2 cannot translate. Such trials failed with UnsupportedOpcode until the optimizing tier was blacklisted, which is terminal and also switched off the installed baseline. - untranslatable_settle measures its windows from the first native entry instead of assuming the background compile installs within 1,000 calls. - differential: allow sanitizer builds 120 s to reach native code while the file's other tests share the runner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under MSan the Tier 2 compile of the comparator had not installed within 20 s while the file's other tests shared the runner's cores. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeping every Tier 2-untranslatable baseline installed made closure and JSON workloads slower than the interpreter (paired matrix at c1037d2: calls-closures 0.34x, calls-recursion-closures 0.49x, strings-json 0.79x the previous JIT's speed). The bounded Tier 2 trial and its return to the interpreter is restored; the settle test keeps the string-literal loop's native assertion and checks object/`new` loops for exact results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
huacnlee
added a commit
that referenced
this pull request
Sep 30, 2026
## Summary Full 35-scenario paired benchmark of `main` (`889f7c1`, PR #30) against the previous JIT (`82d3808` runtime, same v3 harness), same-version QuickJS (JIT detached) and Bun 1.4.0 (default flags). README's `JIT_MATRIX` is replaced; the `6165383` v2 matrix moves to a labeled historical block (its Bun column was inflated by the `bun -e` stall). - Protocol `shared-js-multibatch-v3`, 16 timed batches/process, 5 discarded + 30 retained processes per configuration, CPU 8 pinned, i7-13700KF, 6,125 process records. - vs previous JIT: 28 faster, 2 slower (`quickjs-fibonacci` 0.967x, `calls-recursion-closures` 0.992x), 5 tied. - vs QuickJS: 25 faster, 5 slower, 5 tied. - vs Bun: 0/35 faster; closest `fibonacci-recursive` 0.72x, widest gap `methods-dynamic` 0.013x. Evidence: `benchmarks/results/roadmap-889f7c1-{paired-x86_64.tar.gz,paired-x86_64.json,metadata-x86_64.json,manifest-x86_64.json}`. Caveat: the candidate and previous builds used their own untracked `Cargo.lock` files; both hashes are recorded in the manifest. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
huacnlee
added a commit
that referenced
this pull request
Sep 30, 2026
## Summary 0.12.10 regressed real gpui-shell renders (acceptance sample, one core): compute ran at 0.13x, mixed at 0.49x and panel at 0.82x the speed of 0.12.9. The 35-kernel jit-bench matrix could not show it: it polls the JIT after every batch and warms 640 calls, while gpui-shell never calls `Jit::poll` and is measured from the first frames. Three root causes, one commit each: - **`de40dca` Baseline demoted during the funded Tier2 trial.** Five rejected Tier2 profitability retries returned every Baseline to the interpreter until the trial installed. The retries only say the modeled Tier2 saving has not paid off; the runtime never measures the interpreter. Since P2 widened Tier 1 coverage, the trial waited behind other first Baselines on the single worker, so the layout kernel ran at interpreter speed for about 100 frames. Now only a Baseline whose fastest complete run is under 10 µs (entry bookkeeping can dominate, e.g. `return a + b`) is demoted. A longer one keeps running, and its trial moves to the front of the compile queue. The worker channel never holds more jobs than there are workers, so the backlog stays reorderable. - **`1680c2c` P2 opcodes slower than the interpreter in host code.** The panel builder's short helpers (closures, `push_this`, `typeof`, …) became native, with about 220 entries per render, at 0.8x the interpreter's speed. Stable-IC trials bypass the retry demotion, so nothing returned them. Automatic tiering now rejects functions containing any of the 52 opcodes Tier 1 gained in P2, with the reason 0.12.9 reported. `BaselineOnly`, `Optimize` and forced test tiers still compile them. (Chosen as the conservative option for 0.12.11; letting profitability measure the interpreter is the follow-up.) - **`8fa2a48` Retry backoff stretched without host polling.** Retries are evaluated only by maintenance, which ran every 64 hot ticks (about 31 frames) once the kernel was native. Maintenance is now due at the next retry, and exhausted retries queue the trial at the next maintenance. The demotion rule uses the fastest complete (non-OSR) Baseline run, so preemption or a partial OSR run cannot flip it. A generic "Tier2 first" queue order was tried and dropped. It exposed ordering hazards: a callee reaching Tier2 before its caller's Baseline loses call-link feedback, and `tagged_call_link` caught it. ## Performance **gpui-shell** (`emit_one_jit_acceptance_sample`, 10 retained process pairs, mean render µs): | Workload | Cores | 0.12.9 | 0.12.10 | this PR | vs 0.12.9 | vs 0.12.10 | | --- | --- | ---: | ---: | ---: | ---: | ---: | | compute | 1 | 15.1 | 127.1 | 10.3 | 1.5x (faster) | 12.3x (faster) | | compute | 2 | 16.3 | 62.4 | 10.2 | 1.6x (faster) | 6.6x (faster) | | mixed | 1 | 178.5 | 363.3 | 92.7 | 1.9x (faster) | 3.9x (faster) | | mixed | 2 | 179.2 | 180.1 | 92.4 | 1.9x (faster) | 2.0x (faster) | | panel | 1 | 812.0 | 981.4 | 803.7 | 1.03x (faster) | 1.3x (faster) | | panel | 2 | 806.9 | 987.9 | 805.5 | 1.00x (tied) | 1.2x (faster) | **jit-bench**, all 35 scenarios, `shared-js-multibatch-v3`, 30 retained processes per configuration, CPU 8. Full tables are in the README. - vs v0.12.10: 8 faster, 11 slower, 16 tied. The two large losses are the deliberate P2 deferral: `exceptions-sync` 0.21x and `for-of-array` 0.36x, back to QuickJS speed. The other nine are 0.5–3.6% slower; for three of them the unchanged interpreter moved by a similar amount. - vs QuickJS: 23 faster, 5 slower, 7 tied. - vs Bun: 0 of 35 faster. ## Tests - Regression tests, each verified to fail without its fix: `automatic_gpui_layout_kernel_keeps_baseline_until_tier2_trial`, `automatic_layout_kernel_reaches_tier2_without_host_polling`, `prioritized_request_is_taken_ahead_of_earlier_requests`, `dispatch_keeps_the_backlog_in_the_coordinator_while_workers_are_busy`. - Exception lowering tests moved to `BaselineOnly`. The settle tests assert the automatic deferral. The background tests follow bounded dispatch. - `cargo test -p quickjs-jit-runtime --release --features compiler,test-support`: 56 binaries, 987 tests pass, run twice. `cargo clippy --tests`: no warnings. ## Sanitizer CI The nightly sanitizer job had failed since the P0–P4 merge (#30), including on docs-only #31 and #32. It stopped at the first slow MSan test, so the ASan and TSan steps never ran. A local no-fail-fast run of the CI commands, also pinned to two cores to mimic the runner, found every remaining issue. No ASan memory error and no TSan data race was reported. - `2166391`: sanitizer builds default to a ten-minute compile budget. One MSan-instrumented Tier 1 compile took over 30 s on the runner and timed out on every retry. `object_fast_paths` now waits 300 s. A forced test tier ignores timed Tier2 losses, which timing noise alone could trigger. - `7f88490`: every fixed `let deadline = now + N s` in the JIT tests allows 5x under sanitizers. The `tier2_ownership` runner keeps calling until Tier 2 when the test expects it. `fast_entry`'s intentionally leaked state stays reachable for LeakSanitizer. The absolute-time entry-dominated demotion test is ignored under sanitizers: instrumentation slows the bookkeeping but not the generated code. - `36ddc59`: `frame_inline` waits for the linked Tier2 caller before counting generic calls. CI: MSan, ASan and TSan all pass. Locally, on two cores, 62/62 JIT test binaries pass under each sanitizer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds the V8/Bun gap analysis and roadmap (
docs/PERFORMANCE_ROADMAP_V8_BUN.md) and implements roadmap stages P0–P4. The work was done as 16 independently reviewed branches, merged sequentially onperf/roadmap-integrationwith full test runs after each merge, followed by two cross-feature interaction reviews. Neither review found a blocking issue.shared-js-multibatch-v3: 16 timed batches per process (median), Bun launched from a file wrapper instead ofbun -e, outlier batches flagged. This fixes the Bun fixed-batch stall that inflated Bun latency 1.2x–32x.GENERIC_OP(0026), throw/try/catch/finally with native exception dispatch (0027), and for-of/for-in iteration (0028).Array.prototype.pushfast paths (0032).ABI:
QJSJIT_ABI_MINOR24→25 and runtime API minor 9→10, each bumped once. Helper IDs are append-only (24–30) and all patch digests and fingerprints are re-pinned. Several items are first slices; the remaining work is indocs/roadmap/*.md.Performance
Regression screen, all 35 scenarios. The candidate is
f216b78. The previous JIT is the82d3808runtime built with the same v3 harness. QuickJS is the candidate binary with the JIT detached. Each engine ran 3 alternating processes on CPU 8, and each process reports the median of 16 timed batches. Units are ms per 10 workload calls. Speed is reference latency / candidate latency, so values above 1.00x mean the candidate is faster. Rows are sorted by speed relative to the previous JIT.generic-call-fallbackwas 0.14x the interpreter's speed and is now 6.28x.objects-polymorphicwas 0.26x and is now 1.28x.exceptions-promises-asyncwas 0.63x and is now 0.97x.strings-jsonat 0.60x. An 8-round interleaved re-run across all 17 branch binaries showed that the scenario is bimodal (about 2.0 ms or 3.6 ms per batch) in every binary, including the previous JIT.Evidence limits.
7a4b2d60e1ac23592524d263b909960fc5989600e5e51b874a52550c28646506fb07fbfc42d048e3d0e6aabdd5df844393a6342422573ac5e7e11eb432ec8d31Pre-change baseline at
82d3808(steady-state, diagnostic). Section 3 of the roadmap document has the per-scenario QuickJS / quickjs-jit / Bun table. Every scenario was slower than Bun, and the oldbun -efixed-batch protocol overstated quickjs-jit relative to Bun by up to about 30x.Known non-blocking issues
tagged_call_linkeffectful-link test: 2 failures in about 110 whole-binary runs (a rare stall).82d3808.docs/roadmap/p0-bench-protocol.md).Test plan
cargo fmt --check,jit_patchtests, the full runtime suite (--features compiler,test-support, 0 failures), clippy with-D warnings, and the benchmark bin tests.🤖 Generated with Claude Code