cat / 012
clone

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

Keep recalculation state on each sheet

Dami committed d6e5348
Recalculating a workbook keyed its state by sheet and address and made
a lookup per formula, which cost the stress shapes up to a third more
time and an allocation or three per formula. The state now lives on
each sheet, keyed by address as before, and each sheet's lookup is
made once per recalculation.
commit
d6e534848d4fedbba22d9c3f0a59305a845dfbc0
parent
2ec17a7702bf1b119e3853b2c5b1edc85c158cfa
author
Dami <dami.etoile@gmail.com>
1 file changed, +66 -48
Minternal/sheet/sheet.go114+++++++++++++++++-------------
M internal/sheet/sheet.go +66 -48
@@ -68,6 +68,13 @@ type Sheet struct {
6868 version uint646969 stats statsCache7070 71+ // calc is the recalculation state of cells while the workbook72+ // recalculates, with calcGet and calcFmt, the lookups its formulas73+ // read other cells and formats with; all nil otherwise.74+ calc map[Addr]int75+ calcGet lookup76+ calcFmt func(string, Addr) Format77+7178 charts []Chart // floating charts, bottom first; see chart.go7279 7380 view viewState // frozen panes and the filter, see view.go
@@ -369,16 +376,14 @@ func (w *Workbook) recalcAll() {
369376 start := recalcStart()370377 n := 0371378 for _, s := range w.sheets {372- n += len(s.cells)373- }374- state := make(map[loc]int, n)375- for _, s := range w.sheets {379+ s.calc = make(map[Addr]int, len(s.cells))376380 for a := range s.cells {377- state[loc{s, a}] = dirty381+ s.calc[a] = dirty378382 }383+ n += len(s.calc)379384 }380- w.evaluate(state)381- w.observe(true, start, len(state))385+ w.evaluate()386+ w.observe(true, start, n)382387 }383388 384389 // Recalculation states of a cell.
@@ -392,15 +397,18 @@ const (
392397 // that transitively depends on them, on any sheet.393398 func (w *Workbook) recalc(changed []loc) {394399 start := recalcStart()395- state := w.affected(changed)396- w.evaluate(state)397- w.observe(false, start, len(state))400+ n := w.affected(changed)401+ w.evaluate()402+ w.observe(false, start, n)398403 }399404 400-// affected marks dirty the changed cells, volatile formulas, and every401-// formula that transitively reads them, on any sheet.402-func (w *Workbook) affected(changed []loc) map[loc]int {403- state := make(map[loc]int)405+// affected marks dirty, in each sheet's calc, the changed cells, volatile406+// formulas, and every formula that transitively reads them, on any sheet,407+// and returns how many it marked.408+func (w *Workbook) affected(changed []loc) int {409+ for _, s := range w.sheets {410+ s.calc = make(map[Addr]int)411+ }404412 queue := append([]loc(nil), changed...)405413 for _, s := range w.sheets {406414 for a := range s.volatile {
@@ -421,18 +429,20 @@ func (w *Workbook) affected(changed []loc) map[loc]int {
421429 }422430 // Queue each formula once, not once per changed cell it reads.423431 push := func(u loc) {424- if state[u] != dirty {432+ if u.s.calc[u.a] != dirty {425433 queue = append(queue, u)426434 }427435 }436+ n := 0428437 for len(queue) > 0 {429438 l := queue[0]430439 queue = queue[1:]431- if state[l] == dirty || !l.s.live {440+ s, a := l.s, l.a441+ if !s.live || s.calc[a] == dirty {432442 continue433443 }434- state[l] = dirty435- s, a := l.s, l.a444+ s.calc[a] = dirty445+ n++436446 for d := range s.dependents[a] {437447 push(loc{s, d})438448 }
@@ -444,9 +454,9 @@ func (w *Workbook) affected(changed []loc) map[loc]int {
444454 }445455 }446456 }447- for _, n := range named {448- if n.s == s && n.r.Contains(a) {449- for u := range n.users {457+ for _, nu := range named {458+ if nu.s == s && nu.r.Contains(a) {459+ for u := range nu.users {450460 push(u)451461 }452462 }
@@ -460,25 +470,23 @@ func (w *Workbook) affected(changed []loc) map[loc]int {
460470 }461471 }462472 }463- return state473+ return n464474 }465475 466-// evaluate computes the cells marked dirty in state. Cells are evaluated467-// lazily in dependency order: reading a dirty cell evaluates it first. A468-// cell that is reached again while it is still being evaluated is part of469-// a cycle and becomes ERR.470-func (w *Workbook) evaluate(state map[loc]int) {476+// evaluate computes the cells marked dirty in each sheet's calc. Cells477+// are evaluated lazily in dependency order: reading a dirty cell478+// evaluates it first. A cell that is reached again while it is still479+// being evaluated is part of a cycle and becomes ERR. The state lives on480+// the sheets, keyed by address as on one sheet, and each sheet's lookup481+// is made once, so evaluating a formula allocates nothing for sheets.482+func (w *Workbook) evaluate() {471483 w.Circular = false472- for _, s := range w.sheets {473- s.version++474- s.hidden.valid = false // values may have changed what the filter hides475- }476- var compute func(loc) Value477- compute = func(l loc) Value {484+ var compute func(s *Sheet, a Addr) Value485+ compute = func(s *Sheet, a Addr) Value {478486 // Every cell a formula reads comes through here, so it looks479487 // each map up once: a SUM over 8192 cells makes 8192 calls.480- c := l.s.cells[l.a]481- switch st := state[l]; {488+ c := s.cells[a]489+ switch st := s.calc[a]; {482490 case c == nil:483491 return Value{}484492 case st == visiting:
@@ -487,7 +495,7 @@ func (w *Workbook) evaluate(state map[loc]int) {
487495 case st != dirty:488496 return c.Value489497 }490- state[l] = visiting498+ s.calc[a] = visiting491499 c.auto = Format{}492500 switch {493501 case c.Input == "":
@@ -497,18 +505,28 @@ func (w *Workbook) evaluate(state map[loc]int) {
497505 case c.expr == nil:498506 c.Value = Value{Kind: Text, Str: strings.TrimPrefix(c.Input, "'")}499507 default:500- expr := w.arith(l.s.bound(c))501- // One small closure per formula, to resolve sheet names.502- c.Value = eval(expr, w.lookupFrom(l.s, compute))508+ expr := w.arith(s.bound(c))509+ c.Value = eval(expr, s.calcGet)503510 if _, lit := expr.(numLit); !lit {504- c.auto = inferFormat(expr, w.formatFrom(l.s))511+ c.auto = inferFormat(expr, s.calcFmt)505512 }506513 }507- state[l] = done514+ s.calc[a] = done508515 return c.Value509516 }510- for l := range state {511- compute(l)517+ for _, s := range w.sheets {518+ s.version++519+ s.hidden.valid = false // values may have changed what the filter hides520+ s.calcGet = w.lookupOn(s, compute)521+ s.calcFmt = w.formatFrom(s)522+ }523+ for _, s := range w.sheets {524+ for a := range s.calc {525+ compute(s, a)526+ }527+ }528+ for _, s := range w.sheets {529+ s.calc, s.calcGet, s.calcFmt = nil, nil, nil512530 }513531 }514532
@@ -527,16 +545,16 @@ func (w *Workbook) crossReads(u, l loc) bool {
527545 return false528546 }529547 530-// lookupFrom resolves references in a formula on s with get: references548+// lookupOn resolves references in a formula on s with get: references531549 // without a sheet read s, others the sheet they name (#REF! when no sheet532550 // has that name). The last sheet name is remembered, so a range on533551 // another sheet resolves its name once.534-func (w *Workbook) lookupFrom(s *Sheet, get func(loc) Value) lookup {552+func (w *Workbook) lookupOn(s *Sheet, get func(*Sheet, Addr) Value) lookup {535553 var lastName string536554 var last *Sheet537555 return func(sheet string, a Addr) Value {538556 if sheet == "" {539- return get(loc{s, a})557+ return get(s, a)540558 }541559 if sheet != lastName || last == nil {542560 lastName, last = sheet, w.byKey[sheetKey(sheet)]
@@ -544,13 +562,13 @@ func (w *Workbook) lookupFrom(s *Sheet, get func(loc) Value) lookup {
544562 if last == nil {545563 return ErrRef546564 }547- return get(loc{last, a})565+ return get(last, a)548566 }549567 }550568 551569 // values reads current values for formulas on s, across sheets.552570 func (w *Workbook) values(s *Sheet) lookup {553- return w.lookupFrom(s, func(l loc) Value { return l.s.Value(l.a) })571+ return w.lookupOn(s, func(t *Sheet, a Addr) Value { return t.Value(a) })554572 }555573 556574 // formatFrom reads display formats for formulas on s, across sheets.