-
Notifications
You must be signed in to change notification settings - Fork 247
Multithread processing source files #117
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
Conversation
This is meant as a RFC for how we can achieve this if this is something we want to do and if it's safe to do. Note: I also have only tested this on macOS based on the 5.1 branch so far |
Could you use |
@harlanhaskins I was planning on doing that, but the file discovery right now is lazy (which I assume was very intentional) so we don't have a count up front to use |
This is definitely something we've been wanting to look into, so thanks for kicking it off for us. At a very high-level, this feels like it should be safe because the closure being called ( But I also feel like my Dispatch skills have atrophied enough after years of mostly writing command-line Swift and subsets-of-Python-build-rules that I'm not the best person to make such pronouncements 😛 @dylansturg , care to take a look? One possibility would be to add a |
Definitely happy to add that flag if that's something folks want! |
I patched this change in a couple ways, and the formatter is consistently crashing on:
The first configuration was patching this in The second configuration that I tried was patching in
@allevato Mentioned that he previously encountered an issue where SwiftSyntax's recursive visitor implementation regularly exceeded the stack size on non-main threads. I wonder if that's what is happening here? Could also be a red herring. Finally, what configuration did you use for testing? I'd like to understand what's different, and why I'm seeing this crash consistently but you didn't. |
It might be worth running this with TSan enabled to see if it catches anything. |
See the JIRA bug (https://bugs.swift.org/browse/SR-11170) for the workaround. |
Thanks @akyrtzi , I wasn't able to find that JIRA issue for some reason. So the good news is, that specific problem will at least be mitigated once we migrate |
Are you seeing the stack issues in both debug and release builds or only debug builds? The issue I encountered (and fixed in swiftlang/swift-syntax#147) only occurred in debug builds. |
@ahoppen I only tried debug originally. I tried again on release, and I'm not seeing any crash in release. Sounds like it's the issue that you fixed? |
@dylansturg Yes, the issue should be fixed in |
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 implementation looks reasonable to me, with 1 question. I think we should wait until we update the formatter to the latest version of swift-syntax though, since it'll be hard to develop if debug builds crash regularly.
Formatter has been updated.🙂 |
dd90dde
to
c8940cc
Compare
After rebasing I believe I'm still seeing the stack issue that was mentioned with a debug build, I can try release tomrorow |
Use a |
@keith, I have just gotten a chance to take a look at the stack overflow issue. It seems like the problem I had, resurfaced. I just opened swiftlang/swift-syntax#205 which should solve the issue. Could you try your changes in combination with my patch? |
With that change it does work with debug builds |
c8940cc
to
432f6ba
Compare
@keith Great. I have just merged the changes. Once a tag has been created for the new version, could you also adjust the version of SwiftSyntax that |
I've updated swift-format to include that change, unfortunately I had to use a commit instead of a tag here https://forums.swift.org/t/no-new-master-snapshots-since-2-21/34200 |
I've rebased this again and dropped my Package.swift changes since those are no longer required. I would love to know what we can do to get this merged! |
diagnosticEngine.diagnose( | ||
Diagnostic.Message(.error, "Unable to create a file handle for source from \(path).")) | ||
return | ||
concurrentQueue.async(group: group) { |
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.
You should use DispatchQueue.concurrentPerform(iterations:execute:)
, it will simplify the code a bit and is strongly recommended over passing async
blocks to a concurrent queue without any upper bound check for how many run concurrently. The latter can lead to thread exhaustion.
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 don't see a specific answer about this, but there was previous conversation about it #117 (comment) I think the file discovery is intentionally lazy so we don't know the number of iterations 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.
@allevato is there a reason that the file discovery cannot be eager?
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.
@akyrtzi Just that at the time it was written, there was no reason for it to be, so we could avoid the startup delay of collecting everything up front if someone ran the tool with --recursive
on a large directory structure.
Is DispatchQueue.concurrentPerform
preferred over OperationQueue
, which also provides control over the number of concurrent jobs?
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 pushed the concurrentPerform
option so everyone can see what that would look like, happy to change it to whatever we want here
da6347b
to
558a384
Compare
Yes, it makes sense to have I would propose that diagnostics should be collected for each file and then printed as a group for each file in a sensible manner at the end, instead of printing to stderr as soon as something comes up. |
You could have one diagnostic engine and one consumer per file, and then they can replay their diagnostics out to the outside engine |
Does this startup delay really matter, the total time find+process files will still be the same at the end. And if you go with collecting the diagnostics to avoid having diagnostics printed to stderr interposed from different files, then you'll still not see some diagnostic until all the files are processed.
The benefit is that you'd just let Dispatch decide how many threads to use, instead of having to manually chose. But either way would be better than just continuously doing |
} | ||
|
||
let lock = NSLock() | ||
let allFilePaths = Array(FileIterator(paths: paths)) |
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.
Why not move this into the conditional so that it only eagerly collects the paths in parallel mode? The overhead is unnecessary in the sequential case, so we could retain the original behavior if someone has a huge data set (or a networked mount point, or something else that would be slow to access) with the caveat that they can't use --parallel
.
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.
Should we go back to the other option then? Not sure I see a big win from using concurrentPerform
at the moment especially if it requires this extra branching
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.
(Pushed this change in the meantime)
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's why I was curious about OperationQueue
, because it seems like it still provides a way to let the system choose the parallelism while also letting us walk the file hierarchy lazily, by just calling addOperation
repeatedly instead of requiring the iteration count up front. 🤷♂
But if that's not advisable for some reason, I don't consider pre-computing the file list only in parallel mode to be extra branching—rather, it's choosing the best distribution of the work given the requirements of the APIs we're using (or not using).
I agree that we don't want the diagnostics from different files to be interposed, but I still think we can do better than deferring the output of all diagnostics until all files are processed. If we have to write our own diagnostic consumer that collects diagnostics and groups them by file, then we can just have a method on that consumer that says to flush the diagnostics for that file, call it when processing of that file is complete, and synchronize around that. (For diagnostics with
However, as I mentioned in one of my replies to Keith above, I think it would be fine to have |
Previously every source file was formatted / linted one by one. On our codebase this took a full project format from ~17 minutes to ~5 minutes.
At this point with debug or release mode I see crashes in SwiftSyntax:
|
What does TSan say? These structures should be immutable... |
So with tsan I first hit many other issues. Looks like we're hitting some non-thread safe code elsewhere, like the use of DiagnosticEngine in rules: |
The Swift project moved the default branch to More detail about the branch update - https://forums.swift.org/t/updating-branch-names/40412 |
If someone is interested in picking up this change that would be great! Last I remember I believe the next blocker is that the diagnostics reporting types do not support multithreading |
I'm facing the same threading issues with my use of the SwiftFormatter API for swift-doc. Running with TSAN, it found a data race here:
|
Thanks for catching that one; it's not one of the original data races we dealt with in earlier iterations of this PR (the linked code was added later; it wasn't a race before because we inadvertently were never updating the cache 😬) but we'll need to go back and synchronize that now too. @keith is correct about the original blocking issue being that |
This allows consumers to emit diagnostics from multiple threads. Primarily motivated by swiftlang/swift-format#117
Here's a change to make DiagnosticEngine thread safe: swiftlang/swift-syntax#243 |
Here's a change to fix the issue Mattt mentioned that I also hit once DiagnosticEngine was usable from multiple threads: #242 |
Here's a new PR for this change #243 |
This allows consumers to emit diagnostics from multiple threads. Primarily motivated by swiftlang/swift-format#117
Previously every source file was formatted / linted one by one. On our
codebase this took a full project format from ~17 minutes to ~5 minutes.