From f10598ed348caa722e70f2d6579216145fb49eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 3 Dec 2024 12:20:00 +0100 Subject: [PATCH 1/7] Add a no-op shim for os.OSSignpost --- .../NoOpSignposterShim.swift | 135 ++++++++++++++++++ .../Utility/NoOpSignposterShimTests.swift | 55 +++++++ 2 files changed, 190 insertions(+) create mode 100644 Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift create mode 100644 Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift diff --git a/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift b/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift new file mode 100644 index 0000000000..ce02bcb917 --- /dev/null +++ b/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift @@ -0,0 +1,135 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +/// A shim for `OSSignposter` that does nothing, except for running the passed interval task. +/// +/// This type allows calling code to avoid using `#if canImport(os)` throughout the implementation. +package struct NoOpSignposterShim : @unchecked Sendable { + package init() {} + + package var isEnabled: Bool { false } + + package struct ID { + static var exclusive = ID() + } + package func makeSignpostID() -> ID { ID() } + + package struct IntervalState {} + + // Without messages + + package func beginInterval(_ name: StaticString, id: ID = .exclusive) -> IntervalState { + IntervalState() + } + package func endInterval(_ name: StaticString, _ state: IntervalState) {} + + package func withIntervalSignpost(_ name: StaticString, id: ID = .exclusive, around task: () throws -> T) rethrows -> T { + try task() + } + + package func emitEvent(_ name: StaticString, id: ID = .exclusive) {} + + // With messages + + package func beginInterval(_ name: StaticString, id: ID = .exclusive, _ message: NoOpLogMessage) -> IntervalState { + self.beginInterval(name, id: id) + } + package func endInterval(_ name: StaticString, _ state: IntervalState, _ message: NoOpLogMessage) {} + + package func withIntervalSignpost(_ name: StaticString, id: ID = .exclusive, _ message: NoOpLogMessage, around task: () throws -> T) rethrows -> T { + try self.withIntervalSignpost(name, id: id, around: task) + } + + package func emitEvent(_ name: StaticString, id: ID = .exclusive, _ message: NoOpLogMessage) {} +} + +// MARK: Message + +package struct NoOpLogMessage: ExpressibleByStringInterpolation, ExpressibleByStringLiteral { + package let interpolation: Interpolation + + package init(stringInterpolation: Interpolation) { + interpolation = stringInterpolation + } + package init(stringLiteral value: String) { + self.init(stringInterpolation: .init(literalCapacity: 0, interpolationCount: 0)) + } + + package struct Interpolation: StringInterpolationProtocol { + package init(literalCapacity: Int, interpolationCount: Int) {} + + package mutating func appendLiteral(_ literal: String) {} + + // Append string + package mutating func appendInterpolation(_ argumentString: @autoclosure @escaping () -> String, align: NoOpLogStringAlignment = .none, privacy: NoOpLogPrivacy = .auto) {} + package mutating func appendInterpolation(_ value: @autoclosure @escaping () -> some CustomStringConvertible, align: NoOpLogStringAlignment = .none, privacy: NoOpLogPrivacy = .auto) {} + + // Append booleans + package mutating func appendInterpolation(_ boolean: @autoclosure @escaping () -> Bool, format: NoOpLogBoolFormat = .truth, privacy: NoOpLogPrivacy = .auto) {} + + // Append integers + package mutating func appendInterpolation(_ number: @autoclosure @escaping () -> some FixedWidthInteger, format: NoOpLogIntegerFormatting = .decimal, align: NoOpLogStringAlignment = .none, privacy: NoOpLogPrivacy = .auto) {} + + // Append float/double + package mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Float, format: NoOpLogFloatFormatting = .fixed, align: NoOpLogStringAlignment = .none, privacy: NoOpLogPrivacy = .auto) {} + package mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Double, format: NoOpLogFloatFormatting = .fixed, align: NoOpLogStringAlignment = .none, privacy: NoOpLogPrivacy = .auto) {} + + // Add more interpolations here as needed + } + + package struct NoOpLogStringAlignment { + package static var none: Self { .init() } + package static func right(columns: @autoclosure @escaping () -> Int) -> Self { .init() } + package static func left(columns: @autoclosure @escaping () -> Int) -> Self { .init() } + } + + package struct NoOpLogPrivacy { + package enum Mask { + case hash, none + } + package static var `public`: Self { .init() } + package static var `private`: Self { .init() } + package static var sensitive: Self { .init() } + package static var auto: Self { .init() } + package static func `private`(mask: Mask) -> Self { .init() } + package static func sensitive(mask: Mask) -> Self { .init() } + package static func auto(mask: Mask) -> Self { .init() } + } + + package enum NoOpLogBoolFormat { + case truth, answer + } + + public struct NoOpLogIntegerFormatting { + package static var decimal: Self { .init() } + package static var hex: Self { .init() } + package static var octal: Self { .init() } + package static func decimal(explicitPositiveSign: Bool = false) -> Self { .init() } + package static func decimal(explicitPositiveSign: Bool = false, minDigits: @autoclosure @escaping () -> Int) -> Self { .init() } + package static func hex(explicitPositiveSign: Bool = false, includePrefix: Bool = false, uppercase: Bool = false) -> Self { .init() } + package static func hex(explicitPositiveSign: Bool = false, includePrefix: Bool = false, uppercase: Bool = false, minDigits: @autoclosure @escaping () -> Int) -> Self { .init() } + package static func octal(explicitPositiveSign: Bool = false, includePrefix: Bool = false, uppercase: Bool = false) -> Self { .init() } + package static func octal(explicitPositiveSign: Bool = false, includePrefix: Bool = false, uppercase: Bool = false, minDigits: @autoclosure @escaping () -> Int) -> Self { .init() } + } + + package struct NoOpLogFloatFormatting { + package static var fixed: Self { .init() } + package static var hex: Self { .init() } + package static var exponential: Self { .init() } + package static var hybrid: Self { .init() } + package static func fixed(precision: @autoclosure @escaping () -> Int, explicitPositiveSign: Bool = false, uppercase: Bool = false) -> Self { .init() } + package static func fixed(explicitPositiveSign: Bool = false, uppercase: Bool = false) -> Self { .init() } + package static func hex(explicitPositiveSign: Bool = false, uppercase: Bool = false) -> Self { .init() } + package static func exponential(precision: @autoclosure @escaping () -> Int, explicitPositiveSign: Bool = false, uppercase: Bool = false) -> Self { .init() } + package static func exponential(explicitPositiveSign: Bool = false, uppercase: Bool = false) -> Self { .init() } + package static func hybrid(precision: @autoclosure @escaping () -> Int, explicitPositiveSign: Bool = false, uppercase: Bool = false) -> Self { .init() } + package static func hybrid(explicitPositiveSign: Bool = false, uppercase: Bool = false) -> Self { .init() } + } +} diff --git a/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift b/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift new file mode 100644 index 0000000000..facb69e04e --- /dev/null +++ b/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift @@ -0,0 +1,55 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation +import SwiftDocC +import XCTest + +final class NoOpSignposterShimTests: XCTestCase { + func testRunsIntervalVoidWork() { + let signposter = NoOpSignposterShim() + + let didPerformWork = expectation(description: "Did perform work") + signposter.withIntervalSignpost("Something") { + didPerformWork.fulfill() + } + + wait(for: [didPerformWork], timeout: 10.0) + } + + func testReturnsIntervalWorkResult() { + let signposter = NoOpSignposterShim() + + let didPerformWork = expectation(description: "Did perform work") + let number = signposter.withIntervalSignpost("Something") { + didPerformWork.fulfill() + return 7 + } + XCTAssertEqual(number, 7) + + wait(for: [didPerformWork], timeout: 10.0) + } + + func testCanAcceptMessageInputs() { + let signposter = NoOpSignposterShim() + + let handle = signposter.beginInterval("Some interval", "Some message") + signposter.endInterval("Some interval", handle, "Another message") + + signposter.emitEvent("Some event", id: signposter.makeSignpostID(), "Some static string") + signposter.emitEvent("Some event", "Some formatted bool \(true, format: .answer)") + signposter.emitEvent("Some event", "Some formatted integer \(12, format: .decimal)") + signposter.emitEvent("Some event", "Some formatted float \(7.0, format: .exponential)") + signposter.emitEvent("Some event", "Some sensitive string \("my secret", privacy: .sensitive(mask: .hash))") + signposter.emitEvent("Some event", "Some non-secret string \("my secret", privacy: .public)") + + signposter.emitEvent("Some event", "Some aligned values \(12, align: .right(columns: 5)) \("some text", align: .left(columns: 10))") + } +} From 31d5c3cee04771f8efebbaf5ec3801e1db602146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 3 Dec 2024 15:08:52 +0100 Subject: [PATCH 2/7] Bump platform versions to be able to use OSSignposter --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 525b3b20f9..1d09552a5f 100644 --- a/Package.swift +++ b/Package.swift @@ -19,8 +19,8 @@ let swiftSettings: [SwiftSetting] = [ let package = Package( name: "SwiftDocC", platforms: [ - .macOS(.v10_15), - .iOS(.v13) + .macOS(.v12), + .iOS(.v15) ], products: [ .library( From 7f7b106dc7f9f53d835f671a05d76c451d03bfb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 3 Dec 2024 15:09:27 +0100 Subject: [PATCH 3/7] Fix deprecation warnings about UTType API --- Sources/SwiftDocC/Servers/FileServer.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftDocC/Servers/FileServer.swift b/Sources/SwiftDocC/Servers/FileServer.swift index 90e79fb6ab..c5c714918a 100644 --- a/Sources/SwiftDocC/Servers/FileServer.swift +++ b/Sources/SwiftDocC/Servers/FileServer.swift @@ -13,6 +13,9 @@ import SymbolKit #if canImport(FoundationNetworking) import FoundationNetworking #endif +#if canImport(UniformTypeIdentifiers) +import UniformTypeIdentifiers +#endif #if os(Windows) import WinSDK #endif @@ -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 #elseif os(Windows) From e0c04f50bb3cce5d2a7548deb8bdaa429d3f6b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 3 Dec 2024 15:24:17 +0100 Subject: [PATCH 4/7] Wrap various major convert tasks in signpost intervals --- .../ConvertActionConverter.swift | 23 +++++- .../Infrastructure/DocumentationContext.swift | 82 ++++++++++++++----- .../Symbol Graph/SymbolGraphLoader.swift | 9 +- .../Actions/Convert/ConvertAction.swift | 52 ++++++++---- 4 files changed, 127 insertions(+), 39 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift b/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift index 7262596249..607302e077 100644 --- a/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift +++ b/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift @@ -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. /// @@ -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")) @@ -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. @@ -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 } @@ -146,6 +163,8 @@ package enum ConvertActionConverter { // Wait for any concurrent updates to complete. resultsGroup.wait() + signposter.endInterval("Render", renderSignpostHandle) + guard !Task.isCancelled else { return [] } // Write various metadata diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index 1bd1bebf07..d76bfa4126 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -83,6 +83,7 @@ public typealias BundleIdentifier = String /// - ``parents(of:)`` /// public class DocumentationContext { + private let signposter = ConvertActionConverter.signposter /// An error that's encountered while interacting with a ``SwiftDocC/DocumentationContext``. public enum ContextError: DescribedError { @@ -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, 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) }) @@ -708,6 +714,11 @@ public class DocumentationContext { tutorialArticles: [SemanticResult], 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 @@ -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]() /// Also track the unique relationships across all languages and platforms @@ -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) @@ -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 { @@ -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 @@ -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({ @@ -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({ @@ -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({ @@ -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({ @@ -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) @@ -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() } @@ -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 = []) throws -> Set { + 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 { diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index 2878285ab0..92fb04d88e 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -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)]() @@ -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 @@ -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) // 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` @@ -150,6 +156,7 @@ struct SymbolGraphLoader { (self.unifiedGraphs, self.graphLocations) = graphLoader.finishLoading( createOverloadGroups: FeatureFlags.current.isExperimentalOverloadedSymbolPresentationEnabled ) + signposter.endInterval("Build unified symbol graph", mergeSignpostHandle) for var unifiedGraph in unifiedGraphs.values { var defaultUnavailablePlatforms = [PlatformName]() diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift index 1326a5c363..206c98f75c 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift @@ -15,6 +15,8 @@ import SwiftDocC /// An action that converts a source bundle into compiled documentation. public struct ConvertAction: AsyncAction { + private let signposter = ConvertActionConverter.signposter + let rootURL: URL? let targetDirectory: URL let htmlTemplateDirectory: URL? @@ -165,12 +167,15 @@ public struct ConvertAction: AsyncAction { } configuration.externalDocumentationConfiguration.dependencyArchives = dependencies - let inputProvider = DocumentationContext.InputsProvider(fileManager: fileManager) - let (bundle, dataProvider) = try inputProvider.inputsAndDataProvider( - startingPoint: documentationBundleURL, - allowArbitraryCatalogDirectories: allowArbitraryCatalogDirectories, - options: bundleDiscoveryOptions - ) + let (bundle, dataProvider) = try signposter.withIntervalSignpost("Discover inputs", id: signposter.makeSignpostID()) { + try DocumentationContext.InputsProvider(fileManager: fileManager) + .inputsAndDataProvider( + startingPoint: documentationBundleURL, + allowArbitraryCatalogDirectories: allowArbitraryCatalogDirectories, + options: bundleDiscoveryOptions + ) + } + self.configuration = configuration self.bundle = bundle @@ -206,6 +211,11 @@ public struct ConvertAction: AsyncAction { } private func _perform(logHandle: inout LogHandle, temporaryFolder: URL) async throws -> (ActionResult, DocumentationContext) { + let convertSignpostHandle = signposter.beginInterval("Convert", id: signposter.makeSignpostID()) + defer { + signposter.endInterval("Convert", convertSignpostHandle) + } + // Add the default diagnostic console writer now that we know what log handle it should write to. if !diagnosticEngine.hasConsumer(matching: { $0 is DiagnosticConsoleWriter }) { diagnosticEngine.add( @@ -278,7 +288,9 @@ public struct ConvertAction: AsyncAction { let indexer = try Indexer(outputURL: temporaryFolder, bundleID: bundle.id) - let context = try DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) + let context = try signposter.withIntervalSignpost("Register", id: signposter.makeSignpostID()) { + try DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) + } let outputConsumer = ConvertFileWritingConsumer( targetFolder: temporaryFolder, @@ -304,14 +316,16 @@ public struct ConvertAction: AsyncAction { let analysisProblems: [Problem] let conversionProblems: [Problem] do { - conversionProblems = try ConvertActionConverter.convert( - bundle: bundle, - context: context, - outputConsumer: outputConsumer, - sourceRepository: sourceRepository, - emitDigest: emitDigest, - documentationCoverageOptions: documentationCoverageOptions - ) + conversionProblems = try signposter.withIntervalSignpost("Process") { + try ConvertActionConverter.convert( + bundle: bundle, + context: context, + outputConsumer: outputConsumer, + sourceRepository: sourceRepository, + emitDigest: emitDigest, + documentationCoverageOptions: documentationCoverageOptions + ) + } analysisProblems = context.problems } catch { if emitDigest { @@ -366,7 +380,9 @@ public struct ConvertAction: AsyncAction { // Always emit a JSON representation of the index but only emit the LMDB // index if the user has explicitly opted in with the `--emit-lmdb-index` flag. - let indexerProblems = indexer.finalize(emitJSON: true, emitLMDB: buildLMDBIndex) + let indexerProblems = signposter.withIntervalSignpost("Finalize navigator index") { + indexer.finalize(emitJSON: true, emitLMDB: buildLMDBIndex) + } postConversionProblems.append(contentsOf: indexerProblems) benchmark(end: finalizeNavigationIndexMetric) @@ -436,6 +452,8 @@ public struct ConvertAction: AsyncAction { } func moveOutput(from: URL, to: URL) throws { - return try Self.moveOutput(from: from, to: to, fileManager: fileManager) + try signposter.withIntervalSignpost("Move output") { + try Self.moveOutput(from: from, to: to, fileManager: fileManager) + } } } From 66dd6bfbe71838bd45e37218d392608958713e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 3 Dec 2024 17:41:00 +0100 Subject: [PATCH 5/7] Add a no-op shim for os.Logger --- .../NoOpSignposterShim.swift | 26 ++++++++++++++++++- .../Utility/NoOpSignposterShimTests.swift | 14 ++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift b/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift index ce02bcb917..4bd5cfd6fa 100644 --- a/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift +++ b/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift @@ -8,7 +8,31 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ -/// A shim for `OSSignposter` that does nothing, except for running the passed interval task. +/// A shim for `os.Logger` that does nothing. +/// +/// This type allows calling code to avoid using `#if canImport(os)` throughout the implementation. +package struct NoOpLoggerShim : @unchecked Sendable { + package init() {} + + package var isEnabled: Bool { false } + + package enum Level { + case `default`, info, debug, error, fault + } + + package func log(_ message: NoOpLogMessage) {} + package func log(level: Level, _ message: NoOpLogMessage) {} + + package func trace(_ message: NoOpLogMessage) {} + package func debug(_ message: NoOpLogMessage) {} + package func info(_ message: NoOpLogMessage) {} + package func warning(_ message: NoOpLogMessage) {} + package func error(_ message: NoOpLogMessage) {} + package func critical(_ message: NoOpLogMessage) {} + package func fault(_ message: NoOpLogMessage) {} +} + +/// A shim for `os.OSSignposter` that does nothing, except for running the passed interval task. /// /// This type allows calling code to avoid using `#if canImport(os)` throughout the implementation. package struct NoOpSignposterShim : @unchecked Sendable { diff --git a/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift b/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift index facb69e04e..66caff16fc 100644 --- a/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift +++ b/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift @@ -38,6 +38,8 @@ final class NoOpSignposterShimTests: XCTestCase { } func testCanAcceptMessageInputs() { + // Note: this test has no assertions. + // It simply verifies that the message interpolations compile let signposter = NoOpSignposterShim() let handle = signposter.beginInterval("Some interval", "Some message") @@ -51,5 +53,17 @@ final class NoOpSignposterShimTests: XCTestCase { signposter.emitEvent("Some event", "Some non-secret string \("my secret", privacy: .public)") signposter.emitEvent("Some event", "Some aligned values \(12, align: .right(columns: 5)) \("some text", align: .left(columns: 10))") + + let logger = NoOpLoggerShim() + + logger.log("Some static string") + logger.info("Some formatted bool \(true, format: .answer)") + logger.debug("Some formatted integer \(12, format: .decimal)") + logger.error("Some formatted float \(7.0, format: .exponential)") + logger.fault("Some sensitive string \("my secret", privacy: .sensitive(mask: .hash))") + logger.log(level: .fault, "Some non-secret string \("my secret", privacy: .public)") + + logger.log(level: .default, "Some aligned values \(12, align: .right(columns: 5)) \("some text", align: .left(columns: 10))") + } } From ac8a510c7b03ae7f163b9ab26f73717eed1992ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 3 Dec 2024 18:03:33 +0100 Subject: [PATCH 6/7] Add support for interpolating errors in no-op log messages --- .../Utility/FoundationExtensions/NoOpSignposterShim.swift | 4 ++++ Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift | 2 ++ 2 files changed, 6 insertions(+) diff --git a/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift b/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift index 4bd5cfd6fa..d0668afc7b 100644 --- a/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift +++ b/Sources/SwiftDocC/Utility/FoundationExtensions/NoOpSignposterShim.swift @@ -105,6 +105,10 @@ package struct NoOpLogMessage: ExpressibleByStringInterpolation, ExpressibleBySt package mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Float, format: NoOpLogFloatFormatting = .fixed, align: NoOpLogStringAlignment = .none, privacy: NoOpLogPrivacy = .auto) {} package mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Double, format: NoOpLogFloatFormatting = .fixed, align: NoOpLogStringAlignment = .none, privacy: NoOpLogPrivacy = .auto) {} + // Append errors + package mutating func appendInterpolation(_ error: @autoclosure @escaping () -> any Error, privacy: NoOpLogPrivacy = .auto, attributes: String = "") {} + package mutating func appendInterpolation(_ error: @autoclosure @escaping () -> (any Error)?, privacy: NoOpLogPrivacy = .auto, attributes: String = "") {} + // Add more interpolations here as needed } diff --git a/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift b/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift index 66caff16fc..403f81d390 100644 --- a/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift +++ b/Tests/SwiftDocCTests/Utility/NoOpSignposterShimTests.swift @@ -65,5 +65,7 @@ final class NoOpSignposterShimTests: XCTestCase { logger.log(level: .default, "Some aligned values \(12, align: .right(columns: 5)) \("some text", align: .left(columns: 10))") + struct SomeError: Swift.Error {} + logger.error("Some error \(SomeError())") } } From 40bb242e8a70914f9139158b9204681f411d08ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Wed, 4 Dec 2024 16:20:16 +0100 Subject: [PATCH 7/7] Address code review feedback: - Add signpost intervals around a few more tasks --- .../ConvertActionConverter.swift | 42 +++++++++++-------- .../Symbol Graph/SymbolGraphLoader.swift | 5 +++ 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift b/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift index 607302e077..939d4ef123 100644 --- a/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift +++ b/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift @@ -169,33 +169,39 @@ package enum ConvertActionConverter { // Write various metadata if emitDigest { - do { - try outputConsumer.consume(linkableElementSummaries: linkSummaries) - try outputConsumer.consume(indexingRecords: indexingRecords) - try outputConsumer.consume(assets: assets) - } catch { - recordProblem(from: error, in: &conversionProblems, withIdentifier: "metadata") + signposter.withIntervalSignpost("Emit digest", id: signposter.makeSignpostID()) { + do { + try outputConsumer.consume(linkableElementSummaries: linkSummaries) + try outputConsumer.consume(indexingRecords: indexingRecords) + try outputConsumer.consume(assets: assets) + } catch { + recordProblem(from: error, in: &conversionProblems, withIdentifier: "metadata") + } } } if FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled { - do { - let serializableLinkInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: bundle.id) - try outputConsumer.consume(linkResolutionInformation: serializableLinkInformation) - - if !emitDigest { - try outputConsumer.consume(linkableElementSummaries: linkSummaries) + signposter.withIntervalSignpost("Serialize link hierarchy", id: signposter.makeSignpostID()) { + do { + let serializableLinkInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: bundle.id) + try outputConsumer.consume(linkResolutionInformation: serializableLinkInformation) + + if !emitDigest { + try outputConsumer.consume(linkableElementSummaries: linkSummaries) + } + } catch { + recordProblem(from: error, in: &conversionProblems, withIdentifier: "link-resolver") } - } catch { - recordProblem(from: error, in: &conversionProblems, withIdentifier: "link-resolver") } } if emitDigest { - do { - try outputConsumer.consume(problems: context.problems + conversionProblems) - } catch { - recordProblem(from: error, in: &conversionProblems, withIdentifier: "problems") + signposter.withIntervalSignpost("Emit digest", id: signposter.makeSignpostID()) { + do { + try outputConsumer.consume(problems: context.problems + conversionProblems) + } catch { + recordProblem(from: error, in: &conversionProblems, withIdentifier: "problems") + } } } diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index 92fb04d88e..d4d7730b01 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -158,6 +158,11 @@ struct SymbolGraphLoader { ) signposter.endInterval("Build unified symbol graph", mergeSignpostHandle) + let availabilitySignpostHandle = signposter.beginInterval("Add missing availability", id: signposter.makeSignpostID()) + defer { + signposter.endInterval("Add missing availability", availabilitySignpostHandle) + } + for var unifiedGraph in unifiedGraphs.values { var defaultUnavailablePlatforms = [PlatformName]() var defaultAvailableInformation = [DefaultAvailability.ModuleAvailability]()