Summary
The per-scope JavaScript heap limit check (jsHeapLimitMB, default 1100 MB) incorporated a process-global GC counter into every individual scope's "total bytes used" calculation as of SERVER-109200. Under concurrent JS workloads, the cumulative GC heap from all in-flight scopes inflates each scope's apparent memory usage, causing scope initialization to fail with ExceededMemoryLimit ("Out of memory while trying to initialize javascript scope") even when no individual scope is using significant memory.
Root Cause
In src/mongo/scripting/mozjs/jscustomallocator.cpp:
size_t get_mmap_bytes() {
return track_mmap_bytes ? js::gc::GetProfilerMemoryCounts().bytes : 0;
}
size_t get_total_bytes() {
return get_malloc_bytes() + get_mmap_bytes();
}
js::gc::GetProfilerMemoryCounts().bytes returns gMappedMemorySizeBytes – a process-wide atomic in src/third_party/mozjs/extract/js/src/gc/Memory.cpp:
JS_PUBLIC_API ProfilerMemoryCounts GetProfilerMemoryCounts() {
return {gc::gMappedMemorySizeBytes, gc::gMappedMemoryOperations};
}
malloc_bytes is thread_local, but gMappedMemorySizeBytes is process-global. So get_total_bytes() = (this thread's malloc) + (every running scope's GC mmap, summed).
The per-scope limit check at src/mongo/scripting/mozjs/implscope.cpp:513 therefore fails as soon as the aggregate GC footprint across all concurrent scopes exceeds the per-scope limit:
uassert(ErrorCodes::ExceededMemoryLimit,
"Out of memory while trying to initialize javascript scope",
mallocMemoryLimit == 0 || mongo::sm::get_total_bytes() < mallocMemoryLimit);
The same global counter is also consulted on every mmap allocation via the check_oom_on_mmap_allocation hook injected into MozJS's RecordMemoryAlloc, so OOM signaling fires for any thread whenever the aggregate crosses the threshold.
Failure Sequence
- Concurrent findAndModify+JS (or any JS-using) operations begin executing
- Each scope's InitSelfHostedCode() allocates GC heap, incrementing gMappedMemorySizeBytes
- Subsequent scope-init checks see thread_malloc + GLOBAL_mmap >= jsHeapLimitMB
- Each new scope throws ExceededMemoryLimit, releasing its WiredTiger write ticket
- Application retries -> ticket immediately reacquired -> same failure -> 128-ticket pool stays saturated from churn
Reproduction
A noPassthrough test (jstests/noPassthrough/query/js/js_concurrent_scope_oom.js, attached) reproduces the bug deterministically:
- Single mongod with jsHeapLimitMB=50
- 16 worker threads x 30 iterations each, running findAndModify with a minimal $where
- Phase 1 (jsUseLegacyMemoryTracking: false, default in 8.0.21+): 214 / 480 ops fail with ExceededMemoryLimit (44.6%)
- Phase 2 (jsUseLegacyMemoryTracking: true): 0 failures
Workaround
Server parameter jsUseLegacyMemoryTracking: true (runtime-settable, no restart). Disables mmap tracking, falls back to per-thread malloc-only tracking, matching pre-SERVER-109200 behavior. Eliminates the dominant failure mode (verified above).
Related Tickets
SERVER-109200(mozjs 140.3 upgrade) – introduced the mmap accounting on masterSERVER-117317– backported MozJS 140.7 +SERVER-109200to v8.0 and v7.0- BACKPORT-26789 - 8.0 backport
- BACKPORT-26790 - 7.0 backport
Files
- src/mongo/scripting/mozjs/jscustomallocator.cpp – get_mmap_bytes, get_total_bytes, check_oom_on_mmap_allocation
- src/mongo/scripting/mozjs/implscope.cpp:513 – the failing assertion
- src/third_party/mozjs/extract/js/src/gc/Memory.cpp – gMappedMemorySizeBytes definition / GetProfilerMemoryCounts
- src/mongo/scripting/config_engine.idl – jsUseLegacyMemoryTracking parameter
- is related to
-
SERVER-109200 Upgrade MozJS to ESR 140.3
-
- Closed
-
-
SERVER-117317 Upgrade MozJS to ESR 140.7
-
- Closed
-