github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/benchmany/commits.go (about) 1 // Copyright 2015 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 package main 6 7 import ( 8 "bufio" 9 "bytes" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "log" 14 "os" 15 "os/user" 16 "path/filepath" 17 "regexp" 18 "strings" 19 "time" 20 ) 21 22 type commitInfo struct { 23 hash string 24 commitDate time.Time 25 gover bool 26 logPath string 27 count, fails int 28 buildFailed bool 29 } 30 31 // getCommits returns the commit info for all of the revisions in the 32 // given git revision range, where the revision range is spelled as 33 // documented in gitrevisions(7). Commits are returned in reverse 34 // chronological order, most recent commit first (the same as 35 // git-rev-list(1)). 36 func getCommits(revRange []string, logPath string) []*commitInfo { 37 // Get commit sequence. 38 args := append(append([]string{"--no-walk"}, revRange...), "--") 39 hashes := lines(git("rev-list", args...)) 40 commits := make([]*commitInfo, len(hashes)) 41 commitMap := make(map[string]*commitInfo) 42 for i, hash := range hashes { 43 commits[i] = &commitInfo{ 44 hash: hash, 45 logPath: logPath, 46 } 47 commitMap[hash] = commits[i] 48 } 49 50 // Get commit dates. 51 // 52 // TODO: This can produce a huge command line. 53 args = append([]string{"-s", "--format=format:%cI"}, hashes...) 54 dates := lines(git("show", args...)) 55 for i := range commits { 56 d, err := time.Parse(time.RFC3339, dates[i]) 57 if err != nil { 58 log.Fatalf("cannot parse commit date: %v", err) 59 } 60 commits[i].commitDate = d 61 } 62 63 // Get gover-cached builds. It's okay if this fails. 64 if fis, err := ioutil.ReadDir(goverDir()); err == nil { 65 for _, fi := range fis { 66 if ci := commitMap[fi.Name()]; ci != nil && fi.IsDir() { 67 ci.gover = true 68 } 69 } 70 } 71 72 // Load current benchmark state. 73 logf, err := os.Open(logPath) 74 if err != nil { 75 if !os.IsNotExist(err) { 76 log.Fatalf("opening %s: %v", logPath, err) 77 } 78 } else { 79 defer logf.Close() 80 parseLog(commitMap, logf) 81 } 82 83 return commits 84 } 85 86 // goverDir returns the directory containing gover-cached builds. 87 func goverDir() string { 88 cache := os.Getenv("XDG_CACHE_HOME") 89 if cache == "" { 90 home := os.Getenv("HOME") 91 if home == "" { 92 u, err := user.Current() 93 if err != nil { 94 home = u.HomeDir 95 } 96 } 97 cache = filepath.Join(home, ".cache") 98 } 99 return filepath.Join(cache, "gover") 100 } 101 102 // parseLog parses benchmark runs and failures from r and updates 103 // commits in commitMap. 104 func parseLog(commitMap map[string]*commitInfo, r io.Reader) { 105 scanner := bufio.NewScanner(r) 106 for scanner.Scan() { 107 b := scanner.Bytes() 108 switch { 109 case bytes.HasPrefix(b, []byte("commit: ")): 110 hash := scanner.Text()[len("commit: "):] 111 if ci := commitMap[hash]; ci != nil { 112 ci.count++ 113 } 114 115 case bytes.HasPrefix(b, []byte("# FAILED at ")): 116 hash := scanner.Text()[len("# FAILED at "):] 117 if ci := commitMap[hash]; ci != nil { 118 ci.fails++ 119 } 120 121 case bytes.HasPrefix(b, []byte("# BUILD FAILED at ")): 122 hash := scanner.Text()[len("# BUILD FAILED at "):] 123 if ci := commitMap[hash]; ci != nil { 124 ci.buildFailed = true 125 } 126 } 127 } 128 if err := scanner.Err(); err != nil { 129 log.Fatal("parsing benchmark log: ", err) 130 } 131 } 132 133 // binPath returns the file name of the binary for this commit. 134 func (c *commitInfo) binPath() string { 135 // TODO: This assumes the short commit hash is unique. 136 return fmt.Sprintf("bench.%s", c.hash[:7]) 137 } 138 139 // failed returns whether commit c has failed and should not be run 140 // any more. 141 func (c *commitInfo) failed() bool { 142 return c.buildFailed || c.fails >= maxFails 143 } 144 145 // runnable returns whether commit c needs to be benchmarked at least 146 // one more time. 147 func (c *commitInfo) runnable() bool { 148 return !c.buildFailed && c.fails < maxFails && c.count < run.iterations 149 } 150 151 // partial returns true if this commit is both runnable and already 152 // has some runs. 153 func (c *commitInfo) partial() bool { 154 return c.count > 0 && c.runnable() 155 } 156 157 var commitRe = regexp.MustCompile(`^commit: |^# FAILED|^# BUILD FAILED`) 158 159 // cleanLog escapes lines in l that may confuse the log parser and 160 // makes sure l is newline terminated. 161 func cleanLog(l string) string { 162 l = commitRe.ReplaceAllString(l, "# $0") 163 if !strings.HasSuffix(l, "\n") { 164 l += "\n" 165 } 166 return l 167 } 168 169 // logRun updates c with a successful run. 170 func (c *commitInfo) logRun(out string) { 171 var log bytes.Buffer 172 fmt.Fprintf(&log, "commit: %s\n", c.hash) 173 fmt.Fprintf(&log, "commit-time: %s\n", c.commitDate.UTC().Format(time.RFC3339)) 174 fmt.Fprintf(&log, "\n%s\n", cleanLog(out)) 175 c.writeLog(log.String()) 176 c.count++ 177 } 178 179 // logFailed updates c with a failed run. If buildFailed is true, this 180 // is considered a permanent failure and buildFailed is set. 181 func (c *commitInfo) logFailed(buildFailed bool, out string) { 182 typ := "FAILED" 183 if buildFailed { 184 typ = "BUILD FAILED" 185 } 186 c.writeLog(fmt.Sprintf("# %s at %s\n# %s\n", typ, c.hash, strings.Replace(cleanLog(out), "\n", "\n# ", -1))) 187 if buildFailed { 188 c.buildFailed = true 189 } else { 190 c.fails++ 191 } 192 } 193 194 // writeLog appends msg to c's log file. The caller is responsible for 195 // properly formatting it. 196 func (c *commitInfo) writeLog(msg string) { 197 logFile, err := os.OpenFile(c.logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 198 if err != nil { 199 log.Fatalf("opening %s: %v", c.logPath, err) 200 } 201 if _, err := logFile.WriteString(msg); err != nil { 202 log.Fatalf("writing to %s: %v", c.logPath, err) 203 } 204 if err := logFile.Close(); err != nil { 205 log.Fatalf("closing %s: %v", c.logPath, err) 206 } 207 }