@@ -29,14 +29,13 @@ package enum ConvertActionConverter {
2929 /// - sourceRepository: The source repository where the documentation's sources are hosted.
3030 /// - emitDigest: Whether the conversion should pass additional metadata output––such as linkable entities information, indexing information, or asset references by asset type––to the consumer.
3131 /// - documentationCoverageOptions: The level of experimental documentation coverage information that the conversion should pass to the consumer.
32- /// - Returns: A list of problems that occurred during the conversion (excluding the problems that the context already encountered).
3332 package static func convert(
3433 context: DocumentationContext ,
3534 outputConsumer: some ConvertOutputConsumer & ExternalNodeConsumer ,
3635 sourceRepository: SourceRepository ? ,
3736 emitDigest: Bool ,
3837 documentationCoverageOptions: DocumentationCoverageOptions
39- ) throws -> [ Problem ] {
38+ ) async throws {
4039 let signposter = Self . signposter
4140
4241 defer {
@@ -54,7 +53,7 @@ package enum ConvertActionConverter {
5453 if emitDigest {
5554 try ( _Deprecated ( outputConsumer) as ( any _DeprecatedConsumeProblemsAccess ) ) . _consume ( problems: context. problems)
5655 }
57- return [ ]
56+ return
5857 }
5958
6059 // Precompute the render context
@@ -71,36 +70,7 @@ package enum ConvertActionConverter {
7170 renderContext: renderContext,
7271 sourceRepository: sourceRepository
7372 )
74-
75- // Arrays to gather additional metadata if `emitDigest` is `true`.
76- var indexingRecords = [ IndexingRecord] ( )
77- var linkSummaries = [ LinkDestinationSummary] ( )
78- var assets = [ RenderReferenceType : [ any RenderReference ] ] ( )
79- var coverageInfo = [ CoverageDataEntry] ( )
80- let coverageFilterClosure = documentationCoverageOptions. generateFilterClosure ( )
81-
82- // An inner function to gather problems for errors encountered during the conversion.
83- //
84- // These problems only represent unexpected thrown errors and aren't particularly user-facing.
85- // For now we emit them as diagnostics because `DocumentationConverter.convert(outputConsumer:)` (which this replaced) used to do that.
86- //
87- // FIXME: In the future we could simplify this control flow by not catching these errors and turning them into diagnostics.
88- // Since both error-level diagnostics and thrown errors fail the documentation build,
89- // the only practical different this would have is that we stop on the first unexpected error instead of processing all pages and gathering all unexpected errors.
90- func recordProblem( from error: any Swift . Error , in problems: inout [ Problem ] , withIdentifier identifier: String ) {
91- let problem = Problem ( diagnostic: Diagnostic (
92- severity: . error,
93- identifier: " org.swift.docc.documentation-converter. \( identifier) " ,
94- summary: error. localizedDescription
95- ) , possibleSolutions: [ ] )
96-
97- context. diagnosticEngine. emit ( problem)
98- problems. append ( problem)
99- }
100-
101- let resultsSyncQueue = DispatchQueue ( label: " Convert Serial Queue " , qos: . unspecified, attributes: [ ] )
102- let resultsGroup = DispatchGroup ( )
103-
73+
10474 // Consume external links and add them into the sidebar.
10575 for externalLink in context. externalCache {
10676 // Here we're associating the external node with the **current** bundle's bundle ID.
@@ -112,112 +82,112 @@ package enum ConvertActionConverter {
11282
11383 let renderSignpostHandle = signposter. beginInterval ( " Render " , id: signposter. makeSignpostID ( ) , " Render \( context. knownPages. count) pages " )
11484
115- var conversionProblems : [ Problem ] = context. knownPages. concurrentPerform { identifier, results in
116- // If cancelled skip all concurrent conversion work in this block.
117- guard !Task. isCancelled else { return }
85+ // Render all pages and gather their supplementary "digest" information if enabled.
86+ let supplementaryRenderInfo = try await withThrowingTaskGroup ( of: SupplementaryRenderInformation . self) { taskGroup in
87+ let coverageFilterClosure = documentationCoverageOptions. generateFilterClosure ( )
88+ // Iterate over all the known pages in chunks
89+ var remaining = context. knownPages [ ... ]
11890
119- // Wrap JSON encoding in an autorelease pool to avoid retaining the autoreleased ObjC objects returned by `JSONSerialization`
120- autoreleasepool {
121- do {
122- let entity = try context. entity ( with: identifier)
123-
124- guard let renderNode = converter. renderNode ( for: entity) else {
125- // No render node was produced for this entity, so just skip it.
126- return
127- }
91+ let numberOfBatches = ProcessInfo . processInfo. processorCount * 4
92+ let numberOfElementsPerTask = Int ( Double ( remaining. count) / Double( numberOfBatches) + 1 )
93+
94+ while !remaining. isEmpty {
95+ let slice = remaining. prefix ( numberOfElementsPerTask)
96+ remaining = remaining. dropFirst ( numberOfElementsPerTask)
97+
98+ // Start work of one slice of the known pages
99+ taskGroup. addTask {
100+ var supplementaryRenderInfo = SupplementaryRenderInformation ( )
128101
129- try outputConsumer. consume ( renderNode: renderNode)
102+ for identifier in slice {
103+ try autoreleasepool {
104+ let entity = try context. entity ( with: identifier)
105+
106+ guard let renderNode = converter. renderNode ( for: entity) else {
107+ // No render node was produced for this entity, so just skip it.
108+ return
109+ }
110+
111+ try outputConsumer. consume ( renderNode: renderNode)
130112
131- switch documentationCoverageOptions. level {
132- case . detailed, . brief:
133- let coverageEntry = try CoverageDataEntry (
134- documentationNode: entity,
135- renderNode: renderNode,
136- context: context
137- )
138- if coverageFilterClosure ( coverageEntry) {
139- resultsGroup. async ( queue: resultsSyncQueue) {
140- coverageInfo. append ( coverageEntry)
113+ switch documentationCoverageOptions. level {
114+ case . detailed, . brief:
115+ let coverageEntry = try CoverageDataEntry (
116+ documentationNode: entity,
117+ renderNode: renderNode,
118+ context: context
119+ )
120+ if coverageFilterClosure ( coverageEntry) {
121+ supplementaryRenderInfo. coverageInfo. append ( coverageEntry)
122+ }
123+ case . none:
124+ break
125+ }
126+
127+ if emitDigest {
128+ let nodeLinkSummaries = entity. externallyLinkableElementSummaries ( context: context, renderNode: renderNode, includeTaskGroups: true )
129+ let nodeIndexingRecords = try renderNode. indexingRecords ( onPage: identifier)
130+
131+ supplementaryRenderInfo. assets. merge ( renderNode. assetReferences, uniquingKeysWith: + )
132+ supplementaryRenderInfo. linkSummaries. append ( contentsOf: nodeLinkSummaries)
133+ supplementaryRenderInfo. indexingRecords. append ( contentsOf: nodeIndexingRecords)
134+ } else if FeatureFlags . current. isExperimentalLinkHierarchySerializationEnabled {
135+ let nodeLinkSummaries = entity. externallyLinkableElementSummaries ( context: context, renderNode: renderNode, includeTaskGroups: false )
136+
137+ supplementaryRenderInfo. linkSummaries. append ( contentsOf: nodeLinkSummaries)
141138 }
142139 }
143- case . none:
144- break
145140 }
146141
147- if emitDigest {
148- let nodeLinkSummaries = entity. externallyLinkableElementSummaries ( context: context, renderNode: renderNode, includeTaskGroups: true )
149- let nodeIndexingRecords = try renderNode. indexingRecords ( onPage: identifier)
150-
151- resultsGroup. async ( queue: resultsSyncQueue) {
152- assets. merge ( renderNode. assetReferences, uniquingKeysWith: + )
153- linkSummaries. append ( contentsOf: nodeLinkSummaries)
154- indexingRecords. append ( contentsOf: nodeIndexingRecords)
155- }
156- } else if FeatureFlags . current. isExperimentalLinkHierarchySerializationEnabled {
157- let nodeLinkSummaries = entity. externallyLinkableElementSummaries ( context: context, renderNode: renderNode, includeTaskGroups: false )
158-
159- resultsGroup. async ( queue: resultsSyncQueue) {
160- linkSummaries. append ( contentsOf: nodeLinkSummaries)
161- }
162- }
163- } catch {
164- recordProblem ( from: error, in: & results, withIdentifier: " render-node " )
142+ return supplementaryRenderInfo
165143 }
166144 }
145+
146+ var aggregateSupplementaryRenderInfo = SupplementaryRenderInformation ( )
147+
148+ for try await partialInfo in taskGroup {
149+ aggregateSupplementaryRenderInfo. assets. merge ( partialInfo. assets, uniquingKeysWith: + )
150+ aggregateSupplementaryRenderInfo. linkSummaries. append ( contentsOf: partialInfo. linkSummaries)
151+ aggregateSupplementaryRenderInfo. indexingRecords. append ( contentsOf: partialInfo. indexingRecords)
152+ aggregateSupplementaryRenderInfo. coverageInfo. append ( contentsOf: partialInfo. coverageInfo)
153+ }
154+
155+ return aggregateSupplementaryRenderInfo
167156 }
168157
169- // Wait for any concurrent updates to complete.
170- resultsGroup. wait ( )
171-
172158 signposter. endInterval ( " Render " , renderSignpostHandle)
173159
174- guard !Task. isCancelled else { return [ ] }
160+ guard !Task. isCancelled else { return }
175161
176162 // Write various metadata
177163 if emitDigest {
178- signposter. withIntervalSignpost ( " Emit digest " , id: signposter. makeSignpostID ( ) ) {
179- do {
180- try outputConsumer. consume ( linkableElementSummaries: linkSummaries)
181- try outputConsumer. consume ( indexingRecords: indexingRecords)
182- try outputConsumer. consume ( assets: assets)
183- } catch {
184- recordProblem ( from: error, in: & conversionProblems, withIdentifier: " metadata " )
185- }
164+ try signposter. withIntervalSignpost ( " Emit digest " , id: signposter. makeSignpostID ( ) ) {
165+ try outputConsumer. consume ( linkableElementSummaries: supplementaryRenderInfo. linkSummaries)
166+ try outputConsumer. consume ( indexingRecords: supplementaryRenderInfo. indexingRecords)
167+ try outputConsumer. consume ( assets: supplementaryRenderInfo. assets)
186168 }
187169 }
188170
189171 if FeatureFlags . current. isExperimentalLinkHierarchySerializationEnabled {
190- signposter. withIntervalSignpost ( " Serialize link hierarchy " , id: signposter. makeSignpostID ( ) ) {
191- do {
192- let serializableLinkInformation = try context. linkResolver. localResolver. prepareForSerialization ( bundleID: context. inputs. id)
193- try outputConsumer. consume ( linkResolutionInformation: serializableLinkInformation)
194-
195- if !emitDigest {
196- try outputConsumer. consume ( linkableElementSummaries: linkSummaries)
197- }
198- } catch {
199- recordProblem ( from: error, in: & conversionProblems, withIdentifier: " link-resolver " )
172+ try signposter. withIntervalSignpost ( " Serialize link hierarchy " , id: signposter. makeSignpostID ( ) ) {
173+ let serializableLinkInformation = try context. linkResolver. localResolver. prepareForSerialization ( bundleID: context. inputs. id)
174+ try outputConsumer. consume ( linkResolutionInformation: serializableLinkInformation)
175+
176+ if !emitDigest {
177+ try outputConsumer. consume ( linkableElementSummaries: supplementaryRenderInfo. linkSummaries)
200178 }
201179 }
202180 }
203181
204182 if emitDigest {
205- signposter. withIntervalSignpost ( " Emit digest " , id: signposter. makeSignpostID ( ) ) {
206- do {
207- try ( _Deprecated ( outputConsumer) as ( any _DeprecatedConsumeProblemsAccess ) ) . _consume ( problems: context. problems + conversionProblems)
208- } catch {
209- recordProblem ( from: error, in: & conversionProblems, withIdentifier: " problems " )
210- }
183+ try signposter. withIntervalSignpost ( " Emit digest " , id: signposter. makeSignpostID ( ) ) {
184+ try ( _Deprecated ( outputConsumer) as ( any _DeprecatedConsumeProblemsAccess ) ) . _consume ( problems: context. problems)
211185 }
212186 }
213187
214188 switch documentationCoverageOptions. level {
215189 case . detailed, . brief:
216- do {
217- try outputConsumer. consume ( documentationCoverageInfo: coverageInfo)
218- } catch {
219- recordProblem ( from: error, in: & conversionProblems, withIdentifier: " coverage " )
220- }
190+ try outputConsumer. consume ( documentationCoverageInfo: supplementaryRenderInfo. coverageInfo)
221191 case . none:
222192 break
223193 }
@@ -232,7 +202,12 @@ package enum ConvertActionConverter {
232202 benchmark ( add: Benchmark . ExternalTopicsHash ( context: context) )
233203 // Log the peak memory.
234204 benchmark ( add: Benchmark . PeakMemory ( ) )
235-
236- return conversionProblems
237205 }
238206}
207+
208+ private struct SupplementaryRenderInformation {
209+ var indexingRecords = [ IndexingRecord] ( )
210+ var linkSummaries = [ LinkDestinationSummary] ( )
211+ var assets = [ RenderReferenceType : [ any RenderReference ] ] ( )
212+ var coverageInfo = [ CoverageDataEntry] ( )
213+ }
0 commit comments