perf(planning): yield between asynchronous slices without a timer

Each slice of the asynchronous adapter waited for a zero-delay timer:
about 1.1 ms in Bun, and at least 4 ms in browsers once timers nest.
Browsers now yield through a message-channel task. Bun runs port
messages ahead of timers and incoming worker messages, which starved
cancellation and heartbeats, so it yields with setImmediate. Both still
let queued cancellation and heartbeat tasks run between slices.

Part of #302.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nessarandClaude Opus 5.5 committed 2026-10-18 19:58:40 +02:00
1 parent 2fcba758c8
commit b7ec411c92
3 files changed
+55 -9

No files matched your search

+30
View File
@@ -59,3 +59,33 @@ reconciliation; afterwards retention took 0.26 seconds.
The typed-obligations difference is run-to-run noise; its retention cost was
22 milliseconds.
### Yield between asynchronous slices without a timer
The asynchronous adapter yields roughly every eight milliseconds. Each yield
waited for a zero-delay timer: about 1.1 ms in Bun, and at least 4 ms in
browsers once timers nest. Browsers now yield through a message-channel task.
Bun runs port messages ahead of timers and incoming worker messages, so a
message-channel yield there kept cancellation and the heartbeat from running
until the calculation ended. Bun therefore yields with `setImmediate`, which
costs about 0.01 ms and lets both run between slices.
| Scenario | Before (s) | After (s) |
| --------------------------- | ---------: | --------: |
| representative year | 0.37 | 0.31 |
| typed obligations | 4.74 | 4.02 |
| chained earned entitlements | 0.02 | 0.01 |
| deliberate conflict | 0.01 | 0.00 |
| loaded year | 53.80 | 46.90 |
| two years | 1.23 | 1.04 |
| three years | 3.86 | 3.21 |
The browser path was checked in headless Chromium 149 on the same machine, in a
dedicated worker with a 50 ms heartbeat timer, running the bundled core before
and after this step, alternating, twice each. The `three years` calculation took
3.92 and 3.89 seconds before and 2.70 and 2.66 seconds after; `representative
year` took 0.51 and 0.45 seconds before and 0.30 seconds after. The heartbeat's
largest gap stayed between 58 and 67 ms in every run. Cancelling `three years`
one second after the request returned the retained plan 3.9 to 4.0 ms after the
cancel message reached the worker, against 7.2 to 8.7 ms before. This is desktop
evidence, not a reference-phone measurement.