@@ -175,7 +175,11 @@ package final actor SemanticIndexManager {
175
175
176
176
/// The tasks to generate the build graph (resolving package dependencies, generating the build description,
177
177
/// ...) and to schedule indexing of modified tasks.
178
- private var scheduleIndexingTasks : [ UUID : Task < Void , Never > ] = [ : ]
178
+ private var buildGraphGenerationTasks : [ UUID : Task < Void , Never > ] = [ : ]
179
+
180
+ /// Tasks that were created from `workspace/didChangeWatchedFiles` or `buildTarget/didChange` notifications. Needed
181
+ /// so that we can wait for these tasks to be handled in the poll index request.
182
+ private var fileOrBuildTargetChangedTasks : [ UUID : Task < Void , Never > ] = [ : ]
179
183
180
184
private let preparationUpToDateTracker = UpToDateTracker < BuildTargetIdentifier > ( )
181
185
@@ -225,7 +229,7 @@ package final actor SemanticIndexManager {
225
229
if inProgressPreparationTasks. values. contains ( where: { $0. purpose == . forEditorFunctionality } ) {
226
230
return . preparingFileForEditorFunctionality
227
231
}
228
- if !scheduleIndexingTasks . isEmpty {
232
+ if !buildGraphGenerationTasks . isEmpty {
229
233
return . schedulingIndexing
230
234
}
231
235
let preparationTasks = inProgressPreparationTasks. mapValues { inProgressTask in
@@ -270,46 +274,37 @@ package final actor SemanticIndexManager {
270
274
/// Regenerate the build graph (also resolving package dependencies) and then index all the source files known to the
271
275
/// build system that don't currently have a unit with a timestamp that matches the mtime of the file.
272
276
///
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
277
+ /// This is a costly operation since it iterates through all the unit files on the file
277
278
/// system but if existing unit files are not known to the index, we might re-index those files even if they are
278
279
/// up-to-date. Generally this should be set to `true` during the initial indexing (in which case we might be need to
279
280
/// 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 {
281
+ package func scheduleBuildGraphGenerationAndBackgroundIndexAllFiles( indexFilesWithUpToDateUnit: Bool ) async {
285
282
let taskId = UUID ( )
286
283
let generateBuildGraphTask = Task ( priority: . low) {
287
284
await withLoggingSubsystemAndScope ( subsystem: indexLoggingSubsystem, scope: " build-graph-generation " ) {
288
285
await hooks. buildGraphGenerationDidStart ? ( )
289
286
await self . buildSystemManager. waitForUpToDateBuildGraph ( )
290
- if ensureAllUnitsRegisteredInIndex {
291
- index. pollForUnitChangesAndWait ( )
292
- }
287
+ // Polling for unit changes is a costly operation since it iterates through all the unit files on the file
288
+ // system but if existing unit files are not known to the index, we might re-index those files even if they are
289
+ // up-to-date. This operation is worth the cost during initial indexing and during the manual re-index command.
290
+ index. pollForUnitChangesAndWait ( )
293
291
await hooks. buildGraphGenerationDidFinish ? ( )
294
292
// TODO: Ideally this would be a type like any Collection<DocumentURI> & Sendable but that doesn't work due to
295
293
// https://github.com/swiftlang/swift/issues/75602
296
294
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 (
295
+ await orLog ( " Getting files to index " ) {
296
+ try await self . buildSystemManager. buildableSourceFiles ( ) . sorted { $0. stringValue < $1. stringValue }
297
+ } ?? [ ]
298
+ await self . scheduleIndexing (
305
299
of: filesToIndex,
300
+ waitForBuildGraphGenerationTasks: false ,
306
301
indexFilesWithUpToDateUnit: indexFilesWithUpToDateUnit,
307
302
priority: . low
308
303
)
309
- scheduleIndexingTasks [ taskId] = nil
304
+ buildGraphGenerationTasks [ taskId] = nil
310
305
}
311
306
}
312
- scheduleIndexingTasks [ taskId] = generateBuildGraphTask
307
+ buildGraphGenerationTasks [ taskId] = generateBuildGraphTask
313
308
indexProgressStatusDidChange ( )
314
309
}
315
310
@@ -318,15 +313,11 @@ package final actor SemanticIndexManager {
318
313
package func scheduleReindex( ) async {
319
314
await indexStoreUpToDateTracker. markAllKnownOutOfDate ( )
320
315
await preparationUpToDateTracker. markAllKnownOutOfDate ( )
321
- await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles (
322
- filesToIndex: nil ,
323
- ensureAllUnitsRegisteredInIndex: false ,
324
- indexFilesWithUpToDateUnit: true
325
- )
316
+ await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles ( indexFilesWithUpToDateUnit: true )
326
317
}
327
318
328
319
private func waitForBuildGraphGenerationTasks( ) async {
329
- await scheduleIndexingTasks . values. concurrentForEach { await $0. value }
320
+ await buildGraphGenerationTasks . values. concurrentForEach { await $0. value }
330
321
}
331
322
332
323
/// Wait for all in-progress index tasks to finish.
@@ -336,6 +327,8 @@ package final actor SemanticIndexManager {
336
327
// can await the index tasks below.
337
328
await waitForBuildGraphGenerationTasks ( )
338
329
330
+ await fileOrBuildTargetChangedTasks. concurrentForEach { await $0. value. value }
331
+
339
332
await inProgressIndexTasks. concurrentForEach { ( _, inProgress) in
340
333
switch inProgress. state {
341
334
case . creatingIndexTask:
@@ -358,86 +351,102 @@ package final actor SemanticIndexManager {
358
351
logger. info (
359
352
" Waiting for up-to-date index for \( uris. map { $0. fileURL? . lastPathComponent ?? $0. stringValue } . joined ( separator: " , " ) ) "
360
353
)
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 ( )
364
354
365
355
// Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will
366
356
// - Wait for the existing index operations to finish if they have the same number of files.
367
357
// - 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
358
+ await self . scheduleIndexing (
359
+ of: uris,
360
+ waitForBuildGraphGenerationTasks: true ,
361
+ indexFilesWithUpToDateUnit: false ,
362
+ priority: nil
363
+ ) . value
369
364
index. pollForUnitChangesAndWait ( )
370
365
logger. debug ( " Done waiting for up-to-date index " )
371
366
}
372
367
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)
368
+ package func filesDidChange( _ events: [ FileEvent ] ) {
369
+ // We don't care about the order of file change events, so we can execute the changes in a task, allowing further
370
+ // file change handling to continue before we have scheduled the file re-indexing.
371
+ let uuid = UUID ( )
372
+ fileOrBuildTargetChangedTasks [ uuid] = Task {
373
+ defer { fileOrBuildTargetChangedTasks [ uuid] = nil }
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
+ }
411
388
412
- if let targets {
413
- var targetsAndDependencies = targets
414
- targetsAndDependencies += await buildSystemManager . targets ( dependingOn : Set ( targets ) )
415
- if !targetsAndDependencies . isEmpty {
389
+ let dependentTargets = await buildSystemManager . targets ( dependingOn : changedTargets )
390
+ outOfDateTargets . formUnion ( dependentTargets )
391
+ }
392
+ if !outOfDateTargets . isEmpty {
416
393
logger. info (
417
394
"""
418
395
Marking dependent targets as out-of-date: \
419
- \( String ( targetsAndDependencies . map ( \. uri. stringValue) . joined ( separator: " , " ) ) )
396
+ \( String ( outOfDateTargets . map ( \. uri. stringValue) . joined ( separator: " , " ) ) )
420
397
"""
421
398
)
422
- await preparationUpToDateTracker. markOutOfDate ( targetsAndDependencies )
399
+ await preparationUpToDateTracker. markOutOfDate ( outOfDateTargets )
423
400
}
424
- } else {
425
- await preparationUpToDateTracker. markAllKnownOutOfDate ( )
426
- }
427
401
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,
402
+ await scheduleIndexing (
403
+ of: changedFiles,
404
+ waitForBuildGraphGenerationTasks: true ,
435
405
indexFilesWithUpToDateUnit: false ,
436
406
priority: . low
437
407
)
438
408
}
439
409
}
440
410
411
+ package func buildTargetsChanged( _ changes: [ BuildTargetEvent ] ? ) async {
412
+ // We don't care about the order of target change events, so we can execute the changes in a task, allowing further
413
+ // target change handling to continue.
414
+ let uuid = UUID ( )
415
+ fileOrBuildTargetChangedTasks [ uuid] = Task {
416
+ defer { fileOrBuildTargetChangedTasks [ uuid] = nil }
417
+ let targets = changes? . map ( \. target)
418
+
419
+ if let targets {
420
+ var targetsAndDependencies = targets
421
+ targetsAndDependencies += await buildSystemManager. targets ( dependingOn: Set ( targets) )
422
+ if !targetsAndDependencies. isEmpty {
423
+ logger. info (
424
+ """
425
+ Marking dependent targets as out-of-date: \
426
+ \( String ( targetsAndDependencies. map ( \. uri. stringValue) . joined ( separator: " , " ) ) )
427
+ """
428
+ )
429
+ await preparationUpToDateTracker. markOutOfDate ( targetsAndDependencies)
430
+ }
431
+ } else {
432
+ await preparationUpToDateTracker. markAllKnownOutOfDate ( )
433
+ }
434
+
435
+ await orLog ( " Scheduling re-indexing of changed targets " ) {
436
+ var sourceFiles = try await self . buildSystemManager. sourceFiles ( includeNonBuildableFiles: false )
437
+ if let targets {
438
+ sourceFiles = sourceFiles. filter { !$0. value. targets. isDisjoint ( with: targets) }
439
+ }
440
+ await scheduleIndexing (
441
+ of: sourceFiles. keys,
442
+ waitForBuildGraphGenerationTasks: true ,
443
+ indexFilesWithUpToDateUnit: false ,
444
+ priority: . low
445
+ )
446
+ }
447
+ }
448
+ }
449
+
441
450
/// Returns the files that should be indexed to get up-to-date index information for the given files.
442
451
///
443
452
/// If `files` contains a header file, this will return a `FileToIndex` that re-indexes a main file which includes the
@@ -696,12 +705,23 @@ package final actor SemanticIndexManager {
696
705
/// known to be up-to-date based on `indexStoreUpToDateTracker` or the unit timestamp will not be re-indexed unless
697
706
/// `indexFilesWithUpToDateUnit` is `true`.
698
707
///
708
+ /// If `waitForBuildGraphGenerationTasks` is `true` and there are any tasks in progress that wait for an up-to-date
709
+ /// build graph, wait for those to finish. This is helpful so to avoid the following: We receive a build target change
710
+ /// notification before the initial background indexing has finished. If indexstore-db hasn't been initialized with
711
+ /// `pollForUnitChangesAndWait` yet, we might not know that the changed targets' files' index is actually up-to-date
712
+ /// and would thus schedule an unnecessary re-index of the file.
713
+ ///
699
714
/// The returned task finishes when all files are indexed.
715
+ @discardableResult
700
716
package func scheduleIndexing(
701
717
of files: some Collection < DocumentURI > & Sendable ,
718
+ waitForBuildGraphGenerationTasks: Bool ,
702
719
indexFilesWithUpToDateUnit: Bool ,
703
720
priority: TaskPriority ?
704
721
) async -> Task < Void , Never > {
722
+ if waitForBuildGraphGenerationTasks {
723
+ await self . waitForBuildGraphGenerationTasks ( )
724
+ }
705
725
// Perform a quick initial check to whether the files is up-to-date, in which case we don't need to schedule a
706
726
// prepare and index operation at all.
707
727
// We will check the up-to-date status again in `IndexTaskDescription.execute`. This ensures that if we schedule
0 commit comments