From 7e81d3c876bccd208a2e7b9c33f6fad4e3cf1b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Tue, 13 Jun 2023 21:13:34 +0200 Subject: [PATCH] perf(http): cache verified headers (#19465) Use `Map` to cache validated HTTP headers. Cache has a capacity of 4096 elements and it's cleared once that capacity is reached. In `preactssr` benchmark it lowers the time spent when adding headers from 180ms to 2.5ms. --- ext/fetch/20_headers.js | 51 ++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/ext/fetch/20_headers.js b/ext/fetch/20_headers.js index 6d934a7c1c..dcd3c95fc7 100644 --- a/ext/fetch/20_headers.js +++ b/ext/fetch/20_headers.js @@ -32,11 +32,16 @@ const { ObjectHasOwn, RegExpPrototypeExec, SafeArrayIterator, - SafeRegExp, + SafeMap, + MapPrototypeGet, + MapPrototypeHas, + MapPrototypeSet, + MapPrototypeClear, Symbol, SymbolFor, SymbolIterator, StringPrototypeReplaceAll, + StringPrototypeCharCodeAt, TypeError, } = primordials; @@ -87,9 +92,33 @@ function fillHeaders(headers, object) { } } -// Regex matching illegal chars in a header value -// deno-lint-ignore no-control-regex -const ILLEGAL_VALUE_CHARS = new SafeRegExp(/[\x00\x0A\x0D]/); +function checkForInvalidValueChars(value) { + for (let i = 0; i < value.length; i++) { + const c = StringPrototypeCharCodeAt(value, i); + + if (c === 0x0a || c === 0x0d || c === 0x00) { + return false; + } + } + + return true; +} + +const HEADER_NAME_CACHE = new SafeMap(); +function checkHeaderNameForHttpTokenCodePoint(name) { + if (MapPrototypeHas(HEADER_NAME_CACHE, name)) { + return MapPrototypeGet(HEADER_NAME_CACHE, name); + } + + const valid = RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, name) !== null; + + if (HEADER_NAME_CACHE.size > 4096) { + MapPrototypeClear(HEADER_NAME_CACHE); + } + MapPrototypeSet(HEADER_NAME_CACHE, name, valid); + + return valid; +} /** * https://fetch.spec.whatwg.org/#concept-headers-append @@ -102,10 +131,10 @@ function appendHeader(headers, name, value) { value = normalizeHeaderValue(value); // 2. - if (RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, name) === null) { + if (!checkHeaderNameForHttpTokenCodePoint(name)) { throw new TypeError("Header name is not valid."); } - if (RegExpPrototypeExec(ILLEGAL_VALUE_CHARS, value) !== null) { + if (!checkForInvalidValueChars(value)) { throw new TypeError("Header value is not valid."); } @@ -282,7 +311,7 @@ class Headers { webidl.requiredArguments(arguments.length, 1, prefix); name = webidl.converters["ByteString"](name, prefix, "Argument 1"); - if (RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, name) === null) { + if (!checkHeaderNameForHttpTokenCodePoint(name)) { throw new TypeError("Header name is not valid."); } if (this[_guard] == "immutable") { @@ -307,7 +336,7 @@ class Headers { webidl.requiredArguments(arguments.length, 1, prefix); name = webidl.converters["ByteString"](name, prefix, "Argument 1"); - if (RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, name) === null) { + if (!checkHeaderNameForHttpTokenCodePoint(name)) { throw new TypeError("Header name is not valid."); } @@ -323,7 +352,7 @@ class Headers { webidl.requiredArguments(arguments.length, 1, prefix); name = webidl.converters["ByteString"](name, prefix, "Argument 1"); - if (RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, name) === null) { + if (!checkHeaderNameForHttpTokenCodePoint(name)) { throw new TypeError("Header name is not valid."); } @@ -351,10 +380,10 @@ class Headers { value = normalizeHeaderValue(value); // 2. - if (RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, name) === null) { + if (!checkHeaderNameForHttpTokenCodePoint(name)) { throw new TypeError("Header name is not valid."); } - if (RegExpPrototypeExec(ILLEGAL_VALUE_CHARS, value) !== null) { + if (!checkForInvalidValueChars(value)) { throw new TypeError("Header value is not valid."); }