-
Notifications
You must be signed in to change notification settings - Fork 2k
feat: add ultimate express #9390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
NateBrady23
merged 15 commits into
TechEmpower:master
from
Pho3niX90:feature/add_uexpress
Dec 13, 2024
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
98c2a34
feat: add ultimate express
Pho3niX90 11e2bd7
feat: add ultimate express
Pho3niX90 d468cd8
refactor(express): simplify response handling by removing writeRespon…
Pho3niX90 16db140
fix: unique display names
Pho3niX90 96bba27
fix: names
Pho3niX90 b1ea37d
fix: add db/query/update/fortune urls
Pho3niX90 d45b608
fix: missing orm value
Pho3niX90 543092f
fixes
Pho3niX90 3017dc6
fixes
Pho3niX90 c752e2a
fixes
dimdenGD c89ade1
full refactor
dimdenGD fcdf324
fix etag and disable x-powered-by
dimdenGD 4fcc1db
generate json dynamically
dimdenGD b9b29fa
fix space
dimdenGD 8f5b177
use fjs
dimdenGD File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # UltimateExpress Benchmarking Test | ||
|
|
||
| The Ultimate Express. Fastest http server with full Express compatibility, based on [µWebSockets](https://github.com/uNetworking/uWebSockets.js). | ||
|
|
||
| ## Important Libraries | ||
|
|
||
| The tests were run with: | ||
|
|
||
| - [ultimate-express](https://github.com/dimdenGD/ultimate-express) | ||
| - [postgres](https://github.com/porsager/postgres) | ||
| - [mariadb](https://github.com/mariadb-corporation/mariadb-connector-nodejs) | ||
| - [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= | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| import express from 'ultimate-express'; | ||
| import { LRUCache } from 'lru-cache'; | ||
| import cluster, { isWorker } from 'node:cluster'; | ||
| import { maxQuery, maxRows } from './config.js'; | ||
| import fjs from 'fast-json-stringify'; | ||
|
|
||
| const { DATABASE } = process.env; | ||
| const db = DATABASE ? await import(`./database/${DATABASE}.js`) : null; | ||
|
|
||
| const jsonSerializer = fjs({ | ||
| type: 'object', | ||
| properties: { | ||
| message: { | ||
| type: 'string', | ||
| format: 'unsafe', | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| const generateRandomNumber = () => Math.floor(Math.random() * maxRows) + 1; | ||
|
|
||
| const parseQueries = (i) => Math.min(parseInt(i) || 1, maxQuery); | ||
|
|
||
| const escapeHTMLRules = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/' }; | ||
|
|
||
| const unsafeHTMLMatcher = /[&<>"'\/]/g; | ||
|
|
||
| const escapeHTMLCode = (text) => unsafeHTMLMatcher.test(text) ? text.replace(unsafeHTMLMatcher, function (m) { return escapeHTMLRules[m] || m; }) : text; | ||
|
|
||
| const cache = new LRUCache({ | ||
| max: maxRows | ||
| }); | ||
|
|
||
| const app = express(); | ||
| app.set("etag", false); | ||
| app.set("x-powered-by", false); | ||
|
|
||
| app.get('/plaintext', (req, res) => { | ||
| res.setHeader('Server', 'UltimateExpress'); | ||
| res.setHeader('Content-Type', 'text/plain'); | ||
| res.end('Hello, World!'); | ||
| }); | ||
|
|
||
| app.get('/json', (req, res) => { | ||
| res.setHeader('Server', 'UltimateExpress'); | ||
| res.setHeader('Content-Type', 'application/json'); | ||
| res.end(jsonSerializer({ message: "Hello, World!" })); | ||
| }); | ||
|
|
||
| if (db) { | ||
| app.get('/db', async (req, res) => { | ||
| res.setHeader('Server', 'UltimateExpress'); | ||
|
|
||
| try { | ||
| const world = await db.find(generateRandomNumber()); | ||
| res.json(world); | ||
| } catch (error) { | ||
| throw error; | ||
| } | ||
| }); | ||
|
|
||
| app.get('/queries', async (req, res) => { | ||
| res.setHeader('Server', 'UltimateExpress'); | ||
|
|
||
| try { | ||
| const queries = parseQueries(req.query.queries); | ||
| const worldPromises = new Array(queries); | ||
|
|
||
| for (let i = 0; i < queries; i++) { | ||
| worldPromises[i] = db.find(generateRandomNumber()); | ||
| } | ||
|
|
||
| const worlds = await Promise.all(worldPromises); | ||
|
|
||
| res.json(worlds); | ||
| } catch (error) { | ||
| throw error; | ||
| } | ||
| }) | ||
|
|
||
| app.get('/updates', async (req, res) => { | ||
| res.setHeader('Server', 'UltimateExpress'); | ||
|
|
||
| try { | ||
| const queries = parseQueries(req.query.queries); | ||
| const worldPromises = new Array(queries); | ||
|
|
||
| for (let i = 0; i < queries; i++) { | ||
| worldPromises[i] = db.find(generateRandomNumber()); | ||
| } | ||
|
|
||
| const worlds = await Promise.all(worldPromises); | ||
|
|
||
| for (let i = 0; i < queries; i++) { | ||
| worlds[i].randomNumber = generateRandomNumber(); | ||
| } | ||
|
|
||
| await db.bulkUpdate(worlds); | ||
|
|
||
| res.json(worlds); | ||
| } catch (error) { | ||
| throw error; | ||
| } | ||
| }) | ||
|
|
||
| app.get('/fortunes', async (req, res) => { | ||
| res.setHeader('Server', 'UltimateExpress'); | ||
|
|
||
| try { | ||
| const fortunes = await db.fortunes() | ||
|
|
||
| fortunes.push({ id: 0, message: 'Additional fortune added at request time.' }); | ||
|
|
||
| fortunes.sort((a, b) => (a.message < b.message) ? -1 : 1); | ||
|
|
||
| const n = fortunes.length | ||
|
|
||
| let i = 0, html = '' | ||
| for (; i < n; i++) html += `<tr><td>${fortunes[i].id}</td><td>${escapeHTMLCode(fortunes[i].message)}</td></tr>` | ||
|
|
||
| res | ||
| .header('Content-Type', 'text/html; charset=utf-8') | ||
| .end(`<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>${html}</table></body></html>`); | ||
| } catch (error) { | ||
| throw error; | ||
| } | ||
| }) | ||
|
|
||
| let isCachePopulated = false | ||
| app.get('/cached-worlds', async (req, res) => { | ||
| res.setHeader('Server', 'UltimateExpress'); | ||
|
|
||
| try { | ||
| if (!isCachePopulated) { | ||
| const worlds = await db.getAllWorlds(); | ||
| for (let i = 0; i < worlds.length; i++) { | ||
| cache.set(worlds[i].id, worlds[i]); | ||
| } | ||
| isCachePopulated = true; | ||
| } | ||
| const count = parseQueries(req.query.count); | ||
| const worlds = new Array(count); | ||
|
|
||
| for (let i = 0; i < count; i++) { | ||
| worlds[i] = cache.get(generateRandomNumber()); | ||
| } | ||
|
|
||
| res.json(worlds); | ||
| } catch (error) { | ||
| throw error; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| app.listen(8080, () => { | ||
| console.log(`${isWorker ? `${cluster.worker.id}: ` : ''}Successfully bound to http://0.0.0.0:8080`); | ||
| }); | ||
70 changes: 70 additions & 0 deletions
70
frameworks/JavaScript/ultimate-express/benchmark_config.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| { | ||
| "framework": "ultimate-express", | ||
| "tests": [ | ||
| { | ||
| "default": { | ||
| "json_url": "/json", | ||
| "plaintext_url": "/plaintext", | ||
| "port": 8080, | ||
| "approach": "Realistic", | ||
| "classification": "Micro", | ||
| "database": "None", | ||
| "framework": "ultimate-express", | ||
| "language": "JavaScript", | ||
| "flavor": "None", | ||
| "orm": "Raw", | ||
| "platform": "NodeJS", | ||
| "webserver": "µws", | ||
| "os": "Linux", | ||
| "database_os": "Linux", | ||
| "display_name": "ultimate-express", | ||
| "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": "ultimate-express", | ||
| "language": "JavaScript", | ||
| "flavor": "None", | ||
| "orm": "Raw", | ||
| "platform": "NodeJS", | ||
| "webserver": "µws", | ||
| "os": "Linux", | ||
| "database_os": "Linux", | ||
| "display_name": "ultimate-express-postgres", | ||
| "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": "ultimate-express", | ||
| "language": "JavaScript", | ||
| "flavor": "None", | ||
| "orm": "Raw", | ||
| "platform": "NodeJS", | ||
| "webserver": "µws", | ||
| "os": "Linux", | ||
| "database_os": "Linux", | ||
| "display_name": "ultimate-express-mysql", | ||
| "notes": "", | ||
| "versus": "nodejs" | ||
| } | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { createPool } from 'mariadb' | ||
| import { cpus } from 'node:os' | ||
| import { clientOpts } from '../config.js' | ||
|
|
||
| const pool = createPool({ ...clientOpts, connectionLimit: cpus().length }) | ||
|
|
||
| const execute = (text, values) => pool.execute(text, values || undefined) | ||
|
|
||
| export const fortunes = () => execute('SELECT id, message FROM fortune') | ||
|
|
||
| export const find = (id) => execute('SELECT id, randomNumber FROM world WHERE id = ?', [id]).then(arr => arr[0]) | ||
|
|
||
| export const getAllWorlds = () => execute('SELECT id, randomNumber FROM world') | ||
|
|
||
| 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)) |
14 changes: 14 additions & 0 deletions
14
frameworks/JavaScript/ultimate-express/database/postgres.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, max: 1 }) | ||
|
|
||
| export const fortunes = async () => sql`SELECT id, message 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 id, randomNumber FROM world` | ||
|
|
||
| export const bulkUpdate = async (worlds) => await sql`UPDATE world SET randomNumber = (update_data.randomNumber)::int | ||
| FROM (VALUES ${sql(worlds.map(world => [world.id, world.randomNumber]).sort((a, b) => (a[0] < b[0]) ? -1 : 1))}) AS update_data (id, randomNumber) | ||
| WHERE world.id = (update_data.id)::int`; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be optimized by populating on test init and not on first call, eg:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Benchmark has a warm-up period, so it doesn't matter.