github.com/elek/golangci-lint@v1.42.2-0.20211208090441-c05b7fcb3a9a/pkg/commands/run.go (about) 1 package commands 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "os" 9 "runtime" 10 "strings" 11 "time" 12 13 "github.com/fatih/color" 14 "github.com/pkg/errors" 15 "github.com/spf13/cobra" 16 "github.com/spf13/pflag" 17 18 "github.com/elek/golangci-lint/pkg/config" 19 "github.com/elek/golangci-lint/pkg/exitcodes" 20 "github.com/elek/golangci-lint/pkg/lint" 21 "github.com/elek/golangci-lint/pkg/lint/lintersdb" 22 "github.com/elek/golangci-lint/pkg/logutils" 23 "github.com/elek/golangci-lint/pkg/packages" 24 "github.com/elek/golangci-lint/pkg/printers" 25 "github.com/elek/golangci-lint/pkg/result" 26 "github.com/elek/golangci-lint/pkg/result/processors" 27 ) 28 29 func getDefaultIssueExcludeHelp() string { 30 parts := []string{"Use or not use default excludes:"} 31 for _, ep := range config.DefaultExcludePatterns { 32 parts = append(parts, 33 fmt.Sprintf(" # %s %s: %s", ep.ID, ep.Linter, ep.Why), 34 fmt.Sprintf(" - %s", color.YellowString(ep.Pattern)), 35 "", 36 ) 37 } 38 return strings.Join(parts, "\n") 39 } 40 41 func getDefaultDirectoryExcludeHelp() string { 42 parts := []string{"Use or not use default excluded directories:"} 43 for _, dir := range packages.StdExcludeDirRegexps { 44 parts = append(parts, fmt.Sprintf(" - %s", color.YellowString(dir))) 45 } 46 parts = append(parts, "") 47 return strings.Join(parts, "\n") 48 } 49 50 func wh(text string) string { 51 return color.GreenString(text) 52 } 53 54 const defaultTimeout = time.Minute 55 56 //nolint:funlen 57 func initFlagSet(fs *pflag.FlagSet, cfg *config.Config, m *lintersdb.Manager, isFinalInit bool) { 58 hideFlag := func(name string) { 59 if err := fs.MarkHidden(name); err != nil { 60 panic(err) 61 } 62 63 // we run initFlagSet multiple times, but we wouldn't like to see deprecation message multiple times 64 if isFinalInit { 65 const deprecateMessage = "flag will be removed soon, please, use .golangci.yml config" 66 if err := fs.MarkDeprecated(name, deprecateMessage); err != nil { 67 panic(err) 68 } 69 } 70 } 71 72 // Output config 73 oc := &cfg.Output 74 fs.StringVar(&oc.Format, "out-format", 75 config.OutFormatColoredLineNumber, 76 wh(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|")))) 77 fs.BoolVar(&oc.PrintIssuedLine, "print-issued-lines", true, wh("Print lines of code with issue")) 78 fs.BoolVar(&oc.PrintLinterName, "print-linter-name", true, wh("Print linter name in issue line")) 79 fs.BoolVar(&oc.UniqByLine, "uniq-by-line", true, wh("Make issues output unique by line")) 80 fs.BoolVar(&oc.SortResults, "sort-results", false, wh("Sort linter results")) 81 fs.BoolVar(&oc.PrintWelcomeMessage, "print-welcome", false, wh("Print welcome message")) 82 fs.StringVar(&oc.PathPrefix, "path-prefix", "", wh("Path prefix to add to output")) 83 hideFlag("print-welcome") // no longer used 84 85 fs.BoolVar(&cfg.InternalCmdTest, "internal-cmd-test", false, wh("Option is used only for testing golangci-lint command, don't use it")) 86 if err := fs.MarkHidden("internal-cmd-test"); err != nil { 87 panic(err) 88 } 89 90 // Run config 91 rc := &cfg.Run 92 fs.StringVar(&rc.ModulesDownloadMode, "modules-download-mode", "", 93 "Modules download mode. If not empty, passed as -mod=<mode> to go tools") 94 fs.IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code", 95 exitcodes.IssuesFound, wh("Exit code when issues were found")) 96 fs.StringSliceVar(&rc.BuildTags, "build-tags", nil, wh("Build tags")) 97 98 fs.DurationVar(&rc.Timeout, "deadline", defaultTimeout, wh("Deadline for total work")) 99 if err := fs.MarkHidden("deadline"); err != nil { 100 panic(err) 101 } 102 fs.DurationVar(&rc.Timeout, "timeout", defaultTimeout, wh("Timeout for total work")) 103 104 fs.BoolVar(&rc.AnalyzeTests, "tests", true, wh("Analyze tests (*_test.go)")) 105 fs.BoolVar(&rc.PrintResourcesUsage, "print-resources-usage", false, 106 wh("Print avg and max memory usage of golangci-lint and total time")) 107 fs.StringVarP(&rc.Config, "config", "c", "", wh("Read config from file path `PATH`")) 108 fs.BoolVar(&rc.NoConfig, "no-config", false, wh("Don't read config")) 109 fs.StringSliceVar(&rc.SkipDirs, "skip-dirs", nil, wh("Regexps of directories to skip")) 110 fs.BoolVar(&rc.UseDefaultSkipDirs, "skip-dirs-use-default", true, getDefaultDirectoryExcludeHelp()) 111 fs.StringSliceVar(&rc.SkipFiles, "skip-files", nil, wh("Regexps of files to skip")) 112 113 const allowParallelDesc = "Allow multiple parallel golangci-lint instances running. " + 114 "If false (default) - golangci-lint acquires file lock on start." 115 fs.BoolVar(&rc.AllowParallelRunners, "allow-parallel-runners", false, wh(allowParallelDesc)) 116 const allowSerialDesc = "Allow multiple golangci-lint instances running, but serialize them around a lock. " + 117 "If false (default) - golangci-lint exits with an error if it fails to acquire file lock on start." 118 fs.BoolVar(&rc.AllowSerialRunners, "allow-serial-runners", false, wh(allowSerialDesc)) 119 120 // Linters settings config 121 lsc := &cfg.LintersSettings 122 123 // Hide all linters settings flags: they were initially visible, 124 // but when number of linters started to grow it became obvious that 125 // we can't fill 90% of flags by linters settings: common flags became hard to find. 126 // New linters settings should be done only through config file. 127 fs.BoolVar(&lsc.Errcheck.CheckTypeAssertions, "errcheck.check-type-assertions", 128 false, "Errcheck: check for ignored type assertion results") 129 hideFlag("errcheck.check-type-assertions") 130 fs.BoolVar(&lsc.Errcheck.CheckAssignToBlank, "errcheck.check-blank", false, 131 "Errcheck: check for errors assigned to blank identifier: _ = errFunc()") 132 hideFlag("errcheck.check-blank") 133 fs.StringVar(&lsc.Errcheck.Exclude, "errcheck.exclude", "", 134 "Path to a file containing a list of functions to exclude from checking") 135 hideFlag("errcheck.exclude") 136 fs.StringVar(&lsc.Errcheck.Ignore, "errcheck.ignore", "fmt:.*", 137 `Comma-separated list of pairs of the form pkg:regex. The regex is used to ignore names within pkg`) 138 hideFlag("errcheck.ignore") 139 140 fs.BoolVar(&lsc.Govet.CheckShadowing, "govet.check-shadowing", false, 141 "Govet: check for shadowed variables") 142 hideFlag("govet.check-shadowing") 143 144 fs.Float64Var(&lsc.Golint.MinConfidence, "golint.min-confidence", 0.8, 145 "Golint: minimum confidence of a problem to print it") 146 hideFlag("golint.min-confidence") 147 148 fs.BoolVar(&lsc.Gofmt.Simplify, "gofmt.simplify", true, "Gofmt: simplify code") 149 hideFlag("gofmt.simplify") 150 151 fs.IntVar(&lsc.Gocyclo.MinComplexity, "gocyclo.min-complexity", 152 30, "Minimal complexity of function to report it") 153 hideFlag("gocyclo.min-complexity") 154 155 fs.BoolVar(&lsc.Maligned.SuggestNewOrder, "maligned.suggest-new", false, 156 "Maligned: print suggested more optimal struct fields ordering") 157 hideFlag("maligned.suggest-new") 158 159 fs.IntVar(&lsc.Dupl.Threshold, "dupl.threshold", 160 150, "Dupl: Minimal threshold to detect copy-paste") 161 hideFlag("dupl.threshold") 162 163 fs.BoolVar(&lsc.Goconst.MatchWithConstants, "goconst.match-constant", 164 true, "Goconst: look for existing constants matching the values") 165 hideFlag("goconst.match-constant") 166 fs.IntVar(&lsc.Goconst.MinStringLen, "goconst.min-len", 167 3, "Goconst: minimum constant string length") 168 hideFlag("goconst.min-len") 169 fs.IntVar(&lsc.Goconst.MinOccurrencesCount, "goconst.min-occurrences", 170 3, "Goconst: minimum occurrences of constant string count to trigger issue") 171 hideFlag("goconst.min-occurrences") 172 fs.BoolVar(&lsc.Goconst.ParseNumbers, "goconst.numbers", 173 false, "Goconst: search also for duplicated numbers") 174 hideFlag("goconst.numbers") 175 fs.IntVar(&lsc.Goconst.NumberMin, "goconst.min", 176 3, "minimum value, only works with goconst.numbers") 177 hideFlag("goconst.min") 178 fs.IntVar(&lsc.Goconst.NumberMax, "goconst.max", 179 3, "maximum value, only works with goconst.numbers") 180 hideFlag("goconst.max") 181 fs.BoolVar(&lsc.Goconst.IgnoreCalls, "goconst.ignore-calls", 182 true, "Goconst: ignore when constant is not used as function argument") 183 hideFlag("goconst.ignore-calls") 184 185 // (@dixonwille) These flag is only used for testing purposes. 186 fs.StringSliceVar(&lsc.Depguard.Packages, "depguard.packages", nil, 187 "Depguard: packages to add to the list") 188 hideFlag("depguard.packages") 189 190 fs.BoolVar(&lsc.Depguard.IncludeGoRoot, "depguard.include-go-root", false, 191 "Depguard: check list against standard lib") 192 hideFlag("depguard.include-go-root") 193 194 fs.IntVar(&lsc.Lll.TabWidth, "lll.tab-width", 1, 195 "Lll: tab width in spaces") 196 hideFlag("lll.tab-width") 197 198 // Linters config 199 lc := &cfg.Linters 200 fs.StringSliceVarP(&lc.Enable, "enable", "E", nil, wh("Enable specific linter")) 201 fs.StringSliceVarP(&lc.Disable, "disable", "D", nil, wh("Disable specific linter")) 202 fs.BoolVar(&lc.EnableAll, "enable-all", false, wh("Enable all linters")) 203 if err := fs.MarkHidden("enable-all"); err != nil { 204 panic(err) 205 } 206 207 fs.BoolVar(&lc.DisableAll, "disable-all", false, wh("Disable all linters")) 208 fs.StringSliceVarP(&lc.Presets, "presets", "p", nil, 209 wh(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint linters' to see "+ 210 "them. This option implies option --disable-all", strings.Join(m.AllPresets(), "|")))) 211 fs.BoolVar(&lc.Fast, "fast", false, wh("Run only fast linters from enabled linters set (first run won't be fast)")) 212 213 // Issues config 214 ic := &cfg.Issues 215 fs.StringSliceVarP(&ic.ExcludePatterns, "exclude", "e", nil, wh("Exclude issue by regexp")) 216 fs.BoolVar(&ic.UseDefaultExcludes, "exclude-use-default", true, getDefaultIssueExcludeHelp()) 217 fs.BoolVar(&ic.ExcludeCaseSensitive, "exclude-case-sensitive", false, wh("If set to true exclude "+ 218 "and exclude rules regular expressions are case sensitive")) 219 220 fs.IntVar(&ic.MaxIssuesPerLinter, "max-issues-per-linter", 50, 221 wh("Maximum issues count per one linter. Set to 0 to disable")) 222 fs.IntVar(&ic.MaxSameIssues, "max-same-issues", 3, 223 wh("Maximum count of issues with the same text. Set to 0 to disable")) 224 225 fs.BoolVarP(&ic.Diff, "new", "n", false, 226 wh("Show only new issues: if there are unstaged changes or untracked files, only those changes "+ 227 "are analyzed, else only changes in HEAD~ are analyzed.\nIt's a super-useful option for integration "+ 228 "of golangci-lint into existing large codebase.\nIt's not practical to fix all existing issues at "+ 229 "the moment of integration: much better to not allow issues in new code.\nFor CI setups, prefer "+ 230 "--new-from-rev=HEAD~, as --new can skip linting the current patch if any scripts generate "+ 231 "unstaged files before golangci-lint runs.")) 232 fs.StringVar(&ic.DiffFromRevision, "new-from-rev", "", 233 wh("Show only new issues created after git revision `REV`")) 234 fs.StringVar(&ic.DiffPatchFilePath, "new-from-patch", "", 235 wh("Show only new issues created in git patch with file path `PATH`")) 236 fs.BoolVar(&ic.NeedFix, "fix", false, "Fix found issues (if it's supported by the linter)") 237 } 238 239 func (e *Executor) initRunConfiguration(cmd *cobra.Command) { 240 fs := cmd.Flags() 241 fs.SortFlags = false // sort them as they are defined here 242 initFlagSet(fs, e.cfg, e.DBManager, true) 243 } 244 245 func (e *Executor) getConfigForCommandLine() (*config.Config, error) { 246 // We use another pflag.FlagSet here to not set `changed` flag 247 // on cmd.Flags() options. Otherwise string slice options will be duplicated. 248 fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError) 249 250 var cfg config.Config 251 // Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations: 252 // `changed` variable inside string slice vars will be shared. 253 // Use another config variable here, not e.cfg, to not 254 // affect main parsing by this parsing of only config option. 255 initFlagSet(fs, &cfg, e.DBManager, false) 256 initVersionFlagSet(fs, &cfg) 257 258 // Parse max options, even force version option: don't want 259 // to get access to Executor here: it's error-prone to use 260 // cfg vs e.cfg. 261 initRootFlagSet(fs, &cfg, true) 262 263 fs.Usage = func() {} // otherwise help text will be printed twice 264 if err := fs.Parse(os.Args); err != nil { 265 if err == pflag.ErrHelp { 266 return nil, err 267 } 268 269 return nil, fmt.Errorf("can't parse args: %s", err) 270 } 271 272 return &cfg, nil 273 } 274 275 func (e *Executor) initRun() { 276 e.runCmd = &cobra.Command{ 277 Use: "run", 278 Short: "Run the linters", 279 Run: e.executeRun, 280 PreRun: func(_ *cobra.Command, _ []string) { 281 if ok := e.acquireFileLock(); !ok { 282 e.log.Fatalf("Parallel golangci-lint is running") 283 } 284 }, 285 PostRun: func(_ *cobra.Command, _ []string) { 286 e.releaseFileLock() 287 }, 288 } 289 e.rootCmd.AddCommand(e.runCmd) 290 291 e.runCmd.SetOut(logutils.StdOut) // use custom output to properly color it in Windows terminals 292 e.runCmd.SetErr(logutils.StdErr) 293 294 e.initRunConfiguration(e.runCmd) 295 } 296 297 func fixSlicesFlags(fs *pflag.FlagSet) { 298 // It's a dirty hack to set flag.Changed to true for every string slice flag. 299 // It's necessary to merge config and command-line slices: otherwise command-line 300 // flags will always overwrite ones from the config. 301 fs.VisitAll(func(f *pflag.Flag) { 302 if f.Value.Type() != "stringSlice" { 303 return 304 } 305 306 s, err := fs.GetStringSlice(f.Name) 307 if err != nil { 308 return 309 } 310 311 if s == nil { // assume that every string slice flag has nil as the default 312 return 313 } 314 315 var safe []string 316 for _, v := range s { 317 // add quotes to escape comma because spf13/pflag use a CSV parser: 318 // https://github.com/spf13/pflag/blob/85dd5c8bc61cfa382fecd072378089d4e856579d/string_slice.go#L43 319 safe = append(safe, `"`+v+`"`) 320 } 321 322 // calling Set sets Changed to true: next Set calls will append, not overwrite 323 _ = f.Value.Set(strings.Join(safe, ",")) 324 }) 325 } 326 327 func (e *Executor) runAnalysis(ctx context.Context, args []string) ([]result.Issue, error) { 328 e.cfg.Run.Args = args 329 330 lintersToRun, err := e.EnabledLintersSet.GetOptimizedLinters() 331 if err != nil { 332 return nil, err 333 } 334 335 enabledLintersMap, err := e.EnabledLintersSet.GetEnabledLintersMap() 336 if err != nil { 337 return nil, err 338 } 339 340 for _, lc := range e.DBManager.GetAllSupportedLinterConfigs() { 341 isEnabled := enabledLintersMap[lc.Name()] != nil 342 e.reportData.AddLinter(lc.Name(), isEnabled, lc.EnabledByDefault) 343 } 344 345 lintCtx, err := e.contextLoader.Load(ctx, lintersToRun) 346 if err != nil { 347 return nil, errors.Wrap(err, "context loading failed") 348 } 349 lintCtx.Log = e.log.Child("linters context") 350 351 runner, err := lint.NewRunner(e.cfg, e.log.Child("runner"), 352 e.goenv, e.EnabledLintersSet, e.lineCache, e.DBManager, lintCtx.Packages) 353 if err != nil { 354 return nil, err 355 } 356 357 issues, err := runner.Run(ctx, lintersToRun, lintCtx) 358 if err != nil { 359 return nil, err 360 } 361 362 fixer := processors.NewFixer(e.cfg, e.log, e.fileCache) 363 return fixer.Process(issues), nil 364 } 365 366 func (e *Executor) setOutputToDevNull() (savedStdout, savedStderr *os.File) { 367 savedStdout, savedStderr = os.Stdout, os.Stderr 368 devNull, err := os.Open(os.DevNull) 369 if err != nil { 370 e.log.Warnf("Can't open null device %q: %s", os.DevNull, err) 371 return 372 } 373 374 os.Stdout, os.Stderr = devNull, devNull 375 return 376 } 377 378 func (e *Executor) setExitCodeIfIssuesFound(issues []result.Issue) { 379 if len(issues) != 0 { 380 e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound 381 } 382 } 383 384 func (e *Executor) runAndPrint(ctx context.Context, args []string) error { 385 if err := e.goenv.Discover(ctx); err != nil { 386 e.log.Warnf("Failed to discover go env: %s", err) 387 } 388 389 if !logutils.HaveDebugTag("linters_output") { 390 // Don't allow linters and loader to print anything 391 log.SetOutput(ioutil.Discard) 392 savedStdout, savedStderr := e.setOutputToDevNull() 393 defer func() { 394 os.Stdout, os.Stderr = savedStdout, savedStderr 395 }() 396 } 397 398 issues, err := e.runAnalysis(ctx, args) 399 if err != nil { 400 return err // XXX: don't loose type 401 } 402 403 p, err := e.createPrinter() 404 if err != nil { 405 return err 406 } 407 408 e.setExitCodeIfIssuesFound(issues) 409 410 if err = p.Print(ctx, issues); err != nil { 411 return fmt.Errorf("can't print %d issues: %s", len(issues), err) 412 } 413 414 e.fileCache.PrintStats(e.log) 415 416 return nil 417 } 418 419 func (e *Executor) createPrinter() (printers.Printer, error) { 420 var p printers.Printer 421 format := e.cfg.Output.Format 422 switch format { 423 case config.OutFormatJSON: 424 p = printers.NewJSON(&e.reportData) 425 case config.OutFormatColoredLineNumber, config.OutFormatLineNumber: 426 p = printers.NewText(e.cfg.Output.PrintIssuedLine, 427 format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName, 428 e.log.Child("text_printer")) 429 case config.OutFormatTab: 430 p = printers.NewTab(e.cfg.Output.PrintLinterName, e.log.Child("tab_printer")) 431 case config.OutFormatCheckstyle: 432 p = printers.NewCheckstyle() 433 case config.OutFormatCodeClimate: 434 p = printers.NewCodeClimate() 435 case config.OutFormatHTML: 436 p = printers.NewHTML() 437 case config.OutFormatJunitXML: 438 p = printers.NewJunitXML() 439 case config.OutFormatGithubActions: 440 p = printers.NewGithub() 441 default: 442 return nil, fmt.Errorf("unknown output format %s", format) 443 } 444 445 return p, nil 446 } 447 448 func (e *Executor) executeRun(_ *cobra.Command, args []string) { 449 needTrackResources := e.cfg.Run.IsVerbose || e.cfg.Run.PrintResourcesUsage 450 trackResourcesEndCh := make(chan struct{}) 451 defer func() { // XXX: this defer must be before ctx.cancel defer 452 if needTrackResources { // wait until resource tracking finished to print properly 453 <-trackResourcesEndCh 454 } 455 }() 456 457 e.setTimeoutToDeadlineIfOnlyDeadlineIsSet() 458 ctx, cancel := context.WithTimeout(context.Background(), e.cfg.Run.Timeout) 459 defer cancel() 460 461 if needTrackResources { 462 go watchResources(ctx, trackResourcesEndCh, e.log, e.debugf) 463 } 464 465 if err := e.runAndPrint(ctx, args); err != nil { 466 e.log.Errorf("Running error: %s", err) 467 if e.exitCode == exitcodes.Success { 468 if exitErr, ok := errors.Cause(err).(*exitcodes.ExitError); ok { 469 e.exitCode = exitErr.Code 470 } else { 471 e.exitCode = exitcodes.Failure 472 } 473 } 474 } 475 476 e.setupExitCode(ctx) 477 } 478 479 // to be removed when deadline is finally decommissioned 480 func (e *Executor) setTimeoutToDeadlineIfOnlyDeadlineIsSet() { 481 // nolint:staticcheck 482 deadlineValue := e.cfg.Run.Deadline 483 if deadlineValue != 0 && e.cfg.Run.Timeout == defaultTimeout { 484 e.cfg.Run.Timeout = deadlineValue 485 } 486 } 487 488 func (e *Executor) setupExitCode(ctx context.Context) { 489 if ctx.Err() != nil { 490 e.exitCode = exitcodes.Timeout 491 e.log.Errorf("Timeout exceeded: try increasing it by passing --timeout option") 492 return 493 } 494 495 if e.exitCode != exitcodes.Success { 496 return 497 } 498 499 needFailOnWarnings := (os.Getenv("GL_TEST_RUN") == "1" || os.Getenv("FAIL_ON_WARNINGS") == "1") 500 if needFailOnWarnings && len(e.reportData.Warnings) != 0 { 501 e.exitCode = exitcodes.WarningInTest 502 return 503 } 504 505 if e.reportData.Error != "" { 506 // it's a case e.g. when typecheck linter couldn't parse and error and just logged it 507 e.exitCode = exitcodes.ErrorWasLogged 508 return 509 } 510 } 511 512 func watchResources(ctx context.Context, done chan struct{}, logger logutils.Log, debugf logutils.DebugFunc) { 513 startedAt := time.Now() 514 debugf("Started tracking time") 515 516 var maxRSSMB, totalRSSMB float64 517 var iterationsCount int 518 519 const intervalMS = 100 520 ticker := time.NewTicker(intervalMS * time.Millisecond) 521 defer ticker.Stop() 522 523 logEveryRecord := os.Getenv("GL_MEM_LOG_EVERY") == "1" 524 const MB = 1024 * 1024 525 526 track := func() { 527 var m runtime.MemStats 528 runtime.ReadMemStats(&m) 529 530 if logEveryRecord { 531 debugf("Stopping memory tracing iteration, printing ...") 532 printMemStats(&m, logger) 533 } 534 535 rssMB := float64(m.Sys) / MB 536 if rssMB > maxRSSMB { 537 maxRSSMB = rssMB 538 } 539 totalRSSMB += rssMB 540 iterationsCount++ 541 } 542 543 for { 544 track() 545 546 stop := false 547 select { 548 case <-ctx.Done(): 549 stop = true 550 debugf("Stopped resources tracking") 551 case <-ticker.C: 552 } 553 554 if stop { 555 break 556 } 557 } 558 track() 559 560 avgRSSMB := totalRSSMB / float64(iterationsCount) 561 562 logger.Infof("Memory: %d samples, avg is %.1fMB, max is %.1fMB", 563 iterationsCount, avgRSSMB, maxRSSMB) 564 logger.Infof("Execution took %s", time.Since(startedAt)) 565 close(done) 566 }