Skip to content
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "549170099226f816a4b204ea1d8fa102fb79eefa";
export const WEBKIT_VERSION = "autobuild-preview-pr-362-084879ae";
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
111 changes: 110 additions & 1 deletion test/js/node/inspector/inspector-profiler.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { bunEnv, bunExe, isASAN, isDebug, tempDir } from "harness";
import inspector from "node:inspector";
import inspectorPromises from "node:inspector/promises";

Expand Down Expand Up @@ -686,3 +686,112 @@
expect(inspectorPromises.waitForDebugger).toBe(inspector.waitForDebugger);
});
});

// JSC's BasicBlockLocation::getExecutedRanges() computes the executed
// sub-ranges of a basic block by splitting it around one gap per enclosed
// function body. It used to do that with a selection sort (repeated min-scan
// plus Vector::removeAt), so a module with N top-level functions made
// Profiler.takePreciseCoverage cost O(N^2). The function-gap list interleaves
// declarations and expressions (decls are inserted first, then exprs), so the
// fixture below also exercises the sort on unsorted input.
const manyFunctionsCoverageFixture = `
import { Session } from "node:inspector/promises";
import vm from "node:vm";

const N = Number(process.argv[2]);
let src = "";
for (let i = 0; i < N; i++) {
if (i % 3 === 1) src += \`var fn\${i} = function(a){ if(a>\${i}) return a*\${i}; return -a; };\\n\`;
else src += \`function fn\${i}(a){ if(a>\${i}) return a*\${i}; return -a; }\\n\`;
}
src += "({ fn0, fn1, fn2 });\\n";

const session = new Session();
session.connect();
await session.post("Profiler.enable");
await session.post("Profiler.startPreciseCoverage", { callCount: true, detailed: true });

const url = "file:///many-functions.js";
const exported = vm.runInThisContext(src, { filename: url });
exported.fn0(0);
exported.fn1(0);
exported.fn2(5);

const t0 = performance.now();
const coverage = await session.post("Profiler.takePreciseCoverage");
const elapsed = performance.now() - t0;

await session.post("Profiler.stopPreciseCoverage");
session.disconnect();

const entry = coverage.result.find(s => s.url === url);
// The entry with the most block-level ranges is the one whose BasicBlockLocation
// spans the whole script body with one gap per enclosed function; its ranges[1..]
// are the getExecutedRanges() output this test is exercising.
let topLevel = entry.functions[0];
for (const f of entry.functions) if (f.ranges.length > topLevel.ranges.length) topLevel = f;
// ranges[0] is the synthetic whole-function range; ranges[1..] are the
// getExecutedRanges() output, which a correct sort emits in ascending order.
const blockRanges = topLevel.ranges.slice(1);
process.stdout.write(
JSON.stringify({
elapsed,
functions: entry.functions.length,
topLevelRanges: topLevel.ranges.length,
sorted: blockRanges.every((r, i) => i === 0 || blockRanges[i - 1].startOffset <= r.startOffset),
}),
);
`;

async function runManyFunctionsCoverage(n: number) {
using dir = tempDir("inspector-many-fns", { "run.mjs": manyFunctionsCoverageFixture });
await using proc = Bun.spawn({
cmd: [bunExe(), "run.mjs", String(n)],
Comment thread
robobun marked this conversation as resolved.
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
return JSON.parse(stdout) as {
elapsed: number;
functions: number;
topLevelRanges: number;
sorted: boolean;
};
}

describe("Profiler.takePreciseCoverage with many top-level functions", () => {
test("splits the enclosing block around every function body", async () => {
const N = 120;
const out = await runManyFunctionsCoverage(N);
// One entry per user function plus at least the whole-script entry.
expect(out.functions).toBeGreaterThanOrEqual(N + 1);
// getExecutedRanges() returns gaps+1 sub-ranges; buildScriptCoverageList
// prepends one function-level range and filters end<start, so the
// top-level entry carries ~N+1 block ranges. A broken comparator would
// emit mostly end<start ranges and leave only a handful here.
expect(out.topLevelRanges).toBeGreaterThanOrEqual(N);

Check warning on line 776 in test/js/node/inspector/inspector-profiler.test.ts

View check run for this annotation

Claude / Claude Code Review

Replacement `sorted` assertion is also vacuous — buildScriptCoverageList re-sorts blocks

The `sorted` check that replaced `allValid` in e5594f4 is unfortunately just as vacuous — `buildScriptCoverageList` chains `.sort((a, b) => a[0] - b[0])` right after the `.filter()` on inspector.ts:309, so `ranges[1..]` is ascending by `startOffset` no matter what order JSC emits. My earlier suggestion missed that the sort sits on the same line as the filter it cited; sorry for the misdirection. Only the `topLevelRanges >= N` check is load-bearing here — I'd just drop `sorted` from the fixture o
Comment thread
robobun marked this conversation as resolved.
expect(out.sorted).toBe(true);
});

// Under a debug+ASAN build the linear per-item cost of JSON serialisation
// and buildScriptCoverageList dominates at these sizes, so the ratio sits
// near 4 with or without the quadratic term; only release builds see it.
test.skipIf(isDebug || isASAN)(
"scales sub-quadratically in top-level function count",
async () => {
const small = await runManyFunctionsCoverage(4_000);
const large = await runManyFunctionsCoverage(16_000);
// A 4x increase in functions grows a quadratic term 16x. The selection
// sort in getExecutedRanges() made the release-build ratio here ~12;
// with an O(n log n) sort the remaining work is linear and the ratio
// sits near 4. A 20 ms floor guards against a near-zero small run.
const ratio = large.elapsed / Math.max(small.elapsed, 20);
expect({ small: small.elapsed, large: large.elapsed, ratio }).toSatisfy(r => r.ratio < 8);
Comment thread
robobun marked this conversation as resolved.
Outdated
},
30_000,
Comment thread
robobun marked this conversation as resolved.
Outdated
);
});
Loading