~bigbes/sr-ht-ecore

92fea80afcf9d123988f58fbf2aea125124d7a1c — Eugene Blikh 2 days ago 3aa1fcf
benchmarks for the hot path

Twenty-nine of them, over what every request of every service actually
touches: working-token validation, grant parsing and matching, the chi and
net/http middleware, and the same-origin guard.

They exist to be uploaded. This library had no CI and no benchmarks at all
while seven services pin it, so nothing measured a regression here.
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)
	}
}