mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Add string fast path for postMessage and structuredClone (#21926)
## Summary Implements a string fast path optimization for `postMessage` and `structuredClone` operations that provides significant performance improvements for string-only data transfer, along with various bug fixes and infrastructure improvements. ## Key Performance Improvements **postMessage with Workers:** - **Small strings (11 chars):** ~5% faster (572ns vs 599ns) - **Medium strings (14KB):** **~2.7x faster** (528ns vs 1.40μs) - **Large strings (3MB):** **~660x faster** (540ns vs 356μs) **Compared to Node.js postMessage:** - Similar performance for small strings - Competitive for medium strings - **~455x faster** for large strings (540ns vs 245μs) ## Implementation Details The optimization adds a **string fast path** that bypasses full structured cloning serialization when: - Input is a pure string (`value.isString()`) - No transfer list or message ports are involved - Not being stored persistently ### Core Changes **String Thread-Safety Utilities (`BunString.cpp/h`):** - `isCrossThreadShareable()` - Checks if string can be safely shared across threads - `toCrossThreadShareable()` - Converts strings to thread-safe form via `isolatedCopy()` - Handles edge cases: atoms, symbols, substring slices, external buffers **Serialization Fast Path (`SerializedScriptValue.cpp`):** - New `m_fastPathString` field stores string data directly - Bypasses full object serialization machinery for pure strings - Creates isolated copies for cross-thread safety **Deserialization Fast Path:** - Directly returns JSString from stored string data - Avoids parsing serialized byte streams **Updated Flags System (`JSValue.zig`, `Serialization.cpp`):** - Replaces boolean `forTransfer` with structured `SerializedFlags` - Supports `forCrossProcessTransfer` and `forStorage` distinctions **Structured Clone Infrastructure:** - Moved `structuredClone` implementation to dedicated `StructuredClone.cpp` - Added `jsFunctionStructuredCloneAdvanced` for testing with custom flags - Improved class serialization compatibility checks (`isForTransfer`, `isForStorage`) **IPC Improvements (`ipc.zig`):** - Fixed race conditions in `SendQueue` by deferring cleanup to next tick - Proper fd ownership handling with `bun.take()` - Cached IPC serialize/parse functions for better performance **BlockList Thread Safety Fixes (`BlockList.zig`):** - Fixed potential deadlocks by moving mutex locking inside methods - Added atomic `estimated_size` counter to avoid lock during GC - Corrected pointer handling in comparison functions - Improved GC safety in `rules()` method ## Benchmark Results ``` ❯ bun-21926 bench/string-postmessage.mjs # This branch postMessage(11 chars string) 572.24 ns/iter postMessage(14 KB string) 527.55 ns/iter ← ~2.7x faster postMessage(3 MB string) 539.70 ns/iter ← ~660x faster ❯ bun-1.2.20 bench/string-postmessage.mjs # Previous postMessage(11 chars string) 598.76 ns/iter postMessage(14 KB string) 1.40 µs/iter postMessage(3 MB string) 356.38 µs/iter ❯ node bench/string-postmessage.mjs # Node.js comparison postMessage(11 chars string) 569.63 ns/iter postMessage(14 KB string) 1.46 µs/iter postMessage(3 MB string) 245.46 µs/iter ``` **Key insight:** The fast path achieves **constant time performance** regardless of string size (~540ns), while traditional serialization scales linearly with data size. ## Test Coverage **New Tests:** - `test/js/web/structured-clone-fastpath.test.ts` - Fast path memory usage validation - `test/js/web/workers/structuredClone-classes.test.ts` - Comprehensive class serialization tests - Tests ArrayBuffer transferability - Tests BunFile cloning with storage/transfer restrictions - Tests net.BlockList cloning behavior - Validates different serialization contexts (default, worker, window) **Enhanced Tests:** - `test/js/web/workers/structured-clone.test.ts` - Multi-function testing - Tests `structuredClone`, `jscSerializeRoundtrip`, and cross-process serialization - Validates consistency across different serialization paths - `test/js/node/cluster.test.ts` - Better error handling and debugging **Benchmarks:** - `bench/string-postmessage.mjs` - Worker postMessage performance comparison - `bench/string-fastpath.mjs` - Fast path vs traditional serialization comparison ## Bug Fixes **BlockList Threading Issues:** - Fixed potential deadlocks when multiple threads access BlockList simultaneously - Moved mutex locks inside methods rather than holding across entire function calls - Added atomic size tracking for GC compatibility - Fixed comparison function pointer handling **IPC Race Conditions:** - Fixed race condition where `SendQueue._onAfterIPCClosed()` could be called on wrong thread - Deferred cleanup operations to next tick using task queue - Improved file descriptor ownership with proper `bun.take()` usage **Structured Clone Compatibility:** - Enhanced class serialization with proper transfer/storage mode checking - Fixed edge cases where non-transferable objects were incorrectly handled - Added better error reporting for unsupported clone operations ## Technical Notes - Thread safety ensured via `String.isolatedCopy()` for cross-VM transfers - Memory cost calculation updated to account for string references - Maintains full compatibility with existing structured clone semantics - Does not affect object serialization or transfer lists - Proper cleanup and error handling throughout IPC pipeline --------- Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Meghan Denny <meghan@bun.sh>
This commit is contained in:
56
bench/string-fastpath.mjs
Normal file
56
bench/string-fastpath.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
// Benchmark for string fast path optimization in postMessage and structuredClone
|
||||
|
||||
import { bench, run } from "mitata";
|
||||
|
||||
// Test strings of different sizes
|
||||
const strings = {
|
||||
small: "Hello world",
|
||||
medium: "Hello World!!!".repeat(1024).split("").join(""),
|
||||
large: "Hello World!!!".repeat(1024).repeat(1024).split("").join(""),
|
||||
};
|
||||
|
||||
console.log("String fast path benchmark");
|
||||
console.log("Comparing pure strings (fast path) vs objects containing strings (traditional)");
|
||||
console.log("For structuredClone, pure strings should have constant time regardless of size.");
|
||||
console.log("");
|
||||
|
||||
// Benchmark structuredClone with pure strings (uses fast path)
|
||||
bench("structuredClone small string (fast path)", () => {
|
||||
structuredClone(strings.small);
|
||||
});
|
||||
|
||||
bench("structuredClone medium string (fast path)", () => {
|
||||
structuredClone(strings.medium);
|
||||
});
|
||||
|
||||
bench("structuredClone large string (fast path)", () => {
|
||||
structuredClone(strings.large);
|
||||
});
|
||||
|
||||
// Benchmark structuredClone with objects containing strings (traditional path)
|
||||
bench("structuredClone object with small string", () => {
|
||||
structuredClone({ str: strings.small });
|
||||
});
|
||||
|
||||
bench("structuredClone object with medium string", () => {
|
||||
structuredClone({ str: strings.medium });
|
||||
});
|
||||
|
||||
bench("structuredClone object with large string", () => {
|
||||
structuredClone({ str: strings.large });
|
||||
});
|
||||
|
||||
// Multiple string cloning benchmark
|
||||
bench("structuredClone 100 small strings", () => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
structuredClone(strings.small);
|
||||
}
|
||||
});
|
||||
|
||||
bench("structuredClone 100 small objects", () => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
structuredClone({ str: strings.small });
|
||||
}
|
||||
});
|
||||
|
||||
await run();
|
||||
77
bench/string-postmessage.mjs
Normal file
77
bench/string-postmessage.mjs
Normal file
@@ -0,0 +1,77 @@
|
||||
// Benchmark for string fast path optimization in postMessage with Workers
|
||||
|
||||
import { bench, run } from "mitata";
|
||||
import { Worker, isMainThread, parentPort } from "node:worker_threads";
|
||||
|
||||
// Test strings of different sizes
|
||||
const strings = {
|
||||
small: "Hello world",
|
||||
medium: Buffer.alloc("Hello World!!!".length * 1024, "Hello World!!!").toString(),
|
||||
large: Buffer.alloc("Hello World!!!".length * 1024 * 256, "Hello World!!!").toString(),
|
||||
};
|
||||
|
||||
let worker;
|
||||
let receivedCount = new Int32Array(new SharedArrayBuffer(4));
|
||||
let sentCount = 0;
|
||||
|
||||
function createWorker() {
|
||||
const workerCode = `
|
||||
import { parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
let int = workerData;
|
||||
|
||||
parentPort?.on("message", data => {
|
||||
Atomics.add(int, 0, 1);
|
||||
});
|
||||
`;
|
||||
|
||||
worker = new Worker(workerCode, { eval: true, workerData: receivedCount });
|
||||
|
||||
worker.on("message", confirmationId => {});
|
||||
|
||||
worker.on("error", error => {
|
||||
console.error("Worker error:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize worker before running benchmarks
|
||||
createWorker();
|
||||
|
||||
function fmt(int) {
|
||||
if (int < 1000) {
|
||||
return `${int} chars`;
|
||||
}
|
||||
|
||||
if (int < 100000) {
|
||||
return `${(int / 1024) | 0} KB`;
|
||||
}
|
||||
|
||||
return `${(int / 1024 / 1024) | 0} MB`;
|
||||
}
|
||||
|
||||
// Benchmark postMessage with pure strings (uses fast path)
|
||||
bench("postMessage(" + fmt(strings.small.length) + " string)", async () => {
|
||||
sentCount++;
|
||||
worker.postMessage(strings.small);
|
||||
});
|
||||
|
||||
bench("postMessage(" + fmt(strings.medium.length) + " string)", async () => {
|
||||
sentCount++;
|
||||
worker.postMessage(strings.medium);
|
||||
});
|
||||
|
||||
bench("postMessage(" + fmt(strings.large.length) + " string)", async () => {
|
||||
sentCount++;
|
||||
worker.postMessage(strings.large);
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
if (receivedCount[0] !== sentCount) {
|
||||
throw new Error("Expected " + receivedCount[0] + " to equal " + sentCount);
|
||||
}
|
||||
|
||||
// Cleanup worker
|
||||
worker?.terminate();
|
||||
Reference in New Issue
Block a user