From 296058fb80b33579e9e4a9390f281d24ea8f9133 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 3 Sep 2026 21:46:22 +0200 Subject: [PATCH 1/3] Tests: allocation ceilings for deeply nested and wide renders --- perf_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 perf_test.go diff --git a/perf_test.go b/perf_test.go new file mode 100644 index 0000000..de03460 --- /dev/null +++ b/perf_test.go @@ -0,0 +1,76 @@ +package fejkdata + +import ( + "fmt" + "strings" + "testing" + "testing/fstest" +) + +// The renderer's cost scales with the shape it renders: a deep nest descends one +// level per field, a wide format expands one token per field. The ceiling tests +// below pin the allocations one render costs for those shapes, so a change that +// adds a per-level or per-token allocation (a lost pre-size, a per-item map, an +// extra copy) fails unless the baseline is bumped as a deliberate decision. + +// nestedJSON nests a template depth times: each level's format renders its field +// "a", which is the next template down, so one Fake call recurses depth levels. +func nestedJSON(depth int) string { + s := `"leaf"` + for i := 0; i < depth; i++ { + s = fmt.Sprintf(`{"format":"{a}","a":%s}`, s) + } + return s +} + +// wideTokenJSON is one format with n sibling fields, so one Fake call expands n +// tokens. +func wideTokenJSON(n int) string { + var toks, fields strings.Builder + for i := 0; i < n; i++ { + fmt.Fprintf(&toks, "{f%d}", i) + if i > 0 { + fields.WriteByte(',') + } + fmt.Fprintf(&fields, `"f%d":"x"`, i) + } + return fmt.Sprintf(`{"format":"%s",%s}`, toks.String(), fields.String()) +} + +// TestNoRenderAllocRegression fails when a render allocates more than 10% past its +// recorded baseline. Allocations are deterministic across machines, so this gate +// cannot flake under CI load the way a wall-clock ceiling would; a real slowdown +// almost always costs an allocation too. Raising a baseline here is a deliberate +// "we accept this cost" decision. +func TestNoRenderAllocRegression(t *testing.T) { + shapes := []struct { + name string + json string + base float64 + }{ + {"nested depth 25", nestedJSON(25), 27}, + {"nested depth 100", nestedJSON(100), 102}, + {"wide 500 tokens", wideTokenJSON(500), 9}, + } + for _, s := range shapes { + f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{"x.json": {Data: []byte(s.json)}})) + if err != nil { + t.Fatalf("New(%s): %v", s.name, err) + } + allocs := testing.AllocsPerRun(10000, func() { f.Fake("x") }) + if allocs > s.base*1.10 { + t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); bump the baseline only as a deliberate change", s.name, allocs, s.base*1.10, s.base) + } + } +} + +// A few depth/width benchmarks so the time trend stays visible next to the +// allocation gate. +func BenchmarkNestedDepth25(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(25)), "deep") } +func BenchmarkNestedDepth100(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(100)), "deep") } +func BenchmarkWideTokens100(b *testing.B) { + benchPath(b, tmpData(b, "wide", wideTokenJSON(100)), "wide") +} +func BenchmarkWideTokens500(b *testing.B) { + benchPath(b, tmpData(b, "wide", wideTokenJSON(500)), "wide") +} -- 2.52.0 From 0d2a080b85900d9c95009ec77f78f5dc5ad4483e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 3 Sep 2026 21:46:22 +0200 Subject: [PATCH 2/3] Add the fast-enough goal and record the allocation-gate decision --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index fb4a067..5b7f912 100644 --- a/README.md +++ b/README.md @@ -360,6 +360,9 @@ tokens add cost in proportion to the output. 7. **Zero dependencies** — standard library only. 8. **Docs index the grammar** — every syntax feature is a heading; every example runs under test and shows its output; a rule is stated once. +9. **Fast enough to be free** — a value renders in about a microsecond and `New` + parses and validates the whole set once upfront, so generating fixtures stays + noise against a test's own runtime. ## Decisions @@ -451,6 +454,12 @@ tokens add cost in proportion to the output. - **Samples say what they emit, transforms what they do.** `{upper(2)}` is two letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on whether the argument looks like a number. +- **The performance gate asserts allocations, not wall-clock time.** `AllocsPerRun` + is deterministic across machines, so a ±10% ceiling does not flake under CI load, + while time varies with the machine and its neighbours. A rendering slowdown + almost always costs an allocation too (a lost pre-size, a per-item map, an extra + copy). The benchmark suite (see Development) reports time for a human, not as a + pass/fail gate. ## Development -- 2.52.0 From ce22c83e7d7c47dd905df2b25f6c305c02a97126 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 3 Sep 2026 21:53:51 +0200 Subject: [PATCH 3/3] Cut the review-found narration: two helper invariants, one line each --- perf_test.go | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/perf_test.go b/perf_test.go index de03460..765525a 100644 --- a/perf_test.go +++ b/perf_test.go @@ -7,14 +7,7 @@ import ( "testing/fstest" ) -// The renderer's cost scales with the shape it renders: a deep nest descends one -// level per field, a wide format expands one token per field. The ceiling tests -// below pin the allocations one render costs for those shapes, so a change that -// adds a per-level or per-token allocation (a lost pre-size, a per-item map, an -// extra copy) fails unless the baseline is bumped as a deliberate decision. - -// nestedJSON nests a template depth times: each level's format renders its field -// "a", which is the next template down, so one Fake call recurses depth levels. +// One Fake call descends depth levels: each level's "a" is the next template down. func nestedJSON(depth int) string { s := `"leaf"` for i := 0; i < depth; i++ { @@ -23,8 +16,7 @@ func nestedJSON(depth int) string { return s } -// wideTokenJSON is one format with n sibling fields, so one Fake call expands n -// tokens. +// One Fake call expands n sibling tokens. func wideTokenJSON(n int) string { var toks, fields strings.Builder for i := 0; i < n; i++ { @@ -37,11 +29,6 @@ func wideTokenJSON(n int) string { return fmt.Sprintf(`{"format":"%s",%s}`, toks.String(), fields.String()) } -// TestNoRenderAllocRegression fails when a render allocates more than 10% past its -// recorded baseline. Allocations are deterministic across machines, so this gate -// cannot flake under CI load the way a wall-clock ceiling would; a real slowdown -// almost always costs an allocation too. Raising a baseline here is a deliberate -// "we accept this cost" decision. func TestNoRenderAllocRegression(t *testing.T) { shapes := []struct { name string @@ -64,8 +51,6 @@ func TestNoRenderAllocRegression(t *testing.T) { } } -// A few depth/width benchmarks so the time trend stays visible next to the -// allocation gate. func BenchmarkNestedDepth25(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(25)), "deep") } func BenchmarkNestedDepth100(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(100)), "deep") } func BenchmarkWideTokens100(b *testing.B) { -- 2.52.0