mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
## Summary - Fix O(n²) performance bug in JSON mode IPC when receiving large messages that arrive in chunks - Add `JsonIncomingBuffer` wrapper that tracks newline positions to avoid re-scanning - Each byte is now scanned exactly once (on arrival or when preceding message is consumed) ## Problem When data arrives in chunks in JSON mode, `decodeIPCMessage` was calling `indexOfChar(data, '\n')` on the ENTIRE accumulated buffer every time. For a 10MB message arriving in 160 chunks of 64KB: - Chunk 1: scan 64KB - Chunk 2: scan 128KB - Chunk 3: scan 192KB - ... - Chunk 160: scan 10MB Total: ~800MB scanned for one 10MB message. ## Solution Introduced a `JsonIncomingBuffer` struct that: 1. Tracks `newline_pos: ?u32` - position of known upcoming newline (if any) 2. On `append(bytes)`: Only scans new chunk for `\n` if no position is cached 3. On `consume(bytes)`: Updates or re-scans as needed after message processing This ensures O(n) scanning instead of O(n²). ## Test plan - [x] `bun run zig:check-all` passes (all platforms compile) - [x] `bun bd test test/js/bun/spawn/spawn.ipc.test.ts` - 4 tests pass - [x] `bun bd test test/js/node/child_process/child_process_ipc.test.js` - 1 test pass - [x] `bun bd test test/js/bun/spawn/bun-ipc-inherit.test.ts` - 1 test pass - [x] `bun bd test test/js/bun/spawn/spawn.ipc.bun-node.test.ts` - 1 test pass - [x] `bun bd test test/js/bun/spawn/spawn.ipc.node-bun.test.ts` - 1 test pass - [x] `bun bd test test/js/node/child_process/child_process_ipc_large_disconnect.test.js` - 1 test pass - [x] Manual verification with `child-process-send-cb-more.js` (32KB messages) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
46 lines
1.1 KiB
JavaScript
46 lines
1.1 KiB
JavaScript
import { fork } from "node:child_process";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { bench, run } from "../runner.mjs";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const childPath = path.join(__dirname, "ipc-json-child.mjs");
|
|
|
|
const smallMessage = { type: "ping", id: 1 };
|
|
const largeString = Buffer.alloc(10 * 1024 * 1024, "A").toString();
|
|
const largeMessage = { type: "ping", id: 1, data: largeString };
|
|
|
|
async function runBenchmark(message, count) {
|
|
let received = 0;
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
|
|
const child = fork(childPath, [], {
|
|
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
serialization: "json",
|
|
});
|
|
|
|
child.on("message", () => {
|
|
received++;
|
|
if (received >= count) {
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
child.send(message);
|
|
}
|
|
|
|
await promise;
|
|
child.kill();
|
|
}
|
|
|
|
bench("ipc json - small messages (1000 roundtrips)", async () => {
|
|
await runBenchmark(smallMessage, 1000);
|
|
});
|
|
|
|
bench("ipc json - 10MB messages (10 roundtrips)", async () => {
|
|
await runBenchmark(largeMessage, 10);
|
|
});
|
|
|
|
await run();
|