Skip to content

Commit 1439123

Browse files
authored
[JavaScript] Add HyperExpress (TechEmpower#8305)
* [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
1 parent 2cf9ff1 commit 1439123

File tree

12 files changed

+1095
-0
lines changed

12 files changed

+1095
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# HyperExpress Benchmarking Test
2+
3+
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)
4+
5+
µWebSockets.js is a web server bypass for Node.js (https://github.com/uNetworking/uWebSockets.js)
6+
7+
## Important Libraries
8+
9+
The tests were run with:
10+
11+
- [hyper-express](https://github.com/kartikk221/hyper-express)
12+
- [postgres](https://github.com/porsager/postgres)
13+
- [mysql2](https://github.com/sidorares/node-mysql2)
14+
- [lru-cache](https://github.com/isaacs/node-lru-cache)
15+
16+
## Database
17+
18+
There are individual handlers for each DB approach. The logic for each of them are found here:
19+
20+
- [Postgres](database/postgres.js)
21+
- [MySQL](database/mysql.js)
22+
23+
There are **no database endpoints** or drivers attached by default.
24+
25+
To initialize the application with one of these, run any _one_ of the following commands:
26+
27+
```sh
28+
$ DATABASE=postgres npm start
29+
$ DATABASE=mysql npm start
30+
```
31+
32+
## Test Endpoints
33+
34+
> Visit the test requirements [here](https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview)
35+
36+
```sh
37+
$ curl localhost:8080/json
38+
$ curl localhost:8080/plaintext
39+
40+
# The following are only available with the DATABASE env var
41+
42+
$ curl localhost:8080/db
43+
$ curl localhost:8080/fortunes
44+
45+
$ curl localhost:8080/queries?queries=2
46+
$ curl localhost:8080/queries?queries=0
47+
$ curl localhost:8080/queries?queries=foo
48+
$ curl localhost:8080/queries?queries=501
49+
$ curl localhost:8080/queries?queries=
50+
51+
$ curl localhost:8080/updates?queries=2
52+
$ curl localhost:8080/updates?queries=0
53+
$ curl localhost:8080/updates?queries=foo
54+
$ curl localhost:8080/updates?queries=501
55+
$ curl localhost:8080/updates?queries=
56+
57+
$ curl localhost:8080/cached-worlds?count=2
58+
$ curl localhost:8080/cached-worlds?count=0
59+
$ curl localhost:8080/cached-worlds?count=foo
60+
$ curl localhost:8080/cached-worlds?count=501
61+
$ curl localhost:8080/cached-worlds?count=
62+
```
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { escape } from 'html-escaper'
2+
import { Server } from 'hyper-express'
3+
import { LRUCache } from 'lru-cache'
4+
import cluster, { isWorker } from 'node:cluster'
5+
import { maxQuery, maxRows } from './config.js'
6+
const { DATABASE } = process.env
7+
const db = DATABASE ? await import(`./database/${DATABASE}.js`) : null
8+
9+
const generateRandomNumber = () => Math.ceil(Math.random() * maxRows)
10+
11+
const parseQueries = (i) => Math.min(Math.max(parseInt(i, 10) || 1, 1), maxQuery)
12+
13+
const cache = new LRUCache({
14+
max: maxRows
15+
})
16+
17+
const app = new Server()
18+
19+
// use middleware to add `Server` into response header
20+
app.use((_request, response, next) => {
21+
response.header('Server', 'hyperexpress')
22+
next()
23+
})
24+
25+
app.get('/plaintext', (_request, response) => {
26+
response.atomic(() => {
27+
response
28+
.type('text')
29+
.send('Hello, World!')
30+
})
31+
})
32+
33+
app.get('/json', (_request, response) => {
34+
response.json({ message: 'Hello, World!' })
35+
})
36+
37+
if (db) {
38+
// populate cache
39+
(async () => {
40+
const worlds = await db.getAllWorlds()
41+
for (let i = 0; i < worlds.length; i++) {
42+
cache.set(worlds[i].id, worlds[i])
43+
}
44+
})()
45+
46+
app.get('/db', async (_request, response) => {
47+
try {
48+
const world = await db.find(generateRandomNumber())
49+
response.json(world)
50+
} catch (error) {
51+
throw error
52+
}
53+
})
54+
55+
app.get('/queries', async (request, response) => {
56+
try {
57+
const queries = parseQueries(request.query.queries)
58+
const worldPromises = []
59+
60+
for (let i = 0; i < queries; i++) {
61+
worldPromises.push(db.find(generateRandomNumber()))
62+
}
63+
64+
const worlds = await Promise.all(worldPromises)
65+
response.json(worlds)
66+
} catch (error) {
67+
throw error
68+
}
69+
})
70+
71+
app.get('/updates', async (request, response) => {
72+
try {
73+
const queries = parseQueries(request.query.queries)
74+
const worldPromises = []
75+
76+
for (let i = 0; i < queries; i++) {
77+
worldPromises.push(db.find(generateRandomNumber()))
78+
}
79+
80+
const worlds = await Promise.all(worldPromises)
81+
82+
const updatedWorlds = await Promise.all(worlds.map(async (world) => {
83+
world.randomNumber = generateRandomNumber()
84+
await db.update(world)
85+
return world
86+
}))
87+
response.json(updatedWorlds)
88+
} catch (error) {
89+
throw error
90+
}
91+
})
92+
93+
app.get('/fortunes', async (_request, response) => {
94+
try {
95+
const fortunes = await db.fortunes()
96+
97+
fortunes.push({ id: 0, message: 'Additional fortune added at request time.' })
98+
99+
fortunes.sort((a, b) => a.message.localeCompare(b.message))
100+
101+
let i = 0, html = '<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>'
102+
for (; i < fortunes.length; i++) html += `<tr><td>${fortunes[i].id}</td><td>${escape(fortunes[i].message)}</td></tr>`
103+
html += '</table></body></html>'
104+
105+
response.atomic(() => {
106+
response
107+
// .type('html')
108+
.header('Content-Type', 'text/html; charset=utf-8')
109+
.send(html)
110+
})
111+
} catch (error) {
112+
throw error
113+
}
114+
})
115+
116+
app.get('/cached-worlds', async (request, response) => {
117+
try {
118+
const count = parseQueries(request.query.count)
119+
const worlds = []
120+
121+
for (let i = 0; i < count; i++) {
122+
worlds[i] = cache.get(generateRandomNumber())
123+
}
124+
125+
response.json(worlds)
126+
} catch (error) {
127+
throw error
128+
}
129+
})
130+
}
131+
132+
app.listen(8080).then(() => {
133+
console.log(`${isWorker ? `${cluster.worker.id}: ` : ''}Successfully bound to http://0.0.0.0:8080`)
134+
})
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"framework": "hyperexpress",
3+
"tests": [
4+
{
5+
"default": {
6+
"json_url": "/json",
7+
"plaintext_url": "/plaintext",
8+
"port": 8080,
9+
"approach": "Realistic",
10+
"classification": "Micro",
11+
"database": "None",
12+
"framework": "hyperexpress",
13+
"language": "JavaScript",
14+
"flavor": "None",
15+
"orm": "Raw",
16+
"platform": "NodeJS",
17+
"webserver": "µws",
18+
"os": "Linux",
19+
"database_os": "Linux",
20+
"display_name": "hyperexpress",
21+
"notes": "",
22+
"versus": "nodejs"
23+
},
24+
"postgres": {
25+
"db_url": "/db",
26+
"fortune_url": "/fortunes",
27+
"query_url": "/queries?queries=",
28+
"update_url": "/updates?queries=",
29+
"cached_query_url": "/cached-worlds?count=",
30+
"port": 8080,
31+
"approach": "Realistic",
32+
"classification": "Micro",
33+
"database": "Postgres",
34+
"framework": "hyperexpress",
35+
"language": "JavaScript",
36+
"flavor": "None",
37+
"orm": "Raw",
38+
"platform": "NodeJS",
39+
"webserver": "µws",
40+
"os": "Linux",
41+
"database_os": "Linux",
42+
"display_name": "hyperexpress",
43+
"notes": "",
44+
"versus": "nodejs"
45+
},
46+
"mysql": {
47+
"db_url": "/db",
48+
"fortune_url": "/fortunes",
49+
"query_url": "/queries?queries=",
50+
"update_url": "/updates?queries=",
51+
"cached_query_url": "/cached-worlds?count=",
52+
"port": 8080,
53+
"approach": "Realistic",
54+
"classification": "Micro",
55+
"database": "MySQL",
56+
"framework": "hyperexpress",
57+
"language": "JavaScript",
58+
"flavor": "None",
59+
"orm": "Raw",
60+
"platform": "NodeJS",
61+
"webserver": "µws",
62+
"os": "Linux",
63+
"database_os": "Linux",
64+
"display_name": "hyperexpress",
65+
"notes": "",
66+
"versus": "nodejs"
67+
}
68+
}
69+
]
70+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import cluster, { isPrimary, setupPrimary, fork } from 'node:cluster'
2+
import { cpus } from 'node:os'
3+
4+
if (isPrimary) {
5+
setupPrimary({
6+
exec: 'app.js',
7+
})
8+
cluster.on('exit', (worker) => {
9+
console.log(`worker ${worker.process.pid} died`)
10+
process.exit(1)
11+
})
12+
for (let i = 0; i < cpus().length; i++) {
13+
fork()
14+
}
15+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const maxQuery = 500
2+
export const maxRows = 10000
3+
export const clientOpts = {
4+
host: 'tfb-database',
5+
user: 'benchmarkdbuser',
6+
password: 'benchmarkdbpass',
7+
database: 'hello_world',
8+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createPool, createConnection } from 'mysql2/promise'
2+
import { isWorker } from 'node:cluster'
3+
import { cpus } from 'node:os'
4+
import { clientOpts } from '../config.js'
5+
6+
const client = await createConnection(clientOpts)
7+
8+
const res = await client.query('SHOW VARIABLES LIKE "max_connections"')
9+
10+
let maxConnections = 150
11+
12+
if (isWorker) {
13+
maxConnections = cpus().length > 2 ? Math.ceil(res[0][0].Value * 0.96 / cpus().length) : maxConnections
14+
}
15+
16+
await client.end()
17+
18+
const pool = createPool(Object.assign({ ...clientOpts }, {
19+
connectionLimit: maxConnections,
20+
idleTimeout: 600000
21+
}))
22+
23+
const execute = async (text, values) => (await pool.execute(text, values || undefined))[0]
24+
25+
export const fortunes = async () => execute('SELECT * FROM fortune')
26+
27+
export const find = async (id) => execute('SELECT id, randomNumber FROM world WHERE id = ?', [id]).then(arr => arr[0])
28+
29+
export const getAllWorlds = async () => execute('SELECT * FROM world')
30+
31+
export const update = async (obj) => execute('UPDATE world SET randomNumber = ? WHERE id = ?', [obj.randomNumber, obj.id])
32+
33+
await Promise.all([...Array(maxConnections).keys()].map(fortunes))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import postgres from 'postgres'
2+
import { clientOpts } from '../config.js'
3+
4+
const sql = postgres(clientOpts)
5+
6+
export const fortunes = async () => sql`SELECT * FROM fortune`
7+
8+
export const find = async (id) => sql`SELECT id, randomNumber FROM world WHERE id = ${id}`.then((arr) => arr[0])
9+
10+
export const getAllWorlds = async () => sql`SELECT * FROM world`
11+
12+
export const update = async (obj) => sql`UPDATE world SET randomNumber = ${obj.randomNumber} WHERE id = ${obj.id}`
13+
14+
await Promise.all([...Array(150).keys()].map(fortunes))
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# syntax=docker/dockerfile:1
2+
FROM node:18-slim
3+
4+
WORKDIR /app
5+
6+
COPY --chown=node:node . .
7+
8+
ENV NODE_ENV production
9+
10+
ENV DATABASE mysql
11+
12+
RUN npm install
13+
14+
USER node
15+
16+
EXPOSE 8080
17+
18+
CMD ["node", "clustered.js"]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# syntax=docker/dockerfile:1
2+
FROM node:18-slim
3+
4+
WORKDIR /app
5+
6+
COPY --chown=node:node . .
7+
8+
ENV NODE_ENV production
9+
10+
ENV DATABASE postgres
11+
12+
RUN npm install
13+
14+
USER node
15+
16+
EXPOSE 8080
17+
18+
CMD ["node", "clustered.js"]

0 commit comments

Comments
 (0)