Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(util/parseHeaders): If the header name is buffer #2501

Merged
merged 34 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a8728c4
initial implementation
tsctx Dec 6, 2023
8abb9c9
test: fix
tsctx Dec 6, 2023
8324d21
compatible API
tsctx Dec 6, 2023
0db0b0f
Merge remote-tracking branch 'origin' into perf/improve-parseheaders-…
tsctx Dec 6, 2023
b9fc805
fix: tree
tsctx Dec 6, 2023
42b0a46
add benchmark
tsctx Dec 6, 2023
559e6db
fix: lint
tsctx Dec 6, 2023
007e7de
fix: benchmark
tsctx Dec 6, 2023
82c198d
perf
tsctx Dec 6, 2023
061cd39
use number key
tsctx Dec 6, 2023
7c5f84e
remove unsafe
tsctx Dec 6, 2023
a6a37a7
format & add comment
tsctx Dec 6, 2023
07efe2e
fix: benchmark import path
tsctx Dec 6, 2023
f7dbb98
better benchmark
tsctx Dec 7, 2023
0e7cba8
better benchmark
tsctx Dec 7, 2023
41c0e46
Merge remote-tracking branch 'origin' into perf/improve-parseheaders-…
tsctx Dec 7, 2023
feb97c2
perf: rewrite tree
tsctx Dec 7, 2023
e3dc3f1
test: fuzz test
tsctx Dec 7, 2023
b303023
fix test
tsctx Dec 7, 2023
25f2ad0
test
tsctx Dec 7, 2023
ddeb169
test: remove tree
tsctx Dec 7, 2023
26817f4
Merge remote-tracking branch 'origin' into perf/improve-parseheaders-…
tsctx Dec 7, 2023
292db1d
refactor
tsctx Dec 7, 2023
f5fdfc2
refactor
tsctx Dec 7, 2023
bd17e95
suggested change
tsctx Dec 7, 2023
1d4a848
test: refactor
tsctx Dec 7, 2023
b2b00fe
add use strict
tsctx Dec 7, 2023
5bda182
test: refactor
tsctx Dec 7, 2023
f7a3b8a
add type comment
tsctx Dec 7, 2023
249bc9c
check length
tsctx Dec 8, 2023
7ed1720
test: perf
tsctx Dec 8, 2023
9179784
improve type
tsctx Dec 8, 2023
e68b73d
Merge remote-tracking branch 'origin' into perf/improve-parseheaders-…
tsctx Dec 8, 2023
6efff14
fix: type
tsctx Dec 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions benchmarks/parseHeaders.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { bench, group, run } from 'mitata'
import { parseHeaders } from '../lib/core/util.js'

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 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', () => {})
bench('noop', () => {})
bench('noop', () => {})
bench('noop', () => {})
bench('noop', () => {})
bench('noop', () => {})

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))

await run()
2 changes: 2 additions & 0 deletions lib/core/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use strict'

/** @type {Record<string, string | undefined>} */
const headerNameLowerCasedRecord = {}

Expand Down
129 changes: 129 additions & 0 deletions lib/core/tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
'use strict'

const { wellknownHeaderNames } = require('./constants')
tsctx marked this conversation as resolved.
Show resolved Hide resolved

class TstNode {
/** @type {any} */
value = null
/** @type {null | TstNode} */
left = null
/** @type {null | TstNode} */
middle = null
/** @type {null | TstNode} */
right = null
/** @type {number} */
code
/**
* @param {Uint8Array} key
* @param {any} value
*/
constructor (key, value) {
if (key.length === 0) {
throw new TypeError('Unreachable')
}
this.code = key[0]
if (key.length > 1) {
this.middle = new TstNode(key.subarray(1), value)
} else {
this.value = value
}
}

/**
* @param {Uint8Array} key
* @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) {
this.value = value
} else if (this.middle !== null) {
this.middle.add(key.subarray(1), value)
} else {
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 TstNode(key, value)
}
} else {
if (this.right !== null) {
this.right.add(key, value)
} else {
this.right = new TstNode(key, value)
}
}
}

/**
* @param {Uint8Array} key
* @return {TstNode | null}
*/
search (key) {
const keylength = key.length
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 {TstNode | null} */
node = null

/**
* @param {Uint8Array} key
* @param {any} value
* */
insert (key, value) {
if (this.node === null) {
this.node = new TstNode(key, value)
} else {
this.node.add(key, value)
}
}

/**
* @param {Uint8Array} key
*/
lookup (key) {
return this.node?.search(key)?.value ?? null
}
}

const tree = new TernarySearchTree()

for (let i = 0; i < wellknownHeaderNames.length; ++i) {
const key = wellknownHeaderNames[i].toLowerCase()
tree.insert(Buffer.from(key), key)
}

module.exports = {
TernarySearchTree,
tree
}
32 changes: 24 additions & 8 deletions lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -219,26 +220,40 @@ function parseKeepAliveTimeout (val) {
return m ? parseInt(m[1], 10) * 1000 : null
}

function parseHeaders (headers, obj = {}) {
/**
* @param {string | Buffer} value
*/
function headerNameToString (value) {
return typeof value === 'string'
? headerNameLowerCasedRecord[value] ?? value.toLowerCase()
: tree.lookup(value) ?? value.toString().toLowerCase()
}

/**
* @param {Record<string, string | string[]> | (Buffer | string | (Buffer | string)[])[]} headers
* @param {Record<string, string | string[]>} [obj]
* @returns {Record<string, string | string[]>}
*/
function parseHeaders (headers, obj) {
// For H2 support
if (!Array.isArray(headers)) return headers

if (obj === undefined) obj = {}
for (let i = 0; i < headers.length; i += 2) {
const key = headers[i].toString()
const lowerCasedKey = headerNameLowerCasedRecord[key] ?? key.toLowerCase()
let val = obj[lowerCasedKey]
const key = headerNameToString(headers[i])
let val = obj[key]

if (!val) {
const headersValue = headers[i + 1]
if (typeof headersValue === 'string') {
obj[lowerCasedKey] = headersValue
obj[key] = headersValue
} else {
obj[lowerCasedKey] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8')
obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8')
}
} else {
if (!Array.isArray(val)) {
if (typeof val === 'string') {
val = [val]
obj[lowerCasedKey] = val
obj[key] = val
}
val.push(headers[i + 1].toString('utf8'))
}
Expand Down Expand Up @@ -461,6 +476,7 @@ module.exports = {
isIterable,
isAsyncIterable,
isDestroyed,
headerNameToString,
parseRawHeaders,
parseHeaders,
parseKeepAliveTimeout,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions test/tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict'

const { TernarySearchTree } = require('../lib/core/tree')
tsctx marked this conversation as resolved.
Show resolved Hide resolved
const { test } = require('tap')

test('Ternary Search Tree', (t) => {
t.plan(1)
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length

function generateAsciiString (length) {
let result = ''
for (let i = 0; i < length; ++i) {
result += characters[Math.floor(Math.random() * charactersLength)]
}
return result
}
const tst = new TernarySearchTree()

const LENGTH = 5000

/** @type {string[]} */
const random = new Array(LENGTH)
/** @type {Buffer[]} */
const randomBuffer = new Array(LENGTH)

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) => {
t.plan(LENGTH)
for (let i = 0; i < LENGTH; ++i) {
t.equal(tst.lookup(randomBuffer[i]), random[i])
}
})
})
5 changes: 2 additions & 3 deletions test/util.js
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -125,5 +124,5 @@ test('buildURL', (t) => {

test('headerNameLowerCasedRecord', (t) => {
t.plan(1)
t.ok(typeof headerNameLowerCasedRecord.hasOwnProperty === 'undefined')
t.ok(typeof headerNameLowerCasedRecord.hasOwnProperty !== 'function')
})
Loading