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