carlos / todomvc-bakeoff Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Command bakeoff measures one app and records the result in
2// results/<slug>.json. Run it from the repo root.
3//
4// bakeoff <ttfb|wake|load|weight|code|build|tokens|behaviour|source|readability> --slug <s> [flags]
5package main
6
7import (
8 "encoding/json"
9 "errors"
10 "flag"
11 "fmt"
12 "io"
13 "os"
14 "os/exec"
15 "path/filepath"
16 "strconv"
17 "strings"
18 "time"
19
20 b "bakeoff/harness"
21 "bakeoff/harness/report"
22)
23
24func main() {
25 if len(os.Args) < 2 {
26 usage()
27 }
28 cmd := os.Args[1]
29 fs := flag.NewFlagSet(cmd, flag.ExitOnError)
30 slug := fs.String("slug", "", "app slug, e.g. rust")
31 url := fs.String("url", "", "app base URL (default https://todo-<slug>.bfj.oncarlos.com)")
32 results := fs.String("results", "results", "results directory")
33 dir := fs.String("dir", "", "app directory (default apps/<slug>)")
34 runs := fs.Int("runs", 3, "build: warm and one-line-change runs")
35 coldRuns := fs.Int("cold-runs", 1, "build: cold runs")
36 dur := fs.Duration("duration", 10*time.Second, "load: length of each step")
37 rest := fs.Duration("rest", 10*time.Second, "load: pause between steps")
38 levels := fs.String("levels", "1,2,4,8,16,32,64,128,256,512,1024", "load: concurrency levels, doubling")
39 phase := fs.String("phase", "", "tokens: build|fix|security|review")
40 logs := fs.String("logs", "builders/logs", "tokens: log directory")
41 since := fs.String("since", "", "tokens: log whose cumulative totals this resumed phase starts from (default: the previous phase's)")
42 file := fs.String("file", "", "behaviour: Playwright JSON report")
43 var rec b.Record
44 fs.StringVar(&rec.Language, "language", "", "record: display name")
45 fs.StringVar(&rec.Status, "status", "", "record: LIVE or BLOCKED <reason>")
46 fs.StringVar(&rec.Version, "version", "", "record: deployed version")
47 fs.StringVar(&rec.Stack, "stack", "", "record: one-line stack summary")
48 fs.StringVar(&rec.Source, "source", "", "record: source URL")
49 fs.StringVar(&rec.Note, "note", "", "record: a note shown in the app's section (added once)")
50 recURL := fs.String("app-url", "", "record: live URL")
51 clearNotes := fs.Bool("clear-notes", false, "record: drop existing notes first")
52 clearWake := fs.Bool("clear", false, "wake: drop recorded samples")
53 preview := fs.Bool("preview", false, "report: tag strings for copy review")
54 replies := fs.String("replies", "", "readability: comma-separated files, one scorer reply each")
55 fs.Parse(os.Args[2:])
56 if *slug == "" && cmd != "report" && cmd != "writeup" {
57 usage()
58 }
59 b.ResultsDir = *results
60 if *url == "" {
61 *url = b.URL(*slug)
62 }
63 if *dir == "" {
64 *dir = filepath.Join("apps", *slug)
65 }
66 var err error
67 switch cmd {
68 case "ttfb":
69 err = ttfb(*slug, *url)
70 case "wake":
71 if *clearWake {
72 err = b.Update(*slug, func(r *b.AppResult) { r.Wake = nil })
73 break
74 }
75 err = wake(*slug, *url)
76 case "load":
77 err = load(*slug, *url, *levels, *dur, *rest)
78 case "weight":
79 err = weight(*slug, *url)
80 case "code":
81 err = code(*slug, *dir)
82 case "build":
83 err = build(*slug, *dir, *coldRuns, *runs)
84 case "tokens":
85 err = tokens(*slug, *logs, *phase, *since)
86 case "behaviour":
87 err = behaviour(*slug, *file)
88 case "writeup":
89 err = renderWriteup(*file)
90 case "report":
91 err = renderReport(*file, *preview)
92 case "findings":
93 err = findings(*slug, *file)
94 case "probe":
95 err = probe(*url, *levels, *dur)
96 case "memory":
97 err = memory(*slug, *dir, *dur)
98 case "record":
99 if rec.URL = *recURL; rec.URL == "" && strings.HasPrefix(rec.Status, "LIVE") {
100 rec.URL = *url
101 }
102 err = b.Update(*slug, func(r *b.AppResult) {
103 if *clearNotes {
104 r.Notes = nil
105 }
106 b.ApplyRecord(r, rec)
107 })
108 case "source":
109 err = source(*dir)
110 case "readability":
111 err = readability(*slug, *replies)
112 default:
113 usage()
114 }
115 if err != nil {
116 fmt.Fprintf(os.Stderr, "bakeoff %s %s: %v\n", cmd, *slug, err)
117 os.Exit(1)
118 }
119}
120
121func usage() {
122 fmt.Fprintln(os.Stderr, "usage: bakeoff <ttfb|wake|load|weight|code|build|tokens|behaviour|source|readability> --slug <s> [flags]")
123 os.Exit(2)
124}
125
126func seeded(url string, n int) (*b.Session, error) {
127 s, err := b.NewSession(url)
128 if err != nil {
129 return nil, err
130 }
131 if n > 0 {
132 _, err = b.Seed(s, n)
133 }
134 return s, err
135}
136
137func ttfb(slug, url string) error {
138 empty, err := seeded(url, 0)
139 if err != nil {
140 return err
141 }
142 e, err := b.MeasureTTFB(empty, "/", 3, 30)
143 if err != nil {
144 return err
145 }
146 full, err := seeded(url, 50)
147 if err != nil {
148 return err
149 }
150 f, err := b.MeasureTTFB(full, "/", 3, 30)
151 if err != nil {
152 return err
153 }
154 fmt.Printf("%s ttfb empty p50 %.1fms, 50 items p50 %.1fms\n", slug, e.P50, f.P50)
155 return b.Update(slug, func(r *b.AppResult) { r.TTFBEmpty, r.TTFB50 = e, f })
156}
157
158// wake takes one sample if the instance is asleep, and exits 3 if it is
159// awake, so a polling loop can tell "not asleep yet" from failure.
160func wake(slug, url string) error {
161 asleep, err := b.IsAsleep(b.AppName(slug), b.Host(slug))
162 if err != nil {
163 return err
164 }
165 if !asleep {
166 fmt.Printf("%s is awake; no sample\n", slug)
167 os.Exit(3)
168 }
169 // A fresh visitor, without first fetching / (that would wake it).
170 s, err := b.SessionNoFetch(url)
171 if err != nil {
172 return err
173 }
174 ms, err := b.MeasureWake(s, "/")
175 if err != nil {
176 return err
177 }
178 fmt.Printf("%s wake %.1fms\n", slug, ms)
179 return b.Update(slug, func(r *b.AppResult) { r.Wake = append(r.Wake, ms) })
180}
181
182// load ramps each scenario through the levels, stopping early when the
183// app has stopped scaling or is failing (b.SweepShouldStop).
184func load(slug, url, levels string, d, rest time.Duration) error {
185 s, err := seeded(url, 50)
186 if err != nil {
187 return err
188 }
189 var out []b.LoadResult
190 for _, sc := range []string{"read", "toggle", "read-after"} {
191 var sweep []b.LoadResult
192 for _, l := range strings.Split(levels, ",") {
193 c, err := strconv.Atoi(strings.TrimSpace(l))
194 if err != nil {
195 return err
196 }
197 if len(out)+len(sweep) > 0 {
198 time.Sleep(rest)
199 }
200 r, err := b.RunLoad(s, sc, c, d)
201 if err != nil {
202 // A step that can't run at all (oha failed, or the app is
203 // down) is a step where every request failed: record it and
204 // end this scenario, rather than losing the whole run.
205 fmt.Printf("%s %s c=%d: step failed: %v\n", slug, sc, c, err)
206 r = b.LoadResult{Scenario: sc, Concurrency: c, ErrorRate: 1, Errors: map[string]int{err.Error(): 1}}
207 sweep = append(sweep, r)
208 break
209 }
210 fmt.Printf("%s %s c=%d: %.0f rps, p50 %.1fms, p99 %.1fms, errors %.2f%%", slug, sc, c, r.RPS, r.P50Ms, r.P99Ms, r.ErrorRate*100)
211 if r.ErrorRate > 0 {
212 fmt.Printf(" status %v errors %v", r.Status, r.Errors)
213 }
214 fmt.Println()
215 sweep = append(sweep, r)
216 if b.SweepShouldStop(sweep) {
217 break
218 }
219 }
220 k := b.Knee(sweep)
221 fmt.Printf("%s %s: peak %.0f rps; slows down after %d (found %v, tested to %d)\n", slug, sc, k.PeakRPS, k.Concurrency, k.Found, k.MaxTested)
222 out = append(out, sweep...)
223 }
224 return b.Update(slug, func(r *b.AppResult) { r.Load = out })
225}
226
227func weight(slug, url string) error {
228 s, err := seeded(url, 50)
229 if err != nil {
230 return err
231 }
232 w, err := b.PageWeight(s, "/")
233 if err != nil {
234 return err
235 }
236 fmt.Printf("%s page weight (50 items): html %d bytes, js %d bytes\n", slug, w.HTMLBytes, w.JSBytes)
237 return b.Update(slug, func(r *b.AppResult) { r.Weight = w })
238}
239
240func code(slug, dir string) error {
241 c, err := b.CountCode(dir)
242 if err != nil {
243 return err
244 }
245 fmt.Printf("%s code: %+v\n", slug, c)
246 return b.Update(slug, func(r *b.AppResult) { r.Code = c })
247}
248
249func build(slug, dir string, coldRuns, runs int) error {
250 st, err := b.BuildStats(dir, strings.HasPrefix(slug, "go"), coldRuns, runs)
251 if err != nil {
252 return err
253 }
254 fmt.Printf("%s build: cold %.1fs, warm %.1fs, edit %.1fs, %d bytes on %s\n", slug, st.ColdSeconds, st.WarmSeconds, st.EditSeconds, st.Bytes, st.Host)
255 return b.Update(slug, func(r *b.AppResult) { r.Build = st })
256}
257
258func tokens(slug, logs, phase, since string) error {
259 parse := b.ParseClaudeLog
260 if phase == "review" {
261 parse = b.ParseCodexLog
262 } else if phase != "build" && phase != "fix" && phase != "security" {
263 return errors.New("--phase must be build, fix, security or review")
264 }
265 files, _ := filepath.Glob(filepath.Join(logs, slug+"."+phase+"*.jsonl"))
266 if len(files) == 0 {
267 return fmt.Errorf("no %s logs for %s in %s", phase, slug, logs)
268 }
269 var total b.Tokens
270 for _, f := range files {
271 fh, err := os.Open(f)
272 if err != nil {
273 return err
274 }
275 t, err := parse(fh)
276 fh.Close()
277 if err != nil {
278 return fmt.Errorf("%s: %w", f, err)
279 }
280 total = total.Add(t)
281 }
282 // Fix and security rounds resume the build session, whose result
283 // events report usage cumulatively: subtract the phase before.
284 if phase == "fix" || phase == "security" {
285 prev := []string{"build"}
286 if phase == "security" {
287 prev = []string{"fix", "build"}
288 }
289 if since != "" {
290 prev = []string{since}
291 }
292 for _, p := range prev {
293 f := filepath.Join(logs, slug+"."+p+".jsonl")
294 if since != "" {
295 f = since
296 }
297 fh, err := os.Open(f)
298 if err != nil {
299 continue
300 }
301 before, err := b.ParseClaudeLog(fh)
302 fh.Close()
303 if err != nil {
304 return fmt.Errorf("%s: %w", f, err)
305 }
306 total = total.Since(before)
307 break
308 }
309 }
310 fmt.Printf("%s %s tokens: %+v\n", slug, phase, total)
311 return b.Update(slug, func(r *b.AppResult) {
312 switch phase {
313 case "build":
314 r.TokensBuild = total
315 case "fix":
316 r.TokensFix = total
317 case "security":
318 r.TokensSecurity = total
319 case "review":
320 r.TokensReview = total
321 }
322 })
323}
324
325func behaviour(slug, file string) error {
326 var in io.Reader = os.Stdin
327 if file != "" {
328 f, err := os.Open(file)
329 if err != nil {
330 return err
331 }
332 defer f.Close()
333 in = f
334 }
335 got, err := b.ParsePlaywright(in)
336 if err != nil {
337 return err
338 }
339 fmt.Printf("%s behaviour: js-on %+v, js-off %+v\n", slug, got["js-on"], got["js-off"])
340 return b.Update(slug, func(r *b.AppResult) { r.BehaviourJSOn, r.BehaviourJSOff = got["js-on"], got["js-off"] })
341}
342
343// source prints the app's own code, file by file, for the readability scorer.
344func source(dir string) error {
345 files, err := b.SourceFiles(dir)
346 if err != nil {
347 return err
348 }
349 for _, f := range files {
350 body, err := os.ReadFile(filepath.Join(dir, f))
351 if err != nil {
352 return err
353 }
354 fmt.Printf("===== %s =====\n%s\n", f, body)
355 }
356 return nil
357}
358
359func readability(slug, replies string) error {
360 var texts []string
361 for _, f := range strings.Split(replies, ",") {
362 body, err := os.ReadFile(f)
363 if err != nil {
364 return err
365 }
366 texts = append(texts, string(body))
367 }
368 r, err := b.MedianReadability(texts)
369 if err != nil {
370 return err
371 }
372 fmt.Printf("%s readability: overall %.0f/10 (naming %.0f, structure %.0f, traceability %.0f, errors %.0f, comments %.0f)\n",
373 slug, r.Overall, r.Naming, r.Structure, r.Traceability, r.Errors, r.Comments)
374 return b.Update(slug, func(a *b.AppResult) { a.Readability = r })
375}
376
377// renderReport writes the report page to stdout from every results file,
378// in the contestants' fixed order, with prose from the copy file.
379func renderReport(copyFile string, preview bool) error {
380 if copyFile == "" {
381 copyFile = "copy-review/strings.json"
382 }
383 text, err := os.ReadFile(copyFile)
384 if err != nil {
385 return err
386 }
387 words, err := report.LoadStrings(text)
388 if err != nil {
389 return err
390 }
391 var rs []b.AppResult
392 for _, a := range b.Apps {
393 r, err := b.LoadIn(b.ResultsDir, a.Slug)
394 if err != nil {
395 return err
396 }
397 if r.Language == "" {
398 r.Language = a.Language
399 }
400 if r.Status == "" {
401 r.Status = "not built yet"
402 }
403 rs = append(rs, r)
404 }
405 if preview {
406 return report.RenderPreview(rs, words, os.Stdout)
407 }
408 return report.Render(rs, words, os.Stdout)
409}
410
411// findings records what a review left unfixed and exits 4 if any of it
412// is a security finding (which earns the security-only round).
413func findings(slug, file string) error {
414 text, err := os.ReadFile(file)
415 if err != nil {
416 return err
417 }
418 fs := b.ParseFindings(string(text))
419 fmt.Printf("%s: %d findings remain\n", slug, len(fs))
420 if err := b.Update(slug, func(r *b.AppResult) { r.ReviewFindings = fs }); err != nil {
421 return err
422 }
423 if b.HasSecurity(fs) {
424 fmt.Printf("%s: security findings remain\n", slug)
425 os.Exit(4)
426 }
427 return nil
428}
429
430// probe runs one toggle step per level and prints oha's raw status and
431// error breakdown, to diagnose failures a ramp reports.
432func probe(url, levels string, d time.Duration) error {
433 s, err := seeded(url, 50)
434 if err != nil {
435 return err
436 }
437 ids, err := s.IDs("/")
438 if err != nil {
439 return err
440 }
441 for _, l := range strings.Split(levels, ",") {
442 out, err := exec.Command("oha", "--no-tui", "--output-format", "json", "-z", d.String(), "-c", strings.TrimSpace(l),
443 "--redirect", "0", "-H", "Cookie: "+s.CookieHeader(), "-H", "Origin: "+url,
444 "-m", "POST", "-T", "application/x-www-form-urlencoded", "-d", "filter=all", url+"/todos/"+ids[0]+"/toggle").Output()
445 if err != nil {
446 return err
447 }
448 var o struct {
449 Status map[string]int `json:"statusCodeDistribution"`
450 Errors map[string]int `json:"errorDistribution"`
451 }
452 json.Unmarshal(out, &o)
453 fmt.Printf("c=%s status %v errors %v\n", l, o.Status, o.Errors)
454 }
455 return nil
456}
457
458// memory measures the app's own host build (dist/app, from make build)
459// locally: resident memory at rest, peak under load, and after.
460func memory(slug, dir string, d time.Duration) error {
461 bin, err := filepath.Abs(filepath.Join(dir, "dist", "app"))
462 if err != nil {
463 return err
464 }
465 m, err := b.MeasureMemory(bin, nil, d)
466 if err != nil {
467 return err
468 }
469 fmt.Printf("%s memory: idle %d KB, peak %d KB, after %d KB\n", slug, m.IdleKB, m.PeakKB, m.AfterKB)
470 return b.Update(slug, func(r *b.AppResult) { r.Memory = m })
471}
472
473// renderWriteup writes docs/writeup.md (or --file) as the write-up page.
474func renderWriteup(file string) error {
475 if file == "" {
476 file = "docs/writeup.md"
477 }
478 src, err := os.ReadFile(file)
479 if err != nil {
480 return err
481 }
482 text, err := os.ReadFile("copy-review/strings.json")
483 if err != nil {
484 return err
485 }
486 words, err := report.LoadStrings(text)
487 if err != nil {
488 return err
489 }
490 return report.RenderWriteup(string(src), words, os.Stdout)
491}
492