A bearer/bearer_bench_test.go => bearer/bearer_bench_test.go +178 -0
@@ 0,0 1,178 @@
+package bearer
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+// The grant strings these benchmarks present. The stateless one is what a short
+// token carries; the registered one adds the id: member that turns step 4 from
+// a no-op into a cache lookup.
+const (
+ benchGrants = "bench:upload cov:upload artifacts:upload"
+ benchGrantsRegistered = benchGrants + " id:4711"
+)
+
+// The sinks keep a validated token from being discarded as an unread result.
+var (
+ sinkToken *Token
+ sinkErr error
+)
+
+// liveDaemon stands in for tokens.sr.ht's revocation endpoint and answers 204
+// — live — to everything.
+//
+// It is only ever asked once per benchmark: the warm-up call below populates
+// the cache, and every measured iteration is then the cached path, which is the
+// one a running service spends its time in. A benchmark that hit the server on
+// every iteration would be measuring httptest's loopback, not this package.
+func liveDaemon(b *testing.B) *httptest.Server {
+ b.Helper()
+
+ s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusNoContent)
+ }))
+ b.Cleanup(s.Close)
+ return s
+}
+
+// benchValidator builds a validator pointed at a daemon that says "live".
+func benchValidator(b *testing.B) *Validator {
+ b.Helper()
+
+ s := liveDaemon(b)
+ v, err := New(Options{
+ Origin: s.URL,
+ ClientID: callerClientID,
+ NodeID: callerNodeID,
+ HTTPClient: s.Client(),
+ })
+ if err != nil {
+ b.Fatalf("building the validator: %v", err)
+ }
+ return v
+}
+
+// BenchmarkValidate is the whole of SPEC ch. 6 on one presented token, which is
+// what a service pays per authenticated request.
+//
+// The four cases are the four outcomes that happen in production:
+//
+// - stateless: an HMAC, a grant parse and a map lookup, and no network at all.
+// This is the common case under the default configuration and the number
+// that matters.
+// - registered_cached: the same, plus step 4 answered from the cache under the
+// validator's mutex. The difference between it and stateless is the price of
+// revocability.
+// - forbidden: a good credential that does not carry the action. It costs more
+// than a success, because the refusal renders the grant set into its message
+// — worth knowing, since a misconfigured client retries this in a loop.
+// - invalid: junk. Step 1 refuses it without allocating a token, and this is
+// the path an instance under a flood of forged credentials runs; it must
+// stay the cheapest thing here.
+func BenchmarkValidate(b *testing.B) {
+ ctx := context.Background()
+ v := benchValidator(b)
+
+ stateless := ourToken(benchGrants)
+ registered := ourToken(benchGrantsRegistered)
+
+ // Warm the revocation cache, and check that the fixture validates at all —
+ // a token that fails here would turn every measured iteration into an early
+ // refusal and report it as a fast success.
+ if _, err := v.Validate(ctx, registered, "bench:upload"); err != nil {
+ b.Fatalf("the registered fixture must validate: %v", err)
+ }
+
+ b.Run("stateless", func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkToken, sinkErr = v.Validate(ctx, stateless, "bench:upload")
+ }
+ if sinkErr != nil {
+ b.Fatalf("unexpected refusal: %v", sinkErr)
+ }
+ })
+
+ b.Run("registered_cached", func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkToken, sinkErr = v.Validate(ctx, registered, "bench:upload")
+ }
+ if sinkErr != nil {
+ b.Fatalf("unexpected refusal: %v", sinkErr)
+ }
+ })
+
+ b.Run("forbidden", func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkToken, sinkErr = v.Validate(ctx, stateless, "dolt:push")
+ }
+ if !errors.Is(sinkErr, ErrForbidden) {
+ b.Fatalf("this case must be refused with ErrForbidden, got %v", sinkErr)
+ }
+ })
+
+ b.Run("invalid", func(b *testing.B) {
+ // Not a token: the shape a scanner presents.
+ const junk = "not-a-token-at-all-just-a-string-somebody-tried"
+
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkToken, sinkErr = v.Validate(ctx, junk, "bench:upload")
+ }
+ if !errors.Is(sinkErr, ErrInvalid) {
+ b.Fatalf("this case must be refused with ErrInvalid, got %v", sinkErr)
+ }
+ })
+}
+
+// BenchmarkInspect is the path a service's identity middleware runs: the same
+// four steps without step 3, once per request, upstream of the router. Read it
+// beside BenchmarkValidate/stateless — the gap is the grant check that Inspect
+// leaves to the handler.
+func BenchmarkInspect(b *testing.B) {
+ ctx := context.Background()
+ v := benchValidator(b)
+ presented := ourToken(benchGrants)
+
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkToken, sinkErr = v.Inspect(ctx, presented)
+ }
+ if sinkErr != nil {
+ b.Fatalf("unexpected refusal: %v", sinkErr)
+ }
+}
+
+// BenchmarkValidateParallel is the same registered token validated from many
+// goroutines at once, which is how a service actually holds a Validator: one of
+// them, behind every request handler.
+//
+// The revocation cache is a map behind a single mutex, so this is where that
+// choice shows up. A per-request HMAC is embarrassingly parallel; a shared lock
+// is not, and the point of measuring it is to know which of the two dominates
+// before somebody proposes sharding the cache.
+func BenchmarkValidateParallel(b *testing.B) {
+ ctx := context.Background()
+ v := benchValidator(b)
+ registered := ourToken(benchGrantsRegistered)
+
+ if _, err := v.Validate(ctx, registered, "bench:upload"); err != nil {
+ b.Fatalf("the registered fixture must validate: %v", err)
+ }
+
+ b.ReportAllocs()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ if _, err := v.Validate(ctx, registered, "bench:upload"); err != nil {
+ b.Errorf("unexpected refusal: %v", err)
+ return
+ }
+ }
+ })
+}
A chimw/chimw_bench_test.go => chimw/chimw_bench_test.go +125 -0
@@ 0,0 1,125 @@
+package chimw
+
+import (
+ "io"
+ "log/slog"
+ "net/http"
+ "testing"
+
+ "github.com/go-chi/chi/v5"
+ chimiddleware "github.com/go-chi/chi/v5/middleware"
+)
+
+// nullWriter is a ResponseWriter that keeps nothing; see the same type in
+// middleware for why a recorder is the wrong instrument here.
+type nullWriter struct{ header http.Header }
+
+func (w *nullWriter) Header() http.Header { return w.header }
+func (w *nullWriter) Write(b []byte) (int, error) { return len(b), nil }
+func (w *nullWriter) WriteHeader(int) {}
+
+// discardLogger is a real handler doing real encoding work, writing nowhere.
+// The formatter's cost is the attributes it builds and the record the handler
+// encodes; sending that to a buffer would grow one by b.N records and measure
+// the allocator instead.
+func discardLogger() *slog.Logger {
+ return slog.New(slog.NewJSONHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug}))
+}
+
+// benchRouter mounts one handler at /page behind the middlewares the case
+// wants, which is how a service installs them.
+func benchRouter(f SlogFormatter, withRequestID bool) http.Handler {
+ r := chi.NewRouter()
+ if withRequestID {
+ r.Use(chimiddleware.RequestID)
+ }
+ r.Use(RequestLogger(f))
+ r.Get("/page", func(w http.ResponseWriter, _ *http.Request) {
+ _, _ = w.Write([]byte("ok"))
+ })
+ return r
+}
+
+// benchRequest is reused across iterations: chi routes on a copy carrying its
+// own route context and leaves this value alone.
+func benchRequest(b *testing.B, path string) *http.Request {
+ b.Helper()
+
+ r, err := http.NewRequest(http.MethodGet, "http://bench.example.org"+path, nil)
+ if err != nil {
+ b.Fatalf("building the request: %v", err)
+ }
+ return r
+}
+
+// BenchmarkRequestLogger is the outermost middleware of every service on the
+// instance: one record per request, built and encoded on the request's own
+// goroutine before it returns.
+//
+// The three cases are the three shapes that exist in production. "logged" is
+// the ordinary request. "with_request_id" adds chi's RequestID above, which is
+// how these routers are actually wired and which costs a context lookup plus a
+// sixth attribute. "skipped" is a probe hitting /healthz every second — it must
+// stay far cheaper than the other two, because that is the entire reason Skip
+// exists.
+func BenchmarkRequestLogger(b *testing.B) {
+ logger := discardLogger()
+
+ b.Run("logged", func(b *testing.B) {
+ h := benchRouter(SlogFormatter{Logger: logger}, false)
+ r := benchRequest(b, "/page")
+
+ b.ReportAllocs()
+ for b.Loop() {
+ h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
+ }
+ })
+
+ b.Run("with_request_id", func(b *testing.B) {
+ h := benchRouter(SlogFormatter{Logger: logger}, true)
+ r := benchRequest(b, "/page")
+
+ b.ReportAllocs()
+ for b.Loop() {
+ h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
+ }
+ })
+
+ b.Run("skipped", func(b *testing.B) {
+ h := benchRouter(SlogFormatter{
+ Logger: logger,
+ Skip: SkipPaths("/page"),
+ }, false)
+ r := benchRequest(b, "/page")
+
+ b.ReportAllocs()
+ for b.Loop() {
+ h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
+ }
+ })
+}
+
+// BenchmarkGetHead measures the pair a page route registers: the GET a viewer
+// makes and the HEAD a monitor or a proxy makes through the same handler. Both
+// go through the routing tree this package writes into, so this is the cost of
+// the convenience rather than of chi.
+func BenchmarkGetHead(b *testing.B) {
+ r := chi.NewRouter()
+ GetHead(r, "/page", func(w http.ResponseWriter, _ *http.Request) {
+ _, _ = w.Write([]byte("ok"))
+ })
+
+ for _, method := range []string{http.MethodGet, http.MethodHead} {
+ b.Run(method, func(b *testing.B) {
+ req, err := http.NewRequest(method, "http://bench.example.org/page", nil)
+ if err != nil {
+ b.Fatalf("building the request: %v", err)
+ }
+
+ b.ReportAllocs()
+ for b.Loop() {
+ r.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, req)
+ }
+ })
+ }
+}
A csrf/csrf_bench_test.go => csrf/csrf_bench_test.go +80 -0
@@ 0,0 1,80 @@
+package csrf
+
+import (
+ "net/http"
+ "testing"
+)
+
+// nullWriter is a ResponseWriter that keeps nothing. httptest.NewRecorder grows
+// a buffer and a header map per request, and this measurement is of the guard
+// rather than of the recorder.
+type nullWriter struct{ header http.Header }
+
+func (w *nullWriter) Header() http.Header { return w.header }
+func (w *nullWriter) Write(b []byte) (int, error) { return len(b), nil }
+func (w *nullWriter) WriteHeader(int) {}
+
+// The sink keeps a benchmarked predicate from being discarded as dead code.
+var sinkBool bool
+
+// BenchmarkRequire measures the middleware as it is installed — in front of the
+// whole router, on every request, so the safe-method case below is what the
+// overwhelming majority of an instance's traffic pays.
+//
+// The four cases are the four paths through claimMatches: a method that is
+// exempt without looking at a header, a POST that names us in Origin, a POST
+// that only carries a Referer (which is one more Get and one more parse), and a
+// POST from somewhere else, which reaches the deny handler.
+func BenchmarkRequire(b *testing.B) {
+ // A handler that does nothing: what is measured is the chain above it.
+ next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
+ guarded := Require(selfOrigin, nil)(next)
+
+ cases := []struct {
+ name string
+ req *http.Request
+ }{
+ {"safe_method", request(http.MethodGet, nil)},
+ {"origin_match", request(http.MethodPost, map[string]string{"Origin": selfOrigin})},
+ {"referer_only", request(http.MethodPost, map[string]string{
+ "Referer": selfOrigin + "/settings/tokens",
+ })},
+ {"denied", request(http.MethodPost, map[string]string{"Origin": "https://evil.example.net"})},
+ }
+
+ for _, c := range cases {
+ b.Run(c.name, func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ // A fresh header map per iteration: the deny path writes into
+ // it, and reusing one would measure a second write to a map
+ // that already has the key.
+ guarded.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, c.req)
+ }
+ })
+ }
+}
+
+// BenchmarkSameOrigin is the exported predicate, and it is here to be read next
+// to BenchmarkRequire: the middleware parses selfOrigin once at construction
+// and this one parses it again on every call. That difference is the argument
+// the package comment makes for preferring Require, in nanoseconds.
+func BenchmarkSameOrigin(b *testing.B) {
+ r := request(http.MethodPost, map[string]string{"Origin": selfOrigin})
+
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkBool = SameOrigin(r, selfOrigin)
+ }
+}
+
+// BenchmarkSafeMethod is the first thing every request meets. It is a switch,
+// so the number is expected to be uninteresting — which is the point: if it
+// ever stops being uninteresting, something grew a map or a string compare on
+// the path in front of the whole router.
+func BenchmarkSafeMethod(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkBool = SafeMethod(http.MethodGet)
+ }
+}
A grants/grants_bench_test.go => grants/grants_bench_test.go +124 -0
@@ 0,0 1,124 @@
+package grants
+
+import "testing"
+
+// The grant string these benchmarks work on is the shape a real working token
+// carries on this instance: a handful of <service>:<action> members from the
+// vocabulary of SPEC ch. 3, plus the reserved id: member a registered token is
+// stamped with. It is deliberately unsorted — Parse sorts on the way out, and a
+// pre-sorted input would measure a cheaper parse than the one that runs.
+const benchGrantString = "dolt:push bench:upload cov:upload artifacts:upload dolt:pull meta:profile id:4711"
+
+// The sinks exist so that nothing below can be discarded as a call whose result
+// is never read. b.Loop already keeps the call itself, but the assignment is
+// what keeps the *value* alive across the toolchain versions this builds on.
+var (
+ sinkGrants Grants
+ sinkBool bool
+ sinkString string
+ sinkErr error
+)
+
+// BenchmarkParse is the per-request cost of this package, not a corner of it:
+// bearer.decodeOurs parses the grant string of every presented token on every
+// request, so this allocation profile is the one every service pays per call.
+func BenchmarkParse(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkGrants, sinkErr = Parse(benchGrantString)
+ }
+ if sinkErr != nil {
+ b.Fatalf("the fixture must parse: %v", sinkErr)
+ }
+}
+
+// BenchmarkParseRequested is the mint path — the same parse with the reserved
+// id: member refused. It is here beside Parse because the refusal is a
+// privilege boundary, and a change that made it cost noticeably more than the
+// stored parse would be a change worth seeing.
+func BenchmarkParseRequested(b *testing.B) {
+ // Without the id: member, which ParseRequested refuses by design.
+ const requested = "dolt:push bench:upload cov:upload artifacts:upload dolt:pull meta:profile"
+
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkGrants, sinkErr = ParseRequested(requested)
+ }
+ if sinkErr != nil {
+ b.Fatalf("the fixture must parse: %v", sinkErr)
+ }
+}
+
+// BenchmarkHas is step 3 of the validation: one map lookup, taken on every
+// authorized request. The miss is measured beside the hit because a refusal is
+// what a flood of ill-scoped tokens produces, and the two must cost the same —
+// a set whose miss is slower than its hit answers "was this refused?" to
+// anybody who can time it.
+func BenchmarkHas(b *testing.B) {
+ g, err := Parse(benchGrantString)
+ if err != nil {
+ b.Fatalf("the fixture must parse: %v", err)
+ }
+ universal := All()
+
+ b.Run("hit", func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkBool = g.Has("bench:upload")
+ }
+ })
+ b.Run("miss", func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkBool = g.Has("dolt:admin")
+ }
+ })
+ b.Run("universal", func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkBool = universal.Has("bench:upload")
+ }
+ })
+}
+
+// BenchmarkIsSubsetOf is the narrowing rule of SPEC ch. 2, which runs once per
+// exchange. The "narrower" case walks the whole member set and is the one that
+// bounds the cost; "wider" is the early refusal.
+func BenchmarkIsSubsetOf(b *testing.B) {
+ parent, err := Parse(benchGrantString)
+ if err != nil {
+ b.Fatalf("the fixture must parse: %v", err)
+ }
+ child, err := Parse("bench:upload cov:upload id:8123")
+ if err != nil {
+ b.Fatalf("the fixture must parse: %v", err)
+ }
+
+ b.Run("narrower", func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkBool = child.IsSubsetOf(parent)
+ }
+ })
+ b.Run("wider", func(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkBool = parent.IsSubsetOf(child)
+ }
+ })
+}
+
+// BenchmarkString is the render half: sorting the members and appending the
+// id:. It runs whenever a set is written back into a token payload or a
+// database column, and it is the one operation here that sorts.
+func BenchmarkString(b *testing.B) {
+ g, err := Parse(benchGrantString)
+ if err != nil {
+ b.Fatalf("the fixture must parse: %v", err)
+ }
+
+ b.ReportAllocs()
+ for b.Loop() {
+ sinkString = g.String()
+ }
+}
A middleware/middleware_bench_test.go => middleware/middleware_bench_test.go +112 -0
@@ 0,0 1,112 @@
+package middleware
+
+import (
+ "io"
+ "log/slog"
+ "net/http"
+ "testing"
+)
+
+// nullWriter is a ResponseWriter that keeps nothing: httptest.NewRecorder grows
+// a buffer per request and this measurement is of the middleware.
+type nullWriter struct{ header http.Header }
+
+func (w *nullWriter) Header() http.Header { return w.header }
+func (w *nullWriter) Write(b []byte) (int, error) { return len(b), nil }
+func (w *nullWriter) WriteHeader(int) {}
+
+// discardLog silences the default logger for one benchmark. The panic case
+// below logs a stack per iteration, and without this the run would be measuring
+// the terminal as much as the middleware — and would bury the result under it.
+func discardLog(b *testing.B) {
+ b.Helper()
+
+ previous := slog.Default()
+ slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{
+ Level: slog.LevelDebug,
+ })))
+ b.Cleanup(func() { slog.SetDefault(previous) })
+}
+
+// benchRequest is the request every case below serves. chi and net/http do not
+// mutate it — a router that needs a context puts the new one on a copy — so one
+// value is safe to reuse across iterations, and building it per iteration would
+// measure httptest.NewRequest.
+func benchRequest(b *testing.B) *http.Request {
+ b.Helper()
+
+ r, err := http.NewRequest(http.MethodGet, "http://bench.example.org/tokens", nil)
+ if err != nil {
+ b.Fatalf("building the request: %v", err)
+ }
+ return r
+}
+
+// BenchmarkPrivateCache is two header writes in front of every response on the
+// instance — the cheapest middleware here and the one on the most paths.
+func BenchmarkPrivateCache(b *testing.B) {
+ h := PrivateCache(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+ r := benchRequest(b)
+
+ b.ReportAllocs()
+ for b.Loop() {
+ h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
+ }
+}
+
+// BenchmarkRecoverPanics measures both halves of the guard.
+//
+// "clean" is what every request that does not panic pays: one wrapper
+// allocation, one deferred recover. It is the number that matters, because it
+// is charged to the whole surface for the benefit of the rare request below.
+//
+// "panicking" is the rendered 500, stack capture and log record included. It is
+// slow on purpose — debug.Stack() walks the goroutine — and it is here so that
+// a change which makes an already bad minute worse is visible.
+func BenchmarkRecoverPanics(b *testing.B) {
+ discardLog(b)
+
+ r := benchRequest(b)
+
+ b.Run("clean", func(b *testing.B) {
+ h := RecoverPanics(renderInternal)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+
+ b.ReportAllocs()
+ for b.Loop() {
+ h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
+ }
+ })
+
+ b.Run("panicking", func(b *testing.B) {
+ h := RecoverPanics(renderInternal)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
+ panic("the store is not reachable")
+ }))
+
+ b.ReportAllocs()
+ for b.Loop() {
+ h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
+ }
+ })
+}
+
+// BenchmarkChain is the two middlewares as a service installs them, one inside
+// the other, so that the sum is measured rather than inferred from the two
+// numbers above — the wrapper allocations compose and the header writes do not.
+func BenchmarkChain(b *testing.B) {
+ discardLog(b)
+
+ h := PrivateCache(RecoverPanics(renderInternal)(
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })))
+ r := benchRequest(b)
+
+ b.ReportAllocs()
+ for b.Loop() {
+ h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
+ }
+}