Every answer recalculated every volatile formula, so each of 4000 JEV answers re-evaluated all 4000 cells. The engine now notes, as a JEV function evaluates to Loading..., which formula asked which question (keyed by RemoteCall.Key, moved from internal/jev), and Workbook.RecalcAnswered recomputes just those formulas and what reads them.
- commit
- a0fb6e4b3f7d3e075f2e2d081c649355bf879380
- parent
- ce91bf34953aeb2b29fd158027eafd35bf0ee8bd
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 96 | +----------------------------- |
| M | internal/ | 1 | + |
| M | internal/ | 42 | +++++++++++++ |
| M | internal/ | 21 | +++++-- |
| M | internal/ | 42 | ++++++++++++- |
| A | internal/ | 95 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 13 | +++- |
@@ -1,96 +1,6 @@
11 package jev22 3-import (4- "encoding/json"5- "fmt"6- "maps"7- "slices"8- "strconv"9- "strings"3+import "github.com/FelineStateMachine/012/internal/sheet"104 11- "github.com/FelineStateMachine/012/internal/sheet"12-)13-14-// Key identifies a question by its content. JEV cells are volatile, so15-// every recalculation looks each one up: with thousands of them,16-// encoding the key with encoding/json took most of the time an answer17-// spent in recalc. The shapes a RemoteCall holds are encoded by hand,18-// each value tagged and each string length-prefixed so different calls19-// never share a key; anything else falls back to JSON.20-func Key(c sheet.RemoteCall) string {21- var b strings.Builder22- b.Grow(64 + len(c.Instructions))23- str(&b, c.Kind)24- str(&b, c.Instructions)25- if appendValue(&b, c.State) && appendValue(&b, c.Criteria) {26- return b.String()27- }28- j, err := json.Marshal(c)29- if err != nil {30- return fmt.Sprintf("%#v", c)31- }32- return "j" + string(j)33-}34-35-func str(b *strings.Builder, s string) {36- b.WriteString(strconv.Itoa(len(s)))37- b.WriteByte(':')38- b.WriteString(s)39-}40-41-// appendValue encodes v and reports whether it is a shape it knows.42-func appendValue(b *strings.Builder, v any) bool {43- switch v := v.(type) {44- case nil:45- b.WriteByte('n')46- case string:47- b.WriteByte('s')48- str(b, v)49- case float64:50- b.WriteByte('f')51- b.WriteString(strconv.FormatFloat(v, 'g', -1, 64))52- b.WriteByte(';')53- case bool:54- if v {55- b.WriteByte('T')56- } else {57- b.WriteByte('F')58- }59- case [2]string:60- b.WriteByte('p')61- str(b, v[0])62- str(b, v[1])63- case []string:64- b.WriteByte('l')65- b.WriteString(strconv.Itoa(len(v)))66- for _, s := range v {67- str(b, s)68- }69- case map[string]string:70- b.WriteByte('m')71- b.WriteString(strconv.Itoa(len(v)))72- for _, k := range slices.Sorted(maps.Keys(v)) {73- str(b, k)74- str(b, v[k])75- }76- case []any:77- b.WriteByte('a')78- b.WriteString(strconv.Itoa(len(v)))79- for _, e := range v {80- if !appendValue(b, e) {81- return false82- }83- }84- case [][]any:85- b.WriteByte('r')86- b.WriteString(strconv.Itoa(len(v)))87- for _, row := range v {88- if !appendValue(b, row) {89- return false90- }91- }92- default:93- return false94- }95- return true96-}5+// Key identifies a question by its content; see sheet.RemoteCall.Key.6+func Key(c sheet.RemoteCall) string { return c.Key() }@@ -64,6 +64,7 @@ func remoteEval(build func([]Node, lookup) (RemoteCall, error), result func(Remo
6464 ans, ok := remote.Lookup(call)6565 switch {6666 case !ok:67+ get.w.wait(call)6768 return Pending6869 case ans.Failed != "":6970 return ErrRemote@@ -109,3 +109,45 @@ func TestJEVWithoutKey(t *testing.T) {
109109 t.Errorf("no key: %+v", s.Value(at("A1")))110110 }111111 }112+113+// An answer recalculates the formulas that asked it and what reads them,114+// not every JEV formula.115+func TestRecalcAnswered(t *testing.T) {116+ f := &fakeRemote{answers: map[string]RemoteAnswer{}}117+ s := New()118+ s.SetRemote(f)119+ s.Set(at("A1"), `=JEV.TEST("x", "Q1")`)120+ s.Set(at("A2"), `=JEV.TEST("x", "Q2")`)121+ s.Set(at("A3"), `=IF(JEV.TEST("x", "Q1"), "yes", "no")`)122+ s.Set(at("B1"), `=A1`)123+ if !IsPending(s.Value(at("A1"))) || !IsPending(s.Value(at("A3"))) {124+ t.Fatalf("A1 %+v, A3 %+v", s.Value(at("A1")), s.Value(at("A3")))125+ }126+ f.answers["Q1"] = RemoteAnswer{Noul: 0.9}127+ f.asked = nil128+ q1 := RemoteCall{Kind: "noul", State: "x", Instructions: "Q1", Criteria: [2]string{}}129+ s.Book().RecalcAnswered([]RemoteCall{q1})130+ for _, c := range f.asked {131+ if c.Instructions != "Q1" {132+ t.Errorf("an answer to Q1 asked %q again", c.Instructions)133+ }134+ }135+ if len(f.asked) != 2 {136+ t.Errorf("%d questions looked up, want the 2 cells asking Q1", len(f.asked))137+ }138+ if got := s.Value(at("B1")); got != boolean(true) {139+ t.Errorf("B1, reading A1, = %+v", got)140+ }141+ if got := s.Value(at("A3")); got.Str != "yes" {142+ t.Errorf("A3 = %+v", got)143+ }144+ if !IsPending(s.Value(at("A2"))) {145+ t.Errorf("A2 = %+v", s.Value(at("A2")))146+ }147+ // Answered questions are forgotten; an unknown one does nothing.148+ f.asked = nil149+ s.Book().RecalcAnswered([]RemoteCall{q1, {Kind: "noul", Instructions: "Q9"}})150+ if len(f.asked) != 0 {151+ t.Errorf("answering again looked up %d questions", len(f.asked))152+ }153+}@@ -50,9 +50,14 @@ const (
5050 5151 // recalc recomputes the changed cells, volatile formulas, and everything5252 // that transitively depends on them, on any sheet.53-func (w *Workbook) recalc(changed []loc) {53+func (w *Workbook) recalc(changed []loc) { w.recalcFrom(changed, true) }54+55+// recalcFrom recomputes the changed cells and everything that56+// transitively depends on them, and the volatile formulas with theirs57+// when volatiles is set.58+func (w *Workbook) recalcFrom(changed []loc, volatiles bool) {5459 start := recalcStart()55- n := w.affected(changed)60+ n := w.affected(changed, volatiles)5661 stale := w.stalePivots()5762 w.evaluate()5863 w.observe(false, start, n)@@ -60,13 +65,16 @@ func (w *Workbook) recalc(changed []loc) {
6065 }6166 6267 // affected marks dirty, in each sheet's calc, the changed cells, volatile63-// formulas, and every formula that transitively reads them, on any sheet,64-// and returns how many it marked.65-func (w *Workbook) affected(changed []loc) int {68+// formulas if volatiles is set, and every formula that transitively reads69+// them, on any sheet, and returns how many it marked.70+func (w *Workbook) affected(changed []loc, volatiles bool) int {6671 m := marking{w: w, queue: append([]loc(nil), changed...), named: w.namedInUse(), byName: map[*Sheet]bool{}}6772 for _, s := range w.sheets {6873 s.calc = make(map[Addr]int)6974 m.byName[s] = w.crossKeys[formula.SheetKey(s.name)] > 075+ if !volatiles {76+ continue77+ }7078 for a := range s.volatile {7179 m.queue = append(m.queue, loc{s, a})7280 }@@ -196,7 +204,10 @@ func (w *Workbook) evaluate() {
196204 c.Value = Value{Kind: Text, Str: strings.TrimPrefix(c.Input, "'")}197205 default:198206 expr := s.bound(c)207+ outer := w.evaluating208+ w.evaluating = loc{s, a}199209 c.Value = eval(w.arith(expr), s.calcGet)210+ w.evaluating = outer200211 if _, lit := expr.(formula.Num); !lit {201212 c.auto = inferFormat(expr, s.calcFmt)202213 }@@ -6,11 +6,11 @@ import "github.com/FelineStateMachine/012/internal/formula"
66 // talks to the network: it describes each question as a RemoteCall and77 // asks the workbook's RemoteSource for the answer. Until the answer arrives the cell shows88 // Loading…, and the source queues the call; when answers arrive the UI9-// calls RecalcVolatile, and the functions, being volatile, look again.9+// calls RecalcAnswered, which recomputes the formulas waiting for them.10+// The functions are volatile, so any other change looks again too.1011 1112 // RemoteCall is one question for the model, built from a function's12-// arguments. It is comparable through its JSON form, which callers use as13-// a cache key.13+// arguments. It isn't comparable: Key identifies it by its content.1414 type RemoteCall struct {1515 Kind string `json:"kind"` // "noul", "choice" or "score"1616 State any `json:"state"`@@ -93,3 +93,39 @@ func (s *Sheet) RemoteCalls(a Addr) []RemoteCall {
9393 walk(s.bound(c))9494 return calls9595 }96+97+// wait notes that the formula being evaluated is waiting for the answer98+// to c.99+func (w *Workbook) wait(c RemoteCall) {100+ if w.evaluating.s == nil {101+ return102+ }103+ k := c.Key()104+ if w.waiting == nil {105+ w.waiting = map[string]map[loc]struct{}{}106+ }107+ if w.waiting[k] == nil {108+ w.waiting[k] = map[loc]struct{}{}109+ }110+ w.waiting[k][w.evaluating] = struct{}{}111+}112+113+// RecalcAnswered recomputes the formulas that were waiting for the114+// answers to calls, and what depends on them, on every sheet: with115+// thousands of JEV cells, an answer costs the cells that asked it, not116+// all of them. It doesn't touch the undo history.117+func (w *Workbook) RecalcAnswered(calls []RemoteCall) {118+ var changed []loc119+ for _, c := range calls {120+ k := c.Key()121+ for l := range w.waiting[k] {122+ if l.s.live && l.s.cells.get(l.a) != nil {123+ changed = append(changed, l)124+ }125+ }126+ delete(w.waiting, k)127+ }128+ if len(changed) > 0 {129+ w.recalcFrom(changed, false)130+ }131+}@@ -0,0 +1,95 @@
1+package sheet2+3+import (4+ "encoding/json"5+ "fmt"6+ "maps"7+ "slices"8+ "strconv"9+ "strings"10+)11+12+// Key identifies a question by its content: the answer cache and the13+// engine's list of cells waiting for answers are keyed by it. JEV cells14+// are volatile, so every recalculation looks each one up: with thousands15+// of them, encoding the key with encoding/json took most of the time an16+// answer spent in recalc. The shapes a RemoteCall holds are encoded by17+// hand, each value tagged and each string length-prefixed so different18+// calls never share a key; anything else falls back to JSON.19+func (c RemoteCall) Key() string {20+ var b strings.Builder21+ b.Grow(64 + len(c.Instructions))22+ keyStr(&b, c.Kind)23+ keyStr(&b, c.Instructions)24+ if keyValue(&b, c.State) && keyValue(&b, c.Criteria) {25+ return b.String()26+ }27+ j, err := json.Marshal(c)28+ if err != nil {29+ return fmt.Sprintf("%#v", c)30+ }31+ return "j" + string(j)32+}33+34+func keyStr(b *strings.Builder, s string) {35+ b.WriteString(strconv.Itoa(len(s)))36+ b.WriteByte(':')37+ b.WriteString(s)38+}39+40+// keyValue encodes v and reports whether it is a shape it knows.41+func keyValue(b *strings.Builder, v any) bool {42+ switch v := v.(type) {43+ case nil:44+ b.WriteByte('n')45+ case string:46+ b.WriteByte('s')47+ keyStr(b, v)48+ case float64:49+ b.WriteByte('f')50+ b.WriteString(strconv.FormatFloat(v, 'g', -1, 64))51+ b.WriteByte(';')52+ case bool:53+ if v {54+ b.WriteByte('T')55+ } else {56+ b.WriteByte('F')57+ }58+ case [2]string:59+ b.WriteByte('p')60+ keyStr(b, v[0])61+ keyStr(b, v[1])62+ case []string:63+ b.WriteByte('l')64+ b.WriteString(strconv.Itoa(len(v)))65+ for _, s := range v {66+ keyStr(b, s)67+ }68+ case map[string]string:69+ b.WriteByte('m')70+ b.WriteString(strconv.Itoa(len(v)))71+ for _, k := range slices.Sorted(maps.Keys(v)) {72+ keyStr(b, k)73+ keyStr(b, v[k])74+ }75+ case []any:76+ b.WriteByte('a')77+ b.WriteString(strconv.Itoa(len(v)))78+ for _, e := range v {79+ if !keyValue(b, e) {80+ return false81+ }82+ }83+ case [][]any:84+ b.WriteByte('r')85+ b.WriteString(strconv.Itoa(len(v)))86+ for _, row := range v {87+ if !keyValue(b, row) {88+ return false89+ }90+ }91+ default:92+ return false93+ }94+ return true95+}@@ -39,10 +39,15 @@ type Workbook struct {
3939 // Circular is set when the last recalculation found a cycle.4040 Circular bool4141 42- remote RemoteSource // answers JEV functions, see remote.go43- hist history // undo and redo, see history.go44- active int // the sheet last shown, saved in the file45- decimal bool // decimal arithmetic, see decimal.go42+ remote RemoteSource // answers JEV functions, see remote.go43+ // waiting are the formulas that were shown Loading… by question key,44+ // so an answer recalculates only them; evaluating is the formula45+ // being evaluated, which a question is asked for (see remote.go).46+ waiting map[string]map[loc]struct{}47+ evaluating loc48+ hist history // undo and redo, see history.go49+ active int // the sheet last shown, saved in the file50+ decimal bool // decimal arithmetic, see decimal.go4651 4752 macros []Macro // see macros.go4853 macroOrigin string