Skip to content

Commit 2e6cc33

Browse files
committed
Index targets top-down instead of bottom up
We originally started preparing and indexing the low-level targets in the hope that we can unblock the files of those targets as early as possible for indexing and thus get best indexing performance. Revisiting this design decision, we can actually get a ~2x speed up (SourceKit-LSP gets indexed in 2:45 instead of 5:49 minutes on my machine) in initial indexing performance by preparing indexing top-level targets first. This speed-up is for two reasons: - When preparing a top-level target, all of its dependencies are implicitly also prepared. That means that we don’t need to send any prepare requests to the build server for those targets, which is very advantages for SwiftPM projects, in which the constant cost in the order of 500ms (based on my memory) even for a null build. - The underlying build server is able to prepare dependencies of the top-level target in parallel instead of implicitly serializing them by preparing all targets in the package in dependency order.
1 parent 7437d92 commit 2e6cc33

13 files changed

Lines changed: 132 additions & 57 deletions

Contributor Documentation/BSP Extensions.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,29 @@ To do so, the build server should perform any work that is necessary to typechec
100100
The server communicates during the initialize handshake whether this method is supported or not by setting `prepareProvider: true` in `SourceKitInitializeBuildResponseData`.
101101

102102
- method: `buildTarget/prepare`
103-
- params: `PrepareParams`
104-
- result: `void`
103+
- params: `BuildTargetPrepareRequest`
104+
- result: `BuildTargetPrepareResponse`
105105

106106
```ts
107-
export interface PrepareParams {
107+
export interface BuildTargetPrepareRequest {
108108
/** A list of build targets to prepare. */
109109
targets: BuildTargetIdentifier[];
110110

111111
/** A unique identifier generated by the client to identify this request.
112112
* The server may include this id in triggered notifications or responses. **/
113113
originId?: OriginId;
114114
}
115+
116+
export interface BuildTargetPrepareResponse {
117+
/**
118+
* Targets that were implicitly prepared by the prepare request.
119+
*
120+
* For example, a target may be implicitly prepared if one if its dependents gets prepared.
121+
*
122+
* When omitted, this is the same as an empty array.
123+
*/
124+
implicitlyPreparedTargets?: BuildTargetIdentifier[]
125+
}
115126
```
116127

117128
## `buildTarget/sources`

Sources/BuildServerIntegration/BuildServerManager.swift

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,24 +1340,53 @@ package actor BuildServerManager: QueueBasedMessageHandler {
13401340
return (depths, dependents)
13411341
}
13421342

1343-
/// Sort the targets so that low-level targets occur before high-level targets.
1344-
///
1345-
/// This sorting is best effort but allows the indexer to prepare and index low-level targets first, which allows
1346-
/// index data to be available earlier.
1347-
package func topologicalSort(of targets: [BuildTargetIdentifier]) async throws -> [BuildTargetIdentifier] {
1343+
/// Sort the targets in the order they should be indexed in for best performance.
1344+
package func targetsSortedForIndexing(_ targets: [BuildTargetIdentifier]) async throws -> [BuildTargetIdentifier] {
13481345
guard let buildTargets = await orLog("Getting build targets for topological sort", { try await buildTargets() })
13491346
else {
13501347
return targets.sorted { $0.uri.stringValue < $1.uri.stringValue }
13511348
}
13521349

1353-
return targets.sorted { (lhs: BuildTargetIdentifier, rhs: BuildTargetIdentifier) -> Bool in
1354-
let lhsDepth = buildTargets[lhs]?.depth ?? 0
1355-
let rhsDepth = buildTargets[rhs]?.depth ?? 0
1350+
// Generate a preliminary work list of targets to index in which we prefer top-level targets over low-level targets
1351+
// and targets of the root package over targets in dependencies.
1352+
// We want to index targets in the root package first because those are likely the files that the user is interested
1353+
// in editing. We want to index top-level targets first, because preparing those likely implies preparation of the
1354+
// low-level targets.
1355+
var workList = targets.sorted { (lhs: BuildTargetIdentifier, rhs: BuildTargetIdentifier) -> Bool in
1356+
let lhsTarget = buildTargets[lhs]
1357+
let rhsTarget = buildTargets[rhs]
1358+
1359+
switch (lhsTarget?.target.tags.contains(.dependency), rhsTarget?.target.tags.contains(.dependency)) {
1360+
case (true, false): return false
1361+
case (false, true): return true
1362+
default: break
1363+
}
1364+
1365+
let lhsDepth = lhsTarget?.depth ?? 0
1366+
let rhsDepth = rhsTarget?.depth ?? 0
13561367
if lhsDepth != rhsDepth {
1357-
return lhsDepth > rhsDepth
1368+
return lhsDepth < rhsDepth
13581369
}
1370+
// Use the target's name as a tie-breaker
13591371
return lhs.uri.stringValue < rhs.uri.stringValue
13601372
}
1373+
1374+
// Now walk through the list of targets in the work list. For each target from the work list, index all of its
1375+
// transitive dependencies next. We do this because preparing a top-level target likely also prepared all of its
1376+
// dependencies, so we should be able to index all files in the target's dependencies without needing to perform any
1377+
// target preparation.
1378+
var sorted: [BuildTargetIdentifier] = []
1379+
while !workList.isEmpty {
1380+
let target = workList.removeFirst()
1381+
sorted.append(target)
1382+
1383+
let transitiveDependencies = transitiveClosure(of: [target]) { Set(buildTargets[$0]?.target.dependencies ?? []) }
1384+
let dependenciesInWorkList = workList.filter { transitiveDependencies.contains($0) }
1385+
sorted += dependenciesInWorkList
1386+
workList.removeAll(where: { dependenciesInWorkList.contains($0) })
1387+
}
1388+
1389+
return sorted
13611390
}
13621391

13631392
/// Returns the list of targets that might depend on the given target and that need to be re-prepared when a file in
@@ -1373,14 +1402,18 @@ package actor BuildServerManager: QueueBasedMessageHandler {
13731402
.sorted { $0.uri.stringValue < $1.uri.stringValue }
13741403
}
13751404

1376-
package func prepare(targets: Set<BuildTargetIdentifier>) async throws {
1377-
let _: VoidResponse? = try await buildServerAdapterAfterInitialized?.send(
1405+
package func prepare(targets: Set<BuildTargetIdentifier>) async throws -> BuildTargetPrepareResponse {
1406+
guard let buildServerAdapterAfterInitialized = try await buildServerAdapterAfterInitialized else {
1407+
throw ResponseError.unknown("No connection to build server")
1408+
}
1409+
let response = try await buildServerAdapterAfterInitialized.send(
13781410
BuildTargetPrepareRequest(targets: targets.sorted { $0.uri.stringValue < $1.uri.stringValue })
13791411
)
13801412
await orLog("Calling fileDependenciesUpdated") {
13811413
let filesInPreparedTargets = try await self.sourceFiles(in: targets).flatMap(\.sources).map(\.uri)
13821414
await filesDependenciesUpdatedDebouncer.scheduleCall(Set(filesInPreparedTargets))
13831415
}
1416+
return response
13841417
}
13851418

13861419
package func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {

Sources/BuildServerIntegration/BuiltInBuildServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ package protocol BuiltInBuildServer: AnyObject, Sendable {
5151

5252
/// Prepare the given targets for indexing and semantic functionality. This should build all swift modules of target
5353
/// dependencies.
54-
func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse
54+
func prepare(request: BuildTargetPrepareRequest) async throws -> BuildTargetPrepareResponse
5555

5656
/// Retrieve build settings for the given document.
5757
///

Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ package actor FixedCompilationDatabaseBuildServer: BuiltInBuildServer {
111111
}
112112
}
113113

114-
package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse {
114+
package func prepare(request: BuildTargetPrepareRequest) async throws -> BuildTargetPrepareResponse {
115115
throw ResponseError.methodNotFound(BuildTargetPrepareRequest.method)
116116
}
117117

Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
176176
}
177177
}
178178

179-
package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse {
179+
package func prepare(request: BuildTargetPrepareRequest) async throws -> BuildTargetPrepareResponse {
180180
throw ResponseError.methodNotFound(BuildTargetPrepareRequest.method)
181181
}
182182

Sources/BuildServerIntegration/LegacyBuildServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ actor LegacyBuildServer: MessageHandler, BuiltInBuildServer {
169169

170170
package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) {}
171171

172-
package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse {
172+
package func prepare(request: BuildTargetPrepareRequest) async throws -> BuildTargetPrepareResponse {
173173
throw ResponseError.methodNotFound(BuildTargetPrepareRequest.method)
174174
}
175175

Sources/BuildServerIntegration/SwiftPMBuildServer.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -714,12 +714,17 @@ package actor SwiftPMBuildServer: BuiltInBuildServer {
714714
return VoidResponse()
715715
}
716716

717-
package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse {
717+
package func prepare(request: BuildTargetPrepareRequest) async throws -> BuildTargetPrepareResponse {
718718
// TODO: Support preparation of multiple targets at once. (https://github.com/swiftlang/sourcekit-lsp/issues/1262)
719+
var implicitlyPreparedTargets: [BuildTargetIdentifier] = []
719720
for target in request.targets {
720-
await orLog("Preparing") { try await prepare(singleTarget: target) }
721+
await orLog("Preparing") {
722+
try await prepare(singleTarget: target)
723+
implicitlyPreparedTargets += transitiveClosure(of: [target], successors: { targetDependencies[$0] ?? [] })
724+
.filter { !request.targets.contains($0) }
725+
}
721726
}
722-
return VoidResponse()
727+
return BuildTargetPrepareResponse(implicitlyPreparedTargets: implicitlyPreparedTargets)
723728
}
724729

725730
private func prepare(singleTarget target: BuildTargetIdentifier) async throws {

Sources/SKTestSupport/CustomBuildServerTestProject.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ package protocol CustomBuildServer: MessageHandler {
9292
func textDocumentSourceKitOptionsRequest(
9393
_ request: TextDocumentSourceKitOptionsRequest
9494
) async throws -> TextDocumentSourceKitOptionsResponse?
95-
func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse
95+
func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> BuildTargetPrepareResponse
9696
func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse
9797
nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws
9898
func workspaceWaitForBuildSystemUpdatesRequest(
@@ -243,8 +243,8 @@ package extension CustomBuildServer {
243243
])
244244
}
245245

246-
func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse {
247-
return VoidResponse()
246+
func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> BuildTargetPrepareResponse {
247+
return BuildTargetPrepareResponse()
248248
}
249249

250250
func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse {

Sources/SemanticIndex/PreparationTaskDescription.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,19 @@ package struct PreparationTaskDescription: IndexTaskDescription {
110110
)
111111
signposter.endInterval("Preparing", state)
112112
}
113+
let prepareResponse: BuildTargetPrepareResponse?
113114
do {
114-
try await buildServerManager.prepare(targets: Set(targetsToPrepare))
115+
prepareResponse = try await buildServerManager.prepare(targets: Set(targetsToPrepare))
115116
} catch {
116117
logger.error("Preparation failed: \(error.forLogging)")
118+
prepareResponse = nil
117119
}
118120
await hooks.preparationTaskDidFinish?(self)
119121
if !Task.isCancelled {
120-
await preparationUpToDateTracker.markUpToDate(targetsToPrepare, updateOperationStartDate: startDate)
122+
await preparationUpToDateTracker.markUpToDate(
123+
targetsToPrepare + (prepareResponse?.implicitlyPreparedTargets ?? []),
124+
updateOperationStartDate: startDate
125+
)
121126
}
122127
}
123128
}

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,9 @@ package final actor SemanticIndexManager {
909909
// The targets sorted in reverse topological order, low-level targets before high-level targets. If topological
910910
// sorting fails, sorted in another deterministic way where the actual order doesn't matter.
911911
var sortedTargets: [BuildTargetIdentifier] =
912-
await orLog("Sorting targets") { try await buildServerManager.topologicalSort(of: Array(filesByTarget.keys)) }
912+
await orLog("Sorting targets") {
913+
try await buildServerManager.targetsSortedForIndexing(Array(filesByTarget.keys))
914+
}
913915
?? Array(filesByTarget.keys).sorted { $0.uri.stringValue < $1.uri.stringValue }
914916

915917
if Set(sortedTargets) != Set(filesByTarget.keys) {

0 commit comments

Comments
 (0)