From 06ddc47e225683335f3226911b3b762dc8c4f1d8 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:26:07 +0100 Subject: [PATCH] Measure route-normaliser allocations in an isolated child TestNormalizeRoute_RootFastPathDoesNotAllocate failed in the api-2 shard of PR 2445 with normalizeRoute("/") reporting 419 allocations per call, though the fast path only returns a constant. testing.AllocsPerRun diffs the process-wide malloc counter, so it also counts allocations made by parallel tests and by goroutines earlier tests leave running whenever the scheduler lets one in during the measurement. Under CPU contention with a leaked allocating goroutine, the old checks failed in 30 of 30 runs. Both zero-allocation tests (normalizeRoute's root fast path and normalizeSegment) now re-run themselves in a child of the same test binary, limited to that one test, where nothing else allocates. The assertion is unchanged: removing the fast path still fails with normalizeRoute("/") allocated 1 times per call. The same contended stress passes 30 of 30, and -count=200 passes locally and on pulse-dev, with and without -race. --- internal/api/http_metrics_test.go | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/internal/api/http_metrics_test.go b/internal/api/http_metrics_test.go index 9c68ab9038..d518d80e29 100644 --- a/internal/api/http_metrics_test.go +++ b/internal/api/http_metrics_test.go @@ -1,7 +1,11 @@ package api import ( + "os" + "os/exec" + "regexp" "strconv" + "strings" "testing" "time" @@ -198,7 +202,38 @@ func TestNormalizeSegment(t *testing.T) { // could copy a short segment into a stack buffer and hide the allocation. var normalizeSegmentSink string +// allocIsolationEnv names the test that a child process re-runs on its own. +const allocIsolationEnv = "PULSE_TEST_ALLOC_ISOLATION" + +// measureAllocsInIsolation reports whether the calling test should take its +// testing.AllocsPerRun measurements now. AllocsPerRun diffs the process-wide +// malloc counter, so in a full internal/api run it also counts allocations by +// parallel tests and by goroutines that earlier tests left running; CI once saw +// normalizeRoute("/") report 419 allocations per call that way. In the parent, +// it re-runs only the calling test in a child process of the same test binary, +// where nothing else allocates, fails if the child fails, and returns false. +// In that child, it returns true. +func measureAllocsInIsolation(t *testing.T) bool { + t.Helper() + if os.Getenv(allocIsolationEnv) == t.Name() { + return true + } + cmd := exec.Command(os.Args[0], "-test.run=^"+regexp.QuoteMeta(t.Name())+"$", "-test.v") + cmd.Env = append(os.Environ(), allocIsolationEnv+"="+t.Name()) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("isolated allocation check failed: %v\n%s", err, out) + } + if !strings.Contains(string(out), "--- PASS: "+t.Name()) { + t.Fatalf("isolated allocation check did not run %s:\n%s", t.Name(), out) + } + return false +} + func TestNormalizeSegment_DoesNotAllocate(t *testing.T) { + if !measureAllocsInIsolation(t) { + return + } for _, seg := range []string{ "12345", "550e8400-e29b-41d4-a716-446655440000", @@ -269,6 +304,9 @@ func TestNormalizeRoute(t *testing.T) { // The root and empty paths must return before splitting. Without the fast // path, "/" still normalizes to "/" but allocates a split slice on every call. func TestNormalizeRoute_RootFastPathDoesNotAllocate(t *testing.T) { + if !measureAllocsInIsolation(t) { + return + } for _, path := range []string{"/", ""} { if allocs := testing.AllocsPerRun(100, func() { _ = normalizeRoute(path) }); allocs != 0 { t.Errorf("normalizeRoute(%q) allocated %v times per call, want 0", path, allocs)