MozJS per-scope heap limit check uses process-global GC counter

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Fixed
    • Priority: Major - P3
    • 9.0.0-rc0, 8.3.3, 8.0.24, 7.0.35
    • Affects Version/s: None
    • Component/s: None
    • None
    • Query Integration
    • Fully Compatible
    • ALL
    • v8.3, v8.0, v7.0
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      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 master
      • SERVER-117317 – backported MozJS 140.7 + SERVER-109200 to v8.0 and v7.0
      • BACKPORT-26789 - 8.0 backport
      • BACKPORT-26790 - 7.0 backport

      Files

      • src/mongo/scripting/mozjs/jscustomallocator.cppget_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.cppgMappedMemorySizeBytes definition / GetProfilerMemoryCounts
      • src/mongo/scripting/config_engine.idljsUseLegacyMemoryTracking parameter

            Assignee:
            Anna Veselova
            Reporter:
            Will Buerger
            Votes:
            0 Vote for this issue
            Watchers:
            7 Start watching this issue

              Created:
              Updated:
              Resolved: