github.com/thrasher-corp/golangci-lint@v1.17.3/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 "github.com/golangci/golangci-lint/test/testshared" 17 18 gops "github.com/mitchellh/go-ps" 19 "github.com/shirou/gopsutil/process" 20 21 "github.com/golangci/golangci-lint/pkg/config" 22 ) 23 24 func chdir(b *testing.B, dir string) { 25 if err := os.Chdir(dir); err != nil { 26 b.Fatalf("can't chdir to %s: %s", dir, err) 27 } 28 } 29 30 func prepareGoSource(b *testing.B) { 31 chdir(b, filepath.Join(build.Default.GOROOT, "src")) 32 } 33 34 func prepareGithubProject(owner, name string) func(*testing.B) { 35 return func(b *testing.B) { 36 dir := filepath.Join(build.Default.GOPATH, "src", "github.com", owner, name) 37 _, err := os.Stat(dir) 38 if os.IsNotExist(err) { 39 err = exec.Command("git", "clone", fmt.Sprintf("https://github.com/%s/%s.git", owner, name)).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) { // nolint 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 }