cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Undo: before-images in the store's compact form

Dami committed 36b0623
A step keeps each sheet's before-images as an image: the slots of
plain cells in columns of blocks, with their strings and looks interned
in the image's own tables, and whole Cells only for rich cells. The
first 32 cells are a list, so a one-cell step costs no column index.

Clearing all of dense-1Mx10 holds 193 MB (estimated 193 MB), where a Cell
per cell held 3040 MB. 100 whole-column steps: 15.9 MB held, 16.0
estimated (was 281 and 252). 12 rewrites of 8192x26: 49.3 held, 49.4
estimated. Undo and redo of one number: 1.17 us (was 1.18).

UndoCost estimates what a change to a range would hold, and
WithoutUndo runs a change recording nothing and forgets the history,
for steps past MaxStepBytes (1 GB); StateID still moves.
commit
36b062300e19d900a2f22ec461c149e9a402e678
parent
0aa5dca6bc484aa9aea562f34b573c14d4822b4b
author
Dami <dami.etoile@gmail.com>
7 files changed, +578 -54
Minternal/sheet/history.go70++++-----
Ainternal/sheet/historyimage.go248++++++++++++++++++++++++++++++
Ainternal/sheet/historyscale_test.go163++++++++++++++++++++
Minternal/sheet/historysize.go83+++++++++-
Minternal/sheet/historysize_test.go27+++-
Minternal/sheet/nav.go17++
Minternal/sheet/occupancy.go24+++
M internal/sheet/history.go +31 -39
@@ -12,11 +12,12 @@ const MaxUndo = 100
1212 // before the step; undoing swaps those back in and records the state it1313 // replaced as the redo step.1414 //15-// Steps snapshot whole cells (a shallow copy of the struct), so fields16-// added to Cell later are covered without changes here. That relies on17-// one rule: a stored *Cell is never modified in place except for its18-// computed Value. To change a cell, build a new one and store it with19-// place.15+// Steps keep cells' before-images in the compact form the sheet keeps16+// its cells (see historyimage.go): plain cells as slots, rich ones as a17+// shallow copy of their Cell, so fields added to Cell later are covered18+// without changes here. That relies on one rule: a stored *Cell is never19+// modified in place except for its computed Value. To change a cell,20+// build a new one and store it with place.2021 type history struct {2122 undo, redo []*step2223 open *step // the step being built; nil outside change
@@ -24,6 +25,12 @@ type history struct {
2425 dirty []loc2526 lastID int2627 bytes int64 // estimated heap held by the undo steps; see historysize.go28+ // off is set while a change runs without undo (WithoutUndo): its29+ // step records no before-images.30+ off bool31+ // base is the state ID with no step to undo: 0 until a change ran32+ // without undo, then that change's.33+ base int2734 // mergeWidths lets the next width-only step join the top one, so a2835 // live preview of a column width and its final value (or its2936 // cancellation) are one step. Seal ends the run.
@@ -39,7 +46,7 @@ type step struct {
3946 label string // what the step did, e.g. "clear B3:B5"4047 sheet *Sheet // the sheet the UI shows when the step is undone or redone4148 focus Rect // what the UI selects there42- cells map[loc]*Cell // before the step; nil for blank49+ cells map[*Sheet]*image // before the step, by sheet4350 widths map[colKey]int // before the step; 0 for the default width4451 lines map[lineKey]lineFmt // column and row formats before the step4552 names map[string]*Name // before the step, by key; nil for undefined
@@ -66,12 +73,12 @@ type colKey struct {
6673 }6774 6875 func newStep(label string, s *Sheet, focus Rect) *step {69- return &step{label: label, sheet: s, focus: focus, cells: map[loc]*Cell{}, widths: map[colKey]int{}, lines: map[lineKey]lineFmt{},76+ return &step{label: label, sheet: s, focus: focus, cells: map[*Sheet]*image{}, widths: map[colKey]int{}, lines: map[lineKey]lineFmt{},7077 names: map[string]*Name{}, views: map[*Sheet]*viewState{}, charts: map[*Sheet][]Chart{}, pivots: map[*Sheet]*Pivot{}, rules: map[*Sheet]rulesState{}}7178 }7279 7380 func (st *step) empty() bool {74- return len(st.cells) == 0 && len(st.widths) == 0 && st.widthOnly()81+ return len(st.widths) == 0 && st.widthOnly()7582 }7683 7784 // widthOnly reports whether the step changed nothing but column widths.
@@ -160,11 +167,10 @@ func (s *Sheet) record(a Addr) {
160167 if st == nil {161168 return162169 }163- l := loc{s, a}164- if _, seen := st.cells[l]; !seen {165- st.keep(l, s.cells.copyOf(a))170+ if !s.wb.hist.off {171+ st.keep(s, a)166172 }167- s.wb.hist.dirty = append(s.wb.hist.dirty, l)173+ s.wb.hist.dirty = append(s.wb.hist.dirty, loc{s, a})168174 }169175 170176 // recordWidth saves column c's width before its first change in the open
@@ -244,10 +250,11 @@ func (w *Workbook) push(st *step) {
244250 func (w *Workbook) dropUnchanged(st *step) {245251 st.dropUnchangedLines()246252 st.dropUnchangedRules()247- for l, c := range st.cells {248- if c == nil && !l.s.cells.has(l.a) {249- delete(st.cells, l)250- st.cellBytes -= entryBytes253+ for s, img := range st.cells {254+ st.cellBytes -= img.dropBlanks(s)255+ if img.len() == 0 {256+ delete(st.cells, s)257+ st.cellBytes -= imageBytes251258 }252259 }253260 for k, n := range st.names {
@@ -375,7 +382,7 @@ func (w *Workbook) StateID() int {
375382 if top := w.hist.top(); top != nil {376383 return top.id377384 }378- return 0385+ return w.hist.base379386 }380387 381388 // ClearHistory forgets all undo and redo steps.
@@ -419,11 +426,13 @@ func (w *Workbook) swap(undo bool) (Change, bool) {
419426 inv.sheets = w.sheetList()420427 w.setSheets(st.sheets)421428 }422- changed := make([]loc, 0, len(st.cells))423- for l, c := range st.cells {424- inv.keep(l, l.s.cells.copyOf(l.a))425- l.s.place(l.a, c.clone())426- changed = append(changed, l)429+ var changed []loc430+ for s, img := range st.cells {431+ img.each(func(a Addr, c *Cell) {432+ inv.keep(s, a)433+ s.place(a, c)434+ changed = append(changed, loc{s, a})435+ })427436 }428437 for k, width := range st.widths {429438 inv.widths[k] = k.s.widths[k.col]
@@ -476,20 +485,3 @@ func (w *Workbook) swap(undo bool) (Change, bool) {
476485 macrosOnly := st.macros != nil && len(st.cells) == 0 && len(st.lines) == 0 && len(st.names) == 0 && len(st.charts) == 0 && len(st.pivots) == 0 && len(st.rules) == 0 && st.sheets == nil477486 return Change{Label: st.label, Focus: st.focus, Sheet: st.sheet, Tabs: st.sheets != nil, Macros: macrosOnly}, true478487 }479-480-// colRect is the range covering whole columns from..to.481-func colRect(from, to int) Rect {482- return Rect{From: Addr{Col: from}, To: Addr{Col: to, Row: MaxRows - 1}}483-}484-485-// rowRect is the range covering whole rows from..to.486-func rowRect(from, to int) Rect {487- return Rect{From: Addr{Row: from}, To: Addr{Col: MaxCols - 1, Row: to}}488-}489-490-func union(a, b Rect) Rect {491- return Rect{492- From: Addr{Col: min(a.From.Col, b.From.Col), Row: min(a.From.Row, b.From.Row)},493- To: Addr{Col: max(a.To.Col, b.To.Col), Row: max(a.To.Row, b.To.Row)},494- }495-}
A internal/sheet/historyimage.go +248 -0
@@ -0,0 +1,248 @@
1+package sheet2+3+import "slices"4+5+// An image holds an undo step's before-images of one sheet's cells in6+// the form the sheet keeps them (store.go, slot.go): columns of blocks7+// of 16-byte slots, the text in a table of strings kept once each, the8+// formatting in a table of looks, and whole Cells only for rich cells9+// (formulas, notes, what pivots and spills write). A step that clears10+// or rewrites a whole sheet so holds about what the sheet does, 20 B11+// per number, rather than a Cell per cell. A cell that held nothing12+// before the step is a bit in blank.13+//14+// The columns are indexed by position (occupancy.go), which costs a few15+// KB for the first cell far down a sheet, so an image holds its first16+// smallImage cells in a list and moves them into columns past that:17+// the typical step, one cell or a few, costs what they do.18+type image struct {19+ small []imageCell // the cells while there are at most smallImage20+ few [2]imageCell // small's first cells, without an allocation21+ big bool // the cells are in cells and blank instead22+ // cells holds the tables of strings, looks and rich cells, and the23+ // columns once big; nil until one is needed, as a plain number24+ // needs none.25+ cells *cellStore26+ blank occupancy27+ // looks maps the sheet's looks to the image's: entry i is the28+ // image's look for the sheet's look i, plus one; 0 for not yet29+ // seen. A store's looks are only ever added, so the map holds.30+ looks []uint1631+}32+33+// imageCell is a before-image in an image's list: the slot, in the34+// image's tables, or blank for a cell that held nothing.35+type imageCell struct {36+ a Addr37+ sl slot38+ blank bool39+}40+41+// smallImage is how many cells an image lists before it keeps them in42+// columns: a list's linear search stays cheaper than an index up to43+// about this many.44+const smallImage = 3245+46+// Estimated heap of an image's parts beyond slotBytes a cell, as47+// BenchmarkClearMax and BenchmarkHistoryWide measure it.48+const (49+ blockBytes = 256 // a block of blockRows rows, made for its first cell50+ richBytes = 24 // a rich cell's entry in the side table51+)52+53+// seen reports whether the image holds the cell at a, blank or not.54+func (img *image) seen(a Addr) bool {55+ if img.big {56+ return img.cells.stored.has(a) || img.blank.has(a) // big has cells57+ }58+ for i := range img.small {59+ if img.small[i].a == a {60+ return true61+ }62+ }63+ return false64+}65+66+// len is the number of cells the image holds, blank ones included.67+func (img *image) len() int {68+ n := len(img.small) + img.blank.n69+ if img.cells != nil {70+ n += img.cells.stored.n71+ }72+ return n73+}74+75+// store is img.cells, made if need be.76+func (img *image) store() *cellStore {77+ if img.cells == nil {78+ img.cells = &cellStore{}79+ }80+ return img.cells81+}82+83+// tables is img.cells to read from: an empty store when there is none,84+// as a slot then refers to no table.85+func (img *image) tables() *cellStore {86+ if img.cells == nil {87+ return &noTables88+ }89+ return img.cells90+}91+92+// noTables is an empty store, never written.93+var noTables cellStore94+95+// keep records the cell at a of src, the sheet's store, as it stands,96+// and returns the heap that adds.97+func (img *image) keep(src *cellStore, a Addr) int64 {98+ b, i := src.find(a)99+ if b == nil {100+ return img.put(a, slot{}, true)101+ }102+ sl := b.vals[i]103+ if sl.kind == slotRich {104+ return img.keepRich(a, src.rich[sl.ref].c.clone())105+ }106+ n := int64(slotBytes)107+ if sl.look != 0 {108+ lk, ok := img.look(src, sl.look)109+ if !ok { // the image's table of looks is full110+ return img.keepRich(a, src.view(sl))111+ }112+ sl.look = lk113+ }114+ if sl.ref != 0 {115+ s := src.strs.strs[sl.ref]116+ t := &img.store().strs117+ sl.ref = t.add(s)118+ if t.refs[sl.ref] == 1 {119+ n += strBytes + int64(len(s))120+ }121+ }122+ return n + img.put(a, sl, false)123+}124+125+// keepRich records c, a rich cell or one made to stand for a plain one,126+// as the before-image at a.127+func (img *image) keepRich(a Addr, c *Cell) int64 {128+ n := int64(slotBytes+richBytes) + cellSize(c)129+ return n + img.put(a, slot{kind: slotRich, ref: img.store().addRich(a, c)}, false)130+}131+132+// put stores sl at a, or a blank there, which the image doesn't hold,133+// and returns the heap new blocks take.134+func (img *image) put(a Addr, sl slot, blank bool) int64 {135+ if !img.big {136+ if img.small == nil {137+ img.small = img.few[:0]138+ }139+ if len(img.small) < smallImage {140+ img.small = append(img.small, imageCell{a, sl, blank})141+ return 0142+ }143+ n := img.grow()144+ return n + img.put(a, sl, blank)145+ }146+ o := &img.store().stored147+ if blank {148+ o = &img.blank149+ }150+ var n int64151+ if o.col(a.Col).block(a.Row>>blockShift) == nil {152+ n = blockBytes153+ }154+ b := o.mark(a)155+ if !blank {156+ i, _ := b.index(a.Row & (blockRows - 1))157+ b.vals = slices.Insert(b.vals, i, sl)158+ }159+ return n160+}161+162+// grow moves the listed cells into columns and returns the heap their163+// blocks take.164+func (img *image) grow() int64 {165+ img.big = true166+ var n int64167+ for _, ic := range img.small {168+ n += img.put(ic.a, ic.sl, ic.blank)169+ }170+ img.small = nil171+ return n172+}173+174+// look is the image's entry for the sheet's look id, added if new, and175+// false when the image's table is full.176+func (img *image) look(src *cellStore, id uint16) (uint16, bool) {177+ if int(id) < len(img.looks) && img.looks[id] != 0 {178+ return img.looks[id] - 1, true179+ }180+ l := src.looks[id]181+ lk, ok := img.store().lookID(l.f, l.st)182+ if !ok {183+ return 0, false184+ }185+ if int(id) >= len(img.looks) {186+ img.looks = slices.Grow(img.looks, int(id)+1-len(img.looks))[:int(id)+1]187+ }188+ img.looks[id] = lk + 1189+ return lk, true190+}191+192+// each calls fn with every cell the image holds and its before-image as193+// a Cell to place, nil for blank: a rich cell itself (the image is194+// dropped once restored), a plain one made for the caller. fn mustn't195+// change the image.196+func (img *image) each(fn func(a Addr, c *Cell)) {197+ st := img.tables()198+ for _, ic := range img.small {199+ var c *Cell200+ if !ic.blank {201+ c = st.cellOf(ic.sl)202+ }203+ fn(ic.a, c)204+ }205+ for _, col := range st.stored.colIDs {206+ st.stored.colScan(col, 0, MaxRows-1, func(row int) bool {207+ a := Addr{Col: col, Row: row}208+ b, i := st.find(a)209+ fn(a, st.cellOf(b.vals[i]))210+ return true211+ })212+ }213+ for _, col := range img.blank.colIDs {214+ img.blank.colScan(col, 0, MaxRows-1, func(row int) bool {215+ fn(Addr{Col: col, Row: row}, nil)216+ return true217+ })218+ }219+}220+221+// dropBlanks forgets the blank before-images of cells that are still222+// blank in s, and returns the heap that frees: the blocks emptied.223+func (img *image) dropBlanks(s *Sheet) int64 {224+ img.small = slices.DeleteFunc(img.small, func(ic imageCell) bool { return ic.blank && !s.cells.has(ic.a) })225+ var drop []Addr226+ for _, c := range img.blank.colIDs {227+ img.blank.colScan(c, 0, MaxRows-1, func(row int) bool {228+ if a := (Addr{Col: c, Row: row}); !s.cells.has(a) {229+ drop = append(drop, a)230+ }231+ return true232+ })233+ }234+ blocks := img.blank.blocks()235+ for _, a := range drop {236+ img.blank.unmark(a)237+ }238+ return int64(blocks-img.blank.blocks()) * blockBytes239+}240+241+// blocks is the number of blocks the occupancy holds.242+func (o *occupancy) blocks() int {243+ n := 0244+ for _, c := range o.colIDs {245+ n += len(o.cols[c].ids)246+ }247+ return n248+}
A internal/sheet/historyscale_test.go +163 -0
@@ -0,0 +1,163 @@
1+package sheet2+3+import (4+ "bytes"5+ "fmt"6+ "testing"7+)8+9+// scaleBook is a workbook of two sheets, the first holding rows rows of10+// every kind of cell a before-image keeps: numbers as typed, text,11+// formatted numbers, formulas, booleans, notes and formatting alone.12+func scaleBook(t testing.TB, rows int) *Sheet {13+ s := New()14+ cur := Format{Kind: FmtCurrency, Decimals: 2}15+ for r := range rows {16+ n := r + 117+ load := func(col int, in string, f Format, st Style) {18+ if err := s.Load(Addr{Col: col, Row: r}, in, f, st); err != nil {19+ t.Fatal(err)20+ }21+ }22+ load(0, fmt.Sprintf("%d.50", r%1000), Format{}, Style{})23+ load(1, fmt.Sprintf("item %d", r%500), Format{}, Style{Bold: r%3 == 0})24+ load(2, fmt.Sprint(r*7%10000), cur, Style{})25+ load(3, fmt.Sprintf("=A%d*2+C%d", n, n), Format{}, Style{})26+ switch r % 4 {27+ case 0:28+ load(4, "TRUE", Format{}, Style{})29+ case 1:30+ load(4, "", Format{Kind: FmtPercent}, Style{Italic: true})31+ }32+ }33+ for r := 0; r < rows; r += 100 {34+ if err := s.SetNote(Addr{Col: 1, Row: r}, fmt.Sprintf("note %d", r)); err != nil {35+ t.Fatal(err)36+ }37+ }38+ other, err := s.Book().AddSheet("Other", 1)39+ if err != nil {40+ t.Fatal(err)41+ }42+ other.Set(Addr{}, "=SUM(Sheet1!A:A)")43+ s.RecalcAll()44+ s.ClearHistory()45+ return s46+}47+48+// saved is the workbook as its file, and the value of Other!A1, which49+// reads every number of the first sheet.50+func saved(t *testing.T, s *Sheet) string {51+ t.Helper()52+ var b bytes.Buffer53+ if err := s.Book().Write(&b); err != nil {54+ t.Fatal(err)55+ }56+ w := s.Book()57+ return b.String() + fmt.Sprint(w.Sheet(w.Len()-1).Value(Addr{}))58+}59+60+// Every whole-range operation over a large sheet undoes to the workbook61+// as it was and redoes to the workbook as it made it, and its step is62+// estimated at a slot or so a plain cell, not a Cell.63+func TestUndoWholeRangeAtScale(t *testing.T) {64+ const rows = 1000065+ s := scaleBook(t, rows)66+ w := s.Book()67+ all := NewRect(Addr{}, Addr{Col: 4, Row: rows - 1})68+ ops := []struct {69+ name string70+ op func(t *testing.T)71+ }{72+ {"clear", func(*testing.T) { s.EraseRange(all) }},73+ {"format", func(*testing.T) { s.SetFormat(all, Format{Kind: FmtPercent, Decimals: 1}) }},74+ {"style", func(*testing.T) { s.SetStyle(all, func(st *Style) { st.Underline = true }) }},75+ {"clear formatting", func(*testing.T) { s.ClearFormatting(all) }},76+ {"fill", func(t *testing.T) {77+ if err := s.FillEntry(all, all.From, "7"); err != nil {78+ t.Fatal(err)79+ }80+ }},81+ {"insert rows", func(t *testing.T) {82+ if err := s.InsertRows(10, 5); err != nil {83+ t.Fatal(err)84+ }85+ }},86+ {"delete rows", func(*testing.T) { s.DeleteRows(3, 100) }},87+ {"insert columns", func(t *testing.T) {88+ if err := s.InsertCols(1, 2); err != nil {89+ t.Fatal(err)90+ }91+ }},92+ {"delete columns", func(*testing.T) { s.DeleteCols(0, 1) }},93+ {"sort", func(*testing.T) { s.SortRange(all, []SortKey{{Col: 2, Desc: true}}) }},94+ {"replace sheet", func(t *testing.T) {95+ src := New()96+ src.Set(Addr{}, "42")97+ if _, err := w.ReplaceSheet(s, src, "import"); err != nil {98+ t.Fatal(err)99+ }100+ }},101+ {"delete sheet", func(t *testing.T) {102+ if err := w.DeleteSheet(s); err != nil {103+ t.Fatal(err)104+ }105+ }},106+ }107+ for _, o := range ops {108+ before := saved(t, s)109+ o.op(t)110+ after := saved(t, s)111+ if after == before {112+ t.Fatalf("%s changed nothing", o.name)113+ }114+ if n := w.HistoryBytes(); n > int64(rows*5)*(slotBytes+32)+int64(rows)*(richBytes+cellBytes+formulaBytes+perFormulaByte*16) {115+ t.Errorf("%s: step estimated at %d bytes", o.name, n)116+ }117+ if _, ok := w.Undo(); !ok || saved(t, s) != before {118+ t.Fatalf("%s: undo doesn't restore the workbook", o.name)119+ }120+ if _, ok := w.Redo(); !ok || saved(t, s) != after {121+ t.Fatalf("%s: redo doesn't make it again", o.name)122+ }123+ w.Undo()124+ w.ClearHistory()125+ }126+}127+128+// A step over a sheet of plain numbers holds about a slot a cell.129+func TestUndoPlainCellsAsSlots(t *testing.T) {130+ s := New()131+ const n = 50000132+ for r := range n {133+ s.Load(Addr{Row: r}, fmt.Sprintf("%d.25", r), Format{}, Style{})134+ }135+ s.RecalcAll()136+ s.EraseRange(NewRect(Addr{}, Addr{Row: n - 1}))137+ if got := s.Book().HistoryBytes(); got > n*(slotBytes+4) {138+ t.Errorf("clearing %d numbers is estimated at %d bytes", n, got)139+ }140+}141+142+// A change too large to undo runs without recording, forgets the history143+// and still reads as a change to the workbook.144+func TestWithoutUndo(t *testing.T) {145+ s := New()146+ s.Set(Addr{}, "1")147+ w := s.Book()148+ id := w.StateID()149+ w.WithoutUndo(func() { s.Set(Addr{}, "2") })150+ if w.CanUndo() || w.CanRedo() || w.HistoryBytes() != 0 {151+ t.Error("the history outlived a change without undo")152+ }153+ if inputs(s)["A1"] != "2" || w.StateID() == id || w.StateID() == 0 {154+ t.Errorf("A1 %q, state %d (was %d)", inputs(s)["A1"], w.StateID(), id)155+ }156+ s.Set(Addr{}, "3")157+ if s.Undo(); inputs(s)["A1"] != "2" {158+ t.Errorf("undo after: A1 %q", inputs(s)["A1"])159+ }160+ if cost := s.UndoCost(NewRect(Addr{}, Addr{Col: 9, Row: 9})); cost < slotBytes || cost > 1024 {161+ t.Errorf("one cell's undo cost %d", cost)162+ }163+}
M internal/sheet/historysize.go +72 -11
@@ -5,21 +5,26 @@ import "slices"
55 // MaxUndoBytes caps the memory the undo history's before-images hold, as66 // estimated by step.size. When a new step takes the history past it, the77 // oldest steps are dropped first; the newest step is always kept, however8-// large, so any single change can be undone. 256 MB keeps MaxUndo steps9-// that each rewrite a whole column (about 250 MB, see docs/contributing/limits.md).8+// large, so any single change can be undone (a step past MaxStepBytes9+// asks first). 256 MB holds a step clearing a full max-cells sheet of10+// numbers, about 195 MB, and MaxUndo steps rewriting a whole column of11+// 8192 take 16 MB (see docs/contributing/limits.md).1012 const MaxUndoBytes = 256 << 201113 1214 // undoBudget is MaxUndoBytes; tests lower it.1315 var undoBudget int64 = MaxUndoBytes1416 15-// Estimated heap held by the parts of a step. A before-image is a Cell16-// (216 B) plus its map entry, its input and boxed literal (a plain cell's17-// are made for it from the store, see store.go); a formula also keeps its18-// parsed tree and reference lists alive, which grow with its text. The constants match the heap measured by the stress19-// benchmarks (BenchmarkHistoryFull) within about 10%.17+// Estimated heap held by the parts of a step. A plain cell's18+// before-image is a slot (slotBytes) and its text, once per image (see19+// historyimage.go); a rich one is also a Cell (216 B) with its input,20+// and a formula keeps its parsed tree and reference lists alive, which21+// grow with its text. The constants match the heap measured by the22+// stress benchmarks (BenchmarkHistoryFull, BenchmarkHistoryWide,23+// BenchmarkClearMax) within about 10%.2024 const (2125 stepBytes = 512 // the step and its empty maps2226 entryBytes = 64 // a map entry: the key and a pointer27+ imageBytes = 256 // a sheet's image, empty2328 cellBytes = 256 // a Cell, its input's header and its boxed literal2429 formulaBytes = 256 // a parsed formula's tree and references, plus perFormulaByte per byte of text2530 perFormulaByte = 16
@@ -27,10 +32,18 @@ const (
2732 ruleBytes = 256 // a rule with its ranges and values2833 )2934 30-// keep records c as the before-image of the cell at l and counts it.31-func (st *step) keep(l loc, c *Cell) {32- st.cells[l] = c33- st.cellBytes += entryBytes + cellSize(c)35+// keep records the cell at a of s as it stands, unless the step holds36+// it already, and counts it.37+func (st *step) keep(s *Sheet, a Addr) {38+ img := st.cells[s]39+ if img == nil {40+ img = &image{}41+ st.cells[s] = img42+ st.cellBytes += imageBytes43+ }44+ if !img.seen(a) {45+ st.cellBytes += img.keep(&s.cells, a)46+ }3447 }3548 3649 // size estimates the heap a finished step holds: its cells, counted as
@@ -108,3 +121,51 @@ func (h *history) popUndo() *step {
108121 109122 // HistoryBytes estimates the memory the undo steps hold.110123 func (w *Workbook) HistoryBytes() int64 { return w.hist.bytes }124+125+// MaxStepBytes is the most undo history one change may hold without126+// asking: the UI asks before a change its UndoCost puts over it, and runs127+// it WithoutUndo if told to go on. Plain cells cost about 20 B each, so128+// only millions of formulas or of distinct long texts come near it.129+const MaxStepBytes = 1 << 30130+131+// UndoCost estimates the undo history a change to every cell of r would132+// hold: a slot a cell, the rich cells in r whole, and r's share of the133+// sheet's strings.134+func (s *Sheet) UndoCost(r Rect) int64 {135+ st := &s.cells136+ cells := 0137+ for _, c := range st.stored.colsIn(r.From.Col, r.To.Col) {138+ cells += st.stored.countIn(c, r.From.Row, r.To.Row)139+ }140+ if cells == 0 {141+ return 0142+ }143+ n := int64(cells) * slotBytes144+ for _, rc := range st.rich {145+ if rc.c != nil && r.Contains(rc.a) {146+ n += richBytes + cellSize(rc.c)147+ }148+ }149+ var strs int64150+ for _, str := range st.strs.strs {151+ strs += strBytes + int64(len(str))152+ }153+ return n + strs*int64(cells)/int64(st.len())154+}155+156+// WithoutUndo runs fn, a change too large to undo (see MaxStepBytes),157+// recording no before-images, and forgets the undo history, which could158+// no longer be undone past it. Inside a Batch it runs fn as part of it.159+func (w *Workbook) WithoutUndo(fn func()) {160+ h := &w.hist161+ if h.depth > 0 {162+ fn()163+ return164+ }165+ h.off = true166+ defer func() {167+ id := h.lastID + 1168+ w.hist = history{lastID: id, base: id}169+ }()170+ fn()171+}
M internal/sheet/historysize_test.go +23 -4
@@ -22,9 +22,9 @@ func TestUndoByteBudget(t *testing.T) {
2222 fillColumn(t, s, 1000, "x")2323 w := s.Book()2424 w.ClearHistory()25- fillColumn(t, s, 1000, "0") // before-images of 1000 cells25+ fillColumn(t, s, 1000, "0") // before-images of 1000 cells, slots and one string2626 one := w.HistoryBytes()27- if one < 1000*cellBytes/2 || one > 1000*(entryBytes+cellBytes+64) {27+ if one < 1000*slotBytes || one > 1000*slotBytes+8<<10 {2828 t.Fatalf("a 1000-cell step is estimated at %d bytes", one)2929 }3030 defer func(b int64) { undoBudget = b }(undoBudget)
@@ -120,8 +120,8 @@ func historySum(w *Workbook) int64 {
120120 var n int64121121 for _, st := range w.hist.undo {122122 var cells int64123- for _, c := range st.cells {124- cells += entryBytes + cellSize(c)123+ for _, img := range st.cells {124+ cells += img.walkSize()125125 }126126 if cells != st.cellBytes {127127 return -1
@@ -130,3 +130,22 @@ func historySum(w *Workbook) int64 {
130130 }131131 return n132132 }133+134+// walkSize is what keep counts for the image, found by walking it.135+func (img *image) walkSize() int64 {136+ t := img.tables()137+ n := int64(imageBytes + t.stored.n*slotBytes)138+ for _, ic := range img.small {139+ if !ic.blank {140+ n += slotBytes141+ }142+ }143+ n += int64(t.stored.blocks()+img.blank.blocks()) * blockBytes144+ for _, rc := range t.rich {145+ n += richBytes + cellSize(rc.c)146+ }147+ for _, s := range t.strs.strs[min(1, len(t.strs.strs)):] {148+ n += strBytes + int64(len(s))149+ }150+ return n151+}
M internal/sheet/nav.go +17 -0
M internal/sheet/occupancy.go +24 -0