0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-04 01:44:26 -05:00

perf(ext/http): optimize for zero or one-packet response streams (#18834)

Improve `deno_reactdom_ssr_flash.jsx` by optimizing for zero/one-packet response streams.
This commit is contained in:
Matt Mastracci 2023-04-26 15:33:30 +02:00 committed by GitHub
parent 1b450015e7
commit 38681dfa88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 141 additions and 95 deletions

View file

@ -532,21 +532,43 @@ Deno.test(
}, },
); );
Deno.test( function createStreamTest(count: number, delay: number, action: string) {
{ permissions: { net: true } }, function doAction(controller: ReadableStreamDefaultController, i: number) {
async function httpServerStreamResponse() { if (i == count) {
const stream = new TransformStream(); if (action == "Throw") {
const writer = stream.writable.getWriter(); controller.error(new Error("Expected error!"));
writer.write(new TextEncoder().encode("hello ")); } else {
writer.write(new TextEncoder().encode("world")); controller.close();
writer.close(); }
} else {
controller.enqueue(`a${i}`);
const listeningPromise = deferred(); if (delay == 0) {
doAction(controller, i + 1);
} else {
setTimeout(() => doAction(controller, i + 1), delay);
}
}
}
function makeStream(count: number, delay: number): ReadableStream {
return new ReadableStream({
start(controller) {
if (delay == 0) {
doAction(controller, 0);
} else {
setTimeout(() => doAction(controller, 0), delay);
}
},
}).pipeThrough(new TextEncoderStream());
}
Deno.test(`httpServerStreamCount${count}Delay${delay}${action}`, async () => {
const ac = new AbortController(); const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({ const server = Deno.serve({
handler: (request) => { handler: async (request) => {
assert(!request.body); return new Response(makeStream(count, delay));
return new Response(stream.readable);
}, },
port: 4501, port: 4501,
signal: ac.signal, signal: ac.signal,
@ -556,12 +578,34 @@ Deno.test(
await listeningPromise; await listeningPromise;
const resp = await fetch("http://127.0.0.1:4501/"); const resp = await fetch("http://127.0.0.1:4501/");
const respBody = await resp.text(); const text = await resp.text();
assertEquals("hello world", respBody);
ac.abort(); ac.abort();
await server; await server;
}, let expected = "";
); if (action == "Throw" && count < 2 && delay < 1000) {
// NOTE: This is specific to the current implementation. In some cases where a stream errors, we
// don't send the first packet.
expected = "";
} else {
for (let i = 0; i < count; i++) {
expected += `a${i}`;
}
}
assertEquals(text, expected);
});
}
for (let count of [0, 1, 2, 3]) {
for (let delay of [0, 1, 1000]) {
// Creating a stream that errors in start will throw
if (delay > 0) {
createStreamTest(count, delay, "Throw");
}
createStreamTest(count, delay, "Close");
}
}
Deno.test( Deno.test(
{ permissions: { net: true } }, { permissions: { net: true } },
@ -1690,78 +1734,6 @@ createServerLengthTest("autoResponseWithUnknownLengthEmpty", {
expects_con_len: false, expects_con_len: false,
}); });
Deno.test(
{ permissions: { net: true } },
async function httpServerGetChunkedResponseWithKa() {
const promises = [deferred(), deferred()];
let reqCount = 0;
const listeningPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: async (request) => {
assertEquals(request.method, "GET");
promises[reqCount].resolve();
reqCount++;
return new Response(reqCount <= 1 ? stream("foo bar baz") : "zar quux");
},
port: 4503,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const conn = await Deno.connect({ port: 4503 });
const encoder = new TextEncoder();
{
const body =
`GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: keep-alive\r\n\r\n`;
const writeResult = await conn.write(encoder.encode(body));
assertEquals(body.length, writeResult);
await promises[0];
}
const decoder = new TextDecoder();
{
let msg = "";
while (true) {
try {
const buf = new Uint8Array(1024);
const readResult = await conn.read(buf);
assert(readResult);
msg += decoder.decode(buf.subarray(0, readResult));
assert(msg.endsWith("\r\nfoo bar baz\r\n0\r\n\r\n"));
break;
} catch {
continue;
}
}
}
// once more!
{
const body =
`GET /quux HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`;
const writeResult = await conn.write(encoder.encode(body));
assertEquals(body.length, writeResult);
await promises[1];
}
{
const buf = new Uint8Array(1024);
const readResult = await conn.read(buf);
assert(readResult);
const msg = decoder.decode(buf.subarray(0, readResult));
assert(msg.endsWith("zar quux"));
}
conn.close();
ac.abort();
await server;
},
);
Deno.test( Deno.test(
{ permissions: { net: true } }, { permissions: { net: true } },
async function httpServerPostWithContentLengthBody() { async function httpServerPostWithContentLengthBody() {

View file

@ -28,6 +28,7 @@ import {
import { import {
Deferred, Deferred,
getReadableStreamResourceBacking, getReadableStreamResourceBacking,
readableStreamClose,
readableStreamForRid, readableStreamForRid,
ReadableStreamPrototype, ReadableStreamPrototype,
} from "ext:deno_web/06_streams.js"; } from "ext:deno_web/06_streams.js";
@ -331,24 +332,97 @@ function fastSyncResponseOrStream(req, respBody) {
} }
async function asyncResponse(responseBodies, req, status, stream) { async function asyncResponse(responseBodies, req, status, stream) {
const responseRid = core.ops.op_set_response_body_stream(req);
SetPrototypeAdd(responseBodies, responseRid);
const reader = stream.getReader(); const reader = stream.getReader();
core.ops.op_set_promise_complete(req, status); let responseRid;
let closed = false;
let timeout;
try { try {
// IMPORTANT: We get a performance boost from this optimization, but V8 is very
// sensitive to the order and structure. Benchmark any changes to this code.
// Optimize for streams that are done in zero or one packets. We will not
// have to allocate a resource in this case.
const { value: value1, done: done1 } = await reader.read();
if (done1) {
closed = true;
// Exit 1: no response body at all, extreme fast path
// Reader will be closed by finally block
return;
}
// The second value cannot block indefinitely, as someone may be waiting on a response
// of the first packet that may influence this packet. We set this timeout arbitrarily to 250ms
// and we race it.
let timeoutPromise;
timeout = setTimeout(() => {
responseRid = core.ops.op_set_response_body_stream(req);
SetPrototypeAdd(responseBodies, responseRid);
core.ops.op_set_promise_complete(req, status);
timeoutPromise = core.writeAll(responseRid, value1);
}, 250);
const { value: value2, done: done2 } = await reader.read();
if (timeoutPromise) {
await timeoutPromise;
if (done2) {
closed = true;
// Exit 2(a): read 2 is EOS, and timeout resolved.
// Reader will be closed by finally block
// Response stream will be closed by finally block.
return;
}
// Timeout resolved, value1 written but read2 is not EOS. Carry value2 forward.
} else {
clearTimeout(timeout);
timeout = undefined;
if (done2) {
// Exit 2(b): read 2 is EOS, and timeout did not resolve as we read fast enough.
// Reader will be closed by finally block
// No response stream
closed = true;
core.ops.op_set_response_body_bytes(req, value1);
return;
}
responseRid = core.ops.op_set_response_body_stream(req);
SetPrototypeAdd(responseBodies, responseRid);
core.ops.op_set_promise_complete(req, status);
// Write our first packet
await core.writeAll(responseRid, value1);
}
await core.writeAll(responseRid, value2);
while (true) { while (true) {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done) { if (done) {
closed = true;
break; break;
} }
await core.writeAll(responseRid, value); await core.writeAll(responseRid, value);
} }
} catch (error) { } catch (error) {
closed = true;
try {
await reader.cancel(error); await reader.cancel(error);
} catch {
// Pass
}
} finally { } finally {
if (!closed) {
readableStreamClose(reader);
}
if (timeout !== undefined) {
clearTimeout(timeout);
}
if (responseRid) {
core.tryClose(responseRid); core.tryClose(responseRid);
SetPrototypeDelete(responseBodies, responseRid); SetPrototypeDelete(responseBodies, responseRid);
reader.releaseLock(); } else {
core.ops.op_set_promise_complete(req, status);
}
} }
} }