package bakeoff import ( "fmt" "net" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" ) // Memory is an app's resident memory, from Linux's /proc, in kilobytes: // just after start, the peak over a load run, and a few seconds after. type Memory struct{ IdleKB, PeakKB, AfterKB int } // ParseProcStatus reads VmRSS (resident now) and VmHWM (peak resident) // from /proc//status. func ParseProcStatus(status string) (rss, hwm int, err error) { for _, line := range strings.Split(status, "\n") { k, v, ok := strings.Cut(line, ":") if !ok { continue } f := strings.Fields(v) if len(f) == 0 { continue } n, _ := strconv.Atoi(f[0]) switch k { case "VmRSS": rss = n case "VmHWM": hwm = n } } if rss == 0 { return 0, 0, fmt.Errorf("no VmRSS in status") } return rss, hwm, nil } func procStatus(pid int) (int, int, error) { b, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) if err != nil { return 0, 0, err } return ParseProcStatus(string(b)) } // MeasureMemory runs the app binary locally (on --listen, with a fresh // database), reads its memory at rest, drives it with 64 concurrent reads // and then 64 concurrent toggles for d each, and reads it again. The // working directory is the app's own (the binary's parent's parent, since // builds land in dist/). func MeasureMemory(bin string, extra []string, d time.Duration) (Memory, error) { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return Memory{}, err } addr := ln.Addr().String() ln.Close() tmp, err := os.MkdirTemp(os.Getenv("TMPDIR"), "bakeoff-mem-") if err != nil { return Memory{}, err } defer os.RemoveAll(tmp) cmd := exec.Command(bin, append(extra, "--listen", addr, "--db", filepath.Join(tmp, "app.db"))...) cmd.Dir = filepath.Dir(filepath.Dir(bin)) cmd.Stdout, cmd.Stderr = nil, nil if err := cmd.Start(); err != nil { return Memory{}, err } defer func() { cmd.Process.Signal(syscall.SIGTERM) done := make(chan error, 1) go func() { done <- cmd.Wait() }() select { case <-done: case <-time.After(10 * time.Second): cmd.Process.Kill() } }() base := "http://" + addr up := false for i := 0; i < 150 && !up; i++ { if res, err := http.Get(base + "/healthz"); err == nil { res.Body.Close() up = res.StatusCode == 200 } if !up { time.Sleep(200 * time.Millisecond) } } if !up { return Memory{}, fmt.Errorf("%s did not answer /healthz on %s", bin, addr) } time.Sleep(2 * time.Second) var m Memory if m.IdleKB, _, err = procStatus(cmd.Process.Pid); err != nil { return m, err } s, err := NewSession(base) if err != nil { return m, err } if _, err := Seed(s, 50); err != nil { return m, err } for _, sc := range []string{"read", "toggle"} { if _, err := RunLoad(s, sc, 64, d); err != nil { return m, err } } if _, m.PeakKB, err = procStatus(cmd.Process.Pid); err != nil { return m, err } time.Sleep(5 * time.Second) m.AfterKB, _, err = procStatus(cmd.Process.Pid) return m, err }