Skip to content

Commit f749c9f

Browse files
Pho3niX90dimdenGD
andauthored
feat: add ultimate express (#9390)
* feat: add ultimate express * feat: add ultimate express * refactor(express): simplify response handling by removing writeResponse helper - removed writeResponse function and headerTypes constant for direct header and response handling - refactored routes to set headers and send responses inline, making code more declarative - improved readability and reduced abstraction in response handling for each route * fix: unique display names * fix: names * fix: add db/query/update/fortune urls * fix: missing orm value * fixes * fixes * fixes * full refactor * fix etag and disable x-powered-by * generate json dynamically * fix space * use fjs --------- Co-authored-by: dimden <[email protected]>
1 parent f786fb5 commit f749c9f

File tree

12 files changed

+1175
-0
lines changed

12 files changed

+1175
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# UltimateExpress Benchmarking Test
2+
3+
The Ultimate Express. Fastest http server with full Express compatibility, based on [µWebSockets](https://github.com/uNetworking/uWebSockets.js).
4+
5+
## Important Libraries
6+
7+
The tests were run with:
8+
9+
- [ultimate-express](https://github.com/dimdenGD/ultimate-express)
10+
- [postgres](https://github.com/porsager/postgres)
11+
- [mariadb](https://github.com/mariadb-corporation/mariadb-connector-nodejs)
12+
- [lru-cache](https://github.com/isaacs/node-lru-cache)
13+
14+
## Database
15+
16+
There are individual handlers for each DB approach. The logic for each of them are found here:
17+
18+
- [Postgres](database/postgres.js)
19+
- [MySQL](database/mysql.js)
20+
21+
There are **no database endpoints** or drivers attached by default.
22+
23+
To initialize the application with one of these, run any _one_ of the following commands:
24+
25+
```sh
26+
$ DATABASE=postgres npm start
27+
$ DATABASE=mysql npm start
28+
```
29+
30+
## Test Endpoints
31+
32+
> Visit the test requirements [here](https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview)
33+
34+
```sh
35+
$ curl localhost:8080/json
36+
$ curl localhost:8080/plaintext
37+
38+
# The following are only available with the DATABASE env var
39+
40+
$ curl localhost:8080/db
41+
$ curl localhost:8080/fortunes
42+
43+
$ curl localhost:8080/queries?queries=2
44+
$ curl localhost:8080/queries?queries=0
45+
$ curl localhost:8080/queries?queries=foo
46+
$ curl localhost:8080/queries?queries=501
47+
$ curl localhost:8080/queries?queries=
48+
49+
$ curl localhost:8080/updates?queries=2
50+
$ curl localhost:8080/updates?queries=0
51+
$ curl localhost:8080/updates?queries=foo
52+
$ curl localhost:8080/updates?queries=501
53+
$ curl localhost:8080/updates?queries=
54+
55+
$ curl localhost:8080/cached-worlds?count=2
56+
$ curl localhost:8080/cached-worlds?count=0
57+
$ curl localhost:8080/cached-worlds?count=foo
58+
$ curl localhost:8080/cached-worlds?count=501
59+
$ curl localhost:8080/cached-worlds?count=
60+
```
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import express from 'ultimate-express';
2+
import { LRUCache } from 'lru-cache';
3+
import cluster, { isWorker } from 'node:cluster';
4+
import { maxQuery, maxRows } from './config.js';
5+
import fjs from 'fast-json-stringify';
6+
7+
const { DATABASE } = process.env;
8+
const db = DATABASE ? await import(`./database/${DATABASE}.js`) : null;
9+
10+
const jsonSerializer = fjs({
11+
type: 'object',
12+
properties: {
13+
message: {
14+
type: 'string',
15+
format: 'unsafe',
16+
}
17+
}
18+
});
19+
20+
const generateRandomNumber = () => Math.floor(Math.random() * maxRows) + 1;
21+
22+
const parseQueries = (i) => Math.min(parseInt(i) || 1, maxQuery);
23+
24+
const escapeHTMLRules = { '&': '&#38;', '<': '&#60;', '>': '&#62;', '"': '&#34;', "'": '&#39;', '/': '&#47;' };
25+
26+
const unsafeHTMLMatcher = /[&<>"'\/]/g;
27+
28+
const escapeHTMLCode = (text) => unsafeHTMLMatcher.test(text) ? text.replace(unsafeHTMLMatcher, function (m) { return escapeHTMLRules[m] || m; }) : text;
29+
30+
const cache = new LRUCache({
31+
max: maxRows
32+
});
33+
34+
const app = express();
35+
app.set("etag", false);
36+
app.set("x-powered-by", false);
37+
38+
app.get('/plaintext', (req, res) => {
39+
res.setHeader('Server', 'UltimateExpress');
40+
res.setHeader('Content-Type', 'text/plain');
41+
res.end('Hello, World!');
42+
});
43+
44+
app.get('/json', (req, res) => {
45+
res.setHeader('Server', 'UltimateExpress');
46+
res.setHeader('Content-Type', 'application/json');
47+
res.end(jsonSerializer({ message: "Hello, World!" }));
48+
});
49+
50+
if (db) {
51+
app.get('/db', async (req, res) => {
52+
res.setHeader('Server', 'UltimateExpress');
53+
54+
try {
55+
const world = await db.find(generateRandomNumber());
56+
res.json(world);
57+
} catch (error) {
58+
throw error;
59+
}
60+
});
61+
62+
app.get('/queries', async (req, res) => {
63+
res.setHeader('Server', 'UltimateExpress');
64+
65+
try {
66+
const queries = parseQueries(req.query.queries);
67+
const worldPromises = new Array(queries);
68+
69+
for (let i = 0; i < queries; i++) {
70+
worldPromises[i] = db.find(generateRandomNumber());
71+
}
72+
73+
const worlds = await Promise.all(worldPromises);
74+
75+
res.json(worlds);
76+
} catch (error) {
77+
throw error;
78+
}
79+
})
80+
81+
app.get('/updates', async (req, res) => {
82+
res.setHeader('Server', 'UltimateExpress');
83+
84+
try {
85+
const queries = parseQueries(req.query.queries);
86+
const worldPromises = new Array(queries);
87+
88+
for (let i = 0; i < queries; i++) {
89+
worldPromises[i] = db.find(generateRandomNumber());
90+
}
91+
92+
const worlds = await Promise.all(worldPromises);
93+
94+
for (let i = 0; i < queries; i++) {
95+
worlds[i].randomNumber = generateRandomNumber();
96+
}
97+
98+
await db.bulkUpdate(worlds);
99+
100+
res.json(worlds);
101+
} catch (error) {
102+
throw error;
103+
}
104+
})
105+
106+
app.get('/fortunes', async (req, res) => {
107+
res.setHeader('Server', 'UltimateExpress');
108+
109+
try {
110+
const fortunes = await db.fortunes()
111+
112+
fortunes.push({ id: 0, message: 'Additional fortune added at request time.' });
113+
114+
fortunes.sort((a, b) => (a.message < b.message) ? -1 : 1);
115+
116+
const n = fortunes.length
117+
118+
let i = 0, html = ''
119+
for (; i < n; i++) html += `<tr><td>${fortunes[i].id}</td><td>${escapeHTMLCode(fortunes[i].message)}</td></tr>`
120+
121+
res
122+
.header('Content-Type', 'text/html; charset=utf-8')
123+
.end(`<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>${html}</table></body></html>`);
124+
} catch (error) {
125+
throw error;
126+
}
127+
})
128+
129+
let isCachePopulated = false
130+
app.get('/cached-worlds', async (req, res) => {
131+
res.setHeader('Server', 'UltimateExpress');
132+
133+
try {
134+
if (!isCachePopulated) {
135+
const worlds = await db.getAllWorlds();
136+
for (let i = 0; i < worlds.length; i++) {
137+
cache.set(worlds[i].id, worlds[i]);
138+
}
139+
isCachePopulated = true;
140+
}
141+
const count = parseQueries(req.query.count);
142+
const worlds = new Array(count);
143+
144+
for (let i = 0; i < count; i++) {
145+
worlds[i] = cache.get(generateRandomNumber());
146+
}
147+
148+
res.json(worlds);
149+
} catch (error) {
150+
throw error;
151+
}
152+
});
153+
}
154+
155+
app.listen(8080, () => {
156+
console.log(`${isWorker ? `${cluster.worker.id}: ` : ''}Successfully bound to http://0.0.0.0:8080`);
157+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"framework": "ultimate-express",
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": "ultimate-express",
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": "ultimate-express",
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": "ultimate-express",
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": "ultimate-express-postgres",
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": "ultimate-express",
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": "ultimate-express-mysql",
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createPool } from 'mariadb'
2+
import { cpus } from 'node:os'
3+
import { clientOpts } from '../config.js'
4+
5+
const pool = createPool({ ...clientOpts, connectionLimit: cpus().length })
6+
7+
const execute = (text, values) => pool.execute(text, values || undefined)
8+
9+
export const fortunes = () => execute('SELECT id, message FROM fortune')
10+
11+
export const find = (id) => execute('SELECT id, randomNumber FROM world WHERE id = ?', [id]).then(arr => arr[0])
12+
13+
export const getAllWorlds = () => execute('SELECT id, randomNumber FROM world')
14+
15+
export const bulkUpdate = (worlds) => pool.batch('UPDATE world SET randomNumber = ? WHERE id = ?', worlds.map(world => [world.randomNumber, world.id]).sort((a, b) => (a[1] < b[1]) ? -1 : 1))
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, max: 1 })
5+
6+
export const fortunes = async () => sql`SELECT id, message 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 id, randomNumber FROM world`
11+
12+
export const bulkUpdate = async (worlds) => await sql`UPDATE world SET randomNumber = (update_data.randomNumber)::int
13+
FROM (VALUES ${sql(worlds.map(world => [world.id, world.randomNumber]).sort((a, b) => (a[0] < b[0]) ? -1 : 1))}) AS update_data (id, randomNumber)
14+
WHERE world.id = (update_data.id)::int`;

0 commit comments

Comments
 (0)