Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ on:
workflow_dispatch:
inputs:
engines:
description: 'Comma-separated engines to benchmark. Supported by this workflow: "http-native,bun"'
description: 'Comma-separated engines to benchmark. Supported by this workflow: "http-native,bun,fiber"'
required: false
default: "http-native,bun"
default: "http-native,bun,fiber"
scenarios:
description: 'Comma-separated scenarios: "static,dynamic,opt"'
required: false
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
- name: Run benchmarks
run: |
bun bench/ci.js \
--engines="${{ github.event.inputs.engines || 'http-native,bun' }}" \
--engines="${{ github.event.inputs.engines || 'http-native,bun,fiber' }}" \
--scenarios="${{ github.event.inputs.scenarios || 'static,dynamic,opt' }}" \
--connections="${{ github.event.inputs.connections || '200' }}" \
--duration="${{ github.event.inputs.duration || '10s' }}" \
Expand Down
87 changes: 78 additions & 9 deletions bench/ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { once } from "node:events";
import { resolve } from "node:path";
import { homedir } from "node:os";

const DEFAULT_ENGINES = ["http-native", "bun"];
const DEFAULT_ENGINES = ["http-native", "bun", "fiber"];
const DEFAULT_SCENARIOS = ["static", "dynamic", "opt"];
const DEFAULT_CONNECTIONS = 200;
const DEFAULT_DURATION = "10s";
Expand All @@ -20,6 +20,7 @@ const SERVER_PORTS = Object.freeze({
xitca: { static: 3003, dynamic: 3013, opt: 3023 },
monoio: { static: 3004, dynamic: 3014, opt: 3024 },
zig: { static: 3005, dynamic: 3015, opt: 3025 },
fiber: { static: 3009, dynamic: 3019, opt: 3029 },
});

const SUPPORTED_SCENARIOS = new Set(DEFAULT_SCENARIOS);
Expand Down Expand Up @@ -79,6 +80,7 @@ async function main() {
console.log(summary);
console.log(`[http-native][bench] wrote ${jsonPath}`);
console.log(`[http-native][bench] wrote ${markdownPath}`);
process.exit(0);
}

function parseArgs(argv) {
Expand Down Expand Up @@ -143,7 +145,7 @@ function printUsage() {
console.log("Usage: bun bench/ci.js [options]");
console.log("");
console.log("Options:");
console.log(` --engines=http-native,bun Comma-separated list. Default: ${DEFAULT_ENGINES.join(",")}`);
console.log(` --engines=http-native,bun,fiber Comma-separated list. Default: ${DEFAULT_ENGINES.join(",")}`);
console.log(` --scenarios=static,dynamic,opt Comma-separated list. Default: ${DEFAULT_SCENARIOS.join(",")}`);
console.log(` --connections=${DEFAULT_CONNECTIONS} Bombardier concurrency`);
console.log(` --duration=${DEFAULT_DURATION} Bombardier duration`);
Expand Down Expand Up @@ -213,10 +215,8 @@ async function runBenchmarkCase(testCase, options) {
const server = spawnServer(testCase);
const serverLogs = [];
let readyResolve;
let readyReject;
const ready = new Promise((resolve, reject) => {
const ready = new Promise((resolve) => {
readyResolve = resolve;
readyReject = reject;
});

let stdoutBuffer = "";
Expand Down Expand Up @@ -304,17 +304,28 @@ async function runBenchmarkCase(testCase, options) {
derived: deriveMetrics(parsed.result),
};
} finally {
if (server.exitCode === null) {
server.kill("SIGTERM");
await once(server, "exit").catch(() => {});
}
await stopServer(server, `${testCase.engine}/${testCase.scenario}`);
}
}

function spawnServer(testCase) {
if (testCase.engine === "bun" || testCase.engine === "http-native" || testCase.engine === "old") {
return spawn("bun", ["bench/target.js", testCase.engine, testCase.scenario, String(testCase.port)], {
cwd: process.cwd(),
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
});
}

if (testCase.engine === "fiber") {
const cwd = resolve(process.cwd(), "bench/fiber-server");
if (!existsSync(resolve(cwd, "go.mod"))) {
throw new Error(`Missing Fiber benchmark target at ${cwd}`);
}

return spawn("go", ["run", ".", testCase.scenario, String(testCase.port)], {
cwd,
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
});
}
Expand Down Expand Up @@ -346,6 +357,7 @@ function spawnServer(testCase) {
["run", "--release", "--manifest-path", manifestPath, "--", testCase.scenario, String(testCase.port)],
{
cwd: process.cwd(),
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
},
);
Expand All @@ -354,6 +366,63 @@ function spawnServer(testCase) {
throw new Error(`Unsupported engine ${testCase.engine}`);
}

async function stopServer(server, label) {
if (!server) {
return;
}

if (server.exitCode !== null) {
cleanupServerStreams(server);
return;
}

const waitForExit = once(server, "exit");
const gracefulSignal = "SIGTERM";

try {
if (server.pid && process.platform !== "win32") {
process.kill(-server.pid, gracefulSignal);
} else {
server.kill(gracefulSignal);
}
} catch {
cleanupServerStreams(server);
return;
}

const gracefulExit = await Promise.race([
waitForExit.then(() => true).catch(() => false),
delay(5000).then(() => false),
]);

if (!gracefulExit && server.exitCode === null) {
console.warn(`[http-native][bench] forcing ${label} to exit`);
try {
if (server.pid && process.platform !== "win32") {
process.kill(-server.pid, "SIGKILL");
} else {
server.kill("SIGKILL");
}
await once(server, "exit").catch(() => {});
} catch {
// Ignore kill failures during cleanup
}
}

cleanupServerStreams(server);
}

function cleanupServerStreams(server) {
server.stdout?.destroy();
server.stderr?.destroy();
}

function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

async function runCommand(command, args, options = {}) {
const label = options.label ?? `${command} ${args.join(" ")}`;
const child = spawn(command, args, {
Expand Down
Binary file added bench/fiber-server/fiber-server
Binary file not shown.
19 changes: 19 additions & 0 deletions bench/fiber-server/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module github.com/nadhi/http-native/bench/fiber-server

go 1.23

require github.com/gofiber/fiber/v2 v2.52.6

require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.28.0 // indirect
)
27 changes: 27 additions & 0 deletions bench/fiber-server/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
99 changes: 99 additions & 0 deletions bench/fiber-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package main

import (
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"

"github.com/gofiber/fiber/v2"
)

func main() {
scenario := "static"
if len(os.Args) > 1 {
scenario = os.Args[1]
}

port := "3009"
if len(os.Args) > 2 {
port = os.Args[2]
}

app := fiber.New(fiber.Config{
DisableStartupMessage: true,
})

switch scenario {
case "static":
app.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"ok": true,
"engine": "fiber",
"mode": "static",
})
})
case "dynamic":
app.Get("/users/:id", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"id": c.Params("id"),
"engine": "fiber",
"mode": "dynamic",
})
})
case "opt":
app.Get("/stable", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"ok": true,
"engine": "fiber",
"mode": "opt",
"optimization": "runtime",
})
})
default:
log.Fatalf("unsupported scenario: %s", scenario)
}

app.Use(func(c *fiber.Ctx) error {
c.Status(fiber.StatusNotFound)
return c.JSON(fiber.Map{"error": "Route not found"})
})

listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", port))
if err != nil {
log.Fatal(err)
}
defer listener.Close()

fmt.Printf("READY http://127.0.0.1:%s\n", port)

serverErr := make(chan error, 1)
go func() {
serverErr <- app.Listener(listener)
}()

signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)

select {
case <-signals:
shutdownDone := make(chan struct{})
go func() {
defer close(shutdownDone)
_ = app.Shutdown()
}()

select {
case <-shutdownDone:
case <-time.After(5 * time.Second):
log.Print("fiber shutdown timed out")
}
case err := <-serverErr:
if err != nil {
log.Fatal(err)
}
}
}
9 changes: 7 additions & 2 deletions bench/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const port = Number(portArg ?? 3001);

function printUsage() {
console.log("Usage: bun bench/run.js <engine> <scenario> <port>");
console.log("Engines: http-native | bun | xitca | monoio | zig");
console.log("Engines: http-native | bun | fiber | xitca | monoio | zig");
console.log("Scenarios: static | dynamic | opt");
console.log("");
console.log("Example:");
Expand All @@ -30,7 +30,7 @@ function benchmarkPathForScenario(activeScenario) {
}

async function main() {
if (!["http-native", "bun", "xitca", "monoio", "zig"].includes(engine)) {
if (!["http-native", "bun", "fiber", "xitca", "monoio", "zig"].includes(engine)) {
printUsage();
process.exit(1);
}
Expand Down Expand Up @@ -69,6 +69,11 @@ async function main() {
stdio: ["ignore", "pipe", "inherit"],
},
)
: engine === "fiber"
? spawn("go", ["run", "./bench/fiber-server", scenario, String(port)], {
cwd: process.cwd(),
stdio: ["ignore", "pipe", "inherit"],
})
: spawn("bun", ["bench/target.js", engine, scenario, String(port)], {
cwd: process.cwd(),
stdio: ["ignore", "pipe", "inherit"],
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,21 @@
"bench": "bun run build:release && bun bench/run.js",
"bench:http-native:static": "bun run build:release && bun bench/run.js http-native static 3001",
"bench:bun:static": "bun bench/run.js bun static 3000",
"bench:fiber:static": "bun bench/run.js fiber static 3009",
"bench:xitca:static": "bun bench/run.js xitca static 3003",
"bench:monoio:static": "bun bench/run.js monoio static 3004",
"bench:zig:static": "bun bench/run.js zig static 3005",
"bench:http-native:dynamic": "bun run build:release && bun bench/run.js http-native dynamic 3011",
"bench:bun:dynamic": "bun bench/run.js bun dynamic 3010",
"bench:fiber:dynamic": "bun bench/run.js fiber dynamic 3019",
"bench:xitca:dynamic": "bun bench/run.js xitca dynamic 3013",
"bench:monoio:dynamic": "bun bench/run.js monoio dynamic 3014",
"bench:zig:dynamic": "bun bench/run.js zig dynamic 3015",
"bench:http-native:opt": "bun run build:release && bun bench/run.js http-native opt 3021",
"bench:bun:opt": "bun bench/run.js bun opt 3020",
"bench:fiber:opt": "bun bench/run.js fiber opt 3029",
"bench:xitca:opt": "bun bench/run.js xitca opt 3023",
"bench:monoio:opt": "bun bench/run.js monoio opt 3024",
"bench:zig:opt": "bun bench/run.js zig opt 3025"
}
}
}
4 changes: 4 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<p align="center">
<img src="https://cf-data.pkg.lat/httpnative-banner.png" alt="Http-native banner" width="720" />
</p>

Http-native

Http native is a express like server framework for Javascript that uses the Node-compatible framework with Rust native module way, where the rust binary is evoked through napi-rs or something faster.
Expand Down
Loading
Loading