Skip to content

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

Merged
merged 1 commit into from
Jan 12, 2024

Conversation

joehsieh
Copy link
Contributor

@joehsieh joehsieh commented Nov 9, 2023

I'm following the way we cached computations in SyntaxTreeManager.
#922 (comment)

/// The number of reports to keep
///
/// - Note: This has been chosen without scientific measurements.
private let cacheSize = 5
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

Copy link
Member

@ahoppen ahoppen left a 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.

@joehsieh
Copy link
Contributor Author

ah, right. in this way, we can make the code testable.

@joehsieh joehsieh marked this pull request as draft November 13, 2023 14:59
-> RelatedFullDocumentDiagnosticReport
}

actor DiagnosticReportManager {
Copy link
Contributor Author

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 {
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

@joehsieh joehsieh changed the title Add cache for fullDocumentDiagnosticReport Add cache to avoid requesting diagnostics from sourcekitd frequently. Nov 15, 2023
@joehsieh joehsieh marked this pull request as ready for review November 15, 2023 08:42
return try await reportTask.value
}

func resetCache() async {
Copy link
Member

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?

Copy link
Contributor Author

@joehsieh joehsieh Dec 3, 2023

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.

Copy link
Member

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.

Copy link
Contributor Author

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!

Copy link
Member

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!

Copy link
Member

@ahoppen ahoppen left a 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
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, OK. Fair enough.

Comment on lines 121 to 130
dict[keys.diagnostics]?.forEach { _, diag in
if let diag = Diagnostic(
diag,
in: snapshot,
useEducationalNoteAsCode: self.clientHasDiagnosticsCodeDescriptionSupport
) {
diagnostics.append(diag)
}
return true
}
Copy link
Member

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

Suggested change
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 {
Copy link
Member

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
@ahoppen
Copy link
Member

ahoppen commented Jan 11, 2024

@swift-ci Please test

@ahoppen
Copy link
Member

ahoppen commented Jan 11, 2024

@swift-ci Please test Windows

@ahoppen ahoppen merged commit 3e432c4 into swiftlang:main Jan 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants