Skip to content

Commit

Permalink
perf(router): improve performance of router (#3526)
Browse files Browse the repository at this point in the history
* perf(router):  improve performance of router

* coverage
  • Loading branch information
EdamAme-x authored Oct 26, 2024
1 parent 234b083 commit 10a5e65
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 105 deletions.
13 changes: 13 additions & 0 deletions src/router/linear-router/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,17 @@ describe('LinearRouter', () => {
expect(res[0][0]).toBe('GET /book')
})
})

describe('Skip part', () => {
const router = new LinearRouter<string>()

beforeEach(() => {
router.add('GET', '/products/:id{d+}', 'GET /products/:id{d+}')
})

it('GET /products/list', () => {
const [res] = router.match('GET', '/products/list')
expect(res.length).toBe(0)
})
})
})
176 changes: 91 additions & 85 deletions src/router/linear-router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,117 +13,123 @@ export class LinearRouter<T> implements Router<T> {
routes: [string, string, T][] = []

add(method: string, path: string, handler: T) {
;(checkOptionalParameter(path) || [path]).forEach((p) => {
this.routes.push([method, p, handler])
})
for (
let i = 0, paths = checkOptionalParameter(path) || [path], len = paths.length;
i < len;
i++
) {
this.routes.push([method, paths[i], handler])
}
}

match(method: string, path: string): Result<T> {
const handlers: [T, Params][] = []
ROUTES_LOOP: for (let i = 0, len = this.routes.length; i < len; i++) {
const [routeMethod, routePath, handler] = this.routes[i]
if (routeMethod !== method && routeMethod !== METHOD_NAME_ALL) {
continue
}
if (routePath === '*' || routePath === '/*') {
handlers.push([handler, emptyParams])
continue
}

const hasStar = routePath.indexOf('*') !== -1
const hasLabel = routePath.indexOf(':') !== -1
if (!hasStar && !hasLabel) {
if (routePath === path || routePath + '/' === path) {
if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
if (routePath === '*' || routePath === '/*') {
handlers.push([handler, emptyParams])
continue
}
} else if (hasStar && !hasLabel) {
const endsWithStar = routePath.charCodeAt(routePath.length - 1) === 42
const parts = (endsWithStar ? routePath.slice(0, -2) : routePath).split(splitByStarRe)

const lastIndex = parts.length - 1
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
const part = parts[j]
const index = path.indexOf(part, pos)
if (index !== pos) {
continue ROUTES_LOOP
const hasStar = routePath.indexOf('*') !== -1
const hasLabel = routePath.indexOf(':') !== -1
if (!hasStar && !hasLabel) {
if (routePath === path || routePath + '/' === path) {
handlers.push([handler, emptyParams])
}
pos += part.length
if (j === lastIndex) {
if (
!endsWithStar &&
pos !== path.length &&
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
) {
} else if (hasStar && !hasLabel) {
const endsWithStar = routePath.charCodeAt(routePath.length - 1) === 42
const parts = (endsWithStar ? routePath.slice(0, -2) : routePath).split(splitByStarRe)

const lastIndex = parts.length - 1
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
const part = parts[j]
const index = path.indexOf(part, pos)
if (index !== pos) {
continue ROUTES_LOOP
}
} else {
const index = path.indexOf('/', pos)
if (index === -1) {
continue ROUTES_LOOP
pos += part.length
if (j === lastIndex) {
if (
!endsWithStar &&
pos !== path.length &&
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
) {
continue ROUTES_LOOP
}
} else {
const index = path.indexOf('/', pos)
if (index === -1) {
continue ROUTES_LOOP
}
pos = index
}
pos = index
}
}
handlers.push([handler, emptyParams])
} else if (hasLabel && !hasStar) {
const params: Record<string, string> = Object.create(null)
const parts = routePath.match(splitPathRe) as string[]
handlers.push([handler, emptyParams])
} else if (hasLabel && !hasStar) {
const params: Record<string, string> = Object.create(null)
const parts = routePath.match(splitPathRe) as string[]

const lastIndex = parts.length - 1
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
if (pos === -1 || pos >= path.length) {
continue ROUTES_LOOP
}
const lastIndex = parts.length - 1
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
if (pos === -1 || pos >= path.length) {
continue ROUTES_LOOP
}

const part = parts[j]
if (part.charCodeAt(1) === 58) {
// /:label
let name = part.slice(2)
let value
const part = parts[j]
if (part.charCodeAt(1) === 58) {
// /:label
let name = part.slice(2)
let value

if (name.charCodeAt(name.length - 1) === 125) {
// :label{pattern}
const openBracePos = name.indexOf('{')
const pattern = name.slice(openBracePos + 1, -1)
const restPath = path.slice(pos + 1)
const match = new RegExp(pattern, 'd').exec(restPath) as RegExpMatchArrayWithIndices
if (!match || match.indices[0][0] !== 0 || match.indices[0][1] === 0) {
continue ROUTES_LOOP
}
name = name.slice(0, openBracePos)
value = restPath.slice(...match.indices[0])
pos += match.indices[0][1] + 1
} else {
let endValuePos = path.indexOf('/', pos + 1)
if (endValuePos === -1) {
if (pos + 1 === path.length) {
if (name.charCodeAt(name.length - 1) === 125) {
// :label{pattern}
const openBracePos = name.indexOf('{')
const pattern = name.slice(openBracePos + 1, -1)
const restPath = path.slice(pos + 1)
const match = new RegExp(pattern, 'd').exec(restPath) as RegExpMatchArrayWithIndices
if (!match || match.indices[0][0] !== 0 || match.indices[0][1] === 0) {
continue ROUTES_LOOP
}
endValuePos = path.length
name = name.slice(0, openBracePos)
value = restPath.slice(...match.indices[0])
pos += match.indices[0][1] + 1
} else {
let endValuePos = path.indexOf('/', pos + 1)
if (endValuePos === -1) {
if (pos + 1 === path.length) {
continue ROUTES_LOOP
}
endValuePos = path.length
}
value = path.slice(pos + 1, endValuePos)
pos = endValuePos
}
value = path.slice(pos + 1, endValuePos)
pos = endValuePos
}

params[name] ||= value as string
} else {
const index = path.indexOf(part, pos)
if (index !== pos) {
continue ROUTES_LOOP
params[name] ||= value as string
} else {
const index = path.indexOf(part, pos)
if (index !== pos) {
continue ROUTES_LOOP
}
pos += part.length
}
pos += part.length
}

if (j === lastIndex) {
if (pos !== path.length && !(pos === path.length - 1 && path.charCodeAt(pos) === 47)) {
continue ROUTES_LOOP
if (j === lastIndex) {
if (
pos !== path.length &&
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
) {
continue ROUTES_LOOP
}
}
}
}

handlers.push([handler, params])
} else if (hasLabel && hasStar) {
throw new UnsupportedPathError()
handlers.push([handler, params])
} else if (hasLabel && hasStar) {
throw new UnsupportedPathError()
}
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/router/pattern-router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ export class PatternRouter<T> implements Router<T> {
match(method: string, path: string): Result<T> {
const handlers: [T, Params][] = []

for (const [pattern, routeMethod, handler] of this.routes) {
if (routeMethod === METHOD_NAME_ALL || routeMethod === method) {
for (let i = 0, len = this.routes.length; i < len; i++) {
const [pattern, routeMethod, handler] = this.routes[i]

if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
const match = pattern.exec(path)
if (match) {
handlers.push([handler, match.groups || Object.create(null)])
Expand Down
6 changes: 3 additions & 3 deletions src/router/smart-router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export class SmartRouter<T> implements Router<T> {
for (; i < len; i++) {
const router = routers[i]
try {
routes.forEach((args) => {
router.add(...args)
})
for (let i = 0, len = routes.length; i < len; i++) {
router.add(...routes[i])
}
res = router.match(method, path)
} catch (e) {
if (e instanceof UnsupportedPathError) {
Expand Down
23 changes: 10 additions & 13 deletions src/router/trie-router/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ type HandlerSet<T> = {
handler: T
possibleKeys: string[]
score: number
name: string // For debug
}

type HandlerParamsSet<T> = HandlerSet<T> & {
Expand All @@ -20,23 +19,20 @@ export class Node<T> {
children: Record<string, Node<T>>
patterns: Pattern[]
order: number = 0
name: string
params: Record<string, string> = Object.create(null)

constructor(method?: string, handler?: T, children?: Record<string, Node<T>>) {
this.children = children || Object.create(null)
this.methods = []
this.name = ''
if (method && handler) {
const m: Record<string, HandlerSet<T>> = Object.create(null)
m[method] = { handler, possibleKeys: [], score: 0, name: this.name }
m[method] = { handler, possibleKeys: [], score: 0 }
this.methods = [m]
}
this.patterns = []
}

insert(method: string, path: string, handler: T): Node<T> {
this.name = `${method} ${path}`
this.order = ++this.order

// eslint-disable-next-line @typescript-eslint/no-this-alias
Expand Down Expand Up @@ -76,7 +72,6 @@ export class Node<T> {
const handlerSet: HandlerSet<T> = {
handler,
possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
name: this.name,
score: this.order,
}

Expand All @@ -100,12 +95,14 @@ export class Node<T> {
const processedSet: Record<string, boolean> = Object.create(null)
if (handlerSet !== undefined) {
handlerSet.params = Object.create(null)
handlerSet.possibleKeys.forEach((key) => {
const processed = processedSet[handlerSet.name]
for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) {
const key = handlerSet.possibleKeys[i]
const processed = processedSet[handlerSet.score]
handlerSet.params[key] =
params[key] && !processed ? params[key] : nodeParams[key] ?? params[key]
processedSet[handlerSet.name] = true
})
processedSet[handlerSet.score] = true
}

handlerSets.push(handlerSet)
}
}
Expand All @@ -132,7 +129,7 @@ export class Node<T> {

if (nextNode) {
nextNode.params = node.params
if (isLast === true) {
if (isLast) {
// '/hello/*' => match '/hello'
if (nextNode.children['*']) {
handlerSets.push(
Expand Down Expand Up @@ -177,10 +174,10 @@ export class Node<T> {
continue
}

if (matcher === true || (matcher instanceof RegExp && matcher.test(part))) {
if (matcher === true || matcher.test(part)) {
if (typeof key === 'string') {
params[name] = part
if (isLast === true) {
if (isLast) {
handlerSets.push(...this.gHSets(child, method, params, node.params))
if (child.children['*']) {
handlerSets.push(...this.gHSets(child.children['*'], method, params, node.params))
Expand Down
4 changes: 2 additions & 2 deletions src/router/trie-router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export class TrieRouter<T> implements Router<T> {
add(method: string, path: string, handler: T) {
const results = checkOptionalParameter(path)
if (results) {
for (const p of results) {
this.node.insert(method, p, handler)
for (let i = 0, len = results.length; i < len; i++) {
this.node.insert(method, results[i], handler)
}
return
}
Expand Down

0 comments on commit 10a5e65

Please sign in to comment.