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
46 changes: 46 additions & 0 deletions .github/openclaw/qualification/patches/011-bun-gc-timer-wake.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
diff --git a/src/runtime/timer/WTFTimer.rs b/src/runtime/timer/WTFTimer.rs
index 9a0841e58..053e9c582 100644
--- a/src/runtime/timer/WTFTimer.rs
+++ b/src/runtime/timer/WTFTimer.rs
@@ -43,6 +43,8 @@ pub(crate) struct WTFTimer {
// that owns this wrapper lives on the VM's run loop, so the VM outlives
// the timer.
vm: NonNull<VirtualMachine>,
+ // An uncounted gate can wake the owner without keeping its VM alive.
+ wake_handle: bun_jsc::VmHandle,
// FFI handle into WebKit's RunLoop::TimerBase; owned by C++.
run_loop_timer: NonNull<RunLoopTimer>,
pub(crate) event_loop_timer: EventLoopTimer,
@@ -130,12 +132,18 @@ impl WTFTimer {
// There's only one of these per VM, and each VM has its own imminent_gc_timer.
// Only set imminent if it's not already set to avoid overwriting another timer.
if seconds.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater) {
- let _ = imminent.compare_exchange(
- ptr::null_mut(),
- self_opaque,
- Ordering::SeqCst,
- Ordering::SeqCst,
- );
+ let wake_handle = t.wake_handle.clone();
+ if imminent
+ .compare_exchange(
+ ptr::null_mut(),
+ self_opaque,
+ Ordering::SeqCst,
+ Ordering::SeqCst,
+ )
+ .is_ok()
+ {
+ wake_handle.wake();
+ }
return;
}
// Clear imminent if this timer was the one that set it.
@@ -257,6 +265,7 @@ unsafe extern "C" fn WTFTimer__create(run_loop_timer: *mut RunLoopTimer) -> *mut
let el = &*vm_ref.event_loop();
Box::new(WTFTimer {
vm: NonNull::new_unchecked(vm),
+ wake_handle: vm_ref.handle(),
imminent: bun_ptr::BackRef::new(&el.imminent_gc_timer),
event_loop_timer: EventLoopTimer {
next: ElTimespec {
2 changes: 1 addition & 1 deletion .github/openclaw/qualification/verify-sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
assert rows and all(r['ok'] for r in rows),'sync CI failures'
assert {'test/js/web/atomics.test.ts','test/js/bun/wasm/compile-rss.test.ts'}<=set(selected)
counts={}
for name,minimum in [('memory-release',3),('proxy',4),('resource-limits',14),('namespace',41),('inspector',78),('als-suites',223),('stack-positions',343),('stack-minifier',68),('arraybuffer',19),('worker-statistics',7),('arraybuffer-native',1),('arraybuffer-serializer',60),('inspection-idle',1)]:
for name,minimum in [('memory-release',3),('proxy',4),('resource-limits',16),('namespace',41),('inspector',78),('als-suites',223),('stack-positions',343),('stack-minifier',68),('arraybuffer',19),('worker-statistics',7),('arraybuffer-native',1),('arraybuffer-serializer',60),('inspection-idle',1)]:
text=(out/(name+'.log')).read_text()
passes=re.findall(r'(\d+) pass',text)
assert passes and int(passes[-1])>=minimum,name+' incomplete'
Expand Down
60 changes: 58 additions & 2 deletions .github/openclaw/qualification/worker-resource-limits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("resourceLimits", () => {
expect(reads).toBe(2);
});

async function runLimitedWorker(limits: object, body: string) {
async function runLimitedWorker(limits: object, body: string, env: Record<string, string> = {}) {
await using child = Bun.spawn({
cmd: [bunExe(), "-e", `
const {Worker,resourceLimits}=require('node:worker_threads');
Expand All @@ -47,7 +47,7 @@ describe("resourceLimits", () => {
w.on('error',e=>{result.events.push('error');result.error={name:e.name,code:e.code,message:e.message,limits:w.resourceLimits};});
w.on('exit',code=>{result.events.push('exit');result.exit=code;result.after=w.resourceLimits;console.log(JSON.stringify(result));});
`],
env: bunEnv,
env: { ...bunEnv, ...env },
stdout: "pipe",
stderr: "pipe",
});
Expand All @@ -69,6 +69,62 @@ describe("resourceLimits", () => {
expect({ online: result.online, inside: result.messages[0].limits, after: result.after, exit: result.exit }).toEqual({ online: limits, inside: limits, after: {}, exit: 0 });
});

const withoutIdleGC = {
BUN_GC_TIMER_DISABLE: "1",
BUN_IDLE_GC_SECONDS: "0",
BUN_DISABLE_STOP_IF_NECESSARY_TIMER: "1",
BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS: "0",
};

test.concurrent("allows natural young collections below the old heap limit", async () => {
const result = await runLimitedWorker(
{ maxOldGenerationSizeMb: 64, maxYoungGenerationSizeMb: 1 },
`
const { GCProfiler } = require('node:v8');
const profiler = new GCProfiler();
profiler.start();
let checksum = 0;
for (let round = 0; round < 1000; ++round) {
globalThis.batch = new Array(8192).fill(round);
checksum += batch[(round * 31) % batch.length];
}
const records = profiler.stop().statistics;
parentPort.postMessage({ checksum, young: records.filter(row => row.gcType === 'Scavenge').length });
`,
withoutIdleGC,
);
expect(result.messages.at(-1).checksum).toBe(499500);
expect(result.messages.at(-1).young).toBeGreaterThan(1);
expect({ events: result.events, exit: result.exit }).toEqual({ events: ["exit"], exit: 0 });
});

test.concurrent("verifies an old-limit crossing found by Eden before an idle worker parks", async () => {
const result = await runLimitedWorker(
{ maxOldGenerationSizeMb: 16, maxYoungGenerationSizeMb: 64 },
`
const { edenGC } = require('bun:jsc');
parentPort.on('message', () => {});
globalThis.held = new Array(80000);
Bun.gc(true);
let checksum = 0;
for (let i = 0; i < held.length; ++i) {
const text = String(i).padStart(220, 'x');
checksum += text.charCodeAt(0) + text.charCodeAt(219);
held[i] = text;
}
parentPort.postMessage(checksum);
edenGC();
`,
withoutIdleGC,
);
expect(result.messages.at(-1)).toBe(13800000);
expect({ events: result.events, code: result.error?.code, exit: result.exit }).toEqual({
events: ["error", "exit"],
code: "ERR_WORKER_OUT_OF_MEMORY",
exit: 1,
});
});

test("terminates only the worker with the Node heap OOM event contract", async () => {
const result = await runLimitedWorker({ maxOldGenerationSizeMb: 32, maxYoungGenerationSizeMb: 4 },
"globalThis.held=[];for(let i=0;i<64;i++)held.push(new Array(1024*1024).fill(i));parentPort.postMessage('survived');");
Expand Down
4 changes: 2 additions & 2 deletions .github/openclaw/qualify-arm64.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ jsc="$ENGINE_BUILD_DIR/bin/jsc"
"$ENGINE_BUILD_DIR/bin/testMimallocExit" > "$QUALIFICATION_DIR/mimalloc-exit.log" 2>&1
"$ENGINE_BUILD_DIR/bin/testMimallocExitInFlight" > "$QUALIFICATION_DIR/mimalloc-exit-in-flight.log" 2>&1
Tools/Scripts/run-jsc-stress-tests JSTests/stress --jsc "$jsc" --child-processes "${ENGINE_TEST_JOBS:-4}" \
--filter 'promise|microtask|dynamic-import|bun-async-context|module-loader|top-level-await|async-module|module-namespace|intl-segmenter|callsite-syntax-positions|stack-position-regressions|error-stack' \
--filter 'promise|microtask|dynamic-import|bun-async-context|module-loader|top-level-await|async-module|module-namespace|intl-segmenter|callsite-syntax-positions|stack-position-regressions|error-stack|worker-heap-generational-cadence' \
--output-dir "$QUALIFICATION_DIR/jsc-stress" --no-retry > "$QUALIFICATION_DIR/jsc-stress.log" 2>&1
python3 .github/openclaw/verify-jsc-results.py "$QUALIFICATION_DIR/jsc-stress" --require module-namespace-esmodule-marker.js --require callsite-syntax-positions.js --require stack-position-regressions.js
python3 .github/openclaw/verify-jsc-results.py "$QUALIFICATION_DIR/jsc-stress" --require module-namespace-esmodule-marker.js --require callsite-syntax-positions.js --require stack-position-regressions.js --require worker-heap-generational-cadence.js
Tools/Scripts/run-jsc-stress-tests JSTests/modules.yaml --jsc "$jsc" --child-processes "${ENGINE_TEST_JOBS:-4}" \
--output-dir "$QUALIFICATION_DIR/jsc-modules" --no-retry > "$QUALIFICATION_DIR/jsc-modules.log" 2>&1
# Accounting requires generational GC; run its established explicit modes below.
Expand Down
4 changes: 2 additions & 2 deletions .github/openclaw/qualify-engine.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ tar -xzf "$ARTIFACT_DIR/bun-webkit-linux-amd64.tar.gz" -C "$QUALIFICATION_DIR/en
jsc="$QUALIFICATION_DIR/engine/bun-webkit/bin/jsc"
"$jsc" --useDollarVM=true -e 'if(typeof $vm.moduleNamespaceForRequire!=="function")throw Error("namespace hook absent")'
Tools/Scripts/run-jsc-stress-tests JSTests/stress --jsc "$jsc" --child-processes 16 \
--filter 'promise|microtask|dynamic-import|bun-async-context|module-loader|top-level-await|async-module|module-namespace|intl-segmenter|callsite-syntax-positions|stack-position-regressions|error-stack' \
--filter 'promise|microtask|dynamic-import|bun-async-context|module-loader|top-level-await|async-module|module-namespace|intl-segmenter|callsite-syntax-positions|stack-position-regressions|error-stack|worker-heap-generational-cadence' \
--output-dir "$QUALIFICATION_DIR/jsc-stress" --no-retry > "$QUALIFICATION_DIR/jsc-stress.log" 2>&1
python3 .github/openclaw/verify-jsc-results.py "$QUALIFICATION_DIR/jsc-stress" --require module-namespace-esmodule-marker.js --require callsite-syntax-positions.js --require stack-position-regressions.js --require stack-position-regressions-cache.js
python3 .github/openclaw/verify-jsc-results.py "$QUALIFICATION_DIR/jsc-stress" --require module-namespace-esmodule-marker.js --require callsite-syntax-positions.js --require stack-position-regressions.js --require stack-position-regressions-cache.js --require worker-heap-generational-cadence.js
python3 .github/openclaw/check-stack-visitor.py "$jsc" > "$QUALIFICATION_DIR/stack-visitor.log"
WEBKIT_CHECKOUT="$PWD" JSC="$jsc" RESULT_DIR="$QUALIFICATION_DIR/jsc-stack" bash .github/openclaw/qualification/stack/check-jsc.sh
Tools/Scripts/run-jsc-stress-tests JSTests/modules.yaml --jsc "$jsc" --child-processes 16 \
Expand Down
1 change: 1 addition & 0 deletions .github/openclaw/qualify-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ for patch in 008-namespace.patch 009-allocation-sampling.patch 010-als-regressio
git apply --index --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/$patch"
done
git apply --index "$inputs/sync/inspect-stack-position.patch"
git apply --index "$inputs/patches/011-bun-gc-timer-wake.patch"
cp "$inputs/worker-resource-limits.test.ts" test/js/node/worker_threads/openclaw-resource-limits.test.ts
git add test/js/node/worker_threads/openclaw-resource-limits.test.ts
python3 "$inputs/stage-sync-artifact.py" "$ARTIFACT_DIR/bun-webkit-linux-amd64.tar.gz" --version "$SOURCE_SHA" --output "$QUALIFICATION_DIR/sync-input"
Expand Down
5 changes: 3 additions & 2 deletions .github/openclaw/qualify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ for arm in baseline candidate; do
git apply --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/008-namespace.patch"
git apply --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/009-stack-positions.patch"
git apply --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/010-arraybuffer-accounting.patch"
git apply "$inputs/patches/011-bun-gc-timer-wake.patch"
cp "$inputs/worker-resource-limits.test.ts" test/js/node/worker_threads/openclaw-resource-limits.test.ts
git diff --check
git diff --binary HEAD > "$QUALIFICATION_DIR/bun-adapters.patch"
Expand Down Expand Up @@ -81,12 +82,12 @@ assert faith['selected_files']==44 and faith['result_files']==46
for name in ['als-plugin','als-native']:
rows=json.loads((root/(name+'.json')).read_text())['results']
assert len(rows)==14 and all(r['exitCode']==0 for r in rows)
for name,minimum in [('proxy',4),('resource-limits',14),('namespace',41),('arraybuffer',19),('worker-statistics',7)]:
for name,minimum in [('proxy',4),('resource-limits',16),('namespace',41),('arraybuffer',19),('worker-statistics',7)]:
text=(root/(name+'.log')).read_text()
passes=re.search(r'(\d+) pass',text)
assert passes and int(passes[1])>=minimum, name+' missing passing cases'
assert re.search(r'\b0 fail\b',text),name+' failures'
(root/'gate.json').write_text(json.dumps({'passed':True,'source':sys.argv[2],'bun_commit':sys.argv[3],
'selected_files':44,'result_files':46,'als_variants_per_mode':14,'proxy_minimum':4,'resource_minimum':14,
'selected_files':44,'result_files':46,'als_variants_per_mode':14,'proxy_minimum':4,'resource_minimum':16,
'bun_adapter_sha256':hashlib.sha256((root/'bun-adapters.patch').read_bytes()).hexdigest()},indent=2)+'\n')
PY
23 changes: 23 additions & 0 deletions JSTests/stress/worker-heap-generational-cadence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//@ runDefault("--useDollarVM=true", "--useConcurrentGC=false")

$vm.setWorkerHeapLimits(64 * 1024 * 1024, 256 * 1024);
const allocatedBefore = $vm.heapTotalBytesAllocated();
let sawEden = false;
let checksum = 0;
const rounds = Math.max(80, Math.min(testLoopCount, 160));
for (let round = 0; round < rounds; ++round) {
globalThis.lastBatch = new Array(2048).fill(round);
checksum += globalThis.lastBatch[round % 2048];
sawEden ||= $vm.lastGCWasFull() === false;
}
if (checksum !== rounds * (rounds - 1) / 2)
throw new Error("allocation workload result changed");
if ($vm.heapTotalBytesAllocated() - allocatedBefore < 1024 * 1024)
throw new Error("allocation workload did not exercise collection pressure");
if (!sawEden)
throw new Error("worker limits forced every allocation collection full");
if ($vm.workerHeapLimitExceeded())
throw new Error("temporary allocation exceeded a live-heap limit");
globalThis.lastBatch = null;
$vm.setWorkerHeapLimits(0, 0);
gc();
5 changes: 4 additions & 1 deletion OPENCLAW.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Qualification-only Bun adapters live under `.github/openclaw/qualification`;
they do not update or publish the Bun fork. The job runs the original 44-file
fork selector output plus its two dependency checks, requires identical all-pass
results, and checks Segmenter, four Proxy regressions, both 14-variant ALS hook
modes and at least 14 worker resource-limit cases. Other platforms are built;
modes and at least 16 worker resource-limit cases. Other platforms are built;
this gate does not claim native runtime qualification on every architecture. A
Windows 11 ARM64 job verifies archive provenance and native ARM64 PE identity,
then runs JSC startup, DFG JIT, testFFI and ArrayBuffer accounting in five execution modes. Assembly and publication require
Expand Down Expand Up @@ -150,6 +150,9 @@ baseline. Engine and Bun headers and libraries must be rebuilt together.

## Unreleased

- Let workers use young-generation collections within their heap budgets; verify exhaustion with a full collection and preserve progress while idle through the matching Bun timer adapter.
- Bound budgeted workers' post-full growth independently of machine-wide RAM, while retaining the collector minimum, separate nursery pacing and full-only heap-limit verification.

## Worker termination and allocator exit (2026-10-06)

- 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.
Expand Down
Loading