diff --git a/.golangci.example.yml b/.golangci.example.yml index ec4badb8..53ba5922 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -1,6 +1,4 @@ run: - args: - - ./... verbose: true concurrency: 4 deadline: 1m diff --git a/internal/commands/run.go b/internal/commands/run.go index 2d691dfd..90d1d638 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -2,11 +2,13 @@ package commands import ( "context" + "errors" "fmt" "go/build" "go/token" "log" "os" + "runtime" "strings" "time" @@ -21,6 +23,7 @@ import ( "github.com/golangci/golangci-lint/pkg/result/processors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" "golang.org/x/tools/go/loader" ) @@ -51,6 +54,8 @@ func (e *Executor) initRun() { runCmd.Flags().StringSliceVar(&rc.BuildTags, "build-tags", []string{}, "Build tags (not all linters support them)") runCmd.Flags().DurationVar(&rc.Deadline, "deadline", time.Minute, "Deadline for total work") runCmd.Flags().BoolVar(&rc.AnalyzeTests, "tests", false, "Analyze tests (*_test.go)") + runCmd.Flags().BoolVar(&rc.PrintResourcesUsage, "print-resources-usage", false, "Print avg and max memory usage of golangci-lint and total time") + runCmd.Flags().StringVarP(&rc.Config, "config", "c", "", "Read config from file path `PATH`") // Linters settings config lsc := &e.cfg.LintersSettings @@ -98,8 +103,6 @@ func (e *Executor) initRun() { runCmd.Flags().StringVar(&ic.DiffFromRevision, "new-from-rev", "", "Show only new issues created after git revision `REV`") runCmd.Flags().StringVar(&ic.DiffPatchFilePath, "new-from-patch", "", "Show only new issues created in git patch with file path `PATH`") - runCmd.Flags().StringVarP(&e.cfg.Run.Config, "config", "c", "", "Read config from file path `PATH`") - e.parseConfig(runCmd) } @@ -240,45 +243,53 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) (chan result. return runner.Run(ctx, linters, lintCtx), nil } +func (e *Executor) runAndPrint(ctx context.Context, args []string) error { + issues, err := e.runAnalysis(ctx, args) + if err != nil { + return err + } + + var p printers.Printer + if e.cfg.Output.Format == config.OutFormatJSON { + p = printers.NewJSON() + } else { + p = printers.NewText(e.cfg.Output.PrintIssuedLine, + e.cfg.Output.Format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName) + } + gotAnyIssues, err := p.Print(issues) + if err != nil { + return fmt.Errorf("can't print %d issues: %s", len(issues), err) + } + + if gotAnyIssues { + e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound + return nil + } + + return nil +} + func (e *Executor) executeRun(cmd *cobra.Command, args []string) { + needTrackResources := e.cfg.Run.IsVerbose || e.cfg.Run.PrintResourcesUsage + trackResourcesEndCh := make(chan struct{}) + defer func() { // XXX: this defer must be before ctx.cancel defer + if needTrackResources { // wait until resource tracking finished to print properly + <-trackResourcesEndCh + } + }() + ctx, cancel := context.WithTimeout(context.Background(), e.cfg.Run.Deadline) defer cancel() - defer func(startedAt time.Time) { - logrus.Infof("Run took %s", time.Since(startedAt)) - }(time.Now()) + if needTrackResources { + go watchResources(ctx, trackResourcesEndCh) + } if e.cfg.Output.PrintWelcomeMessage { fmt.Println("Run this tool in cloud on every github pull request in https://golangci.com for free (public repos)") } - f := func() error { - issues, err := e.runAnalysis(ctx, args) - if err != nil { - return err - } - - var p printers.Printer - if e.cfg.Output.Format == config.OutFormatJSON { - p = printers.NewJSON() - } else { - p = printers.NewText(e.cfg.Output.PrintIssuedLine, - e.cfg.Output.Format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName) - } - gotAnyIssues, err := p.Print(issues) - if err != nil { - return fmt.Errorf("can't print %d issues: %s", len(issues), err) - } - - if gotAnyIssues { - e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound - return nil - } - - return nil - } - - if err := f(); err != nil { + if err := e.runAndPrint(ctx, args); err != nil { log.Print(err) if e.exitCode == 0 { e.exitCode = exitCodeIfFailure @@ -289,7 +300,10 @@ func (e *Executor) executeRun(cmd *cobra.Command, args []string) { func (e *Executor) parseConfig(cmd *cobra.Command) { // XXX: hack with double parsing to acces "config" option here if err := cmd.ParseFlags(os.Args); err != nil { - log.Fatalf("Can't parse agrs: %s", err) + if err == pflag.ErrHelp { + return + } + log.Fatalf("Can't parse args: %s", err) } if err := viper.BindPFlags(cmd.Flags()); err != nil { @@ -318,4 +332,63 @@ func (e *Executor) parseConfig(cmd *cobra.Command) { if err := viper.Unmarshal(&e.cfg); err != nil { log.Fatalf("Can't unmarshal config by viper: %s", err) } + + if err := e.validateConfig(); err != nil { + log.Fatal(err) + } +} + +func (e *Executor) validateConfig() error { + c := e.cfg + if len(c.Run.Args) != 0 { + return errors.New("option run.args in config aren't supported now") + } + + if c.Run.CPUProfilePath != "" { + return errors.New("option run.cpuprofilepath in config isn't allowed") + } + + return nil +} + +func watchResources(ctx context.Context, done chan struct{}) { + startedAt := time.Now() + + rssValues := []uint64{} + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + var m runtime.MemStats + runtime.ReadMemStats(&m) + + rssValues = append(rssValues, m.Sys) + + stop := false + select { + case <-ctx.Done(): + stop = true + case <-ticker.C: // track every second + } + + if stop { + break + } + } + + var avg, max uint64 + for _, v := range rssValues { + avg += v + if v > max { + max = v + } + } + avg /= uint64(len(rssValues)) + + const MB = 1024 * 1024 + maxMB := float64(max) / MB + logrus.Infof("Memory: %d samples, avg is %.1fMB, max is %.1fMB", + len(rssValues), float64(avg)/MB, maxMB) + logrus.Infof("Execution took %s", time.Since(startedAt)) + close(done) } diff --git a/pkg/config/config.go b/pkg/config/config.go index b221144c..0e3af880 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -39,9 +39,10 @@ var DefaultExcludePatterns = []string{ } type Run struct { - IsVerbose bool `mapstructure:"verbose"` - CPUProfilePath string - Concurrency int + IsVerbose bool `mapstructure:"verbose"` + CPUProfilePath string + Concurrency int + PrintResourcesUsage bool `mapstructure:"print-resources-usage"` Config string