Files
fejkdata/perf_test.go
T

194 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package fejkdata
import (
"fmt"
"strings"
"testing"
"testing/fstest"
)
// 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++ {
s = fmt.Sprintf(`{"format":"{a}","a":%s}`, s)
}
return s
}
// One Fake call expands n sibling 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())
}
func TestNoRenderAllocRegression(t *testing.T) {
shapes := []struct {
name string
json string
base float64
}{
{"nested depth 25", nestedJSON(25), 26},
{"nested depth 100", nestedJSON(100), 101},
{"wide 500 tokens", wideTokenJSON(500), 8},
{"two paths into one field", `{"format":"{p.a} {p.b}","p":[{"format":"x","a":"1","b":"2"},{"format":"y","a":"A","b":"B"}]}`, 3},
{"ten paths into one field", `{"format":"{r.a}{r.b}{r.c}{r.d}{r.e}{r.f}{r.g}{r.h}{r.i}{r.j}","r":[
{"format":"x","a":"1","b":"2","c":"3","d":"4","e":"5","f":"6","g":"7","h":"8","i":"9","j":"0"},
{"format":"y","a":"A","b":"B","c":"C","d":"D","e":"E","f":"F","g":"G","h":"H","i":"I","j":"J"}]}`, 3},
}
for _, s := range shapes {
f, err := New(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)
}
}
}
// The repeat shape prices what keeps a render's env off the heap: a copy dropped costs an alloc an iteration.
func TestNoReferenceAllocRegression(t *testing.T) {
word := `{"format":"{w}","w":["alpha","beta","gamma","delta"]}`
for _, s := range []struct {
name, json string
base, slack float64
}{
{"a repeat of a reference path", `{"format":"{r}","r":{"format":"{/word.w}","repeat":20,"separator":", "}}`, 25, 1.10},
{"a repeat of a name", `{"format":"{r}","r":{"format":"{/word as n}{n.w}{n.w}","repeat":20,"separator":", "}}`, 162, 1.10},
{"a read two levels under a pick", `{"format":"{place.geo as g}{g} {g.town.zip}","place":{"format":"{geo}","geo":{"format":"{town.zip}","town":{"format":"{zip}","zip":"1"}}}}`, 12, 1},
} {
f, err := New(WithDataFS(fstest.MapFS{
"word.json": {Data: []byte(word)},
"x.json": {Data: []byte(s.json)},
}))
if err != nil {
t.Fatalf("New(%s): %v", s.name, err)
}
if allocs := testing.AllocsPerRun(10000, func() { f.Fake("x") }); allocs > s.base*s.slack {
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f × %.2f); a render's env reaching the heap is the usual cause", s.name, allocs, s.base*s.slack, s.base, s.slack)
}
}
}
// A table read pins a row inline and reads its cells in place.
func TestNoTableAllocRegression(t *testing.T) {
f, err := New(WithDataFS(fstest.MapFS{
"region.json": {Data: []byte(`{"format":"{name}","rows":"region.tsv","key":"code","weight":"population"}`)},
"region.tsv": {Data: []byte("code\tname\tpopulation\n01\tStockholms län\t2400000\n12\tSkåne län\t1400000\n14\tVästra Götalands län\t1750000\n")},
"x.json": {Data: []byte(`"{/region.name}, {/region.code}"`)},
}))
if err != nil {
t.Fatal(err)
}
for _, s := range []struct {
name, path string
base float64
}{
{"a table drawn", "region", 2},
{"a column of a drawn row", "region.name", 1},
{"a row selected", "region[12]", 2},
{"two columns, each drawn", "x", 3},
} {
if allocs := testing.AllocsPerRun(10000, func() { f.Fake(s.path) }); allocs > s.base*1.10 {
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a row lookup built per draw is the usual cause", s.name, allocs, s.base*1.10, s.base)
}
}
// Five linked tables, the depth a country's geo tree has: every pin must stay inline.
deep := fstest.MapFS{"addr.json": {Data: []byte(`"{/e.v} {/d.v} {/c.v} {/b.v} {/a.v}"`)}}
for i, name := range []string{"a", "b", "c", "d", "e"} {
rows, category := "k\tv\n1\tx\n2\ty\n", `{"format":"{v}","rows":"`+name+`.tsv","key":"k"}`
if i > 0 {
parent := string(rune('a' + i - 1))
rows = "k\tv\t" + parent + "\n1\tx\t1\n2\ty\t2\n"
category = `{"format":"{v}","rows":"` + name + `.tsv","key":"k","parent":"` + parent + `"}`
}
deep[name+".json"], deep[name+".tsv"] = &fstest.MapFile{Data: []byte(category)}, &fstest.MapFile{Data: []byte(rows)}
}
f, err = New(WithDataFS(deep))
if err != nil {
t.Fatal(err)
}
const base = 4.0
if allocs := testing.AllocsPerRun(10000, func() { f.Fake("addr") }); allocs > base*1.10 {
t.Errorf("five linked tables: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a pin spilling past the inline set is the usual cause", allocs, base*1.10, base)
}
}
// A record's fences read the compiled tree, so they belong to New, not to a draw.
func TestNoRecordAllocRegression(t *testing.T) {
for _, s := range []struct{ name, json string }{
{"record 3 columns", `{"format":"","a":"x","b":"y","c":"z"}`},
{"record 50 columns", wideTokenJSON(50)},
} {
f, err := New(WithDataFS(fstest.MapFS{"x.json": {Data: []byte(s.json)}}))
if err != nil {
t.Fatalf("New(%s): %v", s.name, err)
}
if _, err := f.FakeRecord("x"); err != nil {
t.Fatalf("FakeRecord(%s): %v", s.name, err) // else the gate would measure the error path
}
const base = 3.0
if allocs := testing.AllocsPerRun(10000, func() { f.FakeRecord("x") }); allocs > base*1.10 {
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a record fence running per draw is the usual cause", s.name, allocs, base*1.10, base)
}
r, _ := f.FakeRecord("x")
if allocs := testing.AllocsPerRun(10000, func() { _ = r.CSVLine() }); allocs > 2 {
t.Errorf("%s: CSVLine() makes %.1f allocs/op, want 2: the fields slice and the joined line", s.name, allocs)
}
}
}
func TestNoStructAllocRegression(t *testing.T) {
f, err := New(WithDataFS(fstest.MapFS{"x.json": {Data: []byte(`{"format":"","a":"x","b":"y","c":"z"}`)}}))
if err != nil {
t.Fatal(err)
}
var v struct {
A string `fake:"x.a"`
B string `fake:"x.b"`
C string `fake:"x.c"`
}
if err := f.FakeStruct(&v); err != nil {
t.Fatal(err)
}
const base = 5.0
if allocs := testing.AllocsPerRun(10000, func() { f.FakeStruct(&v) }); allocs > base*1.10 {
t.Errorf("FakeStruct: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); compiling the type per call is the usual cause", allocs, base*1.10, base)
}
}
func BenchmarkNestedDepth25(b *testing.B) {
benchPath(b, WithDataPath(tmpData(b, "deep", nestedJSON(25))), "deep")
}
func BenchmarkNestedDepth100(b *testing.B) {
benchPath(b, WithDataPath(tmpData(b, "deep", nestedJSON(100))), "deep")
}
func BenchmarkWideTokens100(b *testing.B) {
benchPath(b, WithDataPath(tmpData(b, "wide", wideTokenJSON(100))), "wide")
}
func BenchmarkWideTokens500(b *testing.B) {
benchPath(b, WithDataPath(tmpData(b, "wide", wideTokenJSON(500))), "wide")
}
// New decodes the shipped modules' manifests and loads nothing else.
func TestNoNewAllocRegression(t *testing.T) {
const base = 2864.0
allocs := testing.AllocsPerRun(100, func() {
if _, err := New(withShipped(), WithSeed(1)); err != nil {
t.Fatal(err)
}
})
if allocs > base*1.10 {
t.Errorf("New: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); bump the baseline only as a deliberate change", allocs, base*1.10, base)
}
}