From b0dede74e4e558cdde30e3236deaab998115a802 Mon Sep 17 00:00:00 2001 From: Ciro Date: Fri, 11 Nov 2022 12:23:44 -0300 Subject: [PATCH] added more ws bench --- bench/websockets/chat-client.mjs | 182 ++++++++++++++++++++++++++ bench/websockets/chat-server.bun.js | 53 ++++++++ bench/websockets/chat-server.node.mjs | 51 ++++++++ bench/websockets/falcon_server.py | 47 +++++++ bench/websockets/package.json | 13 ++ bench/websockets/socketify_server.py | 38 ++++++ misc/ws-bar-graph.png | Bin 0 -> 35661 bytes misc/ws-bar-graph.svg | 1 + 8 files changed, 385 insertions(+) create mode 100644 bench/websockets/chat-client.mjs create mode 100644 bench/websockets/chat-server.bun.js create mode 100644 bench/websockets/chat-server.node.mjs create mode 100644 bench/websockets/falcon_server.py create mode 100644 bench/websockets/package.json create mode 100644 bench/websockets/socketify_server.py create mode 100644 misc/ws-bar-graph.png create mode 100644 misc/ws-bar-graph.svg 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 0000000000000000000000000000000000000000..092243edb03ea9a9aacaa4ac86f6948af647365f GIT binary patch literal 35661 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 IDOv^p3wDqEr2qf` literal 0 HcmV?d00001 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