mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -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:
parent
1b450015e7
commit
38681dfa88
2 changed files with 141 additions and 95 deletions
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
await reader.cancel(error);
|
closed = true;
|
||||||
|
try {
|
||||||
|
await reader.cancel(error);
|
||||||
|
} catch {
|
||||||
|
// Pass
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
core.tryClose(responseRid);
|
if (!closed) {
|
||||||
SetPrototypeDelete(responseBodies, responseRid);
|
readableStreamClose(reader);
|
||||||
reader.releaseLock();
|
}
|
||||||
|
if (timeout !== undefined) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
if (responseRid) {
|
||||||
|
core.tryClose(responseRid);
|
||||||
|
SetPrototypeDelete(responseBodies, responseRid);
|
||||||
|
} else {
|
||||||
|
core.ops.op_set_promise_complete(req, status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue