An e2e test appends to a CSV file while 012 follows it: new rows, a partial line waiting, a formula recalculating, truncation, pausing and unlinking. Golden screens show a followed log with its tab mark and a formula over it, the question of how many rows to keep and a linked file that isn't there, dark and light. BenchmarkFollow appends 10,000 rows a second for three seconds and times each update applied and each frame. An error op no longer counts as an update of the region, so its time shows when rows last arrived; the state is shown without monochrome color too.
- commit
- 766b432fe975b2bc5e417013efe3e7190e9518c2
- parent
- e29c1ef868d2cc17b091edd1a8efcd4edbb9dcd0
- author
- Dami <dami.etoile@gmail.com>
- kind
- test risk score 1.7 of 3, Jev's estimate
| A | e2e/ | 83 | ++++++++++++++++++ |
| A | e2e/ | 84 | ++++++++++++++++++ |
| A | e2e/ | 30 | +++++++ |
| A | e2e/ | 30 | +++++++ |
| A | e2e/ | 30 | +++++++ |
| A | e2e/ | 30 | +++++++ |
| A | e2e/ | 30 | +++++++ |
| M | internal/ | 8 | +- |
| M | internal/ | 73 | +++++++++------- |
| M | internal/ | 12 | ++- |
| A | internal/ | 138 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 6 | +- |
| M | internal/ | 27 | ++++++ |
@@ -0,0 +1,83 @@
1+package e2e2+3+import (4+ "fmt"5+ "os"6+ "path/filepath"7+ "strings"8+ "testing"9+)10+11+// appendText adds text to the end of a file, as a program logging to it12+// does.13+func appendText(t *testing.T, name, text string) {14+ t.Helper()15+ f, err := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0)16+ if err != nil {17+ t.Fatal(err)18+ }19+ defer f.Close()20+ if _, err := f.WriteString(text); err != nil {21+ t.Fatal(err)22+ }23+}24+25+// linkTable links name at the active cell through the palette, keeping26+// every row.27+func (s *session) linkTable(name string) {28+ s.t.Helper()29+ s.linkTableAsk(name)30+ s.keys("<enter>")31+}32+33+func TestFollowFileAsItGrows(t *testing.T) {34+ dir := t.TempDir()35+ log := filepath.Join(dir, "app.csv")36+ writeText(t, log, "level,ms\ninfo,12\n")37+ s := start(t, dir)38+ s.keys("<f5>", "D1", "<enter>", "=SUM(B:B)", "<enter>")39+ s.keys("<f5>", "A1", "<enter>")40+ s.linkTable("app.csv")41+ s.waitFor("info")42+ s.waitFor("Sheet1 ●")43+44+ // Rows appended while 012 follows the file come in, a partial line45+ // once it's whole, and the formula over them recalculates.46+ for i := range 5 {47+ appendText(t, log, fmt.Sprintf("warn,%d\n", 100+i))48+ }49+ appendText(t, log, "error,")50+ s.waitFor("104")51+ s.eventually("the sum of the rows", func() bool { return strings.Contains(s.line(4), "522") })52+ if strings.Contains(s.screen(), "error") {53+ t.Fatalf("a partial line was read:\n%s", s.screen())54+ }55+ appendText(t, log, "7\n")56+ s.waitFor("error")57+ s.eventually("the sum with the last row", func() bool { return strings.Contains(s.line(4), "529") })58+59+ // The context line says what the region is doing.60+ s.keys("<f5>", "A3", "<enter>")61+ s.waitFor("● Following app.csv 7 rows")62+63+ // Truncated and written again, it's read from the start.64+ writeText(t, log, "level,ms\nreset,1\n")65+ s.waitFor("reset")66+ s.eventually("the old rows gone", func() bool { return !strings.Contains(s.screen(), "104") })67+68+ // Paused, it waits; unlinked, the values stay as they are.69+ s.keys("<ctrl+k>", "Follow", "<enter>")70+ s.waitFor("Paused following app.csv")71+ appendText(t, log, "late,2\n")72+ s.keys("<up>") // A2, in the region73+ s.waitFor("‖ Paused app.csv")74+ if strings.Contains(s.screen(), "late") {75+ t.Fatalf("a paused region read on:\n%s", s.screen())76+ }77+ s.keys("<ctrl+k>", "Unlink", "<enter>")78+ s.waitFor("Unlinked app.csv")79+ s.waitFor("Sheet1 ")80+ if strings.Contains(s.line(29), "●") {81+ t.Errorf("tab still marked: %q", s.line(29))82+ }83+}@@ -0,0 +1,84 @@
1+package e2e2+3+import (4+ "path/filepath"5+ "testing"6+)7+8+// Golden screens for following files: a log followed in its own sheet,9+// the tab's live mark and a formula over the rows, the question of how10+// many rows to keep, and a linked file that isn't there; the log and the11+// missing file on a light terminal too.12+13+// appLog is a web server's log as a CSV file.14+const appLog = `time,level,path,ms15+09:14:02,info,/,1216+09:14:03,info,/login,4817+09:14:05,warn,/search,31018+09:14:07,info,/cart,2219+09:14:08,error,/pay,120420+09:14:10,info,/,921+09:14:11,info,/orders,5722+09:14:13,warn,/search,28823+09:14:14,info,/login,4124+09:14:16,info,/cart,1925+09:14:17,info,/,1126+`27+28+func writeAppLog(t *testing.T, dir string) { writeText(t, filepath.Join(dir, "app.csv"), appLog) }29+30+// followLog follows app.csv in a new sheet, keeping its last 8 rows, and31+// adds up their times beside it.32+func followLog(s *session) {33+ s.keys("Requests", "<enter>")34+ s.keys("<ctrl+k>", "Import", "<enter>")35+ s.waitFor("Type to filter, or a path")36+ s.keys("app.csv", "<enter>")37+ s.waitFor("Follow the file")38+ s.keys("follow", "<enter>")39+ s.waitFor("Follow app.csv:")40+ s.keys("l")41+ s.waitFor("Keep the last how many rows:")42+ s.keys("8", "<enter>")43+ s.waitFor("/orders")44+ s.keys("<f5>", "F1", "<enter>", "Slowest", "<tab>", "=MAX(D:D)", "<enter>")45+ s.keys("<f5>", "F2", "<enter>", "Errors", "<tab>", `=COUNTIF(B:B,"error")`, "<enter>")46+ s.waitFor("1204")47+}48+49+var followScreens = []screen{50+ {name: "follow-log", files: writeAppLog, setup: followLog},51+ {name: "follow-ask", files: writeAppLog, setup: func(s *session) { s.linkTableAsk("app.csv") }},52+ {name: "follow-missing", files: func(t *testing.T, dir string) {53+ writeText(t, filepath.Join(dir, "gone.012"),54+ `{"version": 2, "cells": {"D1": "Nightly run"}, "links": [{"at": "A1", "path": "nightly.csv"}]}`)55+ }, setup: func(s *session) {56+ s.keys("<ctrl+o>", "gone", "<enter>")57+ s.waitFor("#REF!")58+ s.waitFor("! nightly.csv: the file isn't there")59+ }},60+}61+62+// linkTableAsk links name at the active cell through the palette, up to63+// the question of how many rows to keep.64+func (s *session) linkTableAsk(name string) {65+ s.t.Helper()66+ s.keys("<ctrl+k>", "Link a table")67+ s.waitFor("Link a table")68+ s.keys("<enter>")69+ s.waitFor("Type to filter, or a path")70+ s.keys(name, "<enter>")71+ s.waitFor("Follow " + name + ":")72+}73+74+func init() {75+ for _, sc := range followScreens {76+ screens = append(screens, sc)77+ if sc.name != "follow-ask" {78+ light := sc79+ light.name += "-light"80+ light.opts.light = true81+ screens = append(screens, light)82+ }83+ }84+}@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A1</span><span style="background:var(--vt-palette-8)"> </span>3+Follow app.csv: <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Enter</span><span style="background:var(--vt-palette-8)"> </span> Keep every row <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">L</span><span style="background:var(--vt-palette-8)"> </span> Keep the last rows <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> Cancel4+<span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span>6+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span>7+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span>15+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span>16+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span>18+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>19+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>20+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">21</span><span style="background:var(--vt-palette-8)"> </span>26+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">22</span><span style="background:var(--vt-palette-8)"> </span>27+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">23</span><span style="background:var(--vt-palette-8)"> </span>28+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">24</span><span style="background:var(--vt-palette-8)"> </span>29+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">25</span><span style="background:var(--vt-palette-8)"> </span>30+Keep every row up to max-cells, or only the newest, older ones dropping as new ones arrive</pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F3</span><span style="background:var(--vt-palette-7)"> </span>3+4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">F</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">G</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">H</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">I</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-4);font-style:italic">time</span> <span style="color:var(--vt-palette-4);font-style:italic">level</span> <span style="color:var(--vt-palette-4);font-style:italic">path</span> <span style="color:var(--vt-palette-4);font-style:italic">ms</span> Slowest 12046+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-4);font-style:italic">9:14:07</span> <span style="color:var(--vt-palette-4);font-style:italic">info</span> <span style="color:var(--vt-palette-4);font-style:italic">/cart</span> <span style="color:var(--vt-palette-4);font-style:italic">22</span> Errors 17+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">3</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-4);font-style:italic">9:14:08</span> <span style="color:var(--vt-palette-4);font-style:italic">error</span> <span style="color:var(--vt-palette-4);font-style:italic">/pay</span> <span style="color:var(--vt-palette-4);font-style:italic">1204</span> <span style="background:var(--vt-palette-6)"> </span>8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-4);font-style:italic">9:14:10</span> <span style="color:var(--vt-palette-4);font-style:italic">info</span> <span style="color:var(--vt-palette-4);font-style:italic">/</span> <span style="color:var(--vt-palette-4);font-style:italic">9</span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-4);font-style:italic">9:14:11</span> <span style="color:var(--vt-palette-4);font-style:italic">info</span> <span style="color:var(--vt-palette-4);font-style:italic">/orders</span> <span style="color:var(--vt-palette-4);font-style:italic">57</span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-4);font-style:italic">9:14:13</span> <span style="color:var(--vt-palette-4);font-style:italic">warn</span> <span style="color:var(--vt-palette-4);font-style:italic">/search</span> <span style="color:var(--vt-palette-4);font-style:italic">288</span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-4);font-style:italic">9:14:14</span> <span style="color:var(--vt-palette-4);font-style:italic">info</span> <span style="color:var(--vt-palette-4);font-style:italic">/login</span> <span style="color:var(--vt-palette-4);font-style:italic">41</span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-4);font-style:italic">9:14:16</span> <span style="color:var(--vt-palette-4);font-style:italic">info</span> <span style="color:var(--vt-palette-4);font-style:italic">/cart</span> <span style="color:var(--vt-palette-4);font-style:italic">19</span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-4);font-style:italic">9:14:17</span> <span style="color:var(--vt-palette-4);font-style:italic">info</span> <span style="color:var(--vt-palette-4);font-style:italic">/</span> <span style="color:var(--vt-palette-4);font-style:italic">11</span>14+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">10</span><span style="background:var(--vt-palette-7)"> </span>15+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">11</span><span style="background:var(--vt-palette-7)"> </span>16+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">12</span><span style="background:var(--vt-palette-7)"> </span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">13</span><span style="background:var(--vt-palette-7)"> </span>18+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">14</span><span style="background:var(--vt-palette-7)"> </span>19+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">15</span><span style="background:var(--vt-palette-7)"> </span>20+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">16</span><span style="background:var(--vt-palette-7)"> </span>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17</span><span style="background:var(--vt-palette-7)"> </span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">18</span><span style="background:var(--vt-palette-7)"> </span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">19</span><span style="background:var(--vt-palette-7)"> </span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">20</span><span style="background:var(--vt-palette-7)"> </span>25+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">21</span><span style="background:var(--vt-palette-7)"> </span>26+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">22</span><span style="background:var(--vt-palette-7)"> </span>27+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">23</span><span style="background:var(--vt-palette-7)"> </span>28+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">24</span><span style="background:var(--vt-palette-7)"> </span>29+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">25</span><span style="background:var(--vt-palette-7)"> </span>30+ Sheet1 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">app</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">●</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">menu</span></pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F3</span><span style="background:var(--vt-palette-8)"> </span>3+4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">F</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-6);font-style:italic">time</span> <span style="color:var(--vt-palette-6);font-style:italic">level</span> <span style="color:var(--vt-palette-6);font-style:italic">path</span> <span style="color:var(--vt-palette-6);font-style:italic">ms</span> Slowest 12046+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-6);font-style:italic">9:14:07</span> <span style="color:var(--vt-palette-6);font-style:italic">info</span> <span style="color:var(--vt-palette-6);font-style:italic">/cart</span> <span style="color:var(--vt-palette-6);font-style:italic">22</span> Errors 17+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">3</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-6);font-style:italic">9:14:08</span> <span style="color:var(--vt-palette-6);font-style:italic">error</span> <span style="color:var(--vt-palette-6);font-style:italic">/pay</span> <span style="color:var(--vt-palette-6);font-style:italic">1204</span> <span style="background:var(--vt-palette-6)"> </span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-6);font-style:italic">9:14:10</span> <span style="color:var(--vt-palette-6);font-style:italic">info</span> <span style="color:var(--vt-palette-6);font-style:italic">/</span> <span style="color:var(--vt-palette-6);font-style:italic">9</span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-6);font-style:italic">9:14:11</span> <span style="color:var(--vt-palette-6);font-style:italic">info</span> <span style="color:var(--vt-palette-6);font-style:italic">/orders</span> <span style="color:var(--vt-palette-6);font-style:italic">57</span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-6);font-style:italic">9:14:13</span> <span style="color:var(--vt-palette-6);font-style:italic">warn</span> <span style="color:var(--vt-palette-6);font-style:italic">/search</span> <span style="color:var(--vt-palette-6);font-style:italic">288</span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-6);font-style:italic">9:14:14</span> <span style="color:var(--vt-palette-6);font-style:italic">info</span> <span style="color:var(--vt-palette-6);font-style:italic">/login</span> <span style="color:var(--vt-palette-6);font-style:italic">41</span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-6);font-style:italic">9:14:16</span> <span style="color:var(--vt-palette-6);font-style:italic">info</span> <span style="color:var(--vt-palette-6);font-style:italic">/cart</span> <span style="color:var(--vt-palette-6);font-style:italic">19</span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-6);font-style:italic">9:14:17</span> <span style="color:var(--vt-palette-6);font-style:italic">info</span> <span style="color:var(--vt-palette-6);font-style:italic">/</span> <span style="color:var(--vt-palette-6);font-style:italic">11</span>14+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span>15+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span>16+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span>18+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>19+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>20+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">21</span><span style="background:var(--vt-palette-8)"> </span>26+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">22</span><span style="background:var(--vt-palette-8)"> </span>27+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">23</span><span style="background:var(--vt-palette-8)"> </span>28+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">24</span><span style="background:var(--vt-palette-8)"> </span>29+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">25</span><span style="background:var(--vt-palette-8)"> </span>30+ Sheet1 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">app</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">●</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">menu</span></pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A1</span><span style="background:var(--vt-palette-7)"> </span> #REF!3+<span style="color:var(--vt-palette-3)">!</span> <span style="color:var(--vt-palette-3)">nightly.csv:</span> <span style="color:var(--vt-palette-3)">the</span> <span style="color:var(--vt-palette-3)">file</span> <span style="color:var(--vt-palette-3)">isn't</span> <span style="color:var(--vt-palette-3)">there</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">G</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">H</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">I</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);text-decoration:underline wavy var(--vt-palette-1)">#REF!</span><span style="background:var(--vt-palette-6)"> </span> Nightly run6+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span>7+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span>8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span>14+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">10</span><span style="background:var(--vt-palette-7)"> </span>15+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">11</span><span style="background:var(--vt-palette-7)"> </span>16+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">12</span><span style="background:var(--vt-palette-7)"> </span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">13</span><span style="background:var(--vt-palette-7)"> </span>18+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">14</span><span style="background:var(--vt-palette-7)"> </span>19+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">15</span><span style="background:var(--vt-palette-7)"> </span>20+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">16</span><span style="background:var(--vt-palette-7)"> </span>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17</span><span style="background:var(--vt-palette-7)"> </span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">18</span><span style="background:var(--vt-palette-7)"> </span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">19</span><span style="background:var(--vt-palette-7)"> </span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">20</span><span style="background:var(--vt-palette-7)"> </span>25+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">21</span><span style="background:var(--vt-palette-7)"> </span>26+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">22</span><span style="background:var(--vt-palette-7)"> </span>27+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">23</span><span style="background:var(--vt-palette-7)"> </span>28+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">24</span><span style="background:var(--vt-palette-7)"> </span>29+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">25</span><span style="background:var(--vt-palette-7)"> </span>30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">!</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> gone.012 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">menu</span></pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A1</span><span style="background:var(--vt-palette-8)"> </span> #REF!3+<span style="color:var(--vt-palette-3)">!</span> <span style="color:var(--vt-palette-3)">nightly.csv:</span> <span style="color:var(--vt-palette-3)">the</span> <span style="color:var(--vt-palette-3)">file</span> <span style="color:var(--vt-palette-3)">isn't</span> <span style="color:var(--vt-palette-3)">there</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);text-decoration:underline wavy var(--vt-palette-1)">#REF!</span><span style="background:var(--vt-palette-6)"> </span> Nightly run6+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span>7+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span>15+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span>16+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span>18+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>19+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>20+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">21</span><span style="background:var(--vt-palette-8)"> </span>26+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">22</span><span style="background:var(--vt-palette-8)"> </span>27+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">23</span><span style="background:var(--vt-palette-8)"> </span>28+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">24</span><span style="background:var(--vt-palette-8)"> </span>29+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">25</span><span style="background:var(--vt-palette-8)"> </span>30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">!</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> gone.012 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">menu</span></pre>\ No newline at end of file@@ -54,7 +54,7 @@ type LinkedRegion struct {
5454 // Rows counts the data rows it shows, under the header; Dropped the5555 // older rows the window let go.5656 Rows, Dropped int57- // Updated is when the last rows arrived, zero until they do.57+ // Updated is when the source was last read, zero until it is.5858 Updated time.Time5959 // Err says why the source can't be read, or the rows can't be6060 // written, "" when all is well; Note says what was left out.@@ -220,13 +220,13 @@ func (w *Workbook) SetLinkSource(id int, src LinkSource) error {
220220 return nil221221 }222222 223-// ErrNoLinked is returned for a region that no longer exists: unlinked,224-// or on a sheet deleted.223+// ErrNoLinked is returned for a region the workbook doesn't hold:224+// unlinked, undone, or on a sheet deleted.225225 var ErrNoLinked = errors.New("That linked file is no longer linked")226226 227227 // Unlink turns a region into the values it shows, as one undo step:228228 // they stay as ordinary cells, keeping their formatting and notes, and229-// the source is no longer read.229+// nothing reads the source for them.230230 func (w *Workbook) Unlink(id int) error {231231 s, l := w.findLinked(id)232232 if l == nil {@@ -63,7 +63,8 @@ var OnLive func(trace any, i LiveInfo)
6363 6464 // ApplyLive applies op to its region: its rows are written, the window6565 // drops the oldest, and what reads the cells that changed recalculates.66-// It returns the region as it now is; ErrNoLinked when it's gone.66+// It returns the region as the op leaves it, or ErrNoLinked for a67+// region the workbook doesn't hold.6768 func (w *Workbook) ApplyLive(op LiveOp) (LinkedRegion, error) {6869 s, l := w.findLinked(op.Link)6970 if l == nil {@@ -92,7 +93,7 @@ func (w *Workbook) ApplyLive(op LiveOp) (LinkedRegion, error) {
9293 9394 // applyLive writes op's rows into l and returns the cells that changed.9495 func (s *Sheet) applyLive(l *linked, op LiveOp) []loc {95- if !op.At.IsZero() {96+ if !op.At.IsZero() && op.Err == "" {9697 l.updated = op.At9798 }9899 if op.Reset {@@ -183,17 +184,7 @@ func (s *Sheet) readLinkedRow(l *linked, r int) LiveRow {
183184 // that changed. When a cell it needs holds something of its own, it184185 // writes nothing and says so.185186 func (s *Sheet) writeLinkedRows(l *linked, header LiveRow, rows []LiveRow, reset bool) []loc {186- cols := len(header)187- for _, r := range rows {188- cols = max(cols, len(r))189- }190- if !reset {191- cols = max(cols, l.cols)192- }193- nrows := 0194- if cols > 0 {195- nrows = 1 + len(rows)196- }187+ cols, nrows := linkedSize(l, header, rows, reset)197188 area := Rect{From: l.anchor, To: Addr{Col: l.anchor.Col + max(cols, 1) - 1, Row: l.anchor.Row + max(nrows, 1) - 1}}198189 if at, ok := s.linkedBlocked(l, area); ok {199190 l.err = "The linked rows would overwrite data in " + at.String()@@ -207,29 +198,14 @@ func (s *Sheet) writeLinkedRows(l *linked, header LiveRow, rows []LiveRow, reset
207198 keep = noRect208199 }209200 changed := s.clearSpilled(l.area(), keep)210- write := func(r int, row LiveRow, all bool) {211- for c := range cols {212- var lc LiveCell213- if c < len(row) {214- lc = row[c]215- }216- if lc.V.Kind == Empty && !all {217- continue218- }219- a := Addr{Col: l.anchor.Col + c, Row: l.anchor.Row + r}220- if s.writeSpilled(a, lc.V, lc.F) {221- changed = append(changed, loc{s, a})222- }223- }224- }225201 if cols > 0 {226- write(0, header, true)202+ changed = s.writeLinkedRow(l, 0, header, cols, true, changed)227203 }228204 for i, row := range rows {229205 // A row written over one that showed clears what it doesn't230206 // fill; one below the old region has nothing to clear.231207 if row != nil {232- write(i+1, row, i+1 < l.rows || reset)208+ changed = s.writeLinkedRow(l, i+1, row, cols, i+1 < l.rows || reset, changed)233209 }234210 }235211 l.cols, l.rows = cols, nrows@@ -243,6 +219,43 @@ func (s *Sheet) writeLinkedRows(l *linked, header LiveRow, rows []LiveRow, reset
243219 return changed244220 }245221 222+// linkedSize is how many columns and rows the region takes to show223+// header and rows: none when there's nothing to show.224+func linkedSize(l *linked, header LiveRow, rows []LiveRow, reset bool) (cols, nrows int) {225+ cols = len(header)226+ for _, r := range rows {227+ cols = max(cols, len(r))228+ }229+ if !reset {230+ cols = max(cols, l.cols)231+ }232+ if cols > 0 {233+ nrows = 1 + len(rows)234+ }235+ return cols, nrows236+}237+238+// writeLinkedRow writes row r of the region (0 for the header), cols239+// cells wide, adding the cells that changed to changed. With all, the240+// cells the row leaves blank are cleared; otherwise they're blank241+// already.242+func (s *Sheet) writeLinkedRow(l *linked, r int, row LiveRow, cols int, all bool, changed []loc) []loc {243+ for c := range cols {244+ var lc LiveCell245+ if c < len(row) {246+ lc = row[c]247+ }248+ if lc.V.Kind == Empty && !all {249+ continue250+ }251+ a := Addr{Col: l.anchor.Col + c, Row: l.anchor.Row + r}252+ if s.writeSpilled(a, lc.V, lc.F) {253+ changed = append(changed, loc{s, a})254+ }255+ }256+ return changed257+}258+246259 // linkedBlocked returns a cell of area the region can't write: one247260 // holding something that isn't the region's, or past the sheet's edge.248261 func (s *Sheet) linkedBlocked(l *linked, area Rect) (Addr, bool) {@@ -40,8 +40,9 @@ type follower struct {
4040 src live.Source4141 // busy is set while a poll runs, waiting while one is scheduled;4242 // gone once the region is gone, closing the source when its poll43- // returns; reload asks the next poll to read the source whole.44- busy, waiting, gone, reload bool43+ // returns. reload is set from asking the source to read whole until44+ // it has, and pending when that was asked while a poll ran.45+ busy, waiting, gone, reload, pending bool4546 }4647 4748 // followInterval is how often a region's source is polled; tests lower@@ -108,7 +109,9 @@ func (m *Model) syncFollower(r sheet.LinkedRegion) tea.Cmd {
108109 }109110 if r.Stale && !f.reload {110111 f.reload = true111- if !f.busy {112+ if f.busy {113+ f.pending = true114+ } else {112115 f.src.Reload()113116 }114117 }@@ -188,7 +191,8 @@ func (m *Model) handleFollow(msg tea.Msg) tea.Cmd {
188191 case f.gone || m.follow.by[f.id] != f:189192 f.src.Close()190193 return nil191- case f.reload && !msg.u.Reset:194+ case f.pending:195+ f.pending = false192196 f.src.Reload() // asked while it polled: read it whole now193197 return pollFollower(f)194198 }@@ -0,0 +1,138 @@
1+//go:build stress2+3+package ui4+5+import (6+ "context"7+ "fmt"8+ "os"9+ "path/filepath"10+ "slices"11+ "testing"12+ "time"13+14+ "github.com/FelineStateMachine/012/internal/fileio"15+ "github.com/FelineStateMachine/012/internal/live"16+ "github.com/FelineStateMachine/012/internal/sheet"17+)18+19+// BenchmarkFollow follows a CSV log while another goroutine appends20+// 10,000 rows a second to it for three seconds, polling as the UI does21+// (every live.Interval, at once while more is waiting), applying each22+// update to the workbook, with formulas over the region, and drawing a23+// frame after each. It reports the slowest and the 99th percentile of24+// an update applied (writing the cells and recalculating what reads25+// them) and of a frame, and the rows a second that came in: an update26+// must stay a small part of a frame's budget, however fast the file27+// grows. The window cases keep the last rows, rewriting the region on28+// every update; "all" keeps every row.29+func BenchmarkFollow(b *testing.B) {30+ for _, window := range []int{0, 1000, 10000} {31+ name := "all"32+ if window > 0 {33+ name = fmt.Sprintf("window-%d", window)34+ }35+ b.Run(name, func(b *testing.B) {36+ for b.Loop() {37+ r := followRun(b, window)38+ b.ReportMetric(r.applyMax, "apply-max-ms")39+ b.ReportMetric(r.applyP99, "apply-p99-ms")40+ b.ReportMetric(r.frameMax, "frame-max-ms")41+ b.ReportMetric(r.frameP99, "frame-p99-ms")42+ b.ReportMetric(r.rate, "rows/s")43+ }44+ })45+ }46+}47+48+type followResult struct{ applyMax, applyP99, frameMax, frameP99, rate float64 }49+50+const (51+ followRate = 10000 // rows appended a second52+ followSeconds = 353+ followBatchGap = 10 * time.Millisecond54+)55+56+func followRun(b *testing.B, window int) followResult {57+ b.StopTimer()58+ path := filepath.Join(b.TempDir(), "access.csv")59+ os.WriteFile(path, []byte("time,level,path,ms\n"), 0o644)60+ m := sized(sheet.New(), 200, 60)61+ id, err := m.sheet.AddLinked(sheet.Addr{}, sheet.LinkSource{Path: "access.csv", Window: window})62+ if err != nil {63+ b.Fatal(err)64+ }65+ m.sheet.Set(sheet.Addr{Col: 6}, "=SUM(D:D)")66+ m.sheet.Set(sheet.Addr{Col: 6, Row: 1}, `=COUNTIF(B:B,"error")`)67+ m.sheet.Set(sheet.Addr{Col: 6, Row: 2}, "=AVERAGE(D2:D100000)")68+ src := live.NewFile("access.csv", path, fileio.CSV, fileio.Options{})69+ defer src.Close()70+ t := newFakeTerm(200, 60)71+ done := make(chan int)72+ go appendLog(path, done)73+ b.StartTimer()74+75+ var applies, frames []float6476+ start, total := time.Now(), 077+ writing := true78+ for {79+ select {80+ case total = <-done:81+ writing = false82+ default:83+ }84+ u, ok := src.Poll(context.Background())85+ if ok {86+ at := time.Now()87+ if _, err := m.book().ApplyLive(u.Op(id)); err != nil {88+ b.Fatal(err)89+ }90+ applies = append(applies, ms(time.Since(at)))91+ at = time.Now()92+ t.frame(m)93+ frames = append(frames, ms(time.Since(at)))94+ }95+ switch {96+ case u.More:97+ case writing:98+ time.Sleep(live.Interval)99+ default:100+ if r, _ := m.book().LinkedRegion(id); r.Rows+r.Dropped < total {101+ continue102+ }103+ return followResult{applyMax: slices.Max(applies), applyP99: p99(applies), frameMax: slices.Max(frames),104+ frameP99: p99(frames), rate: float64(total) / time.Since(start).Seconds()}105+ }106+ }107+}108+109+// appendLog appends followRate rows a second to path, in batches, for110+// followSeconds, then sends how many it wrote.111+func appendLog(path string, done chan<- int) {112+ f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)113+ if err != nil {114+ panic(err)115+ }116+ defer f.Close()117+ levels := []string{"info", "info", "info", "warn", "error"}118+ per := followRate * int(followBatchGap) / int(time.Second)119+ n := 0120+ for range followSeconds * int(time.Second/followBatchGap) {121+ var batch []byte122+ for range per {123+ batch = fmt.Appendf(batch, "%d,%s,/api/%d,%d\n", n, levels[n%len(levels)], n%97, n%1300)124+ n++125+ }126+ f.Write(batch)127+ time.Sleep(followBatchGap)128+ }129+ done <- n130+}131+132+func ms(d time.Duration) float64 { return float64(d) / float64(time.Millisecond) }133+134+func p99(xs []float64) float64 {135+ s := slices.Clone(xs)136+ slices.Sort(s)137+ return s[min(len(s)-1, len(s)*99/100)]138+}@@ -271,7 +271,11 @@ func (m *Model) linkedLine() string {
271271 }272272 name := filepath.Base(r.Source.Path)273273 if r.Err != "" {274- return m.th.Warning.Render("! "+name+": "+r.Err) + m.th.Muted.Render(linkedCounts(r))274+ line := m.th.Warning.Render("! " + name + ": " + r.Err)275+ if r.Rows > 0 {276+ line += m.th.Muted.Render(linkedCounts(r))277+ }278+ return line275279 }276280 state := liveMark(r) + " Following "277281 if r.Paused {@@ -233,6 +233,33 @@ func TestMonochromeSpill(t *testing.T) {
233233 }234234 }235235 236+func TestMonochromeLinked(t *testing.T) {237+ m := newModel()238+ id, _ := m.sheet.AddLinked(addr("B1"), sheet.LinkSource{Path: "log.csv"})239+ m.book().ApplyLive(sheet.LiveOp{Link: id, Reset: true, Header: sheet.LiveRow{{V: sheet.Value{Kind: sheet.Text, Str: "n"}}},240+ Rows: []sheet.LiveRow{{{V: sheet.Value{Kind: sheet.Number, Num: 2}}}}})241+ m.cur = addr("D1")242+ c := cellAt(m, addr("B2"))243+ if strings.TrimSpace(cellText(c)) != "2" || !every(c, true, func(c monoCell) bool { return c.italic }) {244+ t.Errorf("a linked value isn't italic: %+v", c)245+ }246+ if l := cellText(monoLine(m, m.height-1)); !strings.Contains(l, "Sheet1 ●") {247+ t.Errorf("status line: %q", l)248+ }249+ m.cur = addr("B2")250+ if l := cellText(monoLine(m, contextLine)); !strings.Contains(l, "● Following log.csv") {251+ t.Errorf("context line: %q", l)252+ }253+ m.book().PauseLinked(id, true)254+ if l := cellText(monoLine(m, contextLine)); !strings.Contains(l, "‖ Paused log.csv") {255+ t.Errorf("context line: %q", l)256+ }257+ m.book().ApplyLive(sheet.LiveOp{Link: id, Err: "the file isn't there"})258+ if l := cellText(monoLine(m, contextLine)); !strings.Contains(l, "! log.csv: the file isn't there") {259+ t.Errorf("context line: %q", l)260+ }261+}262+236263 func TestMonochromeNote(t *testing.T) {237264 m := newModel()238265 m.sheet.Set(addr("B1"), "x")