Files
bun.sh/test/js/web/workers/structuredClone-classes.test.ts
robobun e7672b2d04 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>
2025-08-20 00:25:00 -07:00

137 lines
5.0 KiB
TypeScript

import { structuredCloneAdvanced } from "bun:internal-for-testing";
import { deserialize, serialize } from "bun:jsc";
import { bunEnv, bunExe } from "harness";
enum TransferMode {
no = 0,
yes_in_transfer_list = 1,
yes_but_not_in_transfer_list = 2,
}
const testTypes = [
{
name: "ArrayBuffer (transferable)",
createValue: () => {
const buf = Uint8Array.from([21, 11, 96, 126, 243, 128, 164]);
return buf.buffer.transfer();
},
isTransferable: true,
expectedAfterClone: (original: ArrayBuffer, cloned: any, isTransfer: TransferMode, isStorage: boolean) => {
expect(cloned).toBeInstanceOf(ArrayBuffer);
expect(new Uint8Array(cloned)).toStrictEqual(new Uint8Array([21, 11, 96, 126, 243, 128, 164]));
if (isTransfer === TransferMode.yes_in_transfer_list) {
// Original should be detached after transfer
expect(original.byteLength).toBe(0);
}
},
},
{
name: "BunFile (cloneable, non-transferable)",
createValue: () => Bun.file(import.meta.filename),
isTransferable: false,
expectedAfterClone: (original: any, cloned: any, isTransfer: TransferMode, isStorage: boolean) => {
expect(original).toBeInstanceOf(Blob);
expect(original.name).toEqual(import.meta.filename);
expect(original.type).toEqual("text/javascript;charset=utf-8");
if (isTransfer || isStorage) {
// Non-transferable types should yield an empty object when transferred
expect(cloned).toBeEmptyObject();
} else {
// When not stored or transferred, BunFile maintains its properties
expect(cloned.name).toBe(original.name);
expect(cloned.type).toBe(original.type);
}
},
},
{
name: "net.BlockList (cloneable, non-transferable)",
createValue: () => {
const { BlockList } = require("net");
const blocklist = new BlockList();
blocklist.addAddress("123.123.123.123");
return blocklist;
},
isTransferable: false,
expectedAfterClone: (original: any, cloned: any, isTransfer: TransferMode, isStorage: boolean) => {
if (isStorage || isTransfer !== TransferMode.no) {
// BlockList loses its internal state when stored
expect(cloned.rules).toBeUndefined();
expect(cloned).toBeEmptyObject();
} else {
// When not stored or transferred, BlockList maintains its properties
expect(cloned).toHaveProperty("rules");
expect(cloned.check("123.123.123.123")).toBe(true);
}
},
},
];
describe("serialize & deserialize", () => {
for (const testType of testTypes) {
test(`${testType.name}`, async () => {
const original = testType.createValue();
const serialized = serialize(original);
const result = Bun.spawnSync({
cmd: [
bunExe(),
"-e",
`
import {deserialize, serialize} from "bun:jsc";
const serialized = deserialize(await Bun.stdin.bytes());
const cloned = serialize(serialized);
process.stdout.write(cloned);
`,
],
env: bunEnv,
stdin: serialized,
stdout: "pipe",
stderr: "inherit",
});
const cloned = deserialize(result.stdout);
testType.expectedAfterClone(original, cloned, TransferMode.no, true);
});
}
});
const contexts = ["default", "worker", "window"] as const;
const transferModes = [
TransferMode.yes_but_not_in_transfer_list,
TransferMode.yes_in_transfer_list,
TransferMode.no,
] as const;
const storageModes = [true, false] as const;
for (const testType of testTypes) {
for (const context of contexts) {
for (const isForTransfer of transferModes) {
for (const isForStorage of storageModes) {
test(`${testType.name} - context: ${context}, transfer: ${TransferMode[isForTransfer]}, storage: ${isForStorage}`, () => {
const original = testType.createValue();
if (isForTransfer === TransferMode.yes_in_transfer_list) {
// Test with transfer list (even for non-transferable types)
const transferList = [original];
if (!testType.isTransferable) {
expect(() =>
structuredCloneAdvanced(original, transferList, !!isForTransfer, isForStorage, context),
).toThrowError("The object can not be cloned.");
} else {
const cloned = structuredCloneAdvanced(original, transferList, !!isForTransfer, isForStorage, context);
testType.expectedAfterClone(original, cloned, isForTransfer, isForStorage);
}
} else if (isForTransfer === TransferMode.yes_but_not_in_transfer_list) {
const cloned = structuredCloneAdvanced(original, [], !!isForTransfer, isForStorage, context);
testType.expectedAfterClone(original, cloned, isForTransfer, isForStorage);
} else {
// Test without transfer list
const cloned = structuredCloneAdvanced(original, [], !!isForTransfer, isForStorage, context);
testType.expectedAfterClone(original, cloned, isForTransfer, isForStorage);
}
});
}
}
}
}