()
| 257 | } |
| 258 | |
| 259 | async function main() { |
| 260 | if (!DISABLE_FUSE) await ensureImage(); |
| 261 | |
| 262 | process.stderr.write("booting A (sink) ...\n"); |
| 263 | const a = await bootContainer(); |
| 264 | process.stderr.write(` A: ${a.url} (${a.cid.slice(0, 12)})\n`); |
| 265 | |
| 266 | process.stderr.write("booting B (source, UPSTREAM_URL -> A) ...\n"); |
| 267 | // B's sync loop will push to A. Hostname inside docker: |
| 268 | // we can't reach the host's 127.0.0.1 portably; use |
| 269 | // host.docker.internal which docker-desktop sets up on |
| 270 | // macOS/Windows. On linux we'd need --add-host=host.docker.internal:host-gateway. |
| 271 | // Inside a docker container we reach the host's mapped |
| 272 | // port via host.docker.internal. The capnweb client needs |
| 273 | // a ws:// URL pointing at the /ws endpoint (not just the |
| 274 | // host). |
| 275 | const upstreamForB = `${a.url.replace("http://127.0.0.1", "ws://host.docker.internal")}/ws`; |
| 276 | // Inside the docker container, the host port we're trying |
| 277 | // to reach is 127.0.0.1:<a.port> on the host. Pass the |
| 278 | // mapped host port via host.docker.internal. |
| 279 | const b = await bootContainer({ |
| 280 | UPSTREAM_URL: upstreamForB, |
| 281 | }); |
| 282 | process.stderr.write(` B: ${b.url} (${b.cid.slice(0, 12)}) -> upstream ${upstreamForB}\n`); |
| 283 | |
| 284 | // Header row. |
| 285 | console.log( |
| 286 | "t_ms\tA_currentRev\tA_pushRev\tA_fetchRev\tB_currentRev\tB_pushRev\tB_fetchRev\tA_mem\tB_mem\twrites_sent", |
| 287 | ); |
| 288 | |
| 289 | const start = Date.now(); |
| 290 | const stopAt = start + DURATION_MS; |
| 291 | const intervalMs = Math.max(1, Math.floor(1000 / WRITES_PER_S)); |
| 292 | let writeSeq = 0; |
| 293 | let writesSent = 0; |
| 294 | let writesInFlight = 0; |
| 295 | |
| 296 | // Writes go through B's SyncRPC /ws push path with |
| 297 | // senderRev=0. The server treats them as local writes; |
| 298 | // B's outbound sync loop ships them to A on the next |
| 299 | // tick. This is the path an external orchestrator (a |
| 300 | // DO accepting agent requests, the agent itself) would |
| 301 | // take — the same wire surface a computerd-to-computerd peer |
| 302 | // uses, just with a different senderRev value. |
| 303 | const writeStub = wsStub(b.url); |
| 304 | |
| 305 | // Fire-and-forget write loop. We don't await every write |
| 306 | // because the goal is to saturate; we cap the in-flight |
| 307 | // count to keep memory bounded. |
| 308 | const MAX_INFLIGHT = 32; |
| 309 | const writeLoop = (async () => { |
| 310 | while (Date.now() < stopAt) { |
| 311 | if (writesInFlight >= MAX_INFLIGHT) { |
| 312 | await new Promise((r) => setTimeout(r, 1)); |
| 313 | continue; |
| 314 | } |
| 315 | const i = writeSeq++; |
| 316 | writesInFlight++; |
no test coverage detected