Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ env:

jobs:
autofix:
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') }}
name: Format
runs-on: ubuntu-latest
concurrency:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ env:

jobs:
lint-js:
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') }}
name: "Lint JavaScript"
runs-on: ubuntu-latest
steps:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/openclaw-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ concurrency:

jobs:
linux-x64:
if: github.repository == 'openclaw/bun'
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') && (github.repository == 'openclaw/bun') }}
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 115
env:
Expand Down Expand Up @@ -155,7 +155,7 @@ jobs:
retention-days: 7

darwin-arm64:
if: github.repository == 'openclaw/bun'
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') && (github.repository == 'openclaw/bun') }}
runs-on: macos-15
timeout-minutes: 115
env:
Expand Down
550 changes: 105 additions & 445 deletions .github/workflows/openclaw-release.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/packages-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ env:

jobs:
bun-plugin-svelte:
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/rust-lints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ env:

jobs:
clippy:
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') }}
name: cargo clippy
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -74,6 +75,7 @@ jobs:
run: cargo check --workspace --all-targets --keep-going

miri:
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') }}
name: cargo miri test
runs-on: ubuntu-latest
steps:
Expand All @@ -96,6 +98,7 @@ jobs:
run: bun run rust:miri

lolhtml:
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') }}
# The vendored lol-html is a fork (oven-sh/lol-html, `bun` branch) carrying
# content-handler suspension, and its own test suite is the only thing that
# guards the fork's invariants: nothing in the Bun test suite reaches, for
Expand Down Expand Up @@ -125,7 +128,7 @@ jobs:
mordant:
name: mordant
runs-on: ubuntu-latest
if: github.event_name != 'merge_group'
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') && (github.event_name != 'merge_group') }}
# Advisory while the pack is new: shows up on the PR, does not block it.
continue-on-error: true
env:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/source-lints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ env:

jobs:
source-lints:
if: ${{ !startsWith(github.head_ref, 'claude/diag-parked-worker-gc-') }}
name: "Source lints"
runs-on: ubuntu-latest
timeout-minutes: 10
Expand Down
20 changes: 20 additions & 0 deletions scripts/openclaw-release/gc-r2-async-clear.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--- a/src/jsc/bindings/bindings.cpp
+++ b/src/jsc/bindings/bindings.cpp
@@ -3114,9 +3114,17 @@
global->moduleLoader()->removeEntry(identifier); // takes the loader's cellLock itself
}

+static NEVER_INLINE SUPPRESS_ASAN void clearStackBelowCaller();
+
void JSC__VM__collectAsync(JSC::VM* vm, bool full)
{
JSC::JSLockHolder lock(*vm);
+ static const bool clearAsyncStack = [] {
+ const char* value = std::getenv("BUN_DIAG_ASYNC_GC_CLEAR");
+ return value && value[0] == '1';
+ }();
+ if (clearAsyncStack)
+ clearStackBelowCaller();
if (full)
vm->heap.collectAsync(JSC::CollectionScope::Full);
else
21 changes: 21 additions & 0 deletions scripts/openclaw-release/gc-r2-config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import { pathToFileURL } from "node:url";

export const revisions = {
s8: "6aba42d75d4bcee905087fa518cb2df6ff49bb0d",
s7: "807211511f81e0a6639bc9eb41d67120f6b85316",
};
export const engines = {
s8: "419b5a0470fc059a610e98697691bf6f7cedd841",
s7: "a2976f02e69fde8ccc45dc8fb346173f9d63f8a1",
};
export function variantsFor(experiment) {
assert(["bisect", "async-clear"].includes(experiment));
return experiment === "bisect" ? [
{ id: "s8-bun-s7-engine", source: revisions.s8, engineRef: revisions.s7, kind: "bisect" },
{ id: "s7-bun-s8-engine", source: revisions.s7, engineRef: revisions.s8, kind: "bisect" },
] : [{ id: "async-clear", source: revisions.s8, engineRef: revisions.s8, kind: "async-clear" }];
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
console.log(`matrix=${JSON.stringify({ include: variantsFor(process.argv[2]) })}`);
}
52 changes: 52 additions & 0 deletions scripts/openclaw-release/gc-r2-prepare.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";

const [sourceArg, engineRef, expectedSource, kind = "bisect"] = process.argv.slice(2);
assert(sourceArg && engineRef && expectedSource);
assert(["bisect", "async-clear"].includes(kind));
const source = resolve(sourceArg);
const git = (...args) => execFileSync("git", ["-C", source, ...args], { encoding: "utf8" }).trim();
assert.equal(git("rev-parse", "HEAD"), expectedSource);
const manifestPath = "scripts/build/deps/webkit-artifacts.json";
const manifestText = execFileSync("git", ["show", `${engineRef}:${manifestPath}`], { encoding: "utf8" });
const manifest = JSON.parse(manifestText);
const expectedEngines = {
"807211511f81e0a6639bc9eb41d67120f6b85316": "a2976f02e69fde8ccc45dc8fb346173f9d63f8a1",
"6aba42d75d4bcee905087fa518cb2df6ff49bb0d": "419b5a0470fc059a610e98697691bf6f7cedd841",
};
assert.equal(manifest.version, expectedEngines[engineRef]);
assert.equal(manifest.repository, "openclaw/WebKit");
writeFileSync(join(source, manifestPath), manifestText);
let compatibilityPatch;
let compatibilityPatchSha256;
if (kind === "async-clear") {
execFileSync("git", ["-C", source, "apply", resolve("scripts/openclaw-release/gc-r2-async-clear.patch")], { stdio: "inherit" });
} else {
assert.notEqual(expectedSource, engineRef);
assert(expectedSource in expectedEngines);
// The engine changed coverage-only APIs. These adapters do not touch GC or worker code.
compatibilityPatch = expectedSource === "6aba42d75d4bcee905087fa518cb2df6ff49bb0d"
? "gc-r2-s8-old-engine.patch" : "gc-r2-s7-new-engine.patch";
const patchPath = resolve("scripts/openclaw-release", compatibilityPatch);
compatibilityPatchSha256 = createHash("sha256").update(readFileSync(patchPath)).digest("hex");
execFileSync("git", ["-C", source, "apply", patchPath], { stdio: "inherit" });
}
const file = "test/js/node/worker_threads/worker_threads.test.ts";
const text = readFileSync(join(source, file), "utf8");
const start = text.indexOf(' test.concurrent("wakes a parked worker for an immediate GC timer"');
assert(start >= 0);
const caseHash = createHash("sha256").update(text.slice(start, text.indexOf("\n test(", start))).digest("hex");
assert.equal(caseHash, "bad4f3c5c0bd81289843ce29719182dc536ecffc4b2c247e1882095c01f56efc");
const modified = git("diff", "--name-only").split("\n").filter(Boolean).sort();
const expectedModified = kind === "async-clear" ? ["src/jsc/bindings/bindings.cpp"]
: [manifestPath, "src/jsc/bindings/CodeCoverage.cpp", ...(expectedSource === "6aba42d75d4bcee905087fa518cb2df6ff49bb0d" ? ["src/jsc/bindings/JSInspectorProfiler.cpp"] : [])];
assert.deepEqual(modified, expectedModified.sort());
const metadata = { source: expectedSource, engine: manifest.version, engineRef, kind, caseHash, modified,
compatibilityPatch, compatibilityPatchSha256,
engineManifestSha256: createHash("sha256").update(manifestText).digest("hex") };
mkdirSync(join(source, "build"), { recursive: true });
writeFileSync(join(source, "build/gc-r2-metadata.json"), JSON.stringify(metadata, null, 2) + "\n");
console.log(JSON.stringify(metadata));
86 changes: 86 additions & 0 deletions scripts/openclaw-release/gc-r2-probe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { engines, revisions, variantsFor } from "./gc-r2-config.mjs";

const [experiment, outputArg] = process.argv.slice(2);
assert.equal(process.platform, "win32");
const output = resolve(outputArg);
mkdirSync(output, { recursive: true });
const file = "test/js/node/worker_threads/worker_threads.test.ts";
const name = "wakes a parked worker for an immediate GC timer";
const caseHash = "bad4f3c5c0bd81289843ce29719182dc536ecffc4b2c247e1882095c01f56efc";
const commonEnv = { ...process.env, BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1", BUN_GARBAGE_COLLECTOR_LEVEL: "1",
BUN_JSC_randomIntegrityAuditRate: "1.0", BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0", BUN_DEBUG_QUIET_LOGS: "1", NO_COLOR: "1" };
for (const key of ["NODE_OPTIONS", "BUN_TEST_DRAIN_EVENT_LOOP", "BUN_DIAG_GC_WAKE_DIR", "BUN_DIAG_GC_WAKE_MODE", "BUN_DIAG_ASYNC_GC_CLEAR"]) delete commonEnv[key];
const cases = [
{ id: "original-s8", source: revisions.s8, engine: engines.s8, exe: resolve("s8-runtime/bun-windows-aarch64/bun.exe") },
...variantsFor(experiment).flatMap(variant => {
const meta = JSON.parse(readFileSync(`runtime/gc-r2-build-${variant.id}/gc-r2-metadata.json`, "utf8"));
assert.equal(meta.source, variant.source);
assert.equal(meta.engineRef, variant.engineRef);
assert.equal(meta.engine, variant.engineRef === revisions.s8 ? engines.s8 : engines.s7);
assert.equal(meta.kind, variant.kind);
assert.equal(meta.caseHash, caseHash);
const entry = { id: variant.id, source: meta.source, engine: meta.engine,
exe: resolve(`runtime/gc-r2-build-${variant.id}/bun-windows-aarch64/bun.exe`), metadata: meta };
return experiment === "async-clear" ? [
{ ...entry, id: "async-clear-off", clear: "0" }, { ...entry, id: "async-clear-on", clear: "1" },
] : [entry];
}),
{ id: "original-s7", source: revisions.s7, engine: engines.s7, exe: resolve("s7-runtime/bun-windows-aarch64/bun.exe") },
];
for (const entry of cases) {
entry.cwd = resolve(entry.source === revisions.s8 ? "s8" : "s7");
const source = readFileSync(join(entry.cwd, file), "utf8");
const start = source.indexOf(` test.concurrent("${name}"`);
assert(start >= 0);
assert.equal(createHash("sha256").update(source.slice(start, source.indexOf("\n test(", start))).digest("hex"), caseHash);
const binary = readFileSync(entry.exe);
assert.equal(binary.readUInt16LE(binary.readUInt32LE(0x3c) + 4), 0xaa64);
entry.sha256 = createHash("sha256").update(binary).digest("hex");
const identity = spawnSync(entry.exe, ["-p", "JSON.stringify({revision:Bun.revision,webkit:process.versions.webkit,arch:process.arch})"], { env: commonEnv, encoding: "utf8" });
assert.equal(identity.status, 0);
entry.runtime = JSON.parse(identity.stdout);
assert.deepEqual(entry.runtime, { revision: entry.source, webkit: entry.engine, arch: "arm64" });
}
const results = { experiment, caseHash, cases, trials: [] };
const save = () => writeFileSync(join(output, "results.json"), JSON.stringify(results, null, 2) + "\n");
save();
for (let round = 1; round <= 10; round++) {
for (const entry of round % 2 ? cases : [...cases].reverse()) {
const label = `${String(round).padStart(2, "0")}-${entry.id}`;
const env = { ...commonEnv, ...(entry.clear === undefined ? {} : { BUN_DIAG_ASYNC_GC_CLEAR: entry.clear }) };
const start = Date.now();
const child = spawnSync(entry.exe, ["test", file, "-t", name], { cwd: entry.cwd, env, encoding: "utf8", maxBuffer: 32 * 1024 * 1024,
// A process watchdog only; the fixture's ten-second limit is unchanged.
timeout: 30_000 });
const text = ((child.stdout ?? "") + (child.stderr ?? "")).replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
writeFileSync(join(output, `${label}.log`), text);
const outcome = child.error || child.signal ? "invalid"
: child.status === 0 && /\b1 pass\b/.test(text) && /\b0 fail\b/.test(text) ? "pass"
: child.status !== 0 && /\b1 fail\b/.test(text) && text.includes(name) && /timed out after 10000ms/.test(text) ? "timeout" : "invalid";
results.trials.push({ round, id: entry.id, outcome, durationMs: Date.now() - start, exitCode: child.status, signal: child.signal, error: child.error?.message });
save();
console.log(`${label}: ${outcome} (${Date.now() - start} ms)`);
assert.notEqual(outcome, "invalid", `Invalid diagnostic trial ${label}`);
}
}
results.counts = Object.fromEntries(cases.map(entry => [entry.id, Object.fromEntries(["pass", "timeout", "invalid"].map(outcome => [outcome, results.trials.filter(t => t.id === entry.id && t.outcome === outcome).length]))]));
const controlsValid = results.counts["original-s8"].timeout >= 2 && results.counts["original-s7"].timeout === 0;
results.controlsValid = controlsValid;
if (experiment === "bisect") {
const a = results.counts["s8-bun-s7-engine"], b = results.counts["s7-bun-s8-engine"];
results.interpretation = !controlsValid ? "controls-did-not-confirm"
: a.timeout === 0 && b.timeout >= 2 ? "follows-engine"
: a.timeout >= 2 && b.timeout === 0 ? "follows-bun"
: "interaction-or-inconclusive";
} else {
results.interpretation = !controlsValid ? "controls-did-not-confirm"
: results.counts["async-clear-off"].timeout >= 2 && results.counts["async-clear-on"].timeout === 0 ? "clear-call-changes-outcome-not-root-proof" : "inconclusive-or-no-effect";
}
save();
console.log(JSON.stringify({ counts: results.counts, interpretation: results.interpretation }));
assert(controlsValid, "Original signed controls did not confirm the known regression");
11 changes: 11 additions & 0 deletions scripts/openclaw-release/gc-r2-s7-new-engine.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- a/src/jsc/bindings/CodeCoverage.cpp
+++ b/src/jsc/bindings/CodeCoverage.cpp
@@ -23,7 +23,7 @@

size_t functionStartOffset = basicBlocks.size();

- const Vector<std::tuple<bool, unsigned, unsigned>>& functionRanges = vm.functionHasExecutedCache()->getFunctionRanges(sourceID);
+ const auto& functionRanges = vm.functionHasExecutedCache()->getFunctionRanges(sourceID);

basicBlocks.reserveCapacity(functionRanges.size() + basicBlocks.size());

36 changes: 36 additions & 0 deletions scripts/openclaw-release/gc-r2-s8-old-engine.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--- a/src/jsc/bindings/CodeCoverage.cpp
+++ b/src/jsc/bindings/CodeCoverage.cpp
@@ -28,10 +28,6 @@
basicBlocks.reserveCapacity(functionRanges.size() + basicBlocks.size());

for (const auto& functionRange : functionRanges) {
- // A generator or an async function is one function, and a static block is none.
- auto parseMode = std::get<3>(functionRange);
- if (JSC::isProgramOrModuleParseMode(parseMode) || JSC::isGeneratorOrAsyncFunctionBodyParseMode(parseMode) || parseMode == JSC::SourceParseMode::ClassStaticBlockMode)
- continue;
BasicBlockRange range;
range.m_hasExecuted = std::get<0>(functionRange);
range.m_startOffset = static_cast<int>(std::get<1>(functionRange));
--- a/src/jsc/bindings/JSInspectorProfiler.cpp
+++ b/src/jsc/bindings/JSInspectorProfiler.cpp
@@ -183,7 +183,7 @@
auto scripts = JSON::Array::create();
for (auto& provider : providers) {
SourceID sourceID = provider->asID();
- auto blocks = profiler->getBasicBlocksForSourceIDWithoutFunctionRange(sourceID, vm, JSC::ControlFlowProfiler::Gaps::With);
+ auto blocks = profiler->getBasicBlocksForSourceIDWithoutFunctionRange(sourceID, vm);
auto functionRanges = vm.functionHasExecutedCache()->getFunctionRanges(sourceID);
if (blocks.isEmpty() && functionRanges.isEmpty())
continue;
@@ -209,11 +209,6 @@

auto functionArray = JSON::Array::create();
for (const auto& functionRange : functionRanges) {
- // V8 has a static block as a function, and a generator or an async function once.
- // The program has the whole-script entry.
- auto parseMode = std::get<3>(functionRange);
- if (JSC::isProgramOrModuleParseMode(parseMode) || JSC::isGeneratorOrAsyncFunctionBodyParseMode(parseMode))
- continue;
auto range = JSON::Array::create();
range->pushDouble(static_cast<double>(std::get<1>(functionRange)));
range->pushDouble(static_cast<double>(std::get<2>(functionRange)) + 1);
Loading