- commit
- 8368e2a69d8af41aa9028f5e4dd7cc772f20ec6b
- parent
- 9dc8ad42b4dd0f31c49d216ce430f8930df7e164
- author
- Dami <dami.etoile@gmail.com>
| M | docs/ | 3 | ++- |
| M | docs/ | 8 | ++++---- |
| M | internal/ | 26 | +++++++++++++++++++++++++- |
| M | internal/ | 18 | ++++++++++++++++++ |
@@ -212,7 +212,8 @@ makes comes back to the UI goroutine as a message (`macroCallMsg`), is
212212 run on the model, and releases the script; the UI keeps taking calls213213 within one update for a few milliseconds, so a 1000-action replay costs214214 about 2 ms, and gives the screen back when the script pauses, so Esc can215-stop a runaway. The whole run is one undo step opened with215+stop a runaway. A script whose program is gone (it quit, or its SSH216+session dropped) stops after waiting 30 s for a call to be taken. The whole run is one undo step opened with216217 `Workbook.Begin`.217218 218219 Recording (`macrorec.go`) listens where actions happen: `runCommand` for@@ -28,7 +28,7 @@ it lags, and past a second it stalls.
2828 | Undo | One step of any size: undo costs what the edit cost | 100 steps that each rewrite a whole column: 217 MB held | | Whole-cell before-images, 264 B per cell per step |2929 | JEV | Up to about 1000 JEV cells: 0.3 ms of CPU per answer | 4000 JEV cells: 1.3 ms per answer, 5 s of CPU to answer them all | | Every answer recalculates every JEV cell (they're volatile) |3030 | Formula depth | 10,000 nested parentheses or IFs: under 5 ms | | No explicit limit; recursion grows the stack | Recursive parser and evaluator |31-| Macros | Replaying 1000 recorded actions: 2 ms, one undo step; a script's call to the sheet: about 1.3 us | | Scripts past 10 M Starlark steps: stopped, with the line | One message per call to the sheet, served in batches on the UI goroutine |31+| Macros | Replaying 1000 recorded actions: 2.2 ms, one undo step; a script's call to the sheet: about 1.4 us | | Scripts past 10 M Starlark steps: stopped, with the line | One message per call to the sheet, served in batches on the UI goroutine |3232 | Find, filter, sort, fill | Filter or sort 8191 rows: 1 to 32 ms; find over 213 k cells: 38 ms; fill 8192 rows: 4 ms | Replace all over 213 k cells: 280 ms | | Per-cell string conversion and regexp |3333 3434 ## Sheet size@@ -199,9 +199,9 @@ step whatever its size (`BenchmarkMacroReplay`, `BenchmarkMacroScript`,
199199 200200 | Benchmark | Time | Per action or call | Allocations |201201 |---|---|---|---|202-| Replay 1000 recorded actions, absolute references (entries, selects, extends, a command) | 2.05 ms | 2.0 us | 30 k, 1.6 MB |203-| The same with relative references (moves, formulas moved with the active cell) | 2.07 ms | 2.1 us | 31 k, 1.7 MB |204-| A script's loop: 5000 `set` and 5000 `get` calls | 13.2 ms | 1.3 us per call | 220 k, 7.8 MB |202+| Replay 1000 recorded actions, absolute references (entries, selects, extends, a command) | 2.16 ms | 2.2 us | 30 k, 1.6 MB |203+| The same with relative references (moves, formulas moved with the active cell) | 2.19 ms | 2.2 us | 31 k, 1.7 MB |204+| A script's loop: 5000 `set` and 5000 `get` calls | 13.7 ms | 1.4 us per call | 220 k, 7.8 MB |205205 206206 Reading a range walks its cells; a range larger than 4096 cells (whole207207 columns) is first trimmed to the used range, which costs a pass over the@@ -48,6 +48,8 @@ type macroRun struct {
4848 start time.Time4949 span telemetry.Span5050 print string // the last line the script printed51+52+ orphan *time.Timer // see do; used only on the script's goroutine5153 }5254 5355 type macroDone struct {@@ -93,7 +95,7 @@ func (m *Model) startMacro(mc sheet.Macro) tea.Cmd {
9395 r.end = m.book().Begin(sheet.Change{Label: "run macro " + mc.Name, Focus: m.selection(), Sheet: m.sheet})9496 r.run = macro.New(mc.Name, mc.Source, macro.Env{9597 Host: scriptHost{m},96- Do: func(fn func()) { r.calls <- fn; <-r.ack },98+ Do: r.do,9799 Print: func(s string) { r.print = s },98100 MaxSteps: macroMaxSteps,99101 })@@ -106,6 +108,28 @@ func (m *Model) startMacro(mc sheet.Macro) tea.Cmd {
106108 return r.wait()107109 }108110 111+// orphanAfter is how long a script waits for the UI to take a call before112+// deciding the program is gone (it quit, or its SSH session dropped) and113+// stopping, so its goroutine doesn't outlive the program. A live UI114+// always has a wait running, so it takes calls at once.115+var orphanAfter = 30 * time.Second116+117+// do hands fn to the UI goroutine and waits until it has run. It runs on118+// the script's goroutine.119+func (r *macroRun) do(fn func()) {120+ if r.orphan == nil {121+ r.orphan = time.NewTimer(orphanAfter)122+ } else {123+ r.orphan.Reset(orphanAfter)124+ }125+ select {126+ case r.calls <- fn:127+ <-r.ack128+ case <-r.orphan.C:129+ r.run.Cancel() // the call is dropped; the script stops at its next step130+ }131+}132+109133 // wait is the command that waits for the script's next call or its end.110134 func (r *macroRun) wait() tea.Cmd {111135 return func() tea.Msg {@@ -3,6 +3,7 @@ package ui
33 import (44 "strings"55 "testing"6+ "time"67 78 tea "charm.land/bubbletea/v2"89 @@ -11,6 +12,23 @@ import (
1112 1213 // Scripts run against the model: the API, errors, limits and Esc.1314 15+func TestAScriptOutlivesNoProgram(t *testing.T) {16+ defer func(d time.Duration) { orphanAfter = d }(orphanAfter)17+ orphanAfter = 20 * time.Millisecond18+ m := newModel()19+ m.book().SaveMacro("", sheet.Macro{Name: "Spin", Source: "while True:\n move(0, 0)\n"}, "add")20+ m.trustHere()21+ m.runMacro(mustMacro(t, m, "Spin")) // its wait is never run: the program is gone22+ select {23+ case d := <-m.macros.run.done:24+ if d.err == nil {25+ t.Error("finished without an error")26+ }27+ case <-time.After(5 * time.Second):28+ t.Fatal("the script's goroutine is still waiting")29+ }30+}31+1432 // script saves src as a trusted macro and runs it to the end.1533 func script(t *testing.T, m *Model, src string) {1634 t.Helper()