feat(engine): add deterministic and custom host environments#968
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📝 WalkthroughWalkthroughThe PR adds an engine-owned host environment for JavaScript-visible time, time zones, and randomness. It supports deterministic profiles, custom JavaScript providers, child-realm stream derivation, CLI/configuration wiring, runtime integration, documentation, and end-to-end tests. ChangesEngine-owned host environment
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
Web Tooling Benchmark
18 pinned Web Tooling workloads; 18 workloads produced at least one Goccia sample. Raw results from 1 sample per workload; full stdout/stderr for failures and min/max/CV stay in the |
Suite TimingTest Runner (interpreted: 11,364 passed; bytecode: 11,364 passed)
MemoryGC rows aggregate the main thread plus all worker thread-local GCs. Test runner worker shutdown frees thread-local heaps in bulk; that shutdown reclamation is not counted as GC collections or collected objects.
Benchmarks (interpreted: 437; bytecode: 437)
MemoryGC rows aggregate the main thread plus all worker thread-local GCs. Benchmark runner performs explicit between-file collections, so collection and collected-object counts can be much higher than the test runner.
Boot
Empty-script ( Measured on ubuntu-latest x64. |
Benchmark Results437 benchmarks · PR vs same-runner Interpreted: 🟢 38 improved · 🔴 11 regressed · 388 unchanged · avg +1.2% Typical per-run noise (median variance): interpreted ±2.1%, bytecode ±2.5%. Deltas within noise overlap and read as unchanged. arraybuffer.js — Interp: 14 unch. · avg +1.8% · Bytecode: 🟢 5, 9 unch. · avg +2.9%
arrays.js — Interp: 🟢 3, 16 unch. · avg +0.8% · Bytecode: 🟢 1, 18 unch. · avg +1.1%
async-await.js — Interp: 🟢 2, 4 unch. · avg +3.5% · Bytecode: 6 unch. · avg +2.0%
async-generators.js — Interp: 2 unch. · avg +4.7% · Bytecode: 2 unch. · avg -0.8%
atomics.js — Interp: 6 unch. · avg -1.2% · Bytecode: 6 unch. · avg +1.9%
base64.js — Interp: 🟢 1, 9 unch. · avg +0.4% · Bytecode: 🟢 1, 9 unch. · avg +1.5%
classes.js — Interp: 31 unch. · avg +1.4% · Bytecode: 🟢 4, 🔴 1, 26 unch. · avg +1.1%
closures.js — Interp: 11 unch. · avg -0.2% · Bytecode: 🟢 3, 8 unch. · avg +2.4%
collections.js — Interp: 🟢 1, 11 unch. · avg -0.7% · Bytecode: 🟢 3, 9 unch. · avg +1.4%
csv.js — Interp: 13 unch. · avg -0.5% · Bytecode: 🟢 1, 12 unch. · avg +1.0%
destructuring.js — Interp: 🟢 1, 21 unch. · avg +1.5% · Bytecode: 🟢 4, 🔴 4, 14 unch. · avg -0.3%
fibonacci.js — Interp: 8 unch. · avg +0.2% · Bytecode: 🔴 1, 7 unch. · avg -2.8%
float16array.js — Interp: 🟢 1, 🔴 2, 29 unch. · avg +0.1% · Bytecode: 🟢 3, 🔴 1, 28 unch. · avg +1.6%
for-in/for-in.js — Interp: 3 unch. · avg +0.5% · Bytecode: 3 unch. · avg +3.5%
for-of.js — Interp: 7 unch. · avg -0.1% · Bytecode: 🟢 2, 5 unch. · avg +1.9%
generators.js — Interp: 🟢 1, 3 unch. · avg +3.8% · Bytecode: 4 unch. · avg +2.1%
intl.js — Interp: 🟢 2, 4 unch. · avg +1.3% · Bytecode: 🟢 1, 5 unch. · avg +2.3%
iterators.js — Interp: 🟢 6, 36 unch. · avg +3.0% · Bytecode: 🟢 7, 🔴 2, 33 unch. · avg +2.9%
json.js — Interp: 🟢 1, 22 unch. · avg +1.0% · Bytecode: 🟢 9, 🔴 2, 12 unch. · avg +3.9%
jsx.jsx — Interp: 🔴 2, 19 unch. · avg +0.4% · Bytecode: 🟢 5, 🔴 1, 15 unch. · avg +2.0%
modules.js — Interp: 9 unch. · avg +0.9% · Bytecode: 🔴 1, 8 unch. · avg +0.5%
numbers.js — Interp: 🔴 1, 11 unch. · avg -0.6% · Bytecode: 🟢 4, 🔴 2, 6 unch. · avg +3.1%
objects.js — Interp: 7 unch. · avg -2.5% · Bytecode: 🟢 1, 🔴 1, 5 unch. · avg +3.5%
promises.js — Interp: 🟢 2, 10 unch. · avg +3.5% · Bytecode: 🟢 4, 8 unch. · avg +5.6%
property-access.js — Interp: 5 unch. · avg +0.8% · Bytecode: 🟢 2, 3 unch. · avg +9.6%
regexp.js — Interp: 🟢 1, 10 unch. · avg +1.1% · Bytecode: 🟢 1, 🔴 2, 8 unch. · avg -0.7%
strings.js — Interp: 🟢 3, 🔴 1, 15 unch. · avg +1.2% · Bytecode: 🔴 3, 16 unch. · avg -0.3%
temporal.js — Interp: 6 unch. · avg +3.1% · Bytecode: 🟢 2, 4 unch. · avg -0.2%
tsv.js — Interp: 9 unch. · avg +1.9% · Bytecode: 🟢 2, 🔴 1, 6 unch. · avg +1.9%
typed-arrays.js — Interp: 🟢 5, 🔴 1, 16 unch. · avg -2.5% · Bytecode: 🟢 5, 🔴 2, 15 unch. · avg +6.1%
uint8array-encoding.js — Interp: 🟢 2, 🔴 2, 14 unch. · avg -6.2% · Bytecode: 🟢 2, 🔴 5, 11 unch. · avg -4.0%
weak-collections.js — Interp: 🟢 6, 🔴 2, 7 unch. · avg +17.6% · Bytecode: 🟢 3, 🔴 1, 11 unch. · avg +9.3%
Deterministic profile diffDeterministic profile diff: no significant changes. Measured on ubuntu-latest x64. Each PR run also builds the |
test262 Conformance
Areas closest to 100%
Per-test deltas (+31 / -0 / timeout +0 / -3)Newly passing (31):
Resolved timeouts (3):
Steady-state failures and timeouts are non-blocking; PASS → non-timeout failure transitions fail the conformance gate. Measured on ubuntu-latest x64, bytecode mode. Areas grouped by the first two test262 path components; minimum 25 attempted tests, areas already at 100% excluded. Δ vs main compares against the most recent cached |
JetStream 3 Performance Barometer
Geomean reference ratio: QuickJS 26.15×; Node.js 290.68×. 1.00× means aligned; values above 1.00× mean Goccia was proportionally slower after normalizing JetStream’s higher-is-better score. This is a directional barometer across runtimes with different goals, not a product ranking. Raw samples and failure details remain in the |
AWFY Results
Geomean Ratios
14 pinned AWFY benchmarks. Medians from 5 interleaved samples per engine; raw JSON includes min/max/CV and is attached as the |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
source/units/Goccia.HostEnvironment.Test.pas (1)
114-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest sibling stream uniqueness from the same parent.
Both children currently receive stream ID 1 from separate parents. Derive two children from one parent, then reproduce both positions from a second deterministic parent to cover the distinct-and-repeatable stream contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@source/units/Goccia.HostEnvironment.Test.pas` around lines 114 - 121, Update the test setup around ConfigureAsChildOf so ChildA and ChildB derive from the same deterministic parent, then create a second equivalent parent and reproduce both child positions from it. Assert the two sibling RandomDouble values are distinct while the corresponding values from the second parent match, preserving the existing deterministic expectations where applicable.source/units/Goccia.HostEnvironment.JavaScript.pas (1)
197-206: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
DeriveStreamIdimplements only a partial SplitMix64 finalizer.Canonical SplitMix64 does
add-increment → (z^(z>>30))*C1 → (z^(z>>27))*C2 → z^(z>>31)(two multiply-xorshift rounds plus a final xorshift). This implementation stops after the first multiply-xorshift round and omits both the second multiplication (0x94D049BB133111EB) and the finalxor (z shr 31), weakening avalanche/decorrelation between derived child stream IDs.♻️ Align with the canonical SplitMix64 finalizer
+const + STREAM_HASH_MULTIPLIER_2 = QWord($94D049BB133111EB); + class function TGocciaJavaScriptHostRandom.DeriveStreamId( const AParentStreamId, AChildStreamId: QWord): QWord; begin Result := (AParentStreamId xor STREAM_HASH_INCREMENT) + AChildStreamId; Result := (Result xor (Result shr 30)) * STREAM_HASH_MULTIPLIER; - Result := Result xor (Result shr 27); + Result := (Result xor (Result shr 27)) * STREAM_HASH_MULTIPLIER_2; + Result := Result xor (Result shr 31); end;Based on the reference SplitMix64 implementation (
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; z = (z ^ (z >> 27)) * 0x94d049bb133111eb; return z ^ (z >> 31);).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@source/units/Goccia.HostEnvironment.JavaScript.pas` around lines 197 - 206, Update TGocciaJavaScriptHostRandom.DeriveStreamId to implement the complete SplitMix64 finalizer: retain the increment and first multiply-xorshift, then add the second multiply-xorshift using 0x94D049BB133111EB and finish with xor against Result shifted right by 31. Preserve the existing overflow-check suppression and QWord return behavior.README.md (1)
105-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMention the
--host-environment/--deterministicmutual exclusivity.
GocciaScriptLoaderrejects combining--host-environmentwith--deterministicat the config-resolution step. This section documents both flags side by side without noting they're mutually exclusive, so a reader could try to combine them and hit a parse error with no warning here.📝 Suggested addition
For custom providers, pass a JavaScript module to `--host-environment` or implement the Pascal host interfaces. See [Host Environment](docs/host-environment.md) for both examples and the provider contract. + +`--host-environment` and `--deterministic` are mutually exclusive; deterministic mode always uses the pinned providers.This may already be covered in
docs/host-environment.md, which isn't in this review batch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 105 - 116, Update the “Reproduce An Execution” section to state that --host-environment and --deterministic are mutually exclusive and cannot be combined. Clarify that GocciaScriptLoader rejects this combination during configuration resolution, while preserving the existing guidance for using either deterministic defaults or custom host providers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@source/units/Goccia.HostEnvironment.JavaScript.pas`:
- Around line 36-65: Root the provider callables before assigning them to the
fields in TGocciaJavaScriptHostClock and TGocciaJavaScriptHostRandom. Update
their constructors and lifetime management so each stored TGocciaValue remains
in the GC root set for as long as the host object may invoke it, and release the
roots when the corresponding object is destroyed.
In `@source/units/Goccia.HostEnvironment.pas`:
- Around line 170-176: The NextDouble method currently multiplies RandomBits as
Int64 directly by RANDOM_DOUBLE_SCALE, which can produce target-dependent
results. Convert RandomBits with Int64ToDouble before multiplying by
RANDOM_DOUBLE_SCALE, leaving the seeded state and mixing logic unchanged.
---
Nitpick comments:
In `@README.md`:
- Around line 105-116: Update the “Reproduce An Execution” section to state that
--host-environment and --deterministic are mutually exclusive and cannot be
combined. Clarify that GocciaScriptLoader rejects this combination during
configuration resolution, while preserving the existing guidance for using
either deterministic defaults or custom host providers.
In `@source/units/Goccia.HostEnvironment.JavaScript.pas`:
- Around line 197-206: Update TGocciaJavaScriptHostRandom.DeriveStreamId to
implement the complete SplitMix64 finalizer: retain the increment and first
multiply-xorshift, then add the second multiply-xorshift using
0x94D049BB133111EB and finish with xor against Result shifted right by 31.
Preserve the existing overflow-check suppression and QWord return behavior.
In `@source/units/Goccia.HostEnvironment.Test.pas`:
- Around line 114-121: Update the test setup around ConfigureAsChildOf so ChildA
and ChildB derive from the same deterministic parent, then create a second
equivalent parent and reproduce both child positions from it. Assert the two
sibling RandomDouble values are distinct while the corresponding values from the
second parent match, preserving the existing deterministic expectations where
applicable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 5e57d7dc-8119-44af-b4d2-5ba0b9230093
📒 Files selected for processing (30)
CONTEXT.mdREADME.mddocs/adr/0093-engine-owned-host-environment.mddocs/adr/README.mddocs/architecture.mddocs/build-system.mddocs/embedding.mddocs/host-environment.mdexamples/custom-host-environment.jsscripts/test-cli-apps.tsscripts/test-cli-config.tsscripts/test-cli.tssource/app/Goccia.CLI.Application.passource/app/Goccia.CLI.Options.passource/app/GocciaSandboxRunner.dprsource/app/GocciaScriptLoader.dprsource/app/GocciaScriptLoaderBare.dprsource/units/Goccia.Builtins.GlobalShadowRealm.passource/units/Goccia.Builtins.Intl.passource/units/Goccia.Builtins.Math.passource/units/Goccia.Builtins.Performance.passource/units/Goccia.Builtins.Temporal.passource/units/Goccia.Engine.passource/units/Goccia.HostEnvironment.JavaScript.passource/units/Goccia.HostEnvironment.Test.passource/units/Goccia.HostEnvironment.passource/units/Goccia.RuntimeExtensions.Performance.passource/units/Goccia.Utils.passource/units/Goccia.Values.ArrayValue.passource/units/Goccia.Values.IntlDateTimeFormat.pas
💤 Files with no reviewable changes (1)
- source/units/Goccia.Values.ArrayValue.pas
…ministic-execution
Route seeded random bits through the shared Int64 conversion helper so Free Pascal produces the same sequence across supported architectures.
Summary
TGocciaHostEnvironmentwith injectable clock and random providers, plus a portable SplitMix64 implementation.--deterministicand"deterministic": true: JavaScript-visible epoch and monotonic time are fixed at0, the explicit time-zone override is UTC, and the random seed is unsigned 64-bit0.GocciaScriptLoader --host-environment=<module>and the equivalent config key. Provider modules export callableepochNanoseconds,monotonicNanoseconds,timeZoneIdentifier, andrandomfunctions and load before the runtime profile capturesperformance.timeOrigin.Math.random, Date/Temporal.Now, implicitIntl.DateTimeFormattime, andperformancethrough the host environment. Child ShadowRealm, Test262, and sandbox executions receive stable distinct RNG streams.Intl.DateTimeFormatremains UTC. This fixes the interpreter, bytecode, and Test262 regression caused by Linux reportingEtc/UTC.performancemidpoint and nanosecond conversion arithmetic for realistic custom epochs, and use the shared portableInt64ToDoubleconversion for seeded random values.Testing
./build/GocciaTestRunner tests --output=compact-json— 11,364/11,364 passed./build/GocciaTestRunner tests --mode=bytecode --output=compact-json— 11,364/11,364 passedbun scripts/test-cli-config.tsGoccia.gc()in interpreted and bytecode modesGoccia.HostEnvironment.Test— 5/5 passed on macOS arm64