From 268c7a6d5fda4d37d704863553e348dfd4736e5f Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Sat, 25 Jul 2026 22:18:19 -0300 Subject: [PATCH] add benchmarks --- http/httphi/bench_test.go | 89 ++++++++++++++++++++++++++++++++++++ http/httphi/exchange_test.go | 5 +- http/httphi/router_test.go | 4 +- 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 http/httphi/bench_test.go diff --git a/http/httphi/bench_test.go b/http/httphi/bench_test.go new file mode 100644 index 0000000..14a83cb --- /dev/null +++ b/http/httphi/bench_test.go @@ -0,0 +1,89 @@ +package httphi + +import ( + "io" + "testing" +) + +// benchConn replays a fixed request and discards the response. It allocates +// nothing itself so benchmark alloc counts belong to the package under test. +type benchConn struct { + request string + read int + written int +} + +func (c *benchConn) rewind() { c.read, c.written = 0, 0 } + +func (c *benchConn) Read(b []byte) (int, error) { + if c.read >= len(c.request) { + return 0, io.EOF + } + n := copy(b, c.request[c.read:]) + c.read += n + return n, nil +} + +func (c *benchConn) Write(b []byte) (int, error) { + c.written += len(b) + return len(b), nil +} + +func (c *benchConn) Close() error { return nil } + +// benchBody is package level: converting a string literal to []byte inside the +// handler would allocate on every request and hide the router's own cost. +var benchBody = []byte("hello world") + +func benchExchange(b *testing.B, conn conn) *Exchange { + b.Helper() + const bufferSize = 1024 + exch := new(Exchange) + exch.Configure(make([]byte, 2*bufferSize), bufferSize, false) + if !exch.Acquire(conn) { + b.Fatal("fresh exchange failed to acquire connection") + } + return exch +} + +// BenchmarkHandle measures a whole exchange: read, parse, mux and respond. +func BenchmarkHandle(b *testing.B) { + for _, bb := range []struct { + name string + request string + handler HandlerFunc + }{ + { + name: "GETWithHeaders", + request: "GET / HTTP/1.1\r\nHost: tinygo.org\r\nUser-Agent: bench\r\nAccept: */*\r\nConnection: close\r\n\r\n", + handler: func(ex *Exchange) { + ex.SetHeader("Content-Type", "text/plain") + ex.SetHeaderInt("Content-Length", int64(len(benchBody)), 10) + ex.Write(benchBody) + }, + }, + { + name: "NotFound", + request: "GET /nowhere HTTP/1.1\r\nHost: tinygo.org\r\n\r\n", + handler: nil, // Unregistered: exercises the 404 path. + }, + } { + b.Run(bb.name, func(b *testing.B) { + var mux MuxSlice + if bb.handler != nil { + mux.Handle("GET /", bb.handler) + } + conn := &benchConn{request: bb.request} + exch := benchExchange(b, conn) + b.ReportAllocs() + b.SetBytes(int64(len(bb.request))) + b.ResetTimer() + for b.Loop() { + conn.rewind() + exch.Release() + exch.Acquire(conn) + Handle(exch, &mux, nopBackoff) + } + }) + } +} diff --git a/http/httphi/exchange_test.go b/http/httphi/exchange_test.go index ae2decd..49e2277 100644 --- a/http/httphi/exchange_test.go +++ b/http/httphi/exchange_test.go @@ -50,9 +50,10 @@ func TestExchangeWriteHeader(t *testing.T) { // Longest status text in status.go: worst case for the status line buffer. {code: 511, want: "HTTP/1.1 511 Network Authentication Required\r\n\r\n"}, } { - exch := newExchange(t, newConn(""), 128, false) + conn := newConn("") + exch := newExchange(t, conn, 128, false) exch.WriteHeader(test.code) - if got := exch.rw.(*rwconn).ViewWritten(); got != test.want { + if got := conn.ViewWritten(); got != test.want { t.Errorf("code %d: want %q, got %q", test.code, test.want, got) } } diff --git a/http/httphi/router_test.go b/http/httphi/router_test.go index 4b83cfd..17aa46b 100644 --- a/http/httphi/router_test.go +++ b/http/httphi/router_test.go @@ -357,7 +357,7 @@ func TestRouterConfigureDuringWorkerHandle(t *testing.T) { wg.Add(2) go func() { defer wg.Done() - for i := 0; i < 300; i++ { + for range 300 { conn := newConn("GET / HTTP/1.1\r\nHost: h\r\n\r\n") conn.Hangup() router.Handle(conn) // Drops are fine, panics are not. @@ -367,7 +367,7 @@ func TestRouterConfigureDuringWorkerHandle(t *testing.T) { defer wg.Done() // Each Configure sleeps 5ms tearing down the previous generation, keep // the count low and let the Handle loop supply the concurrency. - for i := 0; i < 20; i++ { + for range 20 { // errBusyExchanges is legitimate backpressure: the previous // generation was still serving when the buffers were needed. if err := router.Configure(cfg); err != nil && err != errBusyExchanges {