diff --git a/benchmark/const.js b/benchmark/const.js index 2539830..1855177 100644 --- a/benchmark/const.js +++ b/benchmark/const.js @@ -35,6 +35,21 @@ export const allRoutes = [ '/::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 = [ '/', '/api/articles', diff --git a/benchmark/mapl_compiler.mjs b/benchmark/mapl_compiler.mjs new file mode 100644 index 0000000..ec52204 --- /dev/null +++ b/benchmark/mapl_compiler.mjs @@ -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); +} \ No newline at end of file diff --git a/benchmark/mapl_router_compile.mjs b/benchmark/mapl_router_compile.mjs new file mode 100644 index 0000000..ede7c28 --- /dev/null +++ b/benchmark/mapl_router_compile.mjs @@ -0,0 +1,64 @@ +import { createRouter, insertItem } from '@mapl/router' +import { compileRouter } from './mapl_compiler.mjs' +import * as consts from './const.js' + +let testPaths = consts.allManyRoutes.map(x => x.replace(/:[^\/]+/g, '*')) // consts.allManyRoutes + +testPaths = [ + '/*/file', + '/*', +] + +const router2 = createRouter(); +for (let route of testPaths) { + insertItem(router2, route, '{works}') +} +const match2 = compileRouter(router2) +for (let route of testPaths) { + let check = route.replace(/\*/g, 'something') + let gotMatched = match2(check.slice(1)) + console.log(check, gotMatched?.[0] || '{ERROR NO MATCH}') +} + +process.exit(0) + +const r = createRouter(); +for (let route of testPaths) { + let cleaned = route.replace(/:[^\/]+/g, '*') + insertItem(r, cleaned, () => { }) +} + +const m = compileRouter(r) +for (let item of testPaths) { + console.log(item, m(item.slice(1))) +} + +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[1] + } + console.log(t, '=', Number((time / unitPower).toFixed(2)), unit) +} + +let paths = testPaths.map(x => ({ path: x })) + +let s1 = process.hrtime.bigint() +let s2 = process.hrtime.bigint() + +const match = compileRouter(router); +console.log(match.toString()); + +let s3 = process.hrtime.bigint() + +let time = s3 - s2 - (s2 - s1) + +printTime(time) diff --git a/benchmark/package.json b/benchmark/package.json index b066afe..a2399d8 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -11,6 +11,7 @@ "author": "", "license": "WTFPL", "dependencies": { + "@mapl/router": "^0.0.14", "benchmarkjs-pretty": "^2.0.0", "express": "^4.17.1", "koa-router": "^8.0.8", diff --git a/benchmark/router_flaska_v2.mjs b/benchmark/router_flaska_v2.mjs index 439b0a7..2a4f451 100644 --- a/benchmark/router_flaska_v2.mjs +++ b/benchmark/router_flaska_v2.mjs @@ -1,6 +1,6 @@ import { summary, run, bench } from 'mitata'; import assert from 'assert' -import { compilePaths } from "../router_v2.mjs" +import { compilePaths } from "./router_v2.mjs" import * as consts from './const.js' function printCurrentStatus(fn) { @@ -42,7 +42,7 @@ let paths = [ { path: '/::rest', }, ] -paths = consts.allManyRoutes.map(x => ({ path: x })) +paths = consts.allRoutesGoldenBalance.map(x => ({ path: x })) let tests = [ ['/', paths[5]], diff --git a/benchmark/router_v2.mjs b/benchmark/router_v2.mjs new file mode 100644 index 0000000..a1384ad --- /dev/null +++ b/benchmark/router_v2.mjs @@ -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)], + ] +} diff --git a/benchmark/router_v2_compile.mjs b/benchmark/router_v2_compile.mjs new file mode 100644 index 0000000..b0bbc16 --- /dev/null +++ b/benchmark/router_v2_compile.mjs @@ -0,0 +1,31 @@ +import { compilePaths } from "../router_v2.mjs" +import * as consts from './const.js' + +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[1] + } + console.log(t, '=', Number((time / unitPower).toFixed(2)), unit) +} + +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) diff --git a/benchmark/router_v2_compiler_runner.mjs b/benchmark/router_v2_compiler_runner.mjs new file mode 100644 index 0000000..e69de29 diff --git a/benchmark/strings_compare.mjs b/benchmark/strings_compare.mjs new file mode 100644 index 0000000..f3f2897 --- /dev/null +++ b/benchmark/strings_compare.mjs @@ -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(); diff --git a/benchmark/test.mjs b/benchmark/test.mjs index 7227824..1cf51b1 100644 --- a/benchmark/test.mjs +++ b/benchmark/test.mjs @@ -1,5 +1,5 @@ import assert from 'assert' -import { compilePaths } from "../router_v2.mjs" +import { compilePaths } from "./router_v2.mjs" import * as consts from './const.js' let paths = [ diff --git a/router_v2.mjs b/router_v2.mjs index 014eb40..a34a99b 100644 --- a/router_v2.mjs +++ b/router_v2.mjs @@ -1,16 +1,3 @@ -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) { @@ -157,7 +144,7 @@ function getIndex(offset, additions, params) { : '') } -function treeIntoCompiledTreeBufferReturnPath(indentString, paths, branch, params, alt) { +function treeIntoCompiledCodeReturnPath(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') @@ -184,7 +171,7 @@ function treeIntoCompiledTreeBufferReturnPath(indentString, paths, branch, param return output } -function treeIntoCompiledTreeBufferBranch(paths, branches, indent = 0, params = [], alt = 0) { +function treeIntoCompiledCodeBranch(paths, branches, indent = 0, params = [], alt = 0) { let output = '' let indentation = ''.padStart((indent - params.length) * 2) let addEndBracket = true @@ -205,26 +192,22 @@ function treeIntoCompiledTreeBufferBranch(paths, branches, indent = 0, params = if (branch.path) { output += '\n' + indentation + ` if (buf.length === ${getIndex(indent, 1, params)}) {` - output += treeIntoCompiledTreeBufferReturnPath(indentation + ' ', paths, branch, params, alt) + output += treeIntoCompiledCodeReturnPath(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 += `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 += treeIntoCompiledTreeBufferReturnPath(indentation, paths, branch, params, alt) + output += treeIntoCompiledCodeReturnPath(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 += treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params, alt) output += '\n' + indentation + `}` } } @@ -235,7 +218,7 @@ function treeIntoCompiledTreeBufferBranch(paths, branches, indent = 0, params = } else { output += '\n' + indentation + ' ' } - output += treeIntoCompiledTreeBufferBranch(paths, branch.children, indent + 1, params.slice()) + output += treeIntoCompiledCodeBranch(paths, branch.children, indent + 1, params.slice()) } if (addEndBracket) { output += '\n' + indentation + '} ' @@ -244,177 +227,22 @@ function treeIntoCompiledTreeBufferBranch(paths, branches, indent = 0, params = 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 -} - -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 treeIntoCompiledTreeBufferAltOne(paths, tree) { +export function treeIntoCompiledCode(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 ' + treeIntoCompiledCodeBranch(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 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)], - ] -} \ No newline at end of file + let compiled = treeIntoCompiledCode(paths, tree) + return compiled.bind(null, paths, splitPaths.staticPaths) +}