Repository navigation
perf(engine): 16-bit JSON escaping on ARM64, microtask call cache per VM - #74
Conversation
…a toJSON or raw JSON The general serializer hands each object or array it reaches to the fast one, which now also copies JSON.rawJSON() values and finds a hidden own toJSON before it serializes an object. tests/aot/json-stringify.mjs checks toJSON order, what a toJSON changes while it runs, getters, errors, cycles, raw JSON, wide strings and indentation against stock Bun.
… time on ARM64 A string with one character beyond Latin-1 is 16-bit, and its escaping stopped at every character that wants it: 163K characters of source code with arrows in it took 342 us, now 57 us. tests/aot/json-stringify.mjs covers every kind of character at every place in a block, and surrogates at block ends.
Bun drains the microtask queue after every callback, and each drain made a new 8-entry cache of async function entry points: the first resume of each function in each drain missed it (about 190 instructions). The VM now owns a 32-entry, set-associative cache that it clears and reconciles like its existing one. An await of a non-object queues its continuation without the promise checks. tests/aot/await-order.mjs checks await order of settled and pending promises, plain values, thenables, subclassed promises, generators and 48 async functions taking turns against stock Bun.
…to Object.prototype The AOT build makes the built-in prototypes immutable.
Measured inside Pi (per-thread instructions of each request body's JSON.stringify over a 40-prompt session), it made requests 6-13% more expensive, although micro-benchmarks gained: kept are the 16-bit JSON escaping and the microtask call cache. tests/aot/json-stringify.mjs stays: it covers the escaping and toJSON semantics.
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Two engine commits appended to
patches/webkit.patch(it applies to the pin withgit apply --cached; the tree matches my WebKit checkout), plus tests/aot cases. Runtime run with exactly this patch: 39351585272 (all four platforms). Pi-level numbers follow when it finishes; the micro numbers below come from run 39326293812, which also contained a third change that has since been dropped (see "Dropped").1. JSON escaping of 16-bit strings on ARM64 (WTF)
Problem. One character outside Latin-1 (an arrow, an em dash, a curly quote) makes the whole string 16-bit. Answers and source files contain such characters all the time. JSON escaping of a 16-bit string used a generic loop that stopped at every character needing an escape, which in source code is every few dozen characters. 8-bit strings use a NEON shuffle path.
Example. 163K characters of
agent-session.tsplus one→:Fix. The 16-bit input is taken 16 characters at a time:
The same string now escapes in 57 µs. Over 200K random strings, with both output widths, the result is identical to the scalar escaper (checked with a harness built with LLVM 23 against the WTF header). tests/aot/json-stringify.mjs covers this too.
2. One microtask call cache per VM (JSC)
Problem. Each microtask drain built an 8-entry cache of async function entry points on the stack. Bun drains after every callback, so the first resume of each async function in each drain missed the cache, and so did every resume once more than 8 functions were taking turns. A miss costs about 190 instructions (MicrotaskCall::initialize, prepareForMicrotaskCall, getCallDataInline). In a chain of an async generator and three nested async calls, 6 of the 10 resumes per event missed.
Fix.
No job is added, removed or reordered.
Numbers (macOS arm64; instructions, because the Mac is shared and wall time is noise)
await undefined(AOT)Pi-level, same Pi built with the 0.9.5 runtime ("base") and with run 39351585272's darwin-arm64 runtime ("final"):
At the Pi level the effect is within noise on these benchmarks. Their content is almost all ASCII: three of the four fixtures have no non-ASCII character, and Pi's cache already turns long strings into raw JSON. The escaping gain applies when a conversation has non-Latin-1 text. The microtask cache gain applies when many async functions take turns. Neither changes behaviour.
Run 39351585272 so far: darwin-arm64 and linux-arm64 succeeded, tests/aot and tests/runtime included. linux-x64 and win32-x64 were still running at 01:58 UTC.
Tests
thencalled, athengetter, athenthat throws;thenor another constructor;Dropped
Hand-over from the general JSON serializer to the fast one (run 39326293812):
JSON.stringify, toJSON included, over a 40-prompt session in the same Pi with both runtimes:That is 6-13% more, and long_session.py instructions per prompt were 1-3% higher. The likely cause is an extra copy and allocation of the whole messages JSON, plus the garbage that goes with it, against Pi's small per-request cost. Not proven.
Not changed (findings)