scenario

package
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 29, 2026 License: MIT Imports: 9 Imported by: 0

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

View Source
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

func Assert(tb testing.TB, stack Stack)

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

func RegisterEnvelopedRoutes(api huma.API)

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

func RegisterRoutes(api huma.API)

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

func RunBenchmark(b *testing.B, stack Stack)

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.).

func RunParallelBenchmark added in v0.1.9

func RunParallelBenchmark(b *testing.B, stack Stack)

RunParallelBenchmark reports how a stack scales across CPU cores by driving the plaintext and valid-POST scenarios concurrently with b.RunParallel, as b.Run sub-benchmarks. It is the contention counterpart to RunBenchmark, whose single-goroutine numbers hide any per-request serialization: a stack that funnels every request through a shared lock barely improves here as GOMAXPROCS rises, while a lock-free stack approaches linear scaling. That gap is exactly what a single-goroutine microbench cannot surface (issue #243, motivated by the metrics-mutex finding in #239).

Read a row by comparing its per-op time across -cpu values, e.g.:

go test ./benchmarks/micro/... -bench=BenchmarkFrameworkTaxParallel -benchmem -cpu=1,2,4,8,16

A row whose ns/op falls roughly in proportion to the core count scales; a row whose ns/op flattens is contention-bound. The shared plaintext GET request is safe to reuse across goroutines: none of the four stacks mutate the inbound *http.Request in place (context changes go through Request.WithContext, which returns a copy). POST cannot share a request — a drained body can't be re-read — so each iteration builds its own.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL