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 }