From 5786e26754c100f5e4e7d8df9e75ab50be9a9ce7 Mon Sep 17 00:00:00 2001 From: Keith Smiley Date: Mon, 6 Apr 2020 19:06:51 -0700 Subject: [PATCH] Multithread processing source files 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. --- Sources/swift-format/Frontend/Frontend.swift | 47 +++++++++++++------ .../Subcommands/LintFormatOptions.swift | 3 ++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index 26f085be7..5593f673f 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -92,7 +92,7 @@ class Frontend { if paths.isEmpty { processStandardInput() } else { - processPaths(paths) + processPaths(paths, parallel: lintFormatOptions.parallel) } } @@ -124,30 +124,47 @@ class Frontend { } /// Processes source content from a list of files and/or directories provided as paths. - private func processPaths(_ paths: [String]) { + private func processPaths(_ paths: [String], parallel: Bool) { precondition( !paths.isEmpty, "processPaths(_:) should only be called when paths is non-empty.") - for path in FileIterator(paths: paths) { - guard let sourceFile = FileHandle(forReadingAtPath: path) else { - diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to open \(path)")) - continue + let lock = NSLock() + if parallel { + let allFilePaths = Array(FileIterator(paths: paths)) + DispatchQueue.concurrentPerform(iterations: allFilePaths.count) { index in + let path = allFilePaths[index] + openAndProcess(path, lock: lock) } - - guard let configuration = configuration( - atPath: lintFormatOptions.configurationPath, orInferredFromSwiftFileAtPath: path) - else { - // Already diagnosed in the called method. - continue + } else { + for path in FileIterator(paths: paths) { + openAndProcess(path, lock: nil) } + } + } - let fileToProcess = FileToProcess( - fileHandle: sourceFile, path: path, configuration: configuration) - processFile(fileToProcess) + /// Read and process the given path, optionally synchronizing diagnostic output with the given lock. + private func openAndProcess(_ path: String, lock: NSLock?) -> Void { + guard let sourceFile = FileHandle(forReadingAtPath: path) else { + lock?.lock() + defer { lock?.unlock() } + diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to open \(path)")) + return } + + guard let configuration = configuration( + atPath: lintFormatOptions.configurationPath, orInferredFromSwiftFileAtPath: path) + else { + // Already diagnosed in the called method. + return + } + + let fileToProcess = FileToProcess( + fileHandle: sourceFile, path: path, configuration: configuration) + processFile(fileToProcess) } + /// Returns the configuration that applies to the given `.swift` source file, when an explicit /// configuration path is also perhaps provided. /// diff --git a/Sources/swift-format/Subcommands/LintFormatOptions.swift b/Sources/swift-format/Subcommands/LintFormatOptions.swift index 01faf545c..8079b6c6a 100644 --- a/Sources/swift-format/Subcommands/LintFormatOptions.swift +++ b/Sources/swift-format/Subcommands/LintFormatOptions.swift @@ -49,6 +49,9 @@ struct LintFormatOptions: ParsableArguments { """) var ignoreUnparsableFiles: Bool + @Flag(help: "Whether or not to run formatting or linting in parallel consuming all possible resources") + var parallel: Bool + /// The list of paths to Swift source files that should be formatted or linted. @Argument(help: "Zero or more input filenames.") var paths: [String]