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
4 changes: 2 additions & 2 deletions .github/openclaw/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import sys
import time

BASE = '1600131e46b5af48bbda3559af8d8a3327230b6e'
BASE = '5718a6ec579b98362ea7276a426deedcc6281ef5'
ROOT = Path(__file__).resolve().parents[2]
CONFIG = json.loads((ROOT / '.github/openclaw/variants.json').read_text())
RECIPE = ['.github/scripts/lanes.mjs', 'Dockerfile', 'Dockerfile.musl', 'Dockerfile.macos', 'Dockerfile.windows', 'icu', 'macos-cross']
Expand Down Expand Up @@ -45,7 +45,7 @@ def preflight():
sha = source()
if sha != os.environ['INPUT_SHA'] or sha != os.environ['GITHUB_SHA']:
raise ValueError('dispatch must use the same reviewed branch head and full input SHA')
if os.environ['GITHUB_REF'] not in ('refs/heads/openclaw/main', 'refs/heads/openclaw/batch-1'):
if os.environ['GITHUB_REF'] not in ('refs/heads/openclaw/main', 'refs/heads/openclaw/batch-1', 'refs/heads/openclaw/release-5718a6ec'):
raise ValueError('only an explicitly permitted owned branch may run this workflow')
upstream = {l['label'] for l in lane_definitions()}
wanted = [l['label'] for l in CONFIG['lanes']]
Expand Down
3 changes: 3 additions & 0 deletions .github/openclaw/qualification/als/jsc/fixtures/a-leaf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const before = $vm.asyncContext();
await Promise.resolve();
export const after = $vm.asyncContext();
2 changes: 2 additions & 0 deletions .github/openclaw/qualification/als/jsc/fixtures/a-parent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { before, after } from './a-leaf.js';
export const contexts = [before, after, $vm.asyncContext()];
3 changes: 3 additions & 0 deletions .github/openclaw/qualification/als/jsc/fixtures/b-leaf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const before = $vm.asyncContext();
await Promise.resolve();
export const after = $vm.asyncContext();
2 changes: 2 additions & 0 deletions .github/openclaw/qualification/als/jsc/fixtures/b-parent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { before, after } from './b-leaf.js';
export const contexts = [before, after, $vm.asyncContext()];
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const dependencyContext = $vm.asyncContext();
await Promise.resolve();
if ($vm.asyncContext() !== dependencyContext) throw Error('TLA context');
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { dependencyContext } from './deferred-leaf.js';
export const rootContext = $vm.asyncContext();
3 changes: 3 additions & 0 deletions .github/openclaw/qualification/als/jsc/fixtures/throw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
globalThis.contextLog.push($vm.asyncContext());
$vm.setAsyncContext(['residue']);
throw Error('module throw');
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
await Promise.resolve();
export const value = 137;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { value } from './unscoped-leaf.js';
57 changes: 57 additions & 0 deletions .github/openclaw/qualification/als/jsc/module-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//@ requireOptions("--useDollarVM=1", "--useImportDefer=1")
function eq(actual, expected, label) {
if (actual !== expected) throw Error(label + ': expected ' + expected + ', got ' + actual);
}
function scoped(ctx, fn) {
const previous = $vm.asyncContext();
$vm.setAsyncContext(ctx);
try { return fn(); } finally { $vm.setAsyncContext(previous); }
}
let complete = false;
// Exercise a raw import.defer payload before async-context tracking is enabled.
import.defer('./fixtures/unscoped-parent.js').then(ns => {
eq(ns.value, 137, 'deferred import without ALS');
complete = true;
}, $vm.abort);
drainMicrotasks();
eq(complete, true, 'unscoped defer completed');
const A = ['A'], B = ['B'];
globalThis.contextLog = [];
let done = 0;
for (const [ctx, file] of [[A, 'a'], [B, 'b']]) {
scoped(ctx, () => import('./fixtures/' + file + '-parent.js').then(ns => {
eq($vm.asyncContext(), ctx, 'caller continuation');
for (const value of ns.contexts) eq(value, ctx, 'graph ' + file);
done++;
}, $vm.abort));
}
gc();
drainMicrotasks();
eq(done, 2, 'disjoint graphs completed');
eq($vm.asyncContext(), undefined, 'outside graphs');
let rejected = 0;
scoped(A, () => import('./fixtures/throw.js').then($vm.abort, e => {
eq(e.message, 'module throw', 'evaluation rejection');
eq($vm.asyncContext(), A, 'rejection caller');
rejected++;
}));
scoped(B, () => import('./fixtures/missing.js').then($vm.abort, () => {
eq($vm.asyncContext(), B, 'load rejection caller');
rejected++;
}));
drainMicrotasks();
eq(rejected, 2, 'rejections completed');
eq(contextLog[0], A, 'throwing body');
eq($vm.asyncContext(), undefined, 'outside rejection');
let deferred = false;
scoped(A, () => import.defer('./fixtures/deferred-parent.js').then(ns => {
scoped(B, () => {
eq(ns.rootContext, B, 'deferred root evaluates at access');
eq(ns.dependencyContext, A, 'TLA dependency evaluates in importer');
});
deferred = true;
}, $vm.abort));
gc();
drainMicrotasks();
eq(deferred, true, 'scoped deferred graph completed');
eq($vm.asyncContext(), undefined, 'outside deferred graph');
27 changes: 27 additions & 0 deletions .github/openclaw/qualification/als/realm-require.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AsyncLocalStorage } from 'node:async_hooks';
import vm from 'node:vm';
import { createRequire } from 'node:module';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const dir = mkdtempSync(join(tmpdir(), 'w137-realm-'));
const als = new AsyncLocalStorage();
const A={name:'A'},B={name:'B'};
globalThis.w137RealmRead=()=>als.getStore();
function eq(a,b,label){if(a!==b)throw Error(label);}
try {
for (const name of ['a','b','sync']) {
writeFileSync(join(dir,name+'-leaf.mjs'), 'export const value=globalThis.w137RealmRead();\n');
writeFileSync(join(dir,name+'.mjs'), `import {value} from './${name}-leaf.mjs'; export const captured=[value,globalThis.w137RealmRead()];\n`);
}
const context=vm.createContext({als});
const run=file=>new vm.Script(`import(${JSON.stringify(pathToFileURL(join(dir,file+'.mjs')).href)})`,{importModuleDynamically:specifier=>import(specifier)}).runInContext(context);
const [a,b]=await Promise.all([als.run(A,()=>run('a')),als.run(B,()=>run('b'))]);
for(const x of a.captured)eq(x,A,'realm A');
for(const x of b.captured)eq(x,B,'realm B');
const required=als.run(A,()=>createRequire(import.meta.url)(join(dir,'sync.mjs')));
for(const x of required.captured)eq(x,A,'synchronous require ESM');
eq(als.getStore(),undefined,'outside realm');
console.log(JSON.stringify({passed:true,vmGraphs:2,syncRequire:1}));
} finally { delete globalThis.w137RealmRead;rmSync(dir,{recursive:true,force:true}); }
51 changes: 51 additions & 0 deletions .github/openclaw/qualification/als/retention.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AsyncLocalStorage } from 'node:async_hooks';
import { heapStats } from 'bun:jsc';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { setImmediate as turn } from 'node:timers/promises';
const dir = mkdtempSync(join(tmpdir(), 'w137-retention-'));
const als = new AsyncLocalStorage();
const refs = [];
const payloadRefs = [];
const metrics = [];
const bytesPerStore = 128 * 1024;
const batchSize = 256;
const batches = 6;
globalThis.w137ReadId = () => als.getStore()?.id;
function schedule(i) {
const store = { id: i, payload: new Uint8Array(bytesPerStore).fill(i & 255) };
refs.push(new WeakRef(store));
payloadRefs.push(new WeakRef(store.payload), new WeakRef(store.payload.buffer));
const file = join(dir, i + '.mjs');
const reject = i % 3 === 0;
writeFileSync(file, `await Promise.resolve(); const id=globalThis.w137ReadId(); if(id!==${i}) throw Error('context:'+id); ${reject ? "throw Error('expected-rejection');" : 'export const value=1;'}\n`);
return als.run(store, () => import(pathToFileURL(file).href)).then(
() => { if (reject) throw Error('missing rejection'); },
error => { if (!reject || error.message !== 'expected-rejection') throw error; }
);
}
async function collect() {
await turn(); Bun.gc(true); await turn(); Bun.gc(true); await turn();
}
try {
await collect();
metrics.push({ batch: 0, ...heapStats(), memory: process.memoryUsage() });
for (let batch = 0; batch < batches; batch++) {
for (let offset = 0; offset < batchSize; offset += 16)
await Promise.all(Array.from({ length: 16 }, (_, j) => schedule(batch * batchSize + offset + j)));
await collect();
const alive = refs.reduce((n, ref) => n + (ref.deref() !== undefined), 0);
const payloadsAlive = payloadRefs.reduce((n, ref) => n + (ref.deref() !== undefined), 0);
metrics.push({ batch: batch + 1, stores: refs.length, alive, payloadsAlive, ...heapStats(), memory: process.memoryUsage() });
if (alive || payloadsAlive) throw Error(`retained ${alive}/${refs.length} contexts or ${payloadsAlive} payload objects`);
if (als.getStore() !== undefined) throw Error('context leaked outside import');
}
const growth = metrics.at(-1).extraMemorySize - metrics[0].extraMemorySize;
if (growth > 8 * 1024 * 1024) throw Error('excessive external heap growth: ' + growth);
console.log(JSON.stringify({ passed: true, bytesPerStore, batchSize, batches, allocatedPayloadBytes: bytesPerStore*batchSize*batches, metrics }, null, 2));
} catch (error) {
console.log(JSON.stringify({ passed: false, error: String(error), bytesPerStore, batchSize, batches, metrics }, null, 2));
process.exitCode = 1;
} finally { delete globalThis.w137ReadId; rmSync(dir, {recursive:true,force:true}); }
56 changes: 56 additions & 0 deletions .github/openclaw/qualification/engine-limit-probe.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#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 <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;
}

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);
return passed ? 0 : 1;
}
13 changes: 13 additions & 0 deletions .github/openclaw/qualification/patches/002-upstream-mimalloc.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/scripts/build/deps/mimalloc.ts b/scripts/build/deps/mimalloc.ts
index 2984efa9..76238c1f 100644
--- a/scripts/build/deps/mimalloc.ts
+++ b/scripts/build/deps/mimalloc.ts
@@ -12,7 +12,7 @@

import type { Dependency, DirectBuild } from "../source.ts";

-const MIMALLOC_COMMIT = "eab09015a5850ae18fc43ccfaa5bbe8272992314";
+const MIMALLOC_COMMIT = "7a828c116d96bb9a66f6c2e3417c739cbbfdb23c";

export const mimalloc: Dependency = {
name: "mimalloc",
Loading