Repository navigation
fix(codemode): run each job to completion - #54276
Conversation
Program fibers took turns at Effect's scheduler yields, so a long job could be preempted mid-statement and another job's writes to shared bindings were lost. Only the fiber holding the execution's turn now runs program code; awaits and reactions queue for it, and tool calls and host work run without it.
|
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
|
The following comment was made by an LLM, it may be inaccurate: |
|
Pushed one small commit on top (6bd8a8a): const log = []
Promise.resolve().then(() => log.push("t"))
const heavy = async () => { for (let i = 0; i < 1500; i++) {} await null; log.push("h") }
heavy()
await null
log.push("main")
await null
return log
// native ["t", "h", "main"]; before this commit ["t", "main", "h"]It also removes the inline nesting and |
There was a problem hiding this comment.
This fixes the problem, and I found nothing that should block a merge.
I ran the #54274 repro from source in this branch and in the base. On the base it still gives 1853 / 145; with this PR it gives 2000 / 1000, the same as plain JavaScript. I also compared 13 more programs with plain JavaScript: generators, async generators, .then ordering, tool calls inside Promise.all, failing tools, allSettled, race, promise executors and async recursion 9,000 calls deep. The base got 4 of them wrong (a generator delegating to another one failed with "Generator is already running", .then callbacks were dropped, the race case differed, and the deep recursion overflowed the stack). This branch gets all of them right except the race case, which differs only because a tool's real sleep races the interpreter's own run time. Programs that loop forever still stop with TimeoutExceeded: next to a slow tool call, inside a generator, inside a .then, inside an async generator, and with a paused generator. bun test (1,402 pass) and typecheck are clean.
Public API: unchanged. Turn and Pending.createHost are internal to the interpreter.
The cost you measured (about 2× time for programs with many concurrent tasks, about +100 MiB peak RSS at 8,000 tasks) is for maintainers to weigh. It stays bounded by the 10,000 pending-promise limit.
Upstream's Code Mode now runs each job to completion (anomalyco#54276): only the fiber holding the program's turn runs program code, and a promise a global returns is host work that runs without the turn. - A global's returned Effect is host work too, so it now runs without the turn. Started on the turn, it held the program until it finished: the program stopped at the call, concurrent fetches ran one at a time, a fetch waiting on its permission request stalled the program, and an un-awaited Effect that never ends hung the execution. A new interpreter test covers the program running on while two such calls are pending. - No other upstream change touches the fork's files; the auto-merged client types and OpenAPI documents match their generators.
Issue for this PR
Closes #54274
Type of change
What does this PR do?
Each async call, reaction and generator body runs on its own fiber, and Effect yields a fiber after 2,048 operations, so ready program fibers interleaved mid-statement and lost each other's writes (repro in the issue).
This adds a per-execution
Turn(promises.ts). Only the fiber holding it runs program code. The main job holds it. An async call or generator step lends the caller's turn to the callee until the callee suspends.await, reactions and combinator settlement pass it to the next waiting job and queue to take it back. Work that runs no program code runs without it: the host side of tool calls, extension promises, and fibers that only settle a promise. So tool calls stay concurrent and settlement timing is unchanged. Preemption stays on, so CPU loops still hittimeoutMs.Synchronous starts and handbacks nest fibers on the host stack, so past 100 levels they go through the scheduler. Without this, the existing "recurses before its first await" test hung: the host stack overflowed inside a fiber start and left a fiber holding the turn. As a side effect, that recursion now reaches the interpreter's 10,000-call limit; on Node the base overflows the host stack at about 1,340 calls.
Cost: once a job is preempted, waiting jobs resume through one more scheduler step. Medians of 7 alternating runs against base
v255dde88, on one 16-core Linux host:.thenreactionsrace/ nestedall/allSettledHow did you verify your code works?
bun testinpackages/codemode: 1,402 pass, including 4 new tests inpromise.test.ts. The two lost-update tests fail on base (1853 vs 2000, 145 vs 1000).bun typecheckand oxlint are clean.all/allSettled/anysettle one job late,breakinfor awaitresumes late, and copy-out dropsvalue: undefined.Screenshots / recordings
Not a UI change.
Checklist