github.com/golangCi/golangCi-lint@v1.10.1/test/bench_test.go (about)

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