Browse Source

Add uwebsocketsjs (#9189)

* Revert "Remove uwebsockets (#9160)"

This reverts commit 4b0a91f07147386c8f11b36b1410f00b34c7611c.

* Remove mysql

* Don't fetch types on connect
Rasmus Porsager 1 year ago
parent
commit
912fb48277

+ 52 - 0
frameworks/JavaScript/uwebsockets.js/README.md

@@ -0,0 +1,52 @@
+# uWebSockets.js Benchmarking Test
+
+uWebSockets is a web server written in C/C++ (https://github.com/uNetworking/uWebSockets)
+
+µWebSockets.js is a web server bypass for Node.js (https://github.com/uNetworking/uWebSockets.js)
+
+## Important Libraries
+
+The tests were run with:
+
+- [uWebSockets.js](https://github.com/uNetworking/uWebSockets.js/)
+- [postgres](https://github.com/porsager/postgres/)
+
+## Database
+
+There are individual handlers for each DB approach. The logic for each of them are found here:
+
+- [PostgreSQL](src/database/postgres.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
+```
+
+## 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/updates?queries=
+$ curl localhost:8080/updates?queries=2
+$ curl localhost:8080/updates?queries=1000
+$ curl localhost:8080/updates?queries=foo
+$ curl localhost:8080/updates?queries=0
+
+$ curl localhost:8080/queries?queries=
+$ curl localhost:8080/queries?queries=2
+$ curl localhost:8080/queries?queries=1000
+$ curl localhost:8080/queries?queries=foo
+$ curl localhost:8080/queries?queries=0
+```

+ 47 - 0
frameworks/JavaScript/uwebsockets.js/benchmark_config.json

@@ -0,0 +1,47 @@
+{
+  "framework": "uwebsockets.js",
+  "tests": [
+    {
+      "default": {
+        "approach": "Realistic",
+        "classification": "Platform",
+        "database": "None",
+        "database_os": "Linux",
+        "display_name": "uWebSockets.js",
+        "flavor": "NodeJS",
+        "framework": "uWebSockets.js",
+        "json_url": "/json",
+        "language": "JavaScript",
+        "notes": "",
+        "orm": "Raw",
+        "os": "Linux",
+        "plaintext_url": "/plaintext",
+        "platform": "nodejs",
+        "port": 8080,
+        "versus": "nodejs",
+        "webserver": "None"
+      },
+      "postgres": {
+        "approach": "Realistic",
+        "classification": "Platform",
+        "database": "Postgres",
+        "database_os": "Linux",
+        "db_url": "/db",
+        "display_name": "uWebSockets.js",
+        "flavor": "NodeJS",
+        "fortune_url": "/fortunes",
+        "framework": "uWebSockets.js",
+        "language": "JavaScript",
+        "notes": "",
+        "orm": "Raw",
+        "os": "Linux",
+        "platform": "None",
+        "port": 8080,
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "versus": "nodejs",
+        "webserver": "None"
+      }
+    }
+  ]
+}

+ 39 - 0
frameworks/JavaScript/uwebsockets.js/package-lock.json

@@ -0,0 +1,39 @@
+{
+  "name": "uwebsockets.js",
+  "version": "0.0.1",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "uwebsockets.js",
+      "version": "0.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "postgres": "3.4.4",
+        "slow-json-stringify": "^2.0.1",
+        "uWebSockets.js": "uNetworking/uWebSockets.js#v20.44.0"
+      }
+    },
+    "node_modules/postgres": {
+      "version": "3.4.4",
+      "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.4.tgz",
+      "integrity": "sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://github.com/sponsors/porsager"
+      }
+    },
+    "node_modules/slow-json-stringify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/slow-json-stringify/-/slow-json-stringify-2.0.1.tgz",
+      "integrity": "sha512-jqyzIqTaSkRGcWdWqjmOLKHZgOGUT71ZCTsvQu1xGu9Mqaod7O26y5FJJEmaUQhaTWh0bkXv2qqN0i+EQsD1jQ=="
+    },
+    "node_modules/uWebSockets.js": {
+      "version": "20.44.0",
+      "resolved": "git+ssh://[email protected]/uNetworking/uWebSockets.js.git#8fa05571bf6ea95be8966ad313d9d39453e381ae"
+    }
+  }
+}

+ 17 - 0
frameworks/JavaScript/uwebsockets.js/package.json

@@ -0,0 +1,17 @@
+{
+  "dependencies": {
+    "postgres": "3.4.4",
+    "slow-json-stringify": "^2.0.1",
+    "uWebSockets.js": "uNetworking/uWebSockets.js#v20.44.0"
+  },
+  "license": "MIT",
+  "main": "src/server.js",
+  "name": "uwebsockets.js",
+  "private": true,
+  "scripts": {
+    "dev": "node src/server.js",
+    "start": "node src/clustered.js"
+  },
+  "type": "module",
+  "version": "0.0.1"
+}

+ 23 - 0
frameworks/JavaScript/uwebsockets.js/src/clustered.js

@@ -0,0 +1,23 @@
+import cluster from "node:cluster";
+import os from "node:os";
+import process from "node:process";
+
+if (cluster.isPrimary) {
+  // Master Node
+  console.log(`Primary ${process.pid} is running`);
+
+  // Fork workers
+  const numCPUs = os.availableParallelism();
+  for (let i = 0; i < numCPUs; i++) {
+    cluster.fork();
+  }
+
+  cluster.on("exit", (worker) => {
+    console.log(`worker ${worker.process.pid} died`);
+    process.exit(1);
+  });
+} else {
+  // Cluster Node
+  await import("./server.js");
+  console.log(`Worker ${process.pid} started`);
+}

+ 18 - 0
frameworks/JavaScript/uwebsockets.js/src/database/postgres.js

@@ -0,0 +1,18 @@
+import postgres from "postgres";
+
+const sql = postgres({
+  host: "tfb-database",
+  user: "benchmarkdbuser",
+  password: "benchmarkdbpass",
+  database: "hello_world",
+  fetch_types: false,
+  max: 1
+});
+
+export const fortunes = async () => await sql`SELECT id, message FROM fortune`;
+
+export const find = async (id) => await sql`SELECT id, randomNumber FROM world WHERE id = ${id}`.then((arr) => arr[0]);
+
+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`;

+ 194 - 0
frameworks/JavaScript/uwebsockets.js/src/server.js

@@ -0,0 +1,194 @@
+import uWebSockets from "uWebSockets.js";
+import {
+  addBenchmarkHeaders,
+  generateRandomNumber,
+  getQueriesCount,
+  handleError,
+  escape,
+  jsonSerializer,
+  worldObjectSerializer,
+  sortByMessage
+} from "./utils.js";
+
+let db;
+const { DATABASE } = process.env;
+if (DATABASE) db = await import(`./database/${DATABASE}.js`);
+
+const webserver = uWebSockets.App();
+
+webserver.get("/plaintext", new uWebSockets.DeclarativeResponse()
+              .writeHeader("Server", "uWebSockets.js")
+              .writeHeader("Content-Type", "text/plain")
+              .end("Hello, World!")
+);
+
+webserver.get("/json", (response) => {
+  addBenchmarkHeaders(response);
+  response.writeHeader("Content-Type", "application/json");
+  // response.end(JSON.stringify({ message: "Hello, World!" }));
+  response.end(jsonSerializer({ message: "Hello, World!" }));
+});
+
+if (db) {
+  webserver.get("/db", async (response) => {
+    response.onAborted(() => {
+      response.aborted = true;
+    });
+
+    try {
+      const row = await db.find(generateRandomNumber());
+
+      if (response.aborted) {
+        return;
+      }
+
+      response.cork(() => {
+        addBenchmarkHeaders(response);
+        response.writeHeader("Content-Type", "application/json");
+        // response.end(JSON.stringify(rows));
+        response.end(worldObjectSerializer(row));
+      });
+    } catch (error) {
+      if (response.aborted) {
+        return;
+      }
+
+      handleError(error, response);
+    }
+  });
+
+  webserver.get("/queries", async (response, request) => {
+    response.onAborted(() => {
+      response.aborted = true;
+    });
+
+    try {
+      const queriesCount = getQueriesCount(request);
+
+      const databaseJobs = new Array(queriesCount);
+
+      for (let i = 0; i < queriesCount; i++) {
+        databaseJobs[i] = db.find(generateRandomNumber());
+      }
+
+      const worldObjects = await Promise.all(databaseJobs);
+
+      if (response.aborted) {
+        return;
+      }
+
+      response.cork(() => {
+        addBenchmarkHeaders(response);
+        response.writeHeader("Content-Type", "application/json");
+        response.end(JSON.stringify(worldObjects));
+      });
+    } catch (error) {
+      if (response.aborted) {
+        return;
+      }
+
+      handleError(error, response);
+    }
+  });
+  
+  const extra = { id: 0, message: "Additional fortune added at request time." };
+
+  webserver.get("/fortunes", async (response) => {
+    response.onAborted(() => {
+      response.aborted = true;
+    });
+
+    try {
+      const rows = [extra, ...await db.fortunes()];
+
+      if (response.aborted) {
+        return;
+      }
+
+      // rows.push({
+      //   id: 0,
+      //   message: "Additional fortune added at request time.",
+      // });
+
+      // rows.sort((a, b) => (a.message < b.message) ? -1 : 1);
+      sortByMessage(rows)
+
+      const n = rows.length
+
+      let html = "", i = 0;
+      for (; i < n; i++) {
+        html += `<tr><td>${rows[i].id}</td><td>${escape(rows[i].message)}</td></tr>`;
+      }
+
+      response.cork(() => {
+        addBenchmarkHeaders(response);
+        response.writeHeader("Content-Type", "text/html; charset=utf-8");
+        response.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) {
+      if (response.aborted) {
+        return;
+      }
+
+      handleError(error, response);
+    }
+  });
+
+  webserver.get("/updates", async (response, request) => {
+    response.onAborted(() => {
+      response.aborted = true;
+    });
+
+    try {
+      const queriesCount = getQueriesCount(request);
+
+      const databaseJobs = new Array(queriesCount);
+
+      for (let i = 0; i < queriesCount; i++) {
+        databaseJobs[i] = db.find(generateRandomNumber());
+      }
+
+      const worldObjects = await Promise.all(databaseJobs);
+
+      for (let i = 0; i < queriesCount; i++) {
+        worldObjects[i].randomNumber = generateRandomNumber();
+      }
+
+      await db.bulkUpdate(worldObjects);
+
+      if (response.aborted) {
+        return;
+      }
+
+      response.cork(() => {
+        addBenchmarkHeaders(response);
+        response.writeHeader("Content-Type", "application/json");
+        response.end(JSON.stringify(worldObjects));
+      });
+    } catch (error) {
+      if (response.aborted) {
+        return;
+      }
+
+      handleError(error, response);
+    }
+  });
+}
+
+webserver.any("/*", (response) => {
+  response.writeStatus("404 Not Found");
+  addBenchmarkHeaders(response);
+  response.writeHeader("Content-Type", "text/plain");
+  response.end("Not Found");
+});
+
+const host = process.env.HOST || "0.0.0.0";
+const port = parseInt(process.env.PORT || "8080");
+webserver.listen(host, port, (socket) => {
+  if (!socket) {
+    console.error(`Couldn't bind to http://${host}:${port}!`);
+    process.exit(1);
+  }
+
+  console.log(`Successfully bound to http://${host}:${port}.`);
+});

+ 86 - 0
frameworks/JavaScript/uwebsockets.js/src/utils.js

@@ -0,0 +1,86 @@
+import { sjs, attr } from 'slow-json-stringify'
+
+/**
+ * Add Benchmark HTTP response headers.
+ *
+ * Add HTTP response headers `Server` which is required by the test suite.
+ * Header `Date` is automatically added by uWebsockets
+ * https://github.com/uNetworking/uWebSockets/blob/master/src/HttpResponse.h#L78
+ * 
+ * https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview
+ *
+ * @param {import('uWebSockets.js').HttpResponse} response
+ */
+export function addBenchmarkHeaders(response) {
+  response.writeHeader("Server", "uWebSockets.js");
+}
+
+/**
+ * Handle error for response
+ *
+ * @param {Error} error
+ * @param {import('uWebSockets.js').HttpResponse} response
+ */
+export function handleError(error, response) {
+  console.error(error);
+  response.cork(() => {
+    addBenchmarkHeaders(response);
+    response.writeHeader("Content-Type", "text/plain");
+    response.end("Internal Server Error");
+  });
+}
+
+/**
+ * Get queries count
+ *
+ * @param {import('uWebSockets.js').HttpRequest} request
+ */
+export function getQueriesCount(request) {
+  return Math.min(parseInt(request.getQuery("queries")) || 1, 500);
+}
+
+/**
+ * Generate random number
+ *
+ */
+export function generateRandomNumber() {
+  return Math.ceil(Math.random() * 10000);
+}
+
+/**
+ * Escape unsafe HTML Code
+ *
+ */
+const escapeHTMLRules = { '&': '&#38;', '<': '&#60;', '>': '&#62;', '"': '&#34;', "'": '&#39;', '/': '&#47;' }
+
+const unsafeHTMLMatcher = /[&<>"'\/]/g
+
+export function escape(text) {
+  if (unsafeHTMLMatcher.test(text) === false) return text;
+  return text.replace(unsafeHTMLMatcher, function (m) { return escapeHTMLRules[m] || m; });
+}
+
+/**
+ * Using Slow json stringify module to get faster results 
+ */
+export const jsonSerializer = sjs({ message: attr("string")}); 
+export const worldObjectSerializer = sjs({ id: attr('number'), randomnumber: attr('number') });
+// export const worldObjectsSerializer = sjs({ rows: attr("array", worldObjectSerializer) });
+
+/**
+ * Using Sort method which is performant for the test scenario
+ * @returns 
+ */
+export function sortByMessage (arr) {
+  const n = arr.length
+  for (let i = 1; i < n; i++) {
+    const c = arr[i]
+    let j = i - 1
+    while ((j > -1) && (c.message < arr[j].message)) {
+      arr[j + 1] = arr[j]
+      j--
+    }
+    arr[j + 1] = c
+  }
+  return arr
+}

+ 12 - 0
frameworks/JavaScript/uwebsockets.js/uwebsockets.js-postgres.dockerfile

@@ -0,0 +1,12 @@
+FROM node:20-slim
+
+COPY ./ ./
+
+RUN npm install
+
+ENV NODE_ENV production
+ENV DATABASE postgres
+
+EXPOSE 8080
+
+CMD ["npm", "start"]

+ 11 - 0
frameworks/JavaScript/uwebsockets.js/uwebsockets.js.dockerfile

@@ -0,0 +1,11 @@
+FROM node:20-slim
+
+COPY ./ ./
+
+RUN npm install
+
+ENV NODE_ENV production
+
+EXPOSE 8080
+
+CMD ["npm", "start"]