Files
zip/bench_test.go
T
zeekayandHanzo Dev 5f71d2496b The router is three nouns, and every one of them is concrete
App, Group, Operation. A group is a prefix and what holds under it, an operation
is one typed route declared there, and the verb methods are generic methods —
which Go 1.27 allows on a concrete type and still refuses on an interface. So In
and Out come from the handler and the caller writes no type arguments:

	users := app.Group("/v1/users")
	users.Post("/", api.Create)

	mint := users.Group("/mint").With(middleware.Mint(gate))
	mint.Post("/deposit", api.Deposit)

A Group is a CLOSED HANDLE: two unexported fields, and no method that hands back
the app, the parent or the fiber underneath. A subsystem given the group at its
own prefix cannot reach above it, so confinement is a property of the type
instead of a rule something has to police — which is what a host needs in order
to hand out a bounded surface and know what comes back.

That is bought by giving up the router interface, deliberately. Router and
OpTarget are gone, and with them the ability to implement a route table outside
this package: a decorator is no longer a router, a gate is middleware on a group,
and a bound is a path. The five package-level verb functions go the same way —
a registration is a method on the thing it registers into.

Raw carries what is not a typed operation: a stream, an upgrade, a probe, a
foreign subtree. It names its method, and it is the longer spelling because
opting out of the typed model is the exception and should read like one.



Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 01:39:17 -07:00

217 lines
7.2 KiB
Go

package zip_test
// Framework-tax benchmarks for zip over its underlying Fiber v3 fork.
//
// Two costs are isolated here, each against a hand-written baseline so the
// number is the *tax*, not the absolute:
//
// 1. Per-request wrapper tax (Benchmark_ZipTax): zip.App vs a raw fiber.App
// serving the identical route with an equivalent minimal handler. The delta
// is what zip's Handler wrapper costs per request — one &Ctx heap value
// (fc, app, log) materialised by toFiberHandler, plus the indirection.
//
// 2. Typed-route tax (Benchmark_TypedRoute): the generic Post[In,Out] (decode →
// validate → run → encode, transport-agnostic op.invoke core) vs an
// idiomatic hand-written zip handler doing c.Bind + c.JSON. The delta is
// what the generic sugar (and the OpenAPI/MCP registry it feeds) costs at
// request time.
//
// Both dispatch through app.Fiber().Handler() on a reused *fasthttp.RequestCtx —
// the low-overhead router idiom, so the framework tax isn't buried under
// net/http↔fasthttp conversion (as it is in the app.Test path used by
// json_bench_test.go). chatRequest / chatResponse / benchReqBody are shared with
// json_bench_test.go (same package).
//
// Run:
//
// go test -run='^$' -bench='Benchmark_ZipTax|Benchmark_TypedRoute' -benchmem -count=6 .
//
// Numbers and methodology: BENCHMARKS.md.
import (
"context"
"fmt"
"testing"
"github.com/valyala/fasthttp"
"github.com/zap-proto/fiber/v3"
"github.com/zap-proto/zip"
)
// zipTaxRoutes covers the routing shapes a wrapper tax should be constant
// across: a static route and a single-param route.
var zipTaxRoutes = []struct {
name string
route string // registration pattern
path string // concrete request path
}{
{"static", "/v1/health", "/v1/health"},
{"param", "/v1/tracker/:id", "/v1/tracker/trk_abc123"},
}
func benchConfig() zip.Config {
return zip.Config{DisableStartupMessage: true, ServerHeader: "-"}
}
// getFctx builds a reusable GET request context.
func getFctx(path string) *fasthttp.RequestCtx {
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod("GET")
fctx.URI().SetPath(path)
return fctx
}
// Benchmark_ZipTax measures zip.App's per-request overhead against a raw
// fiber.App. Handlers are equivalent to the byte: zip's NoContent(204) is
// c.fc.Status(204)+return nil; the fiber baseline is c.Status(204)+return nil.
// The only difference under test is zip's wrapper (the &Ctx materialised per
// request). Expect ~constant delta across routing shapes.
//
// The RequestCtx is reused across iterations, so each one must start where a
// real request starts: with the previous request's user values cleared. Both
// transports do exactly this before dispatching (zap-proto/http's serveConn;
// fasthttp's keep-alive loop, via Request.Reset). Without it an iteration
// inherits the last one's request-scoped state and the wrapper looks free —
// it would report 0 allocs/op for a path that allocates one *Ctx per request.
func Benchmark_ZipTax(b *testing.B) {
za := zip.New(benchConfig())
fa := fiber.New()
for _, r := range zipTaxRoutes {
za.Raw("GET", r.route, func(c *zip.Ctx) error { return c.NoContent(204) })
fa.Get(r.route, func(c fiber.Ctx) error { c.Status(204); return nil })
}
zh := za.Fiber().Handler()
fh := fa.Handler()
for _, r := range zipTaxRoutes {
fctx := getFctx(r.path)
b.Run(r.name+"/zip", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
fctx.ResetUserValues()
zh(fctx)
}
if fctx.Response.StatusCode() != 204 {
b.Fatalf("zip: status %d", fctx.Response.StatusCode())
}
})
b.Run(r.name+"/fiber", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
fctx.ResetUserValues()
fh(fctx)
}
if fctx.Response.StatusCode() != 204 {
b.Fatalf("fiber: status %d", fctx.Response.StatusCode())
}
})
}
}
// Benchmark_ChainTax measures the wrapper tax along the other axis a real
// service moves on: how many handlers the request passes through. Benchmark_ZipTax
// fixes that at one; every deployed service runs a Use stack above its leaf, so
// this is the number the fleet actually pays. Handlers are pass-through, so the
// only thing growing with depth is zip's wrapper.
func Benchmark_ChainTax(b *testing.B) {
for _, mw := range []int{0, 1, 3, 5} {
za := zip.New(benchConfig())
fa := fiber.New()
for i := 0; i < mw; i++ {
za.Use(zip.H(func(c *zip.Ctx) error { return c.Continue() }))
fa.Use(func(c fiber.Ctx) error { return c.Next() })
}
za.Raw("GET", "/v1/health", func(c *zip.Ctx) error { return c.NoContent(204) })
fa.Get("/v1/health", func(c fiber.Ctx) error { c.Status(204); return nil })
for name, h := range map[string]fasthttp.RequestHandler{
"zip": za.Fiber().Handler(), "fiber": fa.Handler(),
} {
b.Run(fmt.Sprintf("mw=%d/%s", mw, name), func(b *testing.B) {
fctx := getFctx("/v1/health")
b.ReportAllocs()
for b.Loop() {
fctx.ResetUserValues()
h(fctx)
}
if fctx.Response.StatusCode() != 204 {
b.Fatalf("%s: status %d", name, fctx.Response.StatusCode())
}
})
}
}
}
// postFctx builds a reusable POST request context carrying a JSON body.
func postFctx(path string, body []byte) *fasthttp.RequestCtx {
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod("POST")
fctx.Request.SetRequestURI(path)
fctx.Request.Header.SetContentType("application/json")
fctx.Request.SetBody(body)
return fctx
}
// makeChatResponse builds the response both handler variants return, so the
// encode side of the comparison is identical.
func makeChatResponse(model string) chatResponse {
return chatResponse{
ID: "chatcmpl-bench",
Model: model,
Content: "Cars are flying. Wire stack: ingress→gateway→subsystem; JSON at edge, ZAP between.",
PromptTokens: 27,
CompletionTokens: 19,
TotalTokens: 46,
FinishReason: "stop",
Latency: 42.5,
}
}
// Benchmark_TypedRoute compares the generic Post[In,Out] against an idiomatic
// hand-written zip handler doing the same decode → build → encode work. The
// delta is the generic wrapper's per-request tax (the op.invoke indirection and
// the any-typed result the OpenAPI/MCP projections require).
func Benchmark_TypedRoute(b *testing.B) {
// Generic typed route.
ta := zip.New(benchConfig())
ta.Post("/v1/chat", func(_ context.Context, in *chatRequest) (*chatResponse, error) {
out := makeChatResponse(in.Model)
return &out, nil
})
th := ta.Fiber().Handler()
// Hand-written zip handler — same work, no generics.
ha := zip.New(benchConfig())
ha.Raw("POST", "/v1/chat", func(c *zip.Ctx) error {
var in chatRequest
if err := c.Bind(&in); err != nil {
return err
}
out := makeChatResponse(in.Model)
return c.JSON(200, &out)
})
hh := ha.Fiber().Handler()
b.Run("typed", func(b *testing.B) {
fctx := postFctx("/v1/chat", benchReqBody)
b.ReportAllocs()
for b.Loop() {
th(fctx)
}
if sc := fctx.Response.StatusCode(); sc != 200 {
b.Fatalf("typed: status %d", sc)
}
})
b.Run("handwritten", func(b *testing.B) {
fctx := postFctx("/v1/chat", benchReqBody)
b.ReportAllocs()
for b.Loop() {
hh(fctx)
}
if sc := fctx.Response.StatusCode(); sc != 200 {
b.Fatalf("handwritten: status %d", sc)
}
})
}