Skip to content

Commit e09518e

Browse files
authored
[Fizz] write chunks to a buffer with no re-use (#24034)
* write chunks to a buffer with no re-use chunks were previously enqueued to a ReadableStream as they were written. We now write them to a view over an ArrayBuffer and enqueue them only when writing has completed or the buffer's size is exceeded. In addition this copy now ensures we don't attempt to re-send buffers that have already been transferred. * refactor writeChunk to be more defensive and efficient We now defend against overflows using the next views length instead of the current one. this protects us against a future where we use byobRequest and we get longer initial views than we might create after overflowing the first time. Additionally we add in an optimization when we have completely filled up the currentView where we avoid creating subarrays of the chunk to write since it lands exactly on a view boundary. Finally we move the view creation to beginWriting to avoid a runtime check on each write and because we want to reset the view on each beginWriting call in case a throw elsewhere in the program leaves the currentView in an unfinished state * add tests to exercise codepaths dealing with buffer overlows
1 parent 14c2be8 commit e09518e

File tree

2 files changed

+105
-5
lines changed

2 files changed

+105
-5
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js

+40
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,44 @@ describe('ReactDOMFizzServer', () => {
248248
expect(rendered).toBe(false);
249249
expect(isComplete).toBe(true);
250250
});
251+
252+
// @gate experimental
253+
it('should stream large contents that might overlow individual buffers', async () => {
254+
const str492 = `(492) This string is intentionally 492 bytes long because we want to make sure we process chunks that will overflow buffer boundaries. It will repeat to fill out the bytes required (inclusive of this prompt):: foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux q :: total count (492)`;
255+
const str2049 = `(2049) This string is intentionally 2049 bytes long because we want to make sure we process chunks that will overflow buffer boundaries. It will repeat to fill out the bytes required (inclusive of this prompt):: foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy :: total count (2049)`;
256+
257+
// this specific layout is somewhat contrived to exercise the landing on
258+
// an exact view boundary. it's not critical to test this edge case but
259+
// since we are setting up a test in general for larger chunks I contrived it
260+
// as such for now. I don't think it needs to be maintained if in the future
261+
// the view sizes change or become dynamic becasue of the use of byobRequest
262+
let stream;
263+
stream = await ReactDOMFizzServer.renderToReadableStream(
264+
<>
265+
<div>
266+
<span>{''}</span>
267+
</div>
268+
<div>{str492}</div>
269+
<div>{str492}</div>
270+
</>,
271+
);
272+
273+
let result;
274+
result = await readResult(stream);
275+
expect(result).toMatchInlineSnapshot(
276+
`"<div><span></span></div><div>${str492}</div><div>${str492}</div>"`,
277+
);
278+
279+
// this size 2049 was chosen to be a couple base 2 orders larger than the current view
280+
// size. if the size changes in the future hopefully this will still exercise
281+
// a chunk that is too large for the view size.
282+
stream = await ReactDOMFizzServer.renderToReadableStream(
283+
<>
284+
<div>{str2049}</div>
285+
</>,
286+
);
287+
288+
result = await readResult(stream);
289+
expect(result).toMatchInlineSnapshot(`"<div>${str2049}</div>"`);
290+
});
251291
});

packages/react-server/src/ReactServerStreamConfigBrowser.js

+65-5
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,84 @@ export function flushBuffered(destination: Destination) {
2121
// transform streams. https://github.com/whatwg/streams/issues/960
2222
}
2323

24-
export function beginWriting(destination: Destination) {}
24+
const VIEW_SIZE = 512;
25+
let currentView = null;
26+
let writtenBytes = 0;
27+
28+
export function beginWriting(destination: Destination) {
29+
currentView = new Uint8Array(VIEW_SIZE);
30+
writtenBytes = 0;
31+
}
2532

2633
export function writeChunk(
2734
destination: Destination,
2835
chunk: PrecomputedChunk | Chunk,
2936
): void {
30-
destination.enqueue(chunk);
37+
if (chunk.length === 0) {
38+
return;
39+
}
40+
41+
if (chunk.length > VIEW_SIZE) {
42+
// this chunk may overflow a single view which implies it was not
43+
// one that is cached by the streaming renderer. We will enqueu
44+
// it directly and expect it is not re-used
45+
if (writtenBytes > 0) {
46+
destination.enqueue(
47+
new Uint8Array(
48+
((currentView: any): Uint8Array).buffer,
49+
0,
50+
writtenBytes,
51+
),
52+
);
53+
currentView = new Uint8Array(VIEW_SIZE);
54+
writtenBytes = 0;
55+
}
56+
destination.enqueue(chunk);
57+
return;
58+
}
59+
60+
let bytesToWrite = chunk;
61+
const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes;
62+
if (allowableBytes < bytesToWrite.length) {
63+
// this chunk would overflow the current view. We enqueue a full view
64+
// and start a new view with the remaining chunk
65+
if (allowableBytes === 0) {
66+
// the current view is already full, send it
67+
destination.enqueue(currentView);
68+
} else {
69+
// fill up the current view and apply the remaining chunk bytes
70+
// to a new view.
71+
((currentView: any): Uint8Array).set(
72+
bytesToWrite.subarray(0, allowableBytes),
73+
writtenBytes,
74+
);
75+
// writtenBytes += allowableBytes; // this can be skipped because we are going to immediately reset the view
76+
destination.enqueue(currentView);
77+
bytesToWrite = bytesToWrite.subarray(allowableBytes);
78+
}
79+
currentView = new Uint8Array(VIEW_SIZE);
80+
writtenBytes = 0;
81+
}
82+
((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes);
83+
writtenBytes += bytesToWrite.length;
3184
}
3285

3386
export function writeChunkAndReturn(
3487
destination: Destination,
3588
chunk: PrecomputedChunk | Chunk,
3689
): boolean {
37-
destination.enqueue(chunk);
38-
return destination.desiredSize > 0;
90+
writeChunk(destination, chunk);
91+
// in web streams there is no backpressure so we can alwas write more
92+
return true;
3993
}
4094

41-
export function completeWriting(destination: Destination) {}
95+
export function completeWriting(destination: Destination) {
96+
if (currentView && writtenBytes > 0) {
97+
destination.enqueue(new Uint8Array(currentView.buffer, 0, writtenBytes));
98+
currentView = null;
99+
writtenBytes = 0;
100+
}
101+
}
42102

43103
export function close(destination: Destination) {
44104
destination.close();

0 commit comments

Comments
 (0)