diff --git a/frameworks/uio-ws/Dockerfile b/frameworks/uio-ws/Dockerfile new file mode 100644 index 000000000..20fbafbd2 --- /dev/null +++ b/frameworks/uio-ws/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.27-alpine AS build +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY main.go ./ +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o server . + +FROM scratch +COPY --from=build /app/server /server +EXPOSE 8080 +CMD ["/server"] diff --git a/frameworks/uio-ws/go.mod b/frameworks/uio-ws/go.mod new file mode 100644 index 000000000..7697afbd7 --- /dev/null +++ b/frameworks/uio-ws/go.mod @@ -0,0 +1,13 @@ +module httparena/uio-ws + +go 1.25.0 + +require github.com/urpc/uio v1.5.3 + +require ( + github.com/klauspost/compress v1.20.1 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/limpo1989/taskgo v1.5.0 // indirect + github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect + golang.org/x/sys v0.44.0 // indirect +) diff --git a/frameworks/uio-ws/go.sum b/frameworks/uio-ws/go.sum new file mode 100644 index 000000000..477205272 --- /dev/null +++ b/frameworks/uio-ws/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/limpo1989/taskgo v1.5.0 h1:xczgiyKl20SyWJ5OwH6CcYFMPliCTMUnRWXciRhegLc= +github.com/limpo1989/taskgo v1.5.0/go.mod h1:x3mZYu4EyiAGBqy/qENxNKr78yLgHfjPg58RJRXklmY= +github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E= +github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urpc/uio v1.5.3 h1:4o8KSldSsLh/tva5rtNA8fxM4+wC4otSPjStCxpFOGs= +github.com/urpc/uio v1.5.3/go.mod h1:Jso/P2w3//g9d2ibEbFjDlqc/lLYBmh709BxKVfOhS8= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/frameworks/uio-ws/main.go b/frameworks/uio-ws/main.go new file mode 100644 index 000000000..db94ee405 --- /dev/null +++ b/frameworks/uio-ws/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "log" + "os" + "os/signal" + "runtime" + "syscall" + + "github.com/urpc/uio" + "github.com/urpc/uio/uws" +) + +// uws answers the RFC 6455 upgrade on uio's connections and parses frames in +// the connection's own I/O task over an edge-triggered loop, so a message is +// echoed by the task that read it, with no goroutine per connection. Anything +// that is not an upgrade is answered with 400 by the server itself. +type echoHandler struct{} + +func (echoHandler) OnOpen(*uws.Conn) {} + +func (echoHandler) OnMessage(conn *uws.Conn, message uws.Message) { + switch message.Type { + case uws.TextMessage: + _ = conn.SendText(message.Payload) + case uws.BinaryMessage: + _ = conn.SendBinary(message.Payload) + } +} + +func (echoHandler) OnClose(*uws.Conn, uws.CloseEvent) {} + +func main() { + server := uws.NewServer(echoHandler{}) + // One event loop per CPU: uio's default is four pollers, which leaves the + // cores of a benchmark host idle. A read round's replies leave coalesced + // in one sendmsg, and a send the socket refuses is finished by the next + // write turn without pausing the read. + server.Events = &uio.Events{Pollers: runtime.NumCPU()} + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-stop + _ = server.Close(nil) + }() + + if err := server.Serve(":8080"); err != nil { + log.Fatal(err) + } +} diff --git a/frameworks/uio-ws/meta.json b/frameworks/uio-ws/meta.json new file mode 100644 index 000000000..a482618d2 --- /dev/null +++ b/frameworks/uio-ws/meta.json @@ -0,0 +1,16 @@ +{ + "display_name": "uio-ws", + "language": "Go", + "type": "experimental", + "mode": "standard", + "engine": "uio", + "description": "uws WebSocket echo on uio: the RFC 6455 handshake and frame parser run in the connection's own I/O task on uio's edge-triggered event loops, one per CPU, so each message is parsed from the buffer its task read into and echoed from that task with no goroutine per connection; a read round's replies leave coalesced in one sendmsg, and a send the socket refuses is finished by the next write turn without pausing the read. Default configuration otherwise.", + "repo": "https://github.com/urpc/uio", + "enabled": true, + "tests": [ + "echo-ws", + "echo-ws-pipeline", + "echo-ws-limited" + ], + "maintainers": ["limpo1989"] +} diff --git a/site/data/frameworks.json b/site/data/frameworks.json index c5a76e1de..6d2348b2a 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -1299,8 +1299,8 @@ "response": true } }, - "neton": { - "dir": "neton", + "neton-hyper": { + "dir": "neton-hyper", "description": "Neton, a Kotlin Multiplatform framework compiled to a native executable. Routing, middleware, request parsing and the response pipeline are the framework's; the hyper4k engine (Tokio + Hyper 1.x, linked in as a Rust static library) only does protocol and transport. Serves HTTP/1.1 on 8080, HTTP/2 cleartext on 8082, HTTP/1.1+TLS on 8081 and HTTP/2+TLS on 8443 (ALPN) from one route table; static files and TLS terminate in the framework.", "repo": "https://github.com/netonframework/neton", "type": "emerging", @@ -1313,6 +1313,20 @@ "response": true } }, + "neton": { + "dir": "neton", + "description": "Neton with the Kotlin/Native neton-http-netonstream adapter, using com.netonstream io/http/tls and OpenSSL. The same framework routes, middleware, request parsing and response pipeline as neton-hyper; only the engine differs. HTTP/1.1 on 8080, h2c on 8082, h1 TLS on 8081 and h2 TLS on 8443.", + "repo": "https://github.com/netonframework/neton", + "type": "emerging", + "engine": "netonstream", + "mode": "standard", + "completeness": { + "routing": true, + "middleware": true, + "request": true, + "response": true + } + }, "nginx": { "dir": "nginx", "description": "Nginx with a custom C handler module, compiled with -O3 -march=native. The module answers /baseline11, /baseline2 and /json/{count}, serializing the JSON body per request from the dataset parsed at worker startup; /static/* is served by nginx core's sendfile path. TLS via quictls: HTTP/1.1 on :8081, h2 and h3 on :8443.", @@ -2088,6 +2102,14 @@ "type": "engine", "engine": "io_uring" }, + "uio-ws": { + "dir": "uio-ws", + "description": "uws WebSocket echo on uio: the RFC 6455 handshake and frame parser run in the connection's own I/O task on uio's edge-triggered event loops, one per CPU, so each message is parsed from the buffer its task read into and echoed from that task with no goroutine per connection; a read round's replies leave coalesced in one sendmsg, and a send the socket refuses is finished by the next write turn without pausing the read. Default configuration otherwise.", + "repo": "https://github.com/urpc/uio", + "type": "experimental", + "engine": "uio", + "mode": "standard" + }, "ultimate-express": { "dir": "ultimate-express", "description": "ultimate-express (Express API on uWebSockets.js) with the cluster module for multi-core scaling.", diff --git a/site/data/results/uio-ws.json b/site/data/results/uio-ws.json new file mode 100644 index 000000000..38d0e2d63 --- /dev/null +++ b/site/data/results/uio-ws.json @@ -0,0 +1,157 @@ +{ + "framework": "uio-ws", + "results": { + "echo-ws-16384": { + "framework": "uio-ws", + "language": "Go", + "rps": 1519552, + "avg_latency": "10.44ms", + "p99_latency": "13.00ms", + "cpu": "3035.5%", + "memory": "165MiB", + "connections": 16384, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "10.54MB/s", + "reconnects": 0, + "status_2xx": 7597763, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "echo-ws-4096": { + "framework": "uio-ws", + "language": "Go", + "rps": 1957550, + "avg_latency": "2.09ms", + "p99_latency": "3.94ms", + "cpu": "3307.9%", + "memory": "77MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "13.11MB/s", + "reconnects": 0, + "status_2xx": 9787750, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "echo-ws-512": { + "framework": "uio-ws", + "language": "Go", + "rps": 1866244, + "avg_latency": "273us", + "p99_latency": "498us", + "cpu": "3276.6%", + "memory": "51MiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "12.46MB/s", + "reconnects": 0, + "status_2xx": 9331221, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "echo-ws-limited-4096": { + "framework": "uio-ws", + "language": "Go", + "rps": 521759, + "avg_latency": "1.71ms", + "p99_latency": "6.33ms", + "cpu": "1441.5%", + "memory": "94MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "9.90MB/s", + "reconnects": 260628, + "status_2xx": 2608799, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "echo-ws-limited-512": { + "framework": "uio-ws", + "language": "Go", + "rps": 410035, + "avg_latency": "920us", + "p99_latency": "3.37ms", + "cpu": "1465.1%", + "memory": "64MiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "7.78MB/s", + "reconnects": 205034, + "status_2xx": 2050176, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "echo-ws-pipeline-16384": { + "framework": "uio-ws", + "language": "Go", + "rps": 23882179, + "avg_latency": "10.64ms", + "p99_latency": "13.50ms", + "cpu": "3121.0%", + "memory": "171MiB", + "connections": 16384, + "threads": 64, + "duration": "5s", + "pipeline": 16, + "bandwidth": "159.78MB/s", + "reconnects": 0, + "status_2xx": 119410896, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "echo-ws-pipeline-4096": { + "framework": "uio-ws", + "language": "Go", + "rps": 30318937, + "avg_latency": "2.15ms", + "p99_latency": "3.98ms", + "cpu": "3765.6%", + "memory": "80MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 16, + "bandwidth": "202.39MB/s", + "reconnects": 0, + "status_2xx": 151594688, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "echo-ws-pipeline-512": { + "framework": "uio-ws", + "language": "Go", + "rps": 29343251, + "avg_latency": "278us", + "p99_latency": "502us", + "cpu": "3681.4%", + "memory": "51MiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 16, + "bandwidth": "195.84MB/s", + "reconnects": 0, + "status_2xx": 146716256, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + } + } +} diff --git a/site/static/logs/echo-ws-limited/4096/uio-ws.log b/site/static/logs/echo-ws-limited/4096/uio-ws.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/echo-ws-limited/512/uio-ws.log b/site/static/logs/echo-ws-limited/512/uio-ws.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/echo-ws-pipeline/16384/uio-ws.log b/site/static/logs/echo-ws-pipeline/16384/uio-ws.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/echo-ws-pipeline/4096/uio-ws.log b/site/static/logs/echo-ws-pipeline/4096/uio-ws.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/echo-ws-pipeline/512/uio-ws.log b/site/static/logs/echo-ws-pipeline/512/uio-ws.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/echo-ws/16384/uio-ws.log b/site/static/logs/echo-ws/16384/uio-ws.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/echo-ws/4096/uio-ws.log b/site/static/logs/echo-ws/4096/uio-ws.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/echo-ws/512/uio-ws.log b/site/static/logs/echo-ws/512/uio-ws.log new file mode 100644 index 000000000..e69de29bb