-
Notifications
You must be signed in to change notification settings - Fork 330
Implement initial background indexing of a project #1216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| default.profraw | ||
| Package.resolved | ||
| /.build | ||
| /.index-build | ||
| /Packages | ||
| /*.xcodeproj | ||
| /*.sublime-project | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift.org open source project | ||
| // | ||
| // Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors | ||
| // Licensed under Apache License v2.0 with Runtime Library Exception | ||
| // | ||
| // See https://swift.org/LICENSE.txt for license information | ||
| // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import Dispatch | ||
|
|
||
| /// Wrapper around `DispatchSemaphore` so that Swift Concurrency doesn't complain about the usage of semaphores in the | ||
| /// tests. | ||
| /// | ||
| /// This should only be used for tests that test priority escalation and thus cannot await a `Task` (which would cause | ||
| /// priority elevations). | ||
| public struct WrappedSemaphore { | ||
| let semaphore = DispatchSemaphore(value: 0) | ||
|
|
||
| public init() {} | ||
|
|
||
| public func signal(value: Int = 1) { | ||
| for _ in 0..<value { | ||
| semaphore.signal() | ||
| } | ||
| } | ||
|
|
||
| public func wait(value: Int = 1) { | ||
| for _ in 0..<value { | ||
| semaphore.wait() | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift.org open source project | ||
| // | ||
| // Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors | ||
| // Licensed under Apache License v2.0 with Runtime Library Exception | ||
| // | ||
| // See https://swift.org/LICENSE.txt for license information | ||
| // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import Foundation | ||
| import LSPLogging | ||
| import LanguageServerProtocol | ||
| import SKCore | ||
|
|
||
| /// Describes the state of indexing for a single source file | ||
| private enum FileIndexStatus { | ||
| /// The index is up-to-date. | ||
| case upToDate | ||
| /// The file is being indexed by the given task. | ||
| case inProgress(Task<Void, Never>) | ||
| } | ||
|
|
||
| /// Schedules index tasks and keeps track of the index status of files. | ||
| public final actor SemanticIndexManager { | ||
| /// 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 | ||
| /// need to be indexed again. | ||
| private let index: CheckedIndex | ||
|
|
||
| /// The build system manager that is used to get compiler arguments for a file. | ||
| private let buildSystemManager: BuildSystemManager | ||
|
|
||
| /// The index status of the source files that the `SemanticIndexManager` knows about. | ||
| /// | ||
| /// Files that have never been indexed are not in this dictionary. | ||
| private var indexStatus: [DocumentURI: FileIndexStatus] = [:] | ||
|
|
||
| /// The `TaskScheduler` that manages the scheduling of index tasks. This is shared among all `SemanticIndexManager`s | ||
| /// in the process, to ensure that we don't schedule more index operations than processor cores from multiple | ||
| /// workspaces. | ||
| private let indexTaskScheduler: TaskScheduler<UpdateIndexStoreTaskDescription> | ||
|
|
||
| /// Callback that is called when an index task has finished. | ||
| /// | ||
| /// Currently only used for testing. | ||
| private let indexTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) -> Void)? | ||
|
|
||
| // MARK: - Public API | ||
|
|
||
| public init( | ||
| index: UncheckedIndex, | ||
| buildSystemManager: BuildSystemManager, | ||
| indexTaskScheduler: TaskScheduler<UpdateIndexStoreTaskDescription>, | ||
| indexTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) -> Void)? | ||
| ) { | ||
| self.index = index.checked(for: .modifiedFiles) | ||
| self.buildSystemManager = buildSystemManager | ||
| self.indexTaskScheduler = indexTaskScheduler | ||
| self.indexTaskDidFinish = indexTaskDidFinish | ||
| } | ||
|
|
||
| /// Schedules a task to index all files in `files` that don't already have an up-to-date index. | ||
| /// Returns immediately after scheduling that task. | ||
| /// | ||
| /// Indexing is being performed with a low priority. | ||
| public func scheduleBackgroundIndex(files: some Collection<DocumentURI>) { | ||
| self.index(files: files, priority: .low) | ||
| } | ||
|
|
||
| /// Wait for all in-progress index tasks to finish. | ||
| public func waitForUpToDateIndex() async { | ||
| logger.info("Waiting for up-to-date index") | ||
| await withTaskGroup(of: Void.self) { taskGroup in | ||
| for (_, status) in indexStatus { | ||
| switch status { | ||
| case .inProgress(let task): | ||
| taskGroup.addTask { | ||
| await task.value | ||
| } | ||
| case .upToDate: | ||
| break | ||
| } | ||
| } | ||
| await taskGroup.waitForAll() | ||
| } | ||
| index.pollForUnitChangesAndWait() | ||
| logger.debug("Done waiting for up-to-date index") | ||
| } | ||
|
|
||
| /// Ensure that the index for the given files is up-to-date. | ||
| /// | ||
| /// This tries to produce an up-to-date index for the given files as quickly as possible. To achieve this, it might | ||
| /// suspend previous target-wide index tasks in favor of index tasks that index a fewer files. | ||
| public func waitForUpToDateIndex(for uris: some Collection<DocumentURI>) async { | ||
| logger.info( | ||
| "Waiting for up-to-date index for \(uris.map { $0.fileURL?.lastPathComponent ?? $0.stringValue }.joined(separator: ", "))" | ||
| ) | ||
| let filesWithOutOfDateIndex = uris.filter { uri in | ||
| switch indexStatus[uri] { | ||
| case .inProgress, nil: return true | ||
| case .upToDate: return false | ||
| } | ||
| } | ||
| // Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will | ||
| // - Wait for the existing index operations to finish if they have the same number of files. | ||
| // - Reschedule the background index task in favor of an index task with fewer source files. | ||
| await self.index(files: filesWithOutOfDateIndex, priority: nil).value | ||
| index.pollForUnitChangesAndWait() | ||
| logger.debug("Done waiting for up-to-date index") | ||
| } | ||
|
|
||
| // MARK: - Helper functions | ||
|
|
||
| /// Index the given set of files at the given priority. | ||
| /// | ||
| /// The returned task finishes when all files are indexed. | ||
| @discardableResult | ||
| private func index(files: some Collection<DocumentURI>, priority: TaskPriority?) -> Task<Void, Never> { | ||
| let outOfDateFiles = files.filter { | ||
| if case .upToDate = indexStatus[$0] { | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| var indexTasks: [Task<Void, Never>] = [] | ||
|
|
||
| // TODO (indexing): Group index operations by target when we support background preparation. | ||
| for files in outOfDateFiles.partition(intoNumberOfBatches: ProcessInfo.processInfo.processorCount * 5) { | ||
|
Comment on lines
+130
to
+131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth a comment explaining the batching size? Or does it not matter since this will change once we support preparation?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| let indexTask = Task(priority: priority) { | ||
| await self.indexTaskScheduler.schedule( | ||
| priority: priority, | ||
| UpdateIndexStoreTaskDescription( | ||
| filesToIndex: Set(files), | ||
| buildSystemManager: self.buildSystemManager, | ||
| index: self.index, | ||
| didFinishCallback: { [weak self] taskDescription in | ||
| self?.indexTaskDidFinish?(taskDescription) | ||
| } | ||
| ) | ||
| ).value | ||
| for file in files { | ||
| indexStatus[file] = .upToDate | ||
| } | ||
| } | ||
| indexTasks.append(indexTask) | ||
|
|
||
| for file in files { | ||
| indexStatus[file] = .inProgress(indexTask) | ||
| } | ||
| } | ||
| let indexTasksImmutable = indexTasks | ||
|
|
||
| return Task(priority: priority) { | ||
| await withTaskGroup(of: Void.self) { taskGroup in | ||
| for indexTask in indexTasksImmutable { | ||
| taskGroup.addTask(priority: priority) { | ||
| await indexTask.value | ||
| } | ||
| } | ||
| await taskGroup.waitForAll() | ||
| } | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this filtering already done by
index(files:priority:)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch 👍🏽