Skip to content

Commit f979ec3

Browse files
authored
perf: avoid unnecessary clone (#3117)
* perf(fetch): improve body mixin methods * perf: avoid unnecessary clone
1 parent 992e39d commit f979ec3

File tree

3 files changed

+46
-13
lines changed

3 files changed

+46
-13
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { group, bench, run } from 'mitata'
2+
import { Response } from '../../lib/web/fetch/response.js'
3+
4+
const settings = {
5+
small: 2 << 8,
6+
middle: 2 << 12,
7+
long: 2 << 16
8+
}
9+
10+
for (const [name, length] of Object.entries(settings)) {
11+
const buffer = Buffer.allocUnsafe(length).map(() => (Math.random() * 100) | 0)
12+
group(`${name} (length ${length})`, () => {
13+
bench('Response#arrayBuffer', async () => {
14+
return await new Response(buffer).arrayBuffer()
15+
})
16+
17+
// for comparison
18+
bench('Response#text', async () => {
19+
return await new Response(buffer).text()
20+
})
21+
})
22+
}
23+
24+
await run()

lib/web/fetch/body.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ function bodyMixinMethods (instance) {
309309
// Return a Blob whose contents are bytes and type attribute
310310
// is mimeType.
311311
return new Blob([bytes], { type: mimeType })
312-
}, instance)
312+
}, instance, false)
313313
},
314314

315315
arrayBuffer () {
@@ -318,20 +318,21 @@ function bodyMixinMethods (instance) {
318318
// given a byte sequence bytes: return a new ArrayBuffer
319319
// whose contents are bytes.
320320
return consumeBody(this, (bytes) => {
321-
return new Uint8Array(bytes).buffer
322-
}, instance)
321+
// Note: arrayBuffer already cloned.
322+
return bytes.buffer
323+
}, instance, true)
323324
},
324325

325326
text () {
326327
// The text() method steps are to return the result of running
327328
// consume body with this and UTF-8 decode.
328-
return consumeBody(this, utf8DecodeBytes, instance)
329+
return consumeBody(this, utf8DecodeBytes, instance, false)
329330
},
330331

331332
json () {
332333
// The json() method steps are to return the result of running
333334
// consume body with this and parse JSON from bytes.
334-
return consumeBody(this, parseJSONFromBytes, instance)
335+
return consumeBody(this, parseJSONFromBytes, instance, false)
335336
},
336337

337338
formData () {
@@ -383,7 +384,7 @@ function bodyMixinMethods (instance) {
383384
throw new TypeError(
384385
'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
385386
)
386-
}, instance)
387+
}, instance, false)
387388
}
388389
}
389390

@@ -399,8 +400,9 @@ function mixinBody (prototype) {
399400
* @param {Response|Request} object
400401
* @param {(value: unknown) => unknown} convertBytesToJSValue
401402
* @param {Response|Request} instance
403+
* @param {boolean} [shouldClone]
402404
*/
403-
async function consumeBody (object, convertBytesToJSValue, instance) {
405+
async function consumeBody (object, convertBytesToJSValue, instance, shouldClone) {
404406
webidl.brandCheck(object, instance)
405407

406408
// 1. If object is unusable, then return a promise rejected
@@ -432,13 +434,13 @@ async function consumeBody (object, convertBytesToJSValue, instance) {
432434
// 5. If object’s body is null, then run successSteps with an
433435
// empty byte sequence.
434436
if (object[kState].body == null) {
435-
successSteps(new Uint8Array())
437+
successSteps(Buffer.allocUnsafe(0))
436438
return promise.promise
437439
}
438440

439441
// 6. Otherwise, fully read object’s body given successSteps,
440442
// errorSteps, and object’s relevant global object.
441-
await fullyReadBody(object[kState].body, successSteps, errorSteps)
443+
await fullyReadBody(object[kState].body, successSteps, errorSteps, shouldClone)
442444

443445
// 7. Return promise.
444446
return promise.promise

lib/web/fetch/util.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,7 @@ function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueInde
10431043
/**
10441044
* @see https://fetch.spec.whatwg.org/#body-fully-read
10451045
*/
1046-
async function fullyReadBody (body, processBody, processBodyError) {
1046+
async function fullyReadBody (body, processBody, processBodyError, shouldClone) {
10471047
// 1. If taskDestination is null, then set taskDestination to
10481048
// the result of starting a new parallel queue.
10491049

@@ -1069,8 +1069,7 @@ async function fullyReadBody (body, processBody, processBodyError) {
10691069

10701070
// 5. Read all bytes from reader, given successSteps and errorSteps.
10711071
try {
1072-
const result = await readAllBytes(reader)
1073-
successSteps(result)
1072+
successSteps(await readAllBytes(reader, shouldClone))
10741073
} catch (e) {
10751074
errorSteps(e)
10761075
}
@@ -1118,8 +1117,9 @@ function isomorphicEncode (input) {
11181117
* @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
11191118
* @see https://streams.spec.whatwg.org/#read-loop
11201119
* @param {ReadableStreamDefaultReader} reader
1120+
* @param {boolean} [shouldClone]
11211121
*/
1122-
async function readAllBytes (reader) {
1122+
async function readAllBytes (reader, shouldClone) {
11231123
const bytes = []
11241124
let byteLength = 0
11251125

@@ -1128,6 +1128,13 @@ async function readAllBytes (reader) {
11281128

11291129
if (done) {
11301130
// 1. Call successSteps with bytes.
1131+
if (bytes.length === 1) {
1132+
const { buffer, byteOffset, byteLength } = bytes[0]
1133+
if (shouldClone === false) {
1134+
return Buffer.from(buffer, byteOffset, byteLength)
1135+
}
1136+
return Buffer.from(buffer.slice(byteOffset, byteOffset + byteLength), 0, byteLength)
1137+
}
11311138
return Buffer.concat(bytes, byteLength)
11321139
}
11331140

0 commit comments

Comments
 (0)