1package ui23import (4 "flag"5 "fmt"6 "runtime"7 "testing"89 tea "charm.land/bubbletea/v2"1011 "src.rest/cat/012/internal/sheet"12 "src.rest/cat/012/internal/stress"13)1415// The speed gate (make speed, part of make check): frames and16// recalculation on mid-sized sheets from internal/stress, held to17// testdata/speed.json. See docs/contributing/limits.md#the-speed-gate.18var (19 speedFlag = flag.Bool("speed", false, "run the speed gate against testdata/speed.json")20 speedUpdateFlag = flag.Bool("speed-update", false, "measure the speed gate and rewrite testdata/speed.json")21)2223// speedCase is one measured operation: setup builds its state and24// returns the operation, which must leave the state as it found it25// after an even number of calls.26type speedCase struct {27 name string28 setup func() func()29}3031// frameCase is an arrow key through to its frame at 200 x 60, down and32// up in turn, on the sheet build makes.33func frameCase(name string, build func() *sheet.Sheet) speedCase {34 return speedCase{"frame/" + name, func() func() {35 m := sized(build(), 200, 60)36 t := newFakeTerm(200, 60)37 keys := [2]tea.KeyPressMsg{{Code: tea.KeyDown}, {Code: tea.KeyUp}}38 i := 039 return func() {40 m.Update(keys[i%2])41 t.frame(m)42 i++43 }44 }}45}4647// outputCase is an arrow key through to its frame at 200 x 60 in a48// notebook output's grid of rows rows, entered, down and up in turn,49// halfway down the output.50func outputCase(rows int) speedCase {51 return speedCase{fmt.Sprintf("frame/output-%dx4", rows), func() func() {52 m := outputModel(bigTable(rows), 200, 60)53 m.runCommand("nb.edit")54 g := m.gridIn()55 g.child.cur.Row = rows / 256 g.child.scrollTo(g.child.cur)57 t := newFakeTerm(200, 60)58 keys := [2]tea.KeyPressMsg{{Code: tea.KeyDown}, {Code: tea.KeyUp}}59 i := 060 return func() {61 m.Update(keys[i%2])62 t.frame(m)63 i++64 }65 }}66}6768// sourceFrameCase is Page Down and Page Up in turn through to their69// frames at 200 x 60, halfway down a linked source's tab, the pages70// they show already read, as scrolling within what's read is.71func sourceFrameCase() speedCase {72 return speedCase{fmt.Sprintf("frame/source-%dx4", speedSourceRows), func() func() {73 m := speedSourceModel()74 m.srcView().MoveTo(speedSourceRows/2, 2)75 keys := [2]tea.KeyPressMsg{{Code: tea.KeyPgDown}, {Code: tea.KeyPgUp}}76 for _, k := range keys { // both windows read77 m.Update(k)78 settleSources(m)79 }80 t := newFakeTerm(200, 60)81 i := 082 return func() {83 m.Update(keys[i%2])84 t.frame(m)85 i++86 }87 }}88}8990// sourceSumCase is a linked source read again and a SUM over one of its91// columns worked out again, as the file changing makes it, run to the92// end rather than in the background.93func sourceSumCase() speedCase {94 return speedCase{fmt.Sprintf("source/sum-%d", speedSourceRows), func() func() {95 m := speedSourceModel()96 info, _ := m.sheet.Source()97 m.showSheet(m.book().Sheet(0))98 m.sheet.Set(sheet.Addr{}, "=SUM("+info.Name+"[amount])")99 settleSources(m)100 return func() {101 m.sources().host.Reload(info.Name)102 if !settleSources(m) || m.sheet.Value(sheet.Addr{}).Kind != sheet.Number {103 panic("the SUM over the source never settled")104 }105 }106 }}107}108109// speedSourceModel is a model showing the speed gate's source.110func speedSourceModel() *Model {111 path, err := speedSources()112 if err != nil {113 panic(err)114 }115 return sourceModel(path, 200, 60)116}117118// editCase is typing into one cell of a stress shape and the119// incremental recalculation that follows, two entries in turn.120func editCase(sh stress.Shape) speedCase {121 return speedCase{"edit/" + sh.Name, func() func() {122 s := sh.Build()123 inputs := [2]string{sh.Input, sh.Input + "1"}124 i := 0125 return func() {126 if err := s.Set(sh.Edit, inputs[i%2]); err != nil {127 panic(err)128 }129 i++130 }131 }}132}133134// recalcCase is a full recalculation of the sheet build makes.135func recalcCase(name string, build func() *sheet.Sheet) speedCase {136 return speedCase{"recalc/" + name, func() func() {137 s := build()138 return s.RecalcAll139 }}140}141142func speedCases() []speedCase {143 dense := func() *sheet.Sheet { return stress.Dense(stress.Rows, 26) }144 laidout := func() *sheet.Sheet { return stress.Laidout(stress.Rows, 26) }145 charts := func() *sheet.Sheet {146 s := stress.Table(200, 3)147 stress.Charts(s, 8)148 return s149 }150 shape := func(name string, build func() *sheet.Sheet, a string, input string) stress.Shape {151 at, _ := sheet.ParseAddr(a)152 return stress.Shape{Name: name, Build: build, Edit: at, Input: input}153 }154 fanin := shape("fanin-1000xSUM8192", func() *sheet.Sheet { return stress.FanIn(stress.Rows, 1000) }, "A4001", "7")155 return []speedCase{156 frameCase("dense-8192x26", dense),157 frameCase("laidout-8192x26", laidout),158 frameCase("scale-8192x26", scaled),159 frameCase("charts-8", charts),160 outputCase(100000),161 sourceFrameCase(),162 sourceSumCase(),163 editCase(fanin),164 editCase(shape("chain-8192", func() *sheet.Sheet { return stress.Chain(stress.Rows) }, "A1", "2")),165 editCase(shape("criteria-10xSUMIF8192", func() *sheet.Sheet { return stress.Criteria(stress.Rows, 10) }, "A4001", "7")),166 editCase(shape("lookup-100xVLOOKUP8192", func() *sheet.Sheet { return stress.Lookup(stress.Rows, 100) }, "A4001", "4000")),167 recalcCase("running-8192", func() *sheet.Sheet { return stress.RunningTotals(stress.Rows) }),168 recalcCase("arrays-50xFILTER8192", func() *sheet.Sheet { return stress.Arrays(stress.Rows, 50) }),169 }170}171172// TestSpeed measures every case and compares it with the baseline:173// allocations per operation, and time per operation relative to a174// calibration workload timed beside it. A case over its margin is175// measured once more before it fails, so one noisy run doesn't.176func TestSpeed(t *testing.T) {177 if !*speedFlag && !*speedUpdateFlag {178 t.Skip("the speed gate runs with -speed (make speed)")179 }180 unlock := benchLock(t)181 defer unlock()182 base, err := readSpeedBaseline()183 if err != nil && !*speedUpdateFlag {184 t.Fatal(err)185 }186 next := speedBaseline{GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, Cases: map[string]speedResult{}}187 for _, c := range speedCases() {188 op := c.setup()189 got := measureSpeed(op)190 if !*speedUpdateFlag {191 if verdict := base.judge(c.name, got); verdict != "" {192 got = measureSpeed(op) // a second chance, against noise193 if verdict = base.judge(c.name, got); verdict != "" {194 t.Errorf("%s: %s", c.name, verdict)195 }196 }197 }198 t.Logf("%-36s %8.3f ms %6.2fx calibration %8.0f allocs", c.name, got.NS/1e6, got.Ratio, got.Allocs)199 next.Cases[c.name] = got200 }201 if *speedUpdateFlag {202 if err := next.write(); err != nil {203 t.Fatal(err)204 }205 fmt.Println("wrote", speedBaselinePath)206 }207}