Skip to content

Commit ef2dc17

Browse files
authored
Merge pull request #1543 from ahoppen/time-out-sourcekitd
Add a maximum duration for sourcekitd requests
2 parents 83cb162 + 191d366 commit ef2dc17

19 files changed

+172
-51
lines changed

Sources/Diagnose/RunSourcekitdRequestCommand.swift

+3
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ public struct RunSourceKitdRequestCommand: AsyncParsableCommand {
9696
case .requestCancelled:
9797
print("request cancelled")
9898
throw ExitCode(1)
99+
case .timedOut:
100+
print("request timed out")
101+
throw ExitCode(1)
99102
case .missingRequiredSymbol:
100103
print("missing required symbol")
101104
throw ExitCode(1)

Sources/LanguageServerProtocol/Error.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public struct ErrorCode: RawRepresentable, Codable, Hashable, Sendable {
8181
/// It doesn't denote a real error code.
8282
public static let lspReservedErrorRangeEnd = ErrorCode(rawValue: -32800)
8383

84-
// MARK: SourceKit-LSP specifiic eror codes
84+
// MARK: SourceKit-LSP specific error codes
8585
public static let workspaceNotOpen: ErrorCode = ErrorCode(rawValue: -32003)
8686
}
8787

Sources/SKCore/SourceKitLSPOptions.swift

+24-4
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ public struct SourceKitLSPOptions: Sendable, Codable {
191191
/// Whether background indexing is enabled.
192192
public var backgroundIndexing: Bool?
193193

194+
public var backgroundIndexingOrDefault: Bool {
195+
return backgroundIndexing ?? false
196+
}
197+
194198
/// Experimental features that are enabled.
195199
public var experimentalFeatures: Set<ExperimentalFeature>? = nil
196200

@@ -220,8 +224,21 @@ public struct SourceKitLSPOptions: Sendable, Codable {
220224
return .seconds(1)
221225
}
222226

223-
public var backgroundIndexingOrDefault: Bool {
224-
return backgroundIndexing ?? false
227+
/// The maximum duration that a sourcekitd request should be allowed to execute before being declared as timed out.
228+
///
229+
/// In general, editors should cancel requests that they are no longer interested in, but in case editors don't cancel
230+
/// requests, this ensures that a long-running non-cancelled request is not blocking sourcekitd and thus most semantic
231+
/// functionality.
232+
///
233+
/// In particular, VS Code does not cancel the semantic tokens request, which can cause a long-running AST build that
234+
/// blocks sourcekitd.
235+
public var sourcekitdRequestTimeout: Double? = nil
236+
237+
public var sourcekitdRequestTimeoutOrDefault: Duration {
238+
if let sourcekitdRequestTimeout {
239+
return .seconds(sourcekitdRequestTimeout)
240+
}
241+
return .seconds(120)
225242
}
226243

227244
public init(
@@ -235,7 +252,8 @@ public struct SourceKitLSPOptions: Sendable, Codable {
235252
backgroundIndexing: Bool? = nil,
236253
experimentalFeatures: Set<ExperimentalFeature>? = nil,
237254
swiftPublishDiagnosticsDebounceDuration: Double? = nil,
238-
workDoneProgressDebounceDuration: Double? = nil
255+
workDoneProgressDebounceDuration: Double? = nil,
256+
sourcekitdRequestTimeout: Double? = nil
239257
) {
240258
self.swiftPM = swiftPM
241259
self.fallbackBuildSystem = fallbackBuildSystem
@@ -248,6 +266,7 @@ public struct SourceKitLSPOptions: Sendable, Codable {
248266
self.experimentalFeatures = experimentalFeatures
249267
self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration
250268
self.workDoneProgressDebounceDuration = workDoneProgressDebounceDuration
269+
self.sourcekitdRequestTimeout = sourcekitdRequestTimeout
251270
}
252271

253272
public init?(fromLSPAny lspAny: LSPAny?) throws {
@@ -300,7 +319,8 @@ public struct SourceKitLSPOptions: Sendable, Codable {
300319
swiftPublishDiagnosticsDebounceDuration: override?.swiftPublishDiagnosticsDebounceDuration
301320
?? base.swiftPublishDiagnosticsDebounceDuration,
302321
workDoneProgressDebounceDuration: override?.workDoneProgressDebounceDuration
303-
?? base.workDoneProgressDebounceDuration
322+
?? base.workDoneProgressDebounceDuration,
323+
sourcekitdRequestTimeout: override?.sourcekitdRequestTimeout ?? base.sourcekitdRequestTimeout
304324
)
305325
}
306326

Sources/SourceKitD/SourceKitD.swift

+26-13
Original file line numberDiff line numberDiff line change
@@ -83,31 +83,41 @@ public enum SKDError: Error, Equatable {
8383
/// The request was cancelled.
8484
case requestCancelled
8585

86+
/// The request exceeded the maximum allowed duration.
87+
case timedOut
88+
8689
/// Loading a required symbol from the sourcekitd library failed.
8790
case missingRequiredSymbol(String)
8891
}
8992

9093
extension SourceKitD {
91-
9294
// MARK: - Convenience API for requests.
9395

9496
/// - Parameters:
95-
/// - req: The request to send to sourcekitd.
97+
/// - request: The request to send to sourcekitd.
98+
/// - timeout: The maximum duration how long to wait for a response. If no response is returned within this time,
99+
/// declare the request as having timed out.
96100
/// - fileContents: The contents of the file that the request operates on. If sourcekitd crashes, the file contents
97101
/// will be logged.
98-
public func send(_ request: SKDRequestDictionary, fileContents: String?) async throws -> SKDResponseDictionary {
102+
public func send(
103+
_ request: SKDRequestDictionary,
104+
timeout: Duration,
105+
fileContents: String?
106+
) async throws -> SKDResponseDictionary {
99107
log(request: request)
100108

101-
let sourcekitdResponse: SKDResponse = try await withCancellableCheckedThrowingContinuation { continuation in
102-
var handle: sourcekitd_api_request_handle_t? = nil
103-
api.send_request(request.dict, &handle) { response in
104-
continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
105-
}
106-
return handle
107-
} cancel: { handle in
108-
if let handle {
109-
logRequestCancellation(request: request)
110-
api.cancel_request(handle)
109+
let sourcekitdResponse = try await withTimeout(timeout) {
110+
return try await withCancellableCheckedThrowingContinuation { continuation in
111+
var handle: sourcekitd_api_request_handle_t? = nil
112+
self.api.send_request(request.dict, &handle) { response in
113+
continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
114+
}
115+
return handle
116+
} cancel: { handle in
117+
if let handle {
118+
self.logRequestCancellation(request: request)
119+
self.api.cancel_request(handle)
120+
}
111121
}
112122
}
113123

@@ -117,6 +127,9 @@ extension SourceKitD {
117127
if sourcekitdResponse.error == .connectionInterrupted {
118128
log(crashedRequest: request, fileContents: fileContents)
119129
}
130+
if sourcekitdResponse.error == .requestCancelled && !Task.isCancelled {
131+
throw SKDError.timedOut
132+
}
120133
throw sourcekitdResponse.error!
121134
}
122135

Sources/SourceKitLSP/Rename.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ extension SwiftLanguageService {
359359
keys.argNames: sourcekitd.array(name.parameters.map { $0.stringOrWildcard }),
360360
])
361361

362-
let response = try await sourcekitd.send(req, fileContents: snapshot.text)
362+
let response = try await sendSourcekitdRequest(req, fileContents: snapshot.text)
363363

364364
guard let isZeroArgSelector: Int = response[keys.isZeroArgSelector],
365365
let selectorPieces: SKDResponseArray = response[keys.selectorPieces]
@@ -416,7 +416,7 @@ extension SwiftLanguageService {
416416
req.set(keys.baseName, to: name)
417417
}
418418

419-
let response = try await sourcekitd.send(req, fileContents: snapshot.text)
419+
let response = try await sendSourcekitdRequest(req, fileContents: snapshot.text)
420420

421421
guard let baseName: String = response[keys.baseName] else {
422422
throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response)
@@ -914,7 +914,7 @@ extension SwiftLanguageService {
914914
keys.renameLocations: locations,
915915
])
916916

917-
let syntacticRenameRangesResponse = try await sourcekitd.send(skreq, fileContents: snapshot.text)
917+
let syntacticRenameRangesResponse = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
918918
guard let categorizedRanges: SKDResponseArray = syntacticRenameRangesResponse[keys.categorizedRanges] else {
919919
throw ResponseError.internalError("sourcekitd did not return categorized ranges")
920920
}

Sources/SourceKitLSP/Swift/CodeCompletion.swift

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ extension SwiftLanguageService {
3333
return try await CodeCompletionSession.completionList(
3434
sourcekitd: sourcekitd,
3535
snapshot: snapshot,
36+
options: options,
3637
indentationWidth: inferredIndentationWidth,
3738
completionPosition: completionPos,
3839
completionUtf8Offset: offset,

Sources/SourceKitLSP/Swift/CodeCompletionSession.swift

+17-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import Dispatch
1414
import LSPLogging
1515
import LanguageServerProtocol
16+
import SKCore
1617
import SKSupport
1718
import SourceKitD
1819
import SwiftExtensions
@@ -92,6 +93,7 @@ class CodeCompletionSession {
9293
static func completionList(
9394
sourcekitd: any SourceKitD,
9495
snapshot: DocumentSnapshot,
96+
options: SourceKitLSPOptions,
9597
indentationWidth: Trivia?,
9698
completionPosition: Position,
9799
completionUtf8Offset: Int,
@@ -122,6 +124,7 @@ class CodeCompletionSession {
122124
let session = CodeCompletionSession(
123125
sourcekitd: sourcekitd,
124126
snapshot: snapshot,
127+
options: options,
125128
indentationWidth: indentationWidth,
126129
utf8Offset: completionUtf8Offset,
127130
position: completionPosition,
@@ -139,6 +142,7 @@ class CodeCompletionSession {
139142

140143
private let sourcekitd: any SourceKitD
141144
private let snapshot: DocumentSnapshot
145+
private let options: SourceKitLSPOptions
142146
/// The inferred indentation width of the source file the completion is being performed in
143147
private let indentationWidth: Trivia?
144148
private let utf8StartOffset: Int
@@ -158,13 +162,15 @@ class CodeCompletionSession {
158162
private init(
159163
sourcekitd: any SourceKitD,
160164
snapshot: DocumentSnapshot,
165+
options: SourceKitLSPOptions,
161166
indentationWidth: Trivia?,
162167
utf8Offset: Int,
163168
position: Position,
164169
compileCommand: SwiftCompileCommand?,
165170
clientSupportsSnippets: Bool
166171
) {
167172
self.sourcekitd = sourcekitd
173+
self.options = options
168174
self.indentationWidth = indentationWidth
169175
self.snapshot = snapshot
170176
self.utf8StartOffset = utf8Offset
@@ -193,7 +199,11 @@ class CodeCompletionSession {
193199
keys.compilerArgs: compileCommand?.compilerArgs as [SKDRequestValue]?,
194200
])
195201

196-
let dict = try await sourcekitd.send(req, fileContents: snapshot.text)
202+
let dict = try await sourcekitd.send(
203+
req,
204+
timeout: options.sourcekitdRequestTimeoutOrDefault,
205+
fileContents: snapshot.text
206+
)
197207
self.state = .open
198208

199209
guard let completions: SKDResponseArray = dict[keys.results] else {
@@ -226,7 +236,11 @@ class CodeCompletionSession {
226236
keys.codeCompleteOptions: optionsDictionary(filterText: filterText),
227237
])
228238

229-
let dict = try await sourcekitd.send(req, fileContents: snapshot.text)
239+
let dict = try await sourcekitd.send(
240+
req,
241+
timeout: options.sourcekitdRequestTimeoutOrDefault,
242+
fileContents: snapshot.text
243+
)
230244
guard let completions: SKDResponseArray = dict[keys.results] else {
231245
return CompletionList(isIncomplete: false, items: [])
232246
}
@@ -269,7 +283,7 @@ class CodeCompletionSession {
269283
keys.name: snapshot.uri.pseudoPath,
270284
])
271285
logger.info("Closing code completion session: \(self.description)")
272-
_ = try? await sourcekitd.send(req, fileContents: nil)
286+
_ = try? await sourcekitd.send(req, timeout: options.sourcekitdRequestTimeoutOrDefault, fileContents: nil)
273287
self.state = .closed
274288
}
275289
}

Sources/SourceKitLSP/Swift/CursorInfo.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ extension SwiftLanguageService {
159159

160160
appendAdditionalParameters?(skreq)
161161

162-
let dict = try await self.sourcekitd.send(skreq, fileContents: snapshot.text)
162+
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
163163

164164
var cursorInfoResults: [CursorInfo] = []
165165
if let cursorInfo = CursorInfo(dict, sourcekitd: sourcekitd) {

Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import LSPLogging
1414
import LanguageServerProtocol
15+
import SKCore
1516
import SKSupport
1617
import SourceKitD
1718
import SwiftExtensions
@@ -22,6 +23,7 @@ actor DiagnosticReportManager {
2223
private typealias ReportTask = RefCountedCancellableTask<RelatedFullDocumentDiagnosticReport>
2324

2425
private let sourcekitd: SourceKitD
26+
private let options: SourceKitLSPOptions
2527
private let syntaxTreeManager: SyntaxTreeManager
2628
private let documentManager: DocumentManager
2729
private let clientHasDiagnosticsCodeDescriptionSupport: Bool
@@ -48,11 +50,13 @@ actor DiagnosticReportManager {
4850

4951
init(
5052
sourcekitd: SourceKitD,
53+
options: SourceKitLSPOptions,
5154
syntaxTreeManager: SyntaxTreeManager,
5255
documentManager: DocumentManager,
5356
clientHasDiagnosticsCodeDescriptionSupport: Bool
5457
) {
5558
self.sourcekitd = sourcekitd
59+
self.options = options
5660
self.syntaxTreeManager = syntaxTreeManager
5761
self.documentManager = documentManager
5862
self.clientHasDiagnosticsCodeDescriptionSupport = clientHasDiagnosticsCodeDescriptionSupport
@@ -104,7 +108,11 @@ actor DiagnosticReportManager {
104108
keys.compilerArgs: compilerArgs as [SKDRequestValue],
105109
])
106110

107-
let dict = try await self.sourcekitd.send(skreq, fileContents: snapshot.text)
111+
let dict = try await self.sourcekitd.send(
112+
skreq,
113+
timeout: options.sourcekitdRequestTimeoutOrDefault,
114+
fileContents: snapshot.text
115+
)
108116

109117
try Task.checkCancellation()
110118

Sources/SourceKitLSP/Swift/OpenInterface.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ extension SwiftLanguageService {
6363
symbol: symbol
6464
)
6565
_ = await orLog("Closing generated interface") {
66-
try await self.sourcekitd.send(closeDocumentSourcekitdRequest(uri: interfaceDocURI), fileContents: nil)
66+
try await sendSourcekitdRequest(closeDocumentSourcekitdRequest(uri: interfaceDocURI), fileContents: nil)
6767
}
6868
return result
6969
}
@@ -95,7 +95,7 @@ extension SwiftLanguageService {
9595
keys.compilerArgs: await self.buildSettings(for: document)?.compilerArgs as [SKDRequestValue]?,
9696
])
9797

98-
let dict = try await self.sourcekitd.send(skreq, fileContents: nil)
98+
let dict = try await sendSourcekitdRequest(skreq, fileContents: nil)
9999
return GeneratedInterfaceInfo(contents: dict[keys.sourceText] ?? "")
100100
}
101101

@@ -115,7 +115,7 @@ extension SwiftLanguageService {
115115
keys.usr: symbol,
116116
])
117117

118-
let dict = try await self.sourcekitd.send(skreq, fileContents: snapshot.text)
118+
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
119119
if let offset: Int = dict[keys.offset] {
120120
return GeneratedInterfaceDetails(uri: uri, position: snapshot.positionOf(utf8Offset: offset))
121121
} else {

Sources/SourceKitLSP/Swift/Refactoring.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ extension SwiftLanguageService {
120120
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
121121
])
122122

123-
let dict = try await self.sourcekitd.send(skreq, fileContents: snapshot.text)
123+
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
124124
guard let refactor = T.Response(refactorCommand.title, dict, snapshot, self.keys) else {
125125
throw SemanticRefactoringError.noEditsNeeded(uri)
126126
}

Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ extension SwiftLanguageService {
7474
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
7575
])
7676

77-
let dict = try await self.sourcekitd.send(skreq, fileContents: snapshot.text)
77+
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
7878

7979
guard let results: SKDResponseArray = dict[self.keys.results] else {
8080
throw ResponseError.internalError("sourcekitd response did not contain results")

Sources/SourceKitLSP/Swift/SemanticTokens.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ extension SwiftLanguageService {
3030
keys.compilerArgs: buildSettings.compilerArgs as [SKDRequestValue],
3131
])
3232

33-
let dict = try await sourcekitd.send(skreq, fileContents: snapshot.text)
33+
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
3434

3535
guard let skTokens: SKDResponseArray = dict[keys.semanticTokens] else {
3636
return nil

Sources/SourceKitLSP/Swift/SourceKitD+ResponseError.swift

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ extension ResponseError {
1818
switch value {
1919
case .requestCancelled:
2020
self = .cancelled
21+
case .timedOut:
22+
self = .unknown("sourcekitd request timed out")
2123
case .requestFailed(let desc):
2224
self = .unknown("sourcekitd request failed: \(desc)")
2325
case .requestInvalid(let desc):

0 commit comments

Comments
 (0)