SwiftLint
SwiftLint copied to clipboard
Excluded files impact the performance of swiftlint
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 versionto 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
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
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'
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?
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
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
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.
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.
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.
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
I still have the same problem in 0.59.1, is there any update on this? 🙏
Met same performance issue on swiftlint 0.61.0 😔
it is extremely slow when use excluded.