Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap various convert subtasks in signpost intervals #1112

Merged
merged 9 commits into from
Dec 4, 2024
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ let swiftSettings: [SwiftSetting] = [
let package = Package(
name: "SwiftDocC",
platforms: [
.macOS(.v10_15),
.iOS(.v13)
.macOS(.v12),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any implication here for who can run DocC? Or is DocC already associated with a certain version of Swift that precludes older versions of MacOS and iOS anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but not in practice.

Since most developers use the DocC that comes with Xcode—rather than build DocC from source—this doesn't change what version of macOS they need to use because Xcode requires a newer version of macOS than what DocC does. Xcode 16.1 (the latest release as of this writing) requires macOS 14.5 and the last release of Xcode to support macOS 12 was Xcode 13.4.1.

Linux or Windows are unaffected by this. It is possible that someone would build DocC from source and run it directly (not through Xcode or Swift Package Manager) on older macOS versions in which case they'd need to pin to the 6.1 release branch for the last version of DocC to support those macOS versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@franklinsch and I talked about this in the past and didn't have any concerns about increasing these platform versions.

.iOS(.v15)
],
products: [
.library(
Expand Down
23 changes: 21 additions & 2 deletions Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@

import Foundation

#if canImport(os)
import os
#endif

package enum ConvertActionConverter {
#if canImport(os)
static package let signposter = OSSignposter(subsystem: "org.swift.docc", category: "Convert")
#else
static package let signposter = NoOpSignposterShim()
#endif

/// Converts the documentation bundle in the given context and passes its output to a given consumer.
///
Expand All @@ -30,8 +39,12 @@ package enum ConvertActionConverter {
emitDigest: Bool,
documentationCoverageOptions: DocumentationCoverageOptions
) throws -> [Problem] {
let signposter = Self.signposter

defer {
context.diagnosticEngine.flush()
signposter.withIntervalSignpost("Display diagnostics", id: signposter.makeSignpostID()) {
context.diagnosticEngine.flush()
}
}

let processingDurationMetric = benchmark(begin: Benchmark.Duration(id: "documentation-processing"))
Expand All @@ -47,7 +60,9 @@ package enum ConvertActionConverter {
}

// Precompute the render context
let renderContext = RenderContext(documentationContext: context, bundle: bundle)
let renderContext = signposter.withIntervalSignpost("Build RenderContext", id: signposter.makeSignpostID()) {
RenderContext(documentationContext: context, bundle: bundle)
}
try outputConsumer.consume(renderReferenceStore: renderContext.store)

// Copy images, sample files, and other static assets.
Expand Down Expand Up @@ -89,6 +104,8 @@ package enum ConvertActionConverter {
let resultsSyncQueue = DispatchQueue(label: "Convert Serial Queue", qos: .unspecified, attributes: [])
let resultsGroup = DispatchGroup()

let renderSignpostHandle = signposter.beginInterval("Render", id: signposter.makeSignpostID(), "Render \(context.knownPages.count) pages")

var conversionProblems: [Problem] = context.knownPages.concurrentPerform { identifier, results in
// If cancelled skip all concurrent conversion work in this block.
guard !Task.isCancelled else { return }
Expand Down Expand Up @@ -146,6 +163,8 @@ package enum ConvertActionConverter {
// Wait for any concurrent updates to complete.
resultsGroup.wait()

signposter.endInterval("Render", renderSignpostHandle)
d-ronnqvist marked this conversation as resolved.
Show resolved Hide resolved

guard !Task.isCancelled else { return [] }

// Write various metadata
Expand Down
82 changes: 63 additions & 19 deletions Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public typealias BundleIdentifier = String
/// - ``parents(of:)``
///
public class DocumentationContext {
private let signposter = ConvertActionConverter.signposter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we pass this into the initializer? ...instead of silently coupling this to ConvertActionConverter like this?

Or if you'd prefer to avoid the noise in all the initializers, we could use a global, singleton class/value instead of referring to ConvertActionConverter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very much an implementation detail so it shouldn't show up in initializers or other API. A global would be one option but preferred to namespace it under a type instead of putting that information in the variable name.


/// An error that's encountered while interacting with a ``SwiftDocC/DocumentationContext``.
public enum ContextError: DescribedError {
Expand Down Expand Up @@ -563,6 +564,11 @@ public class DocumentationContext {
Attempt to resolve links in curation-only documentation, converting any ``TopicReferences`` from `.unresolved` to `.resolved` where possible.
*/
private func resolveLinks(curatedReferences: Set<ResolvedTopicReference>, bundle: DocumentationBundle) {
let signpostHandle = signposter.beginInterval("Resolve links", id: signposter.makeSignpostID())
defer {
signposter.endInterval("Resolve links", signpostHandle)
}

let references = Array(curatedReferences)
let results = Synchronized<[LinkResolveResult]>([])
results.sync({ $0.reserveCapacity(references.count) })
Expand Down Expand Up @@ -708,6 +714,11 @@ public class DocumentationContext {
tutorialArticles: [SemanticResult<TutorialArticle>],
bundle: DocumentationBundle
) {
let signpostHandle = signposter.beginInterval("Resolve links", id: signposter.makeSignpostID())
defer {
signposter.endInterval("Resolve links", signpostHandle)
}

let sourceLanguages = soleRootModuleReference.map { self.sourceLanguages(for: $0) } ?? [.swift]

// Tutorial table-of-contents
Expand Down Expand Up @@ -1147,6 +1158,11 @@ public class DocumentationContext {
) throws {
// Making sure that we correctly let decoding memory get released, do not remove the autorelease pool.
try autoreleasepool {
let signpostHandle = signposter.beginInterval("Register symbols", id: signposter.makeSignpostID())
defer {
signposter.endInterval("Register symbols", signpostHandle)
}

/// We need only unique relationships so we'll collect them in a set.
var combinedRelationshipsBySelector = [UnifiedSymbolGraph.Selector: Set<SymbolGraph.Relationship>]()
/// Also track the unique relationships across all languages and platforms
Expand All @@ -1157,7 +1173,9 @@ public class DocumentationContext {
var moduleReferences = [String: ResolvedTopicReference]()

// Build references for all symbols in all of this module's symbol graphs.
let symbolReferences = linkResolver.localResolver.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self)
let symbolReferences = signposter.withIntervalSignpost("Disambiguate references") {
linkResolver.localResolver.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self)
}

// Set the index and cache storage capacity to avoid ad-hoc storage resizing.
documentationCache.reserveCapacity(symbolReferences.count)
Expand Down Expand Up @@ -1223,7 +1241,9 @@ public class DocumentationContext {
let moduleSymbolReference = SymbolReference(moduleName, interfaceLanguages: moduleInterfaceLanguages, defaultSymbol: moduleSymbol)
moduleReference = ResolvedTopicReference(symbolReference: moduleSymbolReference, moduleName: moduleName, bundle: bundle)

addSymbolsToTopicGraph(symbolGraph: unifiedSymbolGraph, url: fileURL, symbolReferences: symbolReferences, moduleReference: moduleReference)
signposter.withIntervalSignpost("Add symbols to topic graph", id: signposter.makeSignpostID()) {
addSymbolsToTopicGraph(symbolGraph: unifiedSymbolGraph, url: fileURL, symbolReferences: symbolReferences, moduleReference: moduleReference)
}

// For inherited symbols we remove the source docs (if inheriting docs is disabled) before creating their documentation nodes.
for (_, relationships) in unifiedSymbolGraph.relationshipsByLanguage {
Expand Down Expand Up @@ -1375,15 +1395,17 @@ public class DocumentationContext {
)

// Parse and prepare the nodes' content concurrently.
let updatedNodes = Array(documentationCache.symbolReferences).concurrentMap { finalReference in
// Match the symbol's documentation extension and initialize the node content.
let match = uncuratedDocumentationExtensions[finalReference]
let updatedNode = nodeWithInitializedContent(reference: finalReference, match: match)

return ((
node: updatedNode,
matchedArticleURL: match?.source
))
let updatedNodes = signposter.withIntervalSignpost("Parse symbol markup", id: signposter.makeSignpostID()) {
Array(documentationCache.symbolReferences).concurrentMap { finalReference in
// Match the symbol's documentation extension and initialize the node content.
let match = uncuratedDocumentationExtensions[finalReference]
let updatedNode = nodeWithInitializedContent(reference: finalReference, match: match)

return ((
node: updatedNode,
matchedArticleURL: match?.source
))
}
}

// Update cache with up-to-date nodes
Expand Down Expand Up @@ -2177,9 +2199,16 @@ public class DocumentationContext {
)

do {
try symbolGraphLoader.loadAll()
let pathHierarchy = PathHierarchy(symbolGraphLoader: symbolGraphLoader, bundleName: urlReadablePath(bundle.displayName), knownDisambiguatedPathComponents: configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents)
hierarchyBasedResolver = PathHierarchyBasedLinkResolver(pathHierarchy: pathHierarchy)
try signposter.withIntervalSignpost("Load symbols", id: signposter.makeSignpostID()) {
try symbolGraphLoader.loadAll()
}
hierarchyBasedResolver = signposter.withIntervalSignpost("Build PathHierarchy", id: signposter.makeSignpostID()) {
PathHierarchyBasedLinkResolver(pathHierarchy: PathHierarchy(
symbolGraphLoader: symbolGraphLoader,
bundleName: urlReadablePath(bundle.displayName),
knownDisambiguatedPathComponents: configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents
))
}
} catch {
// Pipe the error out of the dispatch queue.
discoveryError.sync({
Expand All @@ -2191,7 +2220,9 @@ public class DocumentationContext {
// First, all the resources are added since they don't reference anything else.
discoveryGroup.async(queue: discoveryQueue) { [unowned self] in
do {
try self.registerMiscResources(from: bundle)
try signposter.withIntervalSignpost("Load resources", id: signposter.makeSignpostID()) {
try self.registerMiscResources(from: bundle)
}
} catch {
// Pipe the error out of the dispatch queue.
discoveryError.sync({
Expand All @@ -2215,7 +2246,9 @@ public class DocumentationContext {

discoveryGroup.async(queue: discoveryQueue) { [unowned self] in
do {
result = try self.registerDocuments(from: bundle)
result = try signposter.withIntervalSignpost("Load documents", id: signposter.makeSignpostID()) {
try self.registerDocuments(from: bundle)
}
} catch {
// Pipe the error out of the dispatch queue.
discoveryError.sync({
Expand All @@ -2226,7 +2259,9 @@ public class DocumentationContext {

discoveryGroup.async(queue: discoveryQueue) { [unowned self] in
do {
try linkResolver.loadExternalResolvers(dependencyArchives: configuration.externalDocumentationConfiguration.dependencyArchives)
try signposter.withIntervalSignpost("Load external resolvers", id: signposter.makeSignpostID()) {
try linkResolver.loadExternalResolvers(dependencyArchives: configuration.externalDocumentationConfiguration.dependencyArchives)
}
} catch {
// Pipe the error out of the dispatch queue.
discoveryError.sync({
Expand Down Expand Up @@ -2361,7 +2396,9 @@ public class DocumentationContext {
try shouldContinueRegistration()

// Fourth, automatically curate all symbols that haven't been curated manually
let automaticallyCurated = autoCurateSymbolsInTopicGraph()
let automaticallyCurated = signposter.withIntervalSignpost("Auto-curate symbols ", id: signposter.makeSignpostID()) {
autoCurateSymbolsInTopicGraph()
}

// Crawl the rest of the symbols that haven't been crawled so far in hierarchy pre-order.
allCuratedReferences = try crawlSymbolCuration(in: automaticallyCurated.map(\.symbol), bundle: bundle, initial: allCuratedReferences)
Expand Down Expand Up @@ -2407,7 +2444,9 @@ public class DocumentationContext {
}

// Seventh, the complete topic graph—with all nodes and all edges added—is analyzed.
topicGraphGlobalAnalysis()
signposter.withIntervalSignpost("Analyze topic graph", id: signposter.makeSignpostID()) {
topicGraphGlobalAnalysis()
}

preResolveModuleNames()
}
Expand Down Expand Up @@ -2606,6 +2645,11 @@ public class DocumentationContext {
/// - Returns: The references of all the symbols that were curated.
@discardableResult
func crawlSymbolCuration(in references: [ResolvedTopicReference], bundle: DocumentationBundle, initial: Set<ResolvedTopicReference> = []) throws -> Set<ResolvedTopicReference> {
let signpostHandle = signposter.beginInterval("Curate symbols", id: signposter.makeSignpostID())
defer {
signposter.endInterval("Curate symbols", signpostHandle)
}

var crawler = DocumentationCurator(in: self, bundle: bundle, initial: initial)

for reference in references {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ struct SymbolGraphLoader {
///
/// - Throws: If loading and decoding any of the symbol graph files throws, this method re-throws one of the encountered errors.
mutating func loadAll() throws {
let signposter = ConvertActionConverter.signposter

let loadingLock = Lock()

var loadedGraphs = [URL: (usesExtensionSymbolFormat: Bool?, graph: SymbolKit.SymbolGraph)]()
Expand Down Expand Up @@ -118,6 +120,8 @@ struct SymbolGraphLoader {
}
#endif

let numberOfSymbolGraphs = bundle.symbolGraphURLs.count
let decodeSignpostHandle = signposter.beginInterval("Decode symbol graphs", id: signposter.makeSignpostID(), "Decode \(numberOfSymbolGraphs) symbol graphs")
switch decodingStrategy {
case .concurrentlyAllFiles:
// Concurrently load and decode all symbol graphs
Expand All @@ -127,12 +131,14 @@ struct SymbolGraphLoader {
// Serially load and decode all symbol graphs, each one in concurrent batches.
bundle.symbolGraphURLs.forEach(loadGraphAtURL)
}
signposter.endInterval("Decode symbol graphs", decodeSignpostHandle)
d-ronnqvist marked this conversation as resolved.
Show resolved Hide resolved

// define an appropriate merging strategy based on the graph formats
let foundGraphUsingExtensionSymbolFormat = loadedGraphs.values.map(\.usesExtensionSymbolFormat).contains(true)

let usingExtensionSymbolFormat = foundGraphUsingExtensionSymbolFormat


let mergeSignpostHandle = signposter.beginInterval("Build unified symbol graph", id: signposter.makeSignpostID())
let graphLoader = GraphCollector(extensionGraphAssociationStrategy: usingExtensionSymbolFormat ? .extendingGraph : .extendedGraph)

// feed the loaded graphs into the `graphLoader`
Expand All @@ -150,6 +156,7 @@ struct SymbolGraphLoader {
(self.unifiedGraphs, self.graphLocations) = graphLoader.finishLoading(
createOverloadGroups: FeatureFlags.current.isExperimentalOverloadedSymbolPresentationEnabled
)
signposter.endInterval("Build unified symbol graph", mergeSignpostHandle)
d-ronnqvist marked this conversation as resolved.
Show resolved Hide resolved

for var unifiedGraph in unifiedGraphs.values {
var defaultUnavailablePlatforms = [PlatformName]()
Expand Down
13 changes: 4 additions & 9 deletions Sources/SwiftDocC/Servers/FileServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import SymbolKit
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
#if canImport(UniformTypeIdentifiers)
import UniformTypeIdentifiers
#endif
#if os(Windows)
import WinSDK
#endif
Expand Down Expand Up @@ -116,15 +119,7 @@ public class FileServer {

#if os(macOS)

let unmanagedFileUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil)
guard let fileUTI = unmanagedFileUTI?.takeRetainedValue() else {
return defaultMimeType
}
guard let mimeType = UTTypeCopyPreferredTagWithClass (fileUTI, kUTTagClassMIMEType)?.takeRetainedValue() else {
return defaultMimeType
}

return (mimeType as NSString) as String
return UTType(filenameExtension: ext)?.preferredMIMEType ?? defaultMimeType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated improvement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was a deprecation warning (see the 7f7b106 commit message) once we increased the platform versions to macOS 12 / iOS 15.


#elseif os(Windows)

Expand Down
Loading