Browse Source

[Hono] New JavaScript Node.js Framework Hono (#8610)

* Add new framework hono

* Fix content type plain text header
Aayush Kapoor 1 year ago
parent
commit
6e83f2a9c8

+ 50 - 0
frameworks/JavaScript/hono/README.md

@@ -0,0 +1,50 @@
+# Hono Benchmarking Test
+
+Hono - [炎] means flame🔥 in Japanese - is a small, simple, and ultrafast web framework for the Edges. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Lagon, AWS Lambda, Lambda@Edge, and Node.js. https://github.com/honojs/hono
+
+## Important Libraries
+
+The tests were run with:
+
+- [Hono](https://github.com/honojs/hono)
+- [Postgres.js](https://github.com/porsager/postgres/)
+
+## Database
+
+There are individual handlers for each DB approach. The logic for each of them are found here:
+
+- [PostgreSQL](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/hono/benchmark_config.json

@@ -0,0 +1,47 @@
+{
+  "framework": "hono",
+  "tests": [
+    {
+      "default": {
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "None",
+        "database_os": "Linux",
+        "display_name": "Hono",
+        "flavor": "NodeJS",
+        "framework": "Hono",
+        "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": "Hono",
+        "flavor": "NodeJS",
+        "fortune_url": "/fortunes",
+        "framework": "Hono",
+        "language": "JavaScript",
+        "notes": "",
+        "orm": "Raw",
+        "os": "Linux",
+        "platform": "None",
+        "port": 8080,
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "versus": "nodejs",
+        "webserver": "None"
+      }
+    }
+  ]
+}

+ 12 - 0
frameworks/JavaScript/hono/hono-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/hono/hono.dockerfile

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

+ 46 - 0
frameworks/JavaScript/hono/package-lock.json

@@ -0,0 +1,46 @@
+{
+  "name": "hono",
+  "version": "0.0.1",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "hono",
+      "version": "0.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "@hono/node-server": "^1.3.1",
+        "hono": "^3.10.4",
+        "postgres": "^3.4.3"
+      }
+    },
+    "node_modules/@hono/node-server": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.3.1.tgz",
+      "integrity": "sha512-eQBCDbH1Vv/TiYXNP8aGfJTuXi9xGhEd/EZg9u6dhr7zC5/WKKztcBmbrOTtixVBvvV6bfcay6KEginwiqHyXg==",
+      "engines": {
+        "node": ">=18.14.1"
+      }
+    },
+    "node_modules/hono": {
+      "version": "3.10.4",
+      "resolved": "https://registry.npmjs.org/hono/-/hono-3.10.4.tgz",
+      "integrity": "sha512-2LJd+a3qyvSuyFlyJSRN1CeH5wg6/Rjua/5L5gdT1W+4U7EUZtnHph74klbyysGg69sfZNXsIrR7PJWSjf2Vww==",
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/postgres": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.3.tgz",
+      "integrity": "sha512-iHJn4+M9vbTdHSdDzNkC0crHq+1CUdFhx+YqCE+SqWxPjm+Zu63jq7yZborOBF64c8pc58O5uMudyL1FQcHacA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://github.com/sponsors/porsager"
+      }
+    }
+  }
+}

+ 17 - 0
frameworks/JavaScript/hono/package.json

@@ -0,0 +1,17 @@
+{
+  "dependencies": {
+    "@hono/node-server": "^1.3.1",
+    "hono": "^3.10.4",
+    "postgres": "^3.4.3"
+  },
+  "license": "MIT",
+  "main": "src/server.js",
+  "name": "hono",
+  "private": true,
+  "scripts": {
+    "dev": "node src/server.js",
+    "start": "node src/clustered.js"
+  },
+  "type": "module",
+  "version": "0.0.1"
+}

+ 23 - 0
frameworks/JavaScript/hono/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`);
+}

+ 25 - 0
frameworks/JavaScript/hono/src/database/postgres.js

@@ -0,0 +1,25 @@
+import postgres from "postgres";
+
+const sql = postgres({
+  host: "tfb-database",
+  user: "benchmarkdbuser",
+  password: "benchmarkdbpass",
+  database: "hello_world",
+  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`;

+ 108 - 0
frameworks/JavaScript/hono/src/server.js

@@ -0,0 +1,108 @@
+import { serve } from "@hono/node-server";
+import { Hono } from "hono";
+import {
+  addBenchmarkHeaders,
+  escape,
+  generateRandomNumber,
+  getQueriesCount,
+  handleError,
+  sortByMessage,
+} from "./utils.js";
+
+let db;
+const { DATABASE } = process.env;
+if (DATABASE) db = await import(`./database/${DATABASE}.js`);
+
+const app = new Hono();
+
+app
+  .get("/plaintext", (c) => {
+    addBenchmarkHeaders(c);
+    return c.text("Hello, World!");
+  })
+  .get("/json", (c) => {
+    addBenchmarkHeaders(c);
+    return c.json({ message: "Hello, World!" });
+  });
+
+if (db) {
+  const extra = { id: 0, message: "Additional fortune added at request time." };
+
+  app
+    .get("/db", async (c) => {
+      const randomNumber = await db.find(generateRandomNumber());
+      addBenchmarkHeaders(c);
+      return c.json(randomNumber);
+    })
+    .get("/queries", async (c) => {
+      const queriesCount = getQueriesCount(c);
+
+      const databaseJobs = new Array(queriesCount);
+
+      for (let i = 0; i < queriesCount; i++) {
+        databaseJobs[i] = db.find(generateRandomNumber());
+      }
+
+      const worldObjects = await Promise.all(databaseJobs);
+
+      addBenchmarkHeaders(c);
+      return c.json(worldObjects);
+    })
+    .get("/fortunes", async (c) => {
+      const rows = [extra, ...(await db.fortunes())];
+
+      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>`;
+      }
+
+      addBenchmarkHeaders(c);
+      return c.html(
+        `<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>${html}</table></body></html>`
+      );
+    })
+    .get("/updates", async (c) => {
+      const queriesCount = getQueriesCount(c);
+
+      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);
+
+      addBenchmarkHeaders(c);
+      return c.json(worldObjects);
+    });
+}
+
+app
+  .all("/*", (c) => {
+    addBenchmarkHeaders(c);
+    return c.text("Not Found", 404);
+  })
+  .onError(handleError);
+
+const port = parseInt(process.env.PORT || "8080");
+const hostname = process.env.HOST || "0.0.0.0";
+serve({ fetch: app.fetch, hostname, port }, (info) => {
+  if (!info) {
+    console.error(`Couldn't bind to http://${hostname}:${port}!`);
+    process.exit(1);
+  }
+  console.log(`Successfully bound to http://${hostname}:${port}.`);
+});

+ 81 - 0
frameworks/JavaScript/hono/src/utils.js

@@ -0,0 +1,81 @@
+/**
+ * Add Benchmark HTTP response headers.
+ *
+ * Add HTTP response headers `Server` and `Date` which is required by the test suite.
+ *
+ * https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview
+ *
+ * @param {import('hono').Context} c
+ */
+export function addBenchmarkHeaders(c) {
+  c.header("Server", "Hono");
+}
+
+/**
+ * Handle error for response
+ *
+ * @param {Error} err
+ * @param {import('hono').Context} c
+ */
+export function handleError(err, c) {
+  console.error(err);
+  addBenchmarkHeaders(c);
+  c.text("Internal Server Error");
+}
+
+/**
+ * Get queries count
+ *
+ * @param {import('hono').Context} c
+ */
+export function getQueriesCount(c) {
+  return Math.min(parseInt(c.req.query("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 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;
+}