Skip to content

Commit

Permalink
[JavaScript] Add HyperExpress (TechEmpower#8305)
Browse files Browse the repository at this point in the history
* [JavaScript] Add HyperExpress

* [HyperExpress] Create readme.md

* Rename readme.md to README.md

* [HyperExpress] Tidy up codes

* [HyperExpress] Add mysql

* [HyperExpress] Tweaking github actions for db pool max connections

* [HyperExpress] Instantiating json for each request (#1)

* [HyperExpress] Instantiating json for each request

* [HyperExpress] add max connections to postgres

* [HyperExpress] Removing postgres pool max connections

* [HyperExpress] Fix starting app in single instance

* [HyperExpress] Remove unused scripts

* [HyperExpress] Removing postgres pool max connections
  • Loading branch information
masfahru authored Jul 12, 2023
1 parent 2cf9ff1 commit 1439123
Show file tree
Hide file tree
Showing 12 changed files with 1,095 additions and 0 deletions.
62 changes: 62 additions & 0 deletions frameworks/JavaScript/hyperexpress/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# HyperExpress Benchmarking Test

HyperExpress is a high performance Node.js webserver with a simple-to-use API powered by µWebSockets.js under the hood. (https://github.com/kartikk221/hyper-express)

µWebSockets.js is a web server bypass for Node.js (https://github.com/uNetworking/uWebSockets.js)

## Important Libraries

The tests were run with:

- [hyper-express](https://github.com/kartikk221/hyper-express)
- [postgres](https://github.com/porsager/postgres)
- [mysql2](https://github.com/sidorares/node-mysql2)
- [lru-cache](https://github.com/isaacs/node-lru-cache)

## Database

There are individual handlers for each DB approach. The logic for each of them are found here:

- [Postgres](database/postgres.js)
- [MySQL](database/mysql.js)

There are **no database endpoints** or drivers attached by default.

To initialize the application with one of these, run any _one_ of the following commands:

```sh
$ DATABASE=postgres npm start
$ DATABASE=mysql npm start
```

## Test Endpoints

> Visit the test requirements [here](https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview)
```sh
$ curl localhost:8080/json
$ curl localhost:8080/plaintext

# The following are only available with the DATABASE env var

$ curl localhost:8080/db
$ curl localhost:8080/fortunes

$ curl localhost:8080/queries?queries=2
$ curl localhost:8080/queries?queries=0
$ curl localhost:8080/queries?queries=foo
$ curl localhost:8080/queries?queries=501
$ curl localhost:8080/queries?queries=

$ curl localhost:8080/updates?queries=2
$ curl localhost:8080/updates?queries=0
$ curl localhost:8080/updates?queries=foo
$ curl localhost:8080/updates?queries=501
$ curl localhost:8080/updates?queries=

$ curl localhost:8080/cached-worlds?count=2
$ curl localhost:8080/cached-worlds?count=0
$ curl localhost:8080/cached-worlds?count=foo
$ curl localhost:8080/cached-worlds?count=501
$ curl localhost:8080/cached-worlds?count=
```
134 changes: 134 additions & 0 deletions frameworks/JavaScript/hyperexpress/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { escape } from 'html-escaper'
import { Server } from 'hyper-express'
import { LRUCache } from 'lru-cache'
import cluster, { isWorker } from 'node:cluster'
import { maxQuery, maxRows } from './config.js'
const { DATABASE } = process.env
const db = DATABASE ? await import(`./database/${DATABASE}.js`) : null

const generateRandomNumber = () => Math.ceil(Math.random() * maxRows)

const parseQueries = (i) => Math.min(Math.max(parseInt(i, 10) || 1, 1), maxQuery)

const cache = new LRUCache({
max: maxRows
})

const app = new Server()

// use middleware to add `Server` into response header
app.use((_request, response, next) => {
response.header('Server', 'hyperexpress')
next()
})

app.get('/plaintext', (_request, response) => {
response.atomic(() => {
response
.type('text')
.send('Hello, World!')
})
})

app.get('/json', (_request, response) => {
response.json({ message: 'Hello, World!' })
})

if (db) {
// populate cache
(async () => {
const worlds = await db.getAllWorlds()
for (let i = 0; i < worlds.length; i++) {
cache.set(worlds[i].id, worlds[i])
}
})()

app.get('/db', async (_request, response) => {
try {
const world = await db.find(generateRandomNumber())
response.json(world)
} catch (error) {
throw error
}
})

app.get('/queries', async (request, response) => {
try {
const queries = parseQueries(request.query.queries)
const worldPromises = []

for (let i = 0; i < queries; i++) {
worldPromises.push(db.find(generateRandomNumber()))
}

const worlds = await Promise.all(worldPromises)
response.json(worlds)
} catch (error) {
throw error
}
})

app.get('/updates', async (request, response) => {
try {
const queries = parseQueries(request.query.queries)
const worldPromises = []

for (let i = 0; i < queries; i++) {
worldPromises.push(db.find(generateRandomNumber()))
}

const worlds = await Promise.all(worldPromises)

const updatedWorlds = await Promise.all(worlds.map(async (world) => {
world.randomNumber = generateRandomNumber()
await db.update(world)
return world
}))
response.json(updatedWorlds)
} catch (error) {
throw error
}
})

app.get('/fortunes', async (_request, response) => {
try {
const fortunes = await db.fortunes()

fortunes.push({ id: 0, message: 'Additional fortune added at request time.' })

fortunes.sort((a, b) => a.message.localeCompare(b.message))

let i = 0, html = '<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>'
for (; i < fortunes.length; i++) html += `<tr><td>${fortunes[i].id}</td><td>${escape(fortunes[i].message)}</td></tr>`
html += '</table></body></html>'

response.atomic(() => {
response
// .type('html')
.header('Content-Type', 'text/html; charset=utf-8')
.send(html)
})
} catch (error) {
throw error
}
})

app.get('/cached-worlds', async (request, response) => {
try {
const count = parseQueries(request.query.count)
const worlds = []

for (let i = 0; i < count; i++) {
worlds[i] = cache.get(generateRandomNumber())
}

response.json(worlds)
} catch (error) {
throw error
}
})
}

app.listen(8080).then(() => {
console.log(`${isWorker ? `${cluster.worker.id}: ` : ''}Successfully bound to http://0.0.0.0:8080`)
})
70 changes: 70 additions & 0 deletions frameworks/JavaScript/hyperexpress/benchmark_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"framework": "hyperexpress",
"tests": [
{
"default": {
"json_url": "/json",
"plaintext_url": "/plaintext",
"port": 8080,
"approach": "Realistic",
"classification": "Micro",
"database": "None",
"framework": "hyperexpress",
"language": "JavaScript",
"flavor": "None",
"orm": "Raw",
"platform": "NodeJS",
"webserver": "µws",
"os": "Linux",
"database_os": "Linux",
"display_name": "hyperexpress",
"notes": "",
"versus": "nodejs"
},
"postgres": {
"db_url": "/db",
"fortune_url": "/fortunes",
"query_url": "/queries?queries=",
"update_url": "/updates?queries=",
"cached_query_url": "/cached-worlds?count=",
"port": 8080,
"approach": "Realistic",
"classification": "Micro",
"database": "Postgres",
"framework": "hyperexpress",
"language": "JavaScript",
"flavor": "None",
"orm": "Raw",
"platform": "NodeJS",
"webserver": "µws",
"os": "Linux",
"database_os": "Linux",
"display_name": "hyperexpress",
"notes": "",
"versus": "nodejs"
},
"mysql": {
"db_url": "/db",
"fortune_url": "/fortunes",
"query_url": "/queries?queries=",
"update_url": "/updates?queries=",
"cached_query_url": "/cached-worlds?count=",
"port": 8080,
"approach": "Realistic",
"classification": "Micro",
"database": "MySQL",
"framework": "hyperexpress",
"language": "JavaScript",
"flavor": "None",
"orm": "Raw",
"platform": "NodeJS",
"webserver": "µws",
"os": "Linux",
"database_os": "Linux",
"display_name": "hyperexpress",
"notes": "",
"versus": "nodejs"
}
}
]
}
15 changes: 15 additions & 0 deletions frameworks/JavaScript/hyperexpress/clustered.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import cluster, { isPrimary, setupPrimary, fork } from 'node:cluster'
import { cpus } from 'node:os'

if (isPrimary) {
setupPrimary({
exec: 'app.js',
})
cluster.on('exit', (worker) => {
console.log(`worker ${worker.process.pid} died`)
process.exit(1)
})
for (let i = 0; i < cpus().length; i++) {
fork()
}
}
8 changes: 8 additions & 0 deletions frameworks/JavaScript/hyperexpress/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const maxQuery = 500
export const maxRows = 10000
export const clientOpts = {
host: 'tfb-database',
user: 'benchmarkdbuser',
password: 'benchmarkdbpass',
database: 'hello_world',
}
33 changes: 33 additions & 0 deletions frameworks/JavaScript/hyperexpress/database/mysql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createPool, createConnection } from 'mysql2/promise'
import { isWorker } from 'node:cluster'
import { cpus } from 'node:os'
import { clientOpts } from '../config.js'

const client = await createConnection(clientOpts)

const res = await client.query('SHOW VARIABLES LIKE "max_connections"')

let maxConnections = 150

if (isWorker) {
maxConnections = cpus().length > 2 ? Math.ceil(res[0][0].Value * 0.96 / cpus().length) : maxConnections
}

await client.end()

const pool = createPool(Object.assign({ ...clientOpts }, {
connectionLimit: maxConnections,
idleTimeout: 600000
}))

const execute = async (text, values) => (await pool.execute(text, values || undefined))[0]

export const fortunes = async () => execute('SELECT * FROM fortune')

export const find = async (id) => execute('SELECT id, randomNumber FROM world WHERE id = ?', [id]).then(arr => arr[0])

export const getAllWorlds = async () => execute('SELECT * FROM world')

export const update = async (obj) => execute('UPDATE world SET randomNumber = ? WHERE id = ?', [obj.randomNumber, obj.id])

await Promise.all([...Array(maxConnections).keys()].map(fortunes))
14 changes: 14 additions & 0 deletions frameworks/JavaScript/hyperexpress/database/postgres.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import postgres from 'postgres'
import { clientOpts } from '../config.js'

const sql = postgres(clientOpts)

export const fortunes = async () => sql`SELECT * FROM fortune`

export const find = async (id) => sql`SELECT id, randomNumber FROM world WHERE id = ${id}`.then((arr) => arr[0])

export const getAllWorlds = async () => sql`SELECT * FROM world`

export const update = async (obj) => sql`UPDATE world SET randomNumber = ${obj.randomNumber} WHERE id = ${obj.id}`

await Promise.all([...Array(150).keys()].map(fortunes))
18 changes: 18 additions & 0 deletions frameworks/JavaScript/hyperexpress/hyperexpress-mysql.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# syntax=docker/dockerfile:1
FROM node:18-slim

WORKDIR /app

COPY --chown=node:node . .

ENV NODE_ENV production

ENV DATABASE mysql

RUN npm install

USER node

EXPOSE 8080

CMD ["node", "clustered.js"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# syntax=docker/dockerfile:1
FROM node:18-slim

WORKDIR /app

COPY --chown=node:node . .

ENV NODE_ENV production

ENV DATABASE postgres

RUN npm install

USER node

EXPOSE 8080

CMD ["node", "clustered.js"]
Loading

0 comments on commit 1439123

Please sign in to comment.