@@ -16,15 +16,15 @@ import LanguageServerProtocol
16
16
import SKCore
17
17
18
18
/// Describes the state of indexing for a single source file
19
- private enum IndexStatus {
19
+ private enum IndexStatus < T > {
20
20
/// The index is up-to-date.
21
21
case upToDate
22
22
/// The file or target is not up to date and we have scheduled a task to update the index store for the file / prepare
23
23
/// the target it but that index operation hasn't been started yet.
24
- case scheduled( Task < Void , Never > )
24
+ case scheduled( T )
25
25
/// We are currently actively updating the index store for the file / preparing the target, ie. we are running a
26
26
/// subprocess that updates the index store / prepares a target.
27
- case executing( Task < Void , Never > )
27
+ case executing( T )
28
28
29
29
var description : String {
30
30
switch self {
@@ -54,12 +54,23 @@ public final actor SemanticIndexManager {
54
54
/// The preparation status of the targets that the `SemanticIndexManager` has started preparation for.
55
55
///
56
56
/// Targets will be removed from this dictionary when they are no longer known to be up-to-date.
57
- private var preparationStatus : [ ConfiguredTarget : IndexStatus ] = [ : ]
57
+ ///
58
+ /// The associated values of the `IndexStatus` are:
59
+ /// - A UUID to track the task. This is used to ensure that status updates from this task don't update
60
+ /// `preparationStatus` for targets that are tracked by a different task.
61
+ /// - The list of targets that are being prepared in a joint preparation operation
62
+ /// - The task that prepares the target
63
+ private var preparationStatus : [ ConfiguredTarget : IndexStatus < ( UUID , [ ConfiguredTarget ] , Task < Void , Never > ) > ] = [ : ]
58
64
59
65
/// The index status of the source files that the `SemanticIndexManager` knows about.
60
66
///
61
67
/// Files will be removed from this dictionary if their index is no longer up-to-date.
62
- private var indexStatus : [ DocumentURI : IndexStatus ] = [ : ]
68
+ ///
69
+ /// The associated values of the `IndexStatus` are:
70
+ /// - A UUID to track the task. This is used to ensure that status updates from this task don't update
71
+ /// `preparationStatus` for targets that are tracked by a different task.
72
+ /// - The task that prepares the target
73
+ private var indexStatus : [ DocumentURI : IndexStatus < ( UUID , Task < Void , Never > ) > ] = [ : ]
63
74
64
75
/// The `TaskScheduler` that manages the scheduling of index tasks. This is shared among all `SemanticIndexManager`s
65
76
/// in the process, to ensure that we don't schedule more index operations than processor cores from multiple
@@ -122,7 +133,7 @@ public final actor SemanticIndexManager {
122
133
///
123
134
/// Indexing is being performed with a low priority.
124
135
private func scheduleBackgroundIndex( files: some Collection < DocumentURI > ) async {
125
- await self . index ( files : files, priority: . low)
136
+ _ = await self . scheduleIndexing ( of : files, priority: . low)
126
137
}
127
138
128
139
/// Regenerate the build graph (also resolving package dependencies) and then index all the source files known to the
@@ -156,7 +167,7 @@ public final actor SemanticIndexManager {
156
167
await withTaskGroup ( of: Void . self) { taskGroup in
157
168
for (_, status) in indexStatus {
158
169
switch status {
159
- case . scheduled( let task) , . executing( let task) :
170
+ case . scheduled( ( _ , let task) ) , . executing( ( _ , let task) ) :
160
171
taskGroup. addTask {
161
172
await task. value
162
173
}
@@ -185,7 +196,7 @@ public final actor SemanticIndexManager {
185
196
// Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will
186
197
// - Wait for the existing index operations to finish if they have the same number of files.
187
198
// - Reschedule the background index task in favor of an index task with fewer source files.
188
- await self . index ( files : uris, priority: nil ) . value
199
+ await self . scheduleIndexing ( of : uris, priority: nil ) . value
189
200
index. pollForUnitChangesAndWait ( )
190
201
logger. debug ( " Done waiting for up-to-date index " )
191
202
}
@@ -233,67 +244,101 @@ public final actor SemanticIndexManager {
233
244
return filesToReIndex
234
245
}
235
246
247
+ /// Schedule preparation of the target that contains the given URI, building all modules that the file depends on.
248
+ ///
249
+ /// This is intended to be called when the user is interacting with the document at the given URI.
250
+ public func schedulePreparation( of uri: DocumentURI , priority: TaskPriority ? = nil ) async {
251
+ Task ( priority: priority) {
252
+ await withLoggingScope ( " preparation " ) {
253
+ guard let target = await buildSystemManager. canonicalConfiguredTarget ( for: uri) else {
254
+ return
255
+ }
256
+ await self . prepare ( targets: [ target] , priority: priority)
257
+ }
258
+ }
259
+ }
260
+
236
261
// MARK: - Helper functions
237
262
238
263
/// Prepare the given targets for indexing
239
264
private func prepare( targets: [ ConfiguredTarget ] , priority: TaskPriority ? ) async {
240
- let targetsToPrepare = targets. filter {
241
- if case . upToDate = preparationStatus [ $0] {
242
- return false
265
+ var targetsToPrepare : [ ConfiguredTarget ] = [ ]
266
+ var preparationTasksToAwait : [ Task < Void , Never > ] = [ ]
267
+ for target in targets {
268
+ switch preparationStatus [ target] {
269
+ case . upToDate:
270
+ break
271
+ case . scheduled( ( _, let configuredTargets, let task) ) , . executing( ( _, let configuredTargets, let task) ) :
272
+ // If we already have a task scheduled that prepares fewer targets, await that instead of overriding the
273
+ // target's preparation status with a longer-running task. The key benefit here is that when we get many
274
+ // preparation requests for the same target (eg. one for every text document request sent to a file), we don't
275
+ // re-create new `PreparationTaskDescription`s for every preparation request. Instead, all the preparation
276
+ // requests await the same task. At the same time, if we have a multi-file preparation request and then get a
277
+ // single-file preparation request, we will override the preparation of that target with the single-file
278
+ // preparation task, ensuring that the task gets prepared as quickly as possible.
279
+ if configuredTargets. count <= targets. count {
280
+ preparationTasksToAwait. append ( task)
281
+ }
282
+ fallthrough
283
+ case nil :
284
+ targetsToPrepare. append ( target)
243
285
}
244
- return true
245
286
}
287
+
246
288
let taskDescription = AnyIndexTaskDescription (
247
289
PreparationTaskDescription (
248
290
targetsToPrepare: targetsToPrepare,
249
291
buildSystemManager: self . buildSystemManager
250
292
)
251
293
)
252
- let preparationTask = await self . indexTaskScheduler. schedule ( priority: priority, taskDescription) { newState in
253
- switch newState {
254
- case . executing:
255
- for target in targetsToPrepare {
256
- if case . scheduled( let task) = self . preparationStatus [ target] {
257
- self . preparationStatus [ target] = . executing( task)
258
- } else {
259
- logger. fault (
260
- """
261
- Preparation status of \( target. forLogging) is in an unexpected state \
262
- ' \( self . preparationStatus [ target] ? . description ?? " <nil> " , privacy: . public) ' when preparation task \
263
- started executing
264
- """
265
- )
294
+ if !targetsToPrepare. isEmpty {
295
+ // A UUID that is used to identify the task. This ensures that status updates from this task don't update
296
+ // `preparationStatus` for targets that are tracked by a different task, eg. because this task is a multi-target
297
+ // preparation task and the target's status is now tracked by a single-file preparation task.
298
+ let taskID = UUID ( )
299
+ let preparationTask = await self . indexTaskScheduler. schedule ( priority: priority, taskDescription) { newState in
300
+ switch newState {
301
+ case . executing:
302
+ for target in targetsToPrepare {
303
+ if case . scheduled( ( taskID, let targets, let task) ) = self . preparationStatus [ target] {
304
+ self . preparationStatus [ target] = . executing( ( taskID, targets, task) )
305
+ }
266
306
}
267
- }
268
- case . cancelledToBeRescheduled:
269
- for target in targetsToPrepare {
270
- if case . executing( let task) = self . preparationStatus [ target] {
271
- self . preparationStatus [ target] = . scheduled( task)
272
- } else {
273
- logger. fault (
274
- """
275
- Preparation status of \( target. forLogging) is in an unexpected state \
276
- ' \( self . preparationStatus [ target] ? . description ?? " <nil> " , privacy: . public) ' when preparation task \
277
- is cancelled to be rescheduled.
278
- """
279
- )
307
+ case . cancelledToBeRescheduled:
308
+ for target in targetsToPrepare {
309
+ if case . executing( ( taskID, let targets, let task) ) = self . preparationStatus [ target] {
310
+ self . preparationStatus [ target] = . scheduled( ( taskID, targets, task) )
311
+ }
280
312
}
313
+ case . finished:
314
+ for target in targetsToPrepare {
315
+ switch self . preparationStatus [ target] {
316
+ case . executing( ( taskID, _, _) ) :
317
+ self . preparationStatus [ target] = . upToDate
318
+ default :
319
+ break
320
+ }
321
+ }
322
+ self . indexTaskDidFinish ( )
281
323
}
282
- case . finished:
283
- for target in targetsToPrepare {
284
- self . preparationStatus [ target] = . upToDate
285
- }
286
- self . indexTaskDidFinish ( )
287
324
}
325
+ for target in targetsToPrepare {
326
+ preparationStatus [ target] = . scheduled( ( taskID, targetsToPrepare, preparationTask) )
327
+ }
328
+ preparationTasksToAwait. append ( preparationTask)
288
329
}
289
- for target in targetsToPrepare {
290
- preparationStatus [ target] = . scheduled( preparationTask)
330
+ await withTaskGroup ( of: Void . self) { taskGroup in
331
+ for task in preparationTasksToAwait {
332
+ taskGroup. addTask {
333
+ await task. value
334
+ }
335
+ }
336
+ await taskGroup. waitForAll ( )
291
337
}
292
- await preparationTask. value
293
338
}
294
339
295
340
/// Update the index store for the given files, assuming that their targets have already been prepared.
296
- private func updateIndexStore( for files: [ FileToIndex ] , priority: TaskPriority ? ) async {
341
+ private func updateIndexStore( for files: [ FileToIndex ] , taskID : UUID , priority: TaskPriority ? ) async {
297
342
let taskDescription = AnyIndexTaskDescription (
298
343
UpdateIndexStoreTaskDescription (
299
344
filesToIndex: Set ( files) ,
@@ -305,47 +350,38 @@ public final actor SemanticIndexManager {
305
350
switch newState {
306
351
case . executing:
307
352
for file in files {
308
- if case . scheduled( let task) = self . indexStatus [ file. uri] {
309
- self . indexStatus [ file. uri] = . executing( task)
310
- } else {
311
- logger. fault (
312
- """
313
- Index status of \( file. uri) is in an unexpected state \
314
- ' \( self . indexStatus [ file. uri] ? . description ?? " <nil> " , privacy: . public) ' when update index store task \
315
- started executing
316
- """
317
- )
353
+ if case . scheduled( ( taskID, let task) ) = self . indexStatus [ file. uri] {
354
+ self . indexStatus [ file. uri] = . executing( ( taskID, task) )
318
355
}
319
356
}
320
357
case . cancelledToBeRescheduled:
321
358
for file in files {
322
- if case . executing( let task) = self . indexStatus [ file. uri] {
323
- self . indexStatus [ file. uri] = . scheduled( task)
324
- } else {
325
- logger. fault (
326
- """
327
- Index status of \( file. uri) is in an unexpected state \
328
- ' \( self . indexStatus [ file. uri] ? . description ?? " <nil> " , privacy: . public) ' when update index store task \
329
- is cancelled to be rescheduled.
330
- """
331
- )
359
+ if case . executing( ( taskID, let task) ) = self . indexStatus [ file. uri] {
360
+ self . indexStatus [ file. uri] = . scheduled( ( taskID, task) )
332
361
}
333
362
}
334
363
case . finished:
335
364
for file in files {
336
- self . indexStatus [ file. uri] = . upToDate
365
+ switch self . indexStatus [ file. uri] {
366
+ case . executing( ( taskID, _) ) :
367
+ self . indexStatus [ file. uri] = . upToDate
368
+ default :
369
+ break
370
+ }
337
371
}
338
372
self . indexTaskDidFinish ( )
339
373
}
340
374
}
341
375
await updateIndexStoreTask. value
342
376
}
343
377
344
- /// Index the given set of files at the given priority.
378
+ /// Index the given set of files at the given priority, preparing their targets beforehand, if needed .
345
379
///
346
380
/// The returned task finishes when all files are indexed.
347
- @discardableResult
348
- private func index( files: some Collection < DocumentURI > , priority: TaskPriority ? ) async -> Task < Void , Never > {
381
+ private func scheduleIndexing(
382
+ of files: some Collection < DocumentURI > ,
383
+ priority: TaskPriority ?
384
+ ) async -> Task < Void , Never > {
349
385
let outOfDateFiles = await filesToIndex ( toCover: files) . filter {
350
386
if case . upToDate = indexStatus [ $0. uri] {
351
387
return false
@@ -389,6 +425,7 @@ public final actor SemanticIndexManager {
389
425
// processor count, so we can get parallelism during preparation.
390
426
// https://github.com/apple/sourcekit-lsp/issues/1262
391
427
for targetsBatch in sortedTargets. partition ( intoBatchesOfSize: 1 ) {
428
+ let taskID = UUID ( )
392
429
let indexTask = Task ( priority: priority) {
393
430
// First prepare the targets.
394
431
await prepare ( targets: targetsBatch, priority: priority)
@@ -401,7 +438,7 @@ public final actor SemanticIndexManager {
401
438
// https://github.com/apple/sourcekit-lsp/issues/1268
402
439
for fileBatch in filesByTarget [ target] !. partition ( intoBatchesOfSize: 1 ) {
403
440
taskGroup. addTask {
404
- await self . updateIndexStore ( for: fileBatch, priority: priority)
441
+ await self . updateIndexStore ( for: fileBatch, taskID : taskID , priority: priority)
405
442
}
406
443
}
407
444
}
@@ -416,7 +453,7 @@ public final actor SemanticIndexManager {
416
453
// setting it to `.scheduled` because we don't have an `await` call between the creation of `indexTask` and
417
454
// this loop, so we still have exclusive access to the `SemanticIndexManager` actor and hence `updateIndexStore`
418
455
// can't execute until we have set all index statuses to `.scheduled`.
419
- indexStatus [ file. uri] = . scheduled( indexTask)
456
+ indexStatus [ file. uri] = . scheduled( ( taskID , indexTask) )
420
457
}
421
458
indexTasksWereScheduled ( filesToIndex. count)
422
459
}
0 commit comments