Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] write chunks to a buffer with no re-use #24034

Merged
merged 3 commits into from
Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,44 @@ describe('ReactDOMFizzServer', () => {
expect(rendered).toBe(false);
expect(isComplete).toBe(true);
});

// @gate experimental
it('should stream large contents that might overlow individual buffers', async () => {
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)`;
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)`;

// this specific layout is somewhat contrived to exercise the landing on
// an exact view boundary. it's not critical to test this edge case but
// since we are setting up a test in general for larger chunks I contrived it
// as such for now. I don't think it needs to be maintained if in the future
// the view sizes change or become dynamic becasue of the use of byobRequest
let stream;
stream = await ReactDOMFizzServer.renderToReadableStream(
<>
<div>
<span>{''}</span>
</div>
<div>{str492}</div>
<div>{str492}</div>
</>,
);

let result;
result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<div><span></span></div><div>${str492}</div><div>${str492}</div>"`,
);

// this size 2049 was chosen to be a couple base 2 orders larger than the current view
// size. if the size changes in the future hopefully this will still exercise
// a chunk that is too large for the view size.
stream = await ReactDOMFizzServer.renderToReadableStream(
<>
<div>{str2049}</div>
</>,
);

result = await readResult(stream);
expect(result).toMatchInlineSnapshot(`"<div>${str2049}</div>"`);
});
});
70 changes: 65 additions & 5 deletions packages/react-server/src/ReactServerStreamConfigBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,84 @@ export function flushBuffered(destination: Destination) {
// transform streams. https://github.com/whatwg/streams/issues/960
}

export function beginWriting(destination: Destination) {}
const VIEW_SIZE = 512;
let currentView = null;
let writtenBytes = 0;

export function beginWriting(destination: Destination) {
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}

export function writeChunk(
destination: Destination,
chunk: PrecomputedChunk | Chunk,
): void {
destination.enqueue(chunk);
if (chunk.length === 0) {
return;
}

if (chunk.length > VIEW_SIZE) {
// this chunk may overflow a single view which implies it was not
// one that is cached by the streaming renderer. We will enqueu
// it directly and expect it is not re-used
if (writtenBytes > 0) {
destination.enqueue(
new Uint8Array(
((currentView: any): Uint8Array).buffer,
0,
writtenBytes,
),
);
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
destination.enqueue(chunk);
return;
}

let bytesToWrite = chunk;
const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes;
if (allowableBytes < bytesToWrite.length) {
// this chunk would overflow the current view. We enqueue a full view
// and start a new view with the remaining chunk
if (allowableBytes === 0) {
// the current view is already full, send it
destination.enqueue(currentView);
} else {
// fill up the current view and apply the remaining chunk bytes
// to a new view.
((currentView: any): Uint8Array).set(
bytesToWrite.subarray(0, allowableBytes),
writtenBytes,
);
// writtenBytes += allowableBytes; // this can be skipped because we are going to immediately reset the view
destination.enqueue(currentView);
bytesToWrite = bytesToWrite.subarray(allowableBytes);
}
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes);
writtenBytes += bytesToWrite.length;
}

export function writeChunkAndReturn(
destination: Destination,
chunk: PrecomputedChunk | Chunk,
): boolean {
destination.enqueue(chunk);
return destination.desiredSize > 0;
writeChunk(destination, chunk);
// in web streams there is no backpressure so we can alwas write more
return true;
}

export function completeWriting(destination: Destination) {}
export function completeWriting(destination: Destination) {
if (currentView && writtenBytes > 0) {
destination.enqueue(new Uint8Array(currentView.buffer, 0, writtenBytes));
currentView = null;
writtenBytes = 0;
}
}

export function close(destination: Destination) {
destination.close();
Expand Down