Skip to content

Commit 511206e

Browse files
authored
reduce buffer copies (#867)
1 parent 9275e12 commit 511206e

File tree

5 files changed

+102
-35
lines changed

5 files changed

+102
-35
lines changed

.changeset/strange-cars-itch.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/util-body-length-node": patch
3+
"@smithy/node-http-handler": patch
4+
---
5+
6+
reduce buffer copies

packages/node-http-handler/src/node-http-handler.spec.ts

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import { NodeHttpHandler } from "./node-http-handler";
88
import { ReadFromBuffers } from "./readable.mock";
99
import {
1010
createContinueResponseFunction,
11+
createMirrorResponseFunction,
1112
createMockHttpServer,
1213
createMockHttpsServer,
1314
createResponseFunction,
15+
getResponseBody,
1416
} from "./server.mock";
1517

1618
describe("NodeHttpHandler", () => {
@@ -203,36 +205,39 @@ describe("NodeHttpHandler", () => {
203205
expect(response.body).toBeDefined();
204206
});
205207

206-
it("can send requests with bodies", async () => {
207-
const body = Buffer.from("test");
208-
const mockResponse = {
209-
statusCode: 200,
210-
headers: {},
211-
};
212-
mockHttpServer.addListener("request", createResponseFunction(mockResponse));
213-
const spy = jest.spyOn(http, "request").mockImplementationOnce(() => {
214-
const calls = spy.mock.calls;
215-
const currentIndex = calls.length - 1;
216-
return http.request(calls[currentIndex][0], calls[currentIndex][1]);
217-
});
218-
219-
const nodeHttpHandler = new NodeHttpHandler();
220-
const { response } = await nodeHttpHandler.handle(
221-
new HttpRequest({
222-
hostname: "localhost",
223-
method: "PUT",
224-
port: (mockHttpServer.address() as AddressInfo).port,
225-
protocol: "http:",
226-
path: "/",
208+
[
209+
{ name: "buffer", body: Buffer.from("Buffering🚀") },
210+
{ name: "uint8Array", body: Uint8Array.from(Buffer.from("uint8Array 🚀")) },
211+
{ name: "string", body: Buffer.from("string-test 🚀") },
212+
{ name: "uint8Array subarray", body: Uint8Array.from(Buffer.from("test")).subarray(1, 3) },
213+
{ name: "buffer subarray", body: Buffer.from("test").subarray(1, 3) },
214+
].forEach(({ body, name }) => {
215+
it(`can send requests with bodies ${name}`, async () => {
216+
const mockResponse = {
217+
statusCode: 200,
227218
headers: {},
228-
body,
229-
}),
230-
{}
231-
);
219+
};
220+
mockHttpServer.addListener("request", createMirrorResponseFunction(mockResponse));
221+
const nodeHttpHandler = new NodeHttpHandler();
222+
const { response } = await nodeHttpHandler.handle(
223+
new HttpRequest({
224+
hostname: "localhost",
225+
method: "PUT",
226+
port: (mockHttpServer.address() as AddressInfo).port,
227+
protocol: "http:",
228+
path: "/",
229+
headers: {},
230+
body,
231+
}),
232+
{}
233+
);
232234

233-
expect(response.statusCode).toEqual(mockResponse.statusCode);
234-
expect(response.headers).toBeDefined();
235-
expect(response.headers).toMatchObject(mockResponse.headers);
235+
expect(response.statusCode).toEqual(mockResponse.statusCode);
236+
expect(response.headers).toBeDefined();
237+
expect(response.headers).toMatchObject(mockResponse.headers);
238+
const responseBody = await getResponseBody(response);
239+
expect(responseBody).toEqual(Buffer.from(body).toString());
240+
});
236241
});
237242

238243
it("can handle expect 100-continue", async () => {

packages/node-http-handler/src/server.mock.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { HeaderBag, HttpResponse } from "@smithy/types";
1+
import { HeaderBag, HttpResponse, NodeJsRuntimeBlobTypes } from "@smithy/types";
22
import { readFileSync } from "fs";
33
import { createServer as createHttpServer, IncomingMessage, Server as HttpServer, ServerResponse } from "http";
44
import { createServer as createHttp2Server, Http2Server } from "http2";
@@ -14,7 +14,7 @@ const setResponseHeaders = (response: ServerResponse, headers: HeaderBag) => {
1414
}
1515
};
1616

17-
const setResponseBody = (response: ServerResponse, body: Readable | string) => {
17+
const setResponseBody = (response: ServerResponse, body: string | NodeJsRuntimeBlobTypes) => {
1818
if (body instanceof Readable) {
1919
body.pipe(response);
2020
} else {
@@ -67,3 +67,38 @@ export const createMockHttp2Server = (): Http2Server => {
6767
const server = createHttp2Server();
6868
return server;
6969
};
70+
71+
export const createMirrorResponseFunction = (httpResp: HttpResponse) => (
72+
request: IncomingMessage,
73+
response: ServerResponse
74+
) => {
75+
const bufs: Buffer[] = [];
76+
request.on("data", (chunk) => {
77+
bufs.push(chunk);
78+
});
79+
request.on("end", () => {
80+
response.statusCode = httpResp.statusCode;
81+
setResponseHeaders(response, httpResp.headers);
82+
setResponseBody(response, Buffer.concat(bufs));
83+
});
84+
request.on("error", (err) => {
85+
response.statusCode = 500;
86+
setResponseHeaders(response, httpResp.headers);
87+
setResponseBody(response, err.message);
88+
});
89+
};
90+
91+
export const getResponseBody = (response: HttpResponse) => {
92+
return new Promise<string>((resolve, reject) => {
93+
const bufs: Buffer[] = [];
94+
response.body.on("data", function (d: Buffer) {
95+
bufs.push(d);
96+
});
97+
response.body.on("end", function () {
98+
resolve(Buffer.concat(bufs).toString());
99+
});
100+
response.body.on("error", (err: Error) => {
101+
reject(err);
102+
});
103+
});
104+
};

packages/node-http-handler/src/write-request-body.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,30 @@ function writeBody(
5858
if (body instanceof Readable) {
5959
// pipe automatically handles end
6060
body.pipe(httpRequest);
61-
} else if (body) {
62-
httpRequest.end(Buffer.from(body as Parameters<typeof Buffer.from>[0]));
63-
} else {
64-
httpRequest.end();
61+
return;
6562
}
63+
64+
if (body) {
65+
if (Buffer.isBuffer(body) || typeof body === "string") {
66+
httpRequest.end(body);
67+
return;
68+
}
69+
70+
const uint8 = body as Uint8Array;
71+
if (
72+
typeof uint8 === "object" &&
73+
uint8.buffer &&
74+
typeof uint8.byteOffset === "number" &&
75+
typeof uint8.byteLength === "number"
76+
) {
77+
// this avoids copying the array.
78+
httpRequest.end(Buffer.from(uint8.buffer, uint8.byteOffset, uint8.byteLength));
79+
return;
80+
}
81+
82+
httpRequest.end(Buffer.from(body as ArrayBuffer));
83+
return;
84+
}
85+
86+
httpRequest.end();
6687
}

packages/util-body-length-node/src/calculateBodyLength.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const calculateBodyLength = (body: any): number | undefined => {
88
return 0;
99
}
1010
if (typeof body === "string") {
11-
return Buffer.from(body).length;
11+
return Buffer.byteLength(body);
1212
} else if (typeof body.byteLength === "number") {
1313
// handles Uint8Array, ArrayBuffer, Buffer, and ArrayBufferView
1414
return body.byteLength;

0 commit comments

Comments
 (0)