package bakeoff import ( "crypto/sha256" "fmt" "io/fs" "os" "os/exec" "path/filepath" "sort" "strings" "time" ) // Build is the three build times for the shipped arm64 binary, as medians // in seconds, and its size. type Build struct { ColdSeconds, WarmSeconds, EditSeconds float64 Bytes int64 Host string ColdRuns, Runs int // how many cold builds, and warm and edit builds, each median is of } func mk(dir string, targets ...string) error { cmd := exec.Command("make", targets...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("make %v: %w\n%s", targets, err, out) } return nil } func timed(dir string, targets ...string) (float64, error) { start := time.Now() err := mk(dir, targets...) return time.Since(start).Seconds(), err } func median(v []float64) float64 { sort.Float64s(v) return v[len(v)/2] } // BuildStats measures cold (clean-deps; COLD=1 for Go, which passes -a) // builds coldRuns times, and warm (clean) and one-line-change (editBuild) // builds runs times each. Cold builds under emulation take up to an hour, // hence the separate count. func BuildStats(dir string, goCold bool, coldRuns, runs int) (Build, error) { coldTarget := []string{"build-arm64"} if goCold { coldTarget = append(coldTarget, "COLD=1") } editFile, err := bumpFile(dir) if err != nil { return Build{}, err } var cold, warm, edit []float64 for i := 0; i < coldRuns; i++ { if err := mk(dir, "clean", "clean-deps"); err != nil { return Build{}, err } s, err := timed(dir, coldTarget...) if err != nil { return Build{}, err } cold = append(cold, s) } for i := 0; i < runs; i++ { if err := mk(dir, "clean"); err != nil { return Build{}, err } s, err := timed(dir, "build-arm64") if err != nil { return Build{}, err } warm = append(warm, s) s, err = editBuild(dir, editFile, i) if err != nil { return Build{}, err } edit = append(edit, s) } if err := mk(dir, "build-arm64"); err != nil { return Build{}, err } fi, err := os.Stat(filepath.Join(dir, "dist", "app-linux-arm64")) if err != nil { return Build{}, err } host, _ := os.Hostname() return Build{ColdSeconds: median(cold), WarmSeconds: median(warm), EditSeconds: median(edit), Bytes: fi.Size(), Host: host, ColdRuns: coldRuns, Runs: runs}, nil } // bumpFile finds the source file `make bump` edits (the one holding the // GET / handler, per the brief) by comparing every file before and after, // then runs `make unbump`. func bumpFile(dir string) (string, error) { before, err := snapshot(dir) if err != nil { return "", err } if err := mk(dir, "bump"); err != nil { return "", err } after, err := snapshot(dir) if uerr := mk(dir, "unbump"); err == nil { err = uerr } if err != nil { return "", err } for f, h := range after { if before[f] != h { return f, nil } } return "", fmt.Errorf("make bump changed no file in %s", dir) } // snapshot hashes every file under dir outside build output and caches. func snapshot(dir string) (map[string][32]byte, error) { out := map[string][32]byte{} err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { if p != dir && (skipDirs[d.Name()] || strings.HasPrefix(d.Name(), ".")) { return filepath.SkipDir } return nil } b, err := os.ReadFile(p) if err != nil { return err } out[p] = sha256.Sum256(b) return nil }) return out, err } // commentPrefix is the line-comment syntax for a source file. func commentPrefix(path string) string { switch strings.ToLower(filepath.Ext(path)) { case ".hs": return "-- " case ".nim", ".rb", ".mojo", ".py": return "# " case ".ml", ".mli": return "(* " } return "// " } // editBuild is the one-line change: append a comment line no compiler // cache has seen to file, time the rebuild, and put the file back. A // comment changes the source's content (and mtime), so content-addressed // caches (Go, Zig, Nim, dune) recompile it as surely as mtime-based ones. func editBuild(dir, file string, run int) (float64, error) { orig, err := os.ReadFile(file) if err != nil { return 0, err } defer os.WriteFile(file, orig, 0o644) line := fmt.Sprintf("%sbakeoff edit %d %d", commentPrefix(file), time.Now().UnixNano(), run) if strings.HasPrefix(line, "(* ") { line += " *)" } if err := os.WriteFile(file, append(append(orig, '\n'), line+"\n"...), 0o644); err != nil { return 0, err } return timed(dir, "build-arm64") }