Skip to content

v3.2.0

Choose a tag to compare

@windischb windischb released this 24 Apr 17:28
· 30 commits to develop since this release

ES-module execution now follows standard JS semantics — top-level code runs once per unique script on a given engine, exports are cached. Unlocks a ~115× speed-up for repeated import-based scripts on pooled engines. New PrepareModule API mirrors the existing Prepare for ES modules.

Added

  • JsEngine.PrepareModule(string) → JsPreparedModule — pre-parse an ES module script once, reuse across calls. Thread-safe, cacheable globally.
  • JsEngine.ExecuteAsync(JsPreparedModule) — overload accepting a pre-parsed module. Cached by reference identity.

Changed (behavioural)

  • ExecuteAsync(string) / ExecuteAsync(JsPreparedModule) follow standard ES-module semantics. Top-level code runs once per unique script content per engine. Repeated calls return the cached module namespace instead of re-parsing + re-evaluating. Matches Node / browsers / Deno.
  • Migration: scripts relying on top-level re-execution (e.g. export const id = Math.random() yielding a new id per call) must expose per-call work as exported functions invoked via InvokeFunction. For true per-call re-execution, use the lightweight path (Evaluate / EvaluateAsync) which has no module system.
  • Module registry memory leak fixed — the previous __main_0__, __main_1__, … naming accumulated one entry per call in Jint's module dict. Now O(unique scripts) instead of O(calls).

Fixed

  • Benchmark harness — scoped JsEngine was resolved from the root provider + disposed after each iteration, so every iteration after the first hit ObjectDisposedException on async paths. All benchmarks now open a fresh DI scope per iteration. Side effect: EngineBenchmarks.AsyncAwait and ValueBenchmarks.TaskInterop now produce real numbers instead of NA; "Engine creation (cold start)" now measures ~9.8 µs instead of the ~1.5 µs DI cache lookup.

Performance (pooled engine, hot loop)

Scenario Before After Factor
ExecuteAsync(string) — repeated same script 14 µs 123 ns 115×
ExecuteAsync(prepared) — repeated 12 µs 1.3 µs 9×

Fresh-engine paths unchanged (nothing to cache on first use). Full table in PERFORMANCE-COMPARISON.md.

Example

// Prepare once (cacheable, thread-safe)
var preparedModule = JsEngine.PrepareModule("""
    import * as common from 'common';
    export function newGuid() { return common.Guid.New().toString(); }
""");

// Per request (pooled engine via scoped DI)
await engine.ExecuteAsync(preparedModule);   // 35 µs first call, 1.3 µs subsequent
var id = engine.InvokeFunction("newGuid");   // fresh GUID each call — as intended

Tests: 329 green (Engine 148, Linq 134, TypeScript 29, Modules 18).