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 "crypto/sha256"
5 "fmt"
6 "io/fs"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "sort"
11 "strings"
12 "time"
13)
14
15// Build is the three build times for the shipped arm64 binary, as medians
16// in seconds, and its size.
17type Build struct {
18 ColdSeconds, WarmSeconds, EditSeconds float64
19 Bytes int64
20 Host string
21 ColdRuns, Runs int // how many cold builds, and warm and edit builds, each median is of
22}
23
24func mk(dir string, targets ...string) error {
25 cmd := exec.Command("make", targets...)
26 cmd.Dir = dir
27 if out, err := cmd.CombinedOutput(); err != nil {
28 return fmt.Errorf("make %v: %w\n%s", targets, err, out)
29 }
30 return nil
31}
32
33func timed(dir string, targets ...string) (float64, error) {
34 start := time.Now()
35 err := mk(dir, targets...)
36 return time.Since(start).Seconds(), err
37}
38
39func median(v []float64) float64 {
40 sort.Float64s(v)
41 return v[len(v)/2]
42}
43
44// BuildStats measures cold (clean-deps; COLD=1 for Go, which passes -a)
45// builds coldRuns times, and warm (clean) and one-line-change (editBuild)
46// builds runs times each. Cold builds under emulation take up to an hour,
47// hence the separate count.
48func BuildStats(dir string, goCold bool, coldRuns, runs int) (Build, error) {
49 coldTarget := []string{"build-arm64"}
50 if goCold {
51 coldTarget = append(coldTarget, "COLD=1")
52 }
53 editFile, err := bumpFile(dir)
54 if err != nil {
55 return Build{}, err
56 }
57 var cold, warm, edit []float64
58 for i := 0; i < coldRuns; i++ {
59 if err := mk(dir, "clean", "clean-deps"); err != nil {
60 return Build{}, err
61 }
62 s, err := timed(dir, coldTarget...)
63 if err != nil {
64 return Build{}, err
65 }
66 cold = append(cold, s)
67 }
68 for i := 0; i < runs; i++ {
69 if err := mk(dir, "clean"); err != nil {
70 return Build{}, err
71 }
72 s, err := timed(dir, "build-arm64")
73 if err != nil {
74 return Build{}, err
75 }
76 warm = append(warm, s)
77
78 s, err = editBuild(dir, editFile, i)
79 if err != nil {
80 return Build{}, err
81 }
82 edit = append(edit, s)
83 }
84 if err := mk(dir, "build-arm64"); err != nil {
85 return Build{}, err
86 }
87 fi, err := os.Stat(filepath.Join(dir, "dist", "app-linux-arm64"))
88 if err != nil {
89 return Build{}, err
90 }
91 host, _ := os.Hostname()
92 return Build{ColdSeconds: median(cold), WarmSeconds: median(warm), EditSeconds: median(edit), Bytes: fi.Size(), Host: host,
93 ColdRuns: coldRuns, Runs: runs}, nil
94}
95
96// bumpFile finds the source file `make bump` edits (the one holding the
97// GET / handler, per the brief) by comparing every file before and after,
98// then runs `make unbump`.
99func bumpFile(dir string) (string, error) {
100 before, err := snapshot(dir)
101 if err != nil {
102 return "", err
103 }
104 if err := mk(dir, "bump"); err != nil {
105 return "", err
106 }
107 after, err := snapshot(dir)
108 if uerr := mk(dir, "unbump"); err == nil {
109 err = uerr
110 }
111 if err != nil {
112 return "", err
113 }
114 for f, h := range after {
115 if before[f] != h {
116 return f, nil
117 }
118 }
119 return "", fmt.Errorf("make bump changed no file in %s", dir)
120}
121
122// snapshot hashes every file under dir outside build output and caches.
123func snapshot(dir string) (map[string][32]byte, error) {
124 out := map[string][32]byte{}
125 err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
126 if err != nil {
127 return err
128 }
129 if d.IsDir() {
130 if p != dir && (skipDirs[d.Name()] || strings.HasPrefix(d.Name(), ".")) {
131 return filepath.SkipDir
132 }
133 return nil
134 }
135 b, err := os.ReadFile(p)
136 if err != nil {
137 return err
138 }
139 out[p] = sha256.Sum256(b)
140 return nil
141 })
142 return out, err
143}
144
145// commentPrefix is the line-comment syntax for a source file.
146func commentPrefix(path string) string {
147 switch strings.ToLower(filepath.Ext(path)) {
148 case ".hs":
149 return "-- "
150 case ".nim", ".rb", ".mojo", ".py":
151 return "# "
152 case ".ml", ".mli":
153 return "(* "
154 }
155 return "// "
156}
157
158// editBuild is the one-line change: append a comment line no compiler
159// cache has seen to file, time the rebuild, and put the file back. A
160// comment changes the source's content (and mtime), so content-addressed
161// caches (Go, Zig, Nim, dune) recompile it as surely as mtime-based ones.
162func editBuild(dir, file string, run int) (float64, error) {
163 orig, err := os.ReadFile(file)
164 if err != nil {
165 return 0, err
166 }
167 defer os.WriteFile(file, orig, 0o644)
168 line := fmt.Sprintf("%sbakeoff edit %d %d", commentPrefix(file), time.Now().UnixNano(), run)
169 if strings.HasPrefix(line, "(* ") {
170 line += " *)"
171 }
172 if err := os.WriteFile(file, append(append(orig, '\n'), line+"\n"...), 0o644); err != nil {
173 return 0, err
174 }
175 return timed(dir, "build-arm64")
176}
177