Every changed cell scanned every formula with a range reference, so a change cost O(changed x range users): 8192 running totals checked 67 million ranges per edit. Range users are now indexed by the columns their ranges cover. Formulas are queued once rather than once per cell they read, named ranges are looked up once per recalc, and the evaluator looks each cell's state up once. RecalcAll marks every cell dirty directly: tracing dependents from every cell found nothing new and cost O(cells x range users). Edit/running-8192 1.83 s -> 0.70 s Edit/fanin-1000xSUM8192 203 ms -> 168 ms Edit/names-1000 165 us -> 94 us RecalcAll/fanin 395 ms -> 172 ms RecalcAll/names-1000 157 ms -> 2.8 ms RecalcAll/dense-8192x26 69 ms -> 38 ms Build/running-8192 3.98 s -> 0.72 s
- commit
- 66fabf0336eefd39d11fc6cbfaa2fd5ba7c06856
- parent
- 1a9be26d5cb914f9973659f415bcf8b7a79e6d2c
- author
- Dami <dami.etoile@gmail.com>
| A | internal/ | 41 | ++++++++++++++ |
| A | internal/ | 51 | +++++++++++++++++ |
| M | internal/ | 89 | +++++++++++++++++++++--------- |
| M | internal/ | 2 | +- |
@@ -0,0 +1,41 @@
1+package sheet2+3+// rangeIndex finds the formulas whose range references may contain a4+// cell, bucketed by column. Recalculation asks it about every changed5+// cell; scanning every range user instead made a change cost O(changed x6+// range users), so filling a column of 8192 running totals checked 677+// million ranges. A formula whose ranges span several columns is in each8+// of their buckets.9+type rangeIndex struct {10+ byCol [MaxCols]map[Addr]struct{}11+}12+13+// add indexes the formula at a under the columns its ranges cover.14+func (x *rangeIndex) add(a Addr, ranges []Rect) {15+ for _, r := range ranges {16+ for c := max(r.From.Col, 0); c <= min(r.To.Col, MaxCols-1); c++ {17+ if x.byCol[c] == nil {18+ x.byCol[c] = make(map[Addr]struct{})19+ }20+ x.byCol[c][a] = struct{}{}21+ }22+ }23+}24+25+// remove undoes add for the same ranges.26+func (x *rangeIndex) remove(a Addr, ranges []Rect) {27+ for _, r := range ranges {28+ for c := max(r.From.Col, 0); c <= min(r.To.Col, MaxCols-1); c++ {29+ delete(x.byCol[c], a)30+ }31+ }32+}33+34+// candidates returns the formulas with a range over column col; callers35+// still test whether one of their ranges contains the cell.36+func (x *rangeIndex) candidates(col int) map[Addr]struct{} {37+ if col < 0 || col >= MaxCols {38+ return nil39+ }40+ return x.byCol[col]41+}@@ -0,0 +1,51 @@
1+package sheet2+3+import "testing"4+5+// Range users are found by column; a formula must follow a change in any6+// column its ranges cover, and stop following a range it no longer has.7+func TestRangeUsersByColumn(t *testing.T) {8+ tests := []struct {9+ name string10+ setup []string // cell=input pairs, in order11+ change string // cell=input12+ cell string13+ want float6414+ }{15+ {"middle column of a block", []string{"A1=1", "B2=2", "C3=3", "E1==SUM(A1:C3)"}, "B3=10", "E1", 16},16+ {"two ranges", []string{"A1=1", "D1=4", "E1==SUM(A1:A2,D1:D2)"}, "D2=5", "E1", 10},17+ {"range replaced by another", []string{"A1=1", "B1=2", "E1==SUM(A1:A5)", "E1==SUM(B1:B5)"}, "B2=3", "E1", 5},18+ {"through a chain", []string{"A1=1", "B1==SUM(A1:A3)", "C1==B1*2"}, "A3=4", "C1", 10},19+ {"row range", []string{"A1=1", "E2==SUM(A1:D1)"}, "D1=2", "E2", 3},20+ }21+ for _, tt := range tests {22+ t.Run(tt.name, func(t *testing.T) {23+ s := New()24+ set := func(pair string) {25+ t.Helper()26+ for i := range pair {27+ if pair[i] == '=' {28+ if err := s.Set(at(pair[:i]), pair[i+1:]); err != nil {29+ t.Fatal(err)30+ }31+ return32+ }33+ }34+ }35+ for _, p := range tt.setup {36+ set(p)37+ }38+ set(tt.change)39+ if got := s.Value(at(tt.cell)); got.Num != tt.want {40+ t.Errorf("%s = %v, want %v", tt.cell, got, tt.want)41+ }42+ })43+ }44+ // Replacing E1's formula leaves nothing indexed under column A.45+ s := New()46+ s.Set(at("E1"), "=SUM(A1:A5)")47+ s.Set(at("E1"), "=SUM(B1:B5)")48+ if n := len(s.rangeUsers.candidates(0)); n != 0 {49+ t.Errorf("%d stale range users in column A", n)50+ }51+}@@ -52,9 +52,10 @@ type Sheet struct {
5252 5353 // dependents maps a cell to the formula cells that reference it5454 // directly. Range references are kept on the formula cell itself and55- // scanned separately, so a huge range doesn't create millions of edges.55+ // found through rangeUsers, by column, so a huge range doesn't create56+ // millions of edges.5657 dependents map[Addr]map[Addr]struct{}57- rangeUsers map[Addr]struct{}58+ rangeUsers rangeIndex5859 volatile map[Addr]struct{} // formulas recalculated on every change5960 6061 // names are the named ranges by upper-case name (see names.go), and@@ -80,7 +81,6 @@ func New() *Sheet {
8081 cells: make(map[Addr]*Cell),8182 widths: make(map[int]int),8283 dependents: make(map[Addr]map[Addr]struct{}),83- rangeUsers: make(map[Addr]struct{}),8484 volatile: make(map[Addr]struct{}),8585 names: make(map[string]Name),8686 nameUsers: make(map[string]map[Addr]struct{}),@@ -292,9 +292,7 @@ func (s *Sheet) place(a Addr, c *Cell) {
292292 }293293 s.dependents[r][a] = struct{}{}294294 }295- if len(c.ranges) > 0 {296- s.rangeUsers[a] = struct{}{}297- }295+ s.rangeUsers.add(a, c.ranges)298296 if c.volatile {299297 s.volatile[a] = struct{}{}300298 }@@ -347,7 +345,7 @@ func (s *Sheet) unlink(a Addr) {
347345 delete(s.nameUsers, k)348346 }349347 }350- delete(s.rangeUsers, a)348+ s.rangeUsers.remove(a, old.ranges)351349 delete(s.volatile, a)352350 delete(s.cells, a)353351 }@@ -355,26 +353,54 @@ func (s *Sheet) unlink(a Addr) {
355353 // RecalcAll recomputes every formula. Loaders call it once the sheet is356354 // built, so it also starts a fresh undo history: loading isn't undoable.357355 func (s *Sheet) RecalcAll() {358- s.recalc(s.Addrs())356+ // Every cell is dirty, so there is nothing to propagate: tracing357+ // dependents from each cell cost O(cells x range users).358+ state := make(map[Addr]int, len(s.cells))359+ for a := range s.cells {360+ state[a] = dirty361+ }362+ s.evaluate(state)359363 s.ClearHistory()360364 }361365 366+// Recalculation states of a cell.367+const (368+ dirty = iota + 1369+ visiting370+ done371+)372+362373 // recalc recomputes the changed cells, volatile formulas, and everything363-// that transitively depends on them. Cells are evaluated lazily in364-// dependency order: reading a dirty cell evaluates it first. A cell that365-// is reached again while it is still being evaluated is part of a cycle366-// and becomes ERR.374+// that transitively depends on them.367375 func (s *Sheet) recalc(changed []Addr) {368- const (369- dirty = iota + 1370- visiting371- done372- )376+ s.evaluate(s.affected(changed))377+}378+379+// affected marks dirty the changed cells, volatile formulas, and every380+// formula that transitively reads them.381+func (s *Sheet) affected(changed []Addr) map[Addr]int {373382 state := make(map[Addr]int)374383 queue := append([]Addr(nil), changed...)375384 for a := range s.volatile {376385 queue = append(queue, a)377386 }387+ // The named ranges in use, looked up once rather than for every cell.388+ type namedUsers struct {389+ r Rect390+ users map[Addr]struct{}391+ }392+ var named []namedUsers393+ for k, users := range s.nameUsers {394+ if nm, ok := s.names[k]; ok && !nm.Lost {395+ named = append(named, namedUsers{nm.Range, users})396+ }397+ }398+ // Queue each formula once, not once per changed cell it reads.399+ push := func(u Addr) {400+ if state[u] != dirty {401+ queue = append(queue, u)402+ }403+ }378404 for len(queue) > 0 {379405 a := queue[0]380406 queue = queue[1:]@@ -383,37 +409,46 @@ func (s *Sheet) recalc(changed []Addr) {
383409 }384410 state[a] = dirty385411 for d := range s.dependents[a] {386- queue = append(queue, d)412+ push(d)387413 }388- for u := range s.rangeUsers {414+ for u := range s.rangeUsers.candidates(a.Col) {389415 for _, r := range s.cells[u].ranges {390416 if r.Contains(a) {391- queue = append(queue, u)417+ push(u)392418 break393419 }394420 }395421 }396- for k, users := range s.nameUsers {397- if nm, ok := s.names[k]; ok && !nm.Lost && nm.Range.Contains(a) {398- for u := range users {399- queue = append(queue, u)422+ for _, n := range named {423+ if n.r.Contains(a) {424+ for u := range n.users {425+ push(u)400426 }401427 }402428 }403429 }430+ return state431+}404432 433+// evaluate computes the cells marked dirty in state. Cells are evaluated434+// lazily in dependency order: reading a dirty cell evaluates it first. A435+// cell that is reached again while it is still being evaluated is part of436+// a cycle and becomes ERR.437+func (s *Sheet) evaluate(state map[Addr]int) {405438 s.Circular = false406439 s.hidden.valid = false // values may have changed what the filter hides407440 var compute func(Addr) Value408441 compute = func(a Addr) Value {442+ // Every cell a formula reads comes through here, so it looks443+ // each map up once: a SUM over 8192 cells makes 8192 calls.409444 c := s.cells[a]410- switch {445+ switch st := state[a]; {411446 case c == nil:412447 return Value{}413- case state[a] == visiting:448+ case st == visiting:414449 s.Circular = true415450 return ErrRef416- case state[a] != dirty:451+ case st != dirty:417452 return c.Value418453 }419454 state[a] = visiting@@ -31,7 +31,7 @@ func (s *Sheet) Dependents(a Addr) []Addr {
3131 for d := range s.dependents[a] {3232 seen[d] = true3333 }34- for u := range s.rangeUsers {34+ for u := range s.rangeUsers.candidates(a.Col) {3535 for _, r := range s.cells[u].ranges {3636 if r.Contains(a) {3737 seen[u] = true