github.com/elek/golangci-lint@v1.42.2-0.20211208090441-c05b7fcb3a9a/test/bench/bench_test.go (about)

     1  package bench
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/build"
     7  	"log"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	gops "github.com/mitchellh/go-ps"
    17  	"github.com/shirou/gopsutil/v3/process"
    18  
    19  	"github.com/golangci/golangci-lint/test/testshared"
    20  )
    21  
    22  func chdir(b *testing.B, dir string) {
    23  	if err := os.Chdir(dir); err != nil {
    24  		b.Fatalf("can't chdir to %s: %s", dir, err)
    25  	}
    26  }
    27  
    28  func prepareGoSource(b *testing.B) {
    29  	chdir(b, filepath.Join(build.Default.GOROOT, "src"))
    30  }
    31  
    32  func prepareGithubProject(owner, name string) func(*testing.B) {
    33  	return func(b *testing.B) {
    34  		dir := filepath.Join(build.Default.GOPATH, "src", "github.com", owner, name)
    35  		_, err := os.Stat(dir)
    36  		if os.IsNotExist(err) {
    37  			repo := fmt.Sprintf("https://github.com/%s/%s.git", owner, name)
    38  			err = exec.Command("git", "clone", repo, dir).Run()
    39  			if err != nil {
    40  				b.Fatalf("can't git clone %s/%s: %s", owner, name, err)
    41  			}
    42  		}
    43  		chdir(b, dir)
    44  	}
    45  }
    46  
    47  func getBenchLintersArgsNoMegacheck() []string {
    48  	return []string{
    49  		"--enable=deadcode",
    50  		"--enable=gocyclo",
    51  		"--enable=golint",
    52  		"--enable=varcheck",
    53  		"--enable=structcheck",
    54  		"--enable=maligned",
    55  		"--enable=errcheck",
    56  		"--enable=dupl",
    57  		"--enable=ineffassign",
    58  		"--enable=unconvert",
    59  		"--enable=goconst",
    60  		"--enable=gosec",
    61  	}
    62  }
    63  
    64  func getBenchLintersArgs() []string {
    65  	return append([]string{
    66  		"--enable=megacheck",
    67  	}, getBenchLintersArgsNoMegacheck()...)
    68  }
    69  
    70  func printCommand(cmd string, args ...string) {
    71  	if os.Getenv("PRINT_CMD") != "1" {
    72  		return
    73  	}
    74  	quotedArgs := []string{}
    75  	for _, a := range args {
    76  		quotedArgs = append(quotedArgs, strconv.Quote(a))
    77  	}
    78  
    79  	log.Printf("%s %s", cmd, strings.Join(quotedArgs, " "))
    80  }
    81  
    82  func getGolangciLintCommonArgs() []string {
    83  	return []string{"run", "--no-config", "--issues-exit-code=0", "--deadline=30m", "--disable-all", "--enable=govet"}
    84  }
    85  
    86  func runGolangciLintForBench(b *testing.B) {
    87  	args := getGolangciLintCommonArgs()
    88  	args = append(args, getBenchLintersArgs()...)
    89  	printCommand("golangci-lint", args...)
    90  	out, err := exec.Command("golangci-lint", args...).CombinedOutput()
    91  	if err != nil {
    92  		b.Fatalf("can't run golangci-lint: %s, %s", err, out)
    93  	}
    94  }
    95  
    96  func getGoLinesTotalCount(b *testing.B) int {
    97  	cmd := exec.Command("bash", "-c", `find . -name "*.go" | fgrep -v vendor | xargs wc -l | tail -1`)
    98  	out, err := cmd.CombinedOutput()
    99  	if err != nil {
   100  		b.Fatalf("can't run go lines counter: %s", err)
   101  	}
   102  
   103  	parts := bytes.Split(bytes.TrimSpace(out), []byte(" "))
   104  	n, err := strconv.Atoi(string(parts[0]))
   105  	if err != nil {
   106  		b.Fatalf("can't parse go lines count: %s", err)
   107  	}
   108  
   109  	return n
   110  }
   111  
   112  func getLinterMemoryMB(b *testing.B, progName string) (int, error) {
   113  	processes, err := gops.Processes()
   114  	if err != nil {
   115  		b.Fatalf("Can't get processes: %s", err)
   116  	}
   117  
   118  	var progPID int
   119  	for _, p := range processes {
   120  		if p.Executable() == progName {
   121  			progPID = p.Pid()
   122  			break
   123  		}
   124  	}
   125  	if progPID == 0 {
   126  		return 0, fmt.Errorf("no process")
   127  	}
   128  
   129  	allProgPIDs := []int{progPID}
   130  	for _, p := range processes {
   131  		if p.PPid() == progPID {
   132  			allProgPIDs = append(allProgPIDs, p.Pid())
   133  		}
   134  	}
   135  
   136  	var totalProgMemBytes uint64
   137  	for _, pid := range allProgPIDs {
   138  		p, err := process.NewProcess(int32(pid))
   139  		if err != nil {
   140  			continue // subprocess could die
   141  		}
   142  
   143  		mi, err := p.MemoryInfo()
   144  		if err != nil {
   145  			continue
   146  		}
   147  
   148  		totalProgMemBytes += mi.RSS
   149  	}
   150  
   151  	return int(totalProgMemBytes / 1024 / 1024), nil
   152  }
   153  
   154  func trackPeakMemoryUsage(b *testing.B, doneCh <-chan struct{}, progName string) chan int {
   155  	resCh := make(chan int)
   156  	go func() {
   157  		var peakUsedMemMB int
   158  		t := time.NewTicker(time.Millisecond * 5)
   159  		defer t.Stop()
   160  
   161  		for {
   162  			select {
   163  			case <-doneCh:
   164  				resCh <- peakUsedMemMB
   165  				close(resCh)
   166  				return
   167  			case <-t.C:
   168  			}
   169  
   170  			m, err := getLinterMemoryMB(b, progName)
   171  			if err != nil {
   172  				continue
   173  			}
   174  			if m > peakUsedMemMB {
   175  				peakUsedMemMB = m
   176  			}
   177  		}
   178  	}()
   179  	return resCh
   180  }
   181  
   182  type runResult struct {
   183  	peakMemMB int
   184  	duration  time.Duration
   185  }
   186  
   187  func runOne(b *testing.B, run func(*testing.B), progName string) *runResult {
   188  	doneCh := make(chan struct{})
   189  	peakMemCh := trackPeakMemoryUsage(b, doneCh, progName)
   190  	startedAt := time.Now()
   191  	run(b)
   192  	duration := time.Since(startedAt)
   193  	close(doneCh)
   194  
   195  	peakUsedMemMB := <-peakMemCh
   196  	return &runResult{
   197  		peakMemMB: peakUsedMemMB,
   198  		duration:  duration,
   199  	}
   200  }
   201  
   202  func BenchmarkGolangciLint(b *testing.B) {
   203  	testshared.NewLintRunner(b).Install()
   204  
   205  	type bcase struct {
   206  		name    string
   207  		prepare func(*testing.B)
   208  	}
   209  	bcases := []bcase{
   210  		{
   211  			name:    "self repo",
   212  			prepare: prepareGithubProject("golangci", "golangci-lint"),
   213  		},
   214  		{
   215  			name:    "hugo",
   216  			prepare: prepareGithubProject("gohugoio", "hugo"),
   217  		},
   218  		{
   219  			name:    "go-ethereum",
   220  			prepare: prepareGithubProject("ethereum", "go-ethereum"),
   221  		},
   222  		{
   223  			name:    "beego",
   224  			prepare: prepareGithubProject("astaxie", "beego"),
   225  		},
   226  		{
   227  			name:    "terraform",
   228  			prepare: prepareGithubProject("hashicorp", "terraform"),
   229  		},
   230  		{
   231  			name:    "consul",
   232  			prepare: prepareGithubProject("hashicorp", "consul"),
   233  		},
   234  		{
   235  			name:    "go source code",
   236  			prepare: prepareGoSource,
   237  		},
   238  	}
   239  	for _, bc := range bcases {
   240  		bc.prepare(b)
   241  		lc := getGoLinesTotalCount(b)
   242  		result := runOne(b, runGolangciLintForBench, "golangci-lint")
   243  
   244  		log.Printf("%s (%d kLoC): time: %s, memory: %dMB",
   245  			bc.name, lc/1000,
   246  			result.duration,
   247  			result.peakMemMB,
   248  		)
   249  	}
   250  }