@W-23545283 native lib benchmarks#137
Open
andres-rad wants to merge 61 commits into
Open
Conversation
…runtime dep, no version skew)
…ng warmup guard + docs
…ers) Root :benchmarkCompare runs every task tagged ext.benchmarkRunner=true across subprojects, then renders the cross-runner comparison via report.mjs once. Engine (benchmarkEngine) and Node (new benchmarkNode, emit-only) are tagged; a new runner integrates itself by writing a schema-conformant result file and setting the tag — no edit to the aggregator. Opt-in via -Pbenchmark=true. Documented the registration contract in benchmarks/README.md.
…ss runners
Cold-start previously timed only the in-process initialize() call in each
child, excluding process launch + library/class load — which erased exactly
the native-image-vs-JVM startup gap the metric exists to show (the engine
child even claimed to measure the full cold path while timing only
`new EngineShell()`).
Introduce a shared spawn protocol across Node, Python, and engine:
- child flushes a "READY" marker the instant its runtime is initialized, then
emits a JSON line with only the in-process firstRunMs
- parent stamps wall-clock just before spawn and again on receiving READY;
coldStart = t_ready - t0, now including process launch + load + init
Parents switch from buffered reads (execFileSync / check_output /
getLines.toList) to streaming reads so the clock can stop mid-run. First-run
stays measured in-process by the child. The engine parent now selects the
JSON line by startsWith("{") rather than last-line, hardening against a stray
merged-stderr line. Contract documented in benchmarks/README.md.
…rerequisites Broaden corpus coverage (6 -> 10 cases) with dimensions the DataWeave engine's own JMH suite exercises but ours did not: - csv-to-json: CSV as an input format (a distinct native reader we only wrote, never parsed) - group-by: allocation-heavy groupBy over the existing generated large input — the operation most represented in JMH and the best signal on the AOT-vs-JIT axis - xml-to-json: XML -> structured JSON - deep-selector: multi-level value-selector traversal Each is a script + committed input + manifest entry; both scripted runners pick them up via the shared manifest with no runner-code change. Verified end-to-end against the real dwlib (correct output; Node + Python emit 23 rows each). Also document the GraalVM (Java 21+, graalvm-community) prerequisite in benchmarks/README.md (Node/Python runners benchmark the staged native dwlib; engine runner alone needs only a plain JVM), add a Benchmarks section to the root README, and refresh the stale Java-11 GraalVM build instructions.
…nput ScalaTest runs all engine suites concurrently in one JVM. The warm test read records-large.json (gitignored, absent on fresh CI) without waiting for it, and ensureGeneratedInputs was an unsynchronized check-then-act, so siblings raced to regenerate it while a reader caught the non-atomic write mid-flight (NoSuchFileException on Windows CI). - Guard the warm test with ensureGeneratedInputs like streaming/emit. - Make ensureGeneratedInputs synchronized and require a non-empty file so concurrent suites serialize on generation and never accept a partial write.
…harts `report.mjs --markdown report/RESULTS.md` renders a self-contained report that GitHub displays inline: a provenance header (commit + run date, pulled from the result files' env.commit and latest timestamp), the numeric table, and Mermaid xychart-beta bar charts grouped by corpus case — one chart per (case, metric) with one bar per runner. A case's metrics differ in unit and scale (e.g. warm ms vs streaming MB/s), so each metric is its own single-unit chart under the case heading. Zero new dependencies (Mermaid renders natively on GitHub, no notebook/plot lib); all charts validated against the mermaid@11 parser. Commit a fresh RESULTS.md snapshot; it reflects one run on one machine, so regenerate to refresh. Adds report tests for the two new render functions.
andres-rad
force-pushed
the
W-23545283-native-lib-benchmarks
branch
from
July 24, 2026 15:51
1aa9f64 to
a8a64cc
Compare
Address the two pre-merge review findings: #1 Emit.sampleOnce spawned ~195 child JVMs and never closed the stdout Source, stdin pipe, or the process — leaking parent-side FDs until GC and risking "Too many open files" under macOS default ulimit -n 256. Now closes stdin immediately and closes the Source + destroys the process in a finally (try/finally, not scala.util.Using — 2.12). #2 report.mjs printed a "Δ vs baseline" for every metric including streaming, whose cross-runner numbers are not like-for-like (engine times full compile+write; native-lib runners time a chunked transform). Mark streaming non-comparable: delta is suppressed and rendered "n/a" (distinct from "—" for missing data), with a footnote explaining the methodology asymmetry. Regenerated RESULTS.md rows.
martincousido
previously approved these changes
Jul 24, 2026
…rite) + reject materialized result
…utStream/guard comments (review minors)
…k report The single "Δ vs engine" column compared engine against only the first non-engine runner (always node-wrapper), silently dropping python-wrapper's delta. Split into one "Δ <runner> vs engine" column per non-baseline runner so every runner is represented. Regenerate RESULTS.md.
Fresh engine/node/python results on weave 2.12.0-20260413 @ f4bca0d (engine on GraalVM jvm 21.0.11 this run). Both per-runner delta columns populated.
…n't hang The weave runtime starts non-daemon "data-weave" threads (backing the deferred streaming PipedInputStream) with no exposed shutdown, so the engine Emit JVM never exits once main returns — it hangs with the result already written. Because :benchmarkCompare runs runners sequentially, that stall blocks the node/python runners from ever starting. Exit explicitly from main after writing (mirroring native-cli's DWCLI), and exit non-zero on failure so a post-thread-start exception surfaces instead of hanging. The fix is in main(), not run(), so EmitTest's in-process call is unaffected. Verified: :benchmarks-engine:benchmarkEngine now completes (BUILD SUCCESSFUL, JVM exits) where it previously hung; all 25 engine unit tests still pass.
martincousido
approved these changes
Jul 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a language-agnostic benchmark harness for the DataWeave runtime under a new top-level
benchmarks/directory. It runs a shared corpus of DataWeave scripts through three runners — the Node and Python native-lib wrappers plus a JVM engine baseline — each emitting the same JSON schema, and renders a cross-runner comparison. It measures four metrics: cold-start, first-run (compile+exec), warm steady-state, and streaming throughput.W-23545283.
How it works
benchmarks/corpus/— the shared contract every runner consumes:manifest.json(each case has an immutableid, a.dwlscript, optional inputs, and an explicitmetrics[]), 10 scripts, committed inputs, and a deterministic large-input generator.benchmarks/schema/result.schema.json— the JSON schema all runners' output conforms to (envcarriesweaveVersion/commit/dwlibBuildIdfor attribution).benchmarks/lib/— dependency-free JS helpers:stats(percentiles + MB/s),manifest(load/validate + fail-fast orphan-id check),env(env stamp). Reference implementation the Python/engine runners claim parity against (parity pinned by always-on tests).benchmarks/runners/):node/— the reference runner: in-process warm/streaming (warm-bench), a fresh-process spawn harness for cold-start/first-run (coldstart), andemit(orchestrate → validate ids → write result JSON).python/— stdlib runner mirroring Node file-for-file over the same stageddwlib; bit-exact stats parity, no third-party deps.engine/— JVM baseline (:benchmarks-engineScala/Gradle subproject) driving the bareDataWeaveScriptingEngineat the sameweaveVersionthe native image is built from, so the node-vs-engine comparison isolates the native-image-vs-JVM axis. Includes a JIT-warmup floor.benchmarks/report/report.mjs— joins result files by caseid, renders a case×metric table with one Δ-vs-baseline column per non-baseline runner (so every runner is represented, not just the first) and a weave-version skew banner; dedupes to the latest result per runner.--emitis a reserved exporter seam.benchmarkCompare(root) auto-discovers every runner taggedext.benchmarkRunner, runs them, and renders the report once. Per-runner tasks (native-lib:benchmark/benchmarkNode,native-lib:benchmarkPython,benchmarks-engine:benchmarkEngine) also exist. All opt-in via-Pbenchmark=true, off by default (never in the normal build/test path). Stats/manifest/env parity tests are always-on so cross-runner drift fails the normal build.Design decisions
READYmarker the instant its runtime is initialized, and the parent records wall-clock from just-before-spawn to that marker. So cold-start includes process launch + library/class load + runtime init on all three runners — which is what makes the native-image-vs-JVM comparison meaningful (the native image has no JVM to boot; the JVM's cold cost is launch + classload).first-runstays measured in-process in the same spawn.hrtime.bigint()/perf_counter_ns/System.nanoTime()), not a bench library, so all three surfaces share the exact methodology for a fair comparison.map,groupBy, deep value selectors, a streaming case, and a compile-heavy case.env.commit/dwlibBuildId+ immutableid+ the--emitseam in place so it's a bolt-on.Testing
node --testacrossbenchmarks/**/*.test.mjs), including integration tests that drive the real native library; Python parity suite (17 tests) and engine scalatest suite (25 tests) pass.emiteach produce 23 schema-conformant rows over the 10-case corpus against the realdwlib; engineEngineChildTest/EmitTestdrive fresh JVM spawns. Numbers are reproducible run-over-run.Sample results
One local run, all three runners on the same
weaveVersion2.12.0-20260413(no skew), baseline = engine. Indicative only — single dev machine (Apple M4 Max; node v22, python 3.9, JVM 21), single run, not a dedicated bench box. Each non-baseline runner gets its own signed Δ-vs-engine column, per unit (ms lower-is-better, MB/s higher-is-better).📊
benchmarks/report/RESULTS.mdrenders this same run as bar charts grouped by corpus case (one bar per runner, Mermaid, inline on GitHub), stamped with the commit + run date. Regenerate withnode report/report.mjs results/*.json --markdown report/RESULTS.md.Reading it:
map-scale,json-stream,group-by) the JVM engine wins substantially, for two reasons worth noting rather than hiding: (1) the wrappers re-marshal the full input (base64 over the ~1 MB+ buffer) across the FFI inside the timed region every iteration, and (2) after a 2000-iteration warmup the engine's C2 JIT is fully hot. Treat these large-input warm deltas as "wrapper end-to-end call cost" not "runtime exec speed".runTransform, and the Δ is meaningful. The engine leads here (~−47% to −51%), reflecting native-image FFI overhead on the chunked path.Prerequisites
The Node/Python runners (and
benchmarkCompare) benchmark the staged nativedwlib, so they require the repo's GraalVM toolchain (Java 21+,graalvm-community, withnative-image); the engine runner alone runs on any JDK. Documented inbenchmarks/README.md, with a Benchmarks section added to the root README.Out of scope (follow-ups)
--emitseam is in place).Design specs and implementation plans are included under
docs/superpowers/.