Skip to content

fix: JavaScript extensions scale to a large corpus - #1319

Merged
alandefreitas merged 9 commits into
cppalliance:developfrom
gennaroprota:fix/js_extensions_scale_to_a_large_corpus
Sep 24, 2026
Merged

alandefreitas merged 9 commits into
cppalliance:developfrom
gennaroprota:fix/js_extensions_scale_to_a_large_corpus

Conversation

@gennaroprota

Copy link
Copy Markdown
Collaborator

A JavaScript extension could not read the corpus of a large project. Reading ctx.corpus.symbols was enough to cause the process to die with an access violation and no diagnostic. Two bugs were behind it, one in how the interpreter is set up and one in how an array reaches it, and a third change makes the limit the script ran into visible.

Changes

  • Source:
    • jerry_port_context_alloc now returns the allocated size, not a pointer to the allocated block. jerry_init asks the port for a block to hold the context and the heap, and takes the size of that block from what the call returns. We returned a pointer, instead, so the engine read an address as a length, believed it had a much larger heap than it had, and wrote past the 512 KiB it really had.
    • Running out of JavaScript heap says so. The engine's default behavior on a fatal condition is to end the process with a bare failure code. Our jerry_port_fatal now says what happened and where the limit is, so the author of the script can see what they ran into.
    • A JavaScript extension can read a corpus of any size. An array was converted to JavaScript element by element, up front. Each element that is an object cost an interpreter object of its own, and the interpreter's heap is a fixed 512 KiB that a build cannot simply enlarge, since 16-bit compressed pointers cap it there, so a few thousand symbols filled it. An array now reaches JavaScript through a proxy, as an object already did, and an element is converted when a script reads it, so what a script holds has to fit rather than the whole corpus.
  • Tests: A unit test reads an array far larger than the heap could hold as JavaScript objects, and checks its length and the sum of its elements.
  • Breaking changes: The heap limit is now enforced. A script that went past it could appear to work while writing into memory that was not the interpreter's; it now stops with the out-of-memory message.

Testing

The new unit test in tests/unit/Engines/JavaScript.cpp covers the lazy conversion. On develop, it crashes with an access violation; with the first two commits and not the third, it ends with the out-of-memory message; with all three it passes. The existing unit tests that pin the snapshot semantics of a converted array, including that a script's writes do not reach the DOM, pass unchanged.

The out-of-memory message itself ends the process, so no test asserts it in-process; it was checked by running the test above without the third commit.

Our own schema generator, docs/mrdocs/extensions/schema.js, now runs within the enforced heap for the first time. Run as the schema-check test runs it, it still produces artifacts byte-identical to the committed ones.

Documentation

Not needed. The documentation never described the heap or the workaround, and the obvious loop over ctx.corpus.symbols that the example extensions use now works on a large corpus. The out-of-memory message states the limit where a script meets it.

Fixes #1294.

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

✨ Highlights

  • 🧪 New golden tests added

🧾 Changes by Scope

Scope Lines Δ% Lines Δ Lines + Lines - Files Δ Files + Files ~ Files ↔ Files -
🛠️ Source 51% 1083 798 285 7 - 7 - -
🧪 Unit Tests 18% 389 363 26 1 - 1 - -
📦 Other 11% 234 234 - 8 6 2 - -
🥇 Golden Tests 8% 173 173 - 4 4 - - -
📄 Docs 7% 141 137 4 5 1 4 - -
🔧 Toolchain 4% 86 78 8 4 2 2 - -
Total 100% 2106 1783 323 29 13 16 - -

Legend: Files + (added), Files ~ (modified), Files ↔ (renamed), Files - (removed)

🔝 Top Files

  • src/mrdocs/Engines/JavaScript/ValueBridge.ipp (Source): 553 lines Δ (+444 / -109)
  • src/mrdocs/Engines/JavaScript.cpp (Source): 485 lines Δ (+325 / -160)
  • tests/unit/Engines/JavaScript.cpp (Unit Tests): 389 lines Δ (+363 / -26)

Generated by 🚫 dangerJS against c7a0050

@gennaroprota gennaroprota changed the title Fix/js extensions scale to a large corpus fix: JavaScript extensions scale to a large corpus Sep 23, 2026
@codecov

codecov Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.12%. Comparing base (1a0bbac) to head (c7a0050).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #1319   +/-   ##
========================================
  Coverage    83.12%   83.12%           
========================================
  Files           35       35           
  Lines         3662     3662           
  Branches       844      844           
========================================
  Hits          3044     3044           
  Misses         410      410           
  Partials       208      208           
Flag Coverage Δ
bootstrap 83.12% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cppalliance-bot

cppalliance-bot commented Sep 23, 2026 •

Copy link
Copy Markdown

An automated preview of the documentation is available at https://1319.mrdocs.prtest2.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-09-24 03:10:00 UTC

gennaroprota and others added 4 commits September 23, 2026 14:17
`jerry_init` asks the port for a block to hold the context and the heap,
and takes the *size* of that block from what the call returns. We
returned a pointer, instead, so the engine read an address as a length
and believed it had a much larger heap than actually available.

Return the size, instead, as the port function is asked to. The heap is
now the size it was allocated, and running out of it ends the way the
engine means it to, rather than by corrupting memory.

Refs cppalliance#1294.
A script that required more memory than the interpreter had available
caused the process to end with a bare failure code, which is what the
engine's default behavior on a fatal condition is.

Say what happened instead, and where the limit is, so the author of the
script can see what they ran into.

Refs cppalliance#1294.
This fixes a bug for which an extension on a large project ran out of
memory before it reached its first line of output. Reading
`ctx.corpus.symbols` was enough to trigger the bug.

The root cause was that an array was converted to JavaScript element by
element, up front. Each element that is an object cost an interpreter
object of its own, and the interpreter's heap is a fixed 512 KiB that a
build cannot simply enlarge, since 16-bit compressed pointers cap it
there. About two thousand symbols filled it.

An array now reaches JavaScript through a proxy, as an object already
did, and an element is converted when a script reads it, so what a
script holds has to fit rather than the whole corpus. The proxy wraps a
real array, so `Array.isArray` still answers `true` and the methods on
`Array.prototype` still work, and a script's own writes go to the
wrapped array, where they stay out of the DOM as copying used to keep
them.

Fixes cppalliance#1294.
JerryScript 3.0.0's parser reads an uninitialized field of its context while parsing a `let` declaration, which MemorySanitizer reports from
any test that uses `let`. That is upstream's bug, not ours, so the JerryScript build now gets a sanitizer ignorelist covering jerry-core
and jerry-port when compiled with -fsanitize=memory. The code stays instrumented and uninitialized values are still tracked into mrdocs; only reports originating inside those sources are silenced.
@alandefreitas
alandefreitas force-pushed the fix/js_extensions_scale_to_a_large_corpus branch from 9ebb3c4 to 5a74d0c Compare September 24, 2026 02:42
@alandefreitas

Copy link
Copy Markdown
Collaborator

The general direction is correct, but it failed CI and was missing components for a more stable fix. I've pushed extra commits to consider that.

JerryScript addresses its arena through compressed pointers, and with the 16-bit variant the heap could not exceed 512 KB, so an extension that kept a few thousand symbol objects alive ran out of memory even with lazy arrays.

JerryScript is now built with JERRY_CPOINTER_32_BIT=ON. Each context reserves just under 4 GB. On POSIX that is mmap(MAP_NORESERVE), so pages cost memory only when written. Windows has no demand-zero pages, so the block is only reserved and a vectored exception handler commits it 1 MB at a time when the engine first touches a page, which gives the same lazy behavior without an upstream change. A failed reservation is halved down to 512 KB. The out-of-memory message names the heap size and leaves through _Exit so static destructors do not call back into the dead engine. The bootstrap patch excludes jerry-port-process.c along with the context port file so the jerry_port_fatal override is portable to MSVC.

Lua needs no change: it allocates from the system heap and reports out-of-memory as a catchable error.
Every DOM proxy used to build its own handler object and its own set of
five or six trap functions, so an element read from ctx.corpus.symbols
cost about eight engine objects and 330 bytes, and creating it dominated
loops over the corpus. The traps only need the wrapped value, which now
lives as a native pointer on the proxy target (their first argument), so
a context builds two handlers once, one for objects and one for arrays,
and every proxy is just a target plus the Proxy object.

On the MrDocs corpus (6414 symbols) a count-by-kind loop went from
154 ms to 65 ms, collecting function names from 302 ms to 119 ms,
nested property reads from 630 ms to 186 ms, and 102,624 live symbol
proxies add 5.4 MB instead of 33.6 MB.
With JERRY_EXTERNAL_CONTEXT every read the engine makes of its own state goes through jerry_port_context_get(), several times per bytecode instruction. Our port answered that with pthread_once plus pthread_getspecific, two library calls per engine field access, which a profile showed as 98% of the samples of any JavaScript loop. The pointer is now a constant-initialized thread_local, one load. On top of that, the JerryScript build force-includes mrdocs-context.h (a compile option set from the patched CMakeLists, no upstream file edited), which defines jerry_port_context_get() as a macro reading that variable, so the engine makes no call at all: the built library references the variable and not the function. An engine built without the header still works through the call, since the variable and the function always agree.

On the MrDocs corpus a count-by-kind loop went from 62 ms to 29 ms and nested property reads from 182 ms to 116 ms.
A page under Extensions that compares Lua and JavaScript as script engines on the technical side: measured speed on six equivalent transforms over the MrDocs corpus (Lua is 2 to 20 times faster), how symbols reach a script and what a live one costs, how each engine uses memory and what happens on out of memory, and the state of the standard libraries in each engine as probed in the built binary. Ends with guidance on which language to pick. The corpus transforms and Handlebars extensions pages name both languages and link to it.

tests/benchmarks/ keeps the benchmarks the page quotes so they can be re-measured: the six transforms in both languages over MrDocs's own corpus, a runner that averages 30 runs and prints the table (Markdown or AsciiDoc), and the mrdocs-benchmark-script-engines custom target. Not CTest tests, since timing is machine-dependent.
@alandefreitas
alandefreitas force-pushed the fix/js_extensions_scale_to_a_large_corpus branch from 5a74d0c to c7a0050 Compare September 24, 2026 03:03
@alandefreitas
alandefreitas added this pull request to the merge queue Sep 24, 2026
Merged via the queue into cppalliance:develop with commit adc6242 Sep 24, 2026
32 checks passed
@gennaroprota

gennaroprota commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks. A few comments:

Do the benchmarks really support the conclusions drawn in the docs?

  • bench.lua uses os.clock (process CPU time), while bench.js uses Date.now (wall-clock time).
  • The JavaScript loop tests i < s.length on every iteration, and each read of length goes through the proxy trap. Lua's ipairs does nothing comparable.
  • "Build text" compares out += in JavaScript with table.concat in Lua. I guess the fair JavaScript version is push and then join.
  • tests/benchmrks/CMakeLists.txt says the target prints a median table, but run.py computes a mean.

Is the MSan ignorelist too broad?

  • We are silencing any MSan finding in JerryScript. But the one finding we currently have is in parser_check_anonymous_function_declaration. Wouldn't a single fun: entry for that be better?

Exit paths

  • jerry_port_fatal correctly leaves through _Exit, but jerry_port_context_alloc exits with std::exit.

Commit-on-demand

  • jerryCommitOnDemand runs on every access violation in the process and takes jerryBlocksMutex. If a crash ever happens inside the unordered_map operations that hold that mutex, the handler tries to lock it again on the same thread. That's UB for std::mutex and, in practice, a deadlock. So a crash would become a hang.
  • The comment at JavaScript.cpp:196 says Windows has no demand-zero pages. I think committed Windows pages are demand-zero, too; the actual constraint is the commit charge, which the next sentence states correctly.

Stale comments

  • ValueBridge.ipp:87 says "the handler was still freed above". Since commit 4, it's the target that holds the holder.
  • Type.hpp:69 still lists "fatal errors" among the port functions taken from the default library, a few lines above the new bullet says jerry_port_fatal is ours.
  • The "small heap" rationale survives in docs/mrdocs/extensions/schema.js:20, utils/codegen/generate-schema.sh:15 and src/mrdocs/Generators/script/ScriptGenerator.cpp:102.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: ctx.corpus.symbols cannot be iterated on a large corpus

3 participants