Skip to content

Instantly share code, notes, and snippets.

@ehfeng
Last active April 30, 2026 17:43
Show Gist options
  • Select an option

  • Save ehfeng/95a8d8ff04f066552ed243e1a351ab67 to your computer and use it in GitHub Desktop.

Select an option

Save ehfeng/95a8d8ff04f066552ed243e1a351ab67 to your computer and use it in GitHub Desktop.
CDP connection drop repro — stress tests + upstream traffic analysis

CDP Connection Drop Repro

Slack: https://onkernel.slack.com/archives/C0925PJ42R4/p1777402605274079 Gist: https://gist.github.com/ehfeng/95a8d8ff04f066552ed243e1a351ab67

Felicity (org au2d7qfvqkhaxcqzmbd9xvch) reported CDP connections dropping with code 1006 during page.evaluate on tickertrends.io. They run two CDP connections (Stagehand + Playwright) with a screenshot loop every ~1s.

Root cause: OOM killer silently kills the Chrome renderer

We ran oom-forensics.mjs which triggers OOM inside the VM and then inspects dmesg. The Linux OOM killer SIGKILL's the Chrome renderer process — and it looks exactly like the customer's symptoms:

dmesg:
  chromium invoked oom-killer: gfp_mask=0x140dca
  oom-kill: task=chromium, pid=2956, uid=1000
  Out of memory: Killed process 2956 (chromium) total-vm:1459995184kB, anon-rss:7379164kB

What happens:

  1. The renderer process (which owns the page, CDP WebSocket context, and neko display) gets SIGKILL'd
  2. TCP connections die instantly — no WebSocket close frame → code 1006
  3. Chrome's main process survivespgrep chromium still finds it
  4. No crash dump is generated — SIGKILL doesn't give Chrome a chance to write one
  5. VM-level free -m looks fine after — the OOM'd renderer's memory is reclaimed

This explains everything about Felicity's case:

  • Code 1006 — SIGKILL means TCP reset, no graceful WebSocket close
  • Neko died first — renderer owns the display; neko detects the dead X11 window and closes with 1001 before the CDP connection fully tears down
  • Sayan saw "no memory pressure" — he checked VM-level free, which was fine. The OOM killer targets individual processes by RSS + oom_score_adj, not total VM usage. Chrome renderers have oom_score_adj=300 (high priority kill target)
  • No crash evidence — SIGKILL leaves no crash dump, no Chrome error log, no "Target crashed" CDP event
  • Main process stayed alive — only the renderer died, so the session looked healthy from the API perspective

For Felicity: tickertrends.io is a data-heavy financial dashboard. The page.evaluate clicks trigger React re-renders that could cause the renderer's RSS to spike. A single renderer can hit 3-4GB on a heavy page while the VM overall shows plenty of free memory. The OOM killer targets the renderer specifically.

Next steps:

  1. Monitor per-renderer RSS (not just VM-level memory) — the renderer process RSS is the signal
  2. Consider increasing oom_score_adj protection for Chrome renderers, or setting memory cgroup limits with better feedback
  3. Surface OOM kills to customers via browser events API — currently they're invisible

Finding: upstream goes silent during page.evaluate

We also ran tcpdump inside the VM during a 20s CPU-blocking page.evaluate and found Chrome stops sending all traffic to the proxy:

Normal:   257 packets in ~8s  (steady bidirectional)
Blocked:    7 packets in 20s  (15.1s gap with zero packets)
Resumed:   51 packets in ~10s (back to normal)

Chrome's CDP WebSocket lives on the IO thread, but with the main thread blocked there are no CDP responses to send. The connection between proxy and Chrome goes completely idle. This is not itself the cause of the drop (we confirmed no idle timeout in the proxy code path), but it explains why screenshots fail during page.evaluate.


Running the tests

npm install
export KERNEL_API_KEY=sk_...
Script What it tests
oom-forensics.mjs OOM crash + dmesg/process forensics (the key finding)
test-upstream.mjs tcpdump inside the VM to measure upstream traffic gaps
repro.mjs Single-connection stress (CPU block, OOM, memory, canvas, disk, etc.)
race.mjs Multi-connection races (parallel commands, blocker+flood, crossfire, etc.)
node oom-forensics.mjs                # the important one
BLOCK_SECONDS=20 node test-upstream.mjs
SCENARIO=oom node repro.mjs
SCENARIO=crossfire node race.mjs
SCENARIO=all node repro.mjs          # run everything

Set TARGET_URL to override the default (https://kernel.sh).


Full results (2026-04-30)

Single-connection stress

Scenario What it does Dropped?
cpu-30 Block main thread 30s No
cpu-45 Block main thread 45s No
cpu-60 Block main thread 60s No
cpu-90 Block main thread 90s No
mem-cpu Allocate 2GB then block 30s No
canvas 50 x 4096px canvases, fill + readback (4.7GB) No
felicity DOM scan + synthetic clicks + 10s block No
fill-disk Write 500MB to /tmp No
window-spam 100 window.open calls No
oom Allocate until Chrome crashes Yes
kill-chrome SIGKILL chromium from VM Yes

Multi-connection races

Scenario What it does Conns Dropped?
cmd-flood 500 parallel CDP commands in batches of 100 1 No
blocker-flood 1 blocks 20s, 3 flood commands 4 No
eval-nav-race evaluate + goto + screenshot simultaneously, 10 rounds 1 No
crossfire Both fire screenshot + evaluate + DOM mutation 30s 2 No
connect-cycle Rapid connect/disconnect while one screenshots 29 No
parallel-ss All screenshot simultaneously 30s 5 No
parallel-ss-10 All screenshot simultaneously 30s 10 No
mixed screenshot + evaluate + title, no delay, 30s 4 No

Takeaways

  1. The Linux OOM killer silently SIGKILL's Chrome renderers. This produces code 1006, no crash dump, no CDP event, and no visible memory pressure at the VM level. It's the most likely cause of Felicity's drops.

  2. The upstream connection goes silent during page.evaluate. 15.1s with zero packets. Not the direct cause (no timeout in the proxy), but explains why screenshots fail.

  3. Nothing else drops the connection. 60s CPU block, 4.7GB canvas memory, 10 parallel connections, 500 concurrent commands, evaluate+navigate races — all survived.

import Kernel from "@onkernel/sdk";
import { chromium } from "playwright-core";
const kernel = new Kernel();
async function exec(sessionId, cmd, timeout = 10) {
try {
const r = await kernel.browsers.process.exec(sessionId, {
command: "bash",
args: ["-c", cmd],
timeout_sec: timeout,
as_root: true,
});
return Buffer.from(r.stdout_b64 || "", "base64").toString();
} catch (e) {
return `(exec failed: ${e.message.slice(0, 80)})`;
}
}
async function forensics(sessionId, label) {
console.log(`\n── ${label} ──`);
console.log(` mem: ${(await exec(sessionId, "free -m | grep Mem")).trim()}`);
console.log(` chrome procs: ${(await exec(sessionId, "ps aux | grep chromium | grep -v grep | wc -l")).trim()}`);
console.log(` chrome PIDs: ${(await exec(sessionId, "pgrep -f chromium | head -5 | tr '\\n' ' '")).trim()}`);
const dmesg = await exec(sessionId, "dmesg -T 2>/dev/null | grep -i 'oom\\|killed\\|segfault\\|signal\\|memory' | tail -10 || echo '(none)'");
console.log(` dmesg OOM/kill/segfault:\n${dmesg}`);
const crashes = await exec(sessionId, "find /home/kernel/.config/chromium/Crash\\ Reports -type f -name '*.dmp' -o -name '*.meta' 2>/dev/null | head -5 || echo 'none'");
console.log(` crash dumps: ${crashes.trim()}`);
const supervisor = await exec(sessionId, "tail -5 /var/log/supervisord/chromium 2>/dev/null || echo '(none)'");
console.log(` supervisor log (last 5): ${supervisor.trim()}`);
}
async function main() {
console.log("Creating session...");
const kb = await kernel.browsers.create({ timeout_seconds: 300 });
const sid = kb.session_id;
console.log(`Session: ${sid}\n`);
await forensics(sid, "BASELINE");
const browser = await chromium.connectOverCDP(kb.cdp_ws_url);
const ctx = browser.contexts()[0];
const page = ctx.pages()[0] || (await ctx.newPage());
let disconnected = false;
browser.on("disconnected", () => {
disconnected = true;
console.log(`\n🔴 DISCONNECTED at ${new Date().toISOString()}`);
});
await page.goto("https://kernel.sh", { waitUntil: "domcontentloaded", timeout: 30000 });
console.log("Page loaded.\n");
await forensics(sid, "AFTER PAGE LOAD");
// OOM with a 30s timeout so we don't hang forever
console.log("\n══ OOM: allocating until crash (30s timeout) ══\n");
const t0 = Date.now();
try {
await Promise.race([
page.evaluate(() => {
const chunks = [];
while (true) {
const arr = new Uint8Array(10 * 1024 * 1024);
arr.fill(42);
chunks.push(arr);
}
}),
new Promise((_, reject) => setTimeout(() => reject(new Error("OOM timeout after 30s")), 30000)),
]);
} catch (err) {
console.log(`Threw after ${Date.now() - t0}ms: ${err.message.split("\n")[0]}`);
}
// Wait for disconnect event to fire
await new Promise((r) => setTimeout(r, 3000));
console.log(`\nDisconnected: ${disconnected}`);
// Post-crash forensics — session is still alive, Chrome might not be
await forensics(sid, "AFTER OOM");
// Extra: check if Chrome process is gone
const chromeAlive = await exec(sid, "pgrep -f 'chromium.*remote-debugging' > /dev/null && echo ALIVE || echo DEAD");
console.log(`\nChrome main process: ${chromeAlive.trim()}`);
// Check for new crash dumps
const newCrashes = await exec(sid, "find /home/kernel/.config/chromium/Crash\\ Reports -type f -newer /proc/1/fd/0 2>/dev/null | head -10 || echo 'none'");
console.log(`New crash artifacts: ${newCrashes.trim()}`);
// Try reconnecting
console.log("\n── Reconnect test ──");
try {
const b2 = await chromium.connectOverCDP(kb.cdp_ws_url);
const p2 = b2.contexts()[0]?.pages()[0];
if (p2) {
const title = await p2.title();
console.log(`Reconnected OK, title: "${title}"`);
} else {
console.log("Reconnected but no pages");
}
await b2.close();
} catch (err) {
console.log(`Reconnect failed: ${err.message.split("\n")[0]}`);
}
console.log(`\nCleaning up ${sid}...`);
await kernel.browsers.deleteByID(sid);
console.log("Done.");
}
main().catch(console.error);
{
"name": "cdp-connection-drop-repro",
"private": true,
"type": "module",
"scripts": {
"start": "node repro.mjs"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.52.0",
"@onkernel/sdk": "^0.52.0",
"playwright-core": "^1.52.0"
}
}
import Kernel from "@onkernel/sdk";
import { chromium } from "playwright-core";
const TARGET_URL = process.env.TARGET_URL || "https://kernel.sh";
const SCENARIO = process.env.SCENARIO || "all";
const kernel = new Kernel();
async function vmExec(sessionId, command) {
try {
const res = await kernel.browsers.process.exec(sessionId, { command: "bash", args: ["-c", command], timeout_sec: 10 });
return Buffer.from(res.stdout_b64 || "", "base64").toString();
} catch {
return "(exec failed)";
}
}
async function vmStats(sessionId) {
const mem = await vmExec(sessionId, "free -m | grep Mem");
const load = await vmExec(sessionId, "cat /proc/loadavg");
console.log(` [VM] load: ${load.trim()} | ${mem.trim()}`);
}
// Connect a new CDP client, return { browser, page, id }
async function connectCDP(cdpWsUrl, id) {
const browser = await chromium.connectOverCDP(cdpWsUrl);
const ctx = browser.contexts()[0];
const page = ctx.pages()[0] || (await ctx.newPage());
return { browser, page, id };
}
// ── Scenario 1: N connections, all fire screenshots simultaneously ──
async function scenarioParallelScreenshots(cdpWsUrl, n = 5) {
console.log(`\n═══ Parallel screenshots: ${n} CDP connections, all screenshotting ═══`);
const conns = [];
for (let i = 0; i < n; i++) {
conns.push(await connectCDP(cdpWsUrl, i));
}
console.log(` ${n} connections established`);
const deadSet = new Set();
for (const c of conns) {
c.browser.on("disconnected", () => {
deadSet.add(c.id);
console.log(` 🔴 Connection ${c.id} DISCONNECTED (${deadSet.size}/${n} dead)`);
});
}
// All connections fire screenshots simultaneously for 30s
const duration = 30_000;
const start = Date.now();
const workers = conns.map(async (c) => {
let ok = 0, fail = 0;
while (Date.now() - start < duration && !deadSet.has(c.id)) {
try {
await c.page.screenshot({ type: "jpeg", quality: 50, timeout: 5000 });
ok++;
} catch (err) {
fail++;
const msg = err.message.split("\n")[0].slice(0, 80);
if (fail <= 3 || fail % 10 === 0) {
console.log(` [conn-${c.id}] fail #${fail}: ${msg}`);
}
if (err.message.includes("closed") || err.message.includes("crashed") || err.message.includes("Target")) {
return { id: c.id, ok, fail, dead: true };
}
}
}
return { id: c.id, ok, fail, dead: deadSet.has(c.id) };
});
const results = await Promise.all(workers);
for (const r of results) {
console.log(` [conn-${r.id}] ${r.ok} ok, ${r.fail} fail, dead=${r.dead}`);
}
const dropped = deadSet.size > 0;
for (const c of conns) {
try { await c.browser.close(); } catch {}
}
return dropped;
}
// ── Scenario 2: N connections, mixed commands (evaluate, screenshot, navigate) firing concurrently ──
async function scenarioMixedCommands(cdpWsUrl, n = 4) {
console.log(`\n═══ Mixed commands: ${n} connections firing evaluate+screenshot+navigate ═══`);
const conns = [];
for (let i = 0; i < n; i++) {
conns.push(await connectCDP(cdpWsUrl, i));
}
console.log(` ${n} connections established`);
const deadSet = new Set();
for (const c of conns) {
c.browser.on("disconnected", () => {
deadSet.add(c.id);
console.log(` 🔴 Connection ${c.id} DISCONNECTED`);
});
}
const duration = 30_000;
const start = Date.now();
const workers = conns.map(async (c) => {
let ok = 0, fail = 0;
const commands = ["screenshot", "evaluate", "title", "evaluate-heavy"];
let cmdIdx = 0;
while (Date.now() - start < duration && !deadSet.has(c.id)) {
const cmd = commands[cmdIdx % commands.length];
cmdIdx++;
try {
switch (cmd) {
case "screenshot":
await c.page.screenshot({ type: "jpeg", quality: 50, timeout: 3000 });
break;
case "evaluate":
await c.page.evaluate(() => document.title);
break;
case "title":
await c.page.title();
break;
case "evaluate-heavy":
await c.page.evaluate(() => {
const divs = document.querySelectorAll("div");
for (const d of divs) void d.innerText;
return divs.length;
});
break;
}
ok++;
} catch (err) {
fail++;
const msg = err.message.split("\n")[0].slice(0, 80);
if (fail <= 5 || fail % 20 === 0) {
console.log(` [conn-${c.id}] ${cmd} fail #${fail}: ${msg}`);
}
if (err.message.includes("closed") || err.message.includes("crashed") || err.message.includes("Target")) {
return { id: c.id, ok, fail, dead: true };
}
}
// No delay — fire as fast as possible
}
return { id: c.id, ok, fail, dead: deadSet.has(c.id) };
});
const results = await Promise.all(workers);
for (const r of results) {
console.log(` [conn-${r.id}] ${r.ok} ok, ${r.fail} fail, dead=${r.dead}`);
}
const dropped = deadSet.size > 0;
for (const c of conns) {
try { await c.browser.close(); } catch {}
}
return dropped;
}
// ── Scenario 3: One connection blocks, others fire rapid commands ──
async function scenarioBlockerPlusFlood(cdpWsUrl) {
console.log("\n═══ Blocker + flood: 1 connection blocks 20s, 3 others fire rapid commands ═══");
const blocker = await connectCDP(cdpWsUrl, "blocker");
const flooders = [];
for (let i = 0; i < 3; i++) {
flooders.push(await connectCDP(cdpWsUrl, `flood-${i}`));
}
console.log(" 4 connections established (1 blocker + 3 flooders)");
const deadSet = new Set();
blocker.browser.on("disconnected", () => {
deadSet.add("blocker");
console.log(" 🔴 Blocker connection DISCONNECTED");
});
for (const f of flooders) {
f.browser.on("disconnected", () => {
deadSet.add(f.id);
console.log(` 🔴 ${f.id} DISCONNECTED`);
});
}
// Start the block
const blockStart = Date.now();
const blockPromise = blocker.page
.evaluate(() => {
const end = Date.now() + 20000;
while (Date.now() < end) Math.random();
return "done";
})
.then(() => console.log(` [blocker] evaluate returned in ${Date.now() - blockStart}ms`))
.catch((err) => console.log(` [blocker] evaluate THREW after ${Date.now() - blockStart}ms: ${err.message.split("\n")[0].slice(0, 100)}`));
// Flooders fire commands as fast as possible
const floodDuration = 25_000;
const floodStart = Date.now();
const floodWorkers = flooders.map(async (f) => {
let ok = 0, fail = 0;
while (Date.now() - floodStart < floodDuration && !deadSet.has(f.id)) {
try {
// Alternate between screenshot and evaluate
if (ok % 2 === 0) {
await f.page.screenshot({ type: "jpeg", quality: 50, timeout: 3000 });
} else {
await f.page.evaluate(() => {
void document.querySelectorAll("*").length;
return Date.now();
});
}
ok++;
} catch (err) {
fail++;
const msg = err.message.split("\n")[0].slice(0, 80);
if (fail <= 3 || fail % 10 === 0) {
const elapsed = ((Date.now() - floodStart) / 1000).toFixed(1);
console.log(` [${f.id}] fail #${fail} at ${elapsed}s: ${msg}`);
}
if (err.message.includes("closed") || err.message.includes("crashed") || err.message.includes("Target")) {
return { id: f.id, ok, fail, dead: true };
}
}
}
return { id: f.id, ok, fail, dead: deadSet.has(f.id) };
});
const [floodResults] = await Promise.all([Promise.all(floodWorkers), blockPromise]);
for (const r of floodResults) {
console.log(` [${r.id}] ${r.ok} ok, ${r.fail} fail, dead=${r.dead}`);
}
console.log(` [blocker] dead=${deadSet.has("blocker")}`);
const dropped = deadSet.size > 0;
try { await blocker.browser.close(); } catch {}
for (const f of flooders) {
try { await f.browser.close(); } catch {}
}
return dropped;
}
// ── Scenario 4: Rapid connect/disconnect cycling while commands are in flight ──
async function scenarioConnectDisconnectCycle(cdpWsUrl) {
console.log("\n═══ Connect/disconnect cycling: rapidly open+close connections while one is active ═══");
const main = await connectCDP(cdpWsUrl, "main");
let mainDead = false;
main.browser.on("disconnected", () => {
mainDead = true;
console.log(" 🔴 Main connection DISCONNECTED");
});
const duration = 20_000;
const start = Date.now();
let cycles = 0;
// Main connection does screenshots
const mainLoop = (async () => {
let ok = 0, fail = 0;
while (Date.now() - start < duration && !mainDead) {
try {
await main.page.screenshot({ type: "jpeg", quality: 50, timeout: 3000 });
ok++;
} catch (err) {
fail++;
if (err.message.includes("closed") || err.message.includes("crashed")) {
return { ok, fail, dead: true };
}
}
await new Promise((r) => setTimeout(r, 500));
}
return { ok, fail, dead: mainDead };
})();
// Rapid connect/disconnect cycling
const cycleLoop = (async () => {
while (Date.now() - start < duration && !mainDead) {
try {
const temp = await chromium.connectOverCDP(cdpWsUrl);
cycles++;
// Immediately fire a command
const ctx = temp.contexts()[0];
const page = ctx?.pages()[0];
if (page) {
await page.evaluate(() => document.title).catch(() => {});
}
// Disconnect immediately
await temp.close();
} catch (err) {
const msg = err.message.split("\n")[0].slice(0, 80);
if (cycles <= 3 || cycles % 20 === 0) {
console.log(` [cycle] error at cycle ${cycles}: ${msg}`);
}
}
}
})();
const [mainResult] = await Promise.all([mainLoop, cycleLoop]);
console.log(` [main] ${mainResult.ok} ok, ${mainResult.fail} fail, dead=${mainResult.dead}`);
console.log(` [cycles] ${cycles} connect/disconnect cycles completed`);
const dropped = mainResult.dead;
try { await main.browser.close(); } catch {}
return dropped;
}
// ── Scenario 5: Command flood — fire hundreds of CDP commands without waiting ──
async function scenarioCommandFlood(cdpWsUrl) {
console.log("\n═══ Command flood: fire 100 parallel CDP commands on one connection ═══");
const conn = await connectCDP(cdpWsUrl, "flood");
let dead = false;
conn.browser.on("disconnected", () => {
dead = true;
console.log(" 🔴 Connection DISCONNECTED");
});
for (let batch = 0; batch < 5 && !dead; batch++) {
console.log(` Batch ${batch}: firing 100 concurrent commands...`);
const promises = [];
for (let i = 0; i < 100; i++) {
switch (i % 4) {
case 0:
promises.push(conn.page.screenshot({ type: "jpeg", quality: 30, timeout: 10000 }).catch((e) => ({ err: e })));
break;
case 1:
promises.push(conn.page.evaluate(() => document.querySelectorAll("*").length).catch((e) => ({ err: e })));
break;
case 2:
promises.push(conn.page.evaluate(() => {
void document.body.innerHTML.length;
return Date.now();
}).catch((e) => ({ err: e })));
break;
case 3:
promises.push(conn.page.title().catch((e) => ({ err: e })));
break;
}
}
const results = await Promise.all(promises);
const ok = results.filter((r) => !r?.err).length;
const fails = results.filter((r) => r?.err).length;
const deadErrors = results.filter((r) => r?.err?.message?.includes("closed") || r?.err?.message?.includes("crashed")).length;
console.log(` Batch ${batch}: ${ok} ok, ${fails} fail (${deadErrors} dead-errors)`);
// Log sample errors
const errSamples = results.filter((r) => r?.err).slice(0, 3);
for (const e of errSamples) {
console.log(` sample error: ${e.err.message.split("\n")[0].slice(0, 100)}`);
}
if (deadErrors > 0 || dead) {
console.log(" Connection died during flood");
try { await conn.browser.close(); } catch {}
return true;
}
await new Promise((r) => setTimeout(r, 1000));
}
const dropped = dead;
try { await conn.browser.close(); } catch {}
return dropped;
}
// ── Scenario 6: Evaluate + navigate race ──
async function scenarioEvalNavigateRace(cdpWsUrl) {
console.log("\n═══ Evaluate + navigate race: start evaluate then immediately navigate ═══");
const conn = await connectCDP(cdpWsUrl, "race");
let dead = false;
conn.browser.on("disconnected", () => {
dead = true;
console.log(" 🔴 Connection DISCONNECTED");
});
for (let round = 0; round < 10 && !dead; round++) {
console.log(` Round ${round}:`);
// Fire a long evaluate and immediately navigate — this creates a race
const evalPromise = conn.page
.evaluate(() => {
const end = Date.now() + 3000;
while (Date.now() < end) Math.random();
return "eval-done";
})
.catch((e) => ({ err: e.message.split("\n")[0].slice(0, 100) }));
// Immediately try to navigate (should race with the evaluate)
const navPromise = conn.page
.goto("about:blank", { timeout: 5000 })
.catch((e) => ({ err: e.message.split("\n")[0].slice(0, 100) }));
// And fire a screenshot too
const ssPromise = conn.page
.screenshot({ type: "jpeg", quality: 50, timeout: 5000 })
.catch((e) => ({ err: e.message.split("\n")[0].slice(0, 100) }));
const [evalR, navR, ssR] = await Promise.all([evalPromise, navPromise, ssPromise]);
console.log(` eval: ${evalR?.err || "ok"}`);
console.log(` nav: ${navR?.err || "ok"}`);
console.log(` ss: ${ssR?.err || "ok"}`);
if (dead) break;
// Navigate back
try {
await conn.page.goto(TARGET_URL, { waitUntil: "domcontentloaded", timeout: 10000 });
} catch (e) {
console.log(` re-nav failed: ${e.message.split("\n")[0].slice(0, 80)}`);
}
}
const droppedR = dead;
try { await conn.browser.close(); } catch {}
return droppedR;
}
// ── Scenario 7: Two connections, cross-fire commands that invalidate each other ──
async function scenarioCrossfire(cdpWsUrl) {
console.log("\n═══ Crossfire: 2 connections, both navigate + evaluate + screenshot simultaneously ═══");
const c1 = await connectCDP(cdpWsUrl, "A");
const c2 = await connectCDP(cdpWsUrl, "B");
const deadSet = new Set();
c1.browser.on("disconnected", () => { deadSet.add("A"); console.log(" 🔴 Connection A DISCONNECTED"); });
c2.browser.on("disconnected", () => { deadSet.add("B"); console.log(" 🔴 Connection B DISCONNECTED"); });
console.log(" 2 connections established, both firing conflicting commands");
const duration = 30_000;
const start = Date.now();
async function worker(c) {
let ok = 0, fail = 0;
while (Date.now() - start < duration && !deadSet.has(c.id)) {
const batch = [
c.page.screenshot({ type: "jpeg", quality: 50, timeout: 3000 }).catch((e) => ({ err: e })),
c.page.evaluate(() => {
// Force layout reflow
const all = document.querySelectorAll("*");
for (const el of all) void el.getBoundingClientRect();
return all.length;
}).catch((e) => ({ err: e })),
c.page.evaluate(() => {
// Modify DOM
const d = document.createElement("div");
d.id = `test-${Date.now()}`;
d.textContent = "x".repeat(10000);
document.body.appendChild(d);
return d.id;
}).catch((e) => ({ err: e })),
];
const results = await Promise.all(batch);
const batchOk = results.filter((r) => !r?.err).length;
const batchFail = results.filter((r) => r?.err).length;
ok += batchOk;
fail += batchFail;
const deadErr = results.find((r) => r?.err?.message?.includes("closed") || r?.err?.message?.includes("crashed"));
if (deadErr) {
return { id: c.id, ok, fail, dead: true };
}
}
return { id: c.id, ok, fail, dead: deadSet.has(c.id) };
}
const [r1, r2] = await Promise.all([worker(c1), worker(c2)]);
console.log(` [A] ${r1.ok} ok, ${r1.fail} fail, dead=${r1.dead}`);
console.log(` [B] ${r2.ok} ok, ${r2.fail} fail, dead=${r2.dead}`);
const droppedCF = deadSet.size > 0;
try { await c1.browser.close(); } catch {}
try { await c2.browser.close(); } catch {}
return droppedCF;
}
// ── Main ──
async function main() {
console.log("Creating Kernel browser session...");
const kernelBrowser = await kernel.browsers.create({ timeout_seconds: 300 });
const sessionId = kernelBrowser.session_id;
const cdpWsUrl = kernelBrowser.cdp_ws_url;
console.log(`Session: ${sessionId}`);
console.log(`Target URL: ${TARGET_URL}\n`);
await vmStats(sessionId);
// Navigate on one connection first to get page loaded
const setup = await connectCDP(cdpWsUrl, "setup");
await setup.page.goto(TARGET_URL, { waitUntil: "domcontentloaded", timeout: 30000 });
console.log("Page loaded via setup connection.");
await setup.browser.close();
const scenarios = {
"parallel-ss": () => scenarioParallelScreenshots(cdpWsUrl, 5),
"parallel-ss-10": () => scenarioParallelScreenshots(cdpWsUrl, 10),
mixed: () => scenarioMixedCommands(cdpWsUrl, 4),
"blocker-flood": () => scenarioBlockerPlusFlood(cdpWsUrl),
"connect-cycle": () => scenarioConnectDisconnectCycle(cdpWsUrl),
"cmd-flood": () => scenarioCommandFlood(cdpWsUrl),
"eval-nav-race": () => scenarioEvalNavigateRace(cdpWsUrl),
crossfire: () => scenarioCrossfire(cdpWsUrl),
};
const toRun = SCENARIO === "all" ? Object.entries(scenarios) : [[SCENARIO, scenarios[SCENARIO]]];
if (SCENARIO !== "all" && !scenarios[SCENARIO]) {
console.error(`Unknown scenario: ${SCENARIO}. Available: ${Object.keys(scenarios).join(", ")}`);
process.exit(1);
}
let anyDied = false;
for (const [name, fn] of toRun) {
console.log(`\n─── VM state before ${name} ───`);
await vmStats(sessionId);
const dropped = await fn();
console.log(`\n─── VM state after ${name} ───`);
await vmStats(sessionId);
if (dropped) {
console.log(`\n🔴 CDP CONNECTION DROPPED during: ${name}`);
anyDied = true;
// Don't break — try to keep going if possible
} else {
console.log(` ✓ ${name} survived`);
}
}
console.log(`\nCleaning up session ${sessionId}...`);
await kernel.browsers.deleteByID(sessionId);
console.log(anyDied ? "\n⚠️ At least one scenario caused a connection drop." : "\nAll scenarios survived.");
}
main().catch(console.error);
import Kernel from "@onkernel/sdk";
import { chromium } from "playwright-core";
const TARGET_URL = process.env.TARGET_URL || "https://kernel.sh";
const SCENARIO = process.env.SCENARIO || "all";
const kernel = new Kernel();
// ── Helpers ──
async function vmExec(sessionId, command) {
try {
const res = await kernel.browsers.process.exec(sessionId, { command: "bash", args: ["-c", command], timeout_sec: 10 });
return Buffer.from(res.stdout_b64 || "", "base64").toString();
} catch {
return "(exec failed)";
}
}
async function vmStats(sessionId) {
const mem = await vmExec(sessionId, "free -m");
const df = await vmExec(sessionId, "df -h /");
const load = await vmExec(sessionId, "cat /proc/loadavg");
console.log(` [VM] load: ${load.trim()}`);
const memLines = mem.split("\n");
const memLine = memLines.find((l) => l.startsWith("Mem:"));
if (memLine) console.log(` [VM] ${memLine.trim()}`);
const dfLine = df.split("\n").find((l) => l.startsWith("/"));
if (dfLine) console.log(` [VM] disk: ${dfLine}`);
}
// ── Screenshot loop ──
async function screenshotLoop(page, signal, label) {
let count = 0;
let failures = 0;
const start = Date.now();
while (!signal.aborted) {
const t0 = Date.now();
try {
await page.screenshot({ type: "jpeg", quality: 50, timeout: 3000 });
count++;
const took = Date.now() - t0;
if (took > 1000) {
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
console.log(` [${label}] screenshot #${count} OK at ${elapsed}s (took ${took}ms — slow!)`);
}
} catch (err) {
failures++;
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
const took = Date.now() - t0;
const msg = err.message.split("\n")[0].slice(0, 120);
console.log(` [${label}] screenshot #${count + failures} FAILED at ${elapsed}s (${took}ms): ${msg}`);
if (err.message.includes("closed") || err.message.includes("crashed") || err.message.includes("Target page")) {
console.log(` [${label}] CONNECTION DEAD`);
return { count, failures, dead: true };
}
}
await new Promise((r) => setTimeout(r, 200));
}
return { count, failures, dead: false };
}
function summarize(label, result) {
console.log(` [${label}] screenshots: ${result.count} ok, ${result.failures} failed, dead=${result.dead}`);
}
// ── Scenario: CPU block ──
async function scenarioCpuBlock(page, seconds = 20) {
console.log(`\n═══ CPU block (${seconds}s) ═══`);
const ac = new AbortController();
const ssLoop = screenshotLoop(page, ac.signal, "cpu");
const t0 = Date.now();
try {
await page.evaluate((dur) => {
const end = Date.now() + dur * 1000;
while (Date.now() < end) Math.random();
}, seconds);
console.log(` evaluate returned in ${Date.now() - t0}ms`);
} catch (err) {
console.log(` evaluate THREW after ${Date.now() - t0}ms: ${err.message.split("\n")[0].slice(0, 200)}`);
}
ac.abort();
const r = await ssLoop;
summarize("cpu", r);
return r.dead;
}
// ── Scenario: OOM crash — allocate until Chrome dies ──
async function scenarioOOM(page) {
console.log("\n═══ OOM: allocate until Chrome dies ═══");
const ac = new AbortController();
const ssLoop = screenshotLoop(page, ac.signal, "oom");
const t0 = Date.now();
try {
await page.evaluate(() => {
const chunks = [];
// Allocate 10MB typed arrays and fill them to force commit
while (true) {
const arr = new Uint8Array(10 * 1024 * 1024);
arr.fill(42);
chunks.push(arr);
}
});
console.log(` evaluate returned in ${Date.now() - t0}ms (unexpected)`);
} catch (err) {
console.log(` evaluate THREW after ${Date.now() - t0}ms: ${err.message.split("\n")[0].slice(0, 200)}`);
}
ac.abort();
const r = await ssLoop;
summarize("oom", r);
return r.dead;
}
// ── Scenario: Renderer crash via window.open spam ──
async function scenarioWindowOpenSpam(page) {
console.log("\n═══ Window.open spam (open 100 tabs) ═══");
const ac = new AbortController();
const ssLoop = screenshotLoop(page, ac.signal, "tabs");
const t0 = Date.now();
try {
await page.evaluate(() => {
for (let i = 0; i < 100; i++) {
window.open("about:blank", `_blank_${i}`);
}
});
console.log(` evaluate returned in ${Date.now() - t0}ms`);
// Now close them
await page.evaluate(() => {
// The windows are opened but blocked by popup blocker usually
});
} catch (err) {
console.log(` evaluate THREW after ${Date.now() - t0}ms: ${err.message.split("\n")[0].slice(0, 200)}`);
}
ac.abort();
const r = await ssLoop;
summarize("tabs", r);
return r.dead;
}
// ── Scenario: WebGL + Canvas memory exhaustion ──
async function scenarioCanvasExhaust(page) {
console.log("\n═══ Canvas memory exhaustion (create huge canvases) ═══");
const ac = new AbortController();
const ssLoop = screenshotLoop(page, ac.signal, "canvas");
const t0 = Date.now();
try {
await page.evaluate(() => {
const canvases = [];
for (let i = 0; i < 50; i++) {
const c = document.createElement("canvas");
c.width = 4096;
c.height = 4096;
const ctx = c.getContext("2d");
// Fill with data to force GPU memory allocation
ctx.fillStyle = `rgb(${i}, ${i}, ${i})`;
ctx.fillRect(0, 0, 4096, 4096);
// Read back to force commit
ctx.getImageData(0, 0, 4096, 4096);
canvases.push(c);
document.body.appendChild(c);
}
return canvases.length;
});
console.log(` evaluate returned in ${Date.now() - t0}ms`);
} catch (err) {
console.log(` evaluate THREW after ${Date.now() - t0}ms: ${err.message.split("\n")[0].slice(0, 200)}`);
}
ac.abort();
const r = await ssLoop;
summarize("canvas", r);
return r.dead;
}
// ── Scenario: Concurrent page.evaluate + screenshot with CPU block (Felicity-like) ──
async function scenarioFelicityLike(page) {
console.log("\n═══ Felicity-like: page.evaluate blocks while screenshots fire ═══");
console.log(" (evaluate blocks 15s, screenshots every 1s like customer)");
const ac = new AbortController();
// Faster screenshot loop (every 1s like customer)
const ssLoop = (async () => {
let count = 0;
let failures = 0;
const start = Date.now();
while (!ac.signal.aborted) {
const t0 = Date.now();
try {
await page.screenshot({ type: "jpeg", quality: 50, timeout: 5000 });
count++;
} catch (err) {
failures++;
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
const msg = err.message.split("\n")[0].slice(0, 100);
console.log(` [felicity] ss #${count + failures} FAILED at ${elapsed}s: ${msg}`);
if (err.message.includes("closed") || err.message.includes("crashed") || err.message.includes("Target page")) {
return { count, failures, dead: true };
}
}
await new Promise((r) => setTimeout(r, 1000));
}
return { count, failures, dead: false };
})();
// page.evaluate that does DOM work + blocks
const t0 = Date.now();
try {
await page.evaluate(() => {
function realClick(el) {
if (!el) return;
el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
// Simulate heavy DOM scanning (like getVisibleMenu + button search)
for (let round = 0; round < 50; round++) {
const divs = document.querySelectorAll("div");
for (const div of divs) {
void div.innerText;
void div.getBoundingClientRect();
}
const buttons = document.querySelectorAll("a, button");
for (const btn of buttons) {
void btn.closest("div")?.innerText;
realClick(btn);
}
}
// Then block for 10s (simulating a page.evaluate that triggers a heavy React re-render)
const end = Date.now() + 10000;
while (Date.now() < end) Math.random();
});
console.log(` evaluate returned in ${Date.now() - t0}ms`);
} catch (err) {
console.log(` evaluate THREW after ${Date.now() - t0}ms: ${err.message.split("\n")[0].slice(0, 200)}`);
}
// Wait a bit to see if connection dies after evaluate returns
console.log(" Waiting 5s post-evaluate to check connection health...");
await new Promise((r) => setTimeout(r, 5000));
ac.abort();
const r = await ssLoop;
summarize("felicity", r);
return r.dead;
}
// ── Scenario: Kill Chrome from inside the VM ──
async function scenarioKillChrome(page, sessionId) {
console.log("\n═══ Kill chromium process from VM (simulates OOM killer) ═══");
const ac = new AbortController();
const ssLoop = screenshotLoop(page, ac.signal, "kill");
// Give screenshots 2s head start
await new Promise((r) => setTimeout(r, 2000));
// Kill chromium from inside the VM
console.log(" Sending SIGKILL to chromium...");
const killResult = await vmExec(sessionId, "pkill -9 -f chromium || pkill -9 -f chrome");
console.log(` Kill result: ${killResult.trim() || "(empty)"}`);
// Wait for death
await new Promise((r) => setTimeout(r, 5000));
ac.abort();
const r = await ssLoop;
summarize("kill", r);
return r.dead;
}
// ── Scenario: Fill disk from VM ──
async function scenarioFillDisk(page, sessionId) {
console.log("\n═══ Fill disk from VM ═══");
const ac = new AbortController();
const ssLoop = screenshotLoop(page, ac.signal, "disk");
console.log(" Writing 500MB of zeros to /tmp...");
await vmExec(sessionId, "dd if=/dev/zero of=/tmp/fill bs=1M count=500 2>&1 || true");
const dfOut = await vmExec(sessionId, "df -h /");
console.log(` ${dfOut.split("\n").find((l) => l.startsWith("/")) || dfOut}`);
// Wait to see effect
await new Promise((r) => setTimeout(r, 5000));
// Try to trigger Chrome to write (profile, cache, etc)
try {
await page.evaluate(() => {
// Force Chrome to write to disk (localStorage, cookies, etc)
for (let i = 0; i < 1000; i++) {
try { localStorage.setItem(`key_${i}`, "x".repeat(10000)); } catch {}
}
});
} catch (err) {
console.log(` evaluate after disk fill: ${err.message.split("\n")[0].slice(0, 100)}`);
}
ac.abort();
const r = await ssLoop;
summarize("disk", r);
// Cleanup
await vmExec(sessionId, "rm -f /tmp/fill");
return r.dead;
}
// ── Scenario: Two CDP connections (Stagehand + Playwright), one blocks ──
async function scenarioDualCDP(kernelBrowser) {
console.log("\n═══ Dual CDP: two connections, evaluate blocks on one, screenshots on other ═══");
let browser1, browser2;
try {
browser1 = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
browser2 = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
} catch (err) {
console.log(` Failed to establish dual connections: ${err.message.slice(0, 100)}`);
return false;
}
const page1 = browser1.contexts()[0]?.pages()[0];
const page2 = browser2.contexts()[0]?.pages()[0];
if (!page1 || !page2) {
console.log(" Could not get pages from both connections");
try { await browser1.close(); } catch {}
try { await browser2.close(); } catch {}
return false;
}
let b1Dead = false, b2Dead = false;
browser1.on("disconnected", () => { b1Dead = true; console.log(" [dual] Connection 1 (Stagehand) DISCONNECTED"); });
browser2.on("disconnected", () => { b2Dead = true; console.log(" [dual] Connection 2 (Playwright) DISCONNECTED"); });
console.log(" Connection 1: idle (simulates Stagehand)");
console.log(" Connection 2: evaluate + screenshot loop (simulates Playwright)");
const ac = new AbortController();
// Screenshot loop on connection 2
const ssLoop = (async () => {
let count = 0, failures = 0;
const start = Date.now();
while (!ac.signal.aborted && !b2Dead) {
const t0 = Date.now();
try {
await page2.screenshot({ type: "jpeg", quality: 50, timeout: 5000 });
count++;
} catch (err) {
failures++;
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
const msg = err.message.split("\n")[0].slice(0, 100);
console.log(` [dual] ss #${count + failures} FAILED at ${elapsed}s: ${msg}`);
if (err.message.includes("closed") || err.message.includes("crashed") || err.message.includes("Target page")) {
return { count, failures, dead: true };
}
}
await new Promise((r) => setTimeout(r, 1000));
}
return { count, failures, dead: b2Dead };
})();
// Evaluate on connection 2 (same connection doing screenshots, like Felicity)
const t0 = Date.now();
try {
await page2.evaluate(() => {
// Simulate Felicity's pattern: DOM scan + clicks + block
function realClick(el) {
if (!el) return;
el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
// Heavy DOM scanning
for (let round = 0; round < 100; round++) {
const divs = document.querySelectorAll("div");
for (const div of divs) {
void div.innerText;
void div.getBoundingClientRect();
}
const buttons = document.querySelectorAll("a, button");
for (const btn of buttons) {
void btn.closest("div")?.innerText;
realClick(btn);
}
}
// Allocate some memory (simulate heavy page data)
const data = [];
for (let i = 0; i < 50; i++) {
data.push(new Uint8Array(1024 * 1024).fill(i));
}
// Block for 20s
const end = Date.now() + 20000;
while (Date.now() < end) Math.random();
});
console.log(` evaluate returned in ${Date.now() - t0}ms`);
} catch (err) {
console.log(` evaluate THREW after ${Date.now() - t0}ms: ${err.message.split("\n")[0].slice(0, 200)}`);
}
// Wait 10s post-evaluate
console.log(" Waiting 10s post-evaluate...");
await new Promise((r) => setTimeout(r, 10000));
ac.abort();
const r = await ssLoop;
console.log(` [dual] screenshots: ${r.count} ok, ${r.failures} failed, dead=${r.dead}`);
console.log(` [dual] connection1 dead: ${b1Dead}, connection2 dead: ${b2Dead}`);
const dropped = r.dead || b1Dead || b2Dead;
try { await browser1.close(); } catch {}
try { await browser2.close(); } catch {}
return dropped;
}
// ── Scenario: Memory + CPU combined (gradual pressure then block) ──
async function scenarioMemoryCPU(page) {
console.log("\n═══ Memory + CPU: allocate 2GB then block 30s ═══");
const ac = new AbortController();
const ssLoop = screenshotLoop(page, ac.signal, "mem+cpu");
const t0 = Date.now();
try {
await page.evaluate(() => {
// Allocate ~2GB
const chunks = [];
for (let i = 0; i < 200; i++) {
const arr = new Uint8Array(10 * 1024 * 1024);
arr.fill(i);
chunks.push(arr);
}
// Then block for 30s
const end = Date.now() + 30000;
while (Date.now() < end) Math.random();
return chunks.length;
});
console.log(` evaluate returned in ${Date.now() - t0}ms`);
} catch (err) {
console.log(` evaluate THREW after ${Date.now() - t0}ms: ${err.message.split("\n")[0].slice(0, 200)}`);
}
ac.abort();
const r = await ssLoop;
summarize("mem+cpu", r);
return r.dead;
}
// ── Main ──
async function main() {
console.log(`Creating Kernel browser session...`);
const kernelBrowser = await kernel.browsers.create({ timeout_seconds: 300 });
const sessionId = kernelBrowser.session_id;
console.log(`Session: ${sessionId}`);
console.log(`Target URL: ${TARGET_URL}\n`);
await vmStats(sessionId);
let dead = false;
try {
const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
const context = browser.contexts()[0];
const page = context.pages()[0] || (await context.newPage());
let intentionalClose = false;
browser.on("disconnected", () => {
if (!intentionalClose) {
console.log("\n🔴 BROWSER DISCONNECTED — CDP connection dropped unexpectedly!");
dead = true;
}
});
console.log(`\nNavigating to ${TARGET_URL}...`);
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded", timeout: 30000 });
console.log("Page loaded.");
await page.screenshot({ type: "jpeg", quality: 50 });
console.log("Baseline screenshot OK.\n");
const scenarios = {
"cpu-30": () => scenarioCpuBlock(page, 30),
"cpu-45": () => scenarioCpuBlock(page, 45),
"cpu-60": () => scenarioCpuBlock(page, 60),
"cpu-90": () => scenarioCpuBlock(page, 90),
oom: () => scenarioOOM(page),
canvas: () => scenarioCanvasExhaust(page),
felicity: () => scenarioFelicityLike(page),
"kill-chrome": () => scenarioKillChrome(page, sessionId),
"fill-disk": () => scenarioFillDisk(page, sessionId),
"window-spam": () => scenarioWindowOpenSpam(page),
"dual-cdp": () => scenarioDualCDP(kernelBrowser),
"mem-cpu": () => scenarioMemoryCPU(page),
};
const toRun = SCENARIO === "all" ? Object.entries(scenarios) : [[SCENARIO, scenarios[SCENARIO]]];
if (SCENARIO !== "all" && !scenarios[SCENARIO]) {
console.error(`Unknown scenario: ${SCENARIO}. Available: ${Object.keys(scenarios).join(", ")}`);
return;
}
for (const [name, fn] of toRun) {
if (dead) {
console.log(`\n⚠️ Connection dead, skipping remaining.`);
break;
}
console.log(`\n [VM before ${name}]`);
await vmStats(sessionId);
const dropped = await fn();
if (!dead) {
console.log(`\n [VM after ${name}]`);
await vmStats(sessionId);
}
if (dropped || dead) {
console.log(`\n🔴 CDP CONNECTION DROPPED during: ${name}`);
dead = true;
} else {
console.log(` ✓ ${name} survived`);
// Re-navigate between scenarios
try {
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded", timeout: 30000 });
} catch {}
}
}
intentionalClose = true;
try { await browser.close(); } catch {}
} catch (err) {
console.error(`Top-level error: ${err.message}`);
} finally {
console.log(`\nCleaning up session ${sessionId}...`);
await kernel.browsers.deleteByID(sessionId);
console.log("Done.");
}
}
main().catch(console.error);
import Kernel from "@onkernel/sdk";
import { chromium } from "playwright-core";
const TARGET_URL = process.env.TARGET_URL || "https://kernel.sh";
const BLOCK_SECONDS = parseInt(process.env.BLOCK_SECONDS || "20");
const kernel = new Kernel();
async function vmExec(sessionId, command, timeout = 10) {
try {
const res = await kernel.browsers.process.exec(sessionId, {
command: "bash",
args: ["-c", command],
timeout_sec: timeout,
as_root: true,
});
return {
stdout: Buffer.from(res.stdout_b64 || "", "base64").toString(),
stderr: Buffer.from(res.stderr_b64 || "", "base64").toString(),
exit_code: res.exit_code,
};
} catch (e) {
return { stdout: "", stderr: e.message, exit_code: -1 };
}
}
async function main() {
console.log("Creating Kernel browser session...");
const kernelBrowser = await kernel.browsers.create({ timeout_seconds: 300 });
const sessionId = kernelBrowser.session_id;
console.log(`Session: ${sessionId}\n`);
try {
// Find Chrome's CDP port and network interfaces
const { stdout: ifconfig } = await vmExec(sessionId, "ip addr show | head -30");
console.log("VM network interfaces:");
console.log(ifconfig);
const { stdout: ports } = await vmExec(sessionId, "ss -tlnp | grep -i chrom || netstat -tlnp 2>/dev/null | grep -i chrom || echo 'no chrome listeners found'");
console.log("Chrome listening ports:");
console.log(ports);
// Find CDP port specifically
const { stdout: cdpPort } = await vmExec(sessionId, "ss -tlnp 2>/dev/null | grep -oP ':\\K[0-9]+(?=.*chrom)' | head -1 || echo ''");
const port = cdpPort.trim() || "9222";
console.log(`CDP port: ${port}\n`);
// Install tcpdump if needed
await vmExec(sessionId, "which tcpdump || (apt-get update -qq && apt-get install -y -qq tcpdump) 2>/dev/null", 30);
// Connect via CDP and load page
const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
const ctx = browser.contexts()[0];
const page = ctx.pages()[0] || (await ctx.newPage());
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded", timeout: 30000 });
console.log("Page loaded.\n");
// Take a baseline screenshot to confirm connection works
await page.screenshot({ type: "jpeg", quality: 50 });
console.log("Baseline screenshot OK.\n");
// Start tcpdump in background — capture all traffic on CDP port
console.log(`Starting tcpdump on port ${port} for ${BLOCK_SECONDS + 10}s...`);
const { stdout: tcpdumpPid } = await vmExec(
sessionId,
`tcpdump -i any port ${port} -w /tmp/cdp_capture.pcap -G ${BLOCK_SECONDS + 15} -W 1 &>/dev/null & echo $!`,
);
console.log(`tcpdump PID: ${tcpdumpPid.trim()}`);
// Also start a simpler text-based capture for quick analysis
await vmExec(
sessionId,
`tcpdump -i any port ${port} -l -n --immediate-mode -tt 2>/dev/null > /tmp/cdp_text.log &`,
);
// Wait for tcpdump to start
await new Promise((r) => setTimeout(r, 1000));
// Phase 1: Normal operation — take a few screenshots to establish baseline traffic
console.log("\n── Phase 1: Normal traffic (5s) ──");
const phase1Start = Date.now();
for (let i = 0; i < 5; i++) {
await page.screenshot({ type: "jpeg", quality: 50, timeout: 5000 });
await new Promise((r) => setTimeout(r, 800));
}
console.log(` ${5} screenshots in ${((Date.now() - phase1Start) / 1000).toFixed(1)}s`);
// Grab traffic stats before blocking
const { stdout: preBlock } = await vmExec(sessionId, `wc -l /tmp/cdp_text.log`);
console.log(` Packets so far: ${preBlock.trim()}`);
// Phase 2: Block main thread
console.log(`\n── Phase 2: Blocking main thread for ${BLOCK_SECONDS}s ──`);
const blockStart = Date.now();
// Fire the blocking evaluate
const evalPromise = page
.evaluate((dur) => {
const end = Date.now() + dur * 1000;
while (Date.now() < end) Math.random();
return "done";
}, BLOCK_SECONDS)
.then(() => console.log(` evaluate returned in ${Date.now() - blockStart}ms`))
.catch((err) => console.log(` evaluate THREW: ${err.message.split("\n")[0].slice(0, 100)}`));
// While blocked, periodically check packet count
const interval = setInterval(async () => {
const elapsed = ((Date.now() - blockStart) / 1000).toFixed(0);
const { stdout: count } = await vmExec(sessionId, `wc -l /tmp/cdp_text.log`);
console.log(` [${elapsed}s] packets: ${count.trim()}`);
}, 5000);
await evalPromise;
clearInterval(interval);
// Phase 3: Post-block — traffic should resume
console.log(`\n── Phase 3: Post-block traffic ──`);
const { stdout: postBlock } = await vmExec(sessionId, `wc -l /tmp/cdp_text.log`);
console.log(` Packets after block: ${postBlock.trim()}`);
// Take a screenshot to confirm connection is alive
try {
await page.screenshot({ type: "jpeg", quality: 50, timeout: 5000 });
console.log(" Post-block screenshot OK");
} catch (err) {
console.log(` Post-block screenshot FAILED: ${err.message.split("\n")[0]}`);
}
await new Promise((r) => setTimeout(r, 2000));
const { stdout: finalCount } = await vmExec(sessionId, `wc -l /tmp/cdp_text.log`);
console.log(` Final packet count: ${finalCount.trim()}`);
// Kill tcpdump
await vmExec(sessionId, "pkill tcpdump || true");
// Analyze the capture
console.log("\n── Traffic analysis ──");
// Get the raw text log
const { stdout: rawLog } = await vmExec(
sessionId,
`cat /tmp/cdp_text.log | head -200`,
15,
);
console.log("\nRaw packet log (first 200 lines):");
console.log(rawLog);
// Analyze traffic direction and timing
const { stdout: analysis } = await vmExec(
sessionId,
`cat /tmp/cdp_text.log | awk '{print $1, $3, $5}' | head -100`,
);
console.log("\nPacket timestamps + direction:");
console.log(analysis);
// Check for gaps in traffic (the key question!)
console.log("\n── Looking for traffic gaps ──");
const { stdout: timestamps } = await vmExec(
sessionId,
`cat /tmp/cdp_text.log | awk '{print $1}' | sort -n`,
15,
);
if (timestamps.trim()) {
const times = timestamps.trim().split("\n").map(Number);
let maxGap = 0;
let gapStart = 0;
for (let i = 1; i < times.length; i++) {
const gap = times[i] - times[i - 1];
if (gap > maxGap) {
maxGap = gap;
gapStart = times[i - 1];
}
}
console.log(` Total packets: ${times.length}`);
console.log(` Time span: ${(times[times.length - 1] - times[0]).toFixed(1)}s`);
console.log(` Largest gap: ${maxGap.toFixed(3)}s at timestamp ${gapStart.toFixed(3)}`);
// Show all gaps > 1s
console.log("\n Gaps > 1 second:");
let gapCount = 0;
for (let i = 1; i < times.length; i++) {
const gap = times[i] - times[i - 1];
if (gap > 1.0) {
console.log(` ${times[i - 1].toFixed(3)} → ${times[i].toFixed(3)} (${gap.toFixed(3)}s gap)`);
gapCount++;
}
}
if (gapCount === 0) console.log(" (none)");
}
try { await browser.close(); } catch {}
} catch (err) {
console.error(`Error: ${err.message}`);
} finally {
console.log(`\nCleaning up session ${sessionId}...`);
await kernel.browsers.deleteByID(sessionId);
console.log("Done.");
}
}
main().catch(console.error);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment