Skip to content

Commit 5405807

Browse files
authored
Merge pull request #1108 from ahoppen/ahoppen/diagnose-progress-bar
Add a progress bar to the diagnose subcommand
2 parents edfe80d + 96ab4a7 commit 5405807

File tree

4 files changed

+205
-32
lines changed

4 files changed

+205
-32
lines changed

Sources/Diagnose/CommandLineArgumentsReducer.swift

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ import Foundation
1515
// MARK: - Entry point
1616

1717
extension RequestInfo {
18-
func reduceCommandLineArguments(using executor: SourceKitRequestExecutor) async throws -> RequestInfo {
19-
let reducer = CommandLineArgumentReducer(sourcekitdExecutor: executor)
20-
return try await reducer.run(initialRequestInfo: self)
18+
func reduceCommandLineArguments(
19+
using executor: SourceKitRequestExecutor,
20+
progressUpdate: (_ progress: Double, _ message: String) -> Void
21+
) async throws -> RequestInfo {
22+
try await withoutActuallyEscaping(progressUpdate) { progressUpdate in
23+
let reducer = CommandLineArgumentReducer(sourcekitdExecutor: executor, progressUpdate: progressUpdate)
24+
return try await reducer.run(initialRequestInfo: self)
25+
}
2126
}
2227
}
2328

@@ -32,22 +37,36 @@ fileprivate class CommandLineArgumentReducer {
3237
/// The file to which we write the reduced source code.
3338
private let temporarySourceFile: URL
3439

35-
init(sourcekitdExecutor: SourceKitRequestExecutor) {
40+
/// A callback to be called when the reducer has made progress reducing the request
41+
private let progressUpdate: (_ progress: Double, _ message: String) -> Void
42+
43+
/// The number of command line arguments when the reducer was started.
44+
private var initialCommandLineCount: Int = 0
45+
46+
init(
47+
sourcekitdExecutor: SourceKitRequestExecutor,
48+
progressUpdate: @escaping (_ progress: Double, _ message: String) -> Void
49+
) {
3650
self.sourcekitdExecutor = sourcekitdExecutor
37-
temporarySourceFile = FileManager.default.temporaryDirectory.appendingPathComponent("reduce-\(UUID()).swift")
51+
self.temporarySourceFile = FileManager.default.temporaryDirectory.appendingPathComponent("reduce-\(UUID()).swift")
52+
self.progressUpdate = progressUpdate
3853
}
3954

4055
deinit {
4156
try? FileManager.default.removeItem(at: temporarySourceFile)
4257
}
4358

4459
func logSuccessfulReduction(_ requestInfo: RequestInfo) {
45-
print("Reduced compiler arguments to \(requestInfo.compilerArgs.count)")
60+
progressUpdate(
61+
1 - (Double(requestInfo.compilerArgs.count) / Double(initialCommandLineCount)),
62+
"Reduced compiler arguments to \(requestInfo.compilerArgs.count)"
63+
)
4664
}
4765

4866
func run(initialRequestInfo: RequestInfo) async throws -> RequestInfo {
4967
try initialRequestInfo.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8)
5068
var requestInfo = initialRequestInfo
69+
self.initialCommandLineCount = requestInfo.compilerArgs.count
5170

5271
var argumentIndexToRemove = requestInfo.compilerArgs.count - 1
5372
while argumentIndexToRemove >= 0 {

Sources/Diagnose/DiagnoseCommand.swift

Lines changed: 121 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ import SKCore
1616

1717
import struct TSCBasic.AbsolutePath
1818
import class TSCBasic.Process
19+
import var TSCBasic.stderrStream
20+
import class TSCUtility.PercentProgressAnimation
21+
22+
/// When diagnosis is started, a progress bar displayed on the terminal that shows how far the diagnose command has
23+
/// progressed.
24+
/// Can't be a member of `DiagnoseCommand` because then `DiagnoseCommand` is no longer codable, which it needs to be
25+
/// to be a `AsyncParsableCommand`.
26+
private var progressBar: PercentProgressAnimation? = nil
1927

2028
public struct DiagnoseCommand: AsyncParsableCommand {
2129
public static var configuration: CommandConfiguration = CommandConfiguration(
@@ -88,24 +96,30 @@ public struct DiagnoseCommand: AsyncParsableCommand {
8896
public init() {}
8997

9098
private func addSourcekitdCrashReproducer(toBundle bundlePath: URL) async throws {
99+
reportProgress(.reproducingSourcekitdCrash(progress: 0), message: "Trying to reduce recent sourcekitd crashes")
91100
guard let sourcekitd = try await sourcekitd else {
92101
throw ReductionError("Unable to find sourcekitd.framework")
93102
}
94103

95104
for (name, requestInfo) in try requestInfos() {
96-
print("-- Reducing \(name)")
105+
reportProgress(.reproducingSourcekitdCrash(progress: 0), message: "Reducing sourcekitd crash \(name)")
97106
do {
98107
try await reduce(
99108
requestInfo: requestInfo,
100109
sourcekitd: sourcekitd,
101-
bundlePath: bundlePath.appendingPathComponent("reproducer")
110+
bundlePath: bundlePath.appendingPathComponent("reproducer"),
111+
progressUpdate: { (progress, message) in
112+
reportProgress(
113+
.reproducingSourcekitdCrash(progress: progress),
114+
message: "Reducing sourcekitd crash \(name): \(message)"
115+
)
116+
}
102117
)
103118
// If reduce didn't throw, we have found a reproducer. Stop.
104119
// Looking further probably won't help because other crashes are likely the same cause.
105120
break
106121
} catch {
107122
// Reducing this request failed. Continue reducing the next one, maybe that one succeeds.
108-
print(error)
109123
}
110124
}
111125
}
@@ -121,10 +135,14 @@ public struct DiagnoseCommand: AsyncParsableCommand {
121135

122136
private func addOsLog(toBundle bundlePath: URL) async throws {
123137
#if os(macOS)
124-
print("-- Collecting log messages")
138+
reportProgress(.collectingLogMessages(progress: 0), message: "Collecting log messages")
125139
let outputFileUrl = bundlePath.appendingPathComponent("log.txt")
126140
FileManager.default.createFile(atPath: outputFileUrl.path, contents: nil)
127141
let fileHandle = try FileHandle(forWritingTo: outputFileUrl)
142+
var bytesCollected = 0
143+
// 50 MB is an average log size collected by sourcekit-lsp diagnose.
144+
// It's a good proxy to show some progress indication for the majority of the time.
145+
let expectedLogSize = 50_000_000
128146
let process = Process(
129147
arguments: [
130148
"/usr/bin/log",
@@ -134,7 +152,16 @@ public struct DiagnoseCommand: AsyncParsableCommand {
134152
"--debug",
135153
],
136154
outputRedirection: .stream(
137-
stdout: { try? fileHandle.write(contentsOf: $0) },
155+
stdout: { bytes in
156+
try? fileHandle.write(contentsOf: bytes)
157+
bytesCollected += bytes.count
158+
var progress = Double(bytesCollected) / Double(expectedLogSize)
159+
if progress > 1 {
160+
// The log is larger than we expected. Halt at 100%
161+
progress = 1
162+
}
163+
reportProgress(.collectingLogMessages(progress: progress), message: "Collecting log messages")
164+
},
138165
stderr: { _ in }
139166
)
140167
)
@@ -145,7 +172,7 @@ public struct DiagnoseCommand: AsyncParsableCommand {
145172

146173
private func addCrashLogs(toBundle bundlePath: URL) throws {
147174
#if os(macOS)
148-
print("-- Collecting crash reports")
175+
reportProgress(.collectingCrashReports, message: "Collecting crash reports")
149176

150177
let destinationDir = bundlePath.appendingPathComponent("crashes")
151178
try FileManager.default.createDirectory(at: destinationDir, withIntermediateDirectories: true)
@@ -170,13 +197,18 @@ public struct DiagnoseCommand: AsyncParsableCommand {
170197
}
171198

172199
private func addSwiftVersion(toBundle bundlePath: URL) async throws {
173-
print("-- Collecting installed Swift versions")
174-
175200
let outputFileUrl = bundlePath.appendingPathComponent("swift-versions.txt")
176201
FileManager.default.createFile(atPath: outputFileUrl.path, contents: nil)
177202
let fileHandle = try FileHandle(forWritingTo: outputFileUrl)
178203

179-
for toolchain in try await toolchainRegistry.toolchains {
204+
let toolchains = try await toolchainRegistry.toolchains
205+
206+
for (index, toolchain) in toolchains.enumerated() {
207+
reportProgress(
208+
.collectingSwiftVersions(progress: Double(index) / Double(toolchains.count)),
209+
message: "Determining Swift version of \(toolchain.identifier)"
210+
)
211+
180212
guard let swiftUrl = toolchain.swift?.asURL else {
181213
continue
182214
}
@@ -195,6 +227,10 @@ public struct DiagnoseCommand: AsyncParsableCommand {
195227
}
196228
}
197229

230+
private func reportProgress(_ state: DiagnoseProgressState, message: String) {
231+
progressBar?.update(step: Int(state.progress * 100), total: 100, text: message)
232+
}
233+
198234
public func run() async throws {
199235
print(
200236
"""
@@ -206,11 +242,12 @@ public struct DiagnoseCommand: AsyncParsableCommand {
206242
- If possible, a minimized project that caused SourceKit to crash
207243
208244
All information is collected locally.
209-
The collection might take a few minutes.
210-
----------------------------------------
245+
211246
"""
212247
)
213248

249+
progressBar = PercentProgressAnimation(stream: stderrStream, header: "Diagnosing sourcekit-lsp issues")
250+
214251
let date = ISO8601DateFormatter().string(from: Date()).replacingOccurrences(of: ":", with: "-")
215252
let bundlePath = FileManager.default.temporaryDirectory
216253
.appendingPathComponent("sourcekitd-reproducer-\(date)")
@@ -221,9 +258,12 @@ public struct DiagnoseCommand: AsyncParsableCommand {
221258
await orPrintError { try await addSwiftVersion(toBundle: bundlePath) }
222259
await orPrintError { try await addSourcekitdCrashReproducer(toBundle: bundlePath) }
223260

261+
progressBar?.complete(success: true)
262+
progressBar?.clear()
263+
224264
print(
225265
"""
226-
----------------------------------------
266+
227267
Bundle created.
228268
When filing an issue at https://github.com/apple/sourcekit-lsp/issues/new,
229269
please attach the bundle located at
@@ -233,7 +273,12 @@ public struct DiagnoseCommand: AsyncParsableCommand {
233273

234274
}
235275

236-
private func reduce(requestInfo: RequestInfo, sourcekitd: String, bundlePath: URL) async throws {
276+
private func reduce(
277+
requestInfo: RequestInfo,
278+
sourcekitd: String,
279+
bundlePath: URL,
280+
progressUpdate: (_ progress: Double, _ message: String) -> Void
281+
) async throws {
237282
var requestInfo = requestInfo
238283
var nspredicate: NSPredicate? = nil
239284
#if canImport(Darwin)
@@ -245,9 +290,70 @@ public struct DiagnoseCommand: AsyncParsableCommand {
245290
sourcekitd: URL(fileURLWithPath: sourcekitd),
246291
reproducerPredicate: nspredicate
247292
)
248-
requestInfo = try await requestInfo.reduceInputFile(using: executor)
249-
requestInfo = try await requestInfo.reduceCommandLineArguments(using: executor)
293+
294+
// How much time of the reduction is expected to be spent reducing the source compared to command line argument
295+
// reduction.
296+
let sourceReductionPercentage = 0.7
297+
298+
requestInfo = try await requestInfo.reduceInputFile(
299+
using: executor,
300+
progressUpdate: { progress, message in
301+
progressUpdate(progress * sourceReductionPercentage, message)
302+
}
303+
)
304+
requestInfo = try await requestInfo.reduceCommandLineArguments(
305+
using: executor,
306+
progressUpdate: { progress, message in
307+
progressUpdate(sourceReductionPercentage + progress * (1 - sourceReductionPercentage), message)
308+
}
309+
)
250310

251311
try makeReproducerBundle(for: requestInfo, bundlePath: bundlePath)
252312
}
253313
}
314+
315+
/// Describes the state that the diagnose command is in. This is used to compute a progress bar.
316+
fileprivate enum DiagnoseProgressState: Comparable {
317+
case collectingCrashReports
318+
case collectingLogMessages(progress: Double)
319+
case collectingSwiftVersions(progress: Double)
320+
case reproducingSourcekitdCrash(progress: Double)
321+
322+
var allFinalStates: [DiagnoseProgressState] {
323+
return [
324+
.collectingCrashReports,
325+
.collectingLogMessages(progress: 1),
326+
.collectingSwiftVersions(progress: 1),
327+
.reproducingSourcekitdCrash(progress: 1),
328+
]
329+
}
330+
331+
/// An estimate of how long this state takes in seconds.
332+
///
333+
/// The actual values are never displayed. We use these values to allocate a portion of the overall progress to this
334+
/// state.
335+
var estimatedDuration: Double {
336+
switch self {
337+
case .collectingCrashReports:
338+
return 1
339+
case .collectingLogMessages:
340+
return 15
341+
case .collectingSwiftVersions:
342+
return 10
343+
case .reproducingSourcekitdCrash:
344+
return 60
345+
}
346+
}
347+
348+
var progress: Double {
349+
let estimatedTotalDuration = allFinalStates.reduce(0, { $0 + $1.estimatedDuration })
350+
var elapsedEstimatedDuration = allFinalStates.filter { $0 < self }.reduce(0, { $0 + $1.estimatedDuration })
351+
switch self {
352+
case .collectingCrashReports: break
353+
case .collectingLogMessages(let progress), .collectingSwiftVersions(progress: let progress),
354+
.reproducingSourcekitdCrash(progress: let progress):
355+
elapsedEstimatedDuration += progress * self.estimatedDuration
356+
}
357+
return elapsedEstimatedDuration / estimatedTotalDuration
358+
}
359+
}

0 commit comments

Comments
 (0)