1//go:build stress23// Stress benchmarks of the engine: build and full recalc, single-edit4// latency per dependency topology, undo, and whole-sheet operations. They5// only build with -tags stress (see `make stress` and docs/contributing/limits.md).6package sheet_test78import (9 "bytes"10 "fmt"11 "runtime"12 "runtime/metrics"13 "sync"14 "sync/atomic"15 "testing"16 "time"1718 "src.rest/cat/012/internal/sheet"19 "src.rest/cat/012/internal/stress"20)2122func BenchmarkBuild(b *testing.B) {23 for _, sh := range stress.Shapes() {24 b.Run(sh.Name, func(b *testing.B) {25 for b.Loop() {26 sh.Build()27 }28 })29 }30}3132func BenchmarkRecalcAll(b *testing.B) {33 for _, sh := range stress.Shapes() {34 b.Run(sh.Name, func(b *testing.B) {35 s := sh.Build()36 for b.Loop() {37 s.RecalcAll()38 }39 })40 }41}4243// BenchmarkEdit is the engine's share of typing one entry: storing it and44// the incremental recalculation it triggers.45func BenchmarkEdit(b *testing.B) {46 for _, sh := range stress.Shapes() {47 b.Run(sh.Name, func(b *testing.B) {48 s := sh.Build()49 inputs := [2]string{sh.Input, sh.Input + "1"}50 i := 051 for b.Loop() {52 if err := s.Set(sh.Edit, inputs[i%2]); err != nil {53 b.Fatal(err)54 }55 i++56 }57 })58 }59}6061// BenchmarkUndo is one undo and one redo of a single-cell edit.62func BenchmarkUndo(b *testing.B) {63 for _, sh := range stress.Shapes() {64 b.Run(sh.Name, func(b *testing.B) {65 s := sh.Build()66 if err := s.Set(sh.Edit, sh.Input+"1"); err != nil {67 b.Fatal(err)68 }69 for b.Loop() {70 s.Undo()71 s.Redo()72 }73 })74 }75}7677// BenchmarkBigUndo clears a dense 8192x26 block and undoes it: the78// largest single step a user makes by hand.79func BenchmarkBigUndo(b *testing.B) {80 s := stress.Dense(stress.Rows, 26)81 all := sheet.NewRect(sheet.Addr{}, sheet.Addr{Col: 25, Row: stress.Rows - 1})82 for b.Loop() {83 s.EraseRange(all)84 s.Undo()85 }86}8788// BenchmarkClearMax clears all of a full max-cells sheet of numbers (1M89// x 10) as one step and undoes it, then reports the heap such a step90// holds (what forgetting it frees) and the budget's estimate of it.91func BenchmarkClearMax(b *testing.B) {92 s := stress.Dense(stress.MaxRows, stress.MaxCols)93 all := sheet.NewRect(sheet.Addr{}, sheet.Addr{Col: stress.MaxCols - 1, Row: stress.MaxRows - 1})94 s.ClearHistory()95 for b.Loop() {96 s.EraseRange(all)97 s.Undo()98 s.ClearHistory()99 }100 s.EraseRange(all)101 est := s.Book().HistoryBytes()102 held := heap()103 s.ClearHistory()104 b.ReportMetric(float64(held-heap())/(1<<20), "MB-held")105 b.ReportMetric(float64(est)/(1<<20), "MB-estimate")106}107108// BenchmarkHistoryFull makes MaxUndo whole-column edits, the most the109// history keeps, and reports the heap they hold.110func BenchmarkHistoryFull(b *testing.B) {111 for b.Loop() {112 s := stress.Dense(stress.Rows, 4)113 before := heap()114 for i := range sheet.MaxUndo {115 col := sheet.NewRect(sheet.Addr{Col: i % 4}, sheet.Addr{Col: i % 4, Row: stress.Rows - 1})116 if err := s.FillEntry(col, col.From, fmt.Sprint(i)); err != nil {117 b.Fatal(err)118 }119 }120 b.ReportMetric(float64(heap()-before)/(1<<20), "MB-history")121 b.ReportMetric(float64(s.Book().HistoryBytes())/(1<<20), "MB-estimate")122 runtime.KeepAlive(s)123 }124}125126// BenchmarkHistoryWide makes 12 edits that each rewrite a whole127// 8192x26 block, more than the history's byte budget holds, and reports128// the heap the history keeps.129func BenchmarkHistoryWide(b *testing.B) {130 all := sheet.NewRect(sheet.Addr{}, sheet.Addr{Col: 25, Row: stress.Rows - 1})131 for b.Loop() {132 s := stress.Dense(stress.Rows, 26)133 before := heap()134 for i := range 12 {135 if err := s.FillEntry(all, all.From, fmt.Sprint(i)); err != nil {136 b.Fatal(err)137 }138 }139 b.ReportMetric(float64(heap()-before)/(1<<20), "MB-history")140 b.ReportMetric(float64(s.Book().HistoryBytes())/(1<<20), "MB-estimate")141 runtime.KeepAlive(s)142 }143}144145// BenchmarkNested enters deeply nested formulas: within the parser's cap146// (formula.MaxDepth, 1024) they're parsed and evaluated, past it refused.147func BenchmarkNested(b *testing.B) {148 for _, depth := range []int{100, 1000, 10000} {149 for name, f := range map[string]func(int) string{"paren": stress.Nested, "if": stress.NestedIF} {150 b.Run(fmt.Sprintf("%s-%d", name, depth), func(b *testing.B) {151 s := sheet.New()152 src := f(depth)153 for b.Loop() {154 if err := s.Set(sheet.Addr{Col: 1}, src); (err != nil) != (depth > 1000) {155 b.Fatal(err)156 }157 }158 })159 }160 }161}162163func BenchmarkSort(b *testing.B) {164 s := stress.Table(stress.Rows-1, 8)165 r := sheet.NewRect(sheet.Addr{Row: 1}, sheet.Addr{Col: 7, Row: stress.Rows - 1})166 desc := false167 for b.Loop() {168 s.SortRange(r, []sheet.SortKey{{Col: 2, Desc: desc}})169 desc = !desc170 }171}172173func BenchmarkFilter(b *testing.B) {174 s := stress.Table(stress.Rows-1, 8)175 s.CreateFilter(sheet.NewRect(sheet.Addr{}, sheet.Addr{Col: 7, Row: stress.Rows - 1}))176 b.Run("apply", func(b *testing.B) {177 arg := [2]string{"5000", "2500"}178 i := 0179 for b.Loop() {180 s.FilterColumn(2, sheet.Criteria{Cond: sheet.Condition{Op: sheet.CondGreater, Arg: arg[i%2]}})181 s.HiddenRows()182 i++183 }184 })185 b.Run("values", func(b *testing.B) {186 for b.Loop() {187 s.FilterValues(3)188 }189 })190}191192func BenchmarkFind(b *testing.B) {193 s := stress.Dense(stress.Rows, 26)194 b.Run("find", func(b *testing.B) {195 for b.Loop() {196 if _, err := s.Find("99.5", sheet.FindOptions{}); err != nil {197 b.Fatal(err)198 }199 }200 })201 b.Run("replace-all", func(b *testing.B) {202 q := [2]string{".", ","}203 i := 0204 for b.Loop() {205 if _, err := s.ReplaceAll(q[i%2], q[(i+1)%2], sheet.FindOptions{}); err != nil {206 b.Fatal(err)207 }208 i++209 }210 })211}212213func BenchmarkFillSeries(b *testing.B) {214 s := sheet.New()215 s.Set(sheet.Addr{}, "1")216 s.Set(sheet.Addr{Row: 1}, "2")217 src := sheet.NewRect(sheet.Addr{}, sheet.Addr{Row: 1})218 dst := sheet.NewRect(sheet.Addr{}, sheet.Addr{Row: stress.Rows - 1})219 for b.Loop() {220 if _, err := s.FillSeries(src, dst); err != nil {221 b.Fatal(err)222 }223 }224}225226// BenchmarkSave writes the native .012 file and reports its size and227// the most heap in use while writing it, above what the sheet holds.228func BenchmarkSave(b *testing.B) {229 for _, sh := range saveShapes() {230 b.Run(sh.name, func(b *testing.B) {231 s := sh.build()232 var out counter233 peak := peakHeap(func() {234 for b.Loop() {235 out = 0236 if err := s.Write(&out); err != nil {237 b.Fatal(err)238 }239 }240 })241 b.ReportMetric(float64(out)/(1<<20), "MB-file")242 b.ReportMetric(float64(peak)/(1<<20), "MB-peak")243 })244 }245}246247// BenchmarkOpen reads the native .012 file back, and reports the most248// heap in use while reading it, the sheet read included.249func BenchmarkOpen(b *testing.B) {250 for _, sh := range saveShapes() {251 b.Run(sh.name, func(b *testing.B) {252 var buf bytes.Buffer253 if err := sh.build().Write(&buf); err != nil {254 b.Fatal(err)255 }256 peak := peakHeap(func() {257 for b.Loop() {258 if _, err := sheet.Read(bytes.NewReader(buf.Bytes())); err != nil {259 b.Fatal(err)260 }261 }262 })263 b.ReportMetric(float64(peak)/(1<<20), "MB-peak")264 })265 }266}267268// counter is a writer that keeps only the count of bytes written.269type counter int64270271func (c *counter) Write(p []byte) (int, error) {272 *c += counter(len(p))273 return len(p), nil274}275276// peakHeap runs fn and returns the most heap in use meanwhile above what277// was live before, sampled every millisecond.278func peakHeap(fn func()) uint64 {279 base := heap()280 sample := []metrics.Sample{{Name: "/memory/classes/heap/objects:bytes"}}281 var peak atomic.Uint64282 done := make(chan struct{})283 var wg sync.WaitGroup284 wg.Go(func() {285 t := time.NewTicker(time.Millisecond)286 defer t.Stop()287 for {288 metrics.Read(sample)289 if v := sample[0].Value.Uint64(); v > peak.Load() {290 peak.Store(v)291 }292 select {293 case <-done:294 return295 case <-t.C:296 }297 }298 })299 fn()300 close(done)301 wg.Wait()302 return peak.Load() - min(base, peak.Load())303}304305type namedBuild struct {306 name string307 build func() *sheet.Sheet308}309310func saveShapes() []namedBuild {311 return []namedBuild{312 {"dense-8192x26", func() *sheet.Sheet { return stress.Dense(stress.Rows, 26) }},313 {"dense-8192x256", func() *sheet.Sheet { return stress.Dense(stress.Rows, stress.Cols) }},314 {"chain-8192", func() *sheet.Sheet { return stress.Chain(stress.Rows) }},315 {"dense-1Mx10", func() *sheet.Sheet { return stress.Dense(stress.MaxRows, stress.MaxCols) }},316 }317}318319// BenchmarkMemory reports the live heap per non-blank cell after a load:320// built through Load, or opened from a .012 file written beforehand.321func BenchmarkMemory(b *testing.B) {322 for _, sh := range []struct {323 name string324 prep func()325 build func() *sheet.Sheet326 }{327 {"numbers", nil, func() *sheet.Sheet { return stress.Dense(stress.Rows, 64) }},328 {"numbers-1Mx10", nil, func() *sheet.Sheet { return stress.Dense(stress.MaxRows, stress.MaxCols) }},329 {"formulas", nil, func() *sheet.Sheet { return stress.Chain(stress.Rows) }},330 {"text-200", nil, func() *sheet.Sheet { return stress.LongText(stress.Rows, 200) }},331 {"open-8192x256", func() { denseFile() }, func() *sheet.Sheet {332 s, err := sheet.Read(bytes.NewReader(denseFile()))333 if err != nil {334 b.Fatal(err)335 }336 return s337 }},338 } {339 b.Run(sh.name, func(b *testing.B) {340 if sh.prep != nil {341 sh.prep()342 }343 for b.Loop() {344 before := heap()345 s := sh.build()346 after := heap()347 b.ReportMetric(float64(after-before)/float64(s.Len()), "B/cell")348 runtime.KeepAlive(s)349 }350 })351 }352}353354// denseFile is the .012 file of 8192x256 numbers.355var denseFile = sync.OnceValue(func() []byte {356 var buf bytes.Buffer357 if err := stress.Dense(stress.Rows, stress.Cols).Write(&buf); err != nil {358 panic(err)359 }360 return buf.Bytes()361})362363// BenchmarkRead is the cost of reading a cell's value, per cell of 8192364// x 26 numbers: through Sheet.Value, and as formulas read cells in a365// recalculation (a SUM of each column, read directly).366func BenchmarkRead(b *testing.B) {367 s := stress.Dense(stress.Rows, 26)368 cells := float64(stress.Rows * 26)369 b.Run("value", func(b *testing.B) {370 sum := 0.0371 for b.Loop() {372 for row := range stress.Rows {373 for col := range 26 {374 sum += s.Value(sheet.Addr{Col: col, Row: row}).Num375 }376 }377 }378 b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N)/cells, "ns/cell")379 })380 b.Run("recalc", func(b *testing.B) {381 for col := range 26 {382 name := sheet.ColName(col)383 if err := s.Set(sheet.Addr{Col: col, Row: stress.Rows}, fmt.Sprintf("=SUM(%s1:%s%d)", name, name, stress.Rows)); err != nil {384 b.Fatal(err)385 }386 }387 for b.Loop() {388 s.RecalcAll()389 }390 b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N)/cells, "ns/cell")391 })392}393394// heap is the live heap after a full collection.395func heap() uint64 {396 runtime.GC()397 var ms runtime.MemStats398 runtime.ReadMemStats(&ms)399 return ms.HeapAlloc400}