Skip to content

Commit aaf3c58

Browse files
author
golangci
authored
Merge pull request #7 from golangci/feature/benchmarks-and-readme
Fill README section about performance
2 parents 3c2ca7b + ab0ce75 commit aaf3c58

File tree

8 files changed

+157
-63
lines changed

8 files changed

+157
-63
lines changed

README.md

+87-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,29 @@ Sponsored by [GolangCI.com](https://golangci.com): SaaS service for running lint
77

88
<a href="https://golangci.com/"><img src="docs/go.png" width="250px"></a>
99

10-
// TOC Here at the end
10+
* [GolangCI-Lint](#golangci-lint)
11+
* [Install](#install)
12+
* [Quick Start](#quick-start)
13+
* [Comparison](#comparison)
14+
* [gometalinter](#gometalinter)
15+
* [Run Needed Linters Manually](#run-needed-linters-manually)
16+
* [Performance](#performance)
17+
* [Default Mode](#default-mode)
18+
* [Fast Mode](#fast-mode)
19+
* [Supported Linters](#supported-linters)
20+
* [Enabled By Default Linters](#enabled-by-default-linters)
21+
* [Disabled By Default Linters (-E/--enable)](#disabled-by-default-linters--e--enable)
22+
* [Configuration](#configuration)
23+
* [Command-Line Options](#command-line-options)
24+
* [Run Options](#run-options)
25+
* [Linters](#linters)
26+
* [Linters Options](#linters-options)
27+
* [Issues Options](#issues-options)
28+
* [Output Options](#output-options)
29+
* [Configuration File](#configuration-file)
30+
* [False Positives](#false-positives)
31+
* [FAQ](#faq)
32+
* [Internals](#internals)
1133

1234
# Install
1335
```bash
@@ -73,7 +95,7 @@ $ golangci-lint run --disable-all -E errcheck
7395
# Comparison
7496
## `gometalinter`
7597
GolangCI-Lint was created to fix next issues with `gometalinter`:
76-
1. Slow work: `gometalinter` usually works for minutes in average projects. GolangCI-Lint works [2-10x times faster](#benchmarks) by [reusing work](#internals).
98+
1. Slow work: `gometalinter` usually works for minutes in average projects. GolangCI-Lint works [2-6x times faster](#benchmarks) by [reusing work](#internals).
7799
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.
78100
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.
79101
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.
@@ -89,14 +111,53 @@ This issue is important because often you'd like to set concurrency to CPUs coun
89111
3. It will take more time because of different usages and need of tracking of version of `n` linters.
90112

91113
# Performance
92-
## Benchmarks
114+
## Default Mode
115+
We compare golangci-lint and gometalinter in default mode, but explicitly specify all linters to enable because of small differences in default configuration.
116+
```bash
117+
$ golangci-lint run --no-config --issues-exit-code=0 --deadline=30m \
118+
--disable-all --enable=deadcode --enable=gocyclo --enable=golint --enable=varcheck \
119+
--enable=structcheck --enable=maligned --enable=errcheck --enable=dupl --enable=ineffassign \
120+
--enable=interfacer --enable=unconvert --enable=goconst --enable=gas --enable=megacheck
121+
$ gometalinter --deadline=30m --vendor --cyclo-over=30 --dupl-threshold=150 \
122+
--exclude=<defaul golangci-lint excludes> --skip=testdata --skip=builtin \
123+
--disable-all --enable=deadcode --enable=gocyclo --enable=golint --enable=varcheck \
124+
--enable=structcheck --enable=maligned --enable=errcheck --enable=dupl --enable=ineffassign \
125+
--enable=interfacer --enable=unconvert --enable=goconst --enable=gas --enable=megacheck
126+
./...
93127
```
94-
BenchmarkWithGometalinter/self_repo/gometalinter_--fast_(4098_lines_of_code)-4 30 1482617961 ns/op
95-
BenchmarkWithGometalinter/self_repo/golangci-lint_fast_(4098_lines_of_code)-4 100 414381899 ns/op
96-
BenchmarkWithGometalinter/self_repo/gometalinter_(4098_lines_of_code)-4 1 39304954722 ns/op
97-
BenchmarkWithGometalinter/self_repo/golangci-lint_(4098_lines_of_code)-4 5 8290405036 ns/op
128+
129+
| Repository | GolangCI Lint Time | GolangCI Is Faster In |
130+
| ---------- | ------------------ | --------------------- |
131+
| self repo, 4.4 kLoC | 9.1s | 6.6x |
132+
| gometalinter repo, 3.8 kLoC | 5.1s | 4.9x |
133+
| hugo, 69 kLoC | 12.4s | 5.8x |
134+
| go source, 1300 kLoC | 3m15s | 1.8x |
135+
136+
On average golangci-lint is 4.8 times faster than gometalinter. Maximum difference is in
137+
self repo: 6.6 times faster, minimum difference is in go source code repo: 1.8 faster.
138+
139+
## Fast Mode
140+
We compare golangci-lint and gometalinter in fast mode (`--fast`), but don't use option `--fast` because it differs a little.
141+
Instead we configure common linters from this option.
142+
```bash
143+
$ golangci-lint run --no-config --issues-exit-code=0 --deadline=30m \
144+
--disable-all --enable=govet --enable=dupl --enable=goconst --enable=gocyclo --enable=golint --enable=ineffassign
145+
$ gometalinter --deadline=30m --vendor --cyclo-over=30 --dupl-threshold=150 \
146+
--exclude=<defaul golangci-lint excludes> --skip=testdata --skip=builtin \
147+
--disable-all --enable=vet --enable=vetshadow -enable=dupl --enable=goconst --enable=gocyclo --enable=golint --enable=ineffassign \
148+
./...
98149
```
99-
## Internals
150+
151+
| Repository | GolangCI Lint Time | GolangCI Is Faster In |
152+
| ---------- | ------------------ | --------------------- |
153+
| self repo, 4.4 kLoC | 0.4s | 3.1x |
154+
| gometalinter repo, 3.8 kLoC | 0.2s | 1.9x |
155+
| hugo, 69 kLoC | 1.6s | 4x |
156+
| go source, 1300 kLoC | 35.4s | 1.17x |
157+
158+
On average golangci-lint is 2.5 times faster than gometalinter. Maximum difference is in
159+
self repo: 3.1 times faster, minimum difference is in go source code repo: 17% faster.
160+
100161

101162
# Supported Linters
102163
To see a list of supported linters and which linters are enabled/disabled by default execute command
@@ -261,3 +322,21 @@ A: You have 2 choices:
261322
1. Update it: `go get -u gopkg.in/golangci/golangci-lint.v1/cmd/golangci-lint`
262323
2. Run it with `-v` option and check output.
263324
3. If it doesn't help create [GitHub issue](https://github.com/golangci/golangci-lint/issues/new).
325+
326+
# Internals
327+
Key difference with gometalinter is that golangci-lint shares work between specific linters (golint, govet, ...).
328+
For small and medium projects 50-80% of work between linters can be reused.
329+
Now we share `loader.Program` and `SSA` representation building. `SSA` representation is used from
330+
a [fork of go-tools](https://github.com/dominikh/go-tools), not the official one. Also we are going to
331+
reuse `AST` parsing and traversal.
332+
333+
We don't fork to call specific linter but use it's API. We forked github repos of almost all linters
334+
to make API. It also allows us to be more performant and control actual count of used threads.
335+
336+
All linters are vendored in `/vendor` folder: their version is fixed, they are builtin
337+
and you don't need to install them separately.
338+
339+
We use chains for issues and independent processors to post-process them: exclude issues by limits,
340+
nolint comment, diff, regexps; prettify paths etc.
341+
342+
We use `cobra` for command-line action.

pkg/commands/root.go

+12
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,23 @@ func (e *Executor) initRoot() {
4141
if e.cfg.Run.CPUProfilePath != "" {
4242
pprof.StopCPUProfile()
4343
}
44+
if e.cfg.Run.MemProfilePath != "" {
45+
f, err := os.Create(e.cfg.Run.MemProfilePath)
46+
if err != nil {
47+
log.Fatal(err)
48+
}
49+
runtime.GC() // get up-to-date statistics
50+
if err := pprof.WriteHeapProfile(f); err != nil {
51+
log.Fatal("could not write memory profile: ", err)
52+
}
53+
}
54+
4455
os.Exit(e.exitCode)
4556
},
4657
}
4758
rootCmd.PersistentFlags().BoolVarP(&e.cfg.Run.IsVerbose, "verbose", "v", false, "verbose output")
4859
rootCmd.PersistentFlags().StringVar(&e.cfg.Run.CPUProfilePath, "cpu-profile-path", "", "Path to CPU profile output file")
60+
rootCmd.PersistentFlags().StringVar(&e.cfg.Run.MemProfilePath, "mem-profile-path", "", "Path to memory profile output file")
4961
rootCmd.PersistentFlags().IntVarP(&e.cfg.Run.Concurrency, "concurrency", "j", runtime.NumCPU(), "Concurrency")
5062

5163
e.rootCmd = rootCmd

pkg/commands/run.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func (e *Executor) initRun() {
5656
runCmd.Flags().BoolVar(&rc.AnalyzeTests, "tests", false, "Analyze tests (*_test.go)")
5757
runCmd.Flags().BoolVar(&rc.PrintResourcesUsage, "print-resources-usage", false, "Print avg and max memory usage of golangci-lint and total time")
5858
runCmd.Flags().StringVarP(&rc.Config, "config", "c", "", "Read config from file path `PATH`")
59+
runCmd.Flags().BoolVar(&rc.NoConfig, "no-config", false, "Don't read config")
5960

6061
// Linters settings config
6162
lsc := &e.cfg.LintersSettings
@@ -315,14 +316,26 @@ func (e *Executor) parseConfig(cmd *cobra.Command) {
315316
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
316317
viper.AutomaticEnv()
317318

318-
configFile := viper.GetString("config")
319+
configFile := e.cfg.Run.Config
320+
if e.cfg.Run.NoConfig && configFile != "" {
321+
log.Fatal("can't combine option --config and --no-config")
322+
}
323+
324+
if e.cfg.Run.NoConfig {
325+
return
326+
}
327+
319328
if configFile == "" {
320329
viper.SetConfigName(".golangci")
321330
viper.AddConfigPath("./")
322331
} else {
323332
viper.SetConfigFile(configFile)
324333
}
325334

335+
e.parseConfigImpl()
336+
}
337+
338+
func (e *Executor) parseConfigImpl() {
326339
commandLineConfig := *e.cfg // make copy
327340

328341
if err := viper.ReadInConfig(); err != nil {
@@ -351,6 +364,10 @@ func (e *Executor) validateConfig(commandLineConfig *config.Config) error {
351364
return errors.New("option run.cpuprofilepath in config isn't allowed")
352365
}
353366

367+
if commandLineConfig.Run.MemProfilePath == "" && c.Run.MemProfilePath != "" {
368+
return errors.New("option run.memprofilepath in config isn't allowed")
369+
}
370+
354371
if !commandLineConfig.Run.IsVerbose && c.Run.IsVerbose {
355372
return errors.New("can't set run.verbose option with config: only on command-line")
356373
}

pkg/config/config.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ var DefaultExcludePatterns = []string{
4141
type Run struct {
4242
IsVerbose bool `mapstructure:"verbose"`
4343
CPUProfilePath string
44+
MemProfilePath string
4445
Concurrency int
4546
PrintResourcesUsage bool `mapstructure:"print-resources-usage"`
4647

47-
Config string
48+
Config string
49+
NoConfig bool
4850

4951
Args []string
5052

pkg/enabled_linters_test.go

+25-48
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/golangci/golangci-lint/pkg/config"
1717
"github.com/shirou/gopsutil/mem"
18+
"github.com/sirupsen/logrus"
1819
"github.com/stretchr/testify/assert"
1920
)
2021

@@ -136,80 +137,59 @@ func getBenchFastLintersArgs() []string {
136137
}
137138
}
138139

139-
func runGometalinter(b *testing.B) {
140-
args := []string{"--disable-all", "--deadline=30m"}
141-
args = append(args, getBenchLintersArgs()...)
142-
args = append(args,
143-
"--enable=vet",
144-
"--enable=vetshadow",
140+
func getGometalinterCommonArgs() []string {
141+
return []string{
142+
"--deadline=30m",
143+
"--skip=testdata",
144+
"--skip=builtin",
145145
"--vendor",
146146
"--cyclo-over=30",
147147
"--dupl-threshold=150",
148148
"--exclude", fmt.Sprintf("(%s)", strings.Join(config.DefaultExcludePatterns, "|")),
149-
"./...",
150-
)
149+
"--disable-all",
150+
"--enable=vet",
151+
"--enable=vetshadow",
152+
}
153+
}
154+
155+
func runGometalinter(b *testing.B) {
156+
args := []string{}
157+
args = append(args, getGometalinterCommonArgs()...)
158+
args = append(args, getBenchLintersArgs()...)
159+
args = append(args, "./...")
151160
_ = exec.Command("gometalinter", args...).Run()
152161
}
153162

154163
func runGometalinterFast(b *testing.B) {
155-
args := []string{"--disable-all", "--deadline=30m"}
164+
args := []string{}
165+
args = append(args, getGometalinterCommonArgs()...)
156166
args = append(args, getBenchFastLintersArgs()...)
157-
args = append(args,
158-
"--enable=vet",
159-
"--enable=vetshadow",
160-
"--vendor",
161-
"--cyclo-over=30",
162-
"--dupl-threshold=150",
163-
"--exclude", fmt.Sprintf("(%s)", strings.Join(config.DefaultExcludePatterns, "|")),
164-
"./...",
165-
)
167+
args = append(args, "./...")
166168
_ = exec.Command("gometalinter", args...).Run()
167169
}
168170

169-
func runGometalinterNoMegacheck(b *testing.B) {
170-
args := []string{"--disable-all", "--deadline=30m"}
171-
args = append(args, getBenchLintersArgsNoMegacheck()...)
172-
args = append(args,
173-
"--enable=vet",
174-
"--enable=vetshadow",
175-
"--vendor",
176-
"--cyclo-over=30",
177-
"--dupl-threshold=150",
178-
"--exclude", fmt.Sprintf("(%s)", strings.Join(config.DefaultExcludePatterns, "|")),
179-
"./...",
180-
)
181-
_ = exec.Command("gometalinter", args...).Run()
171+
func getGolangciLintCommonArgs() []string {
172+
return []string{"run", "--no-config", "--issues-exit-code=0", "--deadline=30m", "--disable-all", "--enable=govet"}
182173
}
183174

184175
func runGolangciLint(b *testing.B) {
185-
args := []string{"run", "--issues-exit-code=0", "--disable-all", "--deadline=30m", "--enable=govet"}
176+
args := getGolangciLintCommonArgs()
186177
args = append(args, getBenchLintersArgs()...)
187-
b.Logf("golangci-lint %s", strings.Join(args, " "))
188178
out, err := exec.Command("golangci-lint", args...).CombinedOutput()
189179
if err != nil {
190180
b.Fatalf("can't run golangci-lint: %s, %s", err, out)
191181
}
192182
}
193183

194184
func runGolangciLintFast(b *testing.B) {
195-
args := []string{"run", "--issues-exit-code=0", "--disable-all", "--deadline=30m", "--enable=govet"}
185+
args := getGolangciLintCommonArgs()
196186
args = append(args, getBenchFastLintersArgs()...)
197187
out, err := exec.Command("golangci-lint", args...).CombinedOutput()
198188
if err != nil {
199189
b.Fatalf("can't run golangci-lint: %s, %s", err, out)
200190
}
201191
}
202192

203-
func runGolangciLintNoMegacheck(b *testing.B) {
204-
args := []string{"run", "--issues-exit-code=0", "--disable-all", "--deadline=30m", "--enable=govet"}
205-
args = append(args, getBenchLintersArgsNoMegacheck()...)
206-
b.Logf("golangci-lint %s", strings.Join(args, " "))
207-
out, err := exec.Command("golangci-lint", args...).CombinedOutput()
208-
if err != nil {
209-
b.Fatalf("can't run golangci-lint: %s, %s", err, out)
210-
}
211-
}
212-
213193
func getGoLinesTotalCount(b *testing.B) int {
214194
cmd := exec.Command("bash", "-c", `find . -name "*.go" | fgrep -v vendor | xargs wc -l | tail -1`)
215195
out, err := cmd.CombinedOutput()
@@ -276,7 +256,7 @@ func runBench(b *testing.B, run func(*testing.B), format string, args ...interfa
276256
if peakUsedMemMB > startUsedMemMB {
277257
linterPeakMemUsage = peakUsedMemMB - startUsedMemMB
278258
}
279-
b.Logf("%s: start used mem is %dMB, peak used mem is %dMB, linter peak mem usage is %dMB",
259+
logrus.Warnf("%s: start used mem is %dMB, peak used mem is %dMB, linter peak mem usage is %dMB",
280260
name, startUsedMemMB, peakUsedMemMB, linterPeakMemUsage)
281261
}
282262

@@ -314,8 +294,5 @@ func BenchmarkWithGometalinter(b *testing.B) {
314294

315295
runBench(b, runGometalinter, "%s/gometalinter (%d lines of code)", bc.name, lc)
316296
runBench(b, runGolangciLint, "%s/golangci-lint (%d lines of code)", bc.name, lc)
317-
318-
runBench(b, runGometalinterNoMegacheck, "%s/gometalinter wo megacheck (%d lines of code)", bc.name, lc)
319-
runBench(b, runGolangciLintNoMegacheck, "%s/golangci-lint wo megacheck (%d lines of code)", bc.name, lc)
320297
}
321298
}

pkg/golinters/golint.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/golang/lint"
99
"github.com/golangci/golangci-lint/pkg/result"
10+
"github.com/sirupsen/logrus"
1011
)
1112

1213
type Golint struct{}
@@ -21,14 +22,18 @@ func (Golint) Desc() string {
2122

2223
func (g Golint) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
2324
var issues []result.Issue
25+
var lintErr error
2426
for _, pkgFiles := range lintCtx.Paths.FilesGrouppedByDirs() {
2527
i, err := g.lintFiles(lintCtx.Settings().Golint.MinConfidence, pkgFiles...)
2628
if err != nil {
27-
// TODO: skip and warn
28-
return nil, fmt.Errorf("can't lint files %s: %s", lintCtx.Paths.Files, err)
29+
lintErr = err
30+
continue
2931
}
3032
issues = append(issues, i...)
3133
}
34+
if lintErr != nil {
35+
logrus.Warnf("golint: %s", lintErr)
36+
}
3237

3338
return issues, nil
3439
}

pkg/runner.go

-2
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,6 @@ func setOutputToDevNull() (savedStdout, savedStderr *os.File) {
203203
}
204204

205205
func (r SimpleRunner) Run(ctx context.Context, linters []Linter, lintCtx *golinters.Context) <-chan result.Issue {
206-
defer timeutils.NewStopwatch("runner").Print()
207-
208206
lintResultsCh := r.runWorkers(ctx, lintCtx, linters)
209207
processedLintResultsCh := r.processLintResults(ctx, lintResultsCh)
210208
if ctx.Err() != nil {

pkg/timeutils/stopwatch.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ func (s *Stopwatch) Print() {
6161
}
6262

6363
func (s *Stopwatch) PrintStages() {
64-
logrus.Infof("%s %s", s.name, s.sprintStages())
64+
var stagesDuration time.Duration
65+
for _, s := range s.stages {
66+
stagesDuration += s
67+
}
68+
logrus.Infof("%s took %s with %s", s.name, stagesDuration, s.sprintStages())
6569
}
6670

6771
func (s *Stopwatch) TrackStage(name string, f func()) {

0 commit comments

Comments
 (0)