diff --git a/bench/websockets/chat-client.mjs b/bench/websockets/chat-client.mjs new file mode 100644 index 0000000..786ebde --- /dev/null +++ b/bench/websockets/chat-client.mjs @@ -0,0 +1,182 @@ +const env = + "process" in globalThis + ? process.env + : "Deno" in globalThis + ? Deno.env.toObject() + : {}; + +const SERVER = env.SERVER || "ws://0.0.0.0:4001"; +const WebSocket = globalThis.WebSocket || (await import("ws")).WebSocket; +const LOG_MESSAGES = env.LOG_MESSAGES === "1"; +const CLIENTS_TO_WAIT_FOR = parseInt(env.CLIENTS_COUNT || "", 10) || 16; +const DELAY = 64; +const MESSAGES_TO_SEND = Array.from({ length: 32 }, () => [ + "Hello World!", + "Hello World! 1", + "Hello World! 2", + "Hello World! 3", + "Hello World! 4", + "Hello World! 5", + "Hello World! 6", + "Hello World! 7", + "Hello World! 8", + "Hello World! 9", + "What is the meaning of life?", + "where is the bathroom?", + "zoo", + "kangaroo", + "erlang", + "elixir", + "bun", + "mochi", + "typescript", + "javascript", + "Hello World! 7", + "Hello World! 8", + "Hello World! 9", + "What is the meaning of life?", + "where is the bathroom?", + "zoo", + "kangaroo", + "erlang", + "elixir", + "bun", + "mochi", + "typescript", + "javascript", + "Hello World! 7", + "Hello World! 8", + "Hello World! 9", + "What is the meaning of life?", + "Hello World! 7", + "Hello World! 8", + "Hello World! 9", + "What is the meaning of life?", + "where is the bathroom?", + "zoo", + "kangaroo", + "erlang", + "elixir", + "bun", + "mochi", + "typescript", + "javascript", +]).flat(); + +const NAMES = Array.from({ length: 50 }, (a, i) => [ + "Alice" + i, + "Bob" + i, + "Charlie" + i, + "David" + i, + "Eve" + i, + "Frank" + i, + "Grace" + i, + "Heidi" + i, + "Ivan" + i, + "Judy" + i, + "Karl" + i, + "Linda" + i, + "Mike" + i, + "Nancy" + i, + "Oscar" + i, + "Peggy" + i, + "Quentin" + i, + "Ruth" + i, + "Steve" + i, + "Trudy" + i, + "Ursula" + i, + "Victor" + i, + "Wendy" + i, + "Xavier" + i, + "Yvonne" + i, + "Zach" + i, +]) + .flat() + .slice(0, CLIENTS_TO_WAIT_FOR); + +console.log(`Connecting ${CLIENTS_TO_WAIT_FOR} WebSocket clients...`); +console.time(`All ${CLIENTS_TO_WAIT_FOR} clients connected`); + +var remainingClients = CLIENTS_TO_WAIT_FOR; +var promises = []; + +const clients = new Array(CLIENTS_TO_WAIT_FOR); +for (let i = 0; i < CLIENTS_TO_WAIT_FOR; i++) { + clients[i] = new WebSocket(`${SERVER}?name=${NAMES[i]}`); + promises.push( + new Promise((resolve, reject) => { + clients[i].onmessage = (event) => { + resolve(); + }; + }) + ); +} + +await Promise.all(promises); +console.timeEnd(`All ${clients.length} clients connected`); + +var received = 0; +var total = 0; +var more = false; +var remaining; + +for (let i = 0; i < CLIENTS_TO_WAIT_FOR; i++) { + clients[i].onmessage = (event) => { + if (LOG_MESSAGES) console.log(event.data); + received++; + remaining--; + + if (remaining === 0) { + more = true; + remaining = total; + } + }; +} + +// each message is supposed to be received +// by each client +// so its an extra loop +for (let i = 0; i < CLIENTS_TO_WAIT_FOR; i++) { + for (let j = 0; j < MESSAGES_TO_SEND.length; j++) { + for (let k = 0; k < CLIENTS_TO_WAIT_FOR; k++) { + total++; + } + } +} +remaining = total; + +function restart() { + for (let i = 0; i < CLIENTS_TO_WAIT_FOR; i++) { + for (let j = 0; j < MESSAGES_TO_SEND.length; j++) { + clients[i].send(MESSAGES_TO_SEND[j]); + } + } +} + +var runs = []; +setInterval(() => { + const last = received; + runs.push(last); + received = 0; + console.log( + last, + `messages per second (${CLIENTS_TO_WAIT_FOR} clients x ${MESSAGES_TO_SEND.length} msg, min delay: ${DELAY}ms)` + ); + + if (runs.length >= 10) { + console.log("10 runs"); + console.log(JSON.stringify(runs, null, 2)); + if ("process" in globalThis) process.exit(0); + runs.length = 0; + } +}, 1000); +var isRestarting = false; +setInterval(() => { + if (more && !isRestarting) { + more = false; + isRestarting = true; + restart(); + isRestarting = false; + } +}, DELAY); +restart(); diff --git a/bench/websockets/chat-server.bun.js b/bench/websockets/chat-server.bun.js new file mode 100644 index 0000000..5bffdf3 --- /dev/null +++ b/bench/websockets/chat-server.bun.js @@ -0,0 +1,53 @@ +// See ./README.md for instructions on how to run this benchmark. +const CLIENTS_TO_WAIT_FOR = parseInt(process.env.CLIENTS_COUNT || "", 10) || 16; +var remainingClients = CLIENTS_TO_WAIT_FOR; +const COMPRESS = process.env.COMPRESS === "1"; +const port = process.PORT || 4001; + +const server = Bun.serve({ + port: port, + websocket: { + open(ws) { + ws.subscribe("room"); + + remainingClients--; + console.log(`${ws.data.name} connected (${remainingClients} remain)`); + + if (remainingClients === 0) { + console.log("All clients connected"); + setTimeout(() => { + console.log('Starting benchmark by sending "ready" message'); + ws.publishText("room", `ready`); + }, 100); + } + }, + message(ws, msg) { + ws.publishText("room", msg); + }, + close(ws) { + remainingClients++; + }, + + perMessageDeflate: false, + }, + + fetch(req, server) { + if ( + server.upgrade(req, { + data: { + name: + new URL(req.url).searchParams.get("name") || + "Client #" + (CLIENTS_TO_WAIT_FOR - remainingClients), + }, + }) + ) + return; + + return new Response("Error"); + }, +}); + +console.log( + `Waiting for ${remainingClients} clients to connect...\n`, + ` http://${server.hostname}:${port}/` +); diff --git a/bench/websockets/chat-server.node.mjs b/bench/websockets/chat-server.node.mjs new file mode 100644 index 0000000..028f9e9 --- /dev/null +++ b/bench/websockets/chat-server.node.mjs @@ -0,0 +1,51 @@ +// See ./README.md for instructions on how to run this benchmark. +const port = process.env.PORT || 4001; +const CLIENTS_TO_WAIT_FOR = parseInt(process.env.CLIENTS_COUNT || "", 10) || 16; + +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +var WebSocketServer = require("ws").Server, + config = { + host: "0.0.0.0", + port, + }, + wss = new WebSocketServer(config, function () { + console.log(`Waiting for ${CLIENTS_TO_WAIT_FOR} clients to connect..`); + }); + +var clients = []; + +wss.on("connection", function (ws, { url }) { + const name = new URL(new URL(url, "http://localhost:3000")).searchParams.get( + "name" + ); + console.log( + `${name} connected (${CLIENTS_TO_WAIT_FOR - clients.length} remain)` + ); + clients.push(ws); + + ws.on("message", function (message) { + for (let client of clients) { + client.send(message); + } + }); + + // when a connection is closed + ws.on("close", function (ws) { + clients.splice(clients.indexOf(ws), 1); + }); + + if (clients.length === CLIENTS_TO_WAIT_FOR) { + sendReadyMessage(); + } +}); + +function sendReadyMessage() { + console.log("All clients connected"); + setTimeout(() => { + console.log("Starting benchmark"); + for (let client of clients) { + client.send(`ready`); + } + }, 100); +} diff --git a/bench/websockets/falcon_server.py b/bench/websockets/falcon_server.py new file mode 100644 index 0000000..f839e20 --- /dev/null +++ b/bench/websockets/falcon_server.py @@ -0,0 +1,47 @@ +import falcon.asgi +import falcon.media +import asyncio + +clients = set([]) +remaining_clients = 16 + +async def broadcast(message): + + # tasks = [ws.send_text(message) for ws in client] + # return await asyncio.wait(tasks, return_when=ALL_COMPLETED) + for ws in clients: + await ws.send_text(message) + # # for ws in clients: + # # tasks.append(ws.send_text(message)) + # await asyncio.wait(tasks, return_when=ALL_COMPLETED) + + +class SomeResource: + + async def on_get(self, req): + pass + + async def on_websocket(self, req, ws): + global remaining_clients + try: + await ws.accept() + clients.add(ws) + remaining_clients = remaining_clients - 1 + if remaining_clients == 0: + await broadcast("ready") + + while True: + payload = await ws.receive_text() + await broadcast(payload) + + except falcon.WebSocketDisconnected: + clients.remove(ws) + remaining_clients = remaining_clients + 1 + + + + + +app = falcon.asgi.App() +app.add_route('/', SomeResource()) +# python3 -m gunicorn falcon_server:app -b 127.0.0.1:4001 -w 1 -k uvicorn.workers.UvicornWorker \ No newline at end of file diff --git a/bench/websockets/package.json b/bench/websockets/package.json new file mode 100644 index 0000000..3877998 --- /dev/null +++ b/bench/websockets/package.json @@ -0,0 +1,13 @@ +{ + "name": "websocket-server", + "module": "index.ts", + "type": "module", + "devDependencies": { + "bun-types": "^0.2.0" + }, + "dependencies": { + "bufferutil": "^4.0.7", + "utf-8-validate": "^5.0.10", + "ws": "^8.11.0" + } +} diff --git a/bench/websockets/socketify_server.py b/bench/websockets/socketify_server.py new file mode 100644 index 0000000..c8e63d7 --- /dev/null +++ b/bench/websockets/socketify_server.py @@ -0,0 +1,38 @@ +from socketify import App, AppOptions, OpCode, CompressOptions + +remaining_clients = 16 + +def ws_open(ws): + ws.subscribe("room") + global remaining_clients + remaining_clients = remaining_clients - 1 + if remaining_clients == 0: + print("All clients connected") + print('Starting benchmark by sending "ready" message') + + ws.publish("room", "ready", OpCode.TEXT) + #publish will send to everyone except it self so send to it self too + ws.send("ready", OpCode.TEXT) + + +def ws_message(ws, message, opcode): + #publish will send to everyone except it self so send to it self too + ws.publish("room", message, opcode) + ws.send(message, opcode) + +def ws_close(ws, close, message): + global remaining_clients + remaining_clients = remaining_clients + 1 + +app = App() +app.ws("/*", { + 'compression': CompressOptions.DISABLED, + 'max_payload_length': 16 * 1024 * 1024, + 'idle_timeout': 60, + 'open': ws_open, + 'message': ws_message, + 'close': ws_close +}) +app.any("/", lambda res,req: res.end("Nothing to see here!'")) +app.listen(4001, lambda config: print("Listening on port http://localhost:%d now\n" % (config.port))) +app.run() \ No newline at end of file diff --git a/misc/ws-bar-graph.png b/misc/ws-bar-graph.png new file mode 100644 index 0000000..092243e Binary files /dev/null and b/misc/ws-bar-graph.png differ diff --git a/misc/ws-bar-graph.svg b/misc/ws-bar-graph.svg new file mode 100644 index 0000000..292d6d4 --- /dev/null +++ b/misc/ws-bar-graph.svg @@ -0,0 +1 @@ +0200,000400,000600,000800,0001,000,000falcon uvicornfalcon uvicorn pypy3nodejsbunsocketifysocketify pypymessages/slibrary / runtime \ No newline at end of file