SwiftLint icon indicating copy to clipboard operation
SwiftLint copied to clipboard

Excluded files impact the performance of swiftlint

Open sebmarchand opened this issue 2 years ago • 9 comments
trafficstars

New Issue Checklist

  • [X] Updated SwiftLint to the latest version => I built it from source
  • [X] I searched for existing GitHub issues

Describe the bug

The number of files present in the working tree of swiftlint has a huge impact on its performance even if most of these files are ignored. Here's a concrete example/repro:

  • Create a temporary directory with just one file to lint:
mkdir /tmp/swiftlint_tests && cd /tmp/swiftlint_tests
touch test.swift
  • Use hyperfine to measure the execution time of swiftlint:
$ hyperfine --warmup 1 'swiftlint'
➜  swiftlint_tests hyperfine --warmup 1 'swiftlint'
  Time (mean ± σ):      61.4 ms ±   0.9 ms    [User: 52.2 ms, System: 7.1 ms]
  Range (min … max):    59.9 ms …  65.4 ms    46 runs
  • Create a subdirectory with a lot of swift files in it, create a linter config to exclude these files:
mkdir .build
for i in {1..10000}
do
  touch .build/$i.swift
done

echo "excluded:\n  - '**/.build'" > .swiftlint.yml
  • Make sure that these ignored files are actually ignored:
$ swiftlint
Linting Swift files in current working directory
Linting 'test.swift' (1/1)
Done linting! Found 0 violations, 0 serious in 1 file.
  • Benchmark swiftlint again:
➜  swiftlint_tests hyperfine --warmup 1 'swiftlint'
  Time (mean ± σ):     378.7 ms ±   6.1 ms    [User: 229.2 ms, System: 377.6 ms]
  Range (min … max):   368.2 ms … 389.6 ms    10 runs

The execution time is 6x what it is when these ignored files don't exist. Using the --use-alternative-excluding flag brings this to 5x but it'd still be nice to completely ignore these file. Not using a glob pattern in the config improves things by another 1x.

It looks like the code is traversing the entire file tree in a few places (e.g. in the glob implementation), we could maybe make it use a recursive approach and stop when it encounters a subdirectory that is ignored (e.g. in my example it'd not look at the content of the .build directory at all).

Environment

  • SwiftLint version (run swiftlint version to be sure)? From source

  • Installation method used (Homebrew, CocoaPods, building from source, etc)? Source

  • Paste your configuration file: See example

  • Are you using nested configurations? No

  • Which Xcode version are you using (check xcodebuild -version)?

$ xcodebuild -version
Xcode 14.3
Build version 14E222b

sebmarchand avatar May 18 '23 12:05 sebmarchand

We hit this case as well, it seems like for us --use-alternative-excluding fixes it, but our case isn't very complex, we just try to exclude some potential nested derived data directories

included: [Modules, tools]
excluded:
  - tools/*/.build
  - tools/*/DerivedData
  - tools/ibmodulelint/tests/fixtures
  - tools/illustrationstrip/tests/fixtures
  - tools/new_feature

Interestingly based on inspecting file opens it looks like what's happening is that when we pass a file list like @/tmp/abc.txt with just ~200 files, for every file it appears that swiftlint is re-resolving all files under the matching globs, which in the case where I noticed it is ~10k files, so I guess swiftlint is attempting ~2 million file stats before even starting to lint. Maybe that isn't the only cause of the slowdown but in my case it results in ~7 minutes to lint these ~200 files, which with --use-alternative-excluding lint in ~1 second.

You can see most of the time is spent in NSFileManager image

keith avatar Jul 26 '23 21:07 keith

Our exclusion list is as follows, and we saw a 3s->150s increase in execution time

included: # paths to include during linting.
  - '*/Sources'
  - '*/Tests'
excluded: # paths to ignore during linting. Takes precedence over `included`.
  - '.build'
  - '.swiftpm'
  - 'Sources/GraphQLQueries'
  - '**/*.graphql.swift'

Mordil avatar Aug 03 '23 17:08 Mordil

There doesn't seem to be a way to actually use the --use-alertnative-excluding flag when using SwiftLint as a SwiftPM plugin?

Could this "easily" be added to the configuration file?

Mordil avatar Aug 03 '23 17:08 Mordil

With further testing, this performance impact is only seen when using globstar exclusion patterns.

If I instead list out the directories directly, SwiftLint execution drops down to the expected sub-second timing

Mordil avatar Aug 04 '23 19:08 Mordil

I'm having same issue, so you solution is to change this

included: # paths to include during linting.
  - '*/Sources'
  - '*/Tests'
excluded: # paths to ignore during linting. Takes precedence over `included`.
  - '.build'
  - '.swiftpm'
  - 'Sources/GraphQLQueries'
  - 'folder/graphql.swift'

removing the ** from the path. asking because I added few more folders to exclude and swift link plugin went from 1.6s to 23.7 seconds

yjdv avatar Aug 16 '23 13:08 yjdv

In my case I've been avoiding the problem by making sure that these `.build' folders don't get generated (via some settings in the Swift VSCode extension), but that's not ideal.

sebmarchand avatar Aug 16 '23 15:08 sebmarchand

As a general rule one should avoid having the include and exclude patterns match too many files. They should be rather specific to avoid performance issues. I hope this restriction can be mitigated by https://github.com/realm/SwiftLint/pull/5157.

SimplyDanny avatar Aug 22 '23 16:08 SimplyDanny

I found that the problem is not limited to **.

If you specify a specific directory (.build for example), the lint speed degradation is also observed. This also slows down the process significantly (if you comment out other exceptions)


I also tried to specify explicitly included paths that do not intersect with excluded in any way. This does not improve performance in any way.

However, I can't just remove excluded list because I pass specific files to swiftlint command and I want it to automatically ignore the files.

Ernest0-Production avatar Mar 22 '25 11:03 Ernest0-Production

I tried to implement the behavior I needed with included+excluded globs using ruby+fastlane instead of swiftlint.yml configurations.

And my solution turned out to be quite fast:

module SwiftLintPaths
  INCLUDED_PATHS = [
    'iOS',
    'tvOS',
    'Frameworks'
  ].map { |path| File.join(Constants::ROOT_DIR, path) }

  EXCLUDED_PATHS = [
    '.build',
    '.testResult',
    '.artifacts',
    '**/Generated/*',
    '**/Derived/Sources/*',
    'Tuist/',
    '**/Project.swift',
    'Workspace.swift',
  ].map { |path| File.join(Constants::ROOT_DIR, path) }

  def self.filter(paths)
    paths.select do |path|
      # Check that file is in included paths
      SwiftLintPaths::INCLUDED_PATHS.any? { |included| path.start_with?(included) } &&
        # check that file is not in excluded paths
        SwiftLintPaths::EXCLUDED_PATHS.none? do |excluded|
          File.fnmatch(excluded, path)
        end
    end
  end
end

desc '
options:
- paths: Array - paths to lint (default: SwiftLintPaths::INCLUDED_PATHS)
- options: Array - options passed to swiftlint
'
private_lane :swiftlint_lint do |options|
  # Extracting all swift files
  paths = options[:paths] || SwiftLintPaths::INCLUDED_PATHS.flat_map do |path|
    Dir.glob(File.join(path, '**/*.swift'))
  end
  UI.command_output "Found #{paths.count} swift-files"

  # Filter by excluded and included
  paths = SwiftLintPaths.filter(paths)

  if paths.empty?
    UI.success('No files to lint')
    next
  end
  UI.command "Lint #{paths.count} swift-files..."

  command = ['swiftlint lint']
  paths.each do |filepath|
    command << filepath.shellescape
  end

  options[:options]&.each do |option|
    command << option
  end

  sh(command.join(' '))
end

Ruby's implementation of glob resolution seems to be more performant than the one currently used in SwiftLint. I think this is the root of the problem

Ernest0-Production avatar Mar 22 '25 21:03 Ernest0-Production

I still have the same problem in 0.59.1, is there any update on this? 🙏

danieleformichelli avatar Aug 06 '25 09:08 danieleformichelli

Met same performance issue on swiftlint 0.61.0 😔 it is extremely slow when use excluded.

hongbo-miao avatar Oct 02 '25 23:10 hongbo-miao