cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / serve / stress_test.go
stress_test.go 4.0K
1//go:build stress && unix23// Stress benchmark of 012 serve: many sessions at once, each over its own4// SSH connection on loopback, measuring the heap each session holds, the5// time from a key press leaving the client to the frame answering it6// arriving back while every session types at once, and the CPU spent per7// frame. The clients run in the same process, so heap and CPU include8// their side of the connections: upper bounds for the server. Only built9// with -tags stress (see `make stress` and docs/contributing/limits.md).10package serve1112import (13 "fmt"14 "io"15 "os"16 "path/filepath"17 "runtime"18 "runtime/pprof"19 "slices"20 "sync"21 "syscall"22 "testing"23 "time"2425 "src.rest/cat/012/internal/stress"26)2728func BenchmarkSessions(b *testing.B) {29 for _, n := range []int{10, 50} {30 for _, shape := range []string{"empty", "dense-1000x26", "dense-1000x26-shared"} {31 b.Run(fmt.Sprintf("sessions=%d/%s", n, shape), func(b *testing.B) { benchSessions(b, n, shape) })32 }33 }34}3536func benchSessions(b *testing.B, n int, shape string) {37 key := newKey(b)38 // A copy of the sheet each, but for the shared shape: one workbook39 // in one room, every key moving a pointer the others draw.40 share := "off"41 if shape == "dense-1000x26-shared" {42 share = "edit"43 }44 srv, addr, dir := testServer(b, func(o *Options) { o.MaxSessions, o.IdleTimeout, o.Share = n, 0, share }, key)45 if shape != "empty" {46 writeDense(b, filepath.Join(dir, "dense.012"))47 }48 before := liveHeap()49 terms := make([]*term, n)50 for i := range terms {51 c, err := dial(addr, key, srv.HostKey())52 if err != nil {53 b.Fatal(err)54 }55 b.Cleanup(func() { c.Close() })56 terms[i] = openTerm(b, c, 120, 40)57 terms[i].waitFor(b, "READY")58 if shape != "empty" {59 io.WriteString(terms[i].stdin, "\x0fdense\r") // Ctrl+O dense60 terms[i].waitFor(b, "dense.012")61 }62 }63 perSession := float64(liveHeap()-before) / float64(n) / 102464 if path := os.Getenv("SERVE_HEAP_PROFILE"); path != "" {65 // What the sessions hold, while they're open.66 f, err := os.Create(path)67 if err != nil {68 b.Fatal(err)69 }70 pprof.WriteHeapProfile(f)71 f.Close()72 }7374 // Every session presses a key at once, b.N times; each waits for its75 // frame.76 keys := []string{"\x1b[B", "\x1b[A"} // down, up77 var mu sync.Mutex78 var lat []time.Duration79 cpu := cpuTime()80 b.ResetTimer()81 for i := 0; i < b.N; i++ {82 var wg sync.WaitGroup83 for _, tm := range terms {84 wg.Go(func() {85 d := tm.press(b, keys[i%2])86 mu.Lock()87 lat = append(lat, d)88 mu.Unlock()89 })90 }91 wg.Wait()92 }93 b.StopTimer()94 b.ReportMetric(float64((cpuTime()-cpu).Microseconds())/float64(b.N*n), "cpu_us/frame")95 b.ReportMetric(perSession, "KiB/session")96 slices.Sort(lat)97 b.ReportMetric(ms(lat[len(lat)/2]), "p50_ms")98 b.ReportMetric(ms(lat[len(lat)*95/100]), "p95_ms")99 b.ReportMetric(ms(lat[len(lat)-1]), "max_ms")100}101102// press sends k and returns how long the first bytes of the answering103// frame took to arrive.104func (tm *term) press(b *testing.B, k string) time.Duration {105 tm.mu.Lock()106 had := tm.out.Len()107 tm.mu.Unlock()108 select { // forget output that arrived before the key109 case <-tm.wrote:110 default:111 }112 start := time.Now()113 io.WriteString(tm.stdin, k)114 timeout := time.After(10 * time.Second)115 for {116 select {117 case <-tm.wrote:118 tm.mu.Lock()119 grew := tm.out.Len() > had120 tm.mu.Unlock()121 if grew {122 return time.Since(start)123 }124 case <-timeout:125 b.Fatal("no frame after a key press")126 }127 }128}129130func writeDense(b *testing.B, path string) {131 f, err := os.Create(path)132 if err != nil {133 b.Fatal(err)134 }135 defer f.Close()136 if err := stress.Dense(1000, 26).Write(f); err != nil {137 b.Fatal(err)138 }139}140141// cpuTime is the CPU time the process has used, user and system.142func cpuTime() time.Duration {143 var ru syscall.Rusage144 syscall.Getrusage(syscall.RUSAGE_SELF, &ru)145 return time.Duration(ru.Utime.Nano() + ru.Stime.Nano())146}147148// liveHeap is the heap in use after a full collection.149func liveHeap() int64 {150 runtime.GC()151 runtime.GC()152 var m runtime.MemStats153 runtime.ReadMemStats(&m)154 return int64(m.HeapAlloc)155}156157func ms(d time.Duration) float64 { return float64(d.Microseconds()) / 1000 }