mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
perf(ipc): fix O(n²) JSON scanning for large chunked messages (#25743)
## 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>
This commit is contained in:
4
bench/snippets/ipc-json-child.mjs
Normal file
4
bench/snippets/ipc-json-child.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
// Child process for IPC benchmarks - echoes messages back to parent
|
||||
process.on("message", message => {
|
||||
process.send(message);
|
||||
});
|
||||
45
bench/snippets/ipc-json.mjs
Normal file
45
bench/snippets/ipc-json.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user