golangci-lint/test/bench_test.go
Denis Isaev 8a478c47ac Prepare for #164: rename GAS to gosec
1. Rename in a backward compatible way
2. Remove gosec default exclude list because gosec is already disabled
by default.
3. Warn about unmatched linter names in //nolint directives
4. Process linter names in //nolint directives in upper case
5. Disable gosec for golangci-lint in .golangci.yml
2018-09-02 09:34:35 +03:00

288 lines
6.5 KiB
Go

package test
import (
"bytes"
"fmt"
"go/build"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/golangci/golangci-lint/pkg/config"
gops "github.com/mitchellh/go-ps"
"github.com/shirou/gopsutil/process"
)
func chdir(b *testing.B, dir string) {
if err := os.Chdir(dir); err != nil {
b.Fatalf("can't chdir to %s: %s", dir, err)
}
}
func prepareGoSource(b *testing.B) {
chdir(b, filepath.Join(build.Default.GOROOT, "src"))
}
func prepareGithubProject(owner, name string) func(*testing.B) {
return func(b *testing.B) {
dir := filepath.Join(build.Default.GOPATH, "src", "github.com", owner, name)
_, err := os.Stat(dir)
if os.IsNotExist(err) {
err = exec.Command("git", "clone", fmt.Sprintf("https://github.com/%s/%s.git", owner, name)).Run()
if err != nil {
b.Fatalf("can't git clone %s/%s: %s", owner, name, err)
}
}
chdir(b, dir)
}
}
func getBenchLintersArgsNoMegacheck() []string {
return []string{
"--enable=deadcode",
"--enable=gocyclo",
"--enable=golint",
"--enable=varcheck",
"--enable=structcheck",
"--enable=maligned",
"--enable=errcheck",
"--enable=dupl",
"--enable=ineffassign",
"--enable=interfacer",
"--enable=unconvert",
"--enable=goconst",
"--enable=gosec",
}
}
func getBenchLintersArgs() []string {
return append([]string{
"--enable=megacheck",
}, getBenchLintersArgsNoMegacheck()...)
}
func getGometalinterCommonArgs() []string {
return []string{
"--deadline=30m",
"--skip=testdata",
"--skip=builtin",
"--vendor",
"--cyclo-over=30",
"--dupl-threshold=150",
"--exclude", fmt.Sprintf("(%s)", strings.Join(config.GetDefaultExcludePatternsStrings(), "|")),
"--disable-all",
"--enable=vet",
"--enable=vetshadow",
}
}
func printCommand(cmd string, args ...string) {
if os.Getenv("PRINT_CMD") != "1" {
return
}
quotedArgs := []string{}
for _, a := range args {
quotedArgs = append(quotedArgs, strconv.Quote(a))
}
log.Printf("%s %s", cmd, strings.Join(quotedArgs, " "))
}
func runGometalinter(b *testing.B) {
args := []string{}
args = append(args, getGometalinterCommonArgs()...)
args = append(args, getBenchLintersArgs()...)
args = append(args, "./...")
printCommand("gometalinter", args...)
_ = exec.Command("gometalinter", args...).Run()
}
func getGolangciLintCommonArgs() []string {
return []string{"run", "--no-config", "--issues-exit-code=0", "--deadline=30m", "--disable-all", "--enable=govet"}
}
func runGolangciLintForBench(b *testing.B) {
args := getGolangciLintCommonArgs()
args = append(args, getBenchLintersArgs()...)
printCommand("golangci-lint", args...)
out, err := exec.Command("golangci-lint", args...).CombinedOutput()
if err != nil {
b.Fatalf("can't run golangci-lint: %s, %s", err, out)
}
}
func getGoLinesTotalCount(b *testing.B) int {
cmd := exec.Command("bash", "-c", `find . -name "*.go" | fgrep -v vendor | xargs wc -l | tail -1`)
out, err := cmd.CombinedOutput()
if err != nil {
b.Fatalf("can't run go lines counter: %s", err)
}
parts := bytes.Split(bytes.TrimSpace(out), []byte(" "))
n, err := strconv.Atoi(string(parts[0]))
if err != nil {
b.Fatalf("can't parse go lines count: %s", err)
}
return n
}
func getLinterMemoryMB(b *testing.B, progName string) (int, error) {
processes, err := gops.Processes()
if err != nil {
b.Fatalf("Can't get processes: %s", err)
}
var progPID int
for _, p := range processes {
if p.Executable() == progName {
progPID = p.Pid()
break
}
}
if progPID == 0 {
return 0, fmt.Errorf("no process")
}
allProgPIDs := []int{progPID}
for _, p := range processes {
if p.PPid() == progPID {
allProgPIDs = append(allProgPIDs, p.Pid())
}
}
var totalProgMemBytes uint64
for _, pid := range allProgPIDs {
p, err := process.NewProcess(int32(pid))
if err != nil {
continue // subprocess could die
}
mi, err := p.MemoryInfo()
if err != nil {
continue
}
totalProgMemBytes += mi.RSS
}
return int(totalProgMemBytes / 1024 / 1024), nil
}
func trackPeakMemoryUsage(b *testing.B, doneCh <-chan struct{}, progName string) chan int {
resCh := make(chan int)
go func() {
var peakUsedMemMB int
t := time.NewTicker(time.Millisecond * 5)
defer t.Stop()
for {
select {
case <-doneCh:
resCh <- peakUsedMemMB
close(resCh)
return
case <-t.C:
}
m, err := getLinterMemoryMB(b, progName)
if err != nil {
continue
}
if m > peakUsedMemMB {
peakUsedMemMB = m
}
}
}()
return resCh
}
type runResult struct {
peakMemMB int
duration time.Duration
}
func compare(b *testing.B, gometalinterRun, golangciLintRun func(*testing.B), repoName, mode string, kLOC int) { // nolint
gometalinterRes := runOne(b, gometalinterRun, "gometalinter")
golangciLintRes := runOne(b, golangciLintRun, "golangci-lint")
if mode != "" {
mode = " " + mode
}
log.Printf("%s (%d kLoC): golangci-lint%s: time: %s, %.1f times faster; memory: %dMB, %.1f times less",
repoName, kLOC, mode,
golangciLintRes.duration, gometalinterRes.duration.Seconds()/golangciLintRes.duration.Seconds(),
golangciLintRes.peakMemMB, float64(gometalinterRes.peakMemMB)/float64(golangciLintRes.peakMemMB),
)
}
func runOne(b *testing.B, run func(*testing.B), progName string) *runResult {
doneCh := make(chan struct{})
peakMemCh := trackPeakMemoryUsage(b, doneCh, progName)
startedAt := time.Now()
run(b)
duration := time.Since(startedAt)
close(doneCh)
peakUsedMemMB := <-peakMemCh
return &runResult{
peakMemMB: peakUsedMemMB,
duration: duration,
}
}
func BenchmarkWithGometalinter(b *testing.B) {
installBinary(b)
type bcase struct {
name string
prepare func(*testing.B)
}
bcases := []bcase{
{
name: "self repo",
prepare: prepareGithubProject("golangci", "golangci-lint"),
},
{
name: "gometalinter repo",
prepare: prepareGithubProject("alecthomas", "gometalinter"),
},
{
name: "hugo",
prepare: prepareGithubProject("gohugoio", "hugo"),
},
{
name: "go-ethereum",
prepare: prepareGithubProject("ethereum", "go-ethereum"),
},
{
name: "beego",
prepare: prepareGithubProject("astaxie", "beego"),
},
{
name: "terraform",
prepare: prepareGithubProject("hashicorp", "terraform"),
},
{
name: "consul",
prepare: prepareGithubProject("hashicorp", "consul"),
},
{
name: "go source code",
prepare: prepareGoSource,
},
}
for _, bc := range bcases {
bc.prepare(b)
lc := getGoLinesTotalCount(b)
compare(b, runGometalinter, runGolangciLintForBench, bc.name, "", lc/1000)
}
}