A test runs every available command with the whole sheet, a whole column and a whole row selected on a sheet of a million rows by 16,384 columns, presses Enter to accept what a dialog offers, draws a frame, and fails past 250 ms or 8 MB. It found: resetting and setting column widths made an undo step per column of the selection (now one step, and a reset touches only the columns that have a width), and copying put every blank between values on the system clipboard (now refused past max-cells cells, with a note; Ctrl+V still pastes).
- commit
- 3953e111662c62d394e9ae8f7a64bde7bee9d708
- parent
- 460847ca0ffc9481d94b393d3ab2b8f4f31609e0
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 7 | ++- |
| M | internal/ | 11 | ++- |
| M | internal/ | 11 | ++- |
| A | internal/ | 105 | ++++++++++++++++++++++++++++++ |
@@ -41,7 +41,12 @@ func init() {
4141 func (m *Model) copy(cut bool) tea.Cmd {4242 clip := m.sheet.Copy(m.copyRange())4343 m.copied = clipboard{clip: clip, sheet: m.sheet, cut: cut, marked: true}44- return tea.SetClipboard(formatTSV(clip.Text()))44+ text := clip.Text()45+ if text == nil {46+ m.note = "Copied; too many cells for the system clipboard, but Ctrl+V pastes them here"47+ return nil48+ }49+ return tea.SetClipboard(formatTSV(text))4550 }4651 4752 // copyRange is the selection, trimmed to the data when whole rows or@@ -48,12 +48,15 @@ func (m *Model) openWidth() tea.Cmd {
4848 return nil4949 }5050 51-// setWidths sets the width of every selected column.51+// setWidths sets the width of every selected column, as one step.5252 func (m *Model) setWidths(w int) {5353 r := m.selection()54- for c := r.From.Col; c <= r.To.Col; c++ {55- m.sheet.SetColWidth(c, w)56- }54+ m.sheet.Batch(sheet.Change{Label: "column width", Focus: r}, func() error {55+ for c := r.From.Col; c <= r.To.Col; c++ {56+ m.sheet.SetColWidth(c, w)57+ }58+ return nil59+ })5760 }5861 5962 // autofit sizes column c to its widest content, as double-clicking a@@ -200,9 +200,14 @@ func init() {
200200 &command{id: "column.width", title: "Column width", desc: "Set the width of the selected columns", run: (*Model).openWidth},201201 &command{id: "column.reset", title: "Reset column width", desc: "Return the selected columns to the default width", run: func(m *Model) tea.Cmd {202202 r := m.selection()203- for c := r.From.Col; c <= r.To.Col; c++ {204- m.sheet.SetColWidth(c, 0)205- }203+ m.sheet.Batch(sheet.Change{Label: "reset column widths", Focus: r}, func() error {204+ for c := range m.sheet.Widths() { // only the columns that have a width205+ if c >= r.From.Col && c <= r.To.Col {206+ m.sheet.SetColWidth(c, 0)207+ }208+ }209+ return nil210+ })206211 m.changed = true207212 return nil208213 }},@@ -0,0 +1,105 @@
1+package ui2+3+import (4+ "fmt"5+ "maps"6+ "os"7+ "runtime"8+ "slices"9+ "strings"10+ "testing"11+ "time"12+13+ tea "charm.land/bubbletea/v2"14+15+ "github.com/FelineStateMachine/012/internal/sheet"16+)17+18+// The guardrail against work proportional to the grid: with the whole19+// sheet (or a whole column, or a whole row) selected on a nearly empty20+// sheet of a million rows by 16,384 columns, every command that can run21+// does, and the frame after it draws, in a small bound of time and22+// memory. A command that walked every address of its selection would23+// visit 17 billion cells here, and one that walked a whole column a24+// million: either fails.25+func TestCommandsCostTheDataNotTheGrid(t *testing.T) {26+ if testing.Short() {27+ t.Skip("runs every command three times")28+ }29+ skip := map[string]bool{30+ "quit": true, // the session, not the sheet31+ "settings.config_edit": true, "settings.config_reload": true, "settings.jev_key": true,32+ }33+ const maxTime, maxAlloc = 250 * time.Millisecond, 8 << 20 // a whole column of 8-byte entries is 8 MB34+ selections := []struct {35+ name string36+ keys []string37+ }{38+ {"sheet", []string{"<ctrl+a>", "<ctrl+a>"}},39+ {"column", []string{"<ctrl+space>"}},40+ {"row", []string{"<shift+space>"}},41+ }42+ var report strings.Builder43+ for _, id := range slices.Sorted(maps.Keys(commands)) {44+ if skip[id] || strings.HasPrefix(id, "file.") {45+ continue46+ }47+ for _, sel := range selections {48+ m := vastModel(t)49+ press(t, m, sel.keys...)50+ if !commands[id].available(m) {51+ continue52+ }53+ if r := m.selection(); sel.name != "row" && !r.AllRows() || sel.name != "column" && !r.AllCols() {54+ t.Fatalf("%s selected %v", sel.name, r)55+ }56+ // Then Enter, which accepts what a dialog the command opened57+ // offers (a sort, a filter, a chart), and a frame.58+ d, alloc := measure(func() {59+ run(m, commands[id].run(m))60+ m.View()61+ press(t, m, "<enter>")62+ m.View()63+ })64+ fmt.Fprintf(&report, "%-28s %-7s %8.2f ms %9d B\n", id, sel.name, float64(d.Microseconds())/1000, alloc)65+ if d > maxTime || alloc > maxAlloc {66+ t.Errorf("%s over the whole %s took %v and allocated %d bytes", id, sel.name, d, alloc)67+ }68+ }69+ }70+ if testing.Verbose() {71+ t.Log("\n" + report.String())72+ }73+}74+75+// vastModel is a model on a sheet with the full grid and a few cells,76+// clear of the top row and the first column (so filling down or right77+// from them has nothing to copy), in a temporary directory.78+func vastModel(t *testing.T) *Model {79+ t.Helper()80+ dir := t.TempDir()81+ wd, _ := os.Getwd()82+ os.Chdir(dir)83+ t.Cleanup(func() { os.Chdir(wd) })84+ s := sheet.New()85+ for _, in := range []struct{ a, v string }{{"B2", "Name"}, {"C2", "Qty"}, {"B3", "pen"}, {"C3", "3"}, {"B4", "ink"}, {"C4", "=C3*2"}, {"D5", "=SUM(C:C)"}} {86+ s.Set(addr(in.a), in.v)87+ }88+ s.ClearHistory()89+ m := New(s, "")90+ m.Update(tea.WindowSizeMsg{Width: 200, Height: 60})91+ return m92+}93+94+// measure runs fn and returns how long it took and how much it95+// allocated.96+func measure(fn func()) (time.Duration, uint64) {97+ var before, after runtime.MemStats98+ runtime.GC()99+ runtime.ReadMemStats(&before)100+ start := time.Now()101+ fn()102+ d := time.Since(start)103+ runtime.ReadMemStats(&after)104+ return d, after.TotalAlloc - before.TotalAlloc105+}