Skip to content

Commit 303ce16

Browse files
committed
Add DiagnosticReportManager to manage the cache for diagnostis
1 parent 8af0bb5 commit 303ce16

File tree

3 files changed

+169
-55
lines changed

3 files changed

+169
-55
lines changed

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ target_sources(SourceKitLSP PRIVATE
2020
Swift/CommentXML.swift
2121
Swift/CursorInfo.swift
2222
Swift/Diagnostic.swift
23+
Swift/DiagnosticReportManager.swift
2324
Swift/DocumentSymbols.swift
2425
Swift/EditorPlaceholder.swift
2526
Swift/OpenInterface.swift
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LSPLogging
14+
import LanguageServerProtocol
15+
16+
protocol DiagnosticReportManagerDelegate: AnyObject {
17+
/// Notify the delegate that compute a report with a given snapshot
18+
func reportWithSnapshotReceived(_ snapshot: DocumentSnapshot, compilerArgs: [String]) async throws
19+
-> RelatedFullDocumentDiagnosticReport
20+
21+
/// Notify the delegate that compute a report with a given snapshot for fallback
22+
func fallbackReportWithSnapshotReceived(_ snapshot: DocumentSnapshot) async throws
23+
-> RelatedFullDocumentDiagnosticReport
24+
}
25+
26+
actor DiagnosticReportManager {
27+
/// The cache that stores reports for snapshot ids
28+
///
29+
/// Conceptually, this is a dictionary. To prevent excessive memory usage we
30+
/// only keep `cacheSize` entries within the array. Older entries are at the
31+
/// end of the list, newer entries at the front.
32+
private var reportCache:
33+
[(
34+
snapshotID: DocumentSnapshot.ID,
35+
report: RelatedFullDocumentDiagnosticReport
36+
)] = []
37+
38+
/// The number of reports to keep
39+
///
40+
/// - Note: This has been chosen without scientific measurements.
41+
private let cacheSize = 5
42+
43+
/// Delegate to handle any DiagnosticReportManager events.
44+
private weak var delegate: DiagnosticReportManagerDelegate?
45+
46+
func setDelegate(_ delegate: DiagnosticReportManagerDelegate?) async {
47+
self.delegate = delegate
48+
}
49+
50+
func diagnosticReport(for snapshot: DocumentSnapshot, buildSettings: SwiftCompileCommand?, useCache: Bool = false)
51+
async throws -> RelatedFullDocumentDiagnosticReport
52+
{
53+
guard let delegate = self.delegate else {
54+
throw ResponseError.unknown("DiagnosticReportManagerDelegate should not be nil")
55+
}
56+
if useCache, let report = report(for: snapshot.id) {
57+
return report
58+
}
59+
guard let buildSettings = buildSettings, !buildSettings.isFallback else {
60+
logger.log(
61+
"Producing syntactic diagnostics from the built-in swift-syntax because we have fallback arguments"
62+
)
63+
// If we don't have build settings or we only have fallback build settings,
64+
// sourcekitd won't be able to give us accurate semantic diagnostics.
65+
// Fall back to providing syntactic diagnostics from the built-in
66+
// swift-syntax. That's the best we can do for now.
67+
let report = try await delegate.fallbackReportWithSnapshotReceived(snapshot)
68+
setReport(for: snapshot.id, report: report)
69+
return report
70+
}
71+
72+
let report = try await delegate.reportWithSnapshotReceived(snapshot, compilerArgs: buildSettings.compilerArgs)
73+
setReport(for: snapshot.id, report: report)
74+
return report
75+
}
76+
77+
/// The report for the given document snapshot.
78+
private func report(for snapshotID: DocumentSnapshot.ID) -> RelatedFullDocumentDiagnosticReport? {
79+
return reportCache.first(where: { $0.snapshotID == snapshotID })?.report
80+
}
81+
82+
/// Set the report for the given document snapshot.
83+
///
84+
/// If we are already storing `cacheSize` many reports, the oldest one
85+
/// will get discarded.
86+
private func setReport(for snapshotID: DocumentSnapshot.ID, report: RelatedFullDocumentDiagnosticReport) {
87+
reportCache.insert((snapshotID, report), at: 0)
88+
89+
// Remove any reports for old versions of this document.
90+
reportCache.removeAll(where: { $0.snapshotID < snapshotID })
91+
92+
// If we still have more than `cacheSize` reports, delete the ones that
93+
// were produced last. We can always re-request them on-demand.
94+
while reportCache.count > cacheSize {
95+
reportCache.removeLast()
96+
}
97+
}
98+
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ public actor SwiftLanguageServer: ToolchainLanguageServer {
139139

140140
private var stateChangeHandlers: [(_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void] = []
141141

142+
private let diagnosticReportManager: DiagnosticReportManager
143+
142144
/// Creates a language server for the given client using the sourcekitd dylib specified in `toolchain`.
143145
/// `reopenDocuments` is a closure that will be called if sourcekitd crashes and the `SwiftLanguageServer` asks its parent server to reopen all of its documents.
144146
/// Returns `nil` if `sourcektid` couldn't be found.
@@ -157,6 +159,7 @@ public actor SwiftLanguageServer: ToolchainLanguageServer {
157159
self.state = .connected
158160
self.generatedInterfacesPath = options.generatedInterfacesPath.asURL
159161
try FileManager.default.createDirectory(at: generatedInterfacesPath, withIntermediateDirectories: true)
162+
self.diagnosticReportManager = DiagnosticReportManager()
160163
}
161164

162165
/// - Important: For testing only
@@ -196,7 +199,8 @@ public actor SwiftLanguageServer: ToolchainLanguageServer {
196199

197200
extension SwiftLanguageServer {
198201

199-
public func initialize(_ initialize: InitializeRequest) throws -> InitializeResult {
202+
public func initialize(_ initialize: InitializeRequest) async throws -> InitializeResult {
203+
await diagnosticReportManager.setDelegate(self)
200204
sourcekitd.addNotificationHandler(self)
201205

202206
return InitializeResult(
@@ -396,8 +400,11 @@ extension SwiftLanguageServer {
396400
return
397401
}
398402
do {
399-
let diagnosticReport = try await self.fullDocumentDiagnosticReport(
400-
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(document))
403+
let snapshot = try await documentManager.latestSnapshot(document)
404+
let buildSettings = await self.buildSettings(for: document)
405+
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
406+
for: snapshot,
407+
buildSettings: buildSettings
401408
)
402409

403410
await sourceKitServer.sendNotificationToClient(
@@ -967,10 +974,12 @@ extension SwiftLanguageServer {
967974
}
968975

969976
func retrieveQuickFixCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] {
970-
let diagnosticReport = try await self.fullDocumentDiagnosticReport(
971-
DocumentDiagnosticsRequest(
972-
textDocument: params.textDocument
973-
)
977+
let snapshot = try documentManager.latestSnapshot(params.textDocument.uri)
978+
let buildSettings = await self.buildSettings(for: params.textDocument.uri)
979+
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
980+
for: snapshot,
981+
buildSettings: buildSettings,
982+
useCache: true
974983
)
975984

976985
let codeActions = diagnosticReport.items.flatMap { (diag) -> [CodeAction] in
@@ -1061,55 +1070,13 @@ extension SwiftLanguageServer {
10611070
}
10621071

10631072
public func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport {
1064-
return try await .full(fullDocumentDiagnosticReport(req))
1065-
}
1066-
1067-
private func fullDocumentDiagnosticReport(
1068-
_ req: DocumentDiagnosticsRequest
1069-
) async throws -> RelatedFullDocumentDiagnosticReport {
10701073
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri)
1071-
guard let buildSettings = await self.buildSettings(for: req.textDocument.uri), !buildSettings.isFallback else {
1072-
logger.log(
1073-
"Producing syntactic diagnostics from the built-in swift-syntax because we have fallback arguments"
1074-
)
1075-
// If we don't have build settings or we only have fallback build settings,
1076-
// sourcekitd won't be able to give us accurate semantic diagnostics.
1077-
// Fall back to providing syntactic diagnostics from the built-in
1078-
// swift-syntax. That's the best we can do for now.
1079-
return try await syntacticDiagnosticFromBuiltInSwiftSyntax(for: snapshot)
1080-
}
1081-
1082-
try Task.checkCancellation()
1083-
1084-
let keys = self.keys
1085-
1086-
let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
1087-
skreq[keys.request] = requests.diagnostics
1088-
skreq[keys.sourcefile] = snapshot.uri.pseudoPath
1089-
1090-
// FIXME: SourceKit should probably cache this for us.
1091-
skreq[keys.compilerargs] = buildSettings.compilerArgs
1092-
1093-
let dict = try await self.sourcekitd.send(skreq, fileContents: snapshot.text)
1094-
1095-
try Task.checkCancellation()
1096-
guard (try? documentManager.latestSnapshot(req.textDocument.uri).id) == snapshot.id else {
1097-
// Check that the document wasn't modified while we were getting diagnostics. This could happen because we are
1098-
// calling `fullDocumentDiagnosticReport` from `publishDiagnosticsIfNeeded` outside of `messageHandlingQueue`
1099-
// and thus a concurrent edit is possible while we are waiting for the sourcekitd request to return a result.
1100-
throw ResponseError.unknown("Document was modified while loading document")
1101-
}
1102-
1103-
let supportsCodeDescription = capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport
1104-
var diagnostics: [Diagnostic] = []
1105-
dict[keys.diagnostics]?.forEach { _, diag in
1106-
if let diag = Diagnostic(diag, in: snapshot, useEducationalNoteAsCode: supportsCodeDescription) {
1107-
diagnostics.append(diag)
1108-
}
1109-
return true
1110-
}
1111-
1112-
return RelatedFullDocumentDiagnosticReport(items: diagnostics)
1074+
let buildSettings = await self.buildSettings(for: req.textDocument.uri)
1075+
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
1076+
for: snapshot,
1077+
buildSettings: buildSettings
1078+
)
1079+
return .full(diagnosticReport)
11131080
}
11141081

11151082
public func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? {
@@ -1331,3 +1298,51 @@ extension sourcekitd_uid_t {
13311298
}
13321299
}
13331300
}
1301+
1302+
extension SwiftLanguageServer: DiagnosticReportManagerDelegate {
1303+
func reportWithSnapshotReceived(_ snapshot: DocumentSnapshot, compilerArgs: [String]) async throws
1304+
-> LanguageServerProtocol.RelatedFullDocumentDiagnosticReport
1305+
{
1306+
try Task.checkCancellation()
1307+
1308+
let keys = self.keys
1309+
1310+
let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
1311+
skreq[keys.request] = requests.diagnostics
1312+
skreq[keys.sourcefile] = snapshot.uri.pseudoPath
1313+
1314+
// FIXME: SourceKit should probably cache this for us.
1315+
skreq[keys.compilerargs] = compilerArgs
1316+
1317+
let dict = try await self.sourcekitd.send(skreq, fileContents: snapshot.text)
1318+
1319+
try Task.checkCancellation()
1320+
guard (try? documentManager.latestSnapshot(snapshot.uri).id) == snapshot.id else {
1321+
// Check that the document wasn't modified while we were getting diagnostics. This could happen because we are
1322+
// calling `fullDocumentDiagnosticReport` from `publishDiagnosticsIfNeeded` outside of `messageHandlingQueue`
1323+
// and thus a concurrent edit is possible while we are waiting for the sourcekitd request to return a result.
1324+
throw ResponseError.unknown("Document was modified while loading document")
1325+
}
1326+
1327+
let supportsCodeDescription = capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport
1328+
var diagnostics: [Diagnostic] = []
1329+
dict[keys.diagnostics]?.forEach { _, diag in
1330+
if let diag = Diagnostic(diag, in: snapshot, useEducationalNoteAsCode: supportsCodeDescription) {
1331+
diagnostics.append(diag)
1332+
}
1333+
return true
1334+
}
1335+
1336+
return RelatedFullDocumentDiagnosticReport(items: diagnostics)
1337+
}
1338+
1339+
func fallbackReportWithSnapshotReceived(_ snapshot: DocumentSnapshot) async throws
1340+
-> LanguageServerProtocol.RelatedFullDocumentDiagnosticReport
1341+
{
1342+
// If we don't have build settings or we only have fallback build settings,
1343+
// sourcekitd won't be able to give us accurate semantic diagnostics.
1344+
// Fall back to providing syntactic diagnostics from the built-in
1345+
// swift-syntax. That's the best we can do for now.
1346+
return try await syntacticDiagnosticFromBuiltInSwiftSyntax(for: snapshot)
1347+
}
1348+
}

0 commit comments

Comments
 (0)