Documentation
¶
Overview ¶
Package scenario defines the five request/response scenarios shared by every row of the BENCH-1 framework-tax microbenchmark matrix (issue #141 "1. Go abstraction-overhead microbenchmarks"): net/http -> Gin -> Huma+Gin -> Gombit, in benchmarks/micro/{nethttp,gin,huma,gombit}. Each stack implements the scenarios idiomatically, but they share this package's resource types so the comparison stays apples-to-apples: every stack serializes the same BenchUser shape.
This package intentionally does not depend on internal/contractspike. The M0-2 spike that lives there (docs/spikes/M0-2_HUMA_GIN_SPIKE.md) is preserved as a historical, standalone artifact; this matrix supersedes it as the ongoing benchmark but does not extend its code.
Index ¶
- Constants
- func Assert(tb testing.TB, stack Stack)
- func Do(handler http.Handler, method, path, body string) *httptest.ResponseRecorder
- func RegisterEnvelopedRoutes(api huma.API)
- func RegisterRoutes(api huma.API)
- func RunBenchmark(b *testing.B, stack Stack)
- type BenchUser
- type CreateBenchUserBody
- type Stack
- type SuccessEnvelope
Constants ¶
const ( ValidCreateUserBody = `{"name":"Ada Lovelace","email":"ada@example.com"}` InvalidCreateUserBody = `{"name":"","email":"not-an-email"}` )
Fixture request bodies shared by every stack's POST /users scenarios.
Variables ¶
This section is empty.
Functions ¶
func Assert ¶
Assert exercises the five framework-tax benchmark scenarios against stack.Handler and fails tb if the stack doesn't implement them correctly. It decodes and checks JSON structurally (not by substring), so a handler that stops doing real JSON serialization work can't silently keep passing: a benchmark over a broken or short-circuited handler is meaningless.
func Do ¶
func Do(handler http.Handler, method, path, body string) *httptest.ResponseRecorder
Do sends a request through handler and returns the recorded response. It is exported so benchmark loops (not just Assert) can reuse the exact same request construction the correctness checks use.
func RegisterEnvelopedRoutes ¶
RegisterEnvelopedRoutes registers the same four scenarios as RegisterRoutes, but wrapped in the D10 SuccessEnvelope, matching what a real Gombit app returns. Used by the Gombit row (benchmarks/micro/gombit) so the delta against RegisterRoutes (bare Huma+Gin) isolates the Gombit runtime's own cost — request-id, security headers, XSS sanitization, D10 error mapping, and this envelope — rather than conflating it with a response-shape choice bare Huma+Gin never made.
func RegisterRoutes ¶
RegisterRoutes registers the four Huma-typed scenario routes (plaintext, JSON, path parameter, validated POST) onto api without the D10 envelope — what a bare Huma+Gin app returns left to Huma's own defaults. Used by the bare Huma+Gin row (benchmarks/micro/huma).
func RunBenchmark ¶
RunBenchmark runs the five framework-tax benchmark scenarios (plaintext, JSON, path parameter, valid POST, invalid POST) against stack.Handler as b.Run sub-benchmarks, reporting ns/op, B/op, and allocs/op for each — of the handler alone, not of the benchmark harness. It is shared by every row (benchmarks/micro/{nethttp,gin,huma,gombit}) so the sub-benchmark set and the request-timing methodology can't drift between packages.
Every scenario builds its *http.Request once, outside the timed loop, and reuses it: none of the four stacks' middleware mutates a request in a way that corrupts reuse (verified — Gombit's XSS-sanitization middleware, the only header/body-mutating middleware in the runtime stack, replaces Body/Content-Length on the WithContext-derived request Gin's own machinery hands to handlers, not the shared object this loop holds; every request-derived context change goes through http.Request.WithContext, which returns a new Request rather than mutating the original; confirmed empirically with a 30-iteration POST reuse loop against a full framework.App before relying on it here). Skipping this hoist and building a fresh request every iteration measured the harness, not the handler: on a trivial "write a canned string" handler, per-iteration httptest.NewRequest+NewRecorder construction cost several times what the handler itself allocated, which — being roughly constant across all four stacks — compressed the very ratios this benchmark exists to report.
GET scenarios have no body to manage. POST scenarios can't reuse the *http.Response body reader itself (an io.Reader that's been drained can't be un-drained), so each iteration replaces Body with a fresh io.NopCloser over the same payload bytes and resets ContentLength — cheap relative to constructing a whole new *http.Request via httptest.NewRequest (which re-parses the URL, rebuilds the request line, etc.).
Types ¶
type BenchUser ¶
type BenchUser struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
BenchUser is the JSON resource shared by the path-parameter and POST scenarios. net/http, Gin, and bare Huma+Gin return it bare; only Gombit wraps it in SuccessEnvelope, matching what each stack would actually do left to its own defaults.
type CreateBenchUserBody ¶
type CreateBenchUserBody struct {
Name string `json:"name" minLength:"1" maxLength:"80" example:"Ada Lovelace" doc:"Human-readable user name"`
Email string `json:"email" pattern:"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$" example:"ada@example.com" doc:"Contact email address"`
}
CreateBenchUserBody is the validated POST /users request body. Huma and Gombit enforce the pattern tag at request-validation time (not just in the generated OpenAPI schema), so the invalid-POST scenario exercises real validation work; net/http and Gin reuse this struct's JSON shape for their own idiomatic (manual / binding-tag) validation.
type Stack ¶
type Stack struct {
Name string
Handler http.Handler
// Envelope is true for stacks that wrap JSON responses in
// SuccessEnvelope (Gombit only — see RegisterEnvelopedRoutes); false for
// stacks that return the resource bare (net/http, Gin, and bare Huma+Gin
// via RegisterRoutes), each idiomatic to its own stack.
Envelope bool
}
Stack is one row of the framework-tax matrix under test.
type SuccessEnvelope ¶
type SuccessEnvelope[T any] struct { Data T `json:"data"` }
SuccessEnvelope is the D10 success response shape. It is a Gombit convention, not something Huma imposes by default, so only RegisterEnvelopedRoutes (the Gombit row) uses it — RegisterRoutes (the bare Huma+Gin row) does not. Wrapping both rows in it would bill Gombit's envelope cost to the Huma+Gin baseline too and understate the delta between them, which is the whole point of comparing those two rows.