Repository navigation
fix(jit): restore gpui-shell performance regressed in 0.12.10 - #33
Merged
Merged
Conversation
Exhausting the five Tier2 profitability retries returned every Baseline to the interpreter until the funded trial installed. The retries only say the modeled Tier2 saving has not paid off yet; the runtime never measures the interpreter, so they are no evidence that the Baseline is harmful. Since P2 widened Tier 1 coverage, the trial waits behind first Baseline compiles of other functions on the single worker, and gpui-shell renders ran at interpreter speed for about 100 frames (compute 0.13x, mixed 0.49x the speed of 0.12.9 in its acceptance window). - Demote on exhausted retries only when a Baseline execution is shorter than 10 us, where native entry bookkeeping can dominate the body. Longer Baselines keep running and stay enterable while blacklisted. - Move such a funded trial to the front of the compile queue, and hand the worker channel no more jobs than there are workers so the backlog stays reorderable. - The call-only and untranslatable demotion paths still demote a retained Baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Roadmap P2 let Tier 1 compile closures, var refs, generic operators, exception regions and for-in/for-of. In host UI code those functions are short helper-bridged calls whose native entry bookkeeping exceeds their body: gpui-shell's panel builder ran at 0.8x the interpreter's speed with about 220 native entries per render, and their compiles queued ahead of hot kernels. Profitability never measures the interpreter, and stable-IC trials bypass the retry demotion, so nothing returned them. Automatic tiering now rejects functions containing any opcode Tier 1 gained in P2, with the reason 0.12.9 reported, until profitability can measure the interpreter. BaselineOnly, Optimize and forced test tiers still compile them; the exception lowering tests move to BaselineOnly and the settle tests assert the automatic deferral. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profitability retries back off by 2, 4, 8 and 16 clock ticks, but they are evaluated only by maintenance. A host that never calls Jit::poll (gpui-shell) runs maintenance every HOT_MAINTENANCE_INTERVAL hot ticks once its kernel runs natively, so each short backoff became about 31 frames and the funded Tier2 trial of gpui-shell's layout kernel waited about 130 frames. 0.12.9 only escaped this through unrelated rejections that happened to trigger maintenance. - Maintenance is due when the earliest future retry is reached; exhausting the retries schedules the funded trial for the next maintenance. - The entry-dominated demotion rule now uses the fastest complete Baseline execution. A preempted sample inflated the average above the threshold under load, and an OSR entry times only the rest of one call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he tiering fixes Full 35-scenario shared-js-multibatch-v3 matrix for 8fa2a48 against the v0.12.10 runtime, same-version QuickJS and Bun 1.4.0, plus the paired gpui-shell acceptance results (compute, mixed, panel on one and two cores) that the matrix protocol cannot observe. The 889f7c1 matrix moves to a labeled historical block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nightly sanitizer job failed on object_fast_paths since the P0-P4 merge: one MSan-instrumented Tier 1 compile took over 30 s on the CI runner, timed out on every retry, and outlived the 60 s wait. CI stopped there, hiding later sanitizer-only failures; a local no-fail-fast MSan run found zero_argument_tier2_callee_deopts_under_a_tier1_generic_op_caller, whose forced Tier2 callee was demoted by the timed trial under load before the deopt could be observed. - Sanitizer builds default to a ten-minute compile budget. - object_fast_paths waits 300 s under sanitizers, like inline_refcount. - A forced test tier ignores timed Tier2 losses; timing noise alone must not tier it down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI's sanitizer job stopped at the first slow failure, hiding the rest. A local no-fail-fast MSan/ASan/TSan run of the CI commands found: - Wait deadlines tuned for release builds: every fixed `let deadline = now + N s` in the JIT tests now allows 5x under sanitizers (array_bce failed on CI at 60 s). - tier2_ownership's shared runner stopped after 200 calls even when the test expects Tier 2; it now keeps calling until Tier 2 or the deadline. - LeakSanitizer reported fast_entry's intentionally leaked Calls once the owning test returned; they stay registered, and reachable, for the process lifetime. - The entry-dominated demotion test relies on absolute timing, which instrumentation inflates for bookkeeping but not for generated code; it is ignored under sanitizers. No ThreadSanitizer race or AddressSanitizer memory error was reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alls On the sanitizer CI runner the caller's first Tier2 artifact was entered before its callee edge was ready, so the test counted two generic CALLs. Wait, as wait_for_caller_tier2 does, until a Tier2 run makes none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 of 4 tasks
huacnlee
added a commit
to longbridge/gpui-kit
that referenced
this pull request
Sep 30, 2026
## Description - Update `quickjs-jit`, `quickjs-jit-runtime`, and the local `rquickjs-compat` facade from 0.12.9 to the v0.12.11 release commit (`96593cd`). - Keep `quickjs-jit-stdlib` on its existing compatibility revision, as in the 0.12.9 update (#3116); it is not part of the release tag. - Refresh the lockfile. Only the five quickjs-jit packages change version and source. 0.12.10 is skipped deliberately: it regressed gpui-shell renders. 0.12.11 ([longbridge/quickjs-jit#33](longbridge/quickjs-jit#33)) fixes that regression. ## Performance `tests::benchmark::emit_one_jit_acceptance_sample`, i7-13700KF, release build. Each fresh process renders 64 warmup frames, then the mean of 200 measured renders is reported in µs. Ten retained process pairs per configuration, alternating order. "1 core" pins the render thread and the JIT worker to one CPU; "2 cores" uses two physical cores. Measured on the quickjs-jit runtime code that shipped as 0.12.11 (`8fa2a48`; later commits only change tests and sanitizer builds). | Workload | Cores | JIT 0.12.9 | JIT 0.12.10 | JIT 0.12.11 | vs 0.12.9 | | --- | --- | ---: | ---: | ---: | ---: | | Compute-heavy JavaScript | 1 | 15.1 µs | 127.1 µs | **10.3 µs** | **1.5x faster** | | Compute-heavy JavaScript | 2 | 16.3 µs | 62.4 µs | **10.2 µs** | **1.6x faster** | | Mixed market + UI | 1 | 178.5 µs | 363.3 µs | **92.7 µs** | **1.9x faster** | | Mixed market + UI | 2 | 179.2 µs | 180.1 µs | **92.4 µs** | **1.9x faster** | | UI-builder dominated | 1 | 812.0 µs | 981.4 µs | **803.7 µs** | 1.03x faster | | UI-builder dominated | 2 | 806.9 µs | 987.9 µs | **805.5 µs** | tied (1.1% slower to 0.4% faster) | Checksums agree in every process; there are no native fallbacks or deopts. The raw data is in [quickjs-jit `benchmarks/results/shell-tiering-8fa2a48-gpui-shell-x86_64.json`](https://github.com/longbridge/quickjs-jit/blob/main/benchmarks/results/shell-tiering-8fa2a48-gpui-shell-x86_64.json). ## How to Test - `cargo fmt -p gpui-shell -- --check` - `cargo check -p gpui-shell --all-targets` - `cargo test -p gpui-shell --release` (714 + 8 passed) - `git diff --check` ## Checklist - [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and followed the guidelines. - [x] Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate. - [ ] Passed `cargo run` for story tests related to the changes. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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
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::polland is measured from the first frames.Three root causes, one commit each:
de40dcaBaseline 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.1680c2cP2 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,Optimizeand forced test tiers still compile them. (Chosen as the conservative option for 0.12.11; letting profitability measure the interpreter is the follow-up.)8fa2a48Retry 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_linkcaught it.Performance
gpui-shell (
emit_one_jit_acceptance_sample, 10 retained process pairs, mean render µs):jit-bench, all 35 scenarios,
shared-js-multibatch-v3, 30 retained processes per configuration, CPU 8. Full tables are in the README.exceptions-sync0.21x andfor-of-array0.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.Tests
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.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_pathsnow waits 300 s. A forced test tier ignores timed Tier2 losses, which timing noise alone could trigger.7f88490: every fixedlet deadline = now + N sin the JIT tests allows 5x under sanitizers. Thetier2_ownershiprunner 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_inlinewaits 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