Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/openclaw/qualification/engine-limit-probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
#include <JavaScriptCore/JSGlobalObject.h>
#include <JavaScriptCore/JSLock.h>
#include <JavaScriptCore/VM.h>
#include <JavaScriptCore/HeapObserver.h>
#include <JavaScriptCore/VMTraps.h>
#include <wtf/threads/BinarySemaphore.h>
#include <wtf/MonotonicTime.h>
#include <wtf/WorkQueue.h>
#include <cstdio>
#include <cstdlib>

Expand Down Expand Up @@ -41,6 +46,67 @@ static bool probe(const char* name, const char* source, size_t heapLimit, size_t
return exceeded == expected;
}

class TerminationObserver final : public JSC::HeapObserver {
public:
explicit TerminationObserver(JSC::VM& vm)
: m_vm(vm) { }
void willGarbageCollect() final { }
void didGarbageCollect(JSC::CollectionScope scope) final
{
if (scope != JSC::CollectionScope::Full || !m_vm.heap.heapLimitExceeded())
return;
if (++m_breaches != 1) {
std::fprintf(stderr, "worker termination was not delivered before another full GC\n");
std::fflush(stderr);
std::_Exit(1);
}
m_firstBreach = WTF::MonotonicTime::now();
m_vm.notifyNeedTermination();
}
JSC::VM& m_vm;
unsigned m_breaches { 0 };
WTF::MonotonicTime m_firstBreach;
};

static bool promptTerminationProbe()
{
auto group = JSContextGroupCreate();
auto context = JSGlobalContextCreateInGroup(group, nullptr);
auto& vm = toJS(context)->vm();
WTF::BinarySemaphore ready, release, done;
// Keep the signal sender unavailable: GC must deliver the pending trap itself.
JSC::VMTraps::queue().dispatch([&] {
ready.signal();
release.wait();
done.signal();
});
ready.wait();
bool passed;
{
JSC::JSLockHolder lock(vm);
// Embedders prepare this exception before requesting termination from a GC observer.
vm.ensureTerminationException();
vm.heap.collectNow(JSC::Sync, JSC::CollectionScope::Full);
vm.heap.setWorkerHeapLimits(32 * 1024 * 1024, 4 * 1024 * 1024);
TerminationObserver observer(vm);
vm.heap.addObserver(&observer);
auto source = JSStringCreateWithUTF8CString("let held=[];for(let i=0;i<2000000;i++)held.push({i,a:i+1,b:i+2,c:i+3});");
JSValueRef exception = nullptr;
JSEvaluateScript(context, source, nullptr, nullptr, 1, &exception);
auto end = WTF::MonotonicTime::now();
JSStringRelease(source);
vm.heap.removeObserver(&observer);
release.signal();
done.wait();
passed = exception && observer.m_breaches == 1;
std::printf("termination exception=%d fullGCsAtOrAboveCap=%u latencyMs=%.3f passed=%d\n", !!exception,
observer.m_breaches, (end - observer.m_firstBreach).milliseconds(), passed);
}
JSGlobalContextRelease(context);
JSContextGroupRelease(group);
return passed;
}

int main()
{
constexpr size_t MB = 1024 * 1024;
Expand All @@ -52,5 +118,7 @@ int main()
passed &= probe("mixed", "globalThis.held=[new Uint8Array(128*1024*1024).fill(7),new Array(8*1024*1024).fill(7)]", 32*MB, 4*MB, true);
passed &= probe("unlimited-other-vm", "globalThis.held = new Array(8*1024*1024).fill(7)", 0, 0, false);
passed &= probe("young-only", "globalThis.held = new Array(8*1024*1024).fill(7)", 0, 4*MB, false);
passed &= probe("near-limit-churn", "globalThis.held = new Array(30*1024*1024/8).fill(7); for(let i=0;i<128;i++){let temporary=new Array(64*1024).fill(i); if(temporary[i]!==i)throw Error('corrupt');}", 32*MB, 4*MB, false);
passed &= promptTerminationProbe();
return passed ? 0 : 1;
}
2 changes: 1 addition & 1 deletion .github/openclaw/qualification/verify-sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
cache=json.loads((out/'stack-cache.json').read_text())
assert len(cache)==4 and all(r['matches']==r['total']==56 for r in cache if r['arm']=='candidate')
assert cache[-1]['arm']=='candidate' and cache[-1]['repeat']=='warm' and cache[-1]['cache_hit_proven'] is True
assert len((out/'engine-limits.log').read_text().splitlines())==7
assert len((out/'engine-limits.log').read_text().splitlines())==9
engine=json.loads((root/'engine-gate.json').read_text())
assert engine['passed'] and engine['source']==sys.argv[2]
compatibility=json.loads((root/'compatibility-gate.json').read_text())
Expand Down
8 changes: 8 additions & 0 deletions .github/openclaw/qualification/worker-resource-limits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ describe("resourceLimits", () => {
expect({ code: result.error?.code, exit: result.exit }).toEqual({ code: "ERR_WORKER_OUT_OF_MEMORY", exit: 1 });
});

test("near-limit managed churn survives with external backing stores", async () => {
const result = await runLimitedWorker({ maxOldGenerationSizeMb: 32, maxYoungGenerationSizeMb: 4 },
"globalThis.held=new Array(22*1024*1024/8).fill(7);globalThis.external=new Uint8Array(64*1024*1024);external[0]=11;let sum=0;for(let i=0;i<128;i++){let temporary=new Array(64*1024).fill(i);sum+=temporary[i];}parentPort.postMessage({sum,length:held.length,value:held[0],external:external.byteLength,byte:external[0]});");
expect({ events: result.events, exit: result.exit, value: result.messages.at(-1) }).toEqual({
events: ["exit"], exit: 0, value: { sum: 8128, length: 2883584, value: 7, external: 67108864, byte: 11 },
});
});

test("a young-only limit sizes the nursery without capping retained objects", async () => {
const result = await runLimitedWorker({ maxYoungGenerationSizeMb: 4 },
"globalThis.held=[];for(let i=0;i<8;i++)held.push(new Array(1024*1024).fill(i));Bun.gc(true);parentPort.postMessage('retained');");
Expand Down
2 changes: 2 additions & 0 deletions .github/openclaw/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ The new engine requires Bun's updated mimalloc idle hook and matching embedding
Source and licenses: LICENSE-SOURCES.txt. Checksums: SHA256SUMS and manifest.json. Build and qualification evidence: provenance.tar.gz.

Rollback by restoring the prior Bun manifest and WebKit pin together, then rebuilding. Prior releases remain available.

Worker heap-cap termination now invalidates optimized code at the mutator GC epilogue, avoiding repeated full collections while a pending termination trap waits for a signal. Full-GC managed live accounting and ArrayBuffer exclusions are unchanged.
2 changes: 2 additions & 0 deletions OPENCLAW.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ baseline. Engine and Bun headers and libraries must be rebuilt together.

## Unreleased

- Deliver pending worker heap-limit termination promptly after GC by invalidating optimized code on the mutator, preserving full-GC live accounting and external-buffer exclusions.

- Prevent late JSC helper-thread allocation from overwriting a recycled pthread TLS key during process exit; add a deterministic native allocator-exit regression. Ports the allocator correction from oven-sh/WebKit#698, thanks @dylan-conway.

## ARM64 arithmetic and Windows artifacts (2026-10-05)
Expand Down
7 changes: 7 additions & 0 deletions Source/JavaScriptCore/heap/Heap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
#include "TypeProfilerLog.h"
#include "UnlinkedEvalCodeBlock.h"
#include "VM.h"
#include "VMTrapsInlines.h"
#include "VerifierSlotVisitorInlines.h"
#include "WasmCallee.h"
#include "WeakMapImplInlines.h"
Expand Down Expand Up @@ -3052,6 +3053,12 @@ void Heap::runCollectionEpilogue()
if (shouldSweepSynchronously())
sweepSynchronously();

#if USE(BUN_JSC_ADDITIONS)
// A capped worker may spend nearly all its time in GC, starving signal-based trap delivery.
if (m_heapLimitExceeded)
vm().traps().invalidateCodeBlocksForPendingTraps();
#endif

if (Options::logGC()) [[unlikely]] {
MonotonicTime after = MonotonicTime::now();
dataLog((after - before).milliseconds(), "ms]\n");
Expand Down
2 changes: 1 addition & 1 deletion Source/JavaScriptCore/runtime/JSLock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ void JSLock::didAcquireLock()
}

// Note: everything below must come after addCurrentThread().
m_vm->traps().notifyGrabAllLocks();
m_vm->traps().invalidateCodeBlocksForPendingTraps();

#if ENABLE(SAMPLING_PROFILER)
{
Expand Down
2 changes: 1 addition & 1 deletion Source/JavaScriptCore/runtime/VMTraps.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class VMTraps {
inline void deferTermination(DeferAction);
inline void undoDeferTermination(DeferAction);

inline void notifyGrabAllLocks();
inline void invalidateCodeBlocksForPendingTraps();

bool hasTrapBit(Event event)
{
Expand Down
4 changes: 2 additions & 2 deletions Source/JavaScriptCore/runtime/VMTrapsInlines.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ ALWAYS_INLINE DeferTraps::~DeferTraps()
m_traps.m_trapsDeferred = m_previousTrapsDeferred;
}

inline void VMTraps::notifyGrabAllLocks()
inline void VMTraps::invalidateCodeBlocksForPendingTraps()
{
if (needHandling(AsyncEvents))
if (!m_trapsDeferred && needHandling(AsyncEvents))
invalidateCodeBlocksOnStack();
}

Expand Down