-
Notifications
You must be signed in to change notification settings - Fork 314
Add cache to avoid requesting diagnostics from sourcekitd frequently. #965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
/// The number of reports to keep | ||
/// | ||
/// - Note: This has been chosen without scientific measurements. | ||
private let cacheSize = 5 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i tentatively set the size of cache to 5 because i'm not sure if there is a better magic-number for it.
) async throws -> RelatedFullDocumentDiagnosticReport { | ||
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) | ||
if useCache, let report = report(for: snapshot.id) { | ||
return report |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i was trying to add a test for the code changes, but i didn't find a way to do it.
at least this change doesn't break any existing tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of implicitly setting the cached reports in SwiftLanguageServer
, I would prefer to have a similar design to SyntaxTreeManager
. SyntaxTreeManager
is an encapsulated class that has a single function
func diagnosticReport(for snapshot: DocumentSnapshot, buildSettings: SwiftCompileCommand) throws -> RelatedFullDocumentDiagnosticReport
And then the manager class will decide whether it has diagnostics cached already and, if not, sends a request to sourcekitd
to compute them.
ah, right. in this way, we can make the code testable. |
0927ce1
to
d10eef5
Compare
-> RelatedFullDocumentDiagnosticReport | ||
} | ||
|
||
actor DiagnosticReportManager { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: I'll add tests for this new actor.
import LSPLogging | ||
import LanguageServerProtocol | ||
|
||
protocol DiagnosticReportManagerDelegate: AnyObject { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t think we need a delegate here. reportWithSnapshotReceived
and fallbackReportWithSnapshotReceived
should be implemented in the DiagnosticReportManager
. I think it just means that DiagnosticReportManager
needs to gain SourceKitD
, DocumentManager
and clientHasDiagnosticsCodeDescriptionSupport: Bool
properties. That way, the logic is isolated and we’re playing less ping-pong between DiagnosticReportManager
and SwiftLanguageServer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i was considering the the way you mentioned above before, but regarding the testability of DiagnosticReportManager, i decided to have a delegate to make the testing easier.
it makes sense to remove the delegate to avoid playing ping-pong (let me update the code for it first), but i was wondering if there is a way to test it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it’s fine to test it by just sending the diagnostic requests followed by a code action request (which I think we already have a test for). That should trigger the path that computes diagnostics and also the one that re-used cached diagnostics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, okie. then, let me make this PR ready for review.
d10eef5
to
9904dcb
Compare
return try await reportTask.value | ||
} | ||
|
||
func resetCache() async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we ever need to reset the cache? The snapshot ID + build settings should be sufficient that we re-request diagnostic whenever they might have changed. Or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are two tests will be failed without reseting cache.
Test Case '-[SourceKitLSPTests.SKTests testDependenciesUpdatedSwift]' failed (5.170 seconds).
Test Case '-[SourceKitLSPTests.LocalSwiftTests testDiagnosticsReopen]' failed (1.400 seconds).
i think the cache for the file becomes dirty if the file is reopened with changed dependencies or content, so i reset the cache when open, reopen and close document for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. That makes sense. But we should not need to reset the entire cache, we should only need to clear the diagnostics for the document that we are opening/closing/reopening.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ahoppen sorry for the late reply. 🙇 i've updated the code. PTAL. thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to apologize., thank you for fixing this issue!
0181498
to
d0e7d81
Compare
d0e7d81
to
38fba69
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve got two nitpicks, otherwise looks good to merge for me.
) async throws -> LanguageServerProtocol.RelatedFullDocumentDiagnosticReport { | ||
try Task.checkCancellation() | ||
|
||
let keys = self.keys |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t think this is necessary. keys
should already refer to self.keys
everywhere below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i checked the var keys the implementations is
private nonisolated var keys: sourcekitd_keys { return sourcekitd.keys }
i think the purpose is to ensure the value of keys won't change in func requestReport
the same pattern is happening in other files.
e.g. SwiftLanguageServer.swift, SemanticRefactoring.swift, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, OK. Fair enough.
dict[keys.diagnostics]?.forEach { _, diag in | ||
if let diag = Diagnostic( | ||
diag, | ||
in: snapshot, | ||
useEducationalNoteAsCode: self.clientHasDiagnosticsCodeDescriptionSupport | ||
) { | ||
diagnostics.append(diag) | ||
} | ||
return true | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recently introduced map
and compactMap
on SKDResponseArray
, which we could use here
dict[keys.diagnostics]?.forEach { _, diag in | |
if let diag = Diagnostic( | |
diag, | |
in: snapshot, | |
useEducationalNoteAsCode: self.clientHasDiagnosticsCodeDescriptionSupport | |
) { | |
diagnostics.append(diag) | |
} | |
return true | |
} | |
let diagnostics: [Diagnostic] = dict[keys.diagnostics]?.compactMap({ diag in | |
Diagnostic( | |
diag, | |
in: snapshot, | |
useEducationalNoteAsCode: self.clientHasDiagnosticsCodeDescriptionSupport | |
) | |
}) ?? [] |
return try await reportTask.value | ||
} | ||
|
||
func resetCache() async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to apologize., thank you for fixing this issue!
Move the responsibility of sending requests to get diagnostics from SwiftLanguageServer to DiagnosticReportManager Make buildSettings as part of the key to the cache Add ReportTask and refine the code Reset cache for reportTasks if needed Finetune the wordings and logics Solve conflicts Only remove necessary items from cache Refine the code
38fba69
to
8dcfb30
Compare
@swift-ci Please test |
@swift-ci Please test Windows |
I'm following the way we cached computations in SyntaxTreeManager.
#922 (comment)