Skip to content

Commit 84007ca

Browse files
authored
Merge pull request #1343 from ahoppen/generating-build-graph-logging
2 parents 2b04ad6 + 069e8a2 commit 84007ca

File tree

8 files changed

+124
-59
lines changed

8 files changed

+124
-59
lines changed

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,15 +248,16 @@ public final class TestSourceKitLSPClient: MessageHandler {
248248
return try await nextNotification(ofType: PublishDiagnosticsNotification.self, timeout: timeout)
249249
}
250250

251-
/// Waits for the next notification of the given type to be sent to the client. Ignores any notifications that are of
252-
/// a different type.
251+
/// Waits for the next notification of the given type to be sent to the client that satisfies the given predicate.
252+
/// Ignores any notifications that are of a different type or that don't satisfy the predicate.
253253
public func nextNotification<ExpectedNotificationType: NotificationType>(
254254
ofType: ExpectedNotificationType.Type,
255+
satisfying predicate: (ExpectedNotificationType) -> Bool = { _ in true },
255256
timeout: TimeInterval = defaultTimeout
256257
) async throws -> ExpectedNotificationType {
257258
while true {
258259
let nextNotification = try await nextNotification(timeout: timeout)
259-
if let notification = nextNotification as? ExpectedNotificationType {
260+
if let notification = nextNotification as? ExpectedNotificationType, predicate(notification) {
260261
return notification
261262
}
262263
}

Sources/SemanticIndex/PreparationTaskDescription.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ public struct PreparationTaskDescription: IndexTaskDescription {
7474
// Only use the last two digits of the preparation ID for the logging scope to avoid creating too many scopes.
7575
// See comment in `withLoggingScope`.
7676
// The last 2 digits should be sufficient to differentiate between multiple concurrently running preparation operations
77-
await withLoggingSubsystemAndScope(
78-
subsystem: "org.swift.sourcekit-lsp.indexing",
79-
scope: "preparation-\(id % 100)"
80-
) {
77+
await withLoggingSubsystemAndScope(subsystem: indexLoggingSubsystem, scope: "preparation-\(id % 100)") {
8178
let targetsToPrepare = await targetsToPrepare.asyncFilter {
8279
await !preparationUpToDateStatus.isUpToDate($0)
8380
}.sorted(by: {
@@ -98,10 +95,13 @@ public struct PreparationTaskDescription: IndexTaskDescription {
9895
let signposter = Logger(subsystem: LoggingScope.subsystem, category: "preparation").makeSignposter()
9996
let signpostID = signposter.makeSignpostID()
10097
let state = signposter.beginInterval("Preparing", id: signpostID, "Preparing \(targetsToPrepareDescription)")
98+
let startDate = Date()
10199
defer {
100+
logger.log(
101+
"Finished preparation in \(Date().timeIntervalSince(startDate) * 1000, privacy: .public)ms: \(targetsToPrepareDescription)"
102+
)
102103
signposter.endInterval("Preparing", state)
103104
}
104-
let startDate = Date()
105105
do {
106106
try await buildSystemManager.prepare(
107107
targets: targetsToPrepare,
@@ -116,9 +116,6 @@ public struct PreparationTaskDescription: IndexTaskDescription {
116116
if !Task.isCancelled {
117117
await preparationUpToDateStatus.markUpToDate(targetsToPrepare, updateOperationStartDate: startDate)
118118
}
119-
logger.log(
120-
"Finished preparation in \(Date().timeIntervalSince(startDate) * 1000, privacy: .public)ms: \(targetsToPrepareDescription)"
121-
)
122119
}
123120
}
124121

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import LSPLogging
1515
import LanguageServerProtocol
1616
import SKCore
1717

18+
/// The logging subsystem that should be used for all index-related logging.
19+
let indexLoggingSubsystem = "org.swift.sourcekit-lsp.indexing"
20+
1821
/// A wrapper around `QueuedTask` that only allows equality comparison and inspection whether the `QueuedTask` is
1922
/// currently executing.
2023
///
@@ -186,28 +189,40 @@ public final actor SemanticIndexManager {
186189
/// This method is intended to initially update the index of a project after it is opened.
187190
public func scheduleBuildGraphGenerationAndBackgroundIndexAllFiles() async {
188191
generateBuildGraphTask = Task(priority: .low) {
189-
let signposter = Logger(subsystem: LoggingScope.subsystem, category: "preparation").makeSignposter()
190-
let signpostID = signposter.makeSignpostID()
191-
let state = signposter.beginInterval("Preparing", id: signpostID, "Generating build graph")
192-
defer {
193-
signposter.endInterval("Preparing", state)
194-
}
195-
await orLog("Generating build graph") { try await self.buildSystemManager.generateBuildGraph() }
196-
// Ensure that we have an up-to-date indexstore-db. Waiting for the indexstore-db to be updated is cheaper than
197-
// potentially not knowing about unit files, which causes the corresponding source files to be re-indexed.
198-
index.pollForUnitChangesAndWait()
199-
let index = index.checked(for: .modifiedFiles)
200-
let filesToIndex = await self.buildSystemManager.sourceFiles().lazy.map(\.uri)
201-
.filter { uri in
202-
guard let url = uri.fileURL else {
203-
// The URI is not a file, so there's nothing we can index.
204-
return false
205-
}
206-
return !index.hasUpToDateUnit(for: url)
192+
await withLoggingSubsystemAndScope(subsystem: indexLoggingSubsystem, scope: "build-graph-generation") {
193+
logger.log(
194+
"Starting build graph generation with priority \(Task.currentPriority.rawValue, privacy: .public)"
195+
)
196+
let signposter = logger.makeSignposter()
197+
let signpostID = signposter.makeSignpostID()
198+
let state = signposter.beginInterval("Preparing", id: signpostID, "Generating build graph")
199+
let startDate = Date()
200+
defer {
201+
logger.log(
202+
"Finished build graph generation in \(Date().timeIntervalSince(startDate) * 1000, privacy: .public)ms"
203+
)
204+
signposter.endInterval("Preparing", state)
207205
}
208-
await scheduleBackgroundIndex(files: filesToIndex)
209-
generateBuildGraphTask = nil
206+
await testHooks.buildGraphGenerationDidStart?()
207+
await orLog("Generating build graph") { try await self.buildSystemManager.generateBuildGraph() }
208+
// Ensure that we have an up-to-date indexstore-db. Waiting for the indexstore-db to be updated is cheaper than
209+
// potentially not knowing about unit files, which causes the corresponding source files to be re-indexed.
210+
index.pollForUnitChangesAndWait()
211+
await testHooks.buildGraphGenerationDidFinish?()
212+
let index = index.checked(for: .modifiedFiles)
213+
let filesToIndex = await self.buildSystemManager.sourceFiles().lazy.map(\.uri)
214+
.filter { uri in
215+
guard let url = uri.fileURL else {
216+
// The URI is not a file, so there's nothing we can index.
217+
return false
218+
}
219+
return !index.hasUpToDateUnit(for: url)
220+
}
221+
await scheduleBackgroundIndex(files: filesToIndex)
222+
generateBuildGraphTask = nil
223+
}
210224
}
225+
indexStatusDidChange()
211226
}
212227

213228
/// Wait for all in-progress index tasks to finish.

Sources/SemanticIndex/TestHooks.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
/// Callbacks that allow inspection of internal state modifications during testing.
1414
public struct IndexTestHooks: Sendable {
15+
public var buildGraphGenerationDidStart: (@Sendable () async -> Void)?
16+
17+
public var buildGraphGenerationDidFinish: (@Sendable () async -> Void)?
18+
1519
public var preparationTaskDidStart: (@Sendable (PreparationTaskDescription) async -> Void)?
1620

1721
public var preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)?
@@ -22,11 +26,15 @@ public struct IndexTestHooks: Sendable {
2226
public var updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)?
2327

2428
public init(
29+
buildGraphGenerationDidStart: (@Sendable () async -> Void)? = nil,
30+
buildGraphGenerationDidFinish: (@Sendable () async -> Void)? = nil,
2531
preparationTaskDidStart: (@Sendable (PreparationTaskDescription) async -> Void)? = nil,
2632
preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)? = nil,
2733
updateIndexStoreTaskDidStart: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? = nil,
2834
updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? = nil
2935
) {
36+
self.buildGraphGenerationDidStart = buildGraphGenerationDidStart
37+
self.buildGraphGenerationDidFinish = buildGraphGenerationDidFinish
3038
self.preparationTaskDidStart = preparationTaskDidStart
3139
self.preparationTaskDidFinish = preparationTaskDidFinish
3240
self.updateIndexStoreTaskDidStart = updateIndexStoreTaskDidStart

Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,7 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
134134
// Only use the last two digits of the indexing ID for the logging scope to avoid creating too many scopes.
135135
// See comment in `withLoggingScope`.
136136
// The last 2 digits should be sufficient to differentiate between multiple concurrently running indexing operation.
137-
await withLoggingSubsystemAndScope(
138-
subsystem: "org.swift.sourcekit-lsp.indexing",
139-
scope: "update-indexstore-\(id % 100)"
140-
) {
137+
await withLoggingSubsystemAndScope(subsystem: indexLoggingSubsystem, scope: "update-indexstore-\(id % 100)") {
141138
let startDate = Date()
142139

143140
await testHooks.updateIndexStoreTaskDidStart?(self)

Sources/SourceKitLSP/IndexProgressManager.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ actor IndexProgressManager {
9393
}
9494
}
9595

96-
if indexTasks.isEmpty {
96+
if indexTasks.isEmpty && !isGeneratingBuildGraph {
9797
// Nothing left to index. Reset the target count and dismiss the work done progress.
9898
queuedIndexTasks = 0
9999
workDoneProgress = nil
@@ -104,7 +104,12 @@ actor IndexProgressManager {
104104
// `indexTasksWereScheduled` calls yet but the semantic index managers already track them in their in-progress tasks.
105105
// Clip the finished tasks to 0 because showing a negative number there looks stupid.
106106
let finishedTasks = max(queuedIndexTasks - indexTasks.count, 0)
107-
var message = "\(finishedTasks) / \(queuedIndexTasks)"
107+
var message: String
108+
if isGeneratingBuildGraph {
109+
message = "Generating build graph"
110+
} else {
111+
message = "\(finishedTasks) / \(queuedIndexTasks)"
112+
}
108113

109114
if await sourceKitLSPServer.options.indexOptions.showActivePreparationTasksInProgress {
110115
var inProgressTasks: [String] = []
@@ -121,7 +126,12 @@ actor IndexProgressManager {
121126
message += "\n\n" + inProgressTasks.joined(separator: "\n")
122127
}
123128

124-
let percentage = Int(Double(finishedTasks) / Double(queuedIndexTasks) * 100)
129+
let percentage: Int
130+
if queuedIndexTasks != 0 {
131+
percentage = Int(Double(finishedTasks) / Double(queuedIndexTasks) * 100)
132+
} else {
133+
percentage = 0
134+
}
125135
if let workDoneProgress {
126136
workDoneProgress.update(message: message, percentage: percentage)
127137
} else {

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,9 @@ public actor SourceKitLSPServer {
508508
private var workspacesAndIsImplicit: [(workspace: Workspace, isImplicit: Bool)] = [] {
509509
didSet {
510510
uriToWorkspaceCache = [:]
511+
// `indexProgressManager` iterates over all workspaces in the SourceKitLSPServer. Modifying workspaces might thus
512+
// update the index progress status.
513+
indexProgressManager.indexStatusDidChange()
511514
}
512515
}
513516

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,21 @@ final class BackgroundIndexingTests: XCTestCase {
333333
}
334334

335335
func testBackgroundIndexingStatusWorkDoneProgress() async throws {
336+
let receivedBeginProgressNotification = self.expectation(
337+
description: "Received work done progress saying build graph generation"
338+
)
339+
let receivedReportProgressNotification = self.expectation(
340+
description: "Received work done progress saying indexing"
341+
)
342+
var serverOptions = backgroundIndexingOptions
343+
serverOptions.indexTestHooks = IndexTestHooks(
344+
buildGraphGenerationDidFinish: {
345+
await self.fulfillment(of: [receivedBeginProgressNotification], timeout: defaultTimeout)
346+
},
347+
updateIndexStoreTaskDidFinish: { _ in
348+
await self.fulfillment(of: [receivedReportProgressNotification], timeout: defaultTimeout)
349+
}
350+
)
336351
let project = try await SwiftPMTestProject(
337352
files: [
338353
"MyFile.swift": """
@@ -343,36 +358,55 @@ final class BackgroundIndexingTests: XCTestCase {
343358
"""
344359
],
345360
capabilities: ClientCapabilities(window: WindowClientCapabilities(workDoneProgress: true)),
346-
serverOptions: backgroundIndexingOptions,
361+
serverOptions: serverOptions,
362+
pollIndex: false,
347363
preInitialization: { testClient in
348364
testClient.handleMultipleRequests { (request: CreateWorkDoneProgressRequest) in
349365
return VoidResponse()
350366
}
351367
}
352368
)
353-
var indexingWorkDoneProgressToken: ProgressToken? = nil
354-
var didGetEndWorkDoneProgress = false
355-
// Loop terminates when we see the work done end progress or if waiting for the next notification times out
356-
LOOP: while true {
357-
let workDoneProgress = try await project.testClient.nextNotification(ofType: WorkDoneProgress.self)
358-
switch workDoneProgress.value {
359-
case .begin(let data):
360-
if data.title == "Indexing" {
361-
XCTAssertNil(indexingWorkDoneProgressToken, "Received multiple work done progress notifications for indexing")
362-
indexingWorkDoneProgressToken = workDoneProgress.token
363-
}
364-
case .report:
365-
// We ignore progress reports in the test because it's non-deterministic how many we get
366-
break
367-
case .end:
368-
if workDoneProgress.token == indexingWorkDoneProgressToken {
369-
didGetEndWorkDoneProgress = true
370-
break LOOP
369+
370+
let beginNotification = try await project.testClient.nextNotification(
371+
ofType: WorkDoneProgress.self,
372+
satisfying: { notification in
373+
guard case .begin(let data) = notification.value else {
374+
return false
371375
}
376+
return data.title == "Indexing"
372377
}
378+
)
379+
receivedBeginProgressNotification.fulfill()
380+
guard case .begin(let beginData) = beginNotification.value else {
381+
XCTFail("Expected begin notification")
382+
return
373383
}
374-
XCTAssertNotNil(indexingWorkDoneProgressToken, "Expected to receive a work done progress start")
375-
XCTAssert(didGetEndWorkDoneProgress, "Expected end work done progress")
384+
XCTAssertEqual(beginData.message, "Generating build graph")
385+
let indexingWorkDoneProgressToken = beginNotification.token
386+
387+
_ = try await project.testClient.nextNotification(
388+
ofType: WorkDoneProgress.self,
389+
satisfying: { notification in
390+
guard notification.token == indexingWorkDoneProgressToken,
391+
case .report(let reportData) = notification.value,
392+
reportData.message == "0 / 1"
393+
else {
394+
return false
395+
}
396+
return true
397+
}
398+
)
399+
receivedReportProgressNotification.fulfill()
400+
401+
_ = try await project.testClient.nextNotification(
402+
ofType: WorkDoneProgress.self,
403+
satisfying: { notification in
404+
guard notification.token == indexingWorkDoneProgressToken, case .end = notification.value else {
405+
return false
406+
}
407+
return true
408+
}
409+
)
376410

377411
withExtendedLifetime(project) {}
378412
}

0 commit comments

Comments
 (0)