Skip to content

Commit

Permalink
πŸ“ chore(.eslintignore): ignore README.md file in eslint checks
Browse files Browse the repository at this point in the history
πŸ“ docs(README.md): add documentation for radix-rapid package and its usage
πŸ“Š chore(benchmark): add benchmarking scripts and results for radix-rapid package
The first commit adds an ignore rule for README.md file in eslint checks. The second commit adds documentation for the radix-rapid package and its usage. The third commit adds benchmarking scripts and results for the radix-rapid package.

πŸŽ‰ feat(http.mjs, utils.mjs): add benchmarking functionality to test server performance
This commit adds benchmarking functionality to the server to test its performance. The benchmarking script uses autocannon to send requests to the server and measure its performance. The benchmarking script is located in `http.mjs`, and the utility functions used by the script are located in `utils.mjs`. The `createServer` function creates a server instance and returns a listener and stats object. The `printEnv` function prints the environment details, and the `printStats` function prints the statistics of the benchmark. The `router` object is used to create routes for the server, and `benchSets` is an array of objects that contain the title of the benchmark and the requests to be sent to the server.
  • Loading branch information
nyxb committed May 19, 2023
1 parent 251c8ef commit 96f7d94
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dist
node_modules
README.md
156 changes: 156 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
[![cover][cover-src]][cover-href]
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![bundle][bundle-src]][bundle-href] [![JSDocs][jsdocs-src]][jsdocs-href]
[![License][license-src]][license-href]

# 🌳 radix-rapid

> ✨ Lightweight and fast router for JavaScript based on [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree)🌱.
## πŸ“ Usage

**Install:**

```sh
# nyxi
nyxi radix-rapid

# pnpm
pnpm i radix-rapid

# npm
npm i radix-rapid

# yarn
yarn add radix-rapid
```

**Import:**

```js
// ESM
import { createRouter } from 'radix-rapid'

// CJS
const { createRouter } = require('radix-rapid')
```

**Create a router instance and insert routes:**

```js
const router = createRouter(/* options */)

router.insert('/path', { payload: 'this path' })
router.insert('/path/:name', { payload: 'named route' })
router.insert('/path/foo/**', { payload: 'wildcard route' })
router.insert('/path/foo/**:name', { payload: 'named wildcard route' })
```

**Match route to access matched data:**

```js
router.lookup('/path')
// { payload: 'this path' }

router.lookup('/path/fooval')
// { payload: 'named route', params: { name: 'fooval' } }

router.lookup('/path/foo/bar/baz')
// { payload: 'wildcard route' }

router.lookup('/')
// null (no route matched for/)
```

## ⚑️ Methods

### βž• `router.insert(path, data)`

`path` can be static or using `:placeholder` or `**` for wildcard paths.

The `data` object will be returned on matching params. It should be an object like `{ handler }` and not containing reserved keyword `params`.

### πŸ” `router.lookup(path)`

Returns matched data for `path` with optional `params` key if mached route using placeholders.

### ❌ `router.remove(path)`

Remove route matching `path`.

## βš™οΈ Options

You can initialize router instance with options:

```ts
const router = createRouter({
strictTrailingSlash: true,
routes: {
'/foo': {}
}
})
```

- πŸ›£οΈ `routes`: An object specifying initial routes to add
- 🚦 `strictTrailingSlash`: By default, the router ignores trailing slashes for matching and adding routes. When set to `true`, matching with trailing slashes is handled differently.

### πŸ”Ž Route Matcher

Creates a multi matcher from router tree that can match **all routes** matching path:

```ts
import { createRouter, toRouteMatcher } from 'radix-rapid'

const router = createRouter({
routes: {
'/foo': { m: 'foo' }, // Matches /foo only
'/foo/**': { m: 'foo/**' }, // Matches /foo/<any>
'/foo/bar': { m: 'foo/bar' }, // Matches /foo/bar only
'/foo/bar/baz': { m: 'foo/bar/baz' }, // Matches /foo/bar/baz only
'/foo/*/baz': { m: 'foo/*/baz' } // Matches /foo/<any>/baz
}
})

const matcher = toRouteMatcher(router)

const matches = matcher.matchAll('/foo/bar/baz')

// [
// {
// "m": "foo/**",
// },
// {
// "m": "foo/*/baz",
// },
// {
// "m": "foo/bar/baz",
// },
// ]
```

## ⚑️ Performance

See [benchmark](./benchmark).


## πŸ“œ License

[MIT](./LICENSE) - Made with πŸ’ž

<!-- Badges -->

[npm-version-src]: https://img.shields.io/npm/v/radix-rapid?style=flat&colorA=18181B&colorB=14F195
[npm-version-href]: https://npmjs.com/package/radix-rapid
[npm-downloads-src]: https://img.shields.io/npm/dm/radix-rapid?style=flat&colorA=18181B&colorB=14F195
[npm-downloads-href]: https://npmjs.com/package/radix-rapid
[bundle-src]: https://img.shields.io/bundlephobia/minzip/radix-rapid?style=flat&colorA=18181B&colorB=14F195
[bundle-href]: https://bundlephobia.com/result?p=radix-rapid
[jsdocs-src]: https://img.shields.io/badge/jsDocs.io-reference-18181B?style=flat&colorA=18181B&colorB=14F195
[jsdocs-href]: https://www.jsdocs.io/package/radix-rapid
[license-src]: https://img.shields.io/github/license/nyxblabs/radix-rapid.svg?style=flat&colorA=18181B&colorB=14F195
[license-href]: https://github.com/nyxblabs/radix-rapid/blob/main/LICENSE

<!-- Cover -->
[cover-src]: https://raw.githubusercontent.com/nyxblabs/radix-rapid/main/.github/assets/cover-github-radix-rapid.png
[cover-href]: https://πŸ’»nyxb.ws
110 changes: 110 additions & 0 deletions benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# πŸ“Š Benchmark Results

Benchmarks are mainly focusing on benchmarking `lookup` method performance.

Below results are based on my personal PC using Windows 11. You can use provided scripts to test in your own env.

## ⚑️ Direct benchmark

Directly benchmarking `lookup` performance using [benchmark](https://www.npmjs.com/package/benchmark)

Scripts:
- πŸ‹οΈβ€β™€οΈ `nyxr bench`
- πŸ‹οΈβ€β™‚οΈ `nyxr bench:profile` (using [0x](https://www.npmjs.com/package/0x) to generate flamegraph)


```
--- πŸ§ͺ Test environment ---
Node.js version: 18.16.0
radix-rapid version: 0.0.1
OS: win32
CPU count: 8
Current load: [ 0, 0, 0 ]
--- 🚧 static route ---
lookup x 18,670,265 ops/sec Β±4.69% (76 runs sampled)
Stats:
- /choot: 96837315
--- πŸ”§ dynamic route ---
lookup x 403,374 ops/sec Β±3.18% (63 runs sampled)
Stats:
- /choot/123: 2065943
```

## ⚑️ HTTP Benchmark


Using [`autocannon`](https://github.com/mcollina/autocannon) and a simple http listener using lookup for realworld performance.

Scripts:
- πŸš€ `nyxr bench:http`

```
--- πŸ§ͺ Test environment ---
Node.js version: 18.16.0
radix-rapid version: 0.0.1
OS: win32
CPU count: 8
Current load: [ 0, 0, 0 ]
--- πŸ“Š Benchmark: static route ---
Running 10s test @ http://localhost:3000/
10 connections
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Stat β”‚ 2.5% β”‚ 50% β”‚ 97.5% β”‚ 99% β”‚ Avg β”‚ Stdev β”‚ Max β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Latency β”‚ 0 ms β”‚ 0 ms β”‚ 1 ms β”‚ 2 ms β”‚ 0.13 ms β”‚ 0.65 ms β”‚ 30 ms β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Stat β”‚ 1% β”‚ 2.5% β”‚ 50% β”‚ 97.5% β”‚ Avg β”‚ Stdev β”‚ Min β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Req/Sec β”‚ 9663 β”‚ 9663 β”‚ 17183 β”‚ 21935 β”‚ 15848.8 β”‚ 4391.92 β”‚ 9660 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Bytes/Sec β”‚ 1.35 MB β”‚ 1.35 MB β”‚ 2.41 MB β”‚ 3.07 MB β”‚ 2.22 MB β”‚ 615 kB β”‚ 1.35 MB β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Req/Bytes counts sampled once per second.
# of samples: 10
159k requests in 10.02s, 22.2 MB read
Stats:
- /choot: 158510
--- πŸ“Š Benchmark: dynamic route ---
Running 10s test @ http://localhost:3000/
10 connections
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Stat β”‚ 2.5% β”‚ 50% β”‚ 97.5% β”‚ 99% β”‚ Avg β”‚ Stdev β”‚ Max β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Latency β”‚ 0 ms β”‚ 0 ms β”‚ 1 ms β”‚ 2 ms β”‚ 0.14 ms β”‚ 0.56 ms β”‚ 17 ms β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Stat β”‚ 1% β”‚ 2.5% β”‚ 50% β”‚ 97.5% β”‚ Avg β”‚ Stdev β”‚ Min β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Req/Sec β”‚ 9663 β”‚ 9663 β”‚ 14935 β”‚ 17631 β”‚ 14243.6 β”‚ 2791.28 β”‚ 9660 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Bytes/Sec β”‚ 1.64 MB β”‚ 1.64 MB β”‚ 2.54 MB β”‚ 3 MB β”‚ 2.42 MB β”‚ 475 kB β”‚ 1.64 MB β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Req/Bytes counts sampled once per second.
# of samples: 10
142k requests in 10.01s, 24.2 MB read
Stats:
- /choot/123: 142410
```


29 changes: 29 additions & 0 deletions benchmark/direct.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable no-console */
import Benchmark from 'benchmark' // https://www.npmjs.com/package/benchmark'
import { benchSets, logSection, printEnv, printStats, router } from './utils.mjs'

async function main() {
printEnv()

for (const bench of benchSets) {
logSection(bench.title)
const suite = new Benchmark.Suite()
const stats = {}
suite.add('lookup', () => {
for (const req of bench.requests) {
const match = router.lookup(req.path)
if (!match)
stats[match] = (stats[match] || 0) + 1
stats[req.path] = (stats[req.path] || 0) + 1
}
})
// eslint-disable-next-line max-statements-per-line
suite.on('cycle', (event) => { console.log(String(event.target)) })
const promise = new Promise(resolve => suite.on('complete', () => resolve()))
suite.run({ async: true })
await promise
printStats(stats)
}
}

main().catch(console.error)
41 changes: 41 additions & 0 deletions benchmark/http.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import autocannon from 'autocannon' // https://github.com/mcollina/autocannon
import { listen } from 'earlist'
import { benchSets, logSection, printEnv, printStats, router } from './utils.mjs'

async function main() {
printEnv()

for (const bench of benchSets) {
logSection(`Benchmark: ${bench.title}`)
const { listener, stats } = await createServer()
const instance = autocannon({
url: listener.url,
requests: bench.requests,
})
autocannon.track(instance)
process.once('SIGINT', () => {
instance.stop()
listener.close()
process.exit(1)
})
await instance // Resolves to details results
printStats(stats)
await listener.close()
}
}

main().catch(console.error)

async function createServer() {
const stats = {}
const listener = await listen((req, res) => {
stats[req.url] = (stats[req.url] || 0) + 1
const match = router.lookup(req.url)
if (!match)
stats[match] = (stats[match] || 0) + 1

res.end(JSON.stringify((match || { error: 404 })))
}, { showURL: false })

return { listener, stats }
}
53 changes: 53 additions & 0 deletions benchmark/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* eslint-disable no-console */
import { readFileSync } from 'node:fs'
import os from 'node:os'
import { createRouter } from 'radix-rapid'

// eslint-disable-next-line max-statements-per-line
export function logSection(title) { console.log(`\n--- ${title} ---\n`) }

const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version

export function printEnv() {
logSection('Test environment')
console.log('Node.js version:', process.versions.node)
console.log('radix-rapid version:', pkgVersion)
console.log('OS:', os.platform())
console.log('CPU count:', os.cpus().length)
console.log('Current load:', os.loadavg())
console.log('')
}

export function printStats(stats) {
console.log(`Stats:\n${Object.entries(stats).map(([path, hits]) => ` - ${path}: ${hits}`).join('\n')}`)
}

export const router = createRouter({
routes: Object.fromEntries([
'/hello',
'/cool',
'/hi',
'/helium',
'/coooool',
'/chrome',
'/choot',
'/choot/:choo',
'/ui/**',
'/ui/components/**',
].map(path => [path, { path }])),
})

export const benchSets = [
{
title: 'static route',
requests: [
{ path: '/choot' },
],
},
{
title: 'dynamic route',
requests: [
{ path: '/choot/123' },
],
},
]

0 comments on commit 96f7d94

Please sign in to comment.