carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1package bakeoff
2
3import (
4 "fmt"
5 "net"
6 "net/http"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strconv"
11 "strings"
12 "syscall"
13 "time"
14)
15
16// Memory is an app's resident memory, from Linux's /proc, in kilobytes:
17// just after start, the peak over a load run, and a few seconds after.
18type Memory struct{ IdleKB, PeakKB, AfterKB int }
19
20// ParseProcStatus reads VmRSS (resident now) and VmHWM (peak resident)
21// from /proc/<pid>/status.
22func ParseProcStatus(status string) (rss, hwm int, err error) {
23 for _, line := range strings.Split(status, "\n") {
24 k, v, ok := strings.Cut(line, ":")
25 if !ok {
26 continue
27 }
28 f := strings.Fields(v)
29 if len(f) == 0 {
30 continue
31 }
32 n, _ := strconv.Atoi(f[0])
33 switch k {
34 case "VmRSS":
35 rss = n
36 case "VmHWM":
37 hwm = n
38 }
39 }
40 if rss == 0 {
41 return 0, 0, fmt.Errorf("no VmRSS in status")
42 }
43 return rss, hwm, nil
44}
45
46func procStatus(pid int) (int, int, error) {
47 b, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
48 if err != nil {
49 return 0, 0, err
50 }
51 return ParseProcStatus(string(b))
52}
53
54// MeasureMemory runs the app binary locally (on --listen, with a fresh
55// database), reads its memory at rest, drives it with 64 concurrent reads
56// and then 64 concurrent toggles for d each, and reads it again. The
57// working directory is the app's own (the binary's parent's parent, since
58// builds land in dist/).
59func MeasureMemory(bin string, extra []string, d time.Duration) (Memory, error) {
60 ln, err := net.Listen("tcp", "127.0.0.1:0")
61 if err != nil {
62 return Memory{}, err
63 }
64 addr := ln.Addr().String()
65 ln.Close()
66 tmp, err := os.MkdirTemp(os.Getenv("TMPDIR"), "bakeoff-mem-")
67 if err != nil {
68 return Memory{}, err
69 }
70 defer os.RemoveAll(tmp)
71 cmd := exec.Command(bin, append(extra, "--listen", addr, "--db", filepath.Join(tmp, "app.db"))...)
72 cmd.Dir = filepath.Dir(filepath.Dir(bin))
73 cmd.Stdout, cmd.Stderr = nil, nil
74 if err := cmd.Start(); err != nil {
75 return Memory{}, err
76 }
77 defer func() {
78 cmd.Process.Signal(syscall.SIGTERM)
79 done := make(chan error, 1)
80 go func() { done <- cmd.Wait() }()
81 select {
82 case <-done:
83 case <-time.After(10 * time.Second):
84 cmd.Process.Kill()
85 }
86 }()
87 base := "http://" + addr
88 up := false
89 for i := 0; i < 150 && !up; i++ {
90 if res, err := http.Get(base + "/healthz"); err == nil {
91 res.Body.Close()
92 up = res.StatusCode == 200
93 }
94 if !up {
95 time.Sleep(200 * time.Millisecond)
96 }
97 }
98 if !up {
99 return Memory{}, fmt.Errorf("%s did not answer /healthz on %s", bin, addr)
100 }
101 time.Sleep(2 * time.Second)
102 var m Memory
103 if m.IdleKB, _, err = procStatus(cmd.Process.Pid); err != nil {
104 return m, err
105 }
106 s, err := NewSession(base)
107 if err != nil {
108 return m, err
109 }
110 if _, err := Seed(s, 50); err != nil {
111 return m, err
112 }
113 for _, sc := range []string{"read", "toggle"} {
114 if _, err := RunLoad(s, sc, 64, d); err != nil {
115 return m, err
116 }
117 }
118 if _, m.PeakKB, err = procStatus(cmd.Process.Pid); err != nil {
119 return m, err
120 }
121 time.Sleep(5 * time.Second)
122 m.AfterKB, _, err = procStatus(cmd.Process.Pid)
123 return m, err
124}
125