Skip to content

Commit fc5b60d

Browse files
bartlomiejuclaude
andcommitted
perf(ext/web): add write buffering for FsFile.writable streams
Small buffer writes (e.g. 128 bytes) via `file.writable.getWriter().write()` were ~100x slower than Node.js because each write immediately dispatched a syscall through the async op machinery with zero buffering. Add an opt-in `bufferSize` option to `writableStreamForRid()` and enable it with a 64KB buffer for `FsFile.writable`. Small chunks are accumulated in a JS-side buffer and flushed when full or on close. Large chunks (>= bufferSize) bypass the buffer entirely. The highWaterMark and sizeAlgorithm are also adjusted to operate on byte length when buffering is enabled. Only FsFile writable streams are affected — network sockets, stdin, QUIC, and WebTransport streams remain unbuffered. Ref: #16667 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4650330 commit fc5b60d

File tree

2 files changed

+59
-6
lines changed

2 files changed

+59
-6
lines changed

ext/fs/30_fs.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,12 @@ class FsFile {
663663

664664
get writable() {
665665
if (this.#writable === undefined) {
666-
this.#writable = writableStreamForRid(this.#rid);
666+
this.#writable = writableStreamForRid(
667+
this.#rid,
668+
true,
669+
undefined,
670+
{ bufferSize: 64 * 1024 },
671+
);
667672
}
668673
return this.#writable;
669674
}

ext/web/06_streams.js

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,7 +1191,7 @@ async function readableStreamCollectIntoUint8Array(stream) {
11911191
* @param {boolean=} autoClose If the resource should be auto-closed when the stream closes. Defaults to true.
11921192
* @returns {ReadableStream<Uint8Array>}
11931193
*/
1194-
function writableStreamForRid(rid, autoClose = true, cfn) {
1194+
function writableStreamForRid(rid, autoClose = true, cfn, options) {
11951195
const stream = cfn ? cfn(_brand) : new WritableStream(_brand);
11961196
stream[_resourceBacking] = { rid, autoClose };
11971197

@@ -1205,29 +1205,77 @@ function writableStreamForRid(rid, autoClose = true, cfn) {
12051205
RESOURCE_REGISTRY.register(stream, rid, stream);
12061206
}
12071207

1208+
const bufferSize = options?.bufferSize ?? 0;
1209+
let buffer = null;
1210+
let bufferOffset = 0;
1211+
1212+
async function flushBuffer() {
1213+
if (bufferOffset > 0) {
1214+
const toWrite = TypedArrayPrototypeSlice(buffer, 0, bufferOffset);
1215+
bufferOffset = 0;
1216+
await core.writeAll(rid, toWrite);
1217+
}
1218+
}
1219+
12081220
const underlyingSink = {
12091221
async write(chunk, controller) {
12101222
try {
1211-
await core.writeAll(rid, chunk);
1223+
if (bufferSize > 0) {
1224+
const chunkLen = TypedArrayPrototypeGetByteLength(chunk);
1225+
// Large chunks: flush buffer then write directly
1226+
if (chunkLen >= bufferSize) {
1227+
await flushBuffer();
1228+
await core.writeAll(rid, chunk);
1229+
return;
1230+
}
1231+
1232+
// Lazily allocate buffer
1233+
if (buffer === null) {
1234+
buffer = new Uint8Array(bufferSize);
1235+
}
1236+
1237+
// If chunk won't fit, flush first
1238+
if (bufferOffset + chunkLen > bufferSize) {
1239+
await flushBuffer();
1240+
}
1241+
1242+
// Copy chunk into buffer
1243+
TypedArrayPrototypeSet(buffer, chunk, bufferOffset);
1244+
bufferOffset += chunkLen;
1245+
} else {
1246+
await core.writeAll(rid, chunk);
1247+
}
12121248
} catch (e) {
12131249
controller.error(e);
12141250
tryClose();
12151251
}
12161252
},
1217-
close() {
1253+
async close() {
1254+
try {
1255+
await flushBuffer();
1256+
} catch {
1257+
// ignore errors on flush during close
1258+
}
12181259
tryClose();
12191260
},
12201261
abort() {
1262+
bufferOffset = 0;
12211263
tryClose();
12221264
},
12231265
};
1266+
1267+
const highWaterMark = bufferSize > 0 ? bufferSize : 1;
1268+
const sizeAlgorithm = bufferSize > 0
1269+
? (chunk) => TypedArrayPrototypeGetByteLength(chunk)
1270+
: () => 1;
1271+
12241272
initializeWritableStream(stream);
12251273
setUpWritableStreamDefaultControllerFromUnderlyingSink(
12261274
stream,
12271275
underlyingSink,
12281276
underlyingSink,
1229-
1,
1230-
() => 1,
1277+
highWaterMark,
1278+
sizeAlgorithm,
12311279
);
12321280
return stream;
12331281
}

0 commit comments

Comments
 (0)