An answer is stored as it arrives; the first since the last recalculation schedules one a frame later (ui.FrameInterval), which recalculates the cells waiting for every answer stored meanwhile. The stress benchmarks also time Ctrl+Shift+arrow over a whole sheet.
- commit
- 09e6bb69bb3ab47bfea0474dad0721b4ec19d90b
- parent
- a0fb6e4b3f7d3e075f2e2d081c649355bf879380
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 48 | +++++++++++++++++---- |
| M | internal/ | 69 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 12 | +++++ |
| M | internal/ | 12 | ++++- |
@@ -16,7 +16,8 @@ import (
1616 1717 // JEV functions are answered in the background: the engine queues1818 // questions in the cache, Update sends them as commands (a few at a time),19-// and each answer triggers a recalculation of the JEV cells.19+// and answers are stored as they arrive and, a frame later, the cells20+// that asked them are recalculated together.2021 2122 const (2223 jevParallel = 8@@ -26,6 +27,11 @@ const (
2627 type jevRunner struct {2728 client jev.Client2829 cache *jev.Cache30+31+ // answered are the questions answered since the last recalculation,32+ // which runs gather after the first of them: at most one per frame.33+ answered []sheet.RemoteCall34+ gather time.Duration2935 }3036 3137 type jevAnswerMsg struct {@@ -33,10 +39,13 @@ type jevAnswerMsg struct {
3339 answer sheet.RemoteAnswer3440 }3541 42+// jevRecalcMsg recalculates the cells whose questions were answered.43+type jevRecalcMsg struct{}44+3645 // EnableJEV turns on JEV functions: the cache answers them, for this3746 // workbook and those opened later, and the client asks what it lacks.3847 func (m *Model) EnableJEV(client jev.Client, cache *jev.Cache) {39- m.jev = &jevRunner{client: client, cache: cache}48+ m.jev = &jevRunner{client: client, cache: cache, gather: FrameInterval}4049 m.sheet.Book().SetRemote(cache)4150 }4251 @@ -99,16 +108,37 @@ func (j *jevRunner) send() tea.Cmd {
99108 return tea.Batch(cmds...)100109 }101110 102-// answerJEV stores an answer and recalculates the JEV cells, telling103-// the user when the last question is answered.111+// answerJEV stores an answer. The first answer since the last112+// recalculation schedules the next one, a frame later, so answers that113+// arrive together recalculate once.104114 func (m *Model) answerJEV(msg jevAnswerMsg) tea.Cmd {105- if m.jev == nil {115+ j := m.jev116+ if j == nil {117+ return nil118+ }119+ j.cache.Store(msg.call, msg.answer)120+ j.answered = append(j.answered, msg.call)121+ if len(j.answered) > 1 {122+ return nil // already scheduled123+ }124+ if j.gather <= 0 {125+ return func() tea.Msg { return jevRecalcMsg{} }126+ }127+ return tea.Tick(j.gather, func(time.Time) tea.Msg { return jevRecalcMsg{} })128+}129+130+// recalcAnswered recalculates the cells waiting for the answers stored131+// since the last time, telling the user when the last question is132+// answered.133+func (m *Model) recalcAnswered() tea.Cmd {134+ j := m.jev135+ if j == nil || len(j.answered) == 0 {106136 return nil107137 }108- busy := m.jev.busy() != ""109- m.jev.cache.Store(msg.call, msg.answer)110- m.sheet.RecalcVolatile()111- if busy && m.jev.busy() == "" {138+ calls := j.answered139+ j.answered = nil140+ m.sheet.Book().RecalcAnswered(calls)141+ if j.busy() == "" {112142 return m.term.notify("JEV finished answering in " + m.displayName())113143 }114144 return nil@@ -106,6 +106,75 @@ func TestJEVWithoutKeyExplains(t *testing.T) {
106106 }107107 }108108 109+// Answers that arrive within a frame are recalculated together, once.110+func TestJEVAnswersRecalculateOncePerFrame(t *testing.T) {111+ m, _ := jevModel(t)112+ var cmds []tea.Cmd113+ for _, q := range []string{"Q1", "Q2", "Q3"} {114+ for _, r := range `=JEV.TEST("x", "` + q + `")` {115+ m.Update(tea.KeyPressMsg{Code: r, Text: string(r)})116+ }117+ _, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})118+ cmds = append(cmds, cmd)119+ }120+ if in, _ := m.jev.cache.Busy(); in != 3 {121+ t.Fatalf("%d questions in flight, want 3", in)122+ }123+ recalcs := 0124+ sheet.OnRecalc = func(sheet.RecalcInfo) { recalcs++ }125+ defer func() { sheet.OnRecalc = nil }()126+ var answers []jevAnswerMsg127+ for _, cmd := range cmds {128+ for _, msg := range collect(cmd) {129+ if a, ok := msg.(jevAnswerMsg); ok {130+ answers = append(answers, a)131+ }132+ }133+ }134+ if len(answers) != 3 {135+ t.Fatalf("%d answers", len(answers))136+ }137+ var tick tea.Cmd138+ for i, a := range answers {139+ _, cmd := m.Update(a)140+ if i == 0 {141+ tick = cmd142+ }143+ }144+ if recalcs != 0 {145+ t.Errorf("%d recalculations before the frame", recalcs)146+ }147+ run(m, tick)148+ if recalcs != 1 {149+ t.Errorf("%d recalculations for 3 answers, want 1", recalcs)150+ }151+ for _, a := range []string{"A1", "A2", "A3"} {152+ if got := m.sheet.Value(addr(a)); got.Kind != sheet.Bool {153+ t.Errorf("%s = %+v", a, got)154+ }155+ }156+}157+158+// collect runs cmd and returns the messages it and its batches produce,159+// without feeding them to a model.160+func collect(cmd tea.Cmd) []tea.Msg {161+ if cmd == nil {162+ return nil163+ }164+ switch msg := cmd().(type) {165+ case nil:166+ return nil167+ case tea.BatchMsg:168+ var out []tea.Msg169+ for _, c := range msg {170+ out = append(out, collect(c)...)171+ }172+ return out173+ default:174+ return []tea.Msg{msg}175+ }176+}177+109178 // Starting a new sheet or opening a file keeps JEV on.110179 func TestJEVSurvivesNewSheet(t *testing.T) {111180 m, fake := jevModel(t)@@ -48,6 +48,16 @@ const (
4848 // doubleClick is the longest gap between two clicks that edits a cell.4949 const doubleClick = 400 * time.Millisecond5050 51+const (52+ // FrameRate is how many frames a second Bubble Tea may draw, its53+ // maximum (tea.WithFPS). A key's answer waits for the next frame, so54+ // this halves the wait at the default 60. The renderer writes a frame55+ // only when the view changed, so an idle screen costs no more.56+ FrameRate = 12057+ // FrameInterval is the time between frames.58+ FrameInterval = time.Second / FrameRate59+)60+5161 // Model is the root of the UI, the tea.Model Bubble Tea runs. It holds5262 // the file, the mode and the grid, owns a component for everything that5363 // takes input or draws a part of the screen, routes each message to the@@ -168,6 +178,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
168178 }169179 case jevAnswerMsg:170180 cmd = m.answerJEV(msg)181+ case jevRecalcMsg:182+ cmd = m.recalcAnswered()171183 case importedMsg, importTickMsg, exportedMsg:172184 cmd = m.handleTransfer(msg)173185 case macroCallMsg:@@ -124,6 +124,12 @@ func key(k string) tea.KeyPressMsg {
124124 return tea.KeyPressMsg{Code: tea.KeyDown, Mod: tea.ModShift}125125 case "shift+up":126126 return tea.KeyPressMsg{Code: tea.KeyUp, Mod: tea.ModShift}127+ case "ctrl+shift+down":128+ return tea.KeyPressMsg{Code: tea.KeyDown, Mod: tea.ModCtrl | tea.ModShift}129+ case "ctrl+shift+up":130+ return tea.KeyPressMsg{Code: tea.KeyUp, Mod: tea.ModCtrl | tea.ModShift}131+ case "ctrl+shift+right":132+ return tea.KeyPressMsg{Code: tea.KeyRight, Mod: tea.ModCtrl | tea.ModShift}127133 case "ctrl+a":128134 return tea.KeyPressMsg{Code: 'a', Mod: tea.ModCtrl}129135 case "enter":@@ -155,6 +161,7 @@ func BenchmarkKeystroke(b *testing.B) {
155161 {"select-data/dense-8192x256", uiShapes()[2], []string{"ctrl+a"}, nil},156162 {"select-sheet/dense-8192x256", uiShapes()[2], []string{"ctrl+a", "ctrl+a"}, nil},157163 {"extend-data/dense-8192x256", uiShapes()[2], []string{"ctrl+a"}, []string{"shift+up", "shift+down"}},164+ {"extend-edge/dense-8192x256", uiShapes()[2], []string{"ctrl+shift+right"}, []string{"ctrl+shift+down", "ctrl+shift+up"}},158165 {"extend/dense-8192x26", uiShapes()[1], []string{"shift+down"}, []string{"shift+down", "shift+up"}},159166 {"type/fanin-1000", uiShape{"fanin", func() *sheet.Sheet { return stress.FanIn(sheet.MaxRows, 1000) }},160167 nil, []string{"7", "enter", "up"}},@@ -246,8 +253,8 @@ func (stressJEV) SystemOne(context.Context, typesafe.SystemOneRequest) (*typesaf
246253 }247254 248255 // BenchmarkJEV loads n JEV.TEST formulas with distinct questions and249-// drains every answer through Update, as the running program would,250-// counting frames drawn along the way at 60 per second.256+// drains every answer through Update, as the running program would.257+// Answers that are queued together (up to jevParallel) recalculate once.251258 func BenchmarkJEV(b *testing.B) {252259 for _, n := range []int{100, 1000, 4000} {253260 b.Run(fmt.Sprint(n), func(b *testing.B) {@@ -261,6 +268,7 @@ func BenchmarkJEV(b *testing.B) {
261268 s.RecalcAll()262269 m := sized(s, 200, 60)263270 m.EnableJEV(stressJEV{}, cache)271+ m.jev.gather = 0 // answers already queued still gather; don't wait a frame264272 start := time.Now()265273 drain(m, m.jev.send())266274 if in, q := cache.Busy(); in+q > 0 {