Skip to content

Reduce per-file filesystem and gating overhead - #6851

Draft
Brett-Best wants to merge 1 commit into
realm:mainfrom
Brett-Best:linter-per-file-overhead
Draft

Reduce per-file filesystem and gating overhead#6851
Brett-Best wants to merge 1 commit into
realm:mainfrom
Brett-Best:linter-per-file-overhead

Conversation

@Brett-Best

@Brett-Best Brett-Best commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Linting each file currently does two avoidable pieces of fixed work:

  • cache validation obtains the complete file attribute dictionary, including extended attributes, just to read modification date;
  • rule gating repeatedly compares each rule's minimum Swift version and can repeatedly acquire the file-contents synchronization path just to test emptiness.

This asks the URL for only contentModificationDateKey. Swift-version eligibility is computed once per shared configuration and carried alongside each selected rule. Before reading file.isEmpty, the linter checks whether any version-compatible rule requires empty-file gating, so the value is computed zero or one time per file. Autocorrect keeps the read lazy behind the same version and empty-file gates.

Verification

  • DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift build -c release --product swiftlint
  • DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test: passed.
  • swift test --filter EmptyFileTests: 3 tests passed, including a deferred nonexistent-file regression proving an unsupported rule does not read the file during autocorrect.
  • make bazel_test_tsan: 18/18 targets passed; the affected //Tests:FrameworkTests target was rerun after adding the regression test.
  • .build/debug/swiftlint lint --strict --quiet --no-cache on all four changed files: clean.
  • Sorted JSON violation lists matched:
    • DuckDuckGo, default configuration: 84 before, 84 after.
    • realm-swift with --enable-all-rules: 64,834 before, 64,834 after.
    • Cache-enabled parity also matched: 84/84 on DuckDuckGo and 64,834/64,834 on realm-swift.
  • Comparisons sorted by file, line, character, rule, reason, and severity; raw JSON order is not stable for parallel linting.

Measurement

--only-rule trailing_newline on DuckDuckGo (6,747 linted files), chosen so that fixed per-file
cost dominates rather than rule work. Both binaries interleaved, best of three, on an otherwise
idle machine:

baseline change
warm cache — cache validation runs for every file 1.01s 0.92s
empty cache on each run 2.47s 2.21s
swiftlint lint --quiet --only-rule trailing_newline --cache-path <dir>

The warm-cache row is the one that exercises the attribute change directly, since validating a
cached entry is what reads the modification date.

A separate seven-run --no-cache check of the version-aware gating revision was neutral:
2.125s before versus 2.126s after. For a full --no-cache lint the end-to-end difference is
within machine noise and is not presented here as a speedup.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings August 1, 2026 02:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reduces fixed per-file overhead in SwiftLint’s linting pipeline by avoiding unnecessary filesystem work during cache validation and by reusing a per-file “is empty” computation during rule gating.

Changes:

  • Fetch file modification dates via a single URL resource value (contentModificationDateKey) instead of building the full file attributes dictionary.
  • Compute fileIsEmpty once per file and pass it into rule gating during linting.
  • Document the performance-oriented change in the changelog.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
Source/SwiftLintFramework/Models/Linter.swift Threads a per-file fileIsEmpty value through rule linting/gating to reduce repeated file-content synchronization overhead.
Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift Optimizes cache validation by retrieving only the content modification date resource value.
CHANGELOG.md Adds a “Main” entry describing the per-file overhead reductions.

Comment on lines +70 to +72
func shouldRun(onFile file: SwiftLintFile) -> Bool {
shouldRun(onFile: file, fileIsEmpty: file.isEmpty)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still seeing if this can actually be addressed, will move to draft temporarily.

@SwiftLintBot

SwiftLintBot commented Aug 1, 2026

Copy link
Copy Markdown
1 Warning
⚠️ If this is a user-facing change, please include a CHANGELOG entry to credit yourself!
You can find it at CHANGELOG.md.
19 Messages
📖 Building this branch resulted in a binary size of 28458.79 KiB vs 28460.45 KiB when built on main (-1% smaller).
📖 Linting Aerial with this PR took 0.62 s vs 0.64 s on main (3% faster).
📖 Linting Alamofire with this PR took 0.9 s vs 0.92 s on main (2% faster).
📖 Linting Brave with this PR took 5.86 s vs 5.94 s on main (1% faster).
📖 Linting DuckDuckGo with this PR took 26.32 s vs 26.7 s on main (1% faster).
📖 Linting Firefox with this PR took 10.49 s vs 10.71 s on main (2% faster).
📖 Linting Kickstarter with this PR took 6.66 s vs 6.76 s on main (1% faster).
📖 Linting Moya with this PR took 0.35 s vs 0.36 s on main (2% faster).
📖 Linting NetNewsWire with this PR took 2.3 s vs 2.34 s on main (1% faster).
📖 Linting Nimble with this PR took 0.51 s vs 0.53 s on main (3% faster).
📖 Linting PocketCasts with this PR took 6.8 s vs 6.89 s on main (1% faster).
📖 Linting Quick with this PR took 0.33 s vs 0.36 s on main (8% faster).
📖 Linting Realm with this PR took 2.83 s vs 2.85 s on main (0% faster).
📖 Linting Sourcery with this PR took 1.6 s vs 1.59 s on main (0% slower).
📖 Linting Swift with this PR took 4.22 s vs 4.25 s on main (0% faster).
📖 Linting SwiftLintPerformanceTests with this PR took 0.18 s vs 0.16 s on main (12% slower).
📖 Linting VLC with this PR took 1.2 s vs 1.24 s on main (3% faster).
📖 Linting Wire with this PR took 15.01 s vs 15.37 s on main (2% faster).
📖 Linting WordPress with this PR took 9.42 s vs 9.63 s on main (2% faster).

Here's an example of your CHANGELOG entry:

* Reduce per-file filesystem and gating overhead.  
  [Brett-Best](https://github.com/Brett-Best)
  [#issue_number](https://github.com/realm/SwiftLint/issues/issue_number)

note: There are two invisible spaces after the entry's text.

Generated by 🚫 Danger

Comment thread Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift Outdated
Comment on lines +70 to +72
func shouldRun(onFile file: SwiftLintFile) -> Bool {
shouldRun(onFile: file, fileIsEmpty: file.isEmpty)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid one.

Comment thread CHANGELOG.md Outdated
@Brett-Best
Brett-Best force-pushed the linter-per-file-overhead branch 3 times, most recently from bcce99b to d64068a Compare August 8, 2026 07:31
// As we need the configuration to get custom identifiers.
// swiftlint:disable:next function_parameter_count
func lint(file: SwiftLintFile,
fileIsEmpty: Bool,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also be a closure.

@Brett-Best
Brett-Best marked this pull request as draft August 8, 2026 12:06
@Brett-Best
Brett-Best force-pushed the linter-per-file-overhead branch from d64068a to 9972c01 Compare August 8, 2026 12:19
- Cache validation read the full attribute dictionary (including an
  enumeration of all extended attributes) per file just for the
  modification date; ask for the single resource value instead.
- Cache each rule type's Swift-version eligibility with the shared
  configuration, keeping version parsing off the per-rule, per-file
  hot path.
- Read a file's emptiness at most once per file, and only when a
  version-compatible rule needs it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Brett-Best
Brett-Best force-pushed the linter-per-file-overhead branch from 9972c01 to 1cea10a Compare August 8, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants