Skip to content

Commit 5aee21d

Browse files
author
golangci
authored
Merge pull request #8 from golangci/feature/improve-benchmark
improve benchmark
2 parents fb40512 + 65330f8 commit 5aee21d

File tree

106 files changed

+20097
-90
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+20097
-90
lines changed

Gopkg.lock

Lines changed: 18 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Gopkg.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@
7373
[[constraint]]
7474
name = "github.com/spf13/viper"
7575
version = "1.0.2"
76+
77+
[[constraint]]
78+
branch = "master"
79+
name = "github.com/mitchellh/go-ps"

README.md

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# GolangCI-Lint
22
[![Build Status](https://travis-ci.com/golangci/golangci-lint.svg?branch=master)](https://travis-ci.com/golangci/golangci-lint)
33

4-
GolangCI-Lint is a linters aggregator. It is [fast](#performance) (2-6 times faster than gometalinter), [easy to integrate and use](#issues-options), has [nice output](quick-start) and has minimum count of false positives.
4+
GolangCI-Lint is a linters aggregator. It is [fast](#performance) (2-7 times faster than gometalinter), [easy to integrate and use](#issues-options), has [nice output](quick-start) and has minimum count of false positives.
55

66
Sponsored by [GolangCI.com](https://golangci.com): SaaS service for running linters on Github pull requests. Free for Open Source.
77

@@ -94,7 +94,7 @@ $ golangci-lint run --disable-all -E errcheck
9494
# Comparison
9595
## `golangci-lint` vs `gometalinter`
9696
GolangCI-Lint was created to fix next issues with `gometalinter`:
97-
1. Slow work: `gometalinter` usually works for minutes in average projects. GolangCI-Lint works [2-6x times faster](#performance) by [reusing work](#internals).
97+
1. Slow work: `gometalinter` usually works for minutes in average projects. GolangCI-Lint works [2-7x times faster](#performance) by [reusing work](#internals).
9898
2. Huge memory consumption: parallel linters don't share the same program representation and can eat `n` times more memory (`n` - concurrency). GolangCI-Lint fixes it by sharing representation.
9999
3. Can't set honest concurrency: if you set it to `n` it can take `n+x` threads because of forced threads in specific linters. `gometalinter` can't do anything about it, because it runs linters as black-boxes in forked processes. In GolangCI-Lint we run all linters in one process and fully control them. Configured concurrency will be honest.
100100
This issue is important because often you'd like to set concurrency to CPUs count minus one to save one CPU for example for IDE. It concurrency isn't correct you will have troubles using IDE while analyzing code.
@@ -112,7 +112,9 @@ This issue is important because often you'd like to set concurrency to CPUs coun
112112
# Performance
113113
Benchmarks were executed on MacBook Pro (Retina, 13-inch, Late 2013), 2,4 GHz Intel Core i5, 8 GB 1600 MHz DDR3. It has 4 cores and concurrency for linters was default: number of cores. Benchmark runs and measures timings automatically, it's code is [here](https://github.com/golangci/golangci-lint/blob/master/pkg/enabled_linters_test.go) (`BenchmarkWithGometalinter`).
114114

115-
## Default Mode
115+
We measure peak memory usage (RSS) by measurement of processes RSS every 5 ms.
116+
117+
## Comparison with gometalinter
116118
We compare golangci-lint and gometalinter in default mode, but explicitly specify all linters to enable because of small differences in default configuration.
117119
```bash
118120
$ golangci-lint run --no-config --issues-exit-code=0 --deadline=30m \
@@ -127,38 +129,20 @@ $ gometalinter --deadline=30m --vendor --cyclo-over=30 --dupl-threshold=150 \
127129
./...
128130
```
129131

130-
| Repository | GolangCI Lint Time | GolangCI Is Faster In |
131-
| ---------- | ------------------ | --------------------- |
132-
| self repo, 4.4 kLoC | 9.1s | **6.6x** |
133-
| gometalinter repo, 3.8 kLoC | 5.1s | **4.9x** |
134-
| hugo, 69 kLoC | 12.4s | **5.8x** |
135-
| go source, 1300 kLoC | 3m15s | **1.8x** |
136-
137-
On average golangci-lint is 4.8 times faster than gometalinter. Maximum difference is in
138-
self repo: 6.6 times faster, minimum difference is in go source code repo: 1.8 times faster.
139-
140-
## Fast Mode
141-
We compare golangci-lint and gometalinter in fast mode (`--fast`), but don't use option `--fast` because it differs a little.
142-
Instead we configure common linters from this option.
143-
```bash
144-
$ golangci-lint run --no-config --issues-exit-code=0 --deadline=30m \
145-
--disable-all --enable=govet --enable=dupl --enable=goconst --enable=gocyclo --enable=golint --enable=ineffassign
146-
$ gometalinter --deadline=30m --vendor --cyclo-over=30 --dupl-threshold=150 \
147-
--exclude=<defaul golangci-lint excludes> --skip=testdata --skip=builtin \
148-
--disable-all --enable=vet --enable=vetshadow -enable=dupl --enable=goconst --enable=gocyclo --enable=golint --enable=ineffassign \
149-
./...
150-
```
151-
152-
| Repository | GolangCI Lint Time | GolangCI Is Faster In |
153-
| ---------- | ------------------ | --------------------- |
154-
| self repo, 4.4 kLoC | 0.4s | **3.1x** |
155-
| gometalinter repo, 3.8 kLoC | 0.2s | **1.9x** |
156-
| hugo, 69 kLoC | 1.6s | **4x** |
157-
| go source, 1300 kLoC | 35.4s | **1.2x** |
132+
| Repository | GolangCI Time | GolangCI Is Faster than Gometalinter | GolangCI Memory | GolangCI eats less memory than Gometalinter |
133+
| ---------- | ------------- | ------------------------------------ | --------------- | ------------------------------------------- |
134+
| gometalinter repo, 4 kLoC | 6s | **6.4x** | 0.7GB | 1.5x |
135+
| self repo, 4 kLoC | 12s | **7.5x** | 1.2GB | 1.7x |
136+
| beego, 50 kLoC | 10s | **4.2x** | 1.4GB | 1.1x |
137+
| hugo, 70 kLoC | 15s | **6.1x** | 1.6GB | 1.8x |
138+
| consul, 127 kLoC | 58s | **4x** | 2.7GB | 1.7x |
139+
| terraform, 190 kLoC | 2m13s | **1.6x** | 4.8GB | 1x |
140+
| go-ethereum, 250 kLoC | 33s | **5x** | 3.6GB | 1x |
141+
| go source, 1300 kLoC | 2m45s | **2x** | 4.7GB | 1x |
158142

159-
On average golangci-lint is 2.5 times faster than gometalinter. Maximum difference is in
160-
self repo: 3.1 times faster, minimum difference is in go source code repo: 20% faster.
161143

144+
**On average golangci-lint is 4.6 times faster** than gometalinter. Maximum difference is in
145+
self repo: **7.5 times faster**, minimum difference is in terraform source code repo: 1.8 times faster.
162146

163147
# Supported Linters
164148
To see a list of supported linters and which linters are enabled/disabled by default execute command

pkg/enabled_linters_test.go

Lines changed: 104 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import (
1414
"time"
1515

1616
"github.com/golangci/golangci-lint/pkg/config"
17-
"github.com/shirou/gopsutil/mem"
17+
gops "github.com/mitchellh/go-ps"
18+
"github.com/shirou/gopsutil/process"
1819
"github.com/sirupsen/logrus"
1920
"github.com/stretchr/testify/assert"
2021
)
@@ -125,18 +126,6 @@ func getBenchLintersArgs() []string {
125126
}, getBenchLintersArgsNoMegacheck()...)
126127
}
127128

128-
func getBenchFastLintersArgs() []string {
129-
return []string{
130-
"--enable=dupl",
131-
"--enable=goconst",
132-
"--enable=gocyclo",
133-
"--enable=golint",
134-
"--enable=ineffassign",
135-
// don't add gas because gometalinter uses old, fast and not working for me version of it.
136-
// golangci-lint uses new and slower version of it.
137-
}
138-
}
139-
140129
func getGometalinterCommonArgs() []string {
141130
return []string{
142131
"--deadline=30m",
@@ -152,19 +141,25 @@ func getGometalinterCommonArgs() []string {
152141
}
153142
}
154143

144+
func printCommand(cmd string, args ...string) {
145+
if os.Getenv("PRINT_CMD") != "1" {
146+
return
147+
}
148+
quotedArgs := []string{}
149+
for _, a := range args {
150+
quotedArgs = append(quotedArgs, strconv.Quote(a))
151+
}
152+
153+
logrus.Warnf("%s %s", cmd, strings.Join(quotedArgs, " "))
154+
}
155+
155156
func runGometalinter(b *testing.B) {
156157
args := []string{}
157158
args = append(args, getGometalinterCommonArgs()...)
158159
args = append(args, getBenchLintersArgs()...)
159160
args = append(args, "./...")
160-
_ = exec.Command("gometalinter", args...).Run()
161-
}
162161

163-
func runGometalinterFast(b *testing.B) {
164-
args := []string{}
165-
args = append(args, getGometalinterCommonArgs()...)
166-
args = append(args, getBenchFastLintersArgs()...)
167-
args = append(args, "./...")
162+
printCommand("gometalinter", args...)
168163
_ = exec.Command("gometalinter", args...).Run()
169164
}
170165

@@ -175,15 +170,7 @@ func getGolangciLintCommonArgs() []string {
175170
func runGolangciLint(b *testing.B) {
176171
args := getGolangciLintCommonArgs()
177172
args = append(args, getBenchLintersArgs()...)
178-
out, err := exec.Command("golangci-lint", args...).CombinedOutput()
179-
if err != nil {
180-
b.Fatalf("can't run golangci-lint: %s, %s", err, out)
181-
}
182-
}
183-
184-
func runGolangciLintFast(b *testing.B) {
185-
args := getGolangciLintCommonArgs()
186-
args = append(args, getBenchFastLintersArgs()...)
173+
printCommand("golangci-lint", args...)
187174
out, err := exec.Command("golangci-lint", args...).CombinedOutput()
188175
if err != nil {
189176
b.Fatalf("can't run golangci-lint: %s, %s", err, out)
@@ -206,20 +193,53 @@ func getGoLinesTotalCount(b *testing.B) int {
206193
return n
207194
}
208195

209-
func getUsedMemoryMb(b *testing.B) int {
210-
v, err := mem.VirtualMemory()
196+
func getLinterMemoryMB(b *testing.B, progName string) (int, error) {
197+
processes, err := gops.Processes()
211198
if err != nil {
212-
b.Fatalf("can't get usedmemory: %s", err)
199+
b.Fatalf("Can't get processes: %s", err)
200+
}
201+
202+
var progPID int
203+
for _, p := range processes {
204+
if p.Executable() == progName {
205+
progPID = p.Pid()
206+
break
207+
}
213208
}
209+
if progPID == 0 {
210+
return 0, fmt.Errorf("no process")
211+
}
212+
213+
allProgPIDs := []int{progPID}
214+
for _, p := range processes {
215+
if p.PPid() == progPID {
216+
allProgPIDs = append(allProgPIDs, p.Pid())
217+
}
218+
}
219+
220+
var totalProgMemBytes uint64
221+
for _, pid := range allProgPIDs {
222+
p, err := process.NewProcess(int32(pid))
223+
if err != nil {
224+
continue // subprocess could die
225+
}
226+
227+
mi, err := p.MemoryInfo()
228+
if err != nil {
229+
continue
230+
}
214231

215-
return int(v.Used / 1024 / 1024)
232+
totalProgMemBytes += mi.RSS
233+
}
234+
235+
return int(totalProgMemBytes / 1024 / 1024), nil
216236
}
217237

218-
func trackPeakMemoryUsage(b *testing.B, doneCh chan struct{}) chan int {
238+
func trackPeakMemoryUsage(b *testing.B, doneCh <-chan struct{}, progName string) chan int {
219239
resCh := make(chan int)
220240
go func() {
221241
var peakUsedMemMB int
222-
t := time.NewTicker(time.Millisecond * 50)
242+
t := time.NewTicker(time.Millisecond * 5)
223243
defer t.Stop()
224244

225245
for {
@@ -231,7 +251,10 @@ func trackPeakMemoryUsage(b *testing.B, doneCh chan struct{}) chan int {
231251
case <-t.C:
232252
}
233253

234-
m := getUsedMemoryMb(b)
254+
m, err := getLinterMemoryMB(b, progName)
255+
if err != nil {
256+
continue
257+
}
235258
if m > peakUsedMemMB {
236259
peakUsedMemMB = m
237260
}
@@ -240,24 +263,38 @@ func trackPeakMemoryUsage(b *testing.B, doneCh chan struct{}) chan int {
240263
return resCh
241264
}
242265

243-
func runBench(b *testing.B, run func(*testing.B), format string, args ...interface{}) {
244-
startUsedMemMB := getUsedMemoryMb(b)
266+
type runResult struct {
267+
peakMemMB int
268+
duration time.Duration
269+
}
270+
271+
func compare(b *testing.B, gometalinterRun, golangciLintRun func(*testing.B), repoName, mode string, kLOC int) { // nolint
272+
gometalinterRes := runOne(b, gometalinterRun, "gometalinter")
273+
golangciLintRes := runOne(b, golangciLintRun, "golangci-lint")
274+
275+
if mode != "" {
276+
mode = " " + mode
277+
}
278+
logrus.Warnf("%s (%d kLoC): golangci-lint%s: time: %s, %.1f times faster; memory: %dMB, %.1f times less",
279+
repoName, kLOC, mode,
280+
golangciLintRes.duration, gometalinterRes.duration.Seconds()/golangciLintRes.duration.Seconds(),
281+
golangciLintRes.peakMemMB, float64(gometalinterRes.peakMemMB)/float64(golangciLintRes.peakMemMB),
282+
)
283+
}
284+
285+
func runOne(b *testing.B, run func(*testing.B), progName string) *runResult {
245286
doneCh := make(chan struct{})
246-
peakMemCh := trackPeakMemoryUsage(b, doneCh)
247-
name := fmt.Sprintf(format, args...)
248-
b.Run(name, func(b *testing.B) {
249-
for i := 0; i < b.N; i++ {
250-
run(b)
251-
}
252-
})
287+
peakMemCh := trackPeakMemoryUsage(b, doneCh, progName)
288+
startedAt := time.Now()
289+
run(b)
290+
duration := time.Since(startedAt)
253291
close(doneCh)
292+
254293
peakUsedMemMB := <-peakMemCh
255-
var linterPeakMemUsage int
256-
if peakUsedMemMB > startUsedMemMB {
257-
linterPeakMemUsage = peakUsedMemMB - startUsedMemMB
294+
return &runResult{
295+
peakMemMB: peakUsedMemMB,
296+
duration: duration,
258297
}
259-
logrus.Warnf("%s: start used mem is %dMB, peak used mem is %dMB, linter peak mem usage is %dMB",
260-
name, startUsedMemMB, peakUsedMemMB, linterPeakMemUsage)
261298
}
262299

263300
func BenchmarkWithGometalinter(b *testing.B) {
@@ -280,6 +317,22 @@ func BenchmarkWithGometalinter(b *testing.B) {
280317
name: "hugo",
281318
prepare: prepareGithubProject("gohugoio", "hugo"),
282319
},
320+
{
321+
name: "go-ethereum",
322+
prepare: prepareGithubProject("ethereum", "go-ethereum"),
323+
},
324+
{
325+
name: "beego",
326+
prepare: prepareGithubProject("astaxie", "beego"),
327+
},
328+
{
329+
name: "terraform",
330+
prepare: prepareGithubProject("hashicorp", "terraform"),
331+
},
332+
{
333+
name: "consul",
334+
prepare: prepareGithubProject("hashicorp", "consul"),
335+
},
283336
{
284337
name: "go source code",
285338
prepare: prepareGoSource,
@@ -289,10 +342,6 @@ func BenchmarkWithGometalinter(b *testing.B) {
289342
bc.prepare(b)
290343
lc := getGoLinesTotalCount(b)
291344

292-
runBench(b, runGometalinterFast, "%s/gometalinter --fast (%d lines of code)", bc.name, lc)
293-
runBench(b, runGolangciLintFast, "%s/golangci-lint fast (%d lines of code)", bc.name, lc)
294-
295-
runBench(b, runGometalinter, "%s/gometalinter (%d lines of code)", bc.name, lc)
296-
runBench(b, runGolangciLint, "%s/golangci-lint (%d lines of code)", bc.name, lc)
345+
compare(b, runGometalinter, runGolangciLint, bc.name, "", lc/1000)
297346
}
298347
}

vendor/github.com/mitchellh/go-ps/.gitignore

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/mitchellh/go-ps/.travis.yml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)