Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions frameworks/hono/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:22-slim
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json .
RUN npm install --omit=dev
COPY server.js .
ENV NODE_ENV=production
EXPOSE 8080
CMD ["node", "server.js"]
16 changes: 16 additions & 0 deletions frameworks/hono/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Hono

[Hono](https://github.com/honojs/hono) is an ultrafast, lightweight web framework built on Web Standards. It runs on any JavaScript runtime — Node.js, Bun, Deno, Cloudflare Workers, and more.

## Implementation Notes

- Uses `@hono/node-server` for the Node.js runtime adapter
- Cluster mode with one worker per CPU core
- `better-sqlite3` for the /db endpoint with mmap enabled
- Manual gzip compression (zlib level 1) for /compression
- HTTP/2 via native `http2` module on port 8443
- Hono's RegExpRouter provides fast pattern matching

## Why Hono?

Hono is one of the fastest-growing JS frameworks (~29k stars). Its key differentiator is being built entirely on Web Standards (Request/Response), making it portable across runtimes. Running it on Node.js via `@hono/node-server` shows how the Web Standards abstraction layer performs compared to native Node.js APIs (Fastify, bare node) and other runtimes (Bun, Deno).
21 changes: 21 additions & 0 deletions frameworks/hono/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"display_name": "Hono",
"language": "JS",
"type": "framework",
"engine": "V8",
"description": "Ultrafast web framework built on Web Standards, runs on any JS runtime.",
"repo": "https://github.com/honojs/hono",
"enabled": true,
"tests": [
"baseline",
"pipelined",
"limited-conn",
"json",
"upload",
"compression",
"mixed",
"noisy",
"baseline-h2",
"static-h2"
]
}
10 changes: 10 additions & 0 deletions frameworks/hono/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "httparena-hono",
"version": "1.0.0",
"private": true,
"dependencies": {
"hono": "^4.7.0",
"@hono/node-server": "^1.14.0",
"better-sqlite3": "^11.0.0"
}
}
252 changes: 252 additions & 0 deletions frameworks/hono/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
const cluster = require('cluster');
const os = require('os');
const fs = require('fs');
const http2 = require('http2');
const zlib = require('zlib');

const SERVER_NAME = 'hono';

let datasetItems;
let largeJsonBuf;
let dbStmt;
const staticFiles = {};
const MIME_TYPES = {
'.css': 'text/css', '.js': 'application/javascript', '.html': 'text/html',
'.woff2': 'font/woff2', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.json': 'application/json'
};

function loadStaticFiles() {
const dir = '/data/static';
try {
for (const name of fs.readdirSync(dir)) {
const buf = fs.readFileSync(dir + '/' + name);
const ext = name.slice(name.lastIndexOf('.'));
staticFiles[name] = { buf, ct: MIME_TYPES[ext] || 'application/octet-stream' };
}
} catch (e) {}
}

function loadDataset() {
const path = process.env.DATASET_PATH || '/data/dataset.json';
try {
datasetItems = JSON.parse(fs.readFileSync(path, 'utf8'));
} catch (e) {}
}

function loadLargeDataset() {
try {
const raw = JSON.parse(fs.readFileSync('/data/dataset-large.json', 'utf8'));
const items = raw.map(d => ({
id: d.id, name: d.name, category: d.category,
price: d.price, quantity: d.quantity, active: d.active,
tags: d.tags, rating: d.rating,
total: Math.round(d.price * d.quantity * 100) / 100
}));
largeJsonBuf = Buffer.from(JSON.stringify({ items, count: items.length }));
} catch (e) {}
}

function loadDatabase() {
try {
const Database = require('better-sqlite3');
const db = new Database('/data/benchmark.db', { readonly: true });
db.pragma('mmap_size=268435456');
dbStmt = db.prepare('SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50');
} catch (e) {}
}

function sumQuery(query) {
let sum = 0;
for (const key in query) {
const n = parseInt(query[key], 10);
if (n === n) sum += n;
}
return sum;
}

function parseQueryString(url) {
const q = url.indexOf('?');
if (q === -1) return {};
const result = {};
const qs = url.slice(q + 1);
let i = 0;
while (i < qs.length) {
const eq = qs.indexOf('=', i);
if (eq === -1) break;
let amp = qs.indexOf('&', eq);
if (amp === -1) amp = qs.length;
result[decodeURIComponent(qs.slice(i, eq))] = decodeURIComponent(qs.slice(eq + 1, amp));
i = amp + 1;
}
return result;
}

function startWorker() {
loadDataset();
loadLargeDataset();
loadStaticFiles();
loadDatabase();

const { Hono } = require('hono');
const { serve } = require('@hono/node-server');

const app = new Hono();

// --- /pipeline ---
app.get('/pipeline', (c) => {
c.header('server', SERVER_NAME);
return c.text('ok');
});

// --- /baseline11 GET & POST ---
app.get('/baseline11', (c) => {
const query = parseQueryString(c.req.raw.url);
const s = sumQuery(query);
c.header('server', SERVER_NAME);
return c.text(String(s));
});

app.post('/baseline11', async (c) => {
const query = parseQueryString(c.req.raw.url);
const querySum = sumQuery(query);
const body = await c.req.text();
let total = querySum;
const n = parseInt(body.trim(), 10);
if (n === n) total += n;
c.header('server', SERVER_NAME);
return c.text(String(total));
});

// --- /baseline2 ---
app.get('/baseline2', (c) => {
const query = parseQueryString(c.req.raw.url);
const s = sumQuery(query);
c.header('server', SERVER_NAME);
return c.text(String(s));
});

// --- /json ---
app.get('/json', (c) => {
if (!datasetItems) {
return c.text('No dataset', 500);
}
const items = datasetItems.map(d => ({
id: d.id, name: d.name, category: d.category,
price: d.price, quantity: d.quantity, active: d.active,
tags: d.tags, rating: d.rating,
total: Math.round(d.price * d.quantity * 100) / 100
}));
const buf = Buffer.from(JSON.stringify({ items, count: items.length }));
c.header('server', SERVER_NAME);
c.header('content-type', 'application/json');
c.header('content-length', String(buf.length));
return c.body(buf);
});

// --- /compression ---
app.get('/compression', (c) => {
if (!largeJsonBuf) {
return c.text('No dataset', 500);
}
const compressed = zlib.gzipSync(largeJsonBuf, { level: 1 });
c.header('server', SERVER_NAME);
c.header('content-type', 'application/json');
c.header('content-encoding', 'gzip');
c.header('content-length', String(compressed.length));
return c.body(compressed);
});

// --- /db ---
app.get('/db', (c) => {
if (!dbStmt) {
c.header('server', SERVER_NAME);
c.header('content-type', 'application/json');
return c.body('{"items":[],"count":0}');
}
const query = parseQueryString(c.req.raw.url);
let min = 10, max = 50;
if (query.min) min = parseFloat(query.min) || 10;
if (query.max) max = parseFloat(query.max) || 50;
const rows = dbStmt.all(min, max);
const items = rows.map(r => ({
id: r.id, name: r.name, category: r.category,
price: r.price, quantity: r.quantity, active: r.active === 1,
tags: JSON.parse(r.tags),
rating: { score: r.rating_score, count: r.rating_count }
}));
const body = JSON.stringify({ items, count: items.length });
c.header('server', SERVER_NAME);
c.header('content-type', 'application/json');
c.header('content-length', String(Buffer.byteLength(body)));
return c.body(body);
});

// --- /upload ---
app.post('/upload', async (c) => {
const buf = Buffer.from(await c.req.arrayBuffer());
c.header('server', SERVER_NAME);
return c.text(String(buf.length));
});

// Start HTTP/1.1 via @hono/node-server
serve({
fetch: app.fetch,
port: 8080,
hostname: '0.0.0.0',
}, () => {
startH2();
});
}

function startH2() {
const certFile = process.env.TLS_CERT || '/certs/server.crt';
const keyFile = process.env.TLS_KEY || '/certs/server.key';
try {
const opts = {
cert: fs.readFileSync(certFile),
key: fs.readFileSync(keyFile),
allowHTTP1: false,
};
const h2server = http2.createSecureServer(opts, (req, res) => {
const url = req.url;
const q = url.indexOf('?');
const p = q === -1 ? url : url.slice(0, q);
if (p.startsWith('/static/')) {
const name = p.slice(8);
const sf = staticFiles[name];
if (sf) {
res.writeHead(200, { 'content-type': sf.ct, 'content-length': sf.buf.length, 'server': SERVER_NAME });
res.end(sf.buf);
} else {
res.writeHead(404);
res.end();
}
} else {
let sum = 0;
if (q !== -1) {
const qs = url.slice(q + 1);
let i = 0;
while (i < qs.length) {
const eq = qs.indexOf('=', i);
if (eq === -1) break;
let amp = qs.indexOf('&', eq);
if (amp === -1) amp = qs.length;
const n = parseInt(qs.slice(eq + 1, amp), 10);
if (n === n) sum += n;
i = amp + 1;
}
}
res.writeHead(200, { 'content-type': 'text/plain', 'server': SERVER_NAME });
res.end(String(sum));
}
});
h2server.listen(8443);
} catch (e) {}
}

if (cluster.isPrimary) {
const numCPUs = os.availableParallelism ? os.availableParallelism() : os.cpus().length;
for (let i = 0; i < numCPUs; i++) cluster.fork();
} else {
startWorker();
}
Loading