Files
lneto/http/httphi/bench_test.go
T
Pat Whittingslow 5c54030f19 add http/httphi (#171)
* add http/httphi

* begin adding httphi tests

* claude found neat bugs

* add low level Handle function and more tests

* more tests, run go generate

* add Hijacker-like functionality

* improve locking and acquisition of Exchanges in reconfiguring

* several bugfixes, add internal.IntLen, round up http-linux example with new router API

* small nit

* add benchmarks

* add query handling

* remove ForEach pattern, allocates in TinyGo

* massive documentation push and code reordering in files

* Router.Handle returns error after being torn down

* run go fix

* rework Mux interface to receive a string request path

* add MethodFrom

* minor doc nit

* fail on incomplete staging

* add raw buffer access

* add streaming API distinct from Exchange

* begin adding multipart form logic

* finish rounding up multipart form parsing

* remove status type

* first Multipart approach

* begin adding readMultiPart

* add Exchange.ReadMultiparts reimagining of clanker slop

* ai insists with backoffs

* simplify clanker slop

* apply go fix

* add a pattern argument to Mux

* explicit header key/value alloc and add ExchangeConfig

* fix tests after excplicit header alloc change

* fix examples

* run go fix

* expose rawsock as experimental package (will use for external benchmarks)

* remove backoff from form parsing

* @MDr164 suggestions get potential fixes

* apply go fix

* add examples

* add README.md

* fix rawsock tinygo implementation

* apply @MDr164 various fixes

* update documentation on ContentLength methods and fix bug in Form reset on empty body

* fix tests

* add fuzz tests

* run go fix

* io.ErrNoProgress on parsing form spin

* run go fix

* remove backoff assumption from Router

* httphi.Handle rejects unsupported protocols

* go format router.go

* add kvbuffer

* rewrite Cookie with KVBuffer

* mid refactor of KVBuffer into Header

* work on KVBuffer exhausted semantics

* add Go's ServeMux Request.PathValue access semantics to Exchange, Mux and MuxSlice

* add PathValue example

* document all the things; improve req Query semantics; add Form.EnableBufferGrowth

* unexport kvBuffer

* add Exchange.PathValueAppend

* use stdlib in example instead of rawsock

* remove rawsock from http example

* add darwin arch rawsock

* fix example

* rename Router.TeardownGoroutines to Shutdown matching http.Server.Shutdown

* rename types and identifiers

* @MDr164 Content-Type and Transfer-Encoding bug catches
2026-07-29 20:20:21 -03:00

135 lines
3.5 KiB
Go

package httphi
import (
"io"
"testing"
"github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/internal"
)
// 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
const numHeaderCap = 2
exch := new(Exchange)
exch.Configure(ExchangeConfig{
RawBuf: make([]byte, 2*bufferSize),
RequestBufferLim: bufferSize,
NumHeaderKVCap: numHeaderCap,
})
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) {
expect := []byte("123")
buf := make([]byte, 64)
for _, bb := range []struct {
name string
request string
handler HandlerFunc
}{
{
name: "GETWithHeadersAndQuery",
request: "GET /?abc=123 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.StageHeader("Content-Type", "text/plain")
ex.StageHeaderInt("Content-Length", int64(len(benchBody)), 10)
data, present := ex.RequestQueryAppend(buf[:0], "abc", true)
if !present || !internal.BytesEqual(data, expect) {
panic("invalid result")
}
ex.WriteBody(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 i := 0; i < b.N; i++ {
conn.rewind()
exch.Release()
exch.Acquire(conn)
Handle(exch, &mux, nopBackoff)
}
})
}
}
// benchForm is package level so the Form's pair slice is reused across requests,
// as a real handler holding one per goroutine would.
var benchForm httpraw.Form
// BenchmarkRequestParseForm measures reading and parsing a urlencoded body into
// a buffer the caller owns. Nothing on the path may allocate.
func BenchmarkRequestParseForm(b *testing.B) {
const request = "POST /f HTTP/1.1\r\nHost: tinygo.org\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\nContent-Length: 27\r\n\r\n" +
"user=gopher&msg=hello+world"
buf := make([]byte, 64)
var mux MuxSlice
mux.Handle("POST /f", func(ex *Exchange) {
err := ex.RequestParseForm(&benchForm, buf)
if err != nil || benchForm.Len() != 2 {
panic("invalid result")
}
})
conn := &benchConn{request: request}
exch := benchExchange(b, conn)
b.ReportAllocs()
b.SetBytes(int64(len(request)))
b.ResetTimer()
for b.Loop() {
conn.rewind()
exch.Release()
exch.Acquire(conn)
Handle(exch, &mux, nopBackoff)
}
}