Skip to content

Commit fcd6e2d

Browse files
authored
Revert "Revert "Define commands as asynchronous and use Task for preview cancellation "" (#1062)
* Revert "Revert "Define commands as asynchronous and use Task for preview canc…" This reverts commit 61ce635. * Workaround miscompilation in `ConvertAction.perform(logHandle:)` * Temporarily skip until `testCancelsConversion()` until #1059 is merged * Change private `DiagnosticEngine.filter` closure to a private function
1 parent eec57a2 commit fcd6e2d

36 files changed

+488
-624
lines changed

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticEngine.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@ public final class DiagnosticEngine {
2727
///
2828
/// This filter level is inclusive, i.e. if a level of ``DiagnosticSeverity/information`` is specified,
2929
/// diagnostics with a severity up to and including `.information` will be printed.
30-
public var filterLevel: DiagnosticSeverity {
31-
didSet {
32-
self.filter = { $0.diagnostic.severity.rawValue <= self.filterLevel.rawValue }
33-
}
34-
}
30+
public var filterLevel: DiagnosticSeverity
3531

3632
/// Returns a Boolean value indicating whether the engine contains a consumer that satisfies the given predicate.
3733
/// - Parameter predicate: A closure that takes one of the engine's consumers as its argument and returns a Boolean value that indicates whether the passed consumer represents a match.
@@ -46,7 +42,9 @@ public final class DiagnosticEngine {
4642
private let treatWarningsAsErrors: Bool
4743

4844
/// Determines which problems should be emitted.
49-
private var filter: (Problem) -> Bool
45+
private func shouldEmit(_ problem: Problem) -> Bool {
46+
problem.diagnostic.severity.rawValue <= filterLevel.rawValue
47+
}
5048

5149
/// A convenience accessor for retrieving all of the diagnostics this engine currently holds.
5250
public var problems: [Problem] {
@@ -57,7 +55,6 @@ public final class DiagnosticEngine {
5755
public init(filterLevel: DiagnosticSeverity = .warning, treatWarningsAsErrors: Bool = false) {
5856
self.filterLevel = filterLevel
5957
self.treatWarningsAsErrors = treatWarningsAsErrors
60-
self.filter = { $0.diagnostic.severity.rawValue <= filterLevel.rawValue }
6158
}
6259

6360
/// Removes all of the encountered diagnostics from this engine.
@@ -85,7 +82,7 @@ public final class DiagnosticEngine {
8582
}
8683
return problem
8784
}
88-
let filteredProblems = mappedProblems.filter(filter)
85+
let filteredProblems = mappedProblems.filter(shouldEmit)
8986
guard !filteredProblems.isEmpty else { return }
9087

9188
if filteredProblems.containsErrors {

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
13361336
}
13371337

13381338
private func shouldContinueRegistration() throws {
1339+
try Task.checkCancellation()
13391340
guard isRegistrationEnabled.sync({ $0 }) else {
13401341
throw ContextError.registrationDisabled
13411342
}
@@ -1778,6 +1779,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
17781779
///
17791780
/// When given `false` the context will try to cancel as quick as possible
17801781
/// any ongoing bundle registrations.
1782+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
17811783
public func setRegistrationEnabled(_ value: Bool) {
17821784
isRegistrationEnabled.sync({ $0 = value })
17831785
}

Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
5050
private var dataProvider: DocumentationWorkspaceDataProvider
5151

5252
/// An optional closure that sets up a context before the conversion begins.
53+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
5354
public var setupContext: ((inout DocumentationContext) -> Void)?
5455

5556
/// Conversion batches should be big enough to keep all cores busy but small enough not to keep
@@ -189,49 +190,17 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
189190
if let dataProvider = self.currentDataProvider {
190191
try workspace.unregisterProvider(dataProvider)
191192
}
192-
193-
// Do additional context setup.
194-
setupContext?(&context)
195193

196-
/*
197-
Asynchronously cancel registration if necessary.
198-
We spawn a timer that periodically checks `isCancelled` and if necessary
199-
disables registration in `DocumentationContext` as registration being
200-
the largest part of a documentation conversion.
201-
*/
202194
let context = self.context
203-
let isCancelled = self.isCancelled
204-
205-
// `true` if the `isCancelled` flag is set.
206-
func isConversionCancelled() -> Bool {
207-
return isCancelled?.sync({ $0 }) == true
208-
}
209-
210-
// Run a timer that synchronizes the cancelled state between the converter and the context directly.
211-
// We need a timer on a separate dispatch queue because `workspace.registerProvider()` blocks
212-
// the current thread until it loads all symbol graphs, markdown files, and builds the topic graph
213-
// so in order to be able to update the context cancellation flag we need to run on a different thread.
214-
var cancelTimerQueue: DispatchQueue? = DispatchQueue(label: "org.swift.docc.ConvertActionCancelTimer", qos: .unspecified, attributes: .concurrent)
215-
let cancelTimer = DispatchSource.makeTimerSource(queue: cancelTimerQueue)
216-
cancelTimer.schedule(deadline: .now(), repeating: .milliseconds(500), leeway: .milliseconds(50))
217-
cancelTimer.setEventHandler {
218-
if isConversionCancelled() {
219-
cancelTimer.cancel()
220-
context.setRegistrationEnabled(false)
221-
}
222-
}
223-
cancelTimer.resume()
224195

225196
// Start bundle registration
226197
try workspace.registerProvider(dataProvider, options: bundleDiscoveryOptions)
227198
self.currentDataProvider = dataProvider
228-
229-
// Bundle registration is finished - stop the timer and reset the context cancellation state.
230-
cancelTimer.cancel()
231-
cancelTimerQueue = nil
232-
context.setRegistrationEnabled(true)
233199

234200
// If cancelled, return early before we emit diagnostics.
201+
func isConversionCancelled() -> Bool {
202+
Task.isCancelled || isCancelled?.sync({ $0 }) == true
203+
}
235204
guard !isConversionCancelled() else { return ([], []) }
236205

237206
processingDurationMetric = benchmark(begin: Benchmark.Duration(id: "documentation-processing"))
Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -13,18 +13,16 @@ import SwiftDocC
1313

1414
/// An independent unit of work in the command-line workflow.
1515
///
16-
/// An `Action` represents a discrete documentation task; it takes options and inputs,
17-
/// performs its work, reports any problems it encounters, and outputs it generates.
18-
public protocol Action {
16+
/// An action represents a discrete documentation task; it takes options and inputs, performs its work, reports any problems it encounters, and outputs it generates.
17+
package protocol AsyncAction {
1918
/// Performs the action and returns an ``ActionResult``.
20-
mutating func perform(logHandle: LogHandle) throws -> ActionResult
19+
mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult
2120
}
2221

23-
/// An action for which you can optionally customize the documentation context.
24-
public protocol RecreatingContext: Action {
25-
/// A closure that an action calls with the action's context for built documentation,
26-
/// before the action performs work.
27-
///
28-
/// Use this closure to set the action's context to a certain state before the action runs.
29-
var setupContext: ((inout DocumentationContext) -> Void)? { get set }
22+
package extension AsyncAction {
23+
mutating func perform(logHandle: LogHandle) async throws -> ActionResult {
24+
var logHandle = logHandle
25+
return try await perform(logHandle: &logHandle)
26+
}
3027
}
28+

Sources/SwiftDocCUtilities/Action/Actions/Action+MoveOutput.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import Foundation
1212
import SwiftDocC
1313

14-
extension Action {
14+
extension AsyncAction {
1515

1616
/// Creates a new unique directory, with an optional template, inside of specified container.
1717
/// - Parameters:

Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift

Lines changed: 33 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Foundation
1414
import SwiftDocC
1515

1616
/// An action that converts a source bundle into compiled documentation.
17-
public struct ConvertAction: Action, RecreatingContext {
17+
public struct ConvertAction: AsyncAction {
1818
enum Error: DescribedError {
1919
case doesNotContainBundle(url: URL)
2020
case cancelPending
@@ -59,12 +59,6 @@ public struct ConvertAction: Action, RecreatingContext {
5959
private var fileManager: FileManagerProtocol
6060
private let temporaryDirectory: URL
6161

62-
public var setupContext: ((inout DocumentationContext) -> Void)? {
63-
didSet {
64-
converter.setupContext = setupContext
65-
}
66-
}
67-
6862
var converter: DocumentationConverter
6963

7064
private var durationMetric: Benchmark.Duration?
@@ -239,52 +233,32 @@ public struct ConvertAction: Action, RecreatingContext {
239233
dataProvider: dataProvider,
240234
bundleDiscoveryOptions: bundleDiscoveryOptions,
241235
sourceRepository: sourceRepository,
242-
isCancelled: isCancelled,
243236
diagnosticEngine: self.diagnosticEngine,
244237
experimentalModifyCatalogWithGeneratedCuration: experimentalModifyCatalogWithGeneratedCuration
245238
)
246239
}
247-
248-
/// `true` if the convert action is cancelled.
249-
private let isCancelled = Synchronized<Bool>(false)
250240

251-
/// `true` if the convert action is currently running.
252-
let isPerforming = Synchronized<Bool>(false)
253-
254-
/// A block to execute when conversion has finished.
255-
/// It's used as a "future" for when the action is cancelled.
256-
var didPerformFuture: (()->Void)?
257-
258-
/// A block to execute when conversion has started.
259-
var willPerformFuture: (()->Void)?
260-
261-
/// Cancels the action.
262-
///
263-
/// The method blocks until the action has completed cancelling.
264-
mutating func cancel() throws {
265-
/// If the action is not running, there is nothing to cancel
266-
guard isPerforming.sync({ $0 }) == true else { return }
267-
268-
/// If the action is already cancelled throw `cancelPending`.
269-
if isCancelled.sync({ $0 }) == true {
270-
throw Error.cancelPending
271-
}
272-
273-
/// Set the cancelled flag.
274-
isCancelled.sync({ $0 = true })
275-
276-
/// Wait for the `perform(logHandle:)` method to call `didPerformFuture()`
277-
let waitGroup = DispatchGroup()
278-
waitGroup.enter()
279-
didPerformFuture = {
280-
waitGroup.leave()
281-
}
282-
waitGroup.wait()
283-
}
241+
/// A block of extra work that tests perform to affect the time it takes to convert documentation
242+
var _extraTestWork: (() async -> Void)?
284243

285244
/// Converts each eligible file from the source documentation bundle,
286245
/// saves the results in the given output alongside the template files.
287-
mutating public func perform(logHandle: LogHandle) throws -> ActionResult {
246+
public mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult {
247+
// FIXME: Use `defer` again when the asynchronous defer-statement miscompilation (rdar://137774949) is fixed.
248+
let temporaryFolder = try createTempFolder(with: htmlTemplateDirectory)
249+
do {
250+
let result = try await _perform(logHandle: &logHandle, temporaryFolder: temporaryFolder)
251+
diagnosticEngine.flush()
252+
try? fileManager.removeItem(at: temporaryFolder)
253+
return result
254+
} catch {
255+
diagnosticEngine.flush()
256+
try? fileManager.removeItem(at: temporaryFolder)
257+
throw error
258+
}
259+
}
260+
261+
private mutating func _perform(logHandle: inout LogHandle, temporaryFolder: URL) async throws -> ActionResult {
288262
// Add the default diagnostic console writer now that we know what log handle it should write to.
289263
if !diagnosticEngine.hasConsumer(matching: { $0 is DiagnosticConsoleWriter }) {
290264
diagnosticEngine.add(
@@ -302,20 +276,19 @@ public struct ConvertAction: Action, RecreatingContext {
302276
var postConversionProblems: [Problem] = []
303277
let totalTimeMetric = benchmark(begin: Benchmark.Duration(id: "convert-total-time"))
304278

305-
// While running this method keep the `isPerforming` flag up.
306-
isPerforming.sync({ $0 = true })
307-
willPerformFuture?()
308-
defer {
309-
didPerformFuture?()
310-
isPerforming.sync({ $0 = false })
311-
diagnosticEngine.flush()
312-
}
279+
// FIXME: Use `defer` here again when the miscompilation of this asynchronous defer-statement (rdar://137774949) is fixed.
280+
// defer {
281+
// diagnosticEngine.flush()
282+
// }
313283

314-
let temporaryFolder = try createTempFolder(with: htmlTemplateDirectory)
284+
// Run any extra work that the test may have injected
285+
await _extraTestWork?()
315286

316-
defer {
317-
try? fileManager.removeItem(at: temporaryFolder)
318-
}
287+
// FIXME: Use `defer` here again when the miscompilation of this asynchronous defer-statement (rdar://137774949) is fixed.
288+
// let temporaryFolder = try createTempFolder(with: htmlTemplateDirectory)
289+
// defer {
290+
// try? fileManager.removeItem(at: temporaryFolder)
291+
// }
319292

320293
let indexHTML: URL?
321294
if let htmlTemplateDirectory {
@@ -433,14 +406,13 @@ public struct ConvertAction: Action, RecreatingContext {
433406
// If we're building a navigation index, finalize the process and collect encountered problems.
434407
if let indexer {
435408
let finalizeNavigationIndexMetric = benchmark(begin: Benchmark.Duration(id: "finalize-navigation-index"))
436-
defer {
437-
benchmark(end: finalizeNavigationIndexMetric)
438-
}
439409

440410
// Always emit a JSON representation of the index but only emit the LMDB
441411
// index if the user has explicitly opted in with the `--emit-lmdb-index` flag.
442412
let indexerProblems = indexer.finalize(emitJSON: true, emitLMDB: buildLMDBIndex)
443413
postConversionProblems.append(contentsOf: indexerProblems)
414+
415+
benchmark(end: finalizeNavigationIndexMetric)
444416
}
445417

446418
// Output to the user the problems encountered during the convert process
@@ -451,7 +423,7 @@ public struct ConvertAction: Action, RecreatingContext {
451423
benchmark(end: totalTimeMetric)
452424

453425
if !didEncounterError {
454-
let coverageResults = try coverageAction.perform(logHandle: logHandle)
426+
let coverageResults = try await coverageAction.perform(logHandle: &logHandle)
455427
postConversionProblems.append(contentsOf: coverageResults.problems)
456428
}
457429

Sources/SwiftDocCUtilities/Action/Actions/CoverageAction.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,24 @@ import Foundation
1212
import SwiftDocC
1313

1414
/// An action that creates documentation coverage info for a documentation bundle.
15-
public struct CoverageAction: Action {
16-
internal init(
15+
public struct CoverageAction: AsyncAction {
16+
init(
1717
documentationCoverageOptions: DocumentationCoverageOptions,
1818
workingDirectory: URL,
19-
fileManager: FileManagerProtocol) {
19+
fileManager: FileManagerProtocol
20+
) {
2021
self.documentationCoverageOptions = documentationCoverageOptions
2122
self.workingDirectory = workingDirectory
2223
self.fileManager = fileManager
2324
}
2425

2526
public let documentationCoverageOptions: DocumentationCoverageOptions
26-
internal let workingDirectory: URL
27+
let workingDirectory: URL
2728
private let fileManager: FileManagerProtocol
2829

29-
public mutating func perform(logHandle: LogHandle) throws -> ActionResult {
30+
public mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult {
3031
switch documentationCoverageOptions.level {
3132
case .brief, .detailed:
32-
var logHandle = logHandle
3333
print(" --- Experimental coverage output enabled. ---", to: &logHandle)
3434

3535
let summaryString = try CoverageDataEntry.generateSummary(

Sources/SwiftDocCUtilities/Action/Actions/EmitGeneratedCurationAction.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Foundation
1212
import SwiftDocC
1313

1414
/// An action that emits documentation extension files that reflect the auto-generated curation.
15-
struct EmitGeneratedCurationAction: Action {
15+
struct EmitGeneratedCurationAction: AsyncAction {
1616
let catalogURL: URL?
1717
let additionalSymbolGraphDirectory: URL?
1818
let outputURL: URL
@@ -41,7 +41,7 @@ struct EmitGeneratedCurationAction: Action {
4141
self.fileManager = fileManager
4242
}
4343

44-
mutating func perform(logHandle: LogHandle) throws -> ActionResult {
44+
mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult {
4545
let workspace = DocumentationWorkspace()
4646
let context = try DocumentationContext(dataProvider: workspace)
4747

Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Foundation
1212
import SwiftDocC
1313

1414
/// An action that creates an index of a documentation bundle.
15-
public struct IndexAction: Action {
15+
public struct IndexAction: AsyncAction {
1616
let rootURL: URL
1717
let outputURL: URL
1818
let bundleIdentifier: String
@@ -22,8 +22,7 @@ public struct IndexAction: Action {
2222
private var dataProvider: LocalFileSystemDataProvider!
2323

2424
/// Initializes the action with the given validated options, creates or uses the given action workspace & context.
25-
public init(documentationBundleURL: URL, outputURL: URL, bundleIdentifier: String, diagnosticEngine: DiagnosticEngine = .init()) throws
26-
{
25+
public init(documentationBundleURL: URL, outputURL: URL, bundleIdentifier: String, diagnosticEngine: DiagnosticEngine = .init()) throws {
2726
// Initialize the action context.
2827
self.rootURL = documentationBundleURL
2928
self.outputURL = outputURL
@@ -35,7 +34,7 @@ public struct IndexAction: Action {
3534

3635
/// Converts each eligible file from the source documentation bundle,
3736
/// saves the results in the given output alongside the template files.
38-
mutating public func perform(logHandle: LogHandle) throws -> ActionResult {
37+
mutating public func perform(logHandle: inout LogHandle) async throws -> ActionResult {
3938
let problems = try buildIndex()
4039
diagnosticEngine.emit(problems)
4140

0 commit comments

Comments
 (0)