-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make getting style violations thread-safe and parallelize linting #1103
Conversation
and make some of its members private instead of fileprivate
Generated by 🚫 danger |
let uuid = NSUUID().uuidString | ||
let jobCount = Int(ceil(Double(count) / Double(maxConcurrentJobs))) | ||
|
||
let queueLabelPrefix = "io.realm.SwiftLintFramework.map.\(uuid)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
curious whether there is a reason you didn't use DispatchQueue.concurrentPerform
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't use it because I wasn't aware of it! Thanks for sharing 😊
The docs are non-existent, unfortunately: https://developer.apple.com/reference/dispatch/dispatchqueue/2016088-concurrentperform
But doing a bit of digging, the Dispatch Swift overlay seems to implement that function with dispatch_apply
, which is well documented here: https://developer.apple.com/reference/dispatch/1452846-dispatch_apply_f
It seems to be exactly what I've reimplemented here, so I think I'll refactor to use that instead.
private var _allDeclarationsByType = [String: [String]]() | ||
private var queueForRebuild = [Structure]() | ||
private struct RebuildQueue { | ||
private let lock = NSLock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure here, but wouldn't using a queue be faster?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What makes you think that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the impression that NSLock
s are really expensive, but can't find any good references right now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, locking in general has considerable overhead, but that goes for whether you're using NSLock
or GCD queues...
Actually, some of the patterns here could avoid some locking by using lock-free data structures, since they're append-only and we end up sorting them afterwards, but I couldn't find good resources on lock-free data structures in Swift, so I've punted on that for now.
@@ -16,6 +17,8 @@ struct LintCommand: CommandProtocol { | |||
let verb = "lint" | |||
let function = "Print lint warnings and errors (default command)" | |||
|
|||
private static let violationsAccumulatorQueue = DispatchQueue(label: "io.realm.swiftlint.violationsAccumulator") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know probably no one will use multiple linters at once, but shouldn't this queue be per instance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 253e1f4.
I got an exception in one of my runs:
|
And another one:
|
If it helps, I'm running on a 6-Core 2013 Mac Pro. |
Actually this just happens with the |
also remove parallelForEach from SwiftLintFramework's public interface since calling the GCD API is almost as concise and avoids polluting the public API. Finally, also remove the "fast" path for parallelMap since GCD should do the reasonable thing for us here.
Yeah, I had neglected to make recording benchmarks thread-safe. Done in 253e1f4. |
Current coverage is 82.41% (diff: 48.95%)@@ master #1103 diff @@
==========================================
Files 151 151
Lines 7209 7279 +70
Methods 0 0
Messages 0 0
Branches 0 0
==========================================
+ Hits 5949 5999 +50
- Misses 1260 1280 +20
Partials 0 0
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This 👏 is 👏 awesome! 👏
I have gotten crashes twice on linting: |
} | ||
autoreleasepool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is autoreleasepool
removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. Nice catch! Re-added in 3608cf4.
Are those crashes reproducible? |
@norio-nomura what project(s) were you linting when you experienced these crashes? |
I experienced on Carthage. It happens rarely. But it happened first and second try with this PR. |
Hmmm, I ran this with Thread Sanitizer enabled against Carthage with all submodules recursively cloned 5 times in a row without hitting any issues. Xcode 8.2 with just a bare The only change I've made was to enable all opt-in rules: --- a/Source/SwiftLintFramework/Models/Configuration.swift
+++ b/Source/SwiftLintFramework/Models/Configuration.swift
@@ -91,11 +91,7 @@ public struct Configuration: Equatable {
return whitelistRules.contains(type(of: rule).description.identifier)
}
} else {
- rules = configuredRules.filter { rule in
- let id = type(of: rule).description.identifier
- if validDisabledRules.contains(id) { return false }
- return optInRules.contains(id) || !(rule is OptInRule)
- }
+ rules = configuredRules
} |
result.append(jobIndexAndResults) | ||
} | ||
} | ||
return result.sorted { $0.0 < $1.0 }.flatMap { $0.1 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the flatMap
correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
lock.unlock() | ||
let value = factory(file) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
factory()
invocation might be happen in parallel for same key file
because it unlocked()
while running factory()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That shouldn't be a problem when factory
is already thread-safe. But anyway, I've refactored to perform the factory while locked in 8906da6, to make sure this keeps working if factory is ever not thread-safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for change.
I thought that it should be locked for functionality of Cache
even if factory
would be already thread-safe. Because cached contents possibly be reference type, and previous implementation could return difference instance for same key.
* master: use first(where:) instead of for loop in RulesCommand.swift remove unnecessary imports from AutoCorrectCommand.swift Typo in changelog Remove unnecessary import Avoid duplicating default value Update number_separator configuration in SwiftLint number_separator can be configured with a min length Enable more opt-in rules in SwiftLint Fix out of range exception in AttributesRule Fix trailing_comma crash
I appreciate everyone's reviews on this! Thanks @norio-nomura, @marcelofabri and special guest @krzysztofzablocki 🙏. I think I'll be merging this despite @norio-nomura's crashes, since neither of us appear to be able to reproduce them now. |
Addresses #1077. Thread sanitizer seems happy with this when running unit tests and linting a few projects I could find, including the Swift standard library.
Generally speeds up linting by ~30% to ~140% on my 2013 MacBook Pro, though the gains should be higher on more recent CPUs.
I had initially added a
--parallel
flag to the lint command, but have since removed that option since I haven't run into any issues with it so far, and couldn't come up with a compelling reason to hide it behind an opt-in flag.