Skip to content

Commit a28eff6

Browse files
committed
Don't wait for build graph generation when file is changed
Since we re-index source files if the build server sends us a `buildTarget/didChange` notification, we no longer need to wait for an up-to-date build graph when a file is modified. This resolves an issue that causes a `Scheduling tasks` progress to appear in the status bar whenever a file in the project is changed. Before swiftlang#1973, this happened fairly frequently during the initial indexing when a header file was written into the build directory. After that PR the `Scheduling Indexing` status appears a lot more frequently, namely every time the index’s database is modified, which happens quite all the time during indexing...
1 parent 71dfc73 commit a28eff6

File tree

2 files changed

+98
-90
lines changed

2 files changed

+98
-90
lines changed

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 98 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ package final actor SemanticIndexManager {
175175

176176
/// The tasks to generate the build graph (resolving package dependencies, generating the build description,
177177
/// ...) and to schedule indexing of modified tasks.
178-
private var scheduleIndexingTasks: [UUID: Task<Void, Never>] = [:]
178+
private var buildGraphGenerationTasks: [UUID: Task<Void, Never>] = [:]
179179

180180
private let preparationUpToDateTracker = UpToDateTracker<BuildTargetIdentifier>()
181181

@@ -225,7 +225,7 @@ package final actor SemanticIndexManager {
225225
if inProgressPreparationTasks.values.contains(where: { $0.purpose == .forEditorFunctionality }) {
226226
return .preparingFileForEditorFunctionality
227227
}
228-
if !scheduleIndexingTasks.isEmpty {
228+
if !buildGraphGenerationTasks.isEmpty {
229229
return .schedulingIndexing
230230
}
231231
let preparationTasks = inProgressPreparationTasks.mapValues { inProgressTask in
@@ -270,46 +270,37 @@ package final actor SemanticIndexManager {
270270
/// Regenerate the build graph (also resolving package dependencies) and then index all the source files known to the
271271
/// build system that don't currently have a unit with a timestamp that matches the mtime of the file.
272272
///
273-
/// If `filesToIndex` is `nil`, all files in the build system with out-of-date units are indexed.
274-
///
275-
/// If `ensureAllUnitsRegisteredInIndex` is `true`, ensure that all units are registered in the index before
276-
/// triggering the indexing. This is a costly operation since it iterates through all the unit files on the file
273+
/// This is a costly operation since it iterates through all the unit files on the file
277274
/// system but if existing unit files are not known to the index, we might re-index those files even if they are
278275
/// up-to-date. Generally this should be set to `true` during the initial indexing (in which case we might be need to
279276
/// build the indexstore-db) and `false` for all subsequent indexing.
280-
package func scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
281-
filesToIndex: [DocumentURI]?,
282-
ensureAllUnitsRegisteredInIndex: Bool,
283-
indexFilesWithUpToDateUnit: Bool
284-
) async {
277+
package func scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(indexFilesWithUpToDateUnit: Bool) async {
285278
let taskId = UUID()
286279
let generateBuildGraphTask = Task(priority: .low) {
287280
await withLoggingSubsystemAndScope(subsystem: indexLoggingSubsystem, scope: "build-graph-generation") {
288281
await hooks.buildGraphGenerationDidStart?()
289282
await self.buildSystemManager.waitForUpToDateBuildGraph()
290-
if ensureAllUnitsRegisteredInIndex {
291-
index.pollForUnitChangesAndWait()
292-
}
283+
// Polling for unit changes is a costly operation since it iterates through all the unit files on the file
284+
// system but if existing unit files are not known to the index, we might re-index those files even if they are
285+
// up-to-date. This operation is worth the cost during initial indexing and during the manual re-index command.
286+
index.pollForUnitChangesAndWait()
293287
await hooks.buildGraphGenerationDidFinish?()
294288
// TODO: Ideally this would be a type like any Collection<DocumentURI> & Sendable but that doesn't work due to
295289
// https://github.com/swiftlang/swift/issues/75602
296290
let filesToIndex: [DocumentURI] =
297-
if let filesToIndex {
298-
filesToIndex
299-
} else {
300-
await orLog("Getting files to index") {
301-
try await self.buildSystemManager.buildableSourceFiles().sorted { $0.stringValue < $1.stringValue }
302-
} ?? []
303-
}
304-
_ = await self.scheduleIndexing(
291+
await orLog("Getting files to index") {
292+
try await self.buildSystemManager.buildableSourceFiles().sorted { $0.stringValue < $1.stringValue }
293+
} ?? []
294+
await self.scheduleIndexing(
305295
of: filesToIndex,
296+
waitForBuildGraphGenerationTasks: false,
306297
indexFilesWithUpToDateUnit: indexFilesWithUpToDateUnit,
307298
priority: .low
308299
)
309-
scheduleIndexingTasks[taskId] = nil
300+
buildGraphGenerationTasks[taskId] = nil
310301
}
311302
}
312-
scheduleIndexingTasks[taskId] = generateBuildGraphTask
303+
buildGraphGenerationTasks[taskId] = generateBuildGraphTask
313304
indexProgressStatusDidChange()
314305
}
315306

@@ -318,15 +309,11 @@ package final actor SemanticIndexManager {
318309
package func scheduleReindex() async {
319310
await indexStoreUpToDateTracker.markAllKnownOutOfDate()
320311
await preparationUpToDateTracker.markAllKnownOutOfDate()
321-
await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
322-
filesToIndex: nil,
323-
ensureAllUnitsRegisteredInIndex: false,
324-
indexFilesWithUpToDateUnit: true
325-
)
312+
await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(indexFilesWithUpToDateUnit: true)
326313
}
327314

328315
private func waitForBuildGraphGenerationTasks() async {
329-
await scheduleIndexingTasks.values.concurrentForEach { await $0.value }
316+
await buildGraphGenerationTasks.values.concurrentForEach { await $0.value }
330317
}
331318

332319
/// Wait for all in-progress index tasks to finish.
@@ -358,86 +345,98 @@ package final actor SemanticIndexManager {
358345
logger.info(
359346
"Waiting for up-to-date index for \(uris.map { $0.fileURL?.lastPathComponent ?? $0.stringValue }.joined(separator: ", "))"
360347
)
361-
// If there's a build graph update in progress wait for that to finish so we can discover new files in the build
362-
// system.
363-
await waitForBuildGraphGenerationTasks()
364348

365349
// Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will
366350
// - Wait for the existing index operations to finish if they have the same number of files.
367351
// - Reschedule the background index task in favor of an index task with fewer source files.
368-
await self.scheduleIndexing(of: uris, indexFilesWithUpToDateUnit: false, priority: nil).value
352+
await self.scheduleIndexing(
353+
of: uris,
354+
waitForBuildGraphGenerationTasks: true,
355+
indexFilesWithUpToDateUnit: false,
356+
priority: nil
357+
).value
369358
index.pollForUnitChangesAndWait()
370359
logger.debug("Done waiting for up-to-date index")
371360
}
372361

373-
package func filesDidChange(_ events: [FileEvent]) async {
374-
// We only re-index the files that were changed and don't re-index any of their dependencies. See the
375-
// `Documentation/Files_To_Reindex.md` file.
376-
let changedFiles = events.map(\.uri)
377-
await indexStoreUpToDateTracker.markOutOfDate(changedFiles)
378-
379-
// Preparation tracking should be per file. For now consider any non-known-language change as having to re-prepare
380-
// the target itself so that we re-prepare potential input files to plugins.
381-
// https://github.com/swiftlang/sourcekit-lsp/issues/1975
382-
var outOfDateTargets = Set<BuildTargetIdentifier>()
383-
for file in changedFiles {
384-
let changedTargets = await buildSystemManager.targets(for: file)
385-
if Language(inferredFromFileExtension: file) == nil {
386-
outOfDateTargets.formUnion(changedTargets)
387-
}
388-
389-
let dependentTargets = await buildSystemManager.targets(dependingOn: changedTargets)
390-
outOfDateTargets.formUnion(dependentTargets)
391-
}
392-
if !outOfDateTargets.isEmpty {
393-
logger.info(
394-
"""
395-
Marking dependent targets as out-of-date: \
396-
\(String(outOfDateTargets.map(\.uri.stringValue).joined(separator: ", ")))
397-
"""
398-
)
399-
await preparationUpToDateTracker.markOutOfDate(outOfDateTargets)
400-
}
401-
402-
await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
403-
filesToIndex: changedFiles,
404-
ensureAllUnitsRegisteredInIndex: false,
405-
indexFilesWithUpToDateUnit: false
406-
)
407-
}
408-
409-
package func buildTargetsChanged(_ changes: [BuildTargetEvent]?) async {
410-
let targets = changes?.map(\.target)
362+
package func filesDidChange(_ events: [FileEvent]) {
363+
// We don't care about the order of file change events, so we can execute the changes in a task, allowing further
364+
// file change handling to continue before we have scheduled the file re-indexing.
365+
Task {
366+
// We only re-index the files that were changed and don't re-index any of their dependencies. See the
367+
// `Documentation/Files_To_Reindex.md` file.
368+
let changedFiles = events.map(\.uri)
369+
await indexStoreUpToDateTracker.markOutOfDate(changedFiles)
370+
371+
// Preparation tracking should be per file. For now consider any non-known-language change as having to re-prepare
372+
// the target itself so that we re-prepare potential input files to plugins.
373+
// https://github.com/swiftlang/sourcekit-lsp/issues/1975
374+
var outOfDateTargets = Set<BuildTargetIdentifier>()
375+
for file in changedFiles {
376+
let changedTargets = await buildSystemManager.targets(for: file)
377+
if Language(inferredFromFileExtension: file) == nil {
378+
outOfDateTargets.formUnion(changedTargets)
379+
}
411380

412-
if let targets {
413-
var targetsAndDependencies = targets
414-
targetsAndDependencies += await buildSystemManager.targets(dependingOn: Set(targets))
415-
if !targetsAndDependencies.isEmpty {
381+
let dependentTargets = await buildSystemManager.targets(dependingOn: changedTargets)
382+
outOfDateTargets.formUnion(dependentTargets)
383+
}
384+
if !outOfDateTargets.isEmpty {
416385
logger.info(
417386
"""
418387
Marking dependent targets as out-of-date: \
419-
\(String(targetsAndDependencies.map(\.uri.stringValue).joined(separator: ", ")))
388+
\(String(outOfDateTargets.map(\.uri.stringValue).joined(separator: ", ")))
420389
"""
421390
)
422-
await preparationUpToDateTracker.markOutOfDate(targetsAndDependencies)
391+
await preparationUpToDateTracker.markOutOfDate(outOfDateTargets)
423392
}
424-
} else {
425-
await preparationUpToDateTracker.markAllKnownOutOfDate()
426-
}
427393

428-
await orLog("Scheduling re-indexing of changed targets") {
429-
var sourceFiles = try await self.buildSystemManager.sourceFiles(includeNonBuildableFiles: false)
430-
if let targets {
431-
sourceFiles = sourceFiles.filter { !$0.value.targets.isDisjoint(with: targets) }
432-
}
433-
_ = await scheduleIndexing(
434-
of: sourceFiles.keys,
394+
await scheduleIndexing(
395+
of: changedFiles,
396+
waitForBuildGraphGenerationTasks: true,
435397
indexFilesWithUpToDateUnit: false,
436398
priority: .low
437399
)
438400
}
439401
}
440402

403+
package func buildTargetsChanged(_ changes: [BuildTargetEvent]?) async {
404+
// We don't care about the order of target change events, so we can execute the changes in a task, allowing further
405+
// target change handling to continue.
406+
Task {
407+
let targets = changes?.map(\.target)
408+
409+
if let targets {
410+
var targetsAndDependencies = targets
411+
targetsAndDependencies += await buildSystemManager.targets(dependingOn: Set(targets))
412+
if !targetsAndDependencies.isEmpty {
413+
logger.info(
414+
"""
415+
Marking dependent targets as out-of-date: \
416+
\(String(targetsAndDependencies.map(\.uri.stringValue).joined(separator: ", ")))
417+
"""
418+
)
419+
await preparationUpToDateTracker.markOutOfDate(targetsAndDependencies)
420+
}
421+
} else {
422+
await preparationUpToDateTracker.markAllKnownOutOfDate()
423+
}
424+
425+
await orLog("Scheduling re-indexing of changed targets") {
426+
var sourceFiles = try await self.buildSystemManager.sourceFiles(includeNonBuildableFiles: false)
427+
if let targets {
428+
sourceFiles = sourceFiles.filter { !$0.value.targets.isDisjoint(with: targets) }
429+
}
430+
await scheduleIndexing(
431+
of: sourceFiles.keys,
432+
waitForBuildGraphGenerationTasks: true,
433+
indexFilesWithUpToDateUnit: false,
434+
priority: .low
435+
)
436+
}
437+
}
438+
}
439+
441440
/// Returns the files that should be indexed to get up-to-date index information for the given files.
442441
///
443442
/// If `files` contains a header file, this will return a `FileToIndex` that re-indexes a main file which includes the
@@ -696,12 +695,23 @@ package final actor SemanticIndexManager {
696695
/// known to be up-to-date based on `indexStoreUpToDateTracker` or the unit timestamp will not be re-indexed unless
697696
/// `indexFilesWithUpToDateUnit` is `true`.
698697
///
698+
/// If `waitForBuildGraphGenerationTasks` is `true` and there are any tasks in progress that wait for an up-to-date
699+
/// build graph, wait for those to finish. This is helpful so to avoid the following: We receive a build target change
700+
/// notification before the initial background indexing has finished. If indexstore-db hasn't been initialized with
701+
/// `pollForUnitChangesAndWait` yet, we might not know that the changed targets' files' index is actually up-to-date
702+
/// and would thus schedule an unnecessary re-index of the file.
703+
///
699704
/// The returned task finishes when all files are indexed.
705+
@discardableResult
700706
package func scheduleIndexing(
701707
of files: some Collection<DocumentURI> & Sendable,
708+
waitForBuildGraphGenerationTasks: Bool,
702709
indexFilesWithUpToDateUnit: Bool,
703710
priority: TaskPriority?
704711
) async -> Task<Void, Never> {
712+
if waitForBuildGraphGenerationTasks {
713+
await self.waitForBuildGraphGenerationTasks()
714+
}
705715
// Perform a quick initial check to whether the files is up-to-date, in which case we don't need to schedule a
706716
// prepare and index operation at all.
707717
// We will check the up-to-date status again in `IndexTaskDescription.execute`. This ensures that if we schedule

Sources/SourceKitLSP/Workspace.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,6 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
175175
}
176176
if let semanticIndexManager {
177177
await semanticIndexManager.scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
178-
filesToIndex: nil,
179-
ensureAllUnitsRegisteredInIndex: true,
180178
indexFilesWithUpToDateUnit: false
181179
)
182180
}

0 commit comments

Comments
 (0)