From a8728c43b90bb36c91515dbdb1afa8d180ede359 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:15:54 +0900 Subject: [PATCH 01/30] initial implementation --- lib/api/api-request.js | 2 +- lib/core/tree.js | 51 +++++++++++++++++++++++++++ lib/core/util.js | 79 ++++++++++++++++++++++++++++++++++-------- test/util.js | 27 ++++++++++----- 4 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 lib/core/tree.js diff --git a/lib/api/api-request.js b/lib/api/api-request.js index d4281ce2449..72303185c66 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -123,7 +123,7 @@ class RequestHandler extends AsyncResource { removeSignal(this) - util.parseHeaders(trailers, this.trailers) + util.parseHeaders(trailers, false, this.trailers) res.push(null) } diff --git a/lib/core/tree.js b/lib/core/tree.js new file mode 100644 index 00000000000..9e5d1e3f1ec --- /dev/null +++ b/lib/core/tree.js @@ -0,0 +1,51 @@ +const { wellknownHeaderNames } = require('./constants') + +class Tree { + #node + constructor () { + this.#node = {} + } + + /** + * @param {string} value + */ + insert (value) { + const target = Buffer.from((value = value.toLowerCase())) + let node = this.#node + for (let i = 0; i < target.length; ++i) { + const key = target[i] + node[key] ??= {} + if (key >= 0x61 && key <= 0x7a) { + node[key & ~32] ??= node[key] + } + node = node[key] + } + node.value = value + } + + /** + * @param {Uint8Array} buffer + * @returns {string | null} + */ + lookup (buffer) { + const length = buffer.length + let node = this.#node + for (let i = 0; i < length; ++i) { + const key = buffer[i] + if (!(key in node) || node === undefined) return null + node = node[key] + } + return node === undefined ? null : node.value + } +} + +const tree = new Tree() + +for (let i = 0; i < wellknownHeaderNames.length; ++i) { + tree.insert(wellknownHeaderNames[i]) +} + +module.exports = { + Tree, + tree +} diff --git a/lib/core/util.js b/lib/core/util.js index 5a28529b663..dc91475acfa 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -10,6 +10,7 @@ const { Blob } = require('buffer') const nodeUtil = require('util') const { stringify } = require('querystring') const { headerNameLowerCasedRecord } = require('./constants') +const { tree } = require('./tree') const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) @@ -219,28 +220,74 @@ function parseKeepAliveTimeout (val) { return m ? parseInt(m[1], 10) * 1000 : null } -function parseHeaders (headers, obj = {}) { +/** + * @param {string | Buffer} value + */ +function headerNameToString (value) { + const key = value.toString() + return headerNameLowerCasedRecord[key] ?? key.toLowerCase() +} + +/** + * @param {string | Buffer} value + */ +function headerNameToStringUnsafe (value) { + return typeof value === 'string' + ? headerNameLowerCasedRecord[value] ?? value.toLowerCase() + : tree.lookup(value) ?? value.toString().toLowerCase() +} + +/** + * @param {Record | (Buffer | string | (Buffer | string)[])[]} headers + * @param {boolean} [allowUnsafe] + * @param {Record} [obj] + * @returns + */ +function parseHeaders (headers, allowUnsafe, obj) { // For H2 support if (!Array.isArray(headers)) return headers - for (let i = 0; i < headers.length; i += 2) { - const key = headers[i].toString() - const lowerCasedKey = headerNameLowerCasedRecord[key] ?? key.toLowerCase() - let val = obj[lowerCasedKey] + if (allowUnsafe) { + if (obj === undefined) obj = {} + for (let i = 0; i < headers.length; i += 2) { + const key = headerNameToStringUnsafe(headers[i]) + let val = obj[key] - if (!val) { - const headersValue = headers[i + 1] - if (typeof headersValue === 'string') { - obj[lowerCasedKey] = headersValue + if (!val) { + const headersValue = headers[i + 1] + if (typeof headersValue === 'string') { + obj[key] = headersValue + } else { + obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8') + } } else { - obj[lowerCasedKey] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8') + if (typeof val === 'string') { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString('utf8')) } - } else { - if (!Array.isArray(val)) { - val = [val] - obj[lowerCasedKey] = val + } + } else { + if (obj === undefined) obj = {} + for (let i = 0; i < headers.length; i += 2) { + const key = headerNameToString(headers[i]) + let val = obj[key] + + if (!val) { + const headersValue = headers[i + 1] + if (typeof headersValue === 'string') { + obj[key] = headersValue + } else { + obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8') + } + } else { + if (typeof val === 'string') { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString('utf8')) } - val.push(headers[i + 1].toString('utf8')) } } @@ -483,6 +530,8 @@ module.exports = { isIterable, isAsyncIterable, isDestroyed, + headerNameToString, + headerNameToStringUnsafe, parseRawHeaders, parseHeaders, parseKeepAliveTimeout, diff --git a/test/util.js b/test/util.js index 75a4d8c1617..17ca55aab78 100644 --- a/test/util.js +++ b/test/util.js @@ -1,7 +1,6 @@ 'use strict' -const t = require('tap') -const { test } = t +const { test } = require('tap') const { Stream } = require('stream') const { EventEmitter } = require('events') @@ -85,12 +84,22 @@ test('validateHandler', (t) => { test('parseHeaders', (t) => { t.plan(6) - t.same(util.parseHeaders(['key', 'value']), { key: 'value' }) - t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), { key: 'value' }) - t.same(util.parseHeaders(['Key', 'Value']), { key: 'Value' }) - t.same(util.parseHeaders(['Key', 'value', 'key', 'Value']), { key: ['value', 'Value'] }) - t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), { key: ['value1', 'value2', 'value3'] }) - t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders(['key', 'value']), false, { key: 'value' }) + t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), false, { key: 'value' }) + t.same(util.parseHeaders(['Key', 'Value']), false, { key: 'Value' }) + t.same(util.parseHeaders(['Key', 'value', 'key', 'Value']), false, { key: ['value', 'Value'] }) + t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), false, { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), false, { key: ['value1', 'value2', 'value3'] }) +}) + +test('parseHeaders allowUnsafe', (t) => { + t.plan(6) + t.same(util.parseHeaders(['key', 'value']), true, { key: 'value' }) + t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), true, { key: 'value' }) + t.same(util.parseHeaders(['Key', 'Value']), true, { key: 'Value' }) + t.same(util.parseHeaders(['Key', 'value', 'key', 'Value']), true, { key: ['value', 'Value'] }) + t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), true, { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), true, { key: ['value1', 'value2', 'value3'] }) }) test('parseRawHeaders', (t) => { @@ -125,5 +134,5 @@ test('buildURL', (t) => { test('headerNameLowerCasedRecord', (t) => { t.plan(1) - t.ok(typeof headerNameLowerCasedRecord.hasOwnProperty === 'undefined') + t.ok(typeof headerNameLowerCasedRecord.hasOwnProperty !== 'function') }) From 8abb9c927ebd102b3b4f12ad0b7e2b62318e5fc8 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:39:08 +0900 Subject: [PATCH 02/30] test: fix --- test/util.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/util.js b/test/util.js index 17ca55aab78..e32c04e98f8 100644 --- a/test/util.js +++ b/test/util.js @@ -84,22 +84,22 @@ test('validateHandler', (t) => { test('parseHeaders', (t) => { t.plan(6) - t.same(util.parseHeaders(['key', 'value']), false, { key: 'value' }) - t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), false, { key: 'value' }) - t.same(util.parseHeaders(['Key', 'Value']), false, { key: 'Value' }) - t.same(util.parseHeaders(['Key', 'value', 'key', 'Value']), false, { key: ['value', 'Value'] }) - t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), false, { key: ['value1', 'value2', 'value3'] }) - t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), false, { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders(['key', 'value'], false), { key: 'value' }) + t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')], false), { key: 'value' }) + t.same(util.parseHeaders(['Key', 'Value'], false), { key: 'Value' }) + t.same(util.parseHeaders(['Key', 'value', 'key', 'Value'], false), { key: ['value', 'Value'] }) + t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']], false), { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]], false), { key: ['value1', 'value2', 'value3'] }) }) test('parseHeaders allowUnsafe', (t) => { t.plan(6) - t.same(util.parseHeaders(['key', 'value']), true, { key: 'value' }) - t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), true, { key: 'value' }) - t.same(util.parseHeaders(['Key', 'Value']), true, { key: 'Value' }) - t.same(util.parseHeaders(['Key', 'value', 'key', 'Value']), true, { key: ['value', 'Value'] }) - t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), true, { key: ['value1', 'value2', 'value3'] }) - t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), true, { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders(['key', 'value'], true), { key: 'value' }) + t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')], true), { key: 'value' }) + t.same(util.parseHeaders(['Key', 'Value'], true), { key: 'Value' }) + t.same(util.parseHeaders(['Key', 'value', 'key', 'Value'], true), { key: ['value', 'Value'] }) + t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']], true), { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]], true), { key: ['value1', 'value2', 'value3'] }) }) test('parseRawHeaders', (t) => { From 8324d21399538aa2f9840a3c337f3fc130b77ac5 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 6 Dec 2023 19:45:39 +0900 Subject: [PATCH 03/30] compatible API --- lib/api/api-request.js | 2 +- lib/core/util.js | 4 ++-- test/util.js | 25 +++++++++++++------------ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/api/api-request.js b/lib/api/api-request.js index 72303185c66..d4281ce2449 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -123,7 +123,7 @@ class RequestHandler extends AsyncResource { removeSignal(this) - util.parseHeaders(trailers, false, this.trailers) + util.parseHeaders(trailers, this.trailers) res.push(null) } diff --git a/lib/core/util.js b/lib/core/util.js index dc91475acfa..fa7ceaa6b9b 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -239,11 +239,11 @@ function headerNameToStringUnsafe (value) { /** * @param {Record | (Buffer | string | (Buffer | string)[])[]} headers - * @param {boolean} [allowUnsafe] * @param {Record} [obj] + * @param {boolean} [allowUnsafe] * @returns */ -function parseHeaders (headers, allowUnsafe, obj) { +function parseHeaders (headers, obj, allowUnsafe) { // For H2 support if (!Array.isArray(headers)) return headers diff --git a/test/util.js b/test/util.js index e32c04e98f8..ba1035eba68 100644 --- a/test/util.js +++ b/test/util.js @@ -84,22 +84,23 @@ test('validateHandler', (t) => { test('parseHeaders', (t) => { t.plan(6) - t.same(util.parseHeaders(['key', 'value'], false), { key: 'value' }) - t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')], false), { key: 'value' }) - t.same(util.parseHeaders(['Key', 'Value'], false), { key: 'Value' }) - t.same(util.parseHeaders(['Key', 'value', 'key', 'Value'], false), { key: ['value', 'Value'] }) - t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']], false), { key: ['value1', 'value2', 'value3'] }) - t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]], false), { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders(['key', 'value']), { key: 'value' }) + t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), { key: 'value' }) + t.same(util.parseHeaders(['Key', 'Value']), { key: 'Value' }) + t.same(util.parseHeaders(['Key', 'value', 'key', 'Value']), { key: ['value', 'Value'] }) + t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), { key: ['value1', 'value2', 'value3'] }) }) test('parseHeaders allowUnsafe', (t) => { t.plan(6) - t.same(util.parseHeaders(['key', 'value'], true), { key: 'value' }) - t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')], true), { key: 'value' }) - t.same(util.parseHeaders(['Key', 'Value'], true), { key: 'Value' }) - t.same(util.parseHeaders(['Key', 'value', 'key', 'Value'], true), { key: ['value', 'Value'] }) - t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']], true), { key: ['value1', 'value2', 'value3'] }) - t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]], true), { key: ['value1', 'value2', 'value3'] }) + const parseHeaders = (a, b) => util.parseHeaders(a, b, true) + t.same(parseHeaders(['key', 'value']), { key: 'value' }) + t.same(parseHeaders([Buffer.from('key'), Buffer.from('value')]), { key: 'value' }) + t.same(parseHeaders(['Key', 'Value']), { key: 'Value' }) + t.same(parseHeaders(['Key', 'value', 'key', 'Value']), { key: ['value', 'Value'] }) + t.same(parseHeaders(['key', ['value1', 'value2', 'value3']]), { key: ['value1', 'value2', 'value3'] }) + t.same(parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), { key: ['value1', 'value2', 'value3'] }) }) test('parseRawHeaders', (t) => { From b9fc8056e3c5d059d869ae63f64f199880823dd7 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 6 Dec 2023 19:50:52 +0900 Subject: [PATCH 04/30] fix: tree --- lib/core/tree.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index 9e5d1e3f1ec..dc2f07b38d5 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -2,8 +2,11 @@ const { wellknownHeaderNames } = require('./constants') class Tree { #node + /** @type {number} */ + #depth constructor () { this.#node = {} + this.#depth = 0 } /** @@ -11,8 +14,9 @@ class Tree { */ insert (value) { const target = Buffer.from((value = value.toLowerCase())) + const length = target.length let node = this.#node - for (let i = 0; i < target.length; ++i) { + for (let i = 0; i < length; ++i) { const key = target[i] node[key] ??= {} if (key >= 0x61 && key <= 0x7a) { @@ -21,6 +25,9 @@ class Tree { node = node[key] } node.value = value + if (length > this.#depth) { + this.#depth = length + } } /** @@ -29,13 +36,14 @@ class Tree { */ lookup (buffer) { const length = buffer.length + if (length > this.#depth) return null let node = this.#node for (let i = 0; i < length; ++i) { const key = buffer[i] if (!(key in node) || node === undefined) return null node = node[key] } - return node === undefined ? null : node.value + return node?.value ?? null } } From 42b0a4622c29094989607b9ededfba1dc154f027 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 6 Dec 2023 21:10:11 +0900 Subject: [PATCH 05/30] add benchmark --- benchmarks/parseHeaders.mjs | 51 +++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 52 insertions(+) create mode 100644 benchmarks/parseHeaders.mjs diff --git a/benchmarks/parseHeaders.mjs b/benchmarks/parseHeaders.mjs new file mode 100644 index 00000000000..296446e2347 --- /dev/null +++ b/benchmarks/parseHeaders.mjs @@ -0,0 +1,51 @@ +import { bench, group, run } from 'mitata' +import { parseHeaders } from './lib/core/util.js' + +const headers = Object.entries({ + 'Content-Type': 'application/json', + Date: 'Wed, 01 Nov 2023 00:00:00 GMT', + 'Powered-By': 'NodeJS', + 'Content-Encoding': 'gzip', + 'Set-Cookie': '__Secure-ID=123; Secure; Domain=example.com', + 'Content-Length': '150', + Vary: 'Accept-Encoding, Accept, X-Requested-With' +}).flat().map(c => Buffer.from(c)) + +const headersIrregular = Object.entries({ + 'Content-type': 'application/json', + DaTe: 'Wed, 01 Nov 2023 00:00:00 GMT', + 'Powered-by': 'NodeJS', + 'Content-encoding': 'gzip', + 'Set-cookie': '__Secure-ID=123; Secure; Domain=example.com', + 'Content-length': '150', + VaRy: 'Accept-Encoding, Accept, X-Requested-With' +}).flat().map(c => Buffer.from(c)) + +// avoid JIT bias +bench('noop', () => {}) +bench('noop', () => {}) +bench('noop', () => {}) +bench('noop', () => {}) +bench('noop', () => {}) +bench('noop', () => {}) + +group('parseHeaders', () => { + bench('default', () => { + parseHeaders(headers, undefined, false) + }) + bench('allowUnsafe', () => { + parseHeaders(headers, undefined, true) + }) +}) + +group('parseHeaders (irregular)', () => { + bench('default', () => { + parseHeaders(headersIrregular, undefined, false) + }) + bench('allowUnsafe', () => { + parseHeaders(headersIrregular, undefined, true) + }) +}) + + +await run() diff --git a/package.json b/package.json index 0933d911830..05aa2050474 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "jest": "^29.0.2", "jsdom": "^23.0.0", "jsfuzz": "^1.0.15", + "mitata": "^0.1.6", "mocha": "^10.0.0", "mockttp": "^3.9.2", "p-timeout": "^3.2.0", From 559e6db6bb797f81d4765e08a1f8560cd65c362c Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 6 Dec 2023 21:11:08 +0900 Subject: [PATCH 06/30] fix: lint --- benchmarks/parseHeaders.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/benchmarks/parseHeaders.mjs b/benchmarks/parseHeaders.mjs index 296446e2347..31999e76e25 100644 --- a/benchmarks/parseHeaders.mjs +++ b/benchmarks/parseHeaders.mjs @@ -47,5 +47,4 @@ group('parseHeaders (irregular)', () => { }) }) - await run() From 007e7de578b1ba756209665c48c2682cd0dc3537 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 6 Dec 2023 21:22:38 +0900 Subject: [PATCH 07/30] fix: benchmark --- benchmarks/parseHeaders.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benchmarks/parseHeaders.mjs b/benchmarks/parseHeaders.mjs index 31999e76e25..65fcc74746b 100644 --- a/benchmarks/parseHeaders.mjs +++ b/benchmarks/parseHeaders.mjs @@ -47,4 +47,6 @@ group('parseHeaders (irregular)', () => { }) }) +await new Promise((resolve) => setTimeout(resolve, 7000)) + await run() From 82c198dd364ee7bfb420f0f3475c078795784c1e Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 6 Dec 2023 21:39:31 +0900 Subject: [PATCH 08/30] perf --- lib/core/tree.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index dc2f07b38d5..9911e97cf3b 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -39,9 +39,7 @@ class Tree { if (length > this.#depth) return null let node = this.#node for (let i = 0; i < length; ++i) { - const key = buffer[i] - if (!(key in node) || node === undefined) return null - node = node[key] + if ((node = node?.[buffer[i]]) === undefined) return null } return node?.value ?? null } From 061cd39e40bbecd2faa165dcaec48ebfdab348e9 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 6 Dec 2023 21:48:56 +0900 Subject: [PATCH 09/30] use number key --- lib/core/tree.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index 9911e97cf3b..d062fe445a8 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -24,7 +24,7 @@ class Tree { } node = node[key] } - node.value = value + node[256] = value if (length > this.#depth) { this.#depth = length } @@ -41,7 +41,7 @@ class Tree { for (let i = 0; i < length; ++i) { if ((node = node?.[buffer[i]]) === undefined) return null } - return node?.value ?? null + return node?.[256] ?? null } } From 7c5f84ea4f4190aaeb5bc32dc231f6164b01bf11 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 05:26:31 +0900 Subject: [PATCH 10/30] remove unsafe --- lib/core/util.js | 63 ++++++++++++------------------------------------ test/util.js | 11 --------- 2 files changed, 15 insertions(+), 59 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index fa7ceaa6b9b..8567f1be15d 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -224,14 +224,6 @@ function parseKeepAliveTimeout (val) { * @param {string | Buffer} value */ function headerNameToString (value) { - const key = value.toString() - return headerNameLowerCasedRecord[key] ?? key.toLowerCase() -} - -/** - * @param {string | Buffer} value - */ -function headerNameToStringUnsafe (value) { return typeof value === 'string' ? headerNameLowerCasedRecord[value] ?? value.toLowerCase() : tree.lookup(value) ?? value.toString().toLowerCase() @@ -240,54 +232,30 @@ function headerNameToStringUnsafe (value) { /** * @param {Record | (Buffer | string | (Buffer | string)[])[]} headers * @param {Record} [obj] - * @param {boolean} [allowUnsafe] * @returns */ -function parseHeaders (headers, obj, allowUnsafe) { +function parseHeaders (headers, obj) { // For H2 support if (!Array.isArray(headers)) return headers - if (allowUnsafe) { - if (obj === undefined) obj = {} - for (let i = 0; i < headers.length; i += 2) { - const key = headerNameToStringUnsafe(headers[i]) - let val = obj[key] + if (obj === undefined) obj = {} + for (let i = 0; i < headers.length; i += 2) { + const key = headerNameToString(headers[i]) + let val = obj[key] - if (!val) { - const headersValue = headers[i + 1] - if (typeof headersValue === 'string') { - obj[key] = headersValue - } else { - obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8') - } + if (!val) { + const headersValue = headers[i + 1] + if (typeof headersValue === 'string') { + obj[key] = headersValue } else { - if (typeof val === 'string') { - val = [val] - obj[key] = val - } - val.push(headers[i + 1].toString('utf8')) + obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8') } - } - } else { - if (obj === undefined) obj = {} - for (let i = 0; i < headers.length; i += 2) { - const key = headerNameToString(headers[i]) - let val = obj[key] - - if (!val) { - const headersValue = headers[i + 1] - if (typeof headersValue === 'string') { - obj[key] = headersValue - } else { - obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8') - } - } else { - if (typeof val === 'string') { - val = [val] - obj[key] = val - } - val.push(headers[i + 1].toString('utf8')) + } else { + if (typeof val === 'string') { + val = [val] + obj[key] = val } + val.push(headers[i + 1].toString('utf8')) } } @@ -531,7 +499,6 @@ module.exports = { isAsyncIterable, isDestroyed, headerNameToString, - headerNameToStringUnsafe, parseRawHeaders, parseHeaders, parseKeepAliveTimeout, diff --git a/test/util.js b/test/util.js index ba1035eba68..71a63f5c8af 100644 --- a/test/util.js +++ b/test/util.js @@ -92,17 +92,6 @@ test('parseHeaders', (t) => { t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), { key: ['value1', 'value2', 'value3'] }) }) -test('parseHeaders allowUnsafe', (t) => { - t.plan(6) - const parseHeaders = (a, b) => util.parseHeaders(a, b, true) - t.same(parseHeaders(['key', 'value']), { key: 'value' }) - t.same(parseHeaders([Buffer.from('key'), Buffer.from('value')]), { key: 'value' }) - t.same(parseHeaders(['Key', 'Value']), { key: 'Value' }) - t.same(parseHeaders(['Key', 'value', 'key', 'Value']), { key: ['value', 'Value'] }) - t.same(parseHeaders(['key', ['value1', 'value2', 'value3']]), { key: ['value1', 'value2', 'value3'] }) - t.same(parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), { key: ['value1', 'value2', 'value3'] }) -}) - test('parseRawHeaders', (t) => { t.plan(1) t.same(util.parseRawHeaders(['key', 'value', Buffer.from('key'), Buffer.from('value')]), ['key', 'value', 'key', 'value']) From a6a37a7fb7809ed7ac4423e851ebe7d2f48351e6 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 05:36:12 +0900 Subject: [PATCH 11/30] format & add comment --- lib/core/tree.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index d062fe445a8..f130179b1a8 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -1,15 +1,11 @@ const { wellknownHeaderNames } = require('./constants') class Tree { - #node - /** @type {number} */ - #depth - constructor () { - this.#node = {} - this.#depth = 0 - } + #node = {} + #depth = 0 /** + * Lowercases key and inserts its value into the node. * @param {string} value */ insert (value) { @@ -19,7 +15,9 @@ class Tree { for (let i = 0; i < length; ++i) { const key = target[i] node[key] ??= {} + // a-z if (key >= 0x61 && key <= 0x7a) { + // Uppercase letters preserve references to lowercase ones. node[key & ~32] ??= node[key] } node = node[key] @@ -31,15 +29,16 @@ class Tree { } /** - * @param {Uint8Array} buffer - * @returns {string | null} + * Retrieves values from a node. + * @param {Uint8Array} key Node Key + * @returns {string | null} Value */ - lookup (buffer) { - const length = buffer.length + lookup (key) { + const length = key.length if (length > this.#depth) return null let node = this.#node for (let i = 0; i < length; ++i) { - if ((node = node?.[buffer[i]]) === undefined) return null + if ((node = node?.[key[i]]) === undefined) return null } return node?.[256] ?? null } From 07efe2e8347ce4528f8e2c9907fe06e1bf8f43cd Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 05:40:22 +0900 Subject: [PATCH 12/30] fix: benchmark import path --- benchmarks/parseHeaders.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/parseHeaders.mjs b/benchmarks/parseHeaders.mjs index 65fcc74746b..960eabc087c 100644 --- a/benchmarks/parseHeaders.mjs +++ b/benchmarks/parseHeaders.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { parseHeaders } from './lib/core/util.js' +import { parseHeaders } from '../lib/core/util.js' const headers = Object.entries({ 'Content-Type': 'application/json', From f7dbb9863a138163085ad08d75955eaf94e4e80d Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:32:22 +0900 Subject: [PATCH 13/30] better benchmark --- benchmarks/parseHeaders.mjs | 77 +++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/benchmarks/parseHeaders.mjs b/benchmarks/parseHeaders.mjs index 960eabc087c..056c3e1d73a 100644 --- a/benchmarks/parseHeaders.mjs +++ b/benchmarks/parseHeaders.mjs @@ -1,7 +1,7 @@ -import { bench, group, run } from 'mitata' +import { bench, run } from 'mitata' import { parseHeaders } from '../lib/core/util.js' -const headers = Object.entries({ +const headers = Array.from([{ 'Content-Type': 'application/json', Date: 'Wed, 01 Nov 2023 00:00:00 GMT', 'Powered-By': 'NodeJS', @@ -9,7 +9,60 @@ const headers = Object.entries({ 'Set-Cookie': '__Secure-ID=123; Secure; Domain=example.com', 'Content-Length': '150', Vary: 'Accept-Encoding, Accept, X-Requested-With' -}).flat().map(c => Buffer.from(c)) +}, +{ + 'Content-Type': 'text/html; charset=UTF-8', + 'Content-Length': '1234', + Date: 'Wed, 06 Dec 2023 12:47:57 GMT', + Server: 'Bing' +}, +{ + 'Content-Type': 'image/jpeg', + 'Content-Length': '56789', + Date: 'Wed, 06 Dec 2023 12:48:12 GMT', + Server: 'Bing', + ETag: '"a1b2c3d4e5f6g7h8i9j0"' +}, +{ + Cookie: 'session_id=1234567890abcdef', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + Host: 'www.bing.com', + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate, br' +}, +{ + Location: 'https://www.bing.com/search?q=bing', + Status: '302 Found', + Date: 'Wed, 06 Dec 2023 12:48:27 GMT', + Server: 'Bing', + 'Content-Type': 'text/html; charset=UTF-8', + 'Content-Length': '0' +}, +{ + 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary1234567890', + 'Content-Length': '98765', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + Host: 'www.bing.com', + Accept: '*/*', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate, br' +}, +{ + 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Length': '2345', + Date: 'Wed, 06 Dec 2023 12:48:42 GMT', + Server: 'Bing', + Status: '200 OK', + 'Cache-Control': 'no-cache, no-store, must-revalidate' +}, +{ + Host: 'www.example.com', + Connection: 'keep-alive', + Accept: 'text/html, application/xhtml+xml, application/xml;q=0.9,;q=0.8', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' +} +], (x) => Object.entries(x).flat().map(c => Buffer.from(c))) const headersIrregular = Object.entries({ 'Content-type': 'application/json', @@ -29,22 +82,12 @@ bench('noop', () => {}) bench('noop', () => {}) bench('noop', () => {}) -group('parseHeaders', () => { - bench('default', () => { - parseHeaders(headers, undefined, false) - }) - bench('allowUnsafe', () => { - parseHeaders(headers, undefined, true) - }) +bench('parseHeaders', () => { + for (let i = 0; i < headers.length; ++i) { parseHeaders(headers[i]) } }) -group('parseHeaders (irregular)', () => { - bench('default', () => { - parseHeaders(headersIrregular, undefined, false) - }) - bench('allowUnsafe', () => { - parseHeaders(headersIrregular, undefined, true) - }) +bench('parseHeaders (irregular)', () => { + parseHeaders(headersIrregular) }) await new Promise((resolve) => setTimeout(resolve, 7000)) From 0e7cba8338f265d6fd95d3c406007f6b156d1fb4 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:39:37 +0900 Subject: [PATCH 14/30] better benchmark --- benchmarks/parseHeaders.mjs | 166 +++++++++++++++++++----------------- 1 file changed, 88 insertions(+), 78 deletions(-) diff --git a/benchmarks/parseHeaders.mjs b/benchmarks/parseHeaders.mjs index 056c3e1d73a..6fb898062b3 100644 --- a/benchmarks/parseHeaders.mjs +++ b/benchmarks/parseHeaders.mjs @@ -1,78 +1,83 @@ -import { bench, run } from 'mitata' +import { bench, group, run } from 'mitata' import { parseHeaders } from '../lib/core/util.js' -const headers = Array.from([{ - 'Content-Type': 'application/json', - Date: 'Wed, 01 Nov 2023 00:00:00 GMT', - 'Powered-By': 'NodeJS', - 'Content-Encoding': 'gzip', - 'Set-Cookie': '__Secure-ID=123; Secure; Domain=example.com', - 'Content-Length': '150', - Vary: 'Accept-Encoding, Accept, X-Requested-With' -}, -{ - 'Content-Type': 'text/html; charset=UTF-8', - 'Content-Length': '1234', - Date: 'Wed, 06 Dec 2023 12:47:57 GMT', - Server: 'Bing' -}, -{ - 'Content-Type': 'image/jpeg', - 'Content-Length': '56789', - Date: 'Wed, 06 Dec 2023 12:48:12 GMT', - Server: 'Bing', - ETag: '"a1b2c3d4e5f6g7h8i9j0"' -}, -{ - Cookie: 'session_id=1234567890abcdef', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', - Host: 'www.bing.com', - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate, br' -}, -{ - Location: 'https://www.bing.com/search?q=bing', - Status: '302 Found', - Date: 'Wed, 06 Dec 2023 12:48:27 GMT', - Server: 'Bing', - 'Content-Type': 'text/html; charset=UTF-8', - 'Content-Length': '0' -}, -{ - 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary1234567890', - 'Content-Length': '98765', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', - Host: 'www.bing.com', - Accept: '*/*', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate, br' -}, -{ - 'Content-Type': 'application/json; charset=UTF-8', - 'Content-Length': '2345', - Date: 'Wed, 06 Dec 2023 12:48:42 GMT', - Server: 'Bing', - Status: '200 OK', - 'Cache-Control': 'no-cache, no-store, must-revalidate' -}, -{ - Host: 'www.example.com', - Connection: 'keep-alive', - Accept: 'text/html, application/xhtml+xml, application/xml;q=0.9,;q=0.8', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' -} -], (x) => Object.entries(x).flat().map(c => Buffer.from(c))) +const target = [ + { + 'Content-Type': 'application/json', + Date: 'Wed, 01 Nov 2023 00:00:00 GMT', + 'Powered-By': 'NodeJS', + 'Content-Encoding': 'gzip', + 'Set-Cookie': '__Secure-ID=123; Secure; Domain=example.com', + 'Content-Length': '150', + Vary: 'Accept-Encoding, Accept, X-Requested-With' + }, + { + 'Content-Type': 'text/html; charset=UTF-8', + 'Content-Length': '1234', + Date: 'Wed, 06 Dec 2023 12:47:57 GMT', + Server: 'Bing' + }, + { + 'Content-Type': 'image/jpeg', + 'Content-Length': '56789', + Date: 'Wed, 06 Dec 2023 12:48:12 GMT', + Server: 'Bing', + ETag: '"a1b2c3d4e5f6g7h8i9j0"' + }, + { + Cookie: 'session_id=1234567890abcdef', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + Host: 'www.bing.com', + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate, br' + }, + { + Location: 'https://www.bing.com/search?q=bing', + Status: '302 Found', + Date: 'Wed, 06 Dec 2023 12:48:27 GMT', + Server: 'Bing', + 'Content-Type': 'text/html; charset=UTF-8', + 'Content-Length': '0' + }, + { + 'Content-Type': + 'multipart/form-data; boundary=----WebKitFormBoundary1234567890', + 'Content-Length': '98765', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + Host: 'www.bing.com', + Accept: '*/*', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate, br' + }, + { + 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Length': '2345', + Date: 'Wed, 06 Dec 2023 12:48:42 GMT', + Server: 'Bing', + Status: '200 OK', + 'Cache-Control': 'no-cache, no-store, must-revalidate' + }, + { + Host: 'www.example.com', + Connection: 'keep-alive', + Accept: 'text/html, application/xhtml+xml, application/xml;q=0.9,;q=0.8', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + } +] -const headersIrregular = Object.entries({ - 'Content-type': 'application/json', - DaTe: 'Wed, 01 Nov 2023 00:00:00 GMT', - 'Powered-by': 'NodeJS', - 'Content-encoding': 'gzip', - 'Set-cookie': '__Secure-ID=123; Secure; Domain=example.com', - 'Content-length': '150', - VaRy: 'Accept-Encoding, Accept, X-Requested-With' -}).flat().map(c => Buffer.from(c)) +const headers = Array.from(target, (x) => + Object.entries(x) + .flat() + .map((c) => Buffer.from(c)) +) + +const headersIrregular = Array.from( + target, + (x) => Object.entries(x) + .flat() + .map((c) => Buffer.from(c.toUpperCase())) +) // avoid JIT bias bench('noop', () => {}) @@ -82,12 +87,17 @@ bench('noop', () => {}) bench('noop', () => {}) bench('noop', () => {}) -bench('parseHeaders', () => { - for (let i = 0; i < headers.length; ++i) { parseHeaders(headers[i]) } -}) - -bench('parseHeaders (irregular)', () => { - parseHeaders(headersIrregular) +group('parseHeaders', () => { + bench('parseHeaders', () => { + for (let i = 0; i < headers.length; ++i) { + parseHeaders(headers[i]) + } + }) + bench('parseHeaders (irregular)', () => { + for (let i = 0; i < headersIrregular.length; ++i) { + parseHeaders(headersIrregular[i]) + } + }) }) await new Promise((resolve) => setTimeout(resolve, 7000)) From feb97c2cd2f3791a92a2f4d7967d1e1cee2d0232 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:15:42 +0900 Subject: [PATCH 15/30] perf: rewrite tree --- lib/core/tree.js | 143 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 110 insertions(+), 33 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index f130179b1a8..081b9a379a9 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -1,56 +1,133 @@ const { wellknownHeaderNames } = require('./constants') -class Tree { - #node = {} - #depth = 0 +class Node { + /** @type {any} */ + value + /** @type {null | Node} */ + left + /** @type {null | Node} */ + middle + /** @type {null | Node} */ + right + /** @type {number} */ + code + /** + * @param {Uint8Array} key + * @param {any} value + */ + constructor (key, value) { + const length = key.length + if (length === 0) { + throw new TypeError('Unreachable') + } + this.value = null + this.left = null + this.middle = null + this.right = null + this.code = key[0] + if (length > 1) { + this.middle = new Node(key.subarray(1), value) + } else { + this.value = value + } + } /** - * Lowercases key and inserts its value into the node. - * @param {string} value + * @param {Uint8Array} key + * @param {any} value */ - insert (value) { - const target = Buffer.from((value = value.toLowerCase())) - const length = target.length - let node = this.#node - for (let i = 0; i < length; ++i) { - const key = target[i] - node[key] ??= {} - // a-z - if (key >= 0x61 && key <= 0x7a) { - // Uppercase letters preserve references to lowercase ones. - node[key & ~32] ??= node[key] + add (key, value) { + const code = key[0] + if (this.code === code) { + if (key.length === 1) { + this.value = value + } else if (this.middle !== null) { + this.middle.add(key.subarray(1), value) + } else { + this.middle = new Node(key.subarray(1), value) + } + } else if (this.code < code) { + if (this.left !== null) { + this.left.add(key, value) + } else { + this.left = new Node(key, value) + } + } else { + if (this.right !== null) { + this.right.add(key, value) + } else { + this.right = new Node(key, value) } - node = node[key] } - node[256] = value - if (length > this.#depth) { - this.#depth = length + } + + /** + * @param {Uint8Array} key + * @return {Node | null} + */ + search (key) { + const keylength = key.length + if (keylength === 0) { + return null } + let index = 0 + let node = this + while (node !== null && index < keylength) { + let code = key[index] + // A-Z + if (code >= 0x41 && code <= 0x5a) { + // Lowercase for uppercase. + code |= 32 + } + while (node !== null) { + if (code === node.code) { + if (keylength === ++index) { + // Returns Node since it is the last key. + return node + } + node = node.middle + break + } + node = node.code < code ? node.left : node.right + } + } + return null } +} + +class TernarySearchTree { + /** @type {Node | null} */ + node = null /** - * Retrieves values from a node. - * @param {Uint8Array} key Node Key - * @returns {string | null} Value + * @param {Uint8Array} key + * @param {any} value + * */ + insert (key, value) { + if (this.node === null) { + this.node = new Node(key, value) + } else { + this.node.add(key, value) + } + } + + /** + * @param {Uint8Array} key */ lookup (key) { - const length = key.length - if (length > this.#depth) return null - let node = this.#node - for (let i = 0; i < length; ++i) { - if ((node = node?.[key[i]]) === undefined) return null - } - return node?.[256] ?? null + return this.node?.search(key)?.value ?? null } } -const tree = new Tree() +const tree = new TernarySearchTree() for (let i = 0; i < wellknownHeaderNames.length; ++i) { - tree.insert(wellknownHeaderNames[i]) + const key = wellknownHeaderNames[i] + const lowerCasedKey = key.toLowerCase() + tree.insert(Buffer.from(lowerCasedKey), lowerCasedKey) } module.exports = { - Tree, + TernarySearchTree, tree } From e3dc3f199533d95c4c27f25fbba2156b8ff17d55 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:35:02 +0900 Subject: [PATCH 16/30] test: fuzz test --- test/tree.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ test/utils/tree.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 test/tree.js create mode 100644 test/utils/tree.js diff --git a/test/tree.js b/test/tree.js new file mode 100644 index 00000000000..f2630898e7b --- /dev/null +++ b/test/tree.js @@ -0,0 +1,46 @@ +const { Tree } = require('./utils/tree') +const { TernarySearchTree } = require('../lib/core/tree') +const assert = require('assert') +const { test } = require('tap') + +function generateAsciiString (length) { + let result = '' + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + const charactersLength = characters.length + for (let i = 0; i < length; ++i) { + result += characters[Math.floor(Math.random() * charactersLength)] + } + return result +} + +const tree = new Tree() +const tst = new TernarySearchTree() + +/** @type {string[]} */ +const random = new Array(10000) + .fill(0) + .map(() => generateAsciiString((Math.random() * 100 + 5) | 0)) +const randomBuffer = random.map((c) => Buffer.from(c)) + +for (let i = 0; i < random.length; ++i) { + const key = random[i] + const lowerCasedKey = key.toLowerCase() + const buffer = Buffer.from(lowerCasedKey) + tree.insert(buffer, lowerCasedKey) + tst.insert(buffer, lowerCasedKey) +} + +test('all', (t) => { + try { + for (let i = 0; i < randomBuffer.length; ++i) { + const a = tree.lookup(randomBuffer[i]) + const b = tst.lookup(randomBuffer[i]) + assert.equal(a, b) + } + t.pass() + } catch (e) { + t.fail(String(e)) + } + t.end() +}) diff --git a/test/utils/tree.js b/test/utils/tree.js new file mode 100644 index 00000000000..c8694c147aa --- /dev/null +++ b/test/utils/tree.js @@ -0,0 +1,42 @@ +class Tree { + #node = {} + #depth = 0 + + /** + * @param {Uint8Array} key + * @param {any} value + */ + insert (key, value) { + const length = key.length + let node = this.#node + for (let i = 0; i < length; ++i) { + const t = key[i] + node[t] ??= {} + // a-z + if (t >= 0x61 && t <= 0x7a) { + // Uppercase letters preserve references to lowercase ones. + node[t & ~32] ??= node[t] + } + node = node[t] + } + node.value = value + if (length > this.#depth) { + this.#depth = length + } + } + + /** + * @param {Uint8Array} key + * @returns {any} + */ + lookup (key) { + const length = key.length + if (length > this.#depth) return null + let node = this.#node + for (let i = 0; i < length; ++i) { + if ((node = node?.[key[i]]) === undefined) return null + } + return node?.value ?? null + } +} +module.exports = { Tree } From b3030230cd9ae745b2553a84966cd7d05689af76 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:40:23 +0900 Subject: [PATCH 17/30] fix test --- test/tree.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/test/tree.js b/test/tree.js index f2630898e7b..94c895f6e25 100644 --- a/test/tree.js +++ b/test/tree.js @@ -1,6 +1,5 @@ const { Tree } = require('./utils/tree') const { TernarySearchTree } = require('../lib/core/tree') -const assert = require('assert') const { test } = require('tap') function generateAsciiString (length) { @@ -18,7 +17,7 @@ const tree = new Tree() const tst = new TernarySearchTree() /** @type {string[]} */ -const random = new Array(10000) +const random = new Array(1000) .fill(0) .map(() => generateAsciiString((Math.random() * 100 + 5) | 0)) const randomBuffer = random.map((c) => Buffer.from(c)) @@ -32,15 +31,8 @@ for (let i = 0; i < random.length; ++i) { } test('all', (t) => { - try { - for (let i = 0; i < randomBuffer.length; ++i) { - const a = tree.lookup(randomBuffer[i]) - const b = tst.lookup(randomBuffer[i]) - assert.equal(a, b) - } - t.pass() - } catch (e) { - t.fail(String(e)) + for (let i = 0; i < randomBuffer.length; ++i) { + t.equal(tst.lookup(randomBuffer[i]), tree.lookup(randomBuffer[i])) } t.end() }) From 25f2ad0001a3f271b1cd5c6def03995fca9609c4 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:41:20 +0900 Subject: [PATCH 18/30] test --- test/tree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tree.js b/test/tree.js index 94c895f6e25..9f995497c37 100644 --- a/test/tree.js +++ b/test/tree.js @@ -17,7 +17,7 @@ const tree = new Tree() const tst = new TernarySearchTree() /** @type {string[]} */ -const random = new Array(1000) +const random = new Array(5000) .fill(0) .map(() => generateAsciiString((Math.random() * 100 + 5) | 0)) const randomBuffer = random.map((c) => Buffer.from(c)) From ddeb1691429f385b24be6846c02142429f046d0d Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 19:36:07 +0900 Subject: [PATCH 19/30] test: remove tree --- test/tree.js | 5 +---- test/utils/tree.js | 42 ------------------------------------------ 2 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 test/utils/tree.js diff --git a/test/tree.js b/test/tree.js index 9f995497c37..bceea0d7158 100644 --- a/test/tree.js +++ b/test/tree.js @@ -1,4 +1,3 @@ -const { Tree } = require('./utils/tree') const { TernarySearchTree } = require('../lib/core/tree') const { test } = require('tap') @@ -13,7 +12,6 @@ function generateAsciiString (length) { return result } -const tree = new Tree() const tst = new TernarySearchTree() /** @type {string[]} */ @@ -26,13 +24,12 @@ for (let i = 0; i < random.length; ++i) { const key = random[i] const lowerCasedKey = key.toLowerCase() const buffer = Buffer.from(lowerCasedKey) - tree.insert(buffer, lowerCasedKey) tst.insert(buffer, lowerCasedKey) } test('all', (t) => { for (let i = 0; i < randomBuffer.length; ++i) { - t.equal(tst.lookup(randomBuffer[i]), tree.lookup(randomBuffer[i])) + t.equal(tst.lookup(randomBuffer[i]), random[i].toLowerCase()) } t.end() }) diff --git a/test/utils/tree.js b/test/utils/tree.js deleted file mode 100644 index c8694c147aa..00000000000 --- a/test/utils/tree.js +++ /dev/null @@ -1,42 +0,0 @@ -class Tree { - #node = {} - #depth = 0 - - /** - * @param {Uint8Array} key - * @param {any} value - */ - insert (key, value) { - const length = key.length - let node = this.#node - for (let i = 0; i < length; ++i) { - const t = key[i] - node[t] ??= {} - // a-z - if (t >= 0x61 && t <= 0x7a) { - // Uppercase letters preserve references to lowercase ones. - node[t & ~32] ??= node[t] - } - node = node[t] - } - node.value = value - if (length > this.#depth) { - this.#depth = length - } - } - - /** - * @param {Uint8Array} key - * @returns {any} - */ - lookup (key) { - const length = key.length - if (length > this.#depth) return null - let node = this.#node - for (let i = 0; i < length; ++i) { - if ((node = node?.[key[i]]) === undefined) return null - } - return node?.value ?? null - } -} -module.exports = { Tree } From 292db1d118efe61b26cc04df4a49b549224ed8bc Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 20:28:02 +0900 Subject: [PATCH 20/30] refactor --- lib/core/tree.js | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index 081b9a379a9..7ce689cfa5d 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -1,14 +1,14 @@ const { wellknownHeaderNames } = require('./constants') -class Node { +class TstNode { /** @type {any} */ - value - /** @type {null | Node} */ - left - /** @type {null | Node} */ - middle - /** @type {null | Node} */ - right + value = null + /** @type {null | TstNode} */ + left = null + /** @type {null | TstNode} */ + middle = null + /** @type {null | TstNode} */ + right = null /** @type {number} */ code /** @@ -16,17 +16,12 @@ class Node { * @param {any} value */ constructor (key, value) { - const length = key.length - if (length === 0) { + if (key.length === 0) { throw new TypeError('Unreachable') } - this.value = null - this.left = null - this.middle = null - this.right = null this.code = key[0] - if (length > 1) { - this.middle = new Node(key.subarray(1), value) + if (key.length > 1) { + this.middle = new TstNode(key.subarray(1), value) } else { this.value = value } @@ -44,32 +39,29 @@ class Node { } else if (this.middle !== null) { this.middle.add(key.subarray(1), value) } else { - this.middle = new Node(key.subarray(1), value) + this.middle = new TstNode(key.subarray(1), value) } } else if (this.code < code) { if (this.left !== null) { this.left.add(key, value) } else { - this.left = new Node(key, value) + this.left = new TstNode(key, value) } } else { if (this.right !== null) { this.right.add(key, value) } else { - this.right = new Node(key, value) + this.right = new TstNode(key, value) } } } /** * @param {Uint8Array} key - * @return {Node | null} + * @return {TstNode | null} */ search (key) { const keylength = key.length - if (keylength === 0) { - return null - } let index = 0 let node = this while (node !== null && index < keylength) { @@ -96,7 +88,7 @@ class Node { } class TernarySearchTree { - /** @type {Node | null} */ + /** @type {TstNode | null} */ node = null /** @@ -105,7 +97,7 @@ class TernarySearchTree { * */ insert (key, value) { if (this.node === null) { - this.node = new Node(key, value) + this.node = new TstNode(key, value) } else { this.node.add(key, value) } From f5fdfc2b770af017963e29642281272809f8707a Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 7 Dec 2023 20:30:30 +0900 Subject: [PATCH 21/30] refactor --- lib/core/tree.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index 7ce689cfa5d..282a48f2223 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -114,9 +114,8 @@ class TernarySearchTree { const tree = new TernarySearchTree() for (let i = 0; i < wellknownHeaderNames.length; ++i) { - const key = wellknownHeaderNames[i] - const lowerCasedKey = key.toLowerCase() - tree.insert(Buffer.from(lowerCasedKey), lowerCasedKey) + const key = wellknownHeaderNames[i].toLowerCase() + tree.insert(Buffer.from(key), key) } module.exports = { From bd17e9561b85d869ab8feef7933d73cfe32b0a84 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 8 Dec 2023 06:54:43 +0900 Subject: [PATCH 22/30] suggested change --- lib/core/tree.js | 2 ++ test/tree.js | 57 +++++++++++++++++++++++++----------------------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index 282a48f2223..20053df56e0 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -1,3 +1,5 @@ +'use strict' + const { wellknownHeaderNames } = require('./constants') class TstNode { diff --git a/test/tree.js b/test/tree.js index bceea0d7158..0b6516a2cd2 100644 --- a/test/tree.js +++ b/test/tree.js @@ -1,35 +1,38 @@ +'use strict' + const { TernarySearchTree } = require('../lib/core/tree') const { test } = require('tap') -function generateAsciiString (length) { - let result = '' - const characters = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - const charactersLength = characters.length - for (let i = 0; i < length; ++i) { - result += characters[Math.floor(Math.random() * charactersLength)] +test('Ternary Search Tree', (t) => { + t.plan(1) + function generateAsciiString (length) { + let result = '' + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + const charactersLength = characters.length + for (let i = 0; i < length; ++i) { + result += characters[Math.floor(Math.random() * charactersLength)] + } + return result } - return result -} - -const tst = new TernarySearchTree() - -/** @type {string[]} */ -const random = new Array(5000) - .fill(0) - .map(() => generateAsciiString((Math.random() * 100 + 5) | 0)) -const randomBuffer = random.map((c) => Buffer.from(c)) + const tst = new TernarySearchTree() -for (let i = 0; i < random.length; ++i) { - const key = random[i] - const lowerCasedKey = key.toLowerCase() - const buffer = Buffer.from(lowerCasedKey) - tst.insert(buffer, lowerCasedKey) -} + /** @type {string[]} */ + const random = new Array(5000) + .fill(0) + .map(() => generateAsciiString((Math.random() * 100 + 5) | 0)) + const randomBuffer = random.map((c) => Buffer.from(c)) -test('all', (t) => { - for (let i = 0; i < randomBuffer.length; ++i) { - t.equal(tst.lookup(randomBuffer[i]), random[i].toLowerCase()) + for (let i = 0; i < random.length; ++i) { + const key = random[i] + const lowerCasedKey = key.toLowerCase() + const buffer = Buffer.from(lowerCasedKey) + tst.insert(buffer, lowerCasedKey) } - t.end() + t.test('all', (t) => { + for (let i = 0; i < randomBuffer.length; ++i) { + t.equal(tst.lookup(randomBuffer[i]), random[i].toLowerCase()) + } + t.end() + }) }) From 1d4a848adfc7c5efbe19abb8a89c5101b66fd903 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 8 Dec 2023 07:38:35 +0900 Subject: [PATCH 23/30] test: refactor --- test/tree.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/tree.js b/test/tree.js index 0b6516a2cd2..811c665156c 100644 --- a/test/tree.js +++ b/test/tree.js @@ -24,14 +24,12 @@ test('Ternary Search Tree', (t) => { const randomBuffer = random.map((c) => Buffer.from(c)) for (let i = 0; i < random.length; ++i) { - const key = random[i] - const lowerCasedKey = key.toLowerCase() - const buffer = Buffer.from(lowerCasedKey) - tst.insert(buffer, lowerCasedKey) + const key = random[i] = random[i].toLowerCase() + tst.insert(Buffer.from(key), key) } t.test('all', (t) => { for (let i = 0; i < randomBuffer.length; ++i) { - t.equal(tst.lookup(randomBuffer[i]), random[i].toLowerCase()) + t.equal(tst.lookup(randomBuffer[i]), random[i]) } t.end() }) From b2b00fef165de7846956fdde86e5b9f43264863a Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 8 Dec 2023 07:52:53 +0900 Subject: [PATCH 24/30] add use strict --- lib/core/constants.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/core/constants.js b/lib/core/constants.js index 0f827cc4ae0..6ec770dd533 100644 --- a/lib/core/constants.js +++ b/lib/core/constants.js @@ -1,3 +1,5 @@ +'use strict' + /** @type {Record} */ const headerNameLowerCasedRecord = {} From 5bda1827b58abce8538e1ead4ee25afb70a9a999 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 8 Dec 2023 08:26:40 +0900 Subject: [PATCH 25/30] test: refactor --- test/tree.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/tree.js b/test/tree.js index 811c665156c..fb37236ee90 100644 --- a/test/tree.js +++ b/test/tree.js @@ -17,18 +17,21 @@ test('Ternary Search Tree', (t) => { } const tst = new TernarySearchTree() + const LENGTH = 5000 + /** @type {string[]} */ - const random = new Array(5000) - .fill(0) - .map(() => generateAsciiString((Math.random() * 100 + 5) | 0)) - const randomBuffer = random.map((c) => Buffer.from(c)) + const random = new Array(LENGTH) + const randomBuffer = new Array(LENGTH) - for (let i = 0; i < random.length; ++i) { - const key = random[i] = random[i].toLowerCase() - tst.insert(Buffer.from(key), key) + for (let i = 0; i < LENGTH; ++i) { + const key = generateAsciiString((Math.random() * 100 + 5) | 0) + const lowerCasedKey = random[i] = key.toLowerCase() + randomBuffer[i] = Buffer.from(key) + tst.insert(Buffer.from(lowerCasedKey), lowerCasedKey) } + t.test('all', (t) => { - for (let i = 0; i < randomBuffer.length; ++i) { + for (let i = 0; i < LENGTH; ++i) { t.equal(tst.lookup(randomBuffer[i]), random[i]) } t.end() From f7a3b8a03cd0d0b1d3f66d27a4caabbb68bf8ee4 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 8 Dec 2023 08:35:52 +0900 Subject: [PATCH 26/30] add type comment --- test/tree.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tree.js b/test/tree.js index fb37236ee90..f84afa977cf 100644 --- a/test/tree.js +++ b/test/tree.js @@ -21,6 +21,7 @@ test('Ternary Search Tree', (t) => { /** @type {string[]} */ const random = new Array(LENGTH) + /** @type {Buffer[]} */ const randomBuffer = new Array(LENGTH) for (let i = 0; i < LENGTH; ++i) { From 249bc9cbef03c85479389a1ad99e7a5090110374 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 8 Dec 2023 09:01:23 +0900 Subject: [PATCH 27/30] check length --- lib/core/tree.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/core/tree.js b/lib/core/tree.js index 20053df56e0..aa1641d217f 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -34,6 +34,9 @@ class TstNode { * @param {any} value */ add (key, value) { + if (key.length === 0) { + throw new TypeError('Unreachable') + } const code = key[0] if (this.code === code) { if (key.length === 1) { From 7ed17200025202f699399e87ec1ca2002601cda7 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:01:52 +0900 Subject: [PATCH 28/30] test: perf --- test/tree.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/tree.js b/test/tree.js index f84afa977cf..2a2342a1961 100644 --- a/test/tree.js +++ b/test/tree.js @@ -5,11 +5,11 @@ const { test } = require('tap') test('Ternary Search Tree', (t) => { t.plan(1) + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + const charactersLength = characters.length + function generateAsciiString (length) { let result = '' - const characters = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - const charactersLength = characters.length for (let i = 0; i < length; ++i) { result += characters[Math.floor(Math.random() * charactersLength)] } @@ -32,9 +32,9 @@ test('Ternary Search Tree', (t) => { } t.test('all', (t) => { + t.plan(LENGTH) for (let i = 0; i < LENGTH; ++i) { t.equal(tst.lookup(randomBuffer[i]), random[i]) } - t.end() }) }) From 9179784657b9731e2020bb96ed94a1ea697e7559 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:39:21 +0900 Subject: [PATCH 29/30] improve type --- lib/core/util.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index ab9903f2a3d..66ef6fb25e1 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -230,9 +230,9 @@ function headerNameToString (value) { } /** - * @param {Record | (Buffer | string | (Buffer | string)[])[]} headers - * @param {Record} [obj] - * @returns + * @param {Record | (Buffer | string | (Buffer | string)[])[]} headers + * @param {Record} [obj] + * @returns {Record} */ function parseHeaders (headers, obj) { // For H2 support From 6efff14ae8e6501437a0802012b851b88fcd08ea Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:47:00 +0900 Subject: [PATCH 30/30] fix: type --- lib/core/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/util.js b/lib/core/util.js index 66ef6fb25e1..75d31888221 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -230,7 +230,7 @@ function headerNameToString (value) { } /** - * @param {Record | (Buffer | string | (Buffer | string)[])[]} headers + * @param {Record | (Buffer | string | (Buffer | string)[])[]} headers * @param {Record} [obj] * @returns {Record} */