Skip to content

Commit 5206343

Browse files
committed
Implement initial background indexing of a project
Implements an initial background index when the project is opened. The following will be implemented in follow-up PRs: - Resolving package dependencies - Preparing dependent modules - Watching for file updates
1 parent 1a64da0 commit 5206343

15 files changed

+759
-35
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
default.profraw
33
Package.resolved
44
/.build
5+
/.index-build
56
/Packages
67
/*.xcodeproj
78
/*.sublime-project

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ let package = Package(
173173
.target(
174174
name: "SemanticIndex",
175175
dependencies: [
176+
"CAtomics",
177+
"LanguageServerProtocol",
176178
"LSPLogging",
177179
"SKCore",
178180
.product(name: "IndexStoreDB", package: "indexstore-db"),

Sources/LSPLogging/Logging.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
/// Which log level to use (from https://developer.apple.com/wwdc20/10168?time=604)
1414
/// - Debug: Useful only during debugging (only logged during debugging)
1515
/// - Info: Helpful but not essential for troubleshooting (not persisted, logged to memory)
16-
/// - Notice/log (Default): Essential for troubleshooting
16+
/// - Notice/log/default: Essential for troubleshooting
1717
/// - Error: Error seen during execution
1818
/// - Used eg. if the user sends an erroneous request or if a request fails
1919
/// - Fault: Bug in program

Sources/LSPLogging/NonDarwinLogging.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ public struct NonDarwinLogger: Sendable {
330330
log(level: .info, message)
331331
}
332332

333-
/// Log a message at the `log` level.
333+
/// Log a message at the `default` level.
334334
public func log(_ message: NonDarwinLogMessage) {
335335
log(level: .default, message)
336336
}

Sources/SKCore/BuildSystemManager.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ extension BuildSystemManager {
159159
/// references to that C file in the build settings by the header file.
160160
public func buildSettingsInferredFromMainFile(
161161
for document: DocumentURI,
162-
language: Language
162+
language: Language,
163+
logBuildSettings: Bool = true
163164
) async -> FileBuildSettings? {
164165
let mainFile = await mainFile(for: document, language: language)
165166
guard var settings = await buildSettings(for: mainFile, language: language) else {
@@ -170,7 +171,9 @@ extension BuildSystemManager {
170171
// to reference `document` instead of `mainFile`.
171172
settings = settings.patching(newFile: document.pseudoPath, originalFile: mainFile.pseudoPath)
172173
}
173-
await BuildSettingsLogger.shared.log(settings: settings, for: document)
174+
if logBuildSettings {
175+
await BuildSettingsLogger.shared.log(settings: settings, for: document)
176+
}
174177
return settings
175178
}
176179

@@ -349,16 +352,24 @@ extension BuildSystemManager {
349352
// MARK: - Build settings logger
350353

351354
/// Shared logger that only logs build settings for a file once unless they change
352-
fileprivate actor BuildSettingsLogger {
353-
static let shared = BuildSettingsLogger()
355+
public actor BuildSettingsLogger {
356+
public static let shared = BuildSettingsLogger()
354357

355358
private var loggedSettings: [DocumentURI: FileBuildSettings] = [:]
356359

357-
func log(settings: FileBuildSettings, for uri: DocumentURI) {
360+
public func log(level: LogLevel = .default, settings: FileBuildSettings, for uri: DocumentURI) {
358361
guard loggedSettings[uri] != settings else {
359362
return
360363
}
361364
loggedSettings[uri] = settings
365+
Self.log(level: level, settings: settings, for: uri)
366+
}
367+
368+
/// Log the given build settings.
369+
///
370+
/// In contrast to the instance method `log`, this will always log the build settings. The instance method only logs
371+
/// the build settings if they have changed.
372+
public static func log(level: LogLevel = .default, settings: FileBuildSettings, for uri: DocumentURI) {
362373
let log = """
363374
Compiler Arguments:
364375
\(settings.compilerArguments.joined(separator: "\n"))
@@ -370,6 +381,7 @@ fileprivate actor BuildSettingsLogger {
370381
let chunks = splitLongMultilineMessage(message: log)
371382
for (index, chunk) in chunks.enumerated() {
372383
logger.log(
384+
level: level,
373385
"""
374386
Build settings for \(uri.forLogging) (\(index + 1)/\(chunks.count))
375387
\(chunk)

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public class SwiftPMTestProject: MultiFileTestProject {
4343
build: Bool = false,
4444
allowBuildFailure: Bool = false,
4545
serverOptions: SourceKitLSPServer.Options = .testDefault,
46+
pollIndex: Bool = true,
4647
usePullDiagnostics: Bool = true,
4748
testName: String = #function
4849
) async throws {
@@ -77,8 +78,10 @@ public class SwiftPMTestProject: MultiFileTestProject {
7778
try await Self.build(at: self.scratchDirectory)
7879
}
7980
}
80-
// Wait for the indexstore-db to finish indexing
81-
_ = try await testClient.send(PollIndexRequest())
81+
if pollIndex {
82+
// Wait for the indexstore-db to finish indexing
83+
_ = try await testClient.send(PollIndexRequest())
84+
}
8285
}
8386

8487
/// Build a SwiftPM package package manifest is located in the directory at `path`.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Dispatch
14+
15+
/// Wrapper around `DispatchSemaphore` so that Swift Concurrency doesn't complain about the usage of semaphores in the
16+
/// tests.
17+
///
18+
/// This should only be used for tests that test priority escalation and thus cannot await a `Task` (which would cause
19+
/// priority elevations).
20+
public struct WrappedSemaphore {
21+
let semaphore = DispatchSemaphore(value: 0)
22+
23+
public init() {}
24+
25+
public func signal(value: Int = 1) {
26+
for _ in 0..<value {
27+
semaphore.signal()
28+
}
29+
}
30+
31+
public func wait(value: Int = 1) {
32+
for _ in 0..<value {
33+
semaphore.wait()
34+
}
35+
}
36+
}

Sources/SemanticIndex/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11

22
add_library(SemanticIndex STATIC
33
CheckedIndex.swift
4+
SemanticIndexManager.swift
5+
UpdateIndexStoreTaskDescription.swift
46
)
57
set_target_properties(SemanticIndex PROPERTIES
68
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import LSPLogging
15+
import LanguageServerProtocol
16+
import SKCore
17+
18+
/// Describes the state of indexing for a single source file
19+
private enum FileIndexStatus {
20+
/// The index is up-to-date.
21+
case upToDate
22+
/// The file is being indexed by the given task.
23+
case inProgress(Task<Void, Never>)
24+
}
25+
26+
/// Schedules index tasks and keeps track of the index status of files.
27+
public final actor SemanticIndexManager {
28+
/// The underlying index. This is used to check if the index of a file is already up-to-date, in which case it doesn't
29+
/// need to be indexed again.
30+
private let index: CheckedIndex
31+
32+
/// The build system manager that is used to get compiler arguments for a file.
33+
private let buildSystemManager: BuildSystemManager
34+
35+
/// The index status of the source files that the `SemanticIndexManager` knows about.
36+
///
37+
/// Files that have never been indexed are not in this dictionary.
38+
private var indexStatus: [DocumentURI: FileIndexStatus] = [:]
39+
40+
/// The `TaskScheduler` that manages the scheduling of index tasks. This is shared among all `SemanticIndexManager`s
41+
/// in the process, to ensure that we don't schedule more index operations than processor cores from multiple
42+
/// workspaces.
43+
private let indexTaskScheduler: TaskScheduler<UpdateIndexStoreTaskDescription>
44+
45+
/// Callback that is called when an index task has finished.
46+
///
47+
/// Currently only used for testing.
48+
private let indexTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) -> Void)?
49+
50+
// MARK: - Public API
51+
52+
public init(
53+
index: UncheckedIndex,
54+
buildSystemManager: BuildSystemManager,
55+
indexTaskScheduler: TaskScheduler<UpdateIndexStoreTaskDescription>,
56+
indexTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) -> Void)?
57+
) {
58+
self.index = index.checked(for: .modifiedFiles)
59+
self.buildSystemManager = buildSystemManager
60+
self.indexTaskScheduler = indexTaskScheduler
61+
self.indexTaskDidFinish = indexTaskDidFinish
62+
}
63+
64+
/// Schedules a task to index all files in `files` that don't already have an up-to-date index.
65+
/// Returns immediately after scheduling that task.
66+
///
67+
/// Indexing is being performed with a low priority.
68+
public func scheduleBackgroundIndex(files: some Collection<DocumentURI>) {
69+
self.index(files: files, priority: .low)
70+
}
71+
72+
/// Wait for all in-progress index tasks to finish.
73+
public func waitForUpToDateIndex() async {
74+
logger.info("Waiting for up-to-date index")
75+
await withTaskGroup(of: Void.self) { taskGroup in
76+
for (_, status) in indexStatus {
77+
switch status {
78+
case .inProgress(let task):
79+
taskGroup.addTask {
80+
await task.value
81+
}
82+
case .upToDate:
83+
break
84+
}
85+
}
86+
await taskGroup.waitForAll()
87+
}
88+
index.pollForUnitChangesAndWait()
89+
logger.debug("Done waiting for up-to-date index")
90+
}
91+
92+
/// Ensure that the index for the given files is up-to-date.
93+
///
94+
/// This tries to produce an up-to-date index for the given files as quickly as possible. To achieve this, it might
95+
/// suspend previous target-wide index tasks in favor of index tasks that index a fewer files.
96+
public func waitForUpToDateIndex(for uris: some Collection<DocumentURI>) async {
97+
logger.info(
98+
"Waiting for up-to-date index for \(uris.map { $0.fileURL?.lastPathComponent ?? $0.stringValue }.joined(separator: ", "))"
99+
)
100+
let filesWithOutOfDateIndex = uris.filter { uri in
101+
switch indexStatus[uri] {
102+
case .inProgress, nil: return true
103+
case .upToDate: return false
104+
}
105+
}
106+
// Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will
107+
// - Wait for the existing index operations to finish if they have the same number of files.
108+
// - Reschedule the background index task in favor of an index task with fewer source files.
109+
await self.index(files: filesWithOutOfDateIndex, priority: nil).value
110+
index.pollForUnitChangesAndWait()
111+
logger.debug("Done waiting for up-to-date index")
112+
}
113+
114+
// MARK: - Helper functions
115+
116+
/// Index the given set of files at the given priority.
117+
///
118+
/// The returned task finishes when all files are indexed.
119+
@discardableResult
120+
private func index(files: some Collection<DocumentURI>, priority: TaskPriority?) -> Task<Void, Never> {
121+
let outOfDateFiles = files.filter {
122+
if case .upToDate = indexStatus[$0] {
123+
return false
124+
}
125+
return true
126+
}
127+
128+
var indexTasks: [Task<Void, Never>] = []
129+
130+
// TODO (indexing): Group index operations by target when we support background preparation.
131+
for files in outOfDateFiles.partition(intoNumberOfBatches: ProcessInfo.processInfo.processorCount * 5) {
132+
let indexTask = Task(priority: priority) {
133+
await self.indexTaskScheduler.schedule(
134+
priority: priority,
135+
UpdateIndexStoreTaskDescription(
136+
filesToIndex: Set(files),
137+
buildSystemManager: self.buildSystemManager,
138+
index: self.index,
139+
didFinishCallback: { [weak self] taskDescription in
140+
self?.indexTaskDidFinish?(taskDescription)
141+
}
142+
)
143+
).value
144+
for file in files {
145+
indexStatus[file] = .upToDate
146+
}
147+
}
148+
indexTasks.append(indexTask)
149+
150+
for file in files {
151+
indexStatus[file] = .inProgress(indexTask)
152+
}
153+
}
154+
let indexTasksImmutable = indexTasks
155+
156+
return Task(priority: priority) {
157+
await withTaskGroup(of: Void.self) { taskGroup in
158+
for indexTask in indexTasksImmutable {
159+
taskGroup.addTask(priority: priority) {
160+
await indexTask.value
161+
}
162+
}
163+
await taskGroup.waitForAll()
164+
}
165+
}
166+
}
167+
}

0 commit comments

Comments
 (0)