@@ -16,6 +16,14 @@ import SKCore
16
16
17
17
import struct TSCBasic. AbsolutePath
18
18
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
19
27
20
28
public struct DiagnoseCommand : AsyncParsableCommand {
21
29
public static var configuration : CommandConfiguration = CommandConfiguration (
@@ -88,24 +96,30 @@ public struct DiagnoseCommand: AsyncParsableCommand {
88
96
public init ( ) { }
89
97
90
98
private func addSourcekitdCrashReproducer( toBundle bundlePath: URL ) async throws {
99
+ reportProgress ( . reproducingSourcekitdCrash( progress: 0 ) , message: " Trying to reduce recent sourcekitd crashes " )
91
100
guard let sourcekitd = try await sourcekitd else {
92
101
throw ReductionError ( " Unable to find sourcekitd.framework " )
93
102
}
94
103
95
104
for (name, requestInfo) in try requestInfos ( ) {
96
- print ( " -- Reducing \( name) " )
105
+ reportProgress ( . reproducingSourcekitdCrash ( progress : 0 ) , message : " Reducing sourcekitd crash \( name) " )
97
106
do {
98
107
try await reduce (
99
108
requestInfo: requestInfo,
100
109
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
+ }
102
117
)
103
118
// If reduce didn't throw, we have found a reproducer. Stop.
104
119
// Looking further probably won't help because other crashes are likely the same cause.
105
120
break
106
121
} catch {
107
122
// Reducing this request failed. Continue reducing the next one, maybe that one succeeds.
108
- print ( error)
109
123
}
110
124
}
111
125
}
@@ -121,10 +135,14 @@ public struct DiagnoseCommand: AsyncParsableCommand {
121
135
122
136
private func addOsLog( toBundle bundlePath: URL ) async throws {
123
137
#if os(macOS)
124
- print ( " -- Collecting log messages" )
138
+ reportProgress ( . collectingLogMessages ( progress : 0 ) , message : " Collecting log messages " )
125
139
let outputFileUrl = bundlePath. appendingPathComponent ( " log.txt " )
126
140
FileManager . default. createFile ( atPath: outputFileUrl. path, contents: nil )
127
141
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
128
146
let process = Process (
129
147
arguments: [
130
148
" /usr/bin/log " ,
@@ -134,7 +152,16 @@ public struct DiagnoseCommand: AsyncParsableCommand {
134
152
" --debug " ,
135
153
] ,
136
154
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
+ } ,
138
165
stderr: { _ in }
139
166
)
140
167
)
@@ -145,7 +172,7 @@ public struct DiagnoseCommand: AsyncParsableCommand {
145
172
146
173
private func addCrashLogs( toBundle bundlePath: URL ) throws {
147
174
#if os(macOS)
148
- print ( " -- Collecting crash reports" )
175
+ reportProgress ( . collectingCrashReports , message : " Collecting crash reports " )
149
176
150
177
let destinationDir = bundlePath. appendingPathComponent ( " crashes " )
151
178
try FileManager . default. createDirectory ( at: destinationDir, withIntermediateDirectories: true )
@@ -170,13 +197,18 @@ public struct DiagnoseCommand: AsyncParsableCommand {
170
197
}
171
198
172
199
private func addSwiftVersion( toBundle bundlePath: URL ) async throws {
173
- print ( " -- Collecting installed Swift versions " )
174
-
175
200
let outputFileUrl = bundlePath. appendingPathComponent ( " swift-versions.txt " )
176
201
FileManager . default. createFile ( atPath: outputFileUrl. path, contents: nil )
177
202
let fileHandle = try FileHandle ( forWritingTo: outputFileUrl)
178
203
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
+
180
212
guard let swiftUrl = toolchain. swift? . asURL else {
181
213
continue
182
214
}
@@ -195,6 +227,10 @@ public struct DiagnoseCommand: AsyncParsableCommand {
195
227
}
196
228
}
197
229
230
+ private func reportProgress( _ state: DiagnoseProgressState , message: String ) {
231
+ progressBar? . update ( step: Int ( state. progress * 100 ) , total: 100 , text: message)
232
+ }
233
+
198
234
public func run( ) async throws {
199
235
print (
200
236
"""
@@ -206,11 +242,12 @@ public struct DiagnoseCommand: AsyncParsableCommand {
206
242
- If possible, a minimized project that caused SourceKit to crash
207
243
208
244
All information is collected locally.
209
- The collection might take a few minutes.
210
- ----------------------------------------
245
+
211
246
"""
212
247
)
213
248
249
+ progressBar = PercentProgressAnimation ( stream: stderrStream, header: " Diagnosing sourcekit-lsp issues " )
250
+
214
251
let date = ISO8601DateFormatter ( ) . string ( from: Date ( ) ) . replacingOccurrences ( of: " : " , with: " - " )
215
252
let bundlePath = FileManager . default. temporaryDirectory
216
253
. appendingPathComponent ( " sourcekitd-reproducer- \( date) " )
@@ -221,9 +258,12 @@ public struct DiagnoseCommand: AsyncParsableCommand {
221
258
await orPrintError { try await addSwiftVersion ( toBundle: bundlePath) }
222
259
await orPrintError { try await addSourcekitdCrashReproducer ( toBundle: bundlePath) }
223
260
261
+ progressBar? . complete ( success: true )
262
+ progressBar? . clear ( )
263
+
224
264
print (
225
265
"""
226
- ----------------------------------------
266
+
227
267
Bundle created.
228
268
When filing an issue at https://github.com/apple/sourcekit-lsp/issues/new,
229
269
please attach the bundle located at
@@ -233,7 +273,12 @@ public struct DiagnoseCommand: AsyncParsableCommand {
233
273
234
274
}
235
275
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 {
237
282
var requestInfo = requestInfo
238
283
var nspredicate : NSPredicate ? = nil
239
284
#if canImport(Darwin)
@@ -245,9 +290,70 @@ public struct DiagnoseCommand: AsyncParsableCommand {
245
290
sourcekitd: URL ( fileURLWithPath: sourcekitd) ,
246
291
reproducerPredicate: nspredicate
247
292
)
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
+ )
250
310
251
311
try makeReproducerBundle ( for: requestInfo, bundlePath: bundlePath)
252
312
}
253
313
}
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