benchmark nonsense

This commit is contained in:
Jonatan Nilsson 2024-11-02 12:44:54 +00:00
parent 8db66f416a
commit 2e1dadbdea
22 changed files with 2074 additions and 1324 deletions

View file

@ -0,0 +1,116 @@
import { printTree } from "./utils.mjs"
/**
* Child {
* char: 'a',
* children: [Child, Child],
* path: null / 'full/path',
* count: 0
* }
*/
function Child(char) {
this.char = char
this.children = []
this.path = null
this.count = 0
}
function buildChild(i, arr) {
let letter = new Child(arr[0][i])
let consume = []
if (arr[0].length === i + 1) {
letter.path = arr[0]
letter.count += 1
} else {
consume = [arr[0]]
}
for (let y = 1; y < arr.length; y++) {
if (arr[y][i] !== letter.char) break
consume.push(arr[y])
}
letter.count += consume.length
while (consume.length) {
letter.children.push(buildChild(i + 1, consume))
consume.splice(0, letter.children[letter.children.length - 1].count)
}
return letter
}
export function buildTree(all) {
let paths = Array.from(new Set(all)).sort()
let builder = []
while (paths.length) {
builder.push(buildChild(0, paths))
paths.splice(0, builder[builder.length - 1].count)
}
printTree(builder)
return builder
}
function IfTreeBranch(branches, indent = 0) {
let output = ''
let indentation = ''.padStart(indent * 2)
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
output += `${i > 0 ? 'else ': ''}if (str.charCodeAt(${indent}) === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (str.length === ${branch.path.length}) {`
output += '\n' + indentation + ` return "${branch.path}"`
output += '\n' + indentation + ` }`
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += IfTreeBranch(branch.children, indent + 1)
}
output += '\n' + indentation + '} '
}
return output
}
export function compileTreeIntoIfs(tree) {
let output = IfTreeBranch(tree)
output += '\nreturn null'
return new Function('str', output)
}
function IfTreeBranchBuffer(branches, indent = 0) {
let output = ''
let indentation = ''.padStart(indent * 2)
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
output += `${i > 0 ? 'else ': ''}if (uint[${indent}] === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (uint.length === ${branch.path.length}) {`
output += '\n' + indentation + ` return "${branch.path}"`
output += '\n' + indentation + ` }`
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += IfTreeBranchBuffer(branch.children, indent + 1)
}
output += '\n' + indentation + '} '
}
return output
}
export function compileTreeIntoIfsWithBuffer(tree) {
let output = `var uint = Buffer.from(str)\n`
output += IfTreeBranchBuffer(tree)
output += '\nreturn null'
return new Function('str', output)
}

View file

@ -0,0 +1,39 @@
export function printTree(children, indent = 0) {
if (!children.length) return
for (let child of children) {
console.log(child.char.padStart(1 + indent * 2))
printTree(child.children, indent + 1)
}
}
export function printIfNotEightyOne(fn) {
let opt = %GetOptimizationStatus(fn)
if (opt === 81) return
printCurrentStatus()
}
export function printCurrentStatus(fn) {
let opt = %GetOptimizationStatus(fn)
console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt})`)
}
export function printStatusHelperText() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned
lite mode
marked for deoptimization
baseline
topmost frame is interpreted
topmost frame is baseline`)
}

View file

@ -6,6 +6,15 @@ export const overrideDummy = function(override) {
export const dummy = function() { callItem() }
export const testRoutes = [
// '/api/articles/:id/file/:fileId',
'/',
// '/api/articles',
// '/api/articles/:id/file',
// '/api/articles/:id',
'/::rest',
]
export const allRoutes = [
'/',
'/api/articles',
@ -23,6 +32,22 @@ export const allRoutes = [
'/api/pages/:id/articles/public',
'/api/staff',
'/api/staff/:id',
'/::rest',
]
export const allRoutesGoldenBalance = [
'/',
'/a/:a/aaaaaaaaaaaaaaaaaaaaaaaaaaaaa/:aa',
'/b/:b/bbbbbbbbbbbbbbbbbbbbbbbbbbbbb/:bb',
'/c/:c/ccccccccccccccccccccccccccccc/:cc',
'/d/:d/ddddddddddddddddddddddddddddd/:dd',
'/e/:e/eeeeeeeeeeeeeeeeeeeeeeeeeeeee/:ee',
'/f/:f/fffffffffffffffffffffffffffff/:ff',
'/g/:g/ggggggggggggggggggggggggggggg/:gg',
'/h/:h/hhhhhhhhhhhhhhhhhhhhhhhhhhhhh/:hh',
'/i/:i/iiiiiiiiiiiiiiiiiiiiiiiiiiiii/:ii',
'/j/:j/jjjjjjjjjjjjjjjjjjjjjjjjjjjjj/:jj',
'/::rest',
]
export const allManyRoutes = [
@ -37,7 +62,7 @@ export const allManyRoutes = [
'/api/categories/:categoryId/properties',
'/api/categories/:categoryId/values/:props',
'/api/categories/:categoryId',
'/api/categories/:categoryId/products/:productId',
//'/api/categories/:categoryId/products/:productId',
'/api/categories/:categoryId/products/:productId',
'/api/customers',
'/api/customers/:id',
@ -61,7 +86,7 @@ export const allManyRoutes = [
'/api/products/:id',
'/api/products/:id/movement',
'/api/products/:id/sub_products/:productId',
'/api/products/:id/sub_products/:productId',
//'/api/products/:id/sub_products/:productId',
'/api/products/code/:code',
'/api/products/property/:propertyId',
'/api/properties',
@ -84,4 +109,5 @@ export const allManyRoutes = [
'/api/works/public',
'/api/staff',
'/api/staff/:id',
'/::rest',
]

148
benchmark/ifs.mjs Normal file
View file

@ -0,0 +1,148 @@
import { summary, run, bench } from 'mitata';
function printCurrentStatus(fn) {
let opt = %GetOptimizationStatus(fn)
console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt}) ${fn.name}`)
}
function printStatusHelperText() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned
lite mode
marked for deoptimization
baseline
topmost frame is interpreted
topmost frame is baseline`)
}
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
function ifSingle(a) {
if (a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) {
return 100
}
return 10
}
function ifChain(a) {
if (a[0] === 97)
if (a[1] === 97)
if (a[2] === 97)
if (a[3] === 97)
if (a.length === 4) {
return 100
}
return 10
}
function ifSingleOpt(a) {
if (a.length >= 4 && a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) {
return 100
}
return 10
}
function ifChainOpt(a) {
if (a.length >= 4)
if (a[0] === 97)
if (a[1] === 97)
if (a[2] === 97)
if (a[3] === 97)
if (a.length === 4) {
return 100
}
return 10
}
function ifSingleOptAlt(a) {
if (a.length >= 4 && a[0] == 97 && a[1] == 97 && a[2] == 97 && a[3] == 97 && a.length == 4) {
return 100
}
return 10
}
function ifChainOptAlt(a) {
if (a.length >= 4)
if (a[0] == 97)
if (a[1] == 97)
if (a[2] == 97)
if (a[3] == 97)
if (a.length == 4) {
return 100
}
return 10
}
let paths = [
[97, 97, 97, 97],
[97, 97, 97, 97, 97],
[97, 97, 96, 97],
[97, 96, 97, 97],
[96, 97, 97, 97],
[],
[98],
[97, 97, 97],
[97, 97],
[97],
[97, 97, 96],
[97, 96],
[96],
]
let paths2 = [
[97, 97, 97, 97],
[97, 97, 97, 97, 97],
[97, 97, 96, 97],
[97, 96, 97, 97],
[96, 97, 97, 97],
[97, 97, 97, 97],
[97, 97, 97, 97, 97],
[97, 97, 96, 97],
[97, 96, 97, 97],
[96, 97, 97, 97],
[97, 97, 97, 97, 97, 97],
[97, 97, 97, 97, 96, 97],
[97, 97, 97, 97, 97, 96],
]
let func1 = [ifSingle, ifChain, ifSingleOpt, ifChainOpt, ifSingleOptAlt, ifChainOptAlt];
for (let fun of func1) {
console.log('-- begin', fun.name)
for (var i = 0; i < 1000000; i++) {
paths.map(fun)
}
printCurrentStatus(fun);
}
printStatusHelperText()
summary(() => {
func1.forEach(function(fun) {
bench(fun.name + ' first', function() {
return paths.map(fun)
})
})
func1.forEach(function(fun) {
bench(fun.name + ' second', function() {
return paths2.map(fun)
})
})
})
run().then(function() {
for (let fun of func1) {
printCurrentStatus(fun);
}
printStatusHelperText()
});

85
benchmark/ifs_compile.mjs Normal file
View file

@ -0,0 +1,85 @@
import { printCurrentStatus, printStatusHelperText } from "./compiler/utils.mjs";
function ifSingle(a) {
if (a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) {
return 100
}
return 10
}
function ifChain(a) {
if (a[0] === 97)
if (a[1] === 97)
if (a[2] === 97)
if (a[3] === 97)
if (a.length === 4) {
return 100
}
return 10
}
function ifSingleOptimized(a) {
if (a.length >= 4 && a[0] === 97 && a[1] === 97 && a[2] === 97 && a[3] === 97 && a.length === 4) {
return 100
}
return 10
}
function ifChainOptimized(a) {
if (a.length >= 4)
if (a[0] === 97)
if (a[1] === 97)
if (a[2] === 97)
if (a[3] === 97)
if (a.length === 4) {
return 100
}
return 10
}
function ifSingleOptimizedAlt(a) {
if (a.length >= 4 && a[0] == 97 && a[1] == 97 && a[2] == 97 && a[3] == 97 && a.length == 4) {
return 100
}
return 10
}
function ifChainOptimizedAlt(a) {
if (a.length >= 4)
if (a[0] == 97)
if (a[1] == 97)
if (a[2] == 97)
if (a[3] == 97)
if (a.length == 4) {
return 100
}
return 10
}
let paths = [
[97, 97, 97, 97],
[97, 97, 97, 97, 97],
[97, 97, 96, 97],
[97, 96, 97, 97],
[96, 97, 97, 97],
[],
[98],
[97, 97, 97],
[97, 97],
[97],
[97, 97, 96],
[97, 96],
[96],
]
let func1 = [ifSingle, ifChain, ifSingleOptimized, ifChainOptimized, ifSingleOptimizedAlt, ifChainOptimizedAlt];
for (let fun of func1) {
console.log('-- begin', fun.name)
for (var i = 0; i < 1000000; i++) {
paths.map(x => fun(x))
}
printCurrentStatus(fun);
}
printStatusHelperText()

106
benchmark/map_query.mjs Normal file
View file

@ -0,0 +1,106 @@
import { summary, run, bench } from 'mitata';
function printCurrentStatus(fn) {
let opt = %GetOptimizationStatus(fn)
console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt}) ${fn.name}`)
}
function printStatusHelperText() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned
lite mode
marked for deoptimization
baseline
topmost frame is interpreted
topmost frame is baseline`)
}
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
function mapGetAndCheck(map, t) {
let x = map.get(t)
if (x) return x
return t
}
function mapCheckBeforeGet(map, t) {
if (map.has(t)) {
return map.get(t)
}
return t
}
let paths = [
'a',
'aa',
'aaa',
'aaaa',
'aaaaa',
'aaaaaa',
'aaaaaaa',
'aaaaaaaa',
]
let tests = [
'a',
'aa',
'aaa',
'aaaa',
'aaaaa',
'aaaaaa',
'aaaaaaa',
'aaaaaaaa',
'b',
'bb',
'bbb',
'bbbb',
'bbbbb',
'bbbbbb',
'bbbbbbb',
'bbbbbbbb',
'c',
'cc',
'ccc',
'cccc',
'ccccc',
'cccccc',
'ccccccc',
'cccccccc',
]
let map = new Map(paths.map(x => [x, { a: x }]))
let func1 = [mapGetAndCheck, mapCheckBeforeGet];
for (let fun of func1) {
console.log('-- begin', fun.name)
for (var i = 0; i < 1000000; i++) {
tests.map(t => fun(map, t))
}
printCurrentStatus(fun);
}
printStatusHelperText()
summary(() => {
func1.forEach(function(fun) {
bench(fun.name, function() {
return paths.map(t => fun(map, t))
})
})
})
run().then(function() {
for (let fun of func1) {
printCurrentStatus(fun);
}
printStatusHelperText()
});

View file

@ -0,0 +1,25 @@
import { getExternalKeys } from '@mapl/compiler';
import { compileRouter as compileRouterContent } from '@mapl/router';
const PATH = "__req_p";
const REQ = '__req';
const PARAMS = `${REQ}_ps`;
export function compileRouter(root) {
const state = {
contentBuilder: [],
declarationBuilders: [],
externalValues: [],
compileItem: (item, state, hasParam) => {
state.contentBuilder.push(`return [f${state.externalValues.push(item)},${hasParam ? PARAMS : '[]'}];`);
}
};
compileRouterContent(root, state);
// eslint-disable-next-line
return Function(
...getExternalKeys(state),
`return (${PATH})=>{${state.contentBuilder.join('')}return null;}`
)(...state.externalValues);
}

View file

@ -0,0 +1,31 @@
import { createRouter, insertItem } from '@mapl/router'
import { compileRouter } from './mapl_compiler.mjs'
import { printTime } from './utils.mjs'
import * as consts from './const.js'
let testPaths = consts.allManyRoutes // consts.allManyRoutes
/*testPaths = [
'/api/works/:id/lock',
'/api/staff/:id',
// '/::rest',
]*/
testPaths = testPaths.map(x => x.replace(/::[^\/]+/g, '**').replace(/:[^\/]+/g, '*'))
const r = createRouter();
for (let route of testPaths) {
let cleaned = route.replace(/:[^\/]+/g, '*')
insertItem(r, cleaned, () => { })
}
let s1 = process.hrtime.bigint()
let s2 = process.hrtime.bigint()
compileRouter(r);
let s3 = process.hrtime.bigint()
let time = s3 - s2 - (s2 - s1)
printTime(time)

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,10 @@
"author": "",
"license": "WTFPL",
"dependencies": {
"@mapl/router": "^0.0.16",
"benchmarkjs-pretty": "^2.0.0",
"express": "^4.17.1",
"koa-router": "^8.0.8"
"koa-router": "^8.0.8",
"mitata": "^1.0.10"
}
}

221
benchmark/promise.mjs Normal file
View file

@ -0,0 +1,221 @@
import { summary, run, bench } from 'mitata';
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
// Example benchmark
/*summary(() => {
const fn = () => null
const fnAlt = function() {}
function fnBasic() {}
const fnAltWithReturn = function() { return null }
function fnBasicWithReturn() { return null }
bench('Reject with uncached error handler', async () => await Promise.reject().catch(() => null));
bench('Reject with cached error handler', async () => await Promise.reject().catch(fn));
bench('TT Reject with uncached error handler', () => Promise.reject().catch(() => null));
bench('TT Reject with cached error handler', () => Promise.reject().catch(fnAlt));
bench('TT Reject with cached basic error handler', () => Promise.reject().catch(fnBasic));
bench('TT Reject with cached error handler with return', () => Promise.reject().catch(fnAltWithReturn));
bench('TT Reject with cached basic error handler with return ', () => Promise.reject().catch(fnBasicWithReturn));
});*/
/*
summary(() => {
function triple () {
const bla = Math.round(Math.random()) ? true : null
return bla === null
}
function doubleNull () {
const bla = Math.round(Math.random()) ? true : null
return bla == null
}
function doubleUndefined () {
const bla = Math.round(Math.random()) ? true : undefined
return bla == null
}
bench('null === null', triple);
bench('null == null', doubleNull);
bench('undefined == null', doubleUndefined);
bench('2x null === null', triple);
bench('2x null == null', doubleNull);
bench('2x undefined == null', doubleUndefined);
})
summary(() => {
function triple () {
const bla = false ? true : null
return bla === null
}
function doubleNull () {
const bla = false ? true : null
return bla == null
}
function doubleUndefined () {
const bla = false ? true : undefined
return bla == null
}
bench('const null === null', triple);
bench('const null == null', doubleNull);
bench('const undefined == null', doubleUndefined);
bench('const 2x null === null', triple);
bench('const 2x null == null', doubleNull);
bench('const 2x undefined == null', doubleUndefined);
})*/
function printCurrentStatus(fn) {
let opt = %GetOptimizationStatus(fn)
console.log(`${fn.toString()}
${opt.toString(2).padStart(12, '0').split('').join(' ')}`)
}
function printTree() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned`)
}
let func1
let func2
let func3
await (async function () {
return
const fn1 = async () => await Promise.reject().catch(() => null) === null ? 1 : 0;
const fn2 = async () => await Promise.reject().catch(() => {}) == null ? 1 : 0;
const fn3 = async () => await Promise.reject().catch(() => null) == null ? 1 : 0;
const noop = () => null;
const fn4 = async () => await Promise.reject().catch(noop) == null ? 1 : 0;
const fn5 = async () => await Promise.reject().catch(noop) === null ? 1 : 0;
const fn6 = () => Promise.reject().catch(() => null).then(x => x === null ? 1 : 0);
const fn7 = () => Promise.reject().catch(() => {}).then(x => x == null ? 1 : 0);
const fn8 = () => Promise.reject().catch(() => null).then(x => x == null ? 1 : 0);
const fn9 = () => Promise.reject().catch(noop).then(x => x == null ? 1 : 0);
const fn10 = () => Promise.reject().catch(noop).then(x => x === null ? 1 : 0);
func1 = [fn1, fn2, fn3, fn4, fn5, fn6, fn7, fn8, fn9, fn10];
for (let fun of func1) {
%OptimizeFunctionOnNextCall(fun);
}
for (var i = 0; i < 100000; i++) {
for (let fun of func1) {
await fun()
}
}
for (let fun of func1) {
printCurrentStatus(fun);
}
printTree()
summary(() => {
bench('Null with ===', fn1);
bench('Undefined with ==', fn2);
bench('Null with ==', fn3);
bench('Cached Null with ==', fn4);
bench('Cached Null with ===', fn5);
bench('[sync] Null with ===', fn6);
bench('[sync] Undefined with ==', fn7);
bench('[sync] Null with ==', fn8);
bench('[sync] Cached Null with ==', fn9);
bench('[sync] Cached Null with ===', fn10);
});
})
await (async function () {
const alt1 = function () { (Math.round(Math.random()) ? null : 1) ?? 0 };
const alt2 = function () { (Math.round(Math.random()) ? null : 1) || 0 };
const alt3 = function () { (Math.round(Math.random()) ? undefined : 1) ?? 0 };
const alt4 = function () { (Math.round(Math.random()) ? undefined : 1) || 0 };
let check = new Array(100).fill(0).map(() => Math.round(Math.random()) ? null : 1)
let check2 = new Array(100).fill(0).map(() => Math.round(Math.random()) ? undefined : 1)
const alt5 = function () { let out = 0; for (let x of check) { out += x ?? 0 } return out };
const alt6 = function () { let out = 0; for (let x of check) { out += x || 0 } return out };
const alt7 = function () { let out = 0; for (let x of check2) { out += x ?? 0 } return out };
const alt8 = function () { let out = 0; for (let x of check2) { out += x || 0 } return out };
func2 = [alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8];
for (let fun of func2) {
%OptimizeFunctionOnNextCall(fun);
}
for (var i = 0; i < 100000; i++) {
for (let fun of func2) {
fun()
}
}
for (let fun of func2) {
printCurrentStatus(fun);
}
printTree()
summary(() => {
bench('null/1 ?? 0', alt1);
bench('null/1 || 0', alt2);
bench('undefined/1 ?? 0', alt3);
bench('undefined/1 || 0', alt4);
});
summary(() => {
bench('arr null/1 ?? null', alt5);
bench('arr null/1 || null', alt6);
bench('arr und/1 ?? null', alt7);
bench('arr und/1 || null', alt8);
});
})()
await (async function () {
const alt1 = function () { let out = 0; for (let x = 0; x < 100; x++) { out += (Math.round(Math.random()) ? null : 1) ?? 0 } return out };
const alt2 = function () { let out = 0; for (let x = 0; x < 100; x++) { out += (Math.round(Math.random()) ? null : 1) || 0 } return out };
const alt3 = function () { let out = 0; for (let x = 0; x < 100; x++) { out += (Math.round(Math.random()) ? undefined : 1) ?? 0 } return out };
const alt4 = function () { let out = 0; for (let x = 0; x < 100; x++) { out += (Math.round(Math.random()) ? undefined : 1) || 0 } return out };
func3 = [alt1, alt2, alt3, alt4];
for (let fun of func3) {
%OptimizeFunctionOnNextCall(fun);
}
for (var i = 0; i < 100000; i++) {
for (let fun of func3) {
fun()
}
}
for (let fun of func3) {
printCurrentStatus(fun);
}
printTree()
summary(() => {
bench('loop null/1 ?? 0', alt1);
bench('loop null/1 || 0', alt2);
bench('loop und/1 ?? 0', alt3);
bench('loop und/1 || 0', alt4);
});
})()
// Start the benchmark
run().then(() => {
for (let fun of func3) {
printCurrentStatus(fun);
}
printTree()
});

View file

@ -0,0 +1,88 @@
import { summary, run, bench } from 'mitata';
import { createRouter, insertItem } from '@mapl/router'
import { compileRouter } from './mapl_compiler.mjs'
import assert from 'assert'
import { compilePaths } from "./router_v2.mjs"
import { compilePaths as mainCompiler, compilePathsClosure } from "../router_v2.mjs"
import * as consts from './const.js'
function printCurrentStatus(fn) {
// console.log(`--- checking optimizations status on ${fn.name} ---`)
// let opt = %GetOptimizationStatus(fn)
// console.log(`${opt.toString(2).padStart(17, '0').split('').join(' ')} (${opt}) ${fn.name}`)
}
function printStatusHelperText() {
console.log(`┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
is function
is never optimized
is always optimized
is maybe deoptimized
is optimized
is optimized by TurboFan
is interpreted
is marked for optimization
is marked for concurrent optimization
is optimizing concurrently
is executing
topmost frame is turbo fanned
lite mode
marked for deoptimization
baseline
topmost frame is interpreted
topmost frame is baseline`)
}
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
let paths = consts.allManyRoutes.map(x => ({ path: x }))
let tests = paths.map(p => ([p.path.replace(/:[^/]+/g, '_'), p]))
let testStrings = tests.map(x => x[0])
let testStringsMapl = testStrings.map(x => x.slice(1))
let func = [[testStrings, ...mainCompiler(paths)]]
func.push([testStrings, ...compilePathsClosure(paths)])
let maplPaths = paths.map(x => x.path.replace(/::[^\/]+/g, '**').replace(/:[^\/]+/g, '*'))
const maplRouter = createRouter();
for (let route of maplPaths) {
insertItem(maplRouter, route, { path: route })
}
let maplMatcher = compileRouter(maplRouter)
func.push([testStringsMapl, maplMatcher, maplMatcher])
for (let [tests, _, fun] of func) {
console.log(`--- warming up ${fun.name || 'mapl'} ---`)
for (var i = 0; i < 10000; i++) {
tests.forEach(fun)
}
}
for (let [tests, _, fun] of func) {
console.log(`--- Sanity checking ${fun.name || 'mapl'} ---`)
for (let a of tests) {
// console.log(a, fun(a))
}
}
console.log('--- sleeping ---')
await new Promise(res => setTimeout(res, 1000))
for (let [_, org] of func) {
printCurrentStatus(org);
}
printStatusHelperText()
summary(() => {
func.forEach(function([tests, _, fun]) {
// console.log(tests, fun, tests.map(fun))
bench((fun.name || 'bound mapl').slice(6), function() {
return tests.map(fun)
})
})
})
run().then(function() {
for (let [_, check] of func) {
printCurrentStatus(check);
}
printStatusHelperText()
});

517
benchmark/router_v2.mjs Normal file
View file

@ -0,0 +1,517 @@
const Debug = false
export function printTree(children, indent = 0) {
if (!children.length || !Debug) return
for (let child of children) {
console.log(''.padStart(indent * 2) + (child.char || '*')
+ ' ' + (child.isParams ? `{ params = ${child.isParams} }` : '')
+ (child.isFullParams ? `{ fullParams = ${child.isFullParams} }` : '')
+ ' ' + (child.path ? `{ handler = ${child.path.path} }` : ''))
printTree(child.children, indent + 1)
}
}
class RouterError extends Error {
constructor(route1, route2, ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, RouterError);
}
this.name = "RouterError";
this.routeA = route1
this.routeB = route2
}
}
function Child(split, x, i) {
this.path = null
this.isParams = split[x].isParams ? split[x].word : null
this.isFullParams = split[x].isFullParams ? split[x].word : null
this.paramVarName = split[x].paramVarName ?? null
this.char = !this.isParams && !this.isFullParams ? split[x].word[i] || '/' : null
this.count = 0
this.children = []
}
function buildChild(x, i, splitPaths) {
let splitPath = splitPaths[0]
let letter = new Child(splitPath.split, x, i)
let consume = []
if (splitPath.split.length === x + 1
&& (splitPath.split[x].isParams
|| splitPath.split[x].isFullParams
|| splitPath.split[x].word.length === i + 1)) {
letter.path = splitPath.entry
letter.count += 1
} else {
consume = [splitPath]
}
for (let y = 1; y < splitPaths.length; y++) {
let checkPath = splitPaths[y]
if (!checkPath.split[x]
|| checkPath.split[x].isParams !== splitPath.split[x].isParams
|| checkPath.split[x].isFullParams !== splitPath.split[x].isFullParams
|| !checkPath.split[x].isParams
&& !checkPath.split[x].isFullParams
&& (checkPath.split[x].word[i] || '/') !== letter.char) break
consume.push(checkPath)
}
letter.count += consume.length
if (splitPath.split[x].word.length === i || splitPath.split[x].isParams || splitPath.split[x].isFullParams) {
x++
i = -1
}
while (consume.length) {
letter.children.push(buildChild(x, i + 1, consume))
consume.splice(0, letter.children[letter.children.length - 1].count)
}
return letter
}
export function buildTree(splitPaths) {
let builder = []
while (splitPaths.length) {
builder.push(buildChild(0, 0, splitPaths))
splitPaths.splice(0, builder[builder.length - 1].count)
}
return builder
}
const regParamPrefix = /^::?/
const regCleanNonAschii = /(?![a-zA-Z_])./g
const regCleanRest = /_+/g
function splitAndSortPaths(paths, separateStatic = true) {
let staticPaths = new Map()
let paramsPaths = []
let collator = new Intl.Collator('en', { sensitivity: 'accent' });
paths.forEach(function(entry) {
if (entry.path[0] !== '/') throw new RouterError(entry, null, 'Specified route was missing forward slash at start')
// Collect static paths separately
if (entry.path.indexOf('/:') < 0 && separateStatic) {
return staticPaths.set(entry.path, {
path: entry,
params: {}
})
}
// Collect params path separately
paramsPaths.push({
split: entry.path.slice(1).split(/\//g).map(function(word) {
let actualWord = word.replace(regParamPrefix, '')
return {
word: actualWord,
isParams: word[0] === ':' && word[1] !== ':',
isFullParams: word[0] === ':' && word[1] === ':',
paramVarName: word[0] === ':'
? actualWord.replace(regCleanNonAschii, '_').replace(regCleanRest, '_')
: null
}
}),
entry,
})
})
paramsPaths.sort(function(aGroup, bGroup) {
let length = Math.max(aGroup.split.length, bGroup.split.length)
for (let x = 0; x < length; x++) {
let a = aGroup.split[x]
let b = bGroup.split[x]
if (!a) return -1
if (!b) return 1
// Full params go last
if (a.isFullParams && b.isFullParams) throw new RouterError(aGroup.entry, bGroup.entry, 'Two full path routes found on same level')
if (a.isFullParams) return 1
if (b.isFullParams) return -1
// Params go second last
if (a.isParams && !b.isParams) return 1
if (!a.isParams && b.isParams) return -1
// otherwise sort alphabetically if not identical
if (a.word !== b.word) return collator.compare(a.word, b.word)
}
throw new RouterError(aGroup, bGroup, 'Two identical paths were found')
})
return {
staticPaths,
paramsPaths,
}
}
const SlashCode = '/'.charCodeAt(0)
function getIndex(offset, additions, params) {
return (offset + additions)
+ (params.length
? ' + ' + params.map(a => `offset${a[1]}`).join(' + ')
: '')
}
function treeIntoCompiledTreeBufferReturnPath(indentString, paths, branch, params, alt) {
let pathIndex = paths.indexOf(branch.path)
if (pathIndex < 0) {
throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths')
}
let output = ''
output += '\n' + indentString + `return {`
output += '\n' + indentString + ` path: paths[${pathIndex}],`
if (params.length) {
output += '\n' + indentString + ` params: {`
for (let param of params) {
if (alt === 0) {
output += '\n' + indentString + ` ${param[0]}: s${param[1]}.toString(),`
} else if (alt === 1 || alt === 3) {
output += '\n' + indentString + ` ${param[0]}: s${param[1]},`
} else if (alt === 2) {
output += '\n' + indentString + ` ${param[0]}: textDecoder.decode(s${param[1]}),`
}
}
output += '\n' + indentString + ` },`
} else {
output += '\n' + indentString + ` params: {},`
}
output += '\n' + indentString + `}`
return output
}
function treeIntoCompiledTreeBufferBranch(paths, branches, indent = 0, params = [], alt = 0) {
let output = ''
let indentation = ''.padStart((indent - params.length) * 2)
let addEndBracket = true
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
if (i > 0) {
if (!branch.isParams && !branch.isFullParams) {
output += ' else '
} else {
// output += '} //'
output += '\n' + indentation
}
}
if (!branch.isParams && !branch.isFullParams) {
output += `if (buf[${getIndex(indent, 0, params)}] === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (buf.length === ${getIndex(indent, 1, params)}) {`
output += treeIntoCompiledTreeBufferReturnPath(indentation + ' ', paths, branch, params, alt)
output += '\n' + indentation + ` }`
}
} else {
addEndBracket = false
let paramVarName = (params.length + 1) + '_' + branch.paramVarName
if (alt === 1 || alt === 3) {
output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${getIndex(indent, 0, params)}) >>> 0`})`
} else {
output += `let s${paramVarName} = buf.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${getIndex(indent, 0, params)}) >>> 0`})`
}
output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length`
output += '\n' + indentation
params.push([branch.isParams || branch.isFullParams, paramVarName])
if (branch.isFullParams) {
output += treeIntoCompiledTreeBufferReturnPath(indentation, paths, branch, params, alt)
} else if (branch.path) {
output += '\n' + indentation + `if (buf.length === ${getIndex(indent, 0, params)}) {`
output += treeIntoCompiledTreeBufferReturnPath(indentation + ' ', paths, branch, params, alt)
output += '\n' + indentation + `}`
}
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += treeIntoCompiledTreeBufferBranch(paths, branch.children, indent + 1, params.slice())
}
if (addEndBracket) {
output += '\n' + indentation + '} '
}
}
return output
}
export function treeIntoCompiledTreeBuffer(paths, tree) {
let output = 'return function RBuffer(paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n var buf = Buffer.from(str)'
output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
function treeIntoCompiledTreeStringReturnPath(indentString, paths, branch, params) {
let pathIndex = paths.indexOf(branch.path)
if (pathIndex < 0) {
throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths')
}
let output = ''
output += '\n' + indentString + `return {`
output += '\n' + indentString + ` path: paths[${pathIndex}],`
if (params.length) {
output += '\n' + indentString + ` params: {`
for (let param of params) {
output += '\n' + indentString + ` ${param[0]}: s${param[1]},`
}
output += '\n' + indentString + ` },`
} else {
output += '\n' + indentString + ` params: {},`
}
output += '\n' + indentString + `}`
return output
}
function treeIntoCompiledTreeStringBranch(paths, branches, indent = 0, params = []) {
let output = ''
let indentation = ''.padStart((indent - params.length) * 2)
let addEndBracket = true
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
if (i > 0) {
if (!branch.isParams && !branch.isFullParams) {
output += ' else '
} else {
// output += '} //'
output += '\n' + indentation
}
}
if (!branch.isParams && !branch.isFullParams) {
output += `if (str.charCodeAt(${getIndex(indent, 0, params)}) === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (str.length === ${getIndex(indent, 1, params)}) {`
output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + ` }`
}
} else {
addEndBracket = false
let paramVarName = (params.length + 1) + '_' + branch.paramVarName
output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, str.indexOf('/', ${getIndex(indent, 0, params)}) >>> 0`})`
output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length`
output += '\n' + indentation
params.push([branch.isParams || branch.isFullParams, paramVarName])
if (branch.isFullParams) {
output += treeIntoCompiledTreeStringReturnPath(indentation, paths, branch, params)
} else if (branch.path) {
output += '\n' + indentation + `if (str.length === ${getIndex(indent, 0, params)}) {`
output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + `}`
}
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += treeIntoCompiledTreeStringBranch(paths, branch.children, indent + 1, params.slice())
}
if (addEndBracket) {
output += '\n' + indentation + '} '
}
}
return output
}
function treeIntoCompiledTreeStringBranchIndices(paths, branches, indent = 0, params = []) {
let output = ''
let indentation = ''.padStart((indent - params.length) * 2)
let addEndBracket = true
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
if (i > 0) {
if (!branch.isParams && !branch.isFullParams) {
output += ' else '
} else {
// output += '} //'
output += '\n' + indentation
}
}
if (!branch.isParams && !branch.isFullParams) {
output += `if (str[${getIndex(indent, 0, params)}] === '${branch.char}') { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (str.length === ${getIndex(indent, 1, params)}) {`
output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + ` }`
}
} else {
addEndBracket = false
let paramVarName = (params.length + 1) + '_' + branch.paramVarName
output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, str.indexOf('/', ${getIndex(indent, 0, params)}) >>> 0`})`
output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length`
output += '\n' + indentation
params.push([branch.isParams || branch.isFullParams, paramVarName])
if (branch.isFullParams) {
output += treeIntoCompiledTreeStringReturnPath(indentation, paths, branch, params)
} else if (branch.path) {
output += '\n' + indentation + `if (str.length === ${getIndex(indent, 0, params)}) {`
output += treeIntoCompiledTreeStringReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + `}`
}
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += treeIntoCompiledTreeStringBranch(paths, branch.children, indent + 1, params.slice())
}
if (addEndBracket) {
output += '\n' + indentation + '} '
}
}
return output
}
export function treeIntoCompiledTreeString(paths, tree) {
let output = 'return function RString(paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n ' + treeIntoCompiledTreeStringBranch(paths, tree, 1)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function treeIntoCompiledTreeStringIndices(paths, tree) {
let output = 'return function RStringIndices(paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n ' + treeIntoCompiledTreeStringBranchIndices(paths, tree, 1)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function treeIntoCompiledTreeBufferAltOne(paths, tree) {
let output = 'return function RBufferStrSlice(paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n var buf = Buffer.from(str)'
output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1, [], 1)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function treeIntoCompiledTreeBufferAltTwo(paths, tree) {
let output = 'return function RTextEncoderDecoder(textEncoder, textDecoder, paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n var buf = textEncoder.encode(str)'
output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1, [], 2)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function treeIntoCompiledTreeBufferAltThree(paths, tree) {
let output = 'return function RTextEncoderStrSlice(textEncoder, paths, static, str) {'
output += '\n let checkStatic = static.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n var buf = textEncoder.encode(str)'
output += '\n ' + treeIntoCompiledTreeBufferBranch(paths, tree, 1, [], 3)
output += '\n return null'
output += '\n}'
if (Debug) {
console.log(output)
}
return new Function(output)()
}
export function compilePaths(paths) {
let splitPaths = splitAndSortPaths(paths)
let tree = buildTree(splitPaths.paramsPaths.slice())
printTree(tree, 0)
let compiled = treeIntoCompiledTreeBuffer(paths, tree)
let compiledString = treeIntoCompiledTreeString(paths, tree)
let compiledStringIndices = treeIntoCompiledTreeStringIndices(paths, tree)
let compiledTextOne = treeIntoCompiledTreeBufferAltOne(paths, tree)
// let compiledTextTwo = treeIntoCompiledTreeBufferAltTwo(paths, tree)
// let compiledTextThree = treeIntoCompiledTreeBufferAltThree(paths, tree)
return [
[compiled, compiled.bind(null, paths, splitPaths.staticPaths)],
[compiledTextOne, compiledTextOne.bind(null, paths, splitPaths.staticPaths)],
// [compiledTextTwo, compiledTextTwo.bind(null, new TextEncoder(), new TextDecoder(), paths, splitPaths.staticPaths)],
// [compiledTextThree, compiledTextThree.bind(null, new TextEncoder(), paths, splitPaths.staticPaths)],
[compiledString, compiledString.bind(null, paths, splitPaths.staticPaths)],
[compiledStringIndices, compiledStringIndices.bind(null, paths, splitPaths.staticPaths)],
]
}
export function compilePathsBuffer(paths) {
let splitPaths = splitAndSortPaths(paths)
let tree = buildTree(splitPaths.paramsPaths.slice())
printTree(tree, 0)
let compiled = treeIntoCompiledTreeBuffer(paths, tree)
let compiledString = treeIntoCompiledTreeString(paths, tree)
let compiledStringIndices = treeIntoCompiledTreeStringIndices(paths, tree)
let compiledTextOne = treeIntoCompiledTreeBufferAltOne(paths, tree)
// let compiledTextTwo = treeIntoCompiledTreeBufferAltTwo(paths, tree)
// let compiledTextThree = treeIntoCompiledTreeBufferAltThree(paths, tree)
return [
[compiled, compiled.bind(null, paths, splitPaths.staticPaths)],
[compiledTextOne, compiledTextOne.bind(null, paths, splitPaths.staticPaths)],
// [compiledTextTwo, compiledTextTwo.bind(null, new TextEncoder(), new TextDecoder(), paths, splitPaths.staticPaths)],
// [compiledTextThree, compiledTextThree.bind(null, new TextEncoder(), paths, splitPaths.staticPaths)],
[compiledString, compiledString.bind(null, paths, splitPaths.staticPaths)],
[compiledStringIndices, compiledStringIndices.bind(null, paths, splitPaths.staticPaths)],
]
}

View file

@ -0,0 +1,16 @@
import { compilePaths } from "../router_v2.mjs"
import { printTime } from './utils.mjs'
import * as consts from './const.js'
let paths = consts.allManyRoutes.map(x => ({ path: x }))
let s1 = process.hrtime.bigint()
let s2 = process.hrtime.bigint()
compilePaths(paths)
let s3 = process.hrtime.bigint()
let time = s3 - s2 - (s2 - s1)
printTime(time)

80
benchmark/string.js Normal file
View file

@ -0,0 +1,80 @@
import { summary, run, bench } from 'mitata';
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
// Example benchmark
summary(() => {
const dataset = new Array(100).fill(0).map(
() => new Array(10).fill(0).map(() => String.fromCharCode(97 + Math.round(Math.random() * 22))).join('')
);
console.log(dataset)
const fn1 = (str) => (str[0] === 'c' ? 1 : 0) + (str[1] === 'c' ? 1 : 0) + (str[2] === 'c' ? 1 : 0);
fn1('a');
fn1('b');
fn1('c');
fn1('d');
// optimizeNextInvocation(fn1);
bench('Char check', () => dataset.map(fn1))
const fn2 = (str) => (str.charCodeAt(0) === 99 ? 1 : 0) + (str.charCodeAt(1) === 99 ? 1 : 0) + (str.charCodeAt(2) === 99 ? 1 : 0);
fn2('a');
fn2('b');
fn2('c');
fn2('d');
// optimizeNextInvocation(fn2);
bench('Char code check', () => dataset.map(fn2));
bench('2x Char check', () => dataset.map(fn1))
bench('2x Char code check', () => dataset.map(fn2));
});
// Example benchmark
summary(() => {
let paths = [
'test1/',
'test/',
'test3/',
'test/',
'test5/',
'something/',
'else/',
'goes/',
'here/',
'too/',
]
function fastCheck(str) {
if (str.charCodeAt(0) === 116) {
if (str.charCodeAt(1) === 101) {
if (str.charCodeAt(2) === 115) {
if (str.charCodeAt(3) === 116) {
if (str.charCodeAt(4) === 47) {
if (str.indexOf('/', 5) === -1) {
return true
}
}
}
}
}
}
return false
}
let r = new RegExp('^test/$')
function regexCheck(str) {
return r.test(str)
}
console.log(paths.map(fastCheck))
console.log(paths.map(regexCheck))
bench('fastCheck', () => paths.map(fastCheck))
bench('regexCheck', () => paths.map(regexCheck));
});
run();

121
benchmark/strings.mjs Normal file
View file

@ -0,0 +1,121 @@
import { buildTree, compileTreeIntoIfs, compileTreeIntoIfsWithBuffer } from "./compiler/compiler.mjs"
import { summary, run, bench } from 'mitata';
import { printCurrentStatus, printStatusHelperText } from "./compiler/utils.mjs";
import { FlaskaRouter } from "../flaska.mjs";
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
let paths = [
'/fsdafasfa',
'/ymreklhmse',
'/h34nmlaeknmgl',
'/asdgsdagas',
'/ahaewhweaaa',
'/adshashaea',
'/sdafasfsadfasdfas',
//'/gdfsfgsfdsgsdrgsregsergsregersgserersgsergersg',
//'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
]
let pathsBla = [
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
'/test5afdlkasdflksad flsakdf lsakf asdlkfa lsdkfasdlkdfalklk',
]
let pathsBuffer = paths.map(x => new Uint8Array(Buffer.from(x)))
let tree = buildTree(paths)
let noop = function() { }
const ifTree = compileTreeIntoIfs(tree)
// console.log(ifTree.toString())
const ifTreeBuffer = compileTreeIntoIfsWithBuffer(tree)
// console.log(ifTreeBuffer.toString())
const flaskaRouter = new FlaskaRouter()
for (let path of paths) {
flaskaRouter.addRoute(path, noop)
}
const m = new Map(paths.map(x => [x, x]))
function mapFlat(str) {
return m.get(str) ?? null
}
function toBuffer(str) {
return Buffer.from(str)
}
function toUint(str) {
return new Uint8Array(Buffer.from(str))
}
function toManualArray(str) {
let length = str.length
let out = new Array(length)
for (let i = 0; i < length; i++) {
out[i] = str.charCodeAt(i)
}
return out
}
function allocBufferUnsafe(str) {
return Buffer.allocUnsafe(str.length)
}
function allocBufferSafe(str) {
return Buffer.alloc(str.length)
}
function allocUint8(str) {
return new Uint8Array(str.length)
}
function toManualArraySplitMap(str) {
return str.split('').map(x => x.charCodeAt(0))
}
let func1 = [mapFlat, toBuffer, toUint, toManualArray, toManualArraySplitMap, allocBufferUnsafe, allocBufferSafe, allocUint8, ifTree];
for (var i = 0; i < 100000; i++) {
for (let fun of func1) {
paths.map(fun)
}
}
for (var i = 0; i < 100000; i++) {
for (let path of paths) {
flaskaRouter.match(path)
}
}
for (let fun of func1) {
printCurrentStatus(fun);
}
printCurrentStatus(flaskaRouter.match)
printStatusHelperText()
summary(() => {
bench('if tree', function() {
return paths.map(ifTree)
})
bench('if tree buffer edition', function() {
return paths.map(ifTreeBuffer)
});
bench('flaskarouter', function() {
return paths.map(flaskaRouter.match.bind(flaskaRouter))
});
/*bench('map edition', function() {
return paths.map(mapFlat)
});
bench('if tree pre buffer edition', function() {
return pathsBuffer.map(ifTreeBuffer)
});
bench('toBuffer', () => pathsBla.map(toBuffer));
bench('toUint', () => pathsBla.map(toUint));
bench('toManualArraySplitMap', () => pathsBla.map(toManualArraySplitMap))
bench('toManualArray', () => pathsBla.map(toManualArray))*/
bench('allocBufferUnsafe', () => pathsBla.map(allocBufferUnsafe))
bench('allocBufferSafe', () => pathsBla.map(allocBufferSafe))
bench('allocUint8', () => pathsBla.map(allocUint8))
})
run();

View file

@ -0,0 +1,115 @@
import { printCurrentStatus, printStatusHelperText } from "./compiler/utils.mjs";
import { summary, run, bench } from 'mitata';
// Do warmup
bench('noop', () => { })
bench('noop2', () => { })
// Do checks with arrays
let arr = ['hi', 'there', 'nagna', 'randomlongstring', 'small']
arr = arr.map(x => [x, { path: x }])
let paths = arr.map(x => x[1])
// Do checks with objects
let obj = {};
let map = new Map()
for (let i = 0, l = arr.length; i < l; ++i) {
obj[arr[i][0]] = arr[i][1]
map.set(arr[i][0], arr[i][1])
};
let objHas1 = (obj, item) => { let x = obj[item]; if (x) { return x } return null };
let objHas2 = (obj, item) => { if (item in obj) { return obj[item] } return null }
let mapGet = (map, item) => { let x = map.get(item); if (x) { return x }; return null };
// Generate a function which compares the input string to all strings in the list
let basicIfChain = Function(`
return (paths, str) => {
${arr.map((item, i) => 'if (str === "' + item[0] + '") { return paths[' + i + '] }').join('\n ')}
return null
}
`)();
let basicIfChainAlt = Function(`
return (str) => {
${arr.map((item, i) => 'if (str === "' + item[0] + '") { return "' + item[0] + '" }').join('\n ')}
return null
}
`)();
function basicIfChainNoFunc(str) {
if (str === "hi") { return "hi" }
if (str === "there") { return "there" }
if (str === "nagna") { return "nagna" }
if (str === "randomlongstring") { return "randomlongstring" }
if (str === "small") { return "small" }
return null
}
function addTwo(a, b) {
return a + b
}
console.log(`
return (str) => {
${arr.map((item, i) => 'if (str === "' + item[0] + '") { return "' + item[0] + '" }').join('\n ')}
return null
}
`)
let func = [
['basicIfChainNoFunc', basicIfChainNoFunc, basicIfChainNoFunc],
['basicIfChainAlt', basicIfChainAlt, basicIfChainAlt],
['objHas1', objHas1, objHas1.bind(null, obj)],
['objHas2', objHas2, objHas2.bind(null, obj)],
['mapGet', mapGet, mapGet.bind(null, map)],
['basicIfChain', basicIfChain, basicIfChain.bind(null, paths)],
]
for (let [name, __, fun] of func) {
console.log(`--- Sanity checking ${name} ---`)
for (let a of arr) {
console.log(a[0], fun(a[0]))
}
}
// Generate strings
let newArr = [];
for (let i = 0; i < 100000; i++)
newArr.push(Math.random() < 0.5 ? `${Math.random()}` : arr[Math.round(Math.random() * 4)].slice(0, -1 >>> 0));
console.log(`--- warming up addTwo ---`)
for (let i = 0; i < 100; i++) {
console.log(`--- run ${i} ---`)
for (let num of newArr) {
addTwo(num.length, num.length + 1)
}
await new Promise(res => setTimeout(res, 1000))
printCurrentStatus(addTwo);
}
for (let [name, org, fun] of func) {
console.log(`--- warming up ${name} ---`)
for (let i = 0; i < 100; i++) {
console.log(`--- run ${i} ---`)
newArr.forEach(fun)
await new Promise(res => setTimeout(res, 1000))
printCurrentStatus(org);
}
}
console.log('--- sleeping ---')
await new Promise(res => setTimeout(res, 1000))
for (let [_, org] of func) {
printCurrentStatus(org);
}
printStatusHelperText()
summary(() => {
func.forEach(([name, _, fun]) => {
bench(name, () => newArr.map(fun));
})
});
run();

44
benchmark/strings_v2.mjs Normal file
View file

@ -0,0 +1,44 @@
import { summary, run, bench } from 'mitata';
import { FlaskaRouter } from "../flaska.mjs";
// Warmup (de-optimize `bench()` calls)
bench('noop', () => { });
bench('noop2', () => { });
function padStart(length) {
return ''.padStart(length * 2)
}
const spaces = ''.padStart(256)
function strSlice(length) {
return spaces.slice(0, length)
}
function strSubstring(length) {
return spaces.substring(0, length)
}
const testData = new Array(100000)
for (let i = 0; i < testData.length; i++) {
testData[i] = Math.round(Math.random() * 200)
}
let func = [padStart, strSlice, strSubstring];
for (let fun of func) {
console.log(`--- warming up ${fun.name || 'mapl'} ---`)
for (var i = 0; i < 100; i++) {
testData.map(fun)
}
}
summary(() => {
func.forEach(function(fun) {
// console.log(tests, fun, tests.map(fun))
bench(fun.name, function() {
return testData.map(fun)
})
})
})
run();

33
benchmark/test.mjs Normal file
View file

@ -0,0 +1,33 @@
import assert from 'assert'
import { compilePaths } from "./router_v2.mjs"
import * as consts from './const.js'
let paths = [
{ path: '/aa/aa', },
{ path: '/aa/:blabla', },
{ path: '/::rest', },
]
// paths = consts.allManyRoutes.map(x => ({ path: x }))
let tests = [
['/', paths[5]],
['/aa', paths[5]],
['/aa/aa', paths[0]],
['/aa/_', paths[1]],
['/aa/_/aa', paths[2]],
['/aa/_/ab', paths[3]],
['/aa/_/bb', paths[4]],
]
tests = paths.map(p => ([p.path.replace(/:[^/]+/g, '_'), p]))
let func = compilePaths(paths)
for (let [_, fun] of func) {
console.log(`--- ${fun.name} ---`)
for (let test of tests) {
let check = fun(test[0])
console.log(test[0], check)
assert.strictEqual(check.path, test[1])
}
}

15
benchmark/utils.mjs Normal file
View file

@ -0,0 +1,15 @@
export function printTime (t) {
let time = Number(t)
let units = ['n', 'μ', 'm', 'c', 's']
let unit = units[0]
let unitPower = 1
for (let i = 0; i < units.length; i++) {
let power = Math.pow(10, (i + 1) * 3)
if (power * 2 > time) {
break
}
unitPower = power
unit = units[i]
}
console.log(t, '=', Number((time / unitPower).toFixed(2)), unit)
}

View file

@ -431,7 +431,7 @@ export class FlaskaRouter {
let isFullParam = false
let branch = this.root
if (route.indexOf(':') < 0) {
if (route.indexOf(':') < 0 && false) {
let name = route
if (name.length > 1 && name[name.length - 1] === '/') {
name = name.slice(0, -1)

242
router_v2.mjs Normal file
View file

@ -0,0 +1,242 @@
class RouterError extends Error {
constructor(route1, route2, ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, RouterError);
}
this.name = "RouterError";
this.routeA = route1
this.routeB = route2
}
}
function Child(split, x, i) {
this.path = null
this.isParams = split[x].isParams ? split[x].word : null
this.isFullParams = split[x].isFullParams ? split[x].word : null
this.paramVarName = split[x].paramVarName ?? null
this.char = !this.isParams && !this.isFullParams ? split[x].word[i] || '/' : null
this.count = 0
this.children = []
}
function buildChild(x, i, splitPaths) {
let splitPath = splitPaths[0]
let letter = new Child(splitPath.split, x, i)
let consume = []
if (splitPath.split.length === x + 1
&& (splitPath.split[x].isParams
|| splitPath.split[x].isFullParams
|| splitPath.split[x].word.length === i + 1)) {
letter.path = splitPath.entry
letter.count += 1
} else {
consume = [splitPath]
}
for (let y = 1; y < splitPaths.length; y++) {
let checkPath = splitPaths[y]
if (!checkPath.split[x]
|| checkPath.split[x].isParams !== splitPath.split[x].isParams
|| checkPath.split[x].isFullParams !== splitPath.split[x].isFullParams
|| !checkPath.split[x].isParams
&& !checkPath.split[x].isFullParams
&& (checkPath.split[x].word[i] || '/') !== letter.char) break
consume.push(checkPath)
}
letter.count += consume.length
if (splitPath.split[x].word.length === i || splitPath.split[x].isParams || splitPath.split[x].isFullParams) {
x++
i = -1
}
while (consume.length) {
letter.children.push(buildChild(x, i + 1, consume))
consume.splice(0, letter.children[letter.children.length - 1].count)
}
return letter
}
function buildTree(splitPaths) {
let builder = []
while (splitPaths.length) {
builder.push(buildChild(0, 0, splitPaths))
splitPaths.splice(0, builder[builder.length - 1].count)
}
return builder
}
const regParamPrefix = /^::?/
const regCleanNonAschii = /(?![a-zA-Z_])./g
const regCleanRest = /_+/g
function splitAndSortPaths(paths, separateStatic = true) {
let staticPaths = new Map()
let paramsPaths = []
let collator = new Intl.Collator('en', { sensitivity: 'accent' });
paths.forEach(function(entry) {
if (entry.path[0] !== '/') throw new RouterError(entry, null, 'Specified route was missing forward slash at start')
// Collect static paths separately
if (entry.path.indexOf('/:') < 0 && separateStatic) {
return staticPaths.set(entry.path, {
path: entry,
params: {}
})
}
// Collect params path separately
paramsPaths.push({
split: entry.path.slice(1).split(/\//g).map(function(word) {
let actualWord = word.replace(regParamPrefix, '')
return {
word: actualWord,
isParams: word[0] === ':' && word[1] !== ':',
isFullParams: word[0] === ':' && word[1] === ':',
paramVarName: word[0] === ':'
? actualWord.replace(regCleanNonAschii, '_').replace(regCleanRest, '_')
: null
}
}),
entry,
})
})
paramsPaths.sort(function(aGroup, bGroup) {
let length = Math.max(aGroup.split.length, bGroup.split.length)
for (let x = 0; x < length; x++) {
let a = aGroup.split[x]
let b = bGroup.split[x]
if (!a) return -1
if (!b) return 1
// Full params go last
if (a.isFullParams && b.isFullParams) throw new RouterError(aGroup.entry, bGroup.entry, 'Two full path routes found on same level')
if (a.isFullParams) return 1
if (b.isFullParams) return -1
// Params go second last
if (a.isParams && !b.isParams) return 1
if (!a.isParams && b.isParams) return -1
// otherwise sort alphabetically if not identical
if (a.word !== b.word) return collator.compare(a.word, b.word)
}
throw new RouterError(aGroup, bGroup, 'Two identical paths were found')
})
return {
staticPaths,
paramsPaths,
}
}
const SlashCode = '/'.charCodeAt(0)
function getIndex(offset, additions, params) {
return (offset + additions)
+ (params.length
? ' + ' + params.map(a => `offset${a[1]}`).join(' + ')
: '')
}
function treeIntoCompiledCodeReturnPath(indentString, paths, branch, params) {
let pathIndex = paths.indexOf(branch.path)
if (pathIndex < 0) {
throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths')
}
let output = '\n' + indentString + `return {`
output += '\n' + indentString + ` path: paths[${pathIndex}],`
if (params.length) {
output += '\n' + indentString + ` params: {`
for (let param of params) {
output += '\n' + indentString + ` ${param[0]}: s${param[1]},`
}
output += '\n' + indentString + ` },`
} else {
output += '\n' + indentString + ` params: {},`
}
output += '\n' + indentString + `}`
return output
}
const spaces = ' '
function treeIntoCompiledCodeBranch(paths, branches, indent = 0, params = []) {
let output = ''
let indentation = spaces.slice(0, (indent - params.length) * 2)
let addEndBracket = true
for (let i = 0; i < branches.length; i++) {
let branch = branches[i]
if (i > 0) {
if (!branch.isParams && !branch.isFullParams) {
output += ' else '
} else {
// output += '} //'
output += '\n' + indentation
}
}
if (!branch.isParams && !branch.isFullParams) {
output += `if (buf[${getIndex(indent, 0, params)}] === ${branch.char.charCodeAt(0)}) { // ${branch.char}`
if (branch.path) {
output += '\n' + indentation + ` if (buf.length === ${getIndex(indent, 1, params)}) {`
output += treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + ` }`
}
} else {
addEndBracket = false
let paramVarName = (params.length + 1) + '_' + branch.paramVarName
output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${getIndex(indent, 0, params)}) >>> 0`})`
output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length`
output += '\n' + indentation
params.push([branch.isParams || branch.isFullParams, paramVarName])
if (branch.isFullParams) {
output += treeIntoCompiledCodeReturnPath(indentation, paths, branch, params)
} else if (branch.path) {
output += '\n' + indentation + `if (buf.length === ${getIndex(indent, 0, params)}) {`
output += treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params)
output += '\n' + indentation + `}`
}
}
if (branch.children.length) {
if (branch.path) {
output += ' else '
} else {
output += '\n' + indentation + ' '
}
output += treeIntoCompiledCodeBranch(paths, branch.children, indent + 1, params.slice())
}
if (addEndBracket) {
output += '\n' + indentation + '} '
}
}
return output
}
function treeIntoCompiledCodeClosure(paths, tree, staticList) {
let output = 'return function RBufferStrSliceClosure(str) {'
output += '\n let checkStatic = staticList.get(str)'
output += '\n if(checkStatic) {'
output += '\n return checkStatic'
output += '\n }'
output += '\n var buf = Buffer.from(str)'
output += '\n ' + treeIntoCompiledCodeBranch(paths, tree, 1, [])
output += '\n return null'
output += '\n}'
return new Function('paths', 'staticList', output)(paths, staticList)
}
export function compilePaths(paths) {
let splitPaths = splitAndSortPaths(paths)
let tree = buildTree(splitPaths.paramsPaths.slice())
return treeIntoCompiledCodeClosure(paths, tree, splitPaths.staticPaths)
}