Skip to content

Eliminate interpreter bridge for async/await in bytecode mode#110

Merged
frostney merged 4 commits intomainfrom
feature/native-async-await
Mar 23, 2026
Merged

Eliminate interpreter bridge for async/await in bytecode mode#110
frostney merged 4 commits intomainfrom
feature/native-async-await

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Mar 23, 2026

Summary

  • Replace AwaitValue() bridge call to Goccia.Evaluator.AwaitValue() with a native implementation in the runtime layer
  • Handle Promises, thenables, and microtask queue draining directly — no interpreter crossing needed
  • Keep the implementation in the runtime layer (not the VM dispatch loop) to avoid the icache pressure that caused PR Eliminate evaluator bridge for async/await #88's regression

The native implementation handles:

  • Primitives and heap strings: return as-is (zero cost)
  • TGocciaPromiseValue: check state directly; if pending, drain microtask queue and re-check; return result or rethrow rejection
  • Thenable objects (objects with callable .then): create Promise, call .then(resolve, reject) natively, drain queue, return settled result
  • Non-thenable objects: return as-is

Test plan

  • Interpreter: 3501 passed, 0 failed, 41 skipped
  • Bytecode: 3501 passed, 0 failed, 0 skipped
  • Async/await tests (Promise resolution, rejection, chaining, thenable adoption)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Restructured async operation handling to improve Promise settlement detection and microtask queue management, enhancing reliability and performance of await operations.

Replace AwaitValue() bridge call to Goccia.Evaluator.AwaitValue() with
a native implementation in the runtime layer that handles Promises,
thenables, and microtask queue draining directly — no interpreter
crossing needed.

The native implementation handles:
- Primitives and heap strings: return as-is (no bridge)
- TGocciaPromiseValue: check state, drain microtask queue if pending,
  return result or rethrow rejection
- Thenable objects: create Promise, call .then(resolve, reject) natively,
  drain queue, return settled result
- Non-thenable objects: return as-is

This keeps the implementation in the runtime layer (not the VM dispatch
loop) to avoid the icache pressure that caused PR #88's regression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 23, 2026

Warning

Rate limit exceeded

@frostney has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd4e9f82-7e8f-4f13-9be9-ff812026fe1b

📥 Commits

Reviewing files that changed from the base of the PR and between eb0c2e8 and 41953d9.

📒 Files selected for processing (1)
  • units/Goccia.Runtime.Operations.pas
📝 Walkthrough

Walkthrough

The TGocciaRuntimeOperations.AwaitValue method was refactored from a delegating implementation to a direct thenable bridge. It now detects promise-like values, manages garbage collection roots, drains microtask queues, and validates promise settlement before returning.

Changes

Cohort / File(s) Summary
Promise Handling Refactor
units/Goccia.Runtime.Operations.pas
Reworked AwaitValue method to implement direct thenable bridge with early returns for primitives, promise detection in wrapped values and objects, garbage collection root management, microtask queue draining, and promise settlement validation. Removed bridge metrics increments.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A promise now settled with care so true,
The microtasks drained, the promises through,
With roots held fast in the collector's keep,
No thenable left in the heap to creep,
The await grows strong, the bridge complete! 🌟

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing an interpreter bridge call with native runtime-layer implementation for async/await in bytecode mode.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/native-async-await

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…Value)

Plain objects like { then(resolve) { resolve(42) } } are TSouffleRecord
in bytecode mode, not TGocciaWrappedValue. The native AwaitValue only
checked for thenables inside TGocciaWrappedValue, missing this case.

Add TSouffleRecord thenable detection: look up 'then' property via
GetProperty, check if it's a callable (TSouffleClosure, NativeFunction,
or BridgedFunction), and invoke it with resolve/reject callbacks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 23, 2026

Suite Timing

Suite Metric Interpreted Bytecode
Tests Total 3501 3501
Tests Passed 3460 ✅ 3501 ✅
Tests Skipped 41 0
Tests Execution 163.5ms 169.0ms
Tests Engine 315.6ms 594.7ms
Benchmarks Total 254 254
Benchmarks Duration 6.95min 12.36min

Measured on ubuntu-latest x64.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 23, 2026

Benchmark Results

254 benchmarks

Interpreted: 🟢 1 improved · 🔴 27 regressed · 226 unchanged · avg -2.4%
Bytecode: 🟢 21 improved · 233 unchanged · avg +2.4%

arraybuffer.js — Interp: 🟢 1, 13 unch. · avg +1.5% · Bytecode: 🟢 1, 13 unch. · avg +2.4%
Benchmark Interpreted Δ Bytecode Δ
create ArrayBuffer(0) 453,591 → 443,831 -2.2% 165,313 → 166,261 +0.6%
create ArrayBuffer(64) 442,559 → 432,324 -2.3% 164,874 → 165,473 +0.4%
create ArrayBuffer(1024) 334,738 → 330,816 -1.2% 144,939 → 147,085 +1.5%
create ArrayBuffer(8192) 144,548 → 142,033 -1.7% 85,856 → 85,947 +0.1%
slice full buffer (64 bytes) 495,899 → 518,785 +4.6% 387,347 → 394,962 +2.0%
slice half buffer (512 of 1024 bytes) 427,110 → 446,654 +4.6% 337,912 → 346,327 +2.5%
slice with negative indices 423,805 → 455,877 🟢 +7.6% 363,839 → 374,983 +3.1%
slice empty range 485,828 → 513,675 +5.7% 382,576 → 389,825 +1.9%
byteLength access 1,562,723 → 1,538,752 -1.5% 1,227,637 → 1,246,758 +1.6%
Symbol.toStringTag access 1,166,642 → 1,174,499 +0.7% 680,390 → 699,818 +2.9%
ArrayBuffer.isView 805,855 → 814,381 +1.1% 575,241 → 574,501 -0.1%
clone ArrayBuffer(64) 425,949 → 435,177 +2.2% 384,158 → 402,653 +4.8%
clone ArrayBuffer(1024) 323,984 → 329,149 +1.6% 279,398 → 300,910 🟢 +7.7%
clone ArrayBuffer inside object 267,398 → 273,067 +2.1% 188,825 → 197,058 +4.4%
arrays.js — Interp: 🔴 6, 13 unch. · avg -4.0% · Bytecode: 19 unch. · avg +0.9%
Benchmark Interpreted Δ Bytecode Δ
Array.from length 100 13,721 → 13,630 -0.7% 15,209 → 15,229 +0.1%
Array.from 10 elements 233,492 → 232,577 -0.4% 181,897 → 186,397 +2.5%
Array.of 10 elements 319,314 → 321,030 +0.5% 248,771 → 257,830 +3.6%
spread into new array 333,371 → 319,744 -4.1% 879,531 → 879,316 -0.0%
map over 50 elements 27,838 → 25,526 🔴 -8.3% 25,729 → 25,713 -0.1%
filter over 50 elements 23,541 → 21,962 -6.7% 25,317 → 25,327 +0.0%
reduce sum 50 elements 27,074 → 24,047 🔴 -11.2% 21,993 → 21,873 -0.5%
forEach over 50 elements 23,456 → 21,334 🔴 -9.0% 27,911 → 27,963 +0.2%
find in 50 elements 35,843 → 32,538 🔴 -9.2% 32,683 → 33,152 +1.4%
sort 20 elements 12,053 → 11,762 -2.4% 13,230 → 13,093 -1.0%
flat nested array 114,473 → 114,142 -0.3% 481,483 → 497,011 +3.2%
flatMap 72,710 → 66,342 🔴 -8.8% 324,370 → 330,055 +1.8%
map inside map (5x5) 21,812 → 21,599 -1.0% 106,786 → 107,581 +0.7%
filter inside map (5x10) 16,057 → 16,086 +0.2% 15,229 → 15,380 +1.0%
reduce inside map (5x10) 19,693 → 19,311 -1.9% 15,485 → 15,649 +1.1%
forEach inside forEach (5x10) 17,112 → 16,882 -1.3% 17,501 → 17,585 +0.5%
find inside some (10x10) 14,105 → 14,126 +0.1% 12,463 → 12,573 +0.9%
map+filter chain nested (5x20) 5,516 → 5,469 -0.9% 5,097 → 5,164 +1.3%
reduce flatten (10x5) 39,368 → 35,493 🔴 -9.8% 6,797 → 6,878 +1.2%
async-await.js — Interp: 6 unch. · avg -2.4% · Bytecode: 6 unch. · avg -1.1%
Benchmark Interpreted Δ Bytecode Δ
single await 379,230 → 367,949 -3.0% 305,464 → 296,401 -3.0%
multiple awaits 169,554 → 167,144 -1.4% 128,965 → 123,977 -3.9%
await non-Promise value 847,710 → 809,896 -4.5% 1,009,314 → 1,008,798 -0.1%
await with try/catch 368,145 → 359,454 -2.4% 296,607 → 290,279 -2.1%
await Promise.all 50,289 → 50,395 +0.2% 46,243 → 48,645 +5.2%
nested async function call 191,625 → 185,349 -3.3% 226,412 → 220,471 -2.6%
classes.js — Interp: 31 unch. · avg -1.5% · Bytecode: 31 unch. · avg +2.3%
Benchmark Interpreted Δ Bytecode Δ
simple class new 138,047 → 135,145 -2.1% 400,746 → 410,576 +2.5%
class with defaults 108,460 → 106,719 -1.6% 283,076 → 285,336 +0.8%
50 instances via Array.from 5,598 → 5,244 -6.3% 7,282 → 7,510 +3.1%
instance method call 66,964 → 66,125 -1.3% 179,565 → 187,767 +4.6%
static method call 111,576 → 111,069 -0.5% 395,505 → 421,109 +6.5%
single-level inheritance 54,302 → 53,751 -1.0% 171,555 → 178,120 +3.8%
two-level inheritance 46,521 → 46,115 -0.9% 139,505 → 144,875 +3.8%
private field access 70,778 → 69,957 -1.2% 191,985 → 199,734 +4.0%
private methods 77,924 → 77,044 -1.1% 244,889 → 260,136 +6.2%
getter/setter access 75,099 → 74,761 -0.5% 188,106 → 191,186 +1.6%
class decorator (identity) 91,440 → 90,155 -1.4% 62,336 → 63,678 +2.2%
class decorator (wrapping) 53,912 → 52,399 -2.8% 41,996 → 39,392 -6.2%
identity method decorator 63,656 → 62,609 -1.6% 49,141 → 49,696 +1.1%
wrapping method decorator 51,456 → 51,042 -0.8% 44,393 → 45,897 +3.4%
stacked method decorators (x3) 34,788 → 34,556 -0.7% 29,745 → 30,117 +1.3%
identity field decorator 70,350 → 69,600 -1.1% 51,348 → 51,116 -0.5%
field initializer decorator 59,270 → 57,475 -3.0% 45,442 → 45,510 +0.2%
getter decorator (identity) 64,159 → 63,648 -0.8% 46,198 → 46,516 +0.7%
setter decorator (identity) 54,405 → 54,080 -0.6% 39,518 → 40,113 +1.5%
static method decorator 69,777 → 69,515 -0.4% 70,252 → 73,557 +4.7%
static field decorator 81,148 → 80,184 -1.2% 73,509 → 76,095 +3.5%
private method decorator 53,144 → 52,007 -2.1% 42,387 → 42,846 +1.1%
private field decorator 58,327 → 57,612 -1.2% 43,271 → 44,099 +1.9%
plain auto-accessor (no decorator) 102,111 → 100,886 -1.2% 61,565 → 62,328 +1.2%
auto-accessor with decorator 54,047 → 53,460 -1.1% 40,850 → 41,614 +1.9%
decorator writing metadata 45,199 → 44,115 -2.4% 45,407 → 46,348 +2.1%
static getter read 128,285 → 124,541 -2.9% 425,043 → 433,405 +2.0%
static getter/setter pair 99,677 → 100,032 +0.4% 225,011 → 236,579 +5.1%
inherited static getter 77,885 → 75,391 -3.2% 293,597 → 295,976 +0.8%
inherited static setter 83,767 → 82,852 -1.1% 226,335 → 232,459 +2.7%
inherited static getter with this binding 71,717 → 70,427 -1.8% 174,406 → 181,723 +4.2%
closures.js — Interp: 11 unch. · avg -0.9% · Bytecode: 11 unch. · avg +3.2%
Benchmark Interpreted Δ Bytecode Δ
closure over single variable 131,207 → 129,091 -1.6% 785,556 → 825,680 +5.1%
closure over multiple variables 121,786 → 120,440 -1.1% 481,274 → 498,869 +3.7%
nested closures 129,108 → 125,980 -2.4% 707,981 → 730,102 +3.1%
function as argument 95,973 → 96,132 +0.2% 717,822 → 727,394 +1.3%
function returning function 120,436 → 119,671 -0.6% 779,194 → 808,223 +3.7%
compose two functions 72,197 → 72,028 -0.2% 470,063 → 491,941 +4.7%
fn.call 153,133 → 151,083 -1.3% 150,731 → 150,181 -0.4%
fn.apply 109,272 → 111,006 +1.6% 97,381 → 103,318 +6.1%
fn.bind 135,350 → 136,125 +0.6% 148,307 → 150,178 +1.3%
recursive sum to 50 11,816 → 11,563 -2.1% 52,132 → 52,759 +1.2%
recursive tree traversal 19,848 → 19,366 -2.4% 75,176 → 78,829 +4.9%
collections.js — Interp: 🔴 2, 10 unch. · avg -3.2% · Bytecode: 🟢 1, 11 unch. · avg +1.2%
Benchmark Interpreted Δ Bytecode Δ
add 50 elements 7,122 → 6,948 -2.4% 5,991 → 6,025 +0.6%
has lookup (50 elements) 90,502 → 91,243 +0.8% 90,558 → 89,870 -0.8%
delete elements 48,125 → 48,513 +0.8% 38,497 → 38,778 +0.7%
forEach iteration 15,891 → 14,426 🔴 -9.2% 17,283 → 17,447 +0.9%
spread to array 29,057 → 27,712 -4.6% 155,273 → 154,779 -0.3%
deduplicate array 39,376 → 38,447 -2.4% 51,523 → 51,893 +0.7%
set 50 entries 5,433 → 5,144 -5.3% 6,148 → 6,185 +0.6%
get lookup (50 entries) 88,217 → 87,172 -1.2% 97,954 → 98,576 +0.6%
has check 132,587 → 130,877 -1.3% 154,813 → 157,015 +1.4%
delete entries 46,410 → 46,075 -0.7% 36,799 → 37,208 +1.1%
forEach iteration 15,964 → 14,654 🔴 -8.2% 17,369 → 17,517 +0.9%
keys/values/entries 7,836 → 7,509 -4.2% 21,187 → 22,900 🟢 +8.1%
destructuring.js — Interp: 🔴 9, 13 unch. · avg -5.1% · Bytecode: 🟢 2, 20 unch. · avg +2.7%
Benchmark Interpreted Δ Bytecode Δ
simple array destructuring 399,474 → 389,979 -2.4% 1,040,394 → 1,079,997 +3.8%
with rest element 274,170 → 271,024 -1.1% 762,139 → 798,187 +4.7%
with defaults 414,446 → 416,127 +0.4% 962,120 → 994,309 +3.3%
skip elements 421,775 → 411,669 -2.4% 1,200,510 → 1,211,378 +0.9%
nested array destructuring 170,777 → 171,125 +0.2% 517,484 → 536,665 +3.7%
swap variables 515,910 → 488,292 -5.4% 1,434,561 → 1,424,615 -0.7%
simple object destructuring 319,872 → 300,669 -6.0% 636,494 → 627,250 -1.5%
with defaults 365,041 → 348,166 -4.6% 341,599 → 346,984 +1.6%
with renaming 323,404 → 319,920 -1.1% 735,396 → 720,180 -2.1%
nested object destructuring 147,370 → 142,925 -3.0% 288,402 → 291,081 +0.9%
rest properties 195,444 → 186,497 -4.6% 242,615 → 245,483 +1.2%
object parameter 95,847 → 93,703 -2.2% 212,938 → 216,017 +1.4%
array parameter 124,131 → 122,588 -1.2% 428,989 → 456,069 +6.3%
mixed destructuring in map 36,287 → 33,280 🔴 -8.3% 38,551 → 37,963 -1.5%
forEach with array destructuring 66,378 → 61,191 🔴 -7.8% 196,078 → 203,562 +3.8%
map with array destructuring 66,813 → 61,197 🔴 -8.4% 233,693 → 265,681 🟢 +13.7%
filter with array destructuring 69,899 → 63,749 🔴 -8.8% 296,881 → 302,849 +2.0%
reduce with array destructuring 75,506 → 68,404 🔴 -9.4% 258,849 → 290,186 🟢 +12.1%
map with object destructuring 81,193 → 73,088 🔴 -10.0% 80,546 → 82,321 +2.2%
map with nested destructuring 65,691 → 60,789 🔴 -7.5% 69,402 → 69,403 +0.0%
map with rest in destructuring 38,694 → 34,934 🔴 -9.7% 24,394 → 24,895 +2.1%
map with defaults in destructuring 60,813 → 55,745 🔴 -8.3% 41,655 → 41,780 +0.3%
fibonacci.js — Interp: 8 unch. · avg -2.5% · Bytecode: 8 unch. · avg +2.2%
Benchmark Interpreted Δ Bytecode Δ
recursive fib(15) 326 → 319 -2.1% 1,466 → 1,478 +0.9%
recursive fib(20) 29 → 29 -0.1% 132 → 134 +1.1%
recursive fib(15) typed 330 → 326 -1.4% 1,944 → 1,982 +2.0%
recursive fib(20) typed 29 → 29 -0.3% 175 → 179 +2.2%
iterative fib(20) via reduce 12,353 → 11,962 -3.2% 9,810 → 9,944 +1.4%
iterator fib(20) 9,490 → 9,497 +0.1% 16,921 → 17,654 +4.3%
iterator fib(20) via Iterator.from + take 15,275 → 14,254 -6.7% 18,864 → 19,108 +1.3%
iterator fib(20) last value via reduce 11,640 → 10,874 -6.6% 13,864 → 14,468 +4.4%
for-of.js — Interp: 7 unch. · avg -1.3% · Bytecode: 🟢 1, 6 unch. · avg +2.7%
Benchmark Interpreted Δ Bytecode Δ
for...of with 10-element array 44,931 → 45,022 +0.2% 168,017 → 169,653 +1.0%
for...of with 100-element array 5,167 → 5,142 -0.5% 24,559 → 24,838 +1.1%
for...of with string (10 chars) 33,333 → 32,683 -2.0% 128,433 → 136,280 +6.1%
for...of with Set (10 elements) 45,683 → 45,252 -0.9% 178,072 → 178,551 +0.3%
for...of with Map entries (10 entries) 29,380 → 28,891 -1.7% 46,238 → 49,548 🟢 +7.2%
for...of with destructuring 39,081 → 38,464 -1.6% 79,251 → 80,424 +1.5%
for-await-of with sync array 44,487 → 43,338 -2.6% 137,578 → 139,807 +1.6%
iterators.js — Interp: 🔴 6, 14 unch. · avg -5.6% · Bytecode: 🟢 3, 17 unch. · avg +3.3%
Benchmark Interpreted Δ Bytecode Δ
Iterator.from({next}).toArray() — 20 elements 15,431 → 14,272 🔴 -7.5% 18,693 → 19,035 +1.8%
Iterator.from({next}).toArray() — 50 elements 6,751 → 6,309 -6.5% 8,390 → 8,594 +2.4%
spread pre-wrapped iterator — 20 elements 11,490 → 11,368 -1.1% 17,589 → 18,905 🟢 +7.5%
Iterator.from({next}).forEach — 50 elements 4,724 → 4,419 -6.5% 5,847 → 6,138 +5.0%
Iterator.from({next}).reduce — 50 elements 4,734 → 4,473 -5.5% 5,587 → 5,936 +6.2%
wrap array iterator 174,872 → 172,415 -1.4% 122,532 → 119,124 -2.8%
wrap plain {next()} object 10,767 → 10,128 -5.9% 14,465 → 14,294 -1.2%
map + toArray (50 elements) 4,853 → 4,558 -6.1% 6,305 → 6,443 +2.2%
filter + toArray (50 elements) 4,814 → 4,400 🔴 -8.6% 6,116 → 6,365 +4.1%
take(10) + toArray (50 element source) 28,250 → 25,969 🔴 -8.1% 32,094 → 32,912 +2.5%
drop(40) + toArray (50 element source) 6,807 → 6,278 🔴 -7.8% 9,136 → 10,052 🟢 +10.0%
chained map + filter + take (100 element source) 8,738 → 8,542 -2.2% 10,434 → 10,795 +3.5%
some + every (50 elements) 2,756 → 2,571 -6.7% 3,765 → 3,867 +2.7%
find (50 elements) 6,053 → 5,639 -6.8% 7,558 → 8,367 🟢 +10.7%
array.values().map().filter().toArray() 9,013 → 8,406 -6.7% 10,253 → 10,563 +3.0%
array.values().take(5).toArray() 210,592 → 207,422 -1.5% 150,498 → 152,622 +1.4%
array.values().drop(45).toArray() 193,857 → 194,604 +0.4% 146,926 → 145,393 -1.0%
map.entries() chained helpers 10,788 → 10,012 🔴 -7.2% 5,549 → 5,757 +3.7%
set.values() chained helpers 18,468 → 17,320 -6.2% 22,158 → 22,601 +2.0%
string iterator map + toArray 14,341 → 12,910 🔴 -10.0% 22,711 → 23,161 +2.0%
json.js — Interp: 20 unch. · avg -2.6% · Bytecode: 🟢 4, 16 unch. · avg +3.2%
Benchmark Interpreted Δ Bytecode Δ
parse simple object 171,473 → 165,343 -3.6% 150,126 → 147,962 -1.4%
parse nested object 104,241 → 101,419 -2.7% 98,864 → 100,643 +1.8%
parse array of objects 55,742 → 53,171 -4.6% 53,645 → 53,286 -0.7%
parse large flat object 50,963 → 48,428 -5.0% 45,207 → 44,910 -0.7%
parse mixed types 71,187 → 69,886 -1.8% 64,766 → 64,602 -0.3%
stringify simple object 205,871 → 195,371 -5.1% 161,872 → 179,124 🟢 +10.7%
stringify nested object 109,977 → 109,220 -0.7% 89,667 → 96,028 🟢 +7.1%
stringify array of objects 60,569 → 59,267 -2.1% 52,677 → 55,002 +4.4%
stringify mixed types 87,844 → 87,657 -0.2% 73,289 → 81,148 🟢 +10.7%
reviver doubles numbers 45,031 → 43,040 -4.4% 41,498 → 43,070 +3.8%
reviver filters properties 38,402 → 38,205 -0.5% 49,888 → 50,844 +1.9%
reviver on nested object 51,021 → 48,700 -4.5% 54,667 → 57,029 +4.3%
reviver on array 29,659 → 28,985 -2.3% 28,935 → 31,785 🟢 +9.8%
replacer function doubles numbers 48,474 → 47,067 -2.9% 54,522 → 52,999 -2.8%
replacer function excludes properties 61,080 → 59,016 -3.4% 65,783 → 68,491 +4.1%
array replacer (allowlist) 116,033 → 114,492 -1.3% 99,821 → 104,596 +4.8%
stringify with 2-space indent 95,717 → 95,680 -0.0% 89,804 → 87,378 -2.7%
stringify with tab indent 100,812 → 95,800 -5.0% 89,858 → 91,970 +2.4%
parse then stringify 52,787 → 52,387 -0.8% 49,857 → 51,800 +3.9%
stringify then parse 31,545 → 31,299 -0.8% 29,052 → 30,071 +3.5%
jsx.jsx — Interp: 21 unch. · avg -1.9% · Bytecode: 🟢 1, 20 unch. · avg +2.1%
Benchmark Interpreted Δ Bytecode Δ
simple element 211,375 → 210,773 -0.3% 691,654 → 732,946 +6.0%
self-closing element 220,726 → 216,787 -1.8% 772,703 → 761,746 -1.4%
element with string attribute 175,292 → 170,420 -2.8% 533,145 → 531,983 -0.2%
element with multiple attributes 157,850 → 151,852 -3.8% 465,447 → 448,493 -3.6%
element with expression attribute 164,999 → 163,732 -0.8% 517,281 → 520,057 +0.5%
text child 210,907 → 206,453 -2.1% 709,788 → 724,000 +2.0%
expression child 205,345 → 205,222 -0.1% 690,096 → 730,508 +5.9%
mixed text and expression 196,683 → 189,206 -3.8% 667,272 → 659,586 -1.2%
nested elements (3 levels) 78,503 → 77,050 -1.8% 268,958 → 286,971 +6.7%
sibling children 58,492 → 57,426 -1.8% 206,504 → 210,755 +2.1%
component element 148,268 → 145,885 -1.6% 493,891 → 502,991 +1.8%
component with children 91,527 → 89,321 -2.4% 304,708 → 313,464 +2.9%
dotted component 124,185 → 123,435 -0.6% 367,337 → 386,439 +5.2%
empty fragment 226,367 → 214,454 -5.3% 777,726 → 789,349 +1.5%
fragment with children 57,793 → 57,494 -0.5% 202,219 → 217,819 🟢 +7.7%
spread attributes 108,418 → 109,956 +1.4% 124,255 → 126,427 +1.7%
spread with overrides 93,806 → 94,496 +0.7% 94,531 → 95,065 +0.6%
shorthand props 163,748 → 158,228 -3.4% 499,461 → 495,556 -0.8%
nav bar structure 27,143 → 26,590 -2.0% 89,202 → 91,267 +2.3%
card component tree 32,060 → 31,197 -2.7% 98,447 → 100,403 +2.0%
10 list items via Array.from 14,884 → 14,183 -4.7% 24,995 → 25,498 +2.0%
numbers.js — Interp: 🔴 1, 10 unch. · avg -0.4% · Bytecode: 🟢 2, 9 unch. · avg +3.3%
Benchmark Interpreted Δ Bytecode Δ
integer arithmetic 568,023 → 526,258 🔴 -7.4% 2,025,306 → 2,095,039 +3.4%
floating point arithmetic 562,983 → 559,673 -0.6% 2,313,759 → 2,297,229 -0.7%
number coercion 181,132 → 179,258 -1.0% 157,528 → 166,867 +5.9%
toFixed 100,406 → 102,179 +1.8% 228,651 → 226,248 -1.1%
toString 151,991 → 158,949 +4.6% 842,529 → 884,647 +5.0%
valueOf 224,050 → 221,614 -1.1% 1,279,855 → 1,309,362 +2.3%
toPrecision 140,422 → 146,233 +4.1% 461,069 → 459,557 -0.3%
Number.isNaN 327,994 → 321,661 -1.9% 213,851 → 217,849 +1.9%
Number.isFinite 319,007 → 314,179 -1.5% 214,791 → 214,019 -0.4%
Number.isInteger 319,639 → 319,006 -0.2% 221,864 → 240,437 🟢 +8.4%
Number.parseInt and parseFloat 253,938 → 249,624 -1.7% 167,738 → 188,384 🟢 +12.3%
objects.js — Interp: 🔴 1, 6 unch. · avg -4.0% · Bytecode: 7 unch. · avg +2.5%
Benchmark Interpreted Δ Bytecode Δ
create simple object 516,642 → 474,064 🔴 -8.2% 965,956 → 994,584 +3.0%
create nested object 231,626 → 220,810 -4.7% 413,480 → 427,484 +3.4%
create 50 objects via Array.from 9,629 → 9,167 -4.8% 8,830 → 9,040 +2.4%
property read 623,127 → 607,567 -2.5% 816,564 → 817,261 +0.1%
Object.keys 290,774 → 284,940 -2.0% 201,091 → 209,037 +4.0%
Object.entries 102,612 → 101,659 -0.9% 67,602 → 69,426 +2.7%
spread operator 182,637 → 173,142 -5.2% 203,392 → 207,019 +1.8%
promises.js — Interp: 12 unch. · avg -1.4% · Bytecode: 🟢 3, 9 unch. · avg +5.1%
Benchmark Interpreted Δ Bytecode Δ
Promise.resolve(value) 559,908 → 529,829 -5.4% 362,486 → 370,114 +2.1%
new Promise(resolve => resolve(value)) 190,293 → 185,532 -2.5% 162,198 → 163,184 +0.6%
Promise.reject(reason) 550,037 → 528,316 -3.9% 344,766 → 359,431 +4.3%
resolve + then (1 handler) 175,524 → 172,285 -1.8% 151,137 → 163,903 🟢 +8.4%
resolve + then chain (3 deep) 67,840 → 66,476 -2.0% 63,859 → 70,092 🟢 +9.8%
resolve + then chain (10 deep) 22,016 → 21,306 -3.2% 21,869 → 22,904 +4.7%
reject + catch + then 98,655 → 97,902 -0.8% 91,071 → 96,796 +6.3%
resolve + finally + then 83,228 → 82,438 -0.9% 77,530 → 80,497 +3.8%
Promise.all (5 resolved) 31,246 → 31,471 +0.7% 27,157 → 28,565 +5.2%
Promise.race (5 resolved) 33,140 → 33,380 +0.7% 28,685 → 30,425 +6.1%
Promise.allSettled (5 mixed) 26,256 → 26,193 -0.2% 23,128 → 24,783 🟢 +7.2%
Promise.any (5 mixed) 30,672 → 31,359 +2.2% 27,190 → 27,885 +2.6%
strings.js — Interp: 🔴 1, 10 unch. · avg -1.7% · Bytecode: 🟢 2, 9 unch. · avg +3.6%
Benchmark Interpreted Δ Bytecode Δ
string concatenation 422,770 → 408,846 -3.3% 380,012 → 409,081 🟢 +7.6%
template literal 749,637 → 684,722 🔴 -8.7% 733,152 → 752,496 +2.6%
string repeat 400,966 → 401,659 +0.2% 1,113,277 → 1,095,052 -1.6%
split and join 137,353 → 134,946 -1.8% 332,162 → 337,762 +1.7%
indexOf and includes 163,790 → 164,391 +0.4% 765,680 → 762,103 -0.5%
toUpperCase and toLowerCase 255,708 → 248,582 -2.8% 754,108 → 816,123 🟢 +8.2%
slice and substring 156,100 → 156,448 +0.2% 857,882 → 900,415 +5.0%
trim operations 190,429 → 184,114 -3.3% 673,313 → 719,181 +6.8%
replace and replaceAll 207,987 → 207,620 -0.2% 628,941 → 670,401 +6.6%
startsWith and endsWith 129,485 → 132,203 +2.1% 645,497 → 656,782 +1.7%
padStart and padEnd 196,643 → 193,431 -1.6% 695,369 → 704,840 +1.4%
typed-arrays.js — Interp: 🔴 1, 21 unch. · avg -1.0% · Bytecode: 🟢 1, 21 unch. · avg +1.3%
Benchmark Interpreted Δ Bytecode Δ
new Int32Array(0) 320,977 → 322,911 +0.6% 142,394 → 141,371 -0.7%
new Int32Array(100) 299,349 → 299,364 +0.0% 138,080 → 135,938 -1.6%
new Int32Array(1000) 169,806 → 170,509 +0.4% 76,905 → 81,679 +6.2%
new Float64Array(100) 265,139 → 269,313 +1.6% 118,194 → 132,208 🟢 +11.9%
Int32Array.from([...]) 185,653 → 178,523 -3.8% 168,261 → 164,216 -2.4%
Int32Array.of(1, 2, 3, 4, 5) 322,834 → 316,557 -1.9% 266,844 → 270,246 +1.3%
sequential write 100 elements 3,576 → 3,610 +0.9% 15,069 → 15,649 +3.9%
sequential read 100 elements 3,690 → 3,671 -0.5% 10,935 → 11,562 +5.7%
Float64Array write 100 elements 3,318 → 3,307 -0.3% 14,604 → 14,855 +1.7%
fill(42) 47,364 → 48,606 +2.6% 45,688 → 46,999 +2.9%
slice() 208,184 → 204,841 -1.6% 190,458 → 188,971 -0.8%
map(x => x * 2) 8,194 → 7,792 -4.9% 8,340 → 8,403 +0.8%
filter(x => x > 50) 8,321 → 7,595 🔴 -8.7% 8,727 → 8,775 +0.6%
reduce (sum) 7,898 → 7,766 -1.7% 7,292 → 7,289 -0.0%
sort() 178,812 → 176,527 -1.3% 160,394 → 154,755 -3.5%
indexOf() 449,273 → 449,259 -0.0% 378,690 → 380,498 +0.5%
reverse() 341,515 → 339,053 -0.7% 283,378 → 282,471 -0.3%
create view over existing buffer 407,627 → 416,197 +2.1% 156,934 → 156,417 -0.3%
subarray() 447,742 → 465,419 +3.9% 365,806 → 368,021 +0.6%
set() from array 600,336 → 595,787 -0.8% 279,207 → 284,849 +2.0%
for-of loop 4,977 → 4,763 -4.3% 20,944 → 21,140 +0.9%
spread into array 16,618 → 16,155 -2.8% 56,556 → 56,626 +0.1%

Measured on ubuntu-latest x64. Changes within ±7% are considered insignificant.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.Runtime.Operations.pas`:
- Around line 4181-4183: Replace the raw
ESouffleThrow.Create(SouffleString('await: Promise did not settle after
microtask drain')) throw with a call into Goccia.Values.ErrorHelper that
produces/raises a JavaScript TypeError (so the thrown value is a
TGocciaThrowValue wrapping a proper JS Error object); locate the current throw
site (the ESouffleThrow.Create + SouffleString usage) and call the appropriate
ErrorHelper TypeError helper (the project's ErrorHelper function that
creates/raises TypeError) with the same message so JS code sees an actual
TypeError instance.
- Around line 4126-4130: ThenVal is being checked against raw AsReference types
but non-native functions may be wrapped in TGocciaWrappedValue, so first unwrap
record ThenVal (e.g., if ThenVal is a TGocciaWrappedValue extract its inner
value) before testing callability; replace hardcoded 'then' string with the
PROP_THEN constant from Goccia.Constants.PropertyNames when calling GetProperty
and when doing the wrapped-object lookup (the earlier lookup at the other
location should also use PROP_THEN), and apply the same unwrap-and-test change
to the similar checks around the 4139-4141 block so
TSouffleClosure/TSouffleNativeFunction/TGocciaBridgedFunction checks operate on
the unwrapped value.
- Around line 4102-4106: The newly created TGocciaPromiseValue must be
temp-rooted immediately after each TGocciaPromiseValue.Create to prevent GC from
collecting it while constructing callbacks or calling Then; update the logic
around the TGocciaPromiseValue.Create sites that build
TGocciaNativeFunctionValue.Create(...) (the Promise/Then construction) to call
AddTempRoot(Promise) right after Create and only call RemoveTempRoot(Promise)
later if this routine actually added the root, and when triggering collection
use CollectIfNeeded(AProtect) so stack-held values are protected per guidelines;
apply the same fix for both creation blocks (the Promise at 4102 and the one at
4132) and ensure any subsequent calls that may allocate (UnwrapToGocciaValue,
TGocciaNativeFunctionValue.Create, and the user then invocation) occur while the
promise is rooted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd9e0176-7375-4b9d-b246-b450cab73de9

📥 Commits

Reviewing files that changed from the base of the PR and between f2e66e5 and eb0c2e8.

📒 Files selected for processing (1)
  • units/Goccia.Runtime.Operations.pas

Comment thread units/Goccia.Runtime.Operations.pas
Comment thread units/Goccia.Runtime.Operations.pas Outdated
Comment thread units/Goccia.Runtime.Operations.pas Outdated
frostney and others added 2 commits March 23, 2026 11:20
- Root synthetic Promise via AddTempRoot immediately after Create,
  before constructing callbacks or invoking then (prevents GC
  collection during allocations in that window)
- Unwrap record then property before testing callability to handle
  mixed-mode thenables wrapped in TGocciaWrappedValue
- Use PROP_THEN constant instead of hardcoded 'then' string
- Use ThrowTypeErrorMessage instead of raw ESouffleThrow for unsettled
  promise error (produces proper JS TypeError catchable from try/catch)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@frostney frostney merged commit 853b16a into main Mar 23, 2026
9 checks passed
@frostney frostney deleted the feature/native-async-await branch March 23, 2026 12:17
@frostney frostney added the performance Performance improvement label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant