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  }