Skip to content

Commit 191d366

Browse files
committed
Add a maximum duration for sourcekitd requests
VS Code does not cancel semantic tokens requests. If a source file gets into a state where an AST build takes very long, this can cause us to wait for the semantic tokens from sourcekitd for a few minutes, effectively blocking all other semantic functionality in that file. To circumvent this problem (or any other problem where an editor might not be cancelling requests they are no longer interested in) add a maximum request duration for SourceKitD requests, defaulting to 2 minutes. rdar://130948453
1 parent e5d93e1 commit 191d366

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
@@ -52,7 +52,7 @@ extension SwiftLanguageService {
5252
symbol: symbol
5353
)
5454
_ = await orLog("Closing generated interface") {
55-
try await self.sourcekitd.send(closeDocumentSourcekitdRequest(uri: interfaceDocURI), fileContents: nil)
55+
try await sendSourcekitdRequest(closeDocumentSourcekitdRequest(uri: interfaceDocURI), fileContents: nil)
5656
}
5757
return result
5858
}
@@ -80,7 +80,7 @@ extension SwiftLanguageService {
8080
keys.compilerArgs: await self.buildSettings(for: request.textDocument.uri)?.compilerArgs as [SKDRequestValue]?,
8181
])
8282

83-
let dict = try await self.sourcekitd.send(skreq, fileContents: nil)
83+
let dict = try await sendSourcekitdRequest(skreq, fileContents: nil)
8484
return GeneratedInterfaceInfo(contents: dict[keys.sourceText] ?? "")
8585
}
8686

@@ -101,7 +101,7 @@ extension SwiftLanguageService {
101101
keys.usr: symbol,
102102
])
103103

104-
let dict = try await self.sourcekitd.send(skreq, fileContents: snapshot.text)
104+
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
105105
if let offset: Int = dict[keys.offset] {
106106
return GeneratedInterfaceDetails(uri: uri, position: snapshot.positionOf(utf8Offset: offset))
107107
} 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)