File: IndexProgressManager.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (138 lines) | stat: -rw-r--r-- 5,916 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//===----------------------------------------------------------------------===//
//
// 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 LSPLogging
import LanguageServerProtocol
import SKCore
import SKSupport
import SemanticIndex
import SwiftExtensions

/// Listens for index status updates from `SemanticIndexManagers`. From that information, it manages a
/// `WorkDoneProgress` that communicates the index progress to the editor.
actor IndexProgressManager {
  /// A queue on which `indexTaskWasQueued` and `indexProgressStatusDidChange` are handled.
  ///
  /// This allows the two functions two be `nonisolated` (and eg. the caller of `indexProgressStatusDidChange` doesn't have to
  /// wait for the work done progress to be updated) while still guaranteeing that there is only one
  /// `indexProgressStatusDidChangeImpl` running at a time, preventing race conditions that would cause two
  /// `WorkDoneProgressManager`s to be created.
  private let queue = AsyncQueue<Serial>()

  /// The `SourceKitLSPServer` for which this manages the index progress. It gathers all `SemanticIndexManagers` from
  /// the workspaces in the `SourceKitLSPServer`.
  private weak var sourceKitLSPServer: SourceKitLSPServer?

  /// This is the target number of index tasks (eg. the `3` in `1/3 done`).
  ///
  /// Every time a new index task is scheduled, this number gets incremented, so that it only ever increases.
  /// When indexing of one session is done (ie. when there are no more `scheduled` or `executing` tasks in any
  /// `SemanticIndexManager`), `queuedIndexTasks` gets reset to 0 and the work done progress gets ended.
  /// This way, when the next work done progress is started, it starts at zero again.
  ///
  /// The number of outstanding tasks is determined from the `scheduled` and `executing` tasks in all the
  /// `SemanticIndexManager`s.
  ///
  /// Note that the `queuedIndexTasks` might exceed the number of files in the project, eg. in the following scenario:
  /// - Schedule indexing of A.swift and B.swift -> 0 / 2
  /// - Indexing of A.swift finishes -> 1 / 2
  /// - A.swift is modified and should be indexed again -> 1 / 3
  /// - B.swift finishes indexing -> 2 / 3
  /// - A.swift finishes indexing for the second time -> 3 / 3 -> Status disappears
  private var queuedIndexTasks = 0

  /// While there are ongoing index tasks, a `WorkDoneProgressManager` that displays the work done progress.
  private var workDoneProgress: WorkDoneProgressManager?

  init(sourceKitLSPServer: SourceKitLSPServer) {
    self.sourceKitLSPServer = sourceKitLSPServer
  }

  /// Called when a new file is scheduled to be indexed. Increments the target index count, eg. the 3 in `1/3`.
  nonisolated func indexTasksWereScheduled(count: Int) {
    queue.async {
      await self.indexTasksWereScheduledImpl(count: count)
    }
  }

  private func indexTasksWereScheduledImpl(count: Int) async {
    queuedIndexTasks += count
    await indexProgressStatusDidChangeImpl()
  }

  /// Called when a `SemanticIndexManager` finishes indexing a file. Adjusts the done index count, eg. the 1 in `1/3`.
  nonisolated func indexProgressStatusDidChange() {
    queue.async {
      await self.indexProgressStatusDidChangeImpl()
    }
  }

  private func indexProgressStatusDidChangeImpl() async {
    guard let sourceKitLSPServer else {
      await workDoneProgress?.end()
      workDoneProgress = nil
      return
    }
    var status = IndexProgressStatus.upToDate
    for indexManager in await sourceKitLSPServer.workspaces.compactMap({ $0.semanticIndexManager }) {
      status = status.merging(with: await indexManager.progressStatus)
    }

    var message: String
    let percentage: Int
    switch status {
    case .preparingFileForEditorFunctionality:
      message = "Preparing current file"
      percentage = 0
    case .generatingBuildGraph:
      message = "Generating build graph"
      percentage = 0
    case .indexing(preparationTasks: let preparationTasks, indexTasks: let indexTasks):
      // We can get into a situation where queuedIndexTasks < indexTasks.count if we haven't processed all
      // `indexTasksWereScheduled` calls yet but the semantic index managers already track them in their in-progress tasks.
      // Clip the finished tasks to 0 because showing a negative number there looks stupid.
      let finishedTasks = max(queuedIndexTasks - indexTasks.count, 0)
      if indexTasks.isEmpty {
        message = "Preparing targets"
        if preparationTasks.isEmpty {
          logger.fault("Indexer status is 'indexing' but there is no update indexstore or preparation task")
        }
      } else {
        message = "\(finishedTasks) / \(queuedIndexTasks)"
      }
      if queuedIndexTasks != 0 {
        percentage = Int(Double(finishedTasks) / Double(queuedIndexTasks) * 100)
      } else {
        percentage = 0
      }
    case .upToDate:
      // Nothing left to index. Reset the target count and dismiss the work done progress.
      queuedIndexTasks = 0
      await workDoneProgress?.end()
      workDoneProgress = nil
      return
    }

    if let workDoneProgress {
      await workDoneProgress.update(message: message, percentage: percentage)
    } else {
      workDoneProgress = await WorkDoneProgressManager(
        server: sourceKitLSPServer,
        tokenPrefix: "indexing",
        initialDebounce: sourceKitLSPServer.options.workDoneProgressDebounceDurationOrDefault,
        title: "Indexing",
        message: message,
        percentage: percentage
      )
    }
  }
}