Skip to content

Commit 950a74a

Browse files
committed
Fix a negation issue that prevented parallel indexing
1 parent 927056b commit 950a74a

File tree

5 files changed

+274
-117
lines changed

5 files changed

+274
-117
lines changed

Sources/SemanticIndex/PreparationTaskDescription.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ public struct PreparationTaskDescription: IndexTaskDescription {
7878
subsystem: "org.swift.sourcekit-lsp.indexing",
7979
scope: "preparation-\(id % 100)"
8080
) {
81-
await testHooks.preparationTaskDidStart?(self)
8281
let targetsToPrepare = await targetsToPrepare.asyncFilter {
8382
await !preparationUpToDateStatus.isUpToDate($0)
8483
}.sorted(by: {
@@ -87,6 +86,7 @@ public struct PreparationTaskDescription: IndexTaskDescription {
8786
if targetsToPrepare.isEmpty {
8887
return
8988
}
89+
await testHooks.preparationTaskDidStart?(self)
9090

9191
let targetsToPrepareDescription =
9292
targetsToPrepare

Sources/SemanticIndex/TestHooks.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,20 @@ public struct IndexTestHooks: Sendable {
1616

1717
public var preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)?
1818

19+
public var updateIndexStoreTaskDidStart: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)?
20+
1921
/// A callback that is called when an index task finishes.
2022
public var updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)?
2123

2224
public init(
2325
preparationTaskDidStart: (@Sendable (PreparationTaskDescription) async -> Void)? = nil,
2426
preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)? = nil,
27+
updateIndexStoreTaskDidStart: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? = nil,
2528
updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? = nil
2629
) {
2730
self.preparationTaskDidStart = preparationTaskDidStart
2831
self.preparationTaskDidFinish = preparationTaskDidFinish
32+
self.updateIndexStoreTaskDidStart = updateIndexStoreTaskDidStart
2933
self.updateIndexStoreTaskDidFinish = updateIndexStoreTaskDidFinish
3034
}
3135
}

Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import class TSCBasic.Process
2222

2323
private nonisolated(unsafe) var updateIndexStoreIDForLogging = AtomicUInt32(initialValue: 1)
2424

25-
enum FileToIndex: CustomLogStringConvertible {
25+
public enum FileToIndex: CustomLogStringConvertible {
2626
/// A non-header file
2727
case indexableFile(DocumentURI)
2828

@@ -33,7 +33,7 @@ enum FileToIndex: CustomLogStringConvertible {
3333
///
3434
/// This file might be a header file that doesn't have build settings associated with it. For the actual compiler
3535
/// invocation that updates the index store, the `mainFile` should be used.
36-
var sourceFile: DocumentURI {
36+
public var sourceFile: DocumentURI {
3737
switch self {
3838
case .indexableFile(let uri): return uri
3939
case .headerFile(header: let header, mainFile: _): return header
@@ -51,7 +51,7 @@ enum FileToIndex: CustomLogStringConvertible {
5151
}
5252
}
5353

54-
var description: String {
54+
public var description: String {
5555
switch self {
5656
case .indexableFile(let uri):
5757
return uri.description
@@ -60,7 +60,7 @@ enum FileToIndex: CustomLogStringConvertible {
6060
}
6161
}
6262

63-
var redactedDescription: String {
63+
public var redactedDescription: String {
6464
switch self {
6565
case .indexableFile(let uri):
6666
return uri.redactedDescription
@@ -71,9 +71,9 @@ enum FileToIndex: CustomLogStringConvertible {
7171
}
7272

7373
/// A file to index and the target in which the file should be indexed.
74-
struct FileAndTarget {
75-
let file: FileToIndex
76-
let target: ConfiguredTarget
74+
public struct FileAndTarget: Sendable {
75+
public let file: FileToIndex
76+
public let target: ConfiguredTarget
7777
}
7878

7979
/// Describes a task to index a set of source files.
@@ -84,7 +84,7 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
8484
public let id = updateIndexStoreIDForLogging.fetchAndIncrement()
8585

8686
/// The files that should be indexed.
87-
private let filesToIndex: [FileAndTarget]
87+
public let filesToIndex: [FileAndTarget]
8888

8989
/// The build system manager that is used to get the toolchain and build settings for the files to index.
9090
private let buildSystemManager: BuildSystemManager
@@ -140,6 +140,8 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
140140
) {
141141
let startDate = Date()
142142

143+
await testHooks.updateIndexStoreTaskDidStart?(self)
144+
143145
let filesToIndexDescription = filesToIndex.map {
144146
$0.file.sourceFile.fileURL?.lastPathComponent ?? $0.file.sourceFile.stringValue
145147
}
@@ -166,9 +168,7 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
166168
) -> [TaskDependencyAction<UpdateIndexStoreTaskDescription>] {
167169
let selfMainFiles = Set(filesToIndex.map(\.file.mainFile))
168170
return currentlyExecutingTasks.compactMap { (other) -> TaskDependencyAction<UpdateIndexStoreTaskDescription>? in
169-
guard
170-
!other.filesToIndex.lazy.map(\.file.mainFile).contains(where: { selfMainFiles.contains($0) })
171-
else {
171+
if !other.filesToIndex.lazy.map(\.file.mainFile).contains(where: { selfMainFiles.contains($0) }) {
172172
// Disjoint sets of files can be indexed concurrently.
173173
return nil
174174
}

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 45 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -22,109 +22,6 @@ fileprivate let backgroundIndexingOptions = SourceKitLSPServer.Options(
2222
indexOptions: IndexOptions(enableBackgroundIndexing: true)
2323
)
2424

25-
fileprivate struct ExpectedPreparation {
26-
let targetID: String
27-
let runDestinationID: String
28-
29-
/// A closure that will be executed when a preparation task starts.
30-
/// This allows the artificial delay of a preparation task to force two preparation task to race.
31-
let didStart: (() -> Void)?
32-
33-
/// A closure that will be executed when a preparation task finishes.
34-
/// This allows the artificial delay of a preparation task to force two preparation task to race.
35-
let didFinish: (() -> Void)?
36-
37-
internal init(
38-
targetID: String,
39-
runDestinationID: String,
40-
didStart: (() -> Void)? = nil,
41-
didFinish: (() -> Void)? = nil
42-
) {
43-
self.targetID = targetID
44-
self.runDestinationID = runDestinationID
45-
self.didStart = didStart
46-
self.didFinish = didFinish
47-
}
48-
49-
var configuredTarget: ConfiguredTarget {
50-
return ConfiguredTarget(targetID: targetID, runDestinationID: runDestinationID)
51-
}
52-
}
53-
54-
fileprivate actor ExpectedPreparationTracker {
55-
/// The targets we expect to be prepared. For targets within the same set, we don't care about the exact order.
56-
private var expectedPreparations: [[ExpectedPreparation]]
57-
58-
/// Implicitly-unwrapped optional so we can reference `self` when creating `IndexTestHooks`.
59-
/// `nonisolated(unsafe)` is fine because this is not modified after `testHooks` is created.
60-
nonisolated(unsafe) var testHooks: IndexTestHooks!
61-
62-
init(expectedPreparations: [[ExpectedPreparation]]) {
63-
self.expectedPreparations = expectedPreparations
64-
self.testHooks = IndexTestHooks(
65-
preparationTaskDidStart: { [weak self] in
66-
await self?.preparationTaskDidStart(taskDescription: $0)
67-
},
68-
preparationTaskDidFinish: { [weak self] in
69-
await self?.preparationTaskDidFinish(taskDescription: $0)
70-
}
71-
)
72-
}
73-
74-
func preparationTaskDidStart(taskDescription: PreparationTaskDescription) -> Void {
75-
if Task.isCancelled {
76-
return
77-
}
78-
guard let expectedTargetsToPrepare = expectedPreparations.first else {
79-
return
80-
}
81-
for expectedPreparation in expectedTargetsToPrepare {
82-
if taskDescription.targetsToPrepare.contains(expectedPreparation.configuredTarget) {
83-
expectedPreparation.didStart?()
84-
}
85-
}
86-
}
87-
88-
func preparationTaskDidFinish(taskDescription: PreparationTaskDescription) -> Void {
89-
if Task.isCancelled {
90-
return
91-
}
92-
guard let expectedTargetsToPrepare = expectedPreparations.first else {
93-
XCTFail("Didn't expect a preparation but received \(taskDescription.targetsToPrepare)")
94-
return
95-
}
96-
guard Set(taskDescription.targetsToPrepare).isSubset(of: expectedTargetsToPrepare.map(\.configuredTarget)) else {
97-
XCTFail("Received unexpected preparation of \(taskDescription.targetsToPrepare)")
98-
return
99-
}
100-
var remainingExpectedTargetsToPrepare: [ExpectedPreparation] = []
101-
for expectedPreparation in expectedTargetsToPrepare {
102-
if taskDescription.targetsToPrepare.contains(expectedPreparation.configuredTarget) {
103-
expectedPreparation.didFinish?()
104-
} else {
105-
remainingExpectedTargetsToPrepare.append(expectedPreparation)
106-
}
107-
}
108-
if remainingExpectedTargetsToPrepare.isEmpty {
109-
expectedPreparations.remove(at: 0)
110-
} else {
111-
expectedPreparations[0] = remainingExpectedTargetsToPrepare
112-
}
113-
}
114-
115-
nonisolated func keepAlive() {
116-
withExtendedLifetime(self) { _ in }
117-
}
118-
119-
deinit {
120-
let expectedPreparations = self.expectedPreparations
121-
XCTAssert(
122-
expectedPreparations.isEmpty,
123-
"ExpectedPreparationTracker destroyed with unfulfilled expected preparations: \(expectedPreparations)."
124-
)
125-
}
126-
}
127-
12825
final class BackgroundIndexingTests: XCTestCase {
12926
func testBackgroundIndexingOfSingleFile() async throws {
13027
let project = try await SwiftPMTestProject(
@@ -614,7 +511,7 @@ final class BackgroundIndexingTests: XCTestCase {
614511
func testPrepareTargetAfterEditToDependency() async throws {
615512
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
616513
var serverOptions = backgroundIndexingOptions
617-
let expectedPreparationTracker = ExpectedPreparationTracker(expectedPreparations: [
514+
let expectedPreparationTracker = ExpectedIndexTaskTracker(expectedPreparations: [
618515
[
619516
ExpectedPreparation(targetID: "LibA", runDestinationID: "dummy"),
620517
ExpectedPreparation(targetID: "LibB", runDestinationID: "dummy"),
@@ -706,7 +603,7 @@ final class BackgroundIndexingTests: XCTestCase {
706603

707604
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
708605
var serverOptions = backgroundIndexingOptions
709-
let expectedPreparationTracker = ExpectedPreparationTracker(expectedPreparations: [
606+
let expectedPreparationTracker = ExpectedIndexTaskTracker(expectedPreparations: [
710607
// Preparation of targets during the initial of the target
711608
[
712609
ExpectedPreparation(targetID: "LibA", runDestinationID: "dummy"),
@@ -799,4 +696,47 @@ final class BackgroundIndexingTests: XCTestCase {
799696
"\(indexFileNotification.message) does not have the expected prefix"
800697
)
801698
}
699+
700+
func testPreparationHappensInParallel() async throws {
701+
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
702+
703+
let fileAIndexingStarted = self.expectation(description: "FileA indexing started")
704+
let fileBIndexingStarted = self.expectation(description: "FileB indexing started")
705+
706+
var serverOptions = backgroundIndexingOptions
707+
let expectedIndexTaskTracker = ExpectedIndexTaskTracker(
708+
expectedIndexStoreUpdates: [
709+
[
710+
ExpectedIndexStoreUpdate(
711+
sourceFileName: "FileA.swift",
712+
didStart: {
713+
fileAIndexingStarted.fulfill()
714+
},
715+
didFinish: {
716+
self.wait(for: [fileBIndexingStarted], timeout: defaultTimeout)
717+
}
718+
),
719+
ExpectedIndexStoreUpdate(
720+
sourceFileName: "FileB.swift",
721+
didStart: {
722+
fileBIndexingStarted.fulfill()
723+
},
724+
didFinish: {
725+
self.wait(for: [fileAIndexingStarted], timeout: defaultTimeout)
726+
}
727+
),
728+
]
729+
]
730+
)
731+
serverOptions.indexTestHooks = expectedIndexTaskTracker.testHooks
732+
733+
_ = try await SwiftPMTestProject(
734+
files: [
735+
"FileA.swift": "",
736+
"FileB.swift": "",
737+
],
738+
serverOptions: serverOptions,
739+
cleanUp: { expectedIndexTaskTracker.keepAlive() }
740+
)
741+
}
802742
}

0 commit comments

Comments
 (0)