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
124 changes: 124 additions & 0 deletions .github/openclaw/qualification/engine-limit-probe.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#include <cmakeconfig.h>
#include <JavaScriptCore/JavaScript.h>
#include <JavaScriptCore/APICast.h>
#include <JavaScriptCore/Heap.h>
#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>

static void evaluate(JSGlobalContextRef context, const char* text)
{
auto source = JSStringCreateWithUTF8CString(text);
JSValueRef exception = nullptr;
JSEvaluateScript(context, source, nullptr, nullptr, 1, &exception);
JSStringRelease(source);
if (exception) {
std::fprintf(stderr, "probe evaluation failed\n");
std::exit(2);
}
}

static bool probe(const char* name, const char* source, size_t heapLimit, size_t nursery, bool expected)
{
auto group = JSContextGroupCreate();
auto context = JSGlobalContextCreateInGroup(group, nullptr);
auto& vm = toJS(context)->vm();
bool exceeded;
{
JSC::JSLockHolder lock(vm);
vm.heap.collectNow(JSC::Sync, JSC::CollectionScope::Full);
vm.heap.setWorkerHeapLimits(heapLimit, nursery);
evaluate(context, source);
vm.heap.collectNow(JSC::Sync, JSC::CollectionScope::Full);
exceeded = vm.heap.heapLimitExceeded();
std::printf("%s managed=%zu exceeded=%d expected=%d\n", name,
vm.heap.sizeAfterLastFullCollectionExcludingArrayBuffers(), exceeded, expected);
}
JSGlobalContextRelease(context);
JSContextGroupRelease(group);
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;
bool passed = true;
passed &= probe("managed", "globalThis.held = new Array(8*1024*1024).fill(7)", 32*MB, 4*MB, true);
passed &= probe("oversize", "globalThis.held = new Uint8Array(128*1024*1024).fill(7)", 32*MB, 4*MB, false);
passed &= probe("fast", "globalThis.held=[];for(let i=0;i<65536;i++)held.push(new Uint8Array(2048).fill(7))", 32*MB, 4*MB, false);
passed &= probe("arraybuffer", "globalThis.held = new Uint8Array(new ArrayBuffer(128*1024*1024)).fill(7)", 32*MB, 4*MB, false);
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;
}
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
9 changes: 9 additions & 0 deletions .github/openclaw/qualify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ unset BUN_WEBKIT_ARTIFACT_MANIFEST BUN_BUILD_CACHE_DIR BUN_BUILD_PREFETCH_DIR
"$candidate" test test/js/node/process/process.test.js -t 'ArrayBuffer memory ownership' > "$QUALIFICATION_DIR/arraybuffer.log" 2>&1
"$candidate" test test/js/node/worker_threads/worker_threads.test.ts -t 'getHeapStatistics' > "$QUALIFICATION_DIR/worker-statistics.log" 2>&1
"$candidate" test test/js/node/worker_threads/openclaw-resource-limits.test.ts -t 'resourceLimits' > "$QUALIFICATION_DIR/resource-limits.log" 2>&1
mkdir -p "$QUALIFICATION_DIR/worker-engine"
tar -xzf "$ARTIFACT_DIR/bun-webkit-linux-amd64.tar.gz" -C "$QUALIFICATION_DIR/worker-engine"
WK="$QUALIFICATION_DIR/worker-engine/bun-webkit"
clang++-23 -std=c++23 -O2 -DNDEBUG -DBUILDING_WITH_CMAKE=1 -DHAVE_CONFIG_H=1 \
-I"$WK/include" -I"$WK/include/JavaScriptCore" -I"$WK/include/wtf" \
"$inputs/engine-limit-probe.cpp" "$QUALIFICATION_DIR/bun/build/qualify-candidate/obj/vendor/mimalloc/src/static.c.o" \
-Wl,--start-group "$WK"/lib/*.a -Wl,--end-group -lpthread -ldl -lm -latomic \
-o "$QUALIFICATION_DIR/engine-limit-probe" > "$QUALIFICATION_DIR/engine-limit-build.log" 2>&1
"$QUALIFICATION_DIR/engine-limit-probe" > "$QUALIFICATION_DIR/engine-limits.log" 2>&1
"$candidate" "$inputs/module-context.mjs" matrix > "$QUALIFICATION_DIR/als-plugin.json"
"$candidate" "$inputs/module-context.mjs" matrix --native-hooks > "$QUALIFICATION_DIR/als-native.json"
python3 - "$QUALIFICATION_DIR" "$SOURCE_SHA" "$BUN_COMMIT" <<'PY'
Expand Down
2 changes: 2 additions & 0 deletions OPENCLAW.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,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.

- Preserve mimalloc pthread TLS-key ownership during shell exit by draining active setters before deletion; cover both late and in-flight allocations with native regressions. Builds on oven-sh/WebKit#698, thanks @dylan-conway.

- Fix ARM64 MacroAssembler register-to-memory `add64` operand order, restoring DFG typed-array allocation accounting and preserving the source register; cover the operation in testmasm and native ARM64 qualification.
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