Skip to content

Instantly share code, notes, and snippets.

@Ethan-Arrowood
Created April 5, 2021 22:37
Show Gist options
  • Save Ethan-Arrowood/1c79aee396eb6c2f1e78380b69ae0f0a to your computer and use it in GitHub Desktop.
Save Ethan-Arrowood/1c79aee396eb6c2f1e78380b69ae0f0a to your computer and use it in GitHub Desktop.
Rough perf benchmark for map vs array based header class
'use strict'
const { types } = require('util')
const { validateHeaderName, validateHeaderValue } = require('http')
const kHeaders = Symbol('headers')
function normalizeAndValidateHeaderName (name) {
const normalizedHeaderName = name.toLowerCase()
validateHeaderName(normalizedHeaderName)
return normalizedHeaderName
}
function normalizeAndValidateHeaderValue (name, value) {
const normalizedHeaderName = normalizeAndValidateHeaderName(name)
const normalizedHeaderValue = value.replace(/^[\n\t\r\x20]+|[\n\t\r\x20]+$/g, '')
validateHeaderValue(normalizedHeaderName, normalizedHeaderValue)
return [normalizedHeaderName, normalizedHeaderValue]
}
function fill (headers, object) {
if (Array.isArray(object)) {
if (Array.isArray(object[0])) {
for (let i = 0, header = object[0]; i < object.length; i++, header = object[i]) {
if (header.length !== 2) throw TypeError('header entry must be of length two')
headers.append(header[0], header[1])
}
} else if (typeof object[0] === 'string') {
if (object.length % 2 !== 0) throw TypeError('flattened header init must have even length')
for (let i = 0; i < object.length; i += 2) {
headers.append(object[i], object[i + 1])
}
} else {
throw TypeError('invalid array-based header init')
}
} else if (kHeaders in object) {
headers[kHeaders] = new Array(...object[kHeaders])
} else if (!types.isBoxedPrimitive(object)) {
for (const [name, value] of Object.entries(object)) {
headers.append(name, value)
}
}
}
class Headers {
constructor (init) {
this[kHeaders] = []
if (init && typeof init === 'object') {
fill(this, init)
}
}
append (name, value) {
const [normalizedName, normalizedValue] = normalizeAndValidateHeaderValue(name, value)
let index = this[kHeaders].length
for (let i = 0; i < this[kHeaders].length; i += 2) {
if (normalizedName === this[kHeaders][i]) {
this[kHeaders][i + 1] += `, ${normalizedValue}`
return
} else if (this[kHeaders][i] > normalizedName) {
index = i
break
}
}
this[kHeaders].splice(index, 0, normalizedName, normalizedValue)
}
delete (name) {
const normalizedName = normalizeAndValidateHeaderName(name)
for (let i = 0; i < this[kHeaders].length; i += 2) {
if (normalizedName === this[kHeaders][i]) {
this[kHeaders].splice(i, 2)
break
}
}
}
get (name) {
const normalizedName = normalizeAndValidateHeaderName(name)
for (let i = 0; i < this[kHeaders].length; i += 2) {
if (normalizedName === this[kHeaders][i]) {
return this[kHeaders][i + 1]
}
}
return null
}
has (name) {
const normalizedName = normalizeAndValidateHeaderName(name)
for (let i = 0; i < this[kHeaders].length; i += 2) {
if (normalizedName === this[kHeaders][i]) {
return true
}
}
return false
}
set (name, value) {
const [normalizedName, normalizedValue] = normalizeAndValidateHeaderValue(name, value)
let index = this[kHeaders].length
for (let i = 0; i < this[kHeaders].length; i += 2) {
if (normalizedName === this[kHeaders][i] || this[kHeaders][i] > normalizedName) {
index = i
break
}
}
this[kHeaders].splice(index, 0, normalizedName, normalizedValue)
}
* keys () {
for (const header of this) {
yield header[0]
}
}
* values () {
for (const header of this) {
yield header[1]
}
}
* entries () {
yield * this
}
forEach (callback, thisArg) {
for (let i = 0; i < this[kHeaders].length; i += 2) {
callback.call(thisArg, this[kHeaders][i + 1], this[kHeaders][i], this)
}
}
* [Symbol.iterator] () {
for (let i = 0; i < this[kHeaders].length; i += 2) {
yield [this[kHeaders][i], this[kHeaders][i + 1]]
}
}
}
module.exports = Headers
'use strict'
const { types } = require('util')
const { validateHeaderName, validateHeaderValue } = require('http')
const kHeaders = Symbol('headers')
function normalizeAndValidateHeaderName (name) {
const normalizedHeaderName = name.toLowerCase()
validateHeaderName(normalizedHeaderName)
return normalizedHeaderName
}
function normalizeAndValidateHeaderValue (name, value) {
const normalizedHeaderName = normalizeAndValidateHeaderName(name)
const normalizedHeaderValue = value.replace(/^[\n\t\r\x20]+|[\n\t\r\x20]+$/g, '')
validateHeaderValue(normalizedHeaderName, normalizedHeaderValue)
return [normalizedHeaderName, normalizedHeaderValue]
}
function fill (headers, object) {
if (Array.isArray(object)) {
for (let i = 0, header = object[0]; i < object.length; i++, header = object[i]) {
if (header.length !== 2) throw TypeError('header entry must be of length two')
headers.append(header[0], header[1])
}
} else if (kHeaders in object) {
headers[kHeaders] = new Map(object[kHeaders])
} else if (!types.isBoxedPrimitive(object)) {
for (const [name, value] of Object.entries(object)) {
headers.append(name, value)
}
}
}
class Headers {
constructor (init) {
this[kHeaders] = new Map()
if (init && typeof init === 'object') {
fill(this, init)
}
}
append (name, value) {
const [normalizedHeaderName, normalizedHeaderValue] = normalizeAndValidateHeaderValue(name, value)
const existing = this[kHeaders].get(normalizedHeaderName)
const newHeaderValue = existing ? existing + ', ' + normalizedHeaderValue : normalizedHeaderValue
this[kHeaders].set(normalizedHeaderName, newHeaderValue)
}
delete (name) {
const normalizedHeaderName = normalizeAndValidateHeaderName(name)
this[kHeaders].delete(normalizedHeaderName)
}
get (name) {
const normalizedHeaderName = normalizeAndValidateHeaderName(name)
const value = this[kHeaders].get(normalizedHeaderName)
return value === undefined ? null : value
}
has (name) {
const normalizedHeaderName = normalizeAndValidateHeaderName(name)
return this[kHeaders].has(normalizedHeaderName)
}
set (name, value) {
const [normalizedHeaderName, normalizedHeaderValue] = normalizeAndValidateHeaderValue(name, value)
this[kHeaders].set(normalizedHeaderName, normalizedHeaderValue)
}
* keys () {
yield * Array.from(this[kHeaders].keys()).sort()
}
* values () {
for (const header of this.entries()) {
yield header[1]
}
}
* entries () {
yield * Array.from(this[kHeaders].entries()).sort()
}
forEach (callback, thisArg) {
for (const [key, value] of this) {
callback.call(thisArg, value, key, this)
}
}
[Symbol.iterator] () {
return this.entries()
}
}
module.exports = Headers
'use strict'
const { performance } = require('perf_hooks')
const MapHeaders = require('./map')
const ArrayHeaders = require('./array')
const alphabet = 'abcdefghijklmnopqrstuvwxyz'
const _alphabet = alphabet.split('')
{
console.log('write performance')
function write(headers) {
const s = performance.now()
for (const letter of _alphabet) {
headers.append(letter, letter)
}
const e = performance.now()
return e-s
}
console.log(`Map: ${write(new MapHeaders())}ms`)
console.log(`Array: ${write(new ArrayHeaders())}ms`)
}
{
console.log('read performance')
function read(headers) {
for (const letter of _alphabet) {
headers.append(letter, letter)
}
const s = performance.now()
let i = 0
for (const header of headers) {
i++
}
const e = performance.now()
return e-s
}
console.log(`Map: ${read(new MapHeaders())}ms`)
console.log(`Array: ${read(new ArrayHeaders())}ms`)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment