Skip to content

Commit 0a54c25

Browse files
authored
Merge pull request #1999 from ahoppen/schedule-initial-syntactic-test-population
Do not block initialization of the build server when build server is unresponsive in returning the list of test files
2 parents 4e44553 + bd6fb5d commit 0a54c25

File tree

6 files changed

+126
-45
lines changed

6 files changed

+126
-45
lines changed

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,20 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
204204
}
205205
preInitialization?(self)
206206
if initialize {
207-
_ = try await self.send(
208-
InitializeRequest(
209-
processId: nil,
210-
rootPath: nil,
211-
rootURI: nil,
212-
initializationOptions: initializationOptions,
213-
capabilities: capabilities,
214-
trace: .off,
215-
workspaceFolders: workspaceFolders
207+
let capabilities = capabilities
208+
try await withTimeout(.seconds(defaultTimeout)) {
209+
_ = try await self.send(
210+
InitializeRequest(
211+
processId: nil,
212+
rootPath: nil,
213+
rootURI: nil,
214+
initializationOptions: initializationOptions,
215+
capabilities: capabilities,
216+
trace: .off,
217+
workspaceFolders: workspaceFolders
218+
)
216219
)
217-
)
220+
}
218221
}
219222
}
220223

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -339,13 +339,7 @@ package final actor SemanticIndexManager {
339339
}
340340

341341
private func waitForBuildGraphGenerationTasks() async {
342-
await withTaskGroup(of: Void.self) { taskGroup in
343-
for generateBuildGraphTask in scheduleIndexingTasks.values {
344-
taskGroup.addTask {
345-
await generateBuildGraphTask.value
346-
}
347-
}
348-
}
342+
await scheduleIndexingTasks.values.concurrentForEach { await $0.value }
349343
}
350344

351345
/// Wait for all in-progress index tasks to finish.
@@ -355,18 +349,13 @@ package final actor SemanticIndexManager {
355349
// can await the index tasks below.
356350
await waitForBuildGraphGenerationTasks()
357351

358-
await withTaskGroup(of: Void.self) { taskGroup in
359-
for (_, status) in inProgressIndexTasks {
360-
switch status {
361-
case .waitingForPreparation(preparationTaskID: _, indexTask: let indexTask),
362-
.preparing(preparationTaskID: _, indexTask: let indexTask),
363-
.updatingIndexStore(updateIndexStoreTask: _, indexTask: let indexTask):
364-
taskGroup.addTask {
365-
await indexTask.value
366-
}
367-
}
352+
await inProgressIndexTasks.concurrentForEach { (_, status) in
353+
switch status {
354+
case .waitingForPreparation(preparationTaskID: _, indexTask: let indexTask),
355+
.preparing(preparationTaskID: _, indexTask: let indexTask),
356+
.updatingIndexStore(updateIndexStoreTask: _, indexTask: let indexTask):
357+
await indexTask.value
368358
}
369-
await taskGroup.waitForAll()
370359
}
371360
index.pollForUnitChangesAndWait()
372361
logger.debug("Done waiting for up-to-date index")
@@ -786,17 +775,9 @@ package final actor SemanticIndexManager {
786775
}
787776
indexTasksWereScheduled(newIndexTasks)
788777
}
789-
let indexTasksImmutable = indexTasks
790778

791779
return Task(priority: priority) {
792-
await withTaskGroup(of: Void.self) { taskGroup in
793-
for indexTask in indexTasksImmutable {
794-
taskGroup.addTask {
795-
await indexTask.value
796-
}
797-
}
798-
await taskGroup.waitForAll()
799-
}
780+
await indexTasks.concurrentForEach { await $0.value }
800781
}
801782
}
802783
}

Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,28 @@ import SwiftExtensions
1818

1919
/// Task metadata for `SyntacticTestIndexer.indexingQueue`
2020
fileprivate enum TaskMetadata: DependencyTracker, Equatable {
21-
case read
21+
/// Determine the list of test files from the build system and scan them for tests. Only created when the
22+
/// `SyntacticTestIndex` is created
23+
case initialPopulation
24+
25+
/// Index the files in the given set for tests
2226
case index(Set<DocumentURI>)
2327

28+
/// Retrieve information about syntactically discovered tests from the index.
29+
case read
30+
2431
/// Reads can be concurrent and files can be indexed concurrently. But we need to wait for all files to finish
2532
/// indexing before reading the index.
2633
func isDependency(of other: TaskMetadata) -> Bool {
2734
switch (self, other) {
35+
case (.initialPopulation, _):
36+
// The initial population need to finish before we can do anything with the task.
37+
return true
38+
case (_, .initialPopulation):
39+
// Should never happen because the initial population should only be scheduled once before any other operations
40+
// on the test index. But be conservative in case we do get an `initialPopulation` somewhere in between and use it
41+
// as a full blocker on the queue.
42+
return true
2843
case (.read, .read):
2944
// We allow concurrent reads
3045
return false
@@ -109,7 +124,23 @@ actor SyntacticTestIndex {
109124
/// indexing tasks to finish.
110125
private let indexingQueue = AsyncQueue<TaskMetadata>()
111126

112-
init() {}
127+
init(determineTestFiles: @Sendable @escaping () async -> [DocumentURI]) {
128+
indexingQueue.async(priority: .low, metadata: .initialPopulation) {
129+
let testFiles = await determineTestFiles()
130+
131+
// Divide the files into multiple batches. This is more efficient than spawning a new task for every file, mostly
132+
// because it keeps the number of pending items in `indexingQueue` low and adding a new task to `indexingQueue` is
133+
// in O(number of pending tasks), since we need to scan for dependency edges to add, which would make scanning files
134+
// be O(number of files).
135+
// Over-subscribe the processor count in case one batch finishes more quickly than another.
136+
let batches = testFiles.partition(intoNumberOfBatches: ProcessInfo.processInfo.processorCount * 4)
137+
await batches.concurrentForEach { filesInBatch in
138+
for uri in filesInBatch {
139+
await self.rescanFileAssumingOnQueue(uri)
140+
}
141+
}
142+
}
143+
}
113144

114145
private func removeFilesFromIndex(_ removedFiles: Set<DocumentURI>) {
115146
self.removedFiles.formUnion(removedFiles)

Sources/SourceKitLSP/Workspace.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
127127
}
128128

129129
/// The index that syntactically scans the workspace for tests.
130-
let syntacticTestIndex = SyntacticTestIndex()
130+
let syntacticTestIndex: SyntacticTestIndex
131131

132132
/// Language service for an open document, if available.
133133
private let documentService: ThreadSafeBox<[DocumentURI: LanguageService]> = ThreadSafeBox(initialValue: [:])
@@ -180,13 +180,15 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
180180
} else {
181181
self.semanticIndexManager = nil
182182
}
183+
// Trigger an initial population of `syntacticTestIndex`.
184+
self.syntacticTestIndex = SyntacticTestIndex(determineTestFiles: {
185+
await orLog("Getting list of test files for initial syntactic index population") {
186+
try await buildSystemManager.testFiles()
187+
} ?? []
188+
})
183189
await indexDelegate?.addMainFileChangedCallback { [weak self] in
184190
await self?.buildSystemManager.mainFilesChanged()
185191
}
186-
// Trigger an initial population of `syntacticTestIndex`.
187-
if let testFiles = await orLog("Getting initial test files", { try await self.buildSystemManager.testFiles() }) {
188-
await syntacticTestIndex.listOfTestFilesDidChange(testFiles)
189-
}
190192
if let semanticIndexManager {
191193
await semanticIndexManager.scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
192194
filesToIndex: nil,
@@ -224,7 +226,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
224226
// `SourceKitLSPServer` has been destructed. We are tearing down the
225227
// language server. Nothing left to do.
226228
logger.error(
227-
"Ignoring notificaiton \(type(of: notification).method) because connection to editor has been closed"
229+
"Ignoring notification \(type(of: notification).method) because connection to editor has been closed"
228230
)
229231
return
230232
}

Sources/SwiftExtensions/AsyncUtils.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ extension Collection where Element: Sendable {
165165
count = indexedResults.count
166166
}
167167
}
168+
169+
/// Invoke `body` for every element in the collection and wait for all calls of `body` to finish
170+
package func concurrentForEach(_ body: @escaping @Sendable (Element) async -> Void) async {
171+
await withTaskGroup(of: Void.self) { taskGroup in
172+
for element in self {
173+
taskGroup.addTask {
174+
await body(element)
175+
}
176+
}
177+
}
178+
}
168179
}
169180

170181
package struct TimeoutError: Error, CustomStringConvertible {

Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ import LanguageServerProtocol
1717
import LanguageServerProtocolExtensions
1818
import SKOptions
1919
import SKTestSupport
20+
import SourceKitLSP
21+
import SwiftExtensions
2022
import TSCBasic
2123
import XCTest
2224

25+
#if os(Windows)
26+
import WinSDK
27+
#endif
28+
2329
final class BuildServerBuildSystemTests: XCTestCase {
2430
func testBuildSettingsFromBuildServer() async throws {
2531
let project = try await BuildServerTestProject(
@@ -517,4 +523,51 @@ final class BuildServerBuildSystemTests: XCTestCase {
517523
// We currently pick the canonical target alphabetically, which means that `bsp://first` wins over `bsp://second`
518524
XCTAssert(try XCTUnwrap(optionsWithoutTarget).compilerArguments.contains("-DFIRST"))
519525
}
526+
527+
func testDontBlockBuildServerInitializationIfBuildSystemIsUnresponsive() async throws {
528+
// A build server that responds to the initialize request but not to any other requests.
529+
final class UnresponsiveBuildServer: MessageHandler {
530+
func handle(_ notification: some LanguageServerProtocol.NotificationType) {}
531+
532+
func handle<Request: RequestType>(
533+
_ request: Request,
534+
id: RequestID,
535+
reply: @escaping @Sendable (LSPResult<Request.Response>) -> Void
536+
) {
537+
switch request {
538+
case is InitializeBuildRequest:
539+
reply(
540+
.success(
541+
InitializeBuildResponse(
542+
displayName: "UnresponsiveBuildServer",
543+
version: "",
544+
bspVersion: "2.2.0",
545+
capabilities: BuildServerCapabilities()
546+
) as! Request.Response
547+
)
548+
)
549+
default:
550+
#if os(Windows)
551+
Sleep(60 * 60 * 1000 /*ms*/)
552+
#else
553+
sleep(60 * 60 /*s*/)
554+
#endif
555+
XCTFail("Build server should be terminated before finishing the timeout")
556+
}
557+
}
558+
}
559+
560+
// Creating the `MultiFileTestProject` waits for the initialize response and times out if it doesn't receive one.
561+
// Make sure that we get that response back.
562+
_ = try await MultiFileTestProject(
563+
files: ["Test.swift": ""],
564+
hooks: Hooks(
565+
buildSystemHooks: BuildSystemHooks(injectBuildServer: { _, _ in
566+
let connection = LocalConnection(receiverName: "Unresponsive build system")
567+
connection.start(handler: UnresponsiveBuildServer())
568+
return connection
569+
})
570+
)
571+
)
572+
}
520573
}

0 commit comments

Comments
 (0)