From f38025126040b01daf95555f7c6ee40f9e773c9e Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 14 Aug 2025 09:27:54 +0200 Subject: [PATCH 1/8] Allow alternate build systems to have larger indexing batch sizes --- .../BuildServerManager.swift | 7 +- .../Messages/InitializeBuildRequest.swift | 13 ++++ .../CustomBuildServerTestProject.swift | 4 +- .../SemanticIndex/SemanticIndexManager.swift | 10 ++- Sources/SourceKitLSP/Workspace.swift | 5 ++ .../BackgroundIndexingTests.swift | 75 +++++++++++++++++++ 6 files changed, 108 insertions(+), 6 deletions(-) diff --git a/Sources/BuildServerIntegration/BuildServerManager.swift b/Sources/BuildServerIntegration/BuildServerManager.swift index 5346d8337..3bda81ae3 100644 --- a/Sources/BuildServerIntegration/BuildServerManager.swift +++ b/Sources/BuildServerIntegration/BuildServerManager.swift @@ -322,9 +322,13 @@ package actor BuildServerManager: QueueBasedMessageHandler { private var connectionToClient: BuildServerManagerConnectionToClient - /// The build serer adapter that is used to answer build server queries. + /// The build server adapter that is used to answer build server queries. private var buildServerAdapter: BuildServerAdapter? + /// The kind of underlying build server adapter that is being managed. + /// `nil` if the `BuildServerManager` does not have an underlying build system. + private var kind: BuildServerSpec.Kind? + /// The build server adapter after initialization finishes. When sending messages to the BSP server, this should be /// preferred over `buildServerAdapter` because no messages must be sent to the build server before initialization /// finishes. @@ -451,6 +455,7 @@ package actor BuildServerManager: QueueBasedMessageHandler { self.toolchainRegistry = toolchainRegistry self.options = options self.connectionToClient = connectionToClient + self.kind = buildServerSpec?.kind self.configPath = buildServerSpec?.configPath self.buildServerAdapter = await buildServerSpec?.createBuildServerAdapter( toolchainRegistry: toolchainRegistry, diff --git a/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift b/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift index 15c720d70..71daca936 100644 --- a/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift +++ b/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift @@ -278,6 +278,9 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send /// Whether the server implements the `textDocument/sourceKitOptions` request. public var sourceKitOptionsProvider: Bool? + /// The number of targets to prepare concurrently, when an index request is scheduled. + public var indexTaskBatchSize: Int? + /// The files to watch for changes. public var watchers: [FileSystemWatcher]? @@ -286,12 +289,14 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send public init( indexDatabasePath: String? = nil, indexStorePath: String? = nil, + indexTaskBatchSize: Int? = nil, watchers: [FileSystemWatcher]? = nil, prepareProvider: Bool? = nil, sourceKitOptionsProvider: Bool? = nil ) { self.indexDatabasePath = indexDatabasePath self.indexStorePath = indexStorePath + self.indexTaskBatchSize = indexTaskBatchSize self.watchers = watchers self.prepareProvider = prepareProvider self.sourceKitOptionsProvider = sourceKitOptionsProvider @@ -300,6 +305,7 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send public init( indexDatabasePath: String? = nil, indexStorePath: String? = nil, + indexTaskBatchSize: Int? = nil, outputPathsProvider: Bool? = nil, prepareProvider: Bool? = nil, sourceKitOptionsProvider: Bool? = nil, @@ -307,6 +313,7 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send ) { self.indexDatabasePath = indexDatabasePath self.indexStorePath = indexStorePath + self.indexTaskBatchSize = indexTaskBatchSize self.outputPathsProvider = outputPathsProvider self.prepareProvider = prepareProvider self.sourceKitOptionsProvider = sourceKitOptionsProvider @@ -320,6 +327,9 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send if case .string(let indexStorePath) = dictionary[CodingKeys.indexStorePath.stringValue] { self.indexStorePath = indexStorePath } + if case .int(let indexTaskBatchSize) = dictionary[CodingKeys.indexTaskBatchSize.stringValue] { + self.indexTaskBatchSize = indexTaskBatchSize + } if case .bool(let outputPathsProvider) = dictionary[CodingKeys.outputPathsProvider.stringValue] { self.outputPathsProvider = outputPathsProvider } @@ -342,6 +352,9 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send if let indexStorePath { result[CodingKeys.indexStorePath.stringValue] = .string(indexStorePath) } + if let indexTaskBatchSize { + result[CodingKeys.indexTaskBatchSize.stringValue] = .int(indexTaskBatchSize) + } if let outputPathsProvider { result[CodingKeys.outputPathsProvider.stringValue] = .bool(outputPathsProvider) } diff --git a/Sources/SKTestSupport/CustomBuildServerTestProject.swift b/Sources/SKTestSupport/CustomBuildServerTestProject.swift index 2f73fdff6..fba25b296 100644 --- a/Sources/SKTestSupport/CustomBuildServerTestProject.swift +++ b/Sources/SKTestSupport/CustomBuildServerTestProject.swift @@ -175,12 +175,14 @@ package extension CustomBuildServer { func initializationResponseSupportingBackgroundIndexing( projectRoot: URL, - outputPathsProvider: Bool + outputPathsProvider: Bool, + indexTaskBatchSize: Int? = nil ) throws -> InitializeBuildResponse { return initializationResponse( initializeData: SourceKitInitializeBuildResponseData( indexDatabasePath: try projectRoot.appendingPathComponent("index-db").filePath, indexStorePath: try projectRoot.appendingPathComponent("index-store").filePath, + indexTaskBatchSize: indexTaskBatchSize, outputPathsProvider: outputPathsProvider, prepareProvider: true, sourceKitOptionsProvider: true diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index cad195edf..d318618b8 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -222,6 +222,9 @@ package final actor SemanticIndexManager { /// The parameter is the number of files that were scheduled to be indexed. private let indexTasksWereScheduled: @Sendable (_ numberOfFileScheduled: Int) -> Void + /// The number of targets to prepare concurrently, whenever a index request is scheduled. + private let indexTaskBatchSize: Int + /// Callback that is called when `progressStatus` might have changed. private let indexProgressStatusDidChange: @Sendable () -> Void @@ -261,6 +264,7 @@ package final actor SemanticIndexManager { updateIndexStoreTimeout: Duration, hooks: IndexHooks, indexTaskScheduler: TaskScheduler, + indexTaskBatchSize: Int, logMessageToIndexLog: @escaping @Sendable ( _ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind @@ -273,6 +277,7 @@ package final actor SemanticIndexManager { self.updateIndexStoreTimeout = updateIndexStoreTimeout self.hooks = hooks self.indexTaskScheduler = indexTaskScheduler + self.indexTaskBatchSize = indexTaskBatchSize self.logMessageToIndexLog = logMessageToIndexLog self.indexTasksWereScheduled = indexTasksWereScheduled self.indexProgressStatusDidChange = indexProgressStatusDidChange @@ -877,10 +882,7 @@ package final actor SemanticIndexManager { var indexTasks: [Task] = [] - // TODO: When we can index multiple targets concurrently in SwiftPM, increase the batch size to half the - // processor count, so we can get parallelism during preparation. - // (https://github.com/swiftlang/sourcekit-lsp/issues/1262) - for targetsBatch in sortedTargets.partition(intoBatchesOfSize: 1) { + for targetsBatch in sortedTargets.partition(intoBatchesOfSize: indexTaskBatchSize) { let preparationTaskID = UUID() let filesToIndex = targetsBatch.flatMap({ filesByTarget[$0]! }) diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 41fd7dc05..70b9ae6a2 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -162,12 +162,17 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { if options.backgroundIndexingOrDefault, let uncheckedIndex, await buildServerManager.initializationData?.prepareProvider ?? false { + // TODO: When we can index multiple targets concurrently in SwiftPM, we may want to default + // to something else other than 1. + // (https://github.com/swiftlang/sourcekit-lsp/issues/1262) + let batchSize = await buildServerManager.initializationData?.indexTaskBatchSize ?? 1 self.semanticIndexManager = SemanticIndexManager( index: uncheckedIndex, buildServerManager: buildServerManager, updateIndexStoreTimeout: options.indexOrDefault.updateIndexStoreTimeoutOrDefault, hooks: hooks.indexHooks, indexTaskScheduler: indexTaskScheduler, + indexTaskBatchSize: batchSize, logMessageToIndexLog: { [weak sourceKitLSPServer] in sourceKitLSPServer?.logMessageToIndexLog(message: $0, type: $1, structure: $2) }, diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 9538222d1..0ac776433 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -2624,6 +2624,81 @@ final class BackgroundIndexingTests: XCTestCase { let symbols = try await project.testClient.send(WorkspaceSymbolsRequest(query: "myTestFu")) XCTAssertEqual(symbols?.count, 1) } + + func testBuildServerUsesCustomTaskBatchSize() async throws { + final class BuildServer: CustomBuildServer { + let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker() + private let projectRoot: URL + private var testFileURL: URL { projectRoot.appendingPathComponent("test.swift").standardized } + + required init(projectRoot: URL, connectionToSourceKitLSP: any LanguageServerProtocol.Connection) { + self.projectRoot = projectRoot + } + + func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { + return try initializationResponseSupportingBackgroundIndexing( + projectRoot: projectRoot, + outputPathsProvider: false, + indexTaskBatchSize: 3 + ) + } + + func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { + var dummyTargets = [BuildTargetIdentifier]() + for i in 0..<10 { + dummyTargets.append(BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-\(i)"))) + } + return BuildTargetSourcesResponse(items: dummyTargets.map { + SourcesItem(target: $0, sources: [SourceItem(uri: URI(testFileURL), kind: .file, generated: false)]) + }) + } + + func textDocumentSourceKitOptionsRequest( + _ request: TextDocumentSourceKitOptionsRequest + ) async throws -> TextDocumentSourceKitOptionsResponse? { + return TextDocumentSourceKitOptionsResponse(compilerArguments: [request.textDocument.uri.pseudoPath]) + } + } + + let preparationTaskSemaphore = WrappedSemaphore(name: "Received a preparation task") + let preparationTasks = ThreadSafeBox<[PreparationTaskDescription]>(initialValue: []) + let project = try await CustomBuildServerTestProject( + files: [ + "test.swift": """ + func testFunction() {} + """ + ], + buildServer: BuildServer.self, + hooks: Hooks( + indexHooks: IndexHooks( + preparationTaskDidStart: { task in + preparationTasks.withLock { preparationTasks in + // Ignore everything after the 4th batch to avoid flakiness. + if preparationTasks.count < 4 { + preparationTasks.append(task) + } + if preparationTasks.count == 4 { + preparationTaskSemaphore.signal() + } + } + } + ) + ), + enableBackgroundIndexing: true + ) + + _ = try await project.testClient.send(SynchronizeRequest(index: true)) + + preparationTaskSemaphore.waitOrXCTFail() + + // The test project has 10 targets, and we should have received them in batches of 3. + XCTAssertEqual(preparationTasks.value.count, 4) + let preparedTargetBatches = preparationTasks.value.map(\.targetsToPrepare).sorted { $0.count > $1.count } + XCTAssertEqual(preparedTargetBatches[0].count, 3) + XCTAssertEqual(preparedTargetBatches[1].count, 3) + XCTAssertEqual(preparedTargetBatches[2].count, 3) + XCTAssertEqual(preparedTargetBatches[3].count, 1) + } } extension HoverResponseContents { From b62d91a0ce3896cf67a2f0a9408ad4dc26e64bbb Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 14 Aug 2025 15:22:28 +0200 Subject: [PATCH 2/8] Let the server control the batch size instead --- Contributor Documentation/BSP Extensions.md | 5 +- .../BuiltInBuildServer.swift | 3 + .../BuiltInBuildServerAdapter.swift | 1 + .../FixedCompilationDatabaseBuildServer.swift | 2 + .../JSONCompilationDatabaseBuildServer.swift | 2 + .../LegacyBuildServer.swift | 2 + .../SwiftPMBuildServer.swift | 2 + .../Messages/InitializeBuildRequest.swift | 45 +++-- .../CustomBuildServerTestProject.swift | 6 +- .../SemanticIndex/SemanticIndexManager.swift | 23 +-- Sources/SemanticIndex/TaskScheduler.swift | 1 - Sources/SourceKitLSP/Workspace.swift | 7 +- .../BackgroundIndexingTests.swift | 163 ++++++++++-------- 13 files changed, 155 insertions(+), 107 deletions(-) diff --git a/Contributor Documentation/BSP Extensions.md b/Contributor Documentation/BSP Extensions.md index cb28c6f3d..d513768a3 100644 --- a/Contributor Documentation/BSP Extensions.md +++ b/Contributor Documentation/BSP Extensions.md @@ -22,6 +22,9 @@ export interface SourceKitInitializeBuildResponseData { * for `swiftc` or `clang` invocations **/ indexStorePath?: string; + /** Options to control how many targets should be prepared simultaneously by SourceKit-LSP. */ + multiTargetPreparation?: MultiTargetPreparationSupport + /** Whether the server set the `outputPath` property in the `buildTarget/sources` request */ outputPathsProvider?: bool; @@ -34,7 +37,7 @@ export interface SourceKitInitializeBuildResponseData { /** The files to watch for changes. * Changes to these files are sent to the BSP server using `workspace/didChangeWatchedFiles`. * `FileSystemWatcher` is the same as in LSP. */ - watchers: [FileSystemWatcher]? + watchers?: [FileSystemWatcher] } ``` diff --git a/Sources/BuildServerIntegration/BuiltInBuildServer.swift b/Sources/BuildServerIntegration/BuiltInBuildServer.swift index 814448e61..aabaecaca 100644 --- a/Sources/BuildServerIntegration/BuiltInBuildServer.swift +++ b/Sources/BuildServerIntegration/BuiltInBuildServer.swift @@ -35,6 +35,9 @@ package protocol BuiltInBuildServer: AnyObject, Sendable { /// The path to put the index database, if any. var indexDatabasePath: URL? { get async } + /// Whether the build server can prepare multiple targets in parallel. + var supportsMultiTargetPreparation: Bool { get } + /// Whether the build server is capable of preparing a target for indexing and determining the output paths for the /// target, ie. whether the `prepare` method has been implemented and this build server populates the `outputPath` /// property in the `buildTarget/sources` request. diff --git a/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift b/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift index a22396175..328a2d45b 100644 --- a/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift +++ b/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift @@ -94,6 +94,7 @@ actor BuiltInBuildServerAdapter: QueueBasedMessageHandler { indexStorePath: await orLog("getting index store file path") { try await underlyingBuildServer.indexStorePath?.filePath }, + multiTargetPreparation: MultiTargetPreparationSupport(supported: underlyingBuildServer.supportsMultiTargetPreparation), outputPathsProvider: underlyingBuildServer.supportsPreparationAndOutputPaths, prepareProvider: underlyingBuildServer.supportsPreparationAndOutputPaths, sourceKitOptionsProvider: true, diff --git a/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift b/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift index ef084482a..46370c77e 100644 --- a/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift +++ b/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift @@ -59,6 +59,8 @@ package actor FixedCompilationDatabaseBuildServer: BuiltInBuildServer { indexStorePath?.deletingLastPathComponent().appendingPathComponent("IndexDatabase") } + package nonisolated var supportsMultiTargetPreparation: Bool { true } + package nonisolated var supportsPreparationAndOutputPaths: Bool { false } private static func parseCompileFlags(at configPath: URL) throws -> [String] { diff --git a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift b/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift index abd8e5196..acf5eb42e 100644 --- a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift +++ b/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift @@ -96,6 +96,8 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer { indexStorePath?.deletingLastPathComponent().appendingPathComponent("IndexDatabase") } + package nonisolated var supportsMultiTargetPreparation: Bool { true } + package nonisolated var supportsPreparationAndOutputPaths: Bool { false } package init( diff --git a/Sources/BuildServerIntegration/LegacyBuildServer.swift b/Sources/BuildServerIntegration/LegacyBuildServer.swift index 44e49b262..026454e1a 100644 --- a/Sources/BuildServerIntegration/LegacyBuildServer.swift +++ b/Sources/BuildServerIntegration/LegacyBuildServer.swift @@ -138,6 +138,8 @@ actor LegacyBuildServer: MessageHandler, BuiltInBuildServer { connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) } + package nonisolated var supportsMultiTargetPreparation: Bool { true } + package nonisolated var supportsPreparationAndOutputPaths: Bool { false } package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse { diff --git a/Sources/BuildServerIntegration/SwiftPMBuildServer.swift b/Sources/BuildServerIntegration/SwiftPMBuildServer.swift index 4f9737a23..db1223820 100644 --- a/Sources/BuildServerIntegration/SwiftPMBuildServer.swift +++ b/Sources/BuildServerIntegration/SwiftPMBuildServer.swift @@ -459,6 +459,8 @@ package actor SwiftPMBuildServer: BuiltInBuildServer { connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) } + package nonisolated var supportsMultiTargetPreparation: Bool { false } + package nonisolated var supportsPreparationAndOutputPaths: Bool { options.backgroundIndexingOrDefault } package var buildPath: URL { diff --git a/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift b/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift index 71daca936..4e01f70e6 100644 --- a/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift +++ b/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift @@ -269,6 +269,9 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send /// The path at which SourceKit-LSP can store its index database, aggregating data from `indexStorePath` public var indexStorePath: String? + /// Options to control how many targets should be prepared simultaneously by SourceKit-LSP. + public var multiTargetPreparation: MultiTargetPreparationSupport? + /// Whether the server implements the `buildTarget/outputPaths` request. public var outputPathsProvider: Bool? @@ -278,9 +281,6 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send /// Whether the server implements the `textDocument/sourceKitOptions` request. public var sourceKitOptionsProvider: Bool? - /// The number of targets to prepare concurrently, when an index request is scheduled. - public var indexTaskBatchSize: Int? - /// The files to watch for changes. public var watchers: [FileSystemWatcher]? @@ -289,14 +289,14 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send public init( indexDatabasePath: String? = nil, indexStorePath: String? = nil, - indexTaskBatchSize: Int? = nil, + multiTargetPreparation: MultiTargetPreparationSupport? = nil, watchers: [FileSystemWatcher]? = nil, prepareProvider: Bool? = nil, sourceKitOptionsProvider: Bool? = nil ) { self.indexDatabasePath = indexDatabasePath self.indexStorePath = indexStorePath - self.indexTaskBatchSize = indexTaskBatchSize + self.multiTargetPreparation = multiTargetPreparation self.watchers = watchers self.prepareProvider = prepareProvider self.sourceKitOptionsProvider = sourceKitOptionsProvider @@ -305,7 +305,7 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send public init( indexDatabasePath: String? = nil, indexStorePath: String? = nil, - indexTaskBatchSize: Int? = nil, + multiTargetPreparation: MultiTargetPreparationSupport? = nil, outputPathsProvider: Bool? = nil, prepareProvider: Bool? = nil, sourceKitOptionsProvider: Bool? = nil, @@ -313,7 +313,7 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send ) { self.indexDatabasePath = indexDatabasePath self.indexStorePath = indexStorePath - self.indexTaskBatchSize = indexTaskBatchSize + self.multiTargetPreparation = multiTargetPreparation self.outputPathsProvider = outputPathsProvider self.prepareProvider = prepareProvider self.sourceKitOptionsProvider = sourceKitOptionsProvider @@ -327,8 +327,8 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send if case .string(let indexStorePath) = dictionary[CodingKeys.indexStorePath.stringValue] { self.indexStorePath = indexStorePath } - if case .int(let indexTaskBatchSize) = dictionary[CodingKeys.indexTaskBatchSize.stringValue] { - self.indexTaskBatchSize = indexTaskBatchSize + if case .dictionary(let multiTargetPreparation) = dictionary[CodingKeys.multiTargetPreparation.stringValue] { + self.multiTargetPreparation = MultiTargetPreparationSupport(fromLSPDictionary: multiTargetPreparation) } if case .bool(let outputPathsProvider) = dictionary[CodingKeys.outputPathsProvider.stringValue] { self.outputPathsProvider = outputPathsProvider @@ -352,8 +352,8 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send if let indexStorePath { result[CodingKeys.indexStorePath.stringValue] = .string(indexStorePath) } - if let indexTaskBatchSize { - result[CodingKeys.indexTaskBatchSize.stringValue] = .int(indexTaskBatchSize) + if let multiTargetPreparation { + result[CodingKeys.multiTargetPreparation.stringValue] = multiTargetPreparation.encodeToLSPAny() } if let outputPathsProvider { result[CodingKeys.outputPathsProvider.stringValue] = .bool(outputPathsProvider) @@ -370,3 +370,26 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send return .dictionary(result) } } + +public struct MultiTargetPreparationSupport: LSPAnyCodable, Codable, Sendable { + /// Whether the build server can prepare multiple targets in parallel. If this value is omitted, it is assumed to be `true`. + public var supported: Bool? + + public init(supported: Bool? = nil) { + self.supported = supported + } + + public init?(fromLSPDictionary dictionary: [String: LanguageServerProtocol.LSPAny]) { + if case .bool(let supported) = dictionary[CodingKeys.supported.stringValue] { + self.supported = supported + } + } + + public func encodeToLSPAny() -> LanguageServerProtocol.LSPAny { + var result: [String: LSPAny] = [:] + if let supported { + result[CodingKeys.supported.stringValue] = .bool(supported) + } + return .dictionary(result) + } +} diff --git a/Sources/SKTestSupport/CustomBuildServerTestProject.swift b/Sources/SKTestSupport/CustomBuildServerTestProject.swift index fba25b296..cb5e92298 100644 --- a/Sources/SKTestSupport/CustomBuildServerTestProject.swift +++ b/Sources/SKTestSupport/CustomBuildServerTestProject.swift @@ -175,17 +175,15 @@ package extension CustomBuildServer { func initializationResponseSupportingBackgroundIndexing( projectRoot: URL, - outputPathsProvider: Bool, - indexTaskBatchSize: Int? = nil + outputPathsProvider: Bool ) throws -> InitializeBuildResponse { return initializationResponse( initializeData: SourceKitInitializeBuildResponseData( indexDatabasePath: try projectRoot.appendingPathComponent("index-db").filePath, indexStorePath: try projectRoot.appendingPathComponent("index-store").filePath, - indexTaskBatchSize: indexTaskBatchSize, outputPathsProvider: outputPathsProvider, prepareProvider: true, - sourceKitOptionsProvider: true + sourceKitOptionsProvider: true, ) ) } diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index d318618b8..02f716aa0 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -222,8 +222,8 @@ package final actor SemanticIndexManager { /// The parameter is the number of files that were scheduled to be indexed. private let indexTasksWereScheduled: @Sendable (_ numberOfFileScheduled: Int) -> Void - /// The number of targets to prepare concurrently, whenever a index request is scheduled. - private let indexTaskBatchSize: Int + /// Determines whether or not the `SemanticIndexManager` should dispatch preparation tasks in batches. + private let shouldIndexInParallel: Bool /// Callback that is called when `progressStatus` might have changed. private let indexProgressStatusDidChange: @Sendable () -> Void @@ -264,7 +264,7 @@ package final actor SemanticIndexManager { updateIndexStoreTimeout: Duration, hooks: IndexHooks, indexTaskScheduler: TaskScheduler, - indexTaskBatchSize: Int, + shouldIndexInParallel: Bool, logMessageToIndexLog: @escaping @Sendable ( _ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind @@ -277,7 +277,7 @@ package final actor SemanticIndexManager { self.updateIndexStoreTimeout = updateIndexStoreTimeout self.hooks = hooks self.indexTaskScheduler = indexTaskScheduler - self.indexTaskBatchSize = indexTaskBatchSize + self.shouldIndexInParallel = shouldIndexInParallel self.logMessageToIndexLog = logMessageToIndexLog self.indexTasksWereScheduled = indexTasksWereScheduled self.indexProgressStatusDidChange = indexProgressStatusDidChange @@ -654,7 +654,6 @@ package final actor SemanticIndexManager { guard !targetsToPrepare.isEmpty else { return } - let taskDescription = AnyIndexTaskDescription( PreparationTaskDescription( targetsToPrepare: targetsToPrepare, @@ -882,7 +881,14 @@ package final actor SemanticIndexManager { var indexTasks: [Task] = [] - for targetsBatch in sortedTargets.partition(intoBatchesOfSize: indexTaskBatchSize) { + let batchSize: Int + if shouldIndexInParallel { + let processorCount = ProcessInfo.processInfo.activeProcessorCount + batchSize = max(1, processorCount * 5) + } else { + batchSize = 1 + } + for targetsBatch in sortedTargets.partition(intoBatchesOfSize: batchSize) { let preparationTaskID = UUID() let filesToIndex = targetsBatch.flatMap({ filesByTarget[$0]! }) @@ -906,10 +912,7 @@ package final actor SemanticIndexManager { // And after preparation is done, index the files in the targets. await withTaskGroup(of: Void.self) { taskGroup in for target in targetsBatch { - // TODO: Once swiftc supports indexing of multiple files in a single invocation, increase the batch size to - // allow it to share AST builds between multiple files within a target. - // (https://github.com/swiftlang/sourcekit-lsp/issues/1268) - for fileBatch in filesByTarget[target]!.partition(intoBatchesOfSize: 1) { + for fileBatch in filesByTarget[target]!.partition(intoBatchesOfSize: batchSize) { taskGroup.addTask { await self.updateIndexStore( for: fileBatch, diff --git a/Sources/SemanticIndex/TaskScheduler.swift b/Sources/SemanticIndex/TaskScheduler.swift index 56c2632b4..e6a8d8f24 100644 --- a/Sources/SemanticIndex/TaskScheduler.swift +++ b/Sources/SemanticIndex/TaskScheduler.swift @@ -370,7 +370,6 @@ package actor TaskScheduler { // We didn't actually change anything, so we don't need to perform any validation or task processing. return } - // Check we are over-subscribed in currently executing tasks by walking through all currently executing tasks and // checking if we could schedule them within the new execution limits. Cancel any tasks that do not fit within the // new limit to be rescheduled when we are within the limit again. diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 70b9ae6a2..c9ce196ad 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -162,17 +162,14 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { if options.backgroundIndexingOrDefault, let uncheckedIndex, await buildServerManager.initializationData?.prepareProvider ?? false { - // TODO: When we can index multiple targets concurrently in SwiftPM, we may want to default - // to something else other than 1. - // (https://github.com/swiftlang/sourcekit-lsp/issues/1262) - let batchSize = await buildServerManager.initializationData?.indexTaskBatchSize ?? 1 + let shouldIndexInParallel = await buildServerManager.initializationData?.multiTargetPreparation?.supported ?? true self.semanticIndexManager = SemanticIndexManager( index: uncheckedIndex, buildServerManager: buildServerManager, updateIndexStoreTimeout: options.indexOrDefault.updateIndexStoreTimeoutOrDefault, hooks: hooks.indexHooks, indexTaskScheduler: indexTaskScheduler, - indexTaskBatchSize: batchSize, + shouldIndexInParallel: shouldIndexInParallel, logMessageToIndexLog: { [weak sourceKitLSPServer] in sourceKitLSPServer?.logMessageToIndexLog(message: $0, type: $1, structure: $2) }, diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 0ac776433..f942c11df 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -2625,81 +2625,94 @@ final class BackgroundIndexingTests: XCTestCase { XCTAssertEqual(symbols?.count, 1) } - func testBuildServerUsesCustomTaskBatchSize() async throws { - final class BuildServer: CustomBuildServer { - let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker() - private let projectRoot: URL - private var testFileURL: URL { projectRoot.appendingPathComponent("test.swift").standardized } - - required init(projectRoot: URL, connectionToSourceKitLSP: any LanguageServerProtocol.Connection) { - self.projectRoot = projectRoot - } - - func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { - return try initializationResponseSupportingBackgroundIndexing( - projectRoot: projectRoot, - outputPathsProvider: false, - indexTaskBatchSize: 3 - ) - } - - func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { - var dummyTargets = [BuildTargetIdentifier]() - for i in 0..<10 { - dummyTargets.append(BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-\(i)"))) - } - return BuildTargetSourcesResponse(items: dummyTargets.map { - SourcesItem(target: $0, sources: [SourceItem(uri: URI(testFileURL), kind: .file, generated: false)]) - }) - } - - func textDocumentSourceKitOptionsRequest( - _ request: TextDocumentSourceKitOptionsRequest - ) async throws -> TextDocumentSourceKitOptionsResponse? { - return TextDocumentSourceKitOptionsResponse(compilerArguments: [request.textDocument.uri.pseudoPath]) - } - } - - let preparationTaskSemaphore = WrappedSemaphore(name: "Received a preparation task") - let preparationTasks = ThreadSafeBox<[PreparationTaskDescription]>(initialValue: []) - let project = try await CustomBuildServerTestProject( - files: [ - "test.swift": """ - func testFunction() {} - """ - ], - buildServer: BuildServer.self, - hooks: Hooks( - indexHooks: IndexHooks( - preparationTaskDidStart: { task in - preparationTasks.withLock { preparationTasks in - // Ignore everything after the 4th batch to avoid flakiness. - if preparationTasks.count < 4 { - preparationTasks.append(task) - } - if preparationTasks.count == 4 { - preparationTaskSemaphore.signal() - } - } - } - ) - ), - enableBackgroundIndexing: true - ) - - _ = try await project.testClient.send(SynchronizeRequest(index: true)) - - preparationTaskSemaphore.waitOrXCTFail() - - // The test project has 10 targets, and we should have received them in batches of 3. - XCTAssertEqual(preparationTasks.value.count, 4) - let preparedTargetBatches = preparationTasks.value.map(\.targetsToPrepare).sorted { $0.count > $1.count } - XCTAssertEqual(preparedTargetBatches[0].count, 3) - XCTAssertEqual(preparedTargetBatches[1].count, 3) - XCTAssertEqual(preparedTargetBatches[2].count, 3) - XCTAssertEqual(preparedTargetBatches[3].count, 1) - } -} +// WIP, not ready for review +// func testBuildServerUsesCustomTaskBatchSize() async throws { +// final class BuildServer: CustomBuildServer { +// let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker() +// private let projectRoot: URL +// private var testFileURL: URL { projectRoot.appendingPathComponent("test.swift").standardized } + +// nonisolated(unsafe) var preparedTargetBatches = [[BuildTargetIdentifier]]() + +// required init(projectRoot: URL, connectionToSourceKitLSP: any LanguageServerProtocol.Connection) { +// self.projectRoot = projectRoot +// } + +// func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { +// return try initializationResponseSupportingBackgroundIndexing( +// projectRoot: projectRoot, +// outputPathsProvider: false, +// ) +// } + +// func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { +// var dummyTargets = [BuildTargetIdentifier]() +// for i in 0..<10 { +// dummyTargets.append(BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-\(i)"))) +// } +// return BuildTargetSourcesResponse(items: dummyTargets.map { +// SourcesItem(target: $0, sources: [SourceItem(uri: URI(testFileURL), kind: .file, generated: false)]) +// }) +// } + +// func textDocumentSourceKitOptionsRequest( +// _ request: TextDocumentSourceKitOptionsRequest +// ) async throws -> TextDocumentSourceKitOptionsResponse? { +// return TextDocumentSourceKitOptionsResponse(compilerArguments: [request.textDocument.uri.pseudoPath]) +// } + +// func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse { +// preparedTargetBatches.append(request.targets.sorted { $0.uri.stringValue < $1.uri.stringValue }) +// return VoidResponse() +// } +// } + +// let project = try await CustomBuildServerTestProject( +// files: [ +// "test.swift": """ +// func testFunction() {} +// """ +// ], +// buildServer: BuildServer.self, +// enableBackgroundIndexing: true, +// ) + +// // Wait for indexing to finish without elevating the priority +// // Otherwise, task re-scheduling would cause the test to become flaky +// let semaphore = WrappedSemaphore(name: "Indexing finished") +// let testClient = project.testClient +// Task(priority: .low) { +// await assertNoThrow { +// try await testClient.send(SynchronizeRequest(index: true)) +// } +// semaphore.signal() +// } +// try semaphore.waitOrThrow() + +// let buildServer = try project.buildServer() +// let preparedBatches = buildServer.preparedTargetBatches.sorted { $0[0].uri.stringValue < $1[0].uri.stringValue } +// XCTAssertEqual(preparedBatches, [ +// [ +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-0")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-1")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-2")), +// ], +// [ +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-3")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-4")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-5")), +// ], +// [ +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-6")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-7")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-8")), +// ], +// [ +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-9")), +// ] +// ]) +// } +// } extension HoverResponseContents { var markupContent: MarkupContent? { From ef3a6dd1627835ebedb61fde0e76e3fa351e0a44 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 14 Aug 2025 15:57:59 +0200 Subject: [PATCH 3/8] (Temporary) Remove unit test --- .../BackgroundIndexingTests.swift | 89 ------------------- 1 file changed, 89 deletions(-) diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index f942c11df..262623384 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -2625,95 +2625,6 @@ final class BackgroundIndexingTests: XCTestCase { XCTAssertEqual(symbols?.count, 1) } -// WIP, not ready for review -// func testBuildServerUsesCustomTaskBatchSize() async throws { -// final class BuildServer: CustomBuildServer { -// let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker() -// private let projectRoot: URL -// private var testFileURL: URL { projectRoot.appendingPathComponent("test.swift").standardized } - -// nonisolated(unsafe) var preparedTargetBatches = [[BuildTargetIdentifier]]() - -// required init(projectRoot: URL, connectionToSourceKitLSP: any LanguageServerProtocol.Connection) { -// self.projectRoot = projectRoot -// } - -// func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { -// return try initializationResponseSupportingBackgroundIndexing( -// projectRoot: projectRoot, -// outputPathsProvider: false, -// ) -// } - -// func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { -// var dummyTargets = [BuildTargetIdentifier]() -// for i in 0..<10 { -// dummyTargets.append(BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-\(i)"))) -// } -// return BuildTargetSourcesResponse(items: dummyTargets.map { -// SourcesItem(target: $0, sources: [SourceItem(uri: URI(testFileURL), kind: .file, generated: false)]) -// }) -// } - -// func textDocumentSourceKitOptionsRequest( -// _ request: TextDocumentSourceKitOptionsRequest -// ) async throws -> TextDocumentSourceKitOptionsResponse? { -// return TextDocumentSourceKitOptionsResponse(compilerArguments: [request.textDocument.uri.pseudoPath]) -// } - -// func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse { -// preparedTargetBatches.append(request.targets.sorted { $0.uri.stringValue < $1.uri.stringValue }) -// return VoidResponse() -// } -// } - -// let project = try await CustomBuildServerTestProject( -// files: [ -// "test.swift": """ -// func testFunction() {} -// """ -// ], -// buildServer: BuildServer.self, -// enableBackgroundIndexing: true, -// ) - -// // Wait for indexing to finish without elevating the priority -// // Otherwise, task re-scheduling would cause the test to become flaky -// let semaphore = WrappedSemaphore(name: "Indexing finished") -// let testClient = project.testClient -// Task(priority: .low) { -// await assertNoThrow { -// try await testClient.send(SynchronizeRequest(index: true)) -// } -// semaphore.signal() -// } -// try semaphore.waitOrThrow() - -// let buildServer = try project.buildServer() -// let preparedBatches = buildServer.preparedTargetBatches.sorted { $0[0].uri.stringValue < $1[0].uri.stringValue } -// XCTAssertEqual(preparedBatches, [ -// [ -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-0")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-1")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-2")), -// ], -// [ -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-3")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-4")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-5")), -// ], -// [ -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-6")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-7")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-8")), -// ], -// [ -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-9")), -// ] -// ]) -// } -// } - extension HoverResponseContents { var markupContent: MarkupContent? { switch self { From 7c96ab66ad916655738bd341a178d8086b6c6f0e Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 14 Aug 2025 15:58:47 +0200 Subject: [PATCH 4/8] Remove unused kind param --- Contributor Documentation/BSP Extensions.md | 5 +++++ Sources/BuildServerIntegration/BuildServerManager.swift | 5 ----- Sources/SKTestSupport/CustomBuildServerTestProject.swift | 2 +- Sources/SemanticIndex/SemanticIndexManager.swift | 1 + Sources/SemanticIndex/TaskScheduler.swift | 1 + Tests/SourceKitLSPTests/BackgroundIndexingTests.swift | 1 + 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Contributor Documentation/BSP Extensions.md b/Contributor Documentation/BSP Extensions.md index d513768a3..de5947b2b 100644 --- a/Contributor Documentation/BSP Extensions.md +++ b/Contributor Documentation/BSP Extensions.md @@ -39,6 +39,11 @@ export interface SourceKitInitializeBuildResponseData { * `FileSystemWatcher` is the same as in LSP. */ watchers?: [FileSystemWatcher] } + +export interface MultiTargetPreparationSupport { + /** Whether the build server can prepare multiple targets in parallel. If this value is omitted, it is assumed to be `true`. */ + supported: bool; +} ``` ## `build/taskStart` diff --git a/Sources/BuildServerIntegration/BuildServerManager.swift b/Sources/BuildServerIntegration/BuildServerManager.swift index 3bda81ae3..679ddcbec 100644 --- a/Sources/BuildServerIntegration/BuildServerManager.swift +++ b/Sources/BuildServerIntegration/BuildServerManager.swift @@ -325,10 +325,6 @@ package actor BuildServerManager: QueueBasedMessageHandler { /// The build server adapter that is used to answer build server queries. private var buildServerAdapter: BuildServerAdapter? - /// The kind of underlying build server adapter that is being managed. - /// `nil` if the `BuildServerManager` does not have an underlying build system. - private var kind: BuildServerSpec.Kind? - /// The build server adapter after initialization finishes. When sending messages to the BSP server, this should be /// preferred over `buildServerAdapter` because no messages must be sent to the build server before initialization /// finishes. @@ -455,7 +451,6 @@ package actor BuildServerManager: QueueBasedMessageHandler { self.toolchainRegistry = toolchainRegistry self.options = options self.connectionToClient = connectionToClient - self.kind = buildServerSpec?.kind self.configPath = buildServerSpec?.configPath self.buildServerAdapter = await buildServerSpec?.createBuildServerAdapter( toolchainRegistry: toolchainRegistry, diff --git a/Sources/SKTestSupport/CustomBuildServerTestProject.swift b/Sources/SKTestSupport/CustomBuildServerTestProject.swift index cb5e92298..2f73fdff6 100644 --- a/Sources/SKTestSupport/CustomBuildServerTestProject.swift +++ b/Sources/SKTestSupport/CustomBuildServerTestProject.swift @@ -183,7 +183,7 @@ package extension CustomBuildServer { indexStorePath: try projectRoot.appendingPathComponent("index-store").filePath, outputPathsProvider: outputPathsProvider, prepareProvider: true, - sourceKitOptionsProvider: true, + sourceKitOptionsProvider: true ) ) } diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index 02f716aa0..182df9fc1 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -654,6 +654,7 @@ package final actor SemanticIndexManager { guard !targetsToPrepare.isEmpty else { return } + let taskDescription = AnyIndexTaskDescription( PreparationTaskDescription( targetsToPrepare: targetsToPrepare, diff --git a/Sources/SemanticIndex/TaskScheduler.swift b/Sources/SemanticIndex/TaskScheduler.swift index e6a8d8f24..56c2632b4 100644 --- a/Sources/SemanticIndex/TaskScheduler.swift +++ b/Sources/SemanticIndex/TaskScheduler.swift @@ -370,6 +370,7 @@ package actor TaskScheduler { // We didn't actually change anything, so we don't need to perform any validation or task processing. return } + // Check we are over-subscribed in currently executing tasks by walking through all currently executing tasks and // checking if we could schedule them within the new execution limits. Cancel any tasks that do not fit within the // new limit to be rescheduled when we are within the limit again. diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 262623384..9538222d1 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -2624,6 +2624,7 @@ final class BackgroundIndexingTests: XCTestCase { let symbols = try await project.testClient.send(WorkspaceSymbolsRequest(query: "myTestFu")) XCTAssertEqual(symbols?.count, 1) } +} extension HoverResponseContents { var markupContent: MarkupContent? { From 25c5a52e967ccc0fe533ea6d055adbdcaf692f55 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 15 Aug 2025 09:50:44 +0200 Subject: [PATCH 5/8] Handle prepare task cancellation based on the purpose --- .../PreparationTaskDescription.swift | 23 +++++++++++---- .../SemanticIndex/SemanticIndexManager.swift | 29 +++++-------------- Sources/SourceKitLSP/Workspace.swift | 9 +++++- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Sources/SemanticIndex/PreparationTaskDescription.swift b/Sources/SemanticIndex/PreparationTaskDescription.swift index 4d45be438..f19a83352 100644 --- a/Sources/SemanticIndex/PreparationTaskDescription.swift +++ b/Sources/SemanticIndex/PreparationTaskDescription.swift @@ -39,6 +39,9 @@ package struct PreparationTaskDescription: IndexTaskDescription { private let preparationUpToDateTracker: UpToDateTracker + /// The purpose of the preparation task. + private let purpose: TargetPreparationPurpose + /// See `SemanticIndexManager.logMessageToIndexLog`. private let logMessageToIndexLog: @Sendable (_ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind) -> Void @@ -63,6 +66,7 @@ package struct PreparationTaskDescription: IndexTaskDescription { targetsToPrepare: [BuildTargetIdentifier], buildServerManager: BuildServerManager, preparationUpToDateTracker: UpToDateTracker, + purpose: TargetPreparationPurpose, logMessageToIndexLog: @escaping @Sendable ( _ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind @@ -72,6 +76,7 @@ package struct PreparationTaskDescription: IndexTaskDescription { self.targetsToPrepare = targetsToPrepare self.buildServerManager = buildServerManager self.preparationUpToDateTracker = preparationUpToDateTracker + self.purpose = purpose self.logMessageToIndexLog = logMessageToIndexLog self.hooks = hooks } @@ -119,14 +124,22 @@ package struct PreparationTaskDescription: IndexTaskDescription { to currentlyExecutingTasks: [PreparationTaskDescription] ) -> [TaskDependencyAction] { return currentlyExecutingTasks.compactMap { (other) -> TaskDependencyAction? in - if other.targetsToPrepare.count > self.targetsToPrepare.count { - // If there is an prepare operation with more targets already running, suspend it. - // The most common use case for this is if we prepare all targets simultaneously during the initial preparation - // when a project is opened and need a single target indexed for user interaction. We should suspend the - // workspace-wide preparation and just prepare the currently needed target. + if other.purpose == .forIndexing && self.purpose == .forEditorFunctionality { + // If we're running a background indexing operation but need a target indexed for user interaction, + // we should prioritize the latter. return .cancelAndRescheduleDependency(other) } return .waitAndElevatePriorityOfDependency(other) } } } + +/// The reason why a target is being prepared. This is used to determine the `IndexProgressStatus` +/// and to prioritize preparation tasks when several of them are running. +package enum TargetPreparationPurpose: Comparable { + /// We are preparing the target so we can index files in it. + case forIndexing + + /// We are preparing the target to provide semantic functionality in one of its files. + case forEditorFunctionality +} diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index 182df9fc1..5335a0edb 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -145,15 +145,6 @@ private struct InProgressPrepareForEditorTask { let task: Task } -/// The reason why a target is being prepared. This is used to determine the `IndexProgressStatus`. -private enum TargetPreparationPurpose: Comparable { - /// We are preparing the target so we can index files in it. - case forIndexing - - /// We are preparing the target to provide semantic functionality in one of its files. - case forEditorFunctionality -} - /// An entry in `SemanticIndexManager.inProgressPreparationTasks`. private struct InProgressPreparationTask { let task: OpaqueQueuedIndexTask @@ -222,8 +213,8 @@ package final actor SemanticIndexManager { /// The parameter is the number of files that were scheduled to be indexed. private let indexTasksWereScheduled: @Sendable (_ numberOfFileScheduled: Int) -> Void - /// Determines whether or not the `SemanticIndexManager` should dispatch preparation tasks in batches. - private let shouldIndexInParallel: Bool + /// The size of the batches in which the `SemanticIndexManager` should dispatch preparation tasks. + private let indexTaskBatchSize: Int /// Callback that is called when `progressStatus` might have changed. private let indexProgressStatusDidChange: @Sendable () -> Void @@ -264,7 +255,7 @@ package final actor SemanticIndexManager { updateIndexStoreTimeout: Duration, hooks: IndexHooks, indexTaskScheduler: TaskScheduler, - shouldIndexInParallel: Bool, + indexTaskBatchSize: Int, logMessageToIndexLog: @escaping @Sendable ( _ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind @@ -277,7 +268,7 @@ package final actor SemanticIndexManager { self.updateIndexStoreTimeout = updateIndexStoreTimeout self.hooks = hooks self.indexTaskScheduler = indexTaskScheduler - self.shouldIndexInParallel = shouldIndexInParallel + self.indexTaskBatchSize = indexTaskBatchSize self.logMessageToIndexLog = logMessageToIndexLog self.indexTasksWereScheduled = indexTasksWereScheduled self.indexProgressStatusDidChange = indexProgressStatusDidChange @@ -660,6 +651,7 @@ package final actor SemanticIndexManager { targetsToPrepare: targetsToPrepare, buildServerManager: self.buildServerManager, preparationUpToDateTracker: preparationUpToDateTracker, + purpose: purpose, logMessageToIndexLog: logMessageToIndexLog, hooks: hooks ) @@ -882,14 +874,7 @@ package final actor SemanticIndexManager { var indexTasks: [Task] = [] - let batchSize: Int - if shouldIndexInParallel { - let processorCount = ProcessInfo.processInfo.activeProcessorCount - batchSize = max(1, processorCount * 5) - } else { - batchSize = 1 - } - for targetsBatch in sortedTargets.partition(intoBatchesOfSize: batchSize) { + for targetsBatch in sortedTargets.partition(intoBatchesOfSize: indexTaskBatchSize) { let preparationTaskID = UUID() let filesToIndex = targetsBatch.flatMap({ filesByTarget[$0]! }) @@ -913,7 +898,7 @@ package final actor SemanticIndexManager { // And after preparation is done, index the files in the targets. await withTaskGroup(of: Void.self) { taskGroup in for target in targetsBatch { - for fileBatch in filesByTarget[target]!.partition(intoBatchesOfSize: batchSize) { + for fileBatch in filesByTarget[target]!.partition(intoBatchesOfSize: indexTaskBatchSize) { taskGroup.addTask { await self.updateIndexStore( for: fileBatch, diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index c9ce196ad..5f5749329 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -163,13 +163,20 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { await buildServerManager.initializationData?.prepareProvider ?? false { let shouldIndexInParallel = await buildServerManager.initializationData?.multiTargetPreparation?.supported ?? true + let batchSize: Int + if shouldIndexInParallel { + let processorCount = ProcessInfo.processInfo.activeProcessorCount + batchSize = max(1, processorCount * 5) + } else { + batchSize = 1 + } self.semanticIndexManager = SemanticIndexManager( index: uncheckedIndex, buildServerManager: buildServerManager, updateIndexStoreTimeout: options.indexOrDefault.updateIndexStoreTimeoutOrDefault, hooks: hooks.indexHooks, indexTaskScheduler: indexTaskScheduler, - shouldIndexInParallel: shouldIndexInParallel, + indexTaskBatchSize: batchSize, logMessageToIndexLog: { [weak sourceKitLSPServer] in sourceKitLSPServer?.logMessageToIndexLog(message: $0, type: $1, structure: $2) }, From b2526c080410f3c541a2b725a241ecd2425367b2 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 15 Aug 2025 09:50:48 +0200 Subject: [PATCH 6/8] Revert "(Temporary) Remove unit test" This reverts commit 19491c96058f83a73684466ef8ea4bfeb5225329. --- .../BackgroundIndexingTests.swift | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 9538222d1..83675f923 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -2626,6 +2626,95 @@ final class BackgroundIndexingTests: XCTestCase { } } +// WIP, not ready for review +// func testBuildServerUsesCustomTaskBatchSize() async throws { +// final class BuildServer: CustomBuildServer { +// let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker() +// private let projectRoot: URL +// private var testFileURL: URL { projectRoot.appendingPathComponent("test.swift").standardized } + +// nonisolated(unsafe) var preparedTargetBatches = [[BuildTargetIdentifier]]() + +// required init(projectRoot: URL, connectionToSourceKitLSP: any LanguageServerProtocol.Connection) { +// self.projectRoot = projectRoot +// } + +// func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { +// return try initializationResponseSupportingBackgroundIndexing( +// projectRoot: projectRoot, +// outputPathsProvider: false, +// ) +// } + +// func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { +// var dummyTargets = [BuildTargetIdentifier]() +// for i in 0..<10 { +// dummyTargets.append(BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-\(i)"))) +// } +// return BuildTargetSourcesResponse(items: dummyTargets.map { +// SourcesItem(target: $0, sources: [SourceItem(uri: URI(testFileURL), kind: .file, generated: false)]) +// }) +// } + +// func textDocumentSourceKitOptionsRequest( +// _ request: TextDocumentSourceKitOptionsRequest +// ) async throws -> TextDocumentSourceKitOptionsResponse? { +// return TextDocumentSourceKitOptionsResponse(compilerArguments: [request.textDocument.uri.pseudoPath]) +// } + +// func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse { +// preparedTargetBatches.append(request.targets.sorted { $0.uri.stringValue < $1.uri.stringValue }) +// return VoidResponse() +// } +// } + +// let project = try await CustomBuildServerTestProject( +// files: [ +// "test.swift": """ +// func testFunction() {} +// """ +// ], +// buildServer: BuildServer.self, +// enableBackgroundIndexing: true, +// ) + +// // Wait for indexing to finish without elevating the priority +// // Otherwise, task re-scheduling would cause the test to become flaky +// let semaphore = WrappedSemaphore(name: "Indexing finished") +// let testClient = project.testClient +// Task(priority: .low) { +// await assertNoThrow { +// try await testClient.send(SynchronizeRequest(index: true)) +// } +// semaphore.signal() +// } +// try semaphore.waitOrThrow() + +// let buildServer = try project.buildServer() +// let preparedBatches = buildServer.preparedTargetBatches.sorted { $0[0].uri.stringValue < $1[0].uri.stringValue } +// XCTAssertEqual(preparedBatches, [ +// [ +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-0")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-1")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-2")), +// ], +// [ +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-3")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-4")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-5")), +// ], +// [ +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-6")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-7")), +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-8")), +// ], +// [ +// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-9")), +// ] +// ]) +// } +// } + extension HoverResponseContents { var markupContent: MarkupContent? { switch self { From 8da0ff7dd92711ba9068bd3c04362bc6de1f6103 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 15 Aug 2025 10:08:08 +0200 Subject: [PATCH 7/8] Allow custom batch size, write test for batch sizes --- Contributor Documentation/BSP Extensions.md | 6 +- .../FixedCompilationDatabaseBuildServer.swift | 2 +- .../JSONCompilationDatabaseBuildServer.swift | 2 +- .../LegacyBuildServer.swift | 2 +- .../Messages/InitializeBuildRequest.swift | 15 +- .../CustomBuildServerTestProject.swift | 4 +- Sources/SourceKitLSP/Workspace.swift | 10 +- .../BackgroundIndexingTests.swift | 167 ++++++++---------- 8 files changed, 109 insertions(+), 99 deletions(-) diff --git a/Contributor Documentation/BSP Extensions.md b/Contributor Documentation/BSP Extensions.md index de5947b2b..ba263001c 100644 --- a/Contributor Documentation/BSP Extensions.md +++ b/Contributor Documentation/BSP Extensions.md @@ -41,8 +41,12 @@ export interface SourceKitInitializeBuildResponseData { } export interface MultiTargetPreparationSupport { - /** Whether the build server can prepare multiple targets in parallel. If this value is omitted, it is assumed to be `true`. */ + /** Whether the build server can prepare multiple targets in parallel. */ supported: bool; + + /** The number of targets to prepare in parallel. + * If not provided, SourceKit-LSP will calculate an appropriate value based on the environment. */ + batchSize?: int; } ``` diff --git a/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift b/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift index 46370c77e..a6545669f 100644 --- a/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift +++ b/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift @@ -59,7 +59,7 @@ package actor FixedCompilationDatabaseBuildServer: BuiltInBuildServer { indexStorePath?.deletingLastPathComponent().appendingPathComponent("IndexDatabase") } - package nonisolated var supportsMultiTargetPreparation: Bool { true } + package nonisolated var supportsMultiTargetPreparation: Bool { false } package nonisolated var supportsPreparationAndOutputPaths: Bool { false } diff --git a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift b/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift index acf5eb42e..75b602517 100644 --- a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift +++ b/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift @@ -96,7 +96,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer { indexStorePath?.deletingLastPathComponent().appendingPathComponent("IndexDatabase") } - package nonisolated var supportsMultiTargetPreparation: Bool { true } + package nonisolated var supportsMultiTargetPreparation: Bool { false } package nonisolated var supportsPreparationAndOutputPaths: Bool { false } diff --git a/Sources/BuildServerIntegration/LegacyBuildServer.swift b/Sources/BuildServerIntegration/LegacyBuildServer.swift index 026454e1a..57569177d 100644 --- a/Sources/BuildServerIntegration/LegacyBuildServer.swift +++ b/Sources/BuildServerIntegration/LegacyBuildServer.swift @@ -138,7 +138,7 @@ actor LegacyBuildServer: MessageHandler, BuiltInBuildServer { connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) } - package nonisolated var supportsMultiTargetPreparation: Bool { true } + package nonisolated var supportsMultiTargetPreparation: Bool { false } package nonisolated var supportsPreparationAndOutputPaths: Bool { false } diff --git a/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift b/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift index 4e01f70e6..e4ecb0928 100644 --- a/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift +++ b/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift @@ -372,17 +372,25 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send } public struct MultiTargetPreparationSupport: LSPAnyCodable, Codable, Sendable { - /// Whether the build server can prepare multiple targets in parallel. If this value is omitted, it is assumed to be `true`. + /// Whether the build server can prepare multiple targets in parallel. public var supported: Bool? - public init(supported: Bool? = nil) { + /// The number of targets to prepare in parallel. + /// If not provided, SourceKit-LSP will calculate an appropriate value based on the environment. + public var batchSize: Int? + + public init(supported: Bool? = nil, batchSize: Int? = nil) { self.supported = supported + self.batchSize = batchSize } public init?(fromLSPDictionary dictionary: [String: LanguageServerProtocol.LSPAny]) { if case .bool(let supported) = dictionary[CodingKeys.supported.stringValue] { self.supported = supported } + if case .int(let batchSize) = dictionary[CodingKeys.batchSize.stringValue] { + self.batchSize = batchSize + } } public func encodeToLSPAny() -> LanguageServerProtocol.LSPAny { @@ -390,6 +398,9 @@ public struct MultiTargetPreparationSupport: LSPAnyCodable, Codable, Sendable { if let supported { result[CodingKeys.supported.stringValue] = .bool(supported) } + if let batchSize { + result[CodingKeys.batchSize.stringValue] = .int(batchSize) + } return .dictionary(result) } } diff --git a/Sources/SKTestSupport/CustomBuildServerTestProject.swift b/Sources/SKTestSupport/CustomBuildServerTestProject.swift index 2f73fdff6..5c082fb2c 100644 --- a/Sources/SKTestSupport/CustomBuildServerTestProject.swift +++ b/Sources/SKTestSupport/CustomBuildServerTestProject.swift @@ -175,12 +175,14 @@ package extension CustomBuildServer { func initializationResponseSupportingBackgroundIndexing( projectRoot: URL, - outputPathsProvider: Bool + outputPathsProvider: Bool, + multiTargetPreparation: MultiTargetPreparationSupport? = nil ) throws -> InitializeBuildResponse { return initializationResponse( initializeData: SourceKitInitializeBuildResponseData( indexDatabasePath: try projectRoot.appendingPathComponent("index-db").filePath, indexStorePath: try projectRoot.appendingPathComponent("index-store").filePath, + multiTargetPreparation: multiTargetPreparation, outputPathsProvider: outputPathsProvider, prepareProvider: true, sourceKitOptionsProvider: true diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 5f5749329..4f4dcf63a 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -162,11 +162,15 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { if options.backgroundIndexingOrDefault, let uncheckedIndex, await buildServerManager.initializationData?.prepareProvider ?? false { - let shouldIndexInParallel = await buildServerManager.initializationData?.multiTargetPreparation?.supported ?? true + let shouldIndexInParallel = await buildServerManager.initializationData?.multiTargetPreparation?.supported ?? false let batchSize: Int if shouldIndexInParallel { - let processorCount = ProcessInfo.processInfo.activeProcessorCount - batchSize = max(1, processorCount * 5) + if let customBatchSize = await buildServerManager.initializationData?.multiTargetPreparation?.batchSize { + batchSize = customBatchSize + } else { + let processorCount = ProcessInfo.processInfo.activeProcessorCount + batchSize = max(1, processorCount / 2) + } } else { batchSize = 1 } diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 83675f923..e1f3b0cb4 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -2624,96 +2624,85 @@ final class BackgroundIndexingTests: XCTestCase { let symbols = try await project.testClient.send(WorkspaceSymbolsRequest(query: "myTestFu")) XCTAssertEqual(symbols?.count, 1) } -} -// WIP, not ready for review -// func testBuildServerUsesCustomTaskBatchSize() async throws { -// final class BuildServer: CustomBuildServer { -// let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker() -// private let projectRoot: URL -// private var testFileURL: URL { projectRoot.appendingPathComponent("test.swift").standardized } - -// nonisolated(unsafe) var preparedTargetBatches = [[BuildTargetIdentifier]]() - -// required init(projectRoot: URL, connectionToSourceKitLSP: any LanguageServerProtocol.Connection) { -// self.projectRoot = projectRoot -// } - -// func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { -// return try initializationResponseSupportingBackgroundIndexing( -// projectRoot: projectRoot, -// outputPathsProvider: false, -// ) -// } - -// func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { -// var dummyTargets = [BuildTargetIdentifier]() -// for i in 0..<10 { -// dummyTargets.append(BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-\(i)"))) -// } -// return BuildTargetSourcesResponse(items: dummyTargets.map { -// SourcesItem(target: $0, sources: [SourceItem(uri: URI(testFileURL), kind: .file, generated: false)]) -// }) -// } - -// func textDocumentSourceKitOptionsRequest( -// _ request: TextDocumentSourceKitOptionsRequest -// ) async throws -> TextDocumentSourceKitOptionsResponse? { -// return TextDocumentSourceKitOptionsResponse(compilerArguments: [request.textDocument.uri.pseudoPath]) -// } - -// func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse { -// preparedTargetBatches.append(request.targets.sorted { $0.uri.stringValue < $1.uri.stringValue }) -// return VoidResponse() -// } -// } - -// let project = try await CustomBuildServerTestProject( -// files: [ -// "test.swift": """ -// func testFunction() {} -// """ -// ], -// buildServer: BuildServer.self, -// enableBackgroundIndexing: true, -// ) - -// // Wait for indexing to finish without elevating the priority -// // Otherwise, task re-scheduling would cause the test to become flaky -// let semaphore = WrappedSemaphore(name: "Indexing finished") -// let testClient = project.testClient -// Task(priority: .low) { -// await assertNoThrow { -// try await testClient.send(SynchronizeRequest(index: true)) -// } -// semaphore.signal() -// } -// try semaphore.waitOrThrow() - -// let buildServer = try project.buildServer() -// let preparedBatches = buildServer.preparedTargetBatches.sorted { $0[0].uri.stringValue < $1[0].uri.stringValue } -// XCTAssertEqual(preparedBatches, [ -// [ -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-0")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-1")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-2")), -// ], -// [ -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-3")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-4")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-5")), -// ], -// [ -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-6")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-7")), -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-8")), -// ], -// [ -// BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-9")), -// ] -// ]) -// } -// } + func testBuildServerUsesCustomTaskBatchSize() async throws { + final class BuildServer: CustomBuildServer { + let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker() + private let projectRoot: URL + private var testFileURL: URL { projectRoot.appendingPathComponent("test.swift").standardized } + + nonisolated(unsafe) var preparedTargetBatches = [[BuildTargetIdentifier]]() + + required init(projectRoot: URL, connectionToSourceKitLSP: any LanguageServerProtocol.Connection) { + self.projectRoot = projectRoot + } + + func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { + return try initializationResponseSupportingBackgroundIndexing( + projectRoot: projectRoot, + outputPathsProvider: false, + multiTargetPreparation: MultiTargetPreparationSupport(supported: true, batchSize: 3) + ) + } + + func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { + var dummyTargets = [BuildTargetIdentifier]() + for i in 0..<10 { + dummyTargets.append(BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-\(i)"))) + } + return BuildTargetSourcesResponse(items: dummyTargets.map { + SourcesItem(target: $0, sources: [SourceItem(uri: URI(testFileURL), kind: .file, generated: false)]) + }) + } + + func textDocumentSourceKitOptionsRequest( + _ request: TextDocumentSourceKitOptionsRequest + ) async throws -> TextDocumentSourceKitOptionsResponse? { + return TextDocumentSourceKitOptionsResponse(compilerArguments: [request.textDocument.uri.pseudoPath]) + } + + func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse { + preparedTargetBatches.append(request.targets.sorted { $0.uri.stringValue < $1.uri.stringValue }) + return VoidResponse() + } + } + + let project = try await CustomBuildServerTestProject( + files: [ + "test.swift": """ + func testFunction() {} + """ + ], + buildServer: BuildServer.self, + enableBackgroundIndexing: true, + ) + + try await project.testClient.send(SynchronizeRequest(index: true)) + + let buildServer = try project.buildServer() + let preparedBatches = buildServer.preparedTargetBatches.sorted { $0[0].uri.stringValue < $1[0].uri.stringValue } + XCTAssertEqual(preparedBatches, [ + [ + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-0")), + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-1")), + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-2")), + ], + [ + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-3")), + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-4")), + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-5")), + ], + [ + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-6")), + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-7")), + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-8")), + ], + [ + BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy-9")), + ] + ]) + } +} extension HoverResponseContents { var markupContent: MarkupContent? { From a61079cdecb21911eb5f3b8ff7de14e31eb9a45c Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 18 Aug 2025 09:58:29 +0200 Subject: [PATCH 8/8] Default multi-target preparation to true --- Contributor Documentation/BSP Extensions.md | 5 +++-- .../FixedCompilationDatabaseBuildServer.swift | 2 +- .../JSONCompilationDatabaseBuildServer.swift | 2 +- Sources/BuildServerIntegration/LegacyBuildServer.swift | 2 +- .../Messages/InitializeBuildRequest.swift | 1 + Sources/SourceKitLSP/Workspace.swift | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Contributor Documentation/BSP Extensions.md b/Contributor Documentation/BSP Extensions.md index ba263001c..2701ef4ab 100644 --- a/Contributor Documentation/BSP Extensions.md +++ b/Contributor Documentation/BSP Extensions.md @@ -41,8 +41,9 @@ export interface SourceKitInitializeBuildResponseData { } export interface MultiTargetPreparationSupport { - /** Whether the build server can prepare multiple targets in parallel. */ - supported: bool; + /** Whether the build server can prepare multiple targets in parallel. + * If this value is not provided, it will be assumed to be `true`. */ + supported?: bool; /** The number of targets to prepare in parallel. * If not provided, SourceKit-LSP will calculate an appropriate value based on the environment. */ diff --git a/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift b/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift index a6545669f..46370c77e 100644 --- a/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift +++ b/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift @@ -59,7 +59,7 @@ package actor FixedCompilationDatabaseBuildServer: BuiltInBuildServer { indexStorePath?.deletingLastPathComponent().appendingPathComponent("IndexDatabase") } - package nonisolated var supportsMultiTargetPreparation: Bool { false } + package nonisolated var supportsMultiTargetPreparation: Bool { true } package nonisolated var supportsPreparationAndOutputPaths: Bool { false } diff --git a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift b/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift index 75b602517..acf5eb42e 100644 --- a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift +++ b/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift @@ -96,7 +96,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer { indexStorePath?.deletingLastPathComponent().appendingPathComponent("IndexDatabase") } - package nonisolated var supportsMultiTargetPreparation: Bool { false } + package nonisolated var supportsMultiTargetPreparation: Bool { true } package nonisolated var supportsPreparationAndOutputPaths: Bool { false } diff --git a/Sources/BuildServerIntegration/LegacyBuildServer.swift b/Sources/BuildServerIntegration/LegacyBuildServer.swift index 57569177d..026454e1a 100644 --- a/Sources/BuildServerIntegration/LegacyBuildServer.swift +++ b/Sources/BuildServerIntegration/LegacyBuildServer.swift @@ -138,7 +138,7 @@ actor LegacyBuildServer: MessageHandler, BuiltInBuildServer { connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) } - package nonisolated var supportsMultiTargetPreparation: Bool { false } + package nonisolated var supportsMultiTargetPreparation: Bool { true } package nonisolated var supportsPreparationAndOutputPaths: Bool { false } diff --git a/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift b/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift index e4ecb0928..13579335c 100644 --- a/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift +++ b/Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift @@ -373,6 +373,7 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send public struct MultiTargetPreparationSupport: LSPAnyCodable, Codable, Sendable { /// Whether the build server can prepare multiple targets in parallel. + /// If this value is not provided, it will be assumed to be `true`. public var supported: Bool? /// The number of targets to prepare in parallel. diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 4f4dcf63a..dba9df84c 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -162,7 +162,7 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { if options.backgroundIndexingOrDefault, let uncheckedIndex, await buildServerManager.initializationData?.prepareProvider ?? false { - let shouldIndexInParallel = await buildServerManager.initializationData?.multiTargetPreparation?.supported ?? false + let shouldIndexInParallel = await buildServerManager.initializationData?.multiTargetPreparation?.supported ?? true let batchSize: Int if shouldIndexInParallel { if let customBatchSize = await buildServerManager.initializationData?.multiTargetPreparation?.batchSize {