github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/benchplot/main.go (about) 1 // Copyright 2016 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Command benchplot plots the results of benchmarks over time. 6 // 7 // benchplot takes an input file in Go benchmark format [1]. Each 8 // benchmark result must have a "commit" configuration key that gives 9 // the full commit hash of the revision that gave that result. 10 // benchplot will cross-reference these hashes against the specified 11 // Git repository and plot each metric over time for each benchmark. 12 // 13 // [1] https://github.com/golang/proposal/blob/master/design/14313-benchmark-format.md 14 package main 15 16 import ( 17 "bytes" 18 "flag" 19 "fmt" 20 "log" 21 "os" 22 "os/exec" 23 "runtime" 24 "runtime/pprof" 25 "strings" 26 27 "github.com/aclements/go-gg/gg" 28 "github.com/aclements/go-gg/table" 29 "github.com/aclements/go-misc/bench" 30 ) 31 32 func main() { 33 log.SetPrefix("benchplot: ") 34 log.SetFlags(0) 35 36 defaultGitDir, _ := exec.Command("git", "rev-parse", "--show-toplevel").Output() 37 defaultGitDir = bytes.TrimRight(defaultGitDir, "\n") 38 var ( 39 flagCPUProfile = flag.String("cpuprofile", "", "write CPU profile to `file`") 40 flagMemProfile = flag.String("memprofile", "", "write heap profile to `file`") 41 flagGitDir = flag.String("C", string(defaultGitDir), "run git in `dir`") 42 flagOut = flag.String("o", "", "write output to `file` (default: stdout)") 43 flagTable = flag.Bool("table", false, "output a table instead of a plot") 44 ) 45 flag.Usage = func() { 46 fmt.Fprintf(os.Stderr, "Usage: %s [flags] [inputs...]\n", os.Args[0]) 47 flag.PrintDefaults() 48 } 49 flag.Parse() 50 51 if *flagCPUProfile != "" { 52 f, err := os.Create(*flagCPUProfile) 53 if err != nil { 54 log.Fatal(err) 55 } 56 pprof.StartCPUProfile(f) 57 defer pprof.StopCPUProfile() 58 } 59 60 if *flagMemProfile != "" { 61 defer func() { 62 runtime.GC() 63 f, err := os.Create(*flagMemProfile) 64 if err != nil { 65 log.Fatal(err) 66 } 67 pprof.WriteHeapProfile(f) 68 f.Close() 69 }() 70 } 71 72 // Parse benchmark inputs. 73 paths := flag.Args() 74 if len(paths) == 0 { 75 paths = []string{"-"} 76 } 77 var benchmarks []*bench.Benchmark 78 for _, path := range paths { 79 func() { 80 f := os.Stdin 81 if path != "-" { 82 var err error 83 f, err = os.Open(path) 84 if err != nil { 85 log.Fatal(err) 86 } 87 defer f.Close() 88 } 89 90 bs, err := bench.Parse(f) 91 if err != nil { 92 log.Fatal(err) 93 } 94 benchmarks = append(benchmarks, bs...) 95 }() 96 } 97 bench.ParseValues(benchmarks, nil) 98 99 // Prepare gg tables. 100 var tab table.Grouping 101 btab, configCols, resultCols := benchmarksToTable(benchmarks) 102 if btab.Column("commit") == nil { 103 tab = btab 104 } else { 105 gtab := commitsToTable(Commits(*flagGitDir)) 106 tab = table.Join(btab, "commit", gtab, "commit") 107 } 108 109 // Prepare for output. 110 f := os.Stdout 111 if *flagOut != "" { 112 var err error 113 f, err = os.Create(*flagOut) 114 if err != nil { 115 log.Fatal(err) 116 } 117 defer f.Close() 118 } 119 120 // Output table. 121 if *flagTable { 122 table.Fprint(f, tab) 123 return 124 } 125 126 // Plot. 127 // 128 // TODO: Collect nrows/ncols from the plot itself. 129 p, nrows, ncols := plot(tab, configCols, resultCols) 130 if !(len(paths) == 1 && paths[0] == "-") { 131 p.Add(gg.Title(strings.Join(paths, " "))) 132 } 133 134 // Render plot. 135 p.WriteSVG(f, 500*ncols, 350*nrows) 136 }