Reduce lint hot-path overhead#6826
Draft
Brett-Best wants to merge 28 commits into
Draft
Conversation
Generated by 🚫 Danger |
Comparing via debugDescription(includeTrivia:false) builds a full indented debug dump of both operand subtrees (keypath-driven child names included) for every infix operator expression. Comparing the lazy token streams short-circuits on the first mismatch and allocates nothing. ~6% of a large lint run in profiling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CharacterSet union was rebuilt through CoreFoundation on every checked identifier; it only changes when allowedSymbols changes. ~83% of identifier_name's cost in profiling a large repo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
comment_spacing and period_spacing only need comment byte ranges, but obtained them via file.syntaxClassifications, which runs SwiftSyntax's general-purpose classifier over every token in the file. A direct trivia walk over the cached syntax tree yields the same ranges without classifying non-comment tokens. Both rules are migrated together since they shared the classification cache; migrating one alone would move the cost to the other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Skip Linter.collect's concurrentPerform when no CollectingRule is enabled: it dispatched rules.count no-op iterations per file, whose existential/RuleDescription copies and task-local bookkeeping cost ~14% of a large lint run. - Memoize the minSwiftVersion support check per rule type; it compared freshly parsed version strings and copied the full RuleDescription per rule and file. - Read SWIFTLINT_DISABLE_SOURCEKIT once; ProcessInfo.environment rebridges the entire environment on every access. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The documentation promises nil when no element satisfies the predicate, but the termination check tested the midpoint index instead of the partition boundary, returning count in that case. Existing callers only survived because they immediately formed (empty) suffix slices from the result; subscripting callers crash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
statement_position, vertical_whitespace_closing_braces, vertical_whitespace_opening_braces, and indentation_width resolve syntax kinds through a shared SwiftSyntax-derived syntax map; multiline_function_chains, multiline_parameters_brackets, and literal_expression_end_indentation are ported to SwiftSyntax visitors reproducing SourceKit's structure geometry. The shared map extends comment tokens over their terminating newline to match sourcekitd's extents, and rules build it only for files whose regexes match at all. With no enabled rule requiring SourceKit, lint runs never pay the per-file sourcekitd editorOpen round trip (~13% of CPU on a large repo profiling run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
execution_mode/default_execution_mode existed in configuration and in isEffectivelySourceKitFree, but validate() always used the sourcekitd-backed syntax map regardless of mode — a swiftsyntax-mode configuration would even hit the "Never call this for file that sourcekitd fails" trap when SourceKit is unavailable. Dispatch on the effective mode and resolve kinds from the SwiftSyntax-derived map, built lazily only when a kind-filtered pattern matches. Also propagate defaultExecutionMode through parent/child configuration merging, which rebuilt CustomRulesConfiguration and silently dropped it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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. - Read a file's emptiness once per file instead of once per rule and file: the check acquired the contents queue lock on every call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- mark: materialize each token's leading trivia once instead of once per piece, and gate the regex on a cheap substring pre-check. - file_length: compute everything from the root node without walking the entire tree. - trailing_whitespace: scan lines for candidates first and only pre-compute comment/string-literal line information when any exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sourcekitd includes the terminating newline in line comment tokens but ends block comment tokens at the closing delimiter. Extending all comment kinds suppressed violations following block comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three rules walk the file for comment trivia; the result is now shared through the file cache. Byte positions are tracked incrementally during a single visitor pass: SyntaxProtocol.position and token-sequence iteration climb the tree per call, which degrades sharply on deeply nested expression trees. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
visitPost fires for every call in a chain and each computation shares its suffix with the enclosing call's, so long chains were re-walked quadratically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every rule walks the same file's syntax tree, and concurrent walks contend on the shared syntax arena's reference counts, slowing all of them down. With at least as many files as cores, file-level parallelism already saturates the machine, so rules now run serially within each file in that case. Few-file invocations (single-file lints, editor integrations) keep concurrent rule execution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brett-Best
force-pushed
the
codex/lint-hot-path-performance
branch
from
July 16, 2026 20:46
4459be4 to
e7c9b0b
Compare
Rewriter-based corrections reconstructed the entire syntax tree and serialized it back to a string for every correctable rule and file, even when there was nothing to correct — the dominant cost of --fix runs on clean codebases. Detect violations first via validate(file:), which reports a superset of what the rewriter corrects (the rewriter additionally skips disabled regions), and only rewrite when needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract trailing_whitespace's candidate collection into a helper to reduce cyclomatic complexity, wrap an overlong line, and move indentation_width's helper visitors into their own file to respect the file length limit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tax map The SourceKit-free syntax map extended line comments over their terminating newline only for LF line endings; the seven rules using it now also run on platforms where checkouts commonly use CRLF. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With serial per-file rule execution, a huge file dispatched near the end of a run occupies a single core while the remaining cores sit idle. Sorting files by on-disk size before dispatch hides long single-file runtimes behind the remaining work regardless of core count. Reporting order for parallel runs is already scheduler-dependent, so this changes no observable behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-file artifact cache guarded every slot access with a concurrent DispatchQueue, taking a barrier sync on writes and a plain sync on reads. Each lint forces roughly a dozen slots per file across all rules, so the queue's dispatch overhead was a measurable share of a run. An NSLock with the same computed-outside-the-lock / DispatchGroup-dedup semantics is cheaper for this short-critical-section workload. Also invalidate the comment-byte-ranges slot, which was missing from invalidateAll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The body-length rules computed the comment/whitespace-adjusted line count for every body, forcing the per-file linesWithTokens set and allocating a Set of the body's line range each time. The effective count can never exceed the physical brace-to-brace span, so bodies whose span is already within the threshold cannot violate and skip the analysis entirely. Where the count is still needed, it is now derived without materializing a Set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate trivia materialization on a byte-length check so empty-trivia tokens skip re-lexing, iterate trivia forward instead of copying reversed arrays, compute the token kind only when needed to detect end-of-file, and resolve line numbers through a per-line bounds cache instead of a converter lookup per token. Comment-line results are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rule ran separate tree walks for multi-line condition continuations and multiline string-literal spans; merge them into one visitor pass. Rename the helper file to match its renamed type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SourceLocationConverter.sourceLines rebuilds the whole file as a fresh [String] on every access; the rule invoked it once per parameter of every multi-parameter declaration. Cache it once per file and compute grapheme columns only for the parameters actually compared. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
allTerms and allAllowedTerms were re-derived (set union, lowercasing, dedup, sort) on every visited token. Compute them once per file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hoist the stringView access out of the per-comment loop and run the violation regex only on comments a cheap UTF-16 unit scan cannot rule out, matching against the file's string view in place instead of re-materializing each comment body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hoist per-file invariants out of per-node work and gate expensive checks on cheap pre-conditions in these four rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skip region partitioning and identifier-set construction when they cannot affect the result (no violations, or a single enable-everywhere region). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lint already resolves the identifier and passes it into the rule context; performLint recomputed it. Reading it goes through the rule's description witness, which copies the whole RuleDescription value, so this removes one such copy per rule and file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Some other benchmarks which also include the fix jpsim/SourceKitten#846 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR reduces lint and correction hot-path overhead across rule execution, syntax classification, file caching, and parallel scheduling. In SwiftLint's OSS-check suite, the combined series improves end-to-end lint time by 9–35%.
Motivation
Profiling a large repository showed that a substantial portion of runtime was spent outside rule-specific analysis: SourceKit
editorOpenrequests, repeated syntax-tree walks, classification of unrelated tokens, full-tree correction rewrites on clean files, filesystem and cache synchronization, repeated source-line and term-list construction, task-local bookkeeping, violation-region partitioning, and concurrent walks contending on the same syntax arena.Most of this work is invariant for a rule type or file, can be cached, or can be avoided entirely after a cheap candidate check. This series moves invariant work out of repeated paths and makes expensive analysis conditional on a file having relevant content.
Details
Remove unnecessary SourceKit work
indentation_width,literal_expression_end_indentation,multiline_function_chains,multiline_parameters_brackets,statement_position, and the vertical-whitespace brace rules to SwiftSyntax-backed implementationsdefault_execution_modeAvoid unnecessary correction and rule work
comment_spacingregex work behind a cheap UTF-16 scan and match directly against the file's string viewcolon,comma,closure_end_indentation, andredundant_selflintinstead of copying itsRuleDescriptionagain inperformLintCache and narrow repeated analysis
NSLockwhile retaining computation deduplicationCommentLinesVisitorby avoiding empty-trivia re-lexing, reversed trivia copies, repeated token-kind checks, and per-token converter lookupsindentation_widthsyntax data in one tree walkvertical_parameter_alignmentinclusive_languageterm lists once per fileallowedSymbolsAndAlphanumerics, minimum-Swift-version support, environment state, file emptiness, and targeted file metadataImprove scheduling
Correctness fixes
RandomAccessCollection.firstIndexAssumingSorted(where:)returnnil, rather thanendIndex, when no element matchesexecution_modeand inheriteddefault_execution_modeImpact
Large multi-file lint runs should spend less CPU time in SourceKit, syntax-tree bookkeeping, filesystem access, cache synchronization, rule post-processing, and shared-arena contention. Clean
--fixruns avoid reconstructing the syntax tree for every correctable rule. Single-file and editor integrations retain concurrent rule execution. No configuration migration is required, and SourceKit execution remains available where explicitly selected or required.Validation
Brett-Best <brettbest@live.com.au>as author and committergit diff --checkreports only the two intentional Markdown hard breaks in this PR's changelog entries.