1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 15:24:46 -05:00
denoland-deno/ext/fetch/23_request.js
Marcos Casagrande 050ca39409
perf(ext/request): optimize validate and normalize HTTP method (#20143)
This PR optimizes `Request` constructor init method step. It doubles the
speed for known lowercased methods. I also added `PATCH` to known
methods

**this patch**

```
benchmark                   time (avg)        iter/s             (min … max)       p75       p99      p995
---------------------------------------------------------------------------- -----------------------------
method: GET                  1.49 µs/iter     669,336.9     (1.35 µs … 2.02 µs)   1.54 µs   2.02 µs   2.02 µs
method: PATCH                1.85 µs/iter     540,921.5     (1.65 µs … 2.02 µs)   1.91 µs   2.02 µs   2.02 µs
method: get                  1.49 µs/iter     669,067.9     (1.28 µs … 1.69 µs)   1.55 µs   1.69 µs   1.69 µs
```

**main**
```
cpu: 13th Gen Intel(R) Core(TM) i9-13900H
runtime: deno 1.36.1 (x86_64-unknown-linux-gnu)

benchmark                   time (avg)        iter/s             (min … max)       p75       p99      p995
---------------------------------------------------------------------------- -----------------------------
method: GET                   1.5 µs/iter     665,232.3      (1.3 µs … 2.02 µs)   1.54 µs   2.02 µs   2.02 µs
method: PATCH                2.47 µs/iter     404,052.7     (2.06 µs … 4.05 µs)   2.51 µs   4.05 µs   4.05 µs
method: get                     3 µs/iter     333,277.2     (2.72 µs … 4.04 µs)   3.05 µs   4.04 µs   4.04 µs
```

```js
Deno.bench("method: GET", () => {
  const r = new Request("https://deno.land", {
    method: "GET",
  });
});

Deno.bench("method: PATCH", () => {
  const r = new Request("https://deno.land", {
    method: "PATCH",
    body: '{"foo": "bar"}',
  });
});

Deno.bench("method: get", () => {
  const r = new Request("https://deno.land", {
    method: "get",
  });
});
```
2023-08-12 12:29:00 -06:00

571 lines
15 KiB
JavaScript

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// @ts-check
/// <reference path="../webidl/internal.d.ts" />
/// <reference path="../web/internal.d.ts" />
/// <reference path="../web/lib.deno_web.d.ts" />
/// <reference path="./internal.d.ts" />
/// <reference path="../web/06_streams_types.d.ts" />
/// <reference path="./lib.deno_fetch.d.ts" />
/// <reference lib="esnext" />
import * as webidl from "ext:deno_webidl/00_webidl.js";
import { createFilteredInspectProxy } from "ext:deno_console/01_console.js";
import {
byteUpperCase,
HTTP_TOKEN_CODE_POINT_RE,
} from "ext:deno_web/00_infra.js";
import { URL } from "ext:deno_url/00_url.js";
import { extractBody, mixinBody } from "ext:deno_fetch/22_body.js";
import { getLocationHref } from "ext:deno_web/12_location.js";
import { extractMimeType } from "ext:deno_web/01_mimesniff.js";
import { blobFromObjectUrl } from "ext:deno_web/09_file.js";
import {
fillHeaders,
getDecodeSplitHeader,
guardFromHeaders,
headerListFromHeaders,
headersFromHeaderList,
} from "ext:deno_fetch/20_headers.js";
import { HttpClientPrototype } from "ext:deno_fetch/22_http_client.js";
import * as abortSignal from "ext:deno_web/03_abort_signal.js";
const primordials = globalThis.__bootstrap.primordials;
const {
ArrayPrototypeMap,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
RegExpPrototypeExec,
StringPrototypeStartsWith,
Symbol,
SymbolFor,
TypeError,
} = primordials;
const _request = Symbol("request");
const _headers = Symbol("headers");
const _getHeaders = Symbol("get headers");
const _headersCache = Symbol("headers cache");
const _signal = Symbol("signal");
const _mimeType = Symbol("mime type");
const _body = Symbol("body");
const _url = Symbol("url");
const _method = Symbol("method");
/**
* @param {(() => string)[]} urlList
* @param {string[]} urlListProcessed
*/
function processUrlList(urlList, urlListProcessed) {
for (let i = 0; i < urlList.length; i++) {
if (urlListProcessed[i] === undefined) {
urlListProcessed[i] = urlList[i]();
}
}
return urlListProcessed;
}
/**
* @typedef InnerRequest
* @property {() => string} method
* @property {() => string} url
* @property {() => string} currentUrl
* @property {() => [string, string][]} headerList
* @property {null | typeof __window.bootstrap.fetchBody.InnerBody} body
* @property {"follow" | "error" | "manual"} redirectMode
* @property {number} redirectCount
* @property {(() => string)[]} urlList
* @property {string[]} urlListProcessed
* @property {number | null} clientRid NOTE: non standard extension for `Deno.HttpClient`.
* @property {Blob | null} blobUrlEntry
*/
/**
* @param {string} method
* @param {string | () => string} url
* @param {() => [string, string][]} headerList
* @param {typeof __window.bootstrap.fetchBody.InnerBody} body
* @param {boolean} maybeBlob
* @returns {InnerRequest}
*/
function newInnerRequest(method, url, headerList, body, maybeBlob) {
let blobUrlEntry = null;
if (
maybeBlob &&
typeof url === "string" &&
StringPrototypeStartsWith(url, "blob:")
) {
blobUrlEntry = blobFromObjectUrl(url);
}
return {
methodInner: method,
get method() {
return this.methodInner;
},
set method(value) {
this.methodInner = value;
},
headerListInner: null,
get headerList() {
if (this.headerListInner === null) {
try {
this.headerListInner = headerList();
} catch {
throw new TypeError("cannot read headers: request closed");
}
}
return this.headerListInner;
},
set headerList(value) {
this.headerListInner = value;
},
body,
redirectMode: "follow",
redirectCount: 0,
urlList: [typeof url === "string" ? () => url : url],
urlListProcessed: [],
clientRid: null,
blobUrlEntry,
url() {
if (this.urlListProcessed[0] === undefined) {
try {
this.urlListProcessed[0] = this.urlList[0]();
} catch {
throw new TypeError("cannot read url: request closed");
}
}
return this.urlListProcessed[0];
},
currentUrl() {
const currentIndex = this.urlList.length - 1;
if (this.urlListProcessed[currentIndex] === undefined) {
try {
this.urlListProcessed[currentIndex] = this.urlList[currentIndex]();
} catch {
throw new TypeError("cannot read url: request closed");
}
}
return this.urlListProcessed[currentIndex];
},
};
}
/**
* https://fetch.spec.whatwg.org/#concept-request-clone
* @param {InnerRequest} request
* @param {boolean} skipBody
* @returns {InnerRequest}
*/
function cloneInnerRequest(request, skipBody = false) {
const headerList = ArrayPrototypeMap(
request.headerList,
(x) => [x[0], x[1]],
);
let body = null;
if (request.body !== null && !skipBody) {
body = request.body.clone();
}
return {
method: request.method,
headerList,
body,
redirectMode: request.redirectMode,
redirectCount: request.redirectCount,
urlList: [() => request.url()],
urlListProcessed: [request.url()],
clientRid: request.clientRid,
blobUrlEntry: request.blobUrlEntry,
url() {
if (this.urlListProcessed[0] === undefined) {
try {
this.urlListProcessed[0] = this.urlList[0]();
} catch {
throw new TypeError("cannot read url: request closed");
}
}
return this.urlListProcessed[0];
},
currentUrl() {
const currentIndex = this.urlList.length - 1;
if (this.urlListProcessed[currentIndex] === undefined) {
try {
this.urlListProcessed[currentIndex] = this.urlList[currentIndex]();
} catch {
throw new TypeError("cannot read url: request closed");
}
}
return this.urlListProcessed[currentIndex];
},
};
}
// method => normalized method
const KNOWN_METHODS = {
"DELETE": "DELETE",
"delete": "DELETE",
"GET": "GET",
"get": "GET",
"HEAD": "HEAD",
"head": "HEAD",
"OPTIONS": "OPTIONS",
"options": "OPTIONS",
"PATCH": "PATCH",
"patch": "PATCH",
"POST": "POST",
"post": "POST",
"PUT": "PUT",
"put": "PUT",
};
/**
* @param {string} m
* @returns {string}
*/
function validateAndNormalizeMethod(m) {
if (RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, m) === null) {
throw new TypeError("Method is not valid.");
}
const upperCase = byteUpperCase(m);
if (
upperCase === "CONNECT" || upperCase === "TRACE" || upperCase === "TRACK"
) {
throw new TypeError("Method is forbidden.");
}
return upperCase;
}
class Request {
/** @type {InnerRequest} */
[_request];
/** @type {Headers} */
[_headersCache];
[_getHeaders];
/** @type {Headers} */
get [_headers]() {
if (this[_headersCache] === undefined) {
this[_headersCache] = this[_getHeaders]();
}
return this[_headersCache];
}
set [_headers](value) {
this[_headersCache] = value;
}
/** @type {AbortSignal} */
[_signal];
get [_mimeType]() {
const values = getDecodeSplitHeader(
headerListFromHeaders(this[_headers]),
"Content-Type",
);
return extractMimeType(values);
}
get [_body]() {
return this[_request].body;
}
/**
* https://fetch.spec.whatwg.org/#dom-request
* @param {RequestInfo} input
* @param {RequestInit} init
*/
constructor(input, init = {}) {
const prefix = "Failed to construct 'Request'";
webidl.requiredArguments(arguments.length, 1, prefix);
input = webidl.converters["RequestInfo_DOMString"](
input,
prefix,
"Argument 1",
);
init = webidl.converters["RequestInit"](init, prefix, "Argument 2");
this[webidl.brand] = webidl.brand;
/** @type {InnerRequest} */
let request;
const baseURL = getLocationHref();
// 4.
let signal = null;
// 5.
if (typeof input === "string") {
const parsedURL = new URL(input, baseURL);
request = newInnerRequest(
"GET",
parsedURL.href,
() => [],
null,
true,
);
} else { // 6.
if (!ObjectPrototypeIsPrototypeOf(RequestPrototype, input)) {
throw new TypeError("Unreachable");
}
const originalReq = input[_request];
// fold in of step 12 from below
request = cloneInnerRequest(originalReq, true);
request.redirectCount = 0; // reset to 0 - cloneInnerRequest copies the value
signal = input[_signal];
}
// 12. is folded into the else statement of step 6 above.
// 22.
if (init.redirect !== undefined) {
request.redirectMode = init.redirect;
}
// 25.
if (init.method !== undefined) {
const method = init.method;
// fast path: check for known methods
request.method = KNOWN_METHODS[method] ??
validateAndNormalizeMethod(method);
}
// 26.
if (init.signal !== undefined) {
signal = init.signal;
}
// NOTE: non standard extension. This handles Deno.HttpClient parameter
if (init.client !== undefined) {
if (
init.client !== null &&
!ObjectPrototypeIsPrototypeOf(HttpClientPrototype, init.client)
) {
throw webidl.makeException(
TypeError,
"`client` must be a Deno.HttpClient",
prefix,
"Argument 2",
);
}
request.clientRid = init.client?.rid ?? null;
}
// 27.
this[_request] = request;
// 28.
this[_signal] = abortSignal.newSignal();
// 29.
if (signal !== null) {
abortSignal.follow(this[_signal], signal);
}
// 30.
this[_headers] = headersFromHeaderList(request.headerList, "request");
// 32.
if (init.headers || ObjectKeys(init).length > 0) {
const headerList = headerListFromHeaders(this[_headers]);
const headers = init.headers ?? ArrayPrototypeSlice(
headerList,
0,
headerList.length,
);
if (headerList.length !== 0) {
ArrayPrototypeSplice(headerList, 0, headerList.length);
}
fillHeaders(this[_headers], headers);
}
// 33.
let inputBody = null;
if (ObjectPrototypeIsPrototypeOf(RequestPrototype, input)) {
inputBody = input[_body];
}
// 34.
if (
(request.method === "GET" || request.method === "HEAD") &&
((init.body !== undefined && init.body !== null) ||
inputBody !== null)
) {
throw new TypeError("Request with GET/HEAD method cannot have body.");
}
// 35.
let initBody = null;
// 36.
if (init.body !== undefined && init.body !== null) {
const res = extractBody(init.body);
initBody = res.body;
if (res.contentType !== null && !this[_headers].has("content-type")) {
this[_headers].append("Content-Type", res.contentType);
}
}
// 37.
const inputOrInitBody = initBody ?? inputBody;
// 39.
let finalBody = inputOrInitBody;
// 40.
if (initBody === null && inputBody !== null) {
if (input[_body] && input[_body].unusable()) {
throw new TypeError("Input request's body is unusable.");
}
finalBody = inputBody.createProxy();
}
// 41.
request.body = finalBody;
}
get method() {
webidl.assertBranded(this, RequestPrototype);
if (this[_method]) {
return this[_method];
}
this[_method] = this[_request].method;
return this[_method];
}
get url() {
webidl.assertBranded(this, RequestPrototype);
if (this[_url]) {
return this[_url];
}
this[_url] = this[_request].url();
return this[_url];
}
get headers() {
webidl.assertBranded(this, RequestPrototype);
return this[_headers];
}
get redirect() {
webidl.assertBranded(this, RequestPrototype);
return this[_request].redirectMode;
}
get signal() {
webidl.assertBranded(this, RequestPrototype);
return this[_signal];
}
clone() {
webidl.assertBranded(this, RequestPrototype);
if (this[_body] && this[_body].unusable()) {
throw new TypeError("Body is unusable.");
}
const newReq = cloneInnerRequest(this[_request]);
const newSignal = abortSignal.newSignal();
if (this[_signal]) {
abortSignal.follow(newSignal, this[_signal]);
}
return fromInnerRequest(
newReq,
newSignal,
guardFromHeaders(this[_headers]),
);
}
[SymbolFor("Deno.customInspect")](inspect) {
return inspect(createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(RequestPrototype, this),
keys: [
"bodyUsed",
"headers",
"method",
"redirect",
"url",
],
}));
}
}
webidl.configurePrototype(Request);
const RequestPrototype = Request.prototype;
mixinBody(RequestPrototype, _body, _mimeType);
webidl.converters["Request"] = webidl.createInterfaceConverter(
"Request",
RequestPrototype,
);
webidl.converters["RequestInfo_DOMString"] = (V, prefix, context, opts) => {
// Union for (Request or USVString)
if (typeof V == "object") {
if (ObjectPrototypeIsPrototypeOf(RequestPrototype, V)) {
return webidl.converters["Request"](V, prefix, context, opts);
}
}
// Passed to new URL(...) which implicitly converts DOMString -> USVString
return webidl.converters["DOMString"](V, prefix, context, opts);
};
webidl.converters["RequestRedirect"] = webidl.createEnumConverter(
"RequestRedirect",
[
"follow",
"error",
"manual",
],
);
webidl.converters["RequestInit"] = webidl.createDictionaryConverter(
"RequestInit",
[
{ key: "method", converter: webidl.converters["ByteString"] },
{ key: "headers", converter: webidl.converters["HeadersInit"] },
{
key: "body",
converter: webidl.createNullableConverter(
webidl.converters["BodyInit_DOMString"],
),
},
{ key: "redirect", converter: webidl.converters["RequestRedirect"] },
{
key: "signal",
converter: webidl.createNullableConverter(
webidl.converters["AbortSignal"],
),
},
{ key: "client", converter: webidl.converters.any },
],
);
/**
* @param {Request} request
* @returns {InnerRequest}
*/
function toInnerRequest(request) {
return request[_request];
}
/**
* @param {InnerRequest} inner
* @param {AbortSignal} signal
* @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard
* @returns {Request}
*/
function fromInnerRequest(inner, signal, guard) {
const request = webidl.createBranded(Request);
request[_request] = inner;
request[_signal] = signal;
request[_getHeaders] = () => headersFromHeaderList(inner.headerList, guard);
return request;
}
export {
fromInnerRequest,
newInnerRequest,
processUrlList,
Request,
RequestPrototype,
toInnerRequest,
};