Skip to content

Commit b1c29d3

Browse files
Much faster navigator index creation for mixed Swift and Objective-C projects (#917)
* Fix a navigator index performance for mixed Swift/Objective-C projects rdar://127759734 * Use new apply-patch function in other places * Deprecate unused `RenderNode/childrenRelationship(for:)` * Move inner function in test to avoid warning about captured state * Fix unrelated issue where the language in disambiguated references wasn't stable * Move new indexable-render-node-representation protocol top-level * Remove extra word in code comment Co-authored-by: Maya Epps <[email protected]> --------- Co-authored-by: Maya Epps <[email protected]>
1 parent fc2d491 commit b1c29d3

14 files changed

+469
-207
lines changed

Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift

+4-55
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
@@ -75,61 +75,10 @@ public class FileSystemRenderNodeProvider: RenderNodeProvider {
7575
}
7676

7777
extension RenderNode {
78-
private static let typesThatShouldNotUseNavigatorTitle: Set<NavigatorIndex.PageType> = [
79-
.framework, .class, .structure, .enumeration, .protocol, .typeAlias, .associatedType, .extension
80-
]
81-
82-
/// Returns a navigator title preferring the fragments inside the metadata, if applicable.
83-
func navigatorTitle() -> String? {
84-
let fragments: [DeclarationRenderSection.Token]?
85-
86-
// FIXME: Use `metadata.navigatorTitle` for all Swift symbols (github.com/apple/swift-docc/issues/176).
87-
if identifier.sourceLanguage == .swift || (metadata.navigatorTitle ?? []).isEmpty {
88-
let pageType = navigatorPageType()
89-
guard !Self.typesThatShouldNotUseNavigatorTitle.contains(pageType) else {
90-
return metadata.title
91-
}
92-
fragments = metadata.fragments
93-
} else {
94-
fragments = metadata.navigatorTitle
95-
}
96-
97-
return fragments?.map(\.text).joined() ?? metadata.title
98-
}
99-
10078
/// Returns the NavigatorIndex.PageType indicating the type of the page.
79+
@_disfavoredOverload
80+
@available(*, deprecated, message: "This deprecated API will be removed after 6.1 is released")
10181
public func navigatorPageType() -> NavigatorIndex.PageType {
102-
103-
// This is a workaround to support plist keys.
104-
if let roleHeading = metadata.roleHeading?.lowercased() {
105-
if roleHeading == "property list key" {
106-
return .propertyListKey
107-
} else if roleHeading == "property list key reference" {
108-
return .propertyListKeyReference
109-
}
110-
}
111-
112-
switch self.kind {
113-
case .article:
114-
if let role = metadata.role {
115-
return NavigatorIndex.PageType(role: role)
116-
}
117-
return NavigatorIndex.PageType.article
118-
case .tutorial:
119-
return NavigatorIndex.PageType.tutorial
120-
case .section:
121-
return NavigatorIndex.PageType.section
122-
case .overview:
123-
return NavigatorIndex.PageType.overview
124-
case .symbol:
125-
if let symbolKind = metadata.symbolKind {
126-
return NavigatorIndex.PageType(symbolKind: symbolKind)
127-
}
128-
if let role = metadata.role {
129-
return NavigatorIndex.PageType(role: role)
130-
}
131-
return NavigatorIndex.PageType.symbol
132-
}
82+
return (self as any NavigatorIndexableRenderNodeRepresentation).navigatorPageType()
13383
}
134-
13584
}

Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift

+54-29
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,57 @@ extension NavigatorIndex {
618618
/// Index a single render `RenderNode`.
619619
/// - Parameter renderNode: The render node to be indexed.
620620
public func index(renderNode: RenderNode) throws {
621+
// Always index the main render node representation
622+
let language = try index(renderNode, traits: nil)
621623

624+
// Additionally, for Swift want to also index the Objective-C variant, if there is any.
625+
guard language == .swift else {
626+
return
627+
}
628+
629+
// Check if the render node has an Objective-C representation
630+
guard let objCVariantTrait = renderNode.variants?.flatMap(\.traits).first(where: { trait in
631+
switch trait {
632+
case .interfaceLanguage(let language):
633+
return InterfaceLanguage.from(string: language) == .objc
634+
}
635+
}) else {
636+
return
637+
}
638+
639+
// A render node is structured differently depending on if it was created by "rendering" a documentation node
640+
// or if it was deserialized from a documentation archive.
641+
//
642+
// If it was created by rendering a documentation node, all variant information is stored in each individual variant collection and the variant overrides are nil.
643+
// If it was deserialized from a documentation archive, all variant information is stored in the variant overrides and the variant collections are empty.
644+
645+
// Operating on the variant override is _significantly_ slower, so we only take that code path if we have to.
646+
// The only reason why this code path still exists is to support the `docc process-archive index` command, which creates an navigation index from an already build documentation archive.
647+
if let overrides = renderNode.variantOverrides, !overrides.isEmpty {
648+
// This code looks peculiar and very inefficient because it is.
649+
// I didn't write it and I really wanted to remove it, but it's the only way to support the `docc process-archive index` command for now.
650+
// rdar://128050800 Tracks fixing the inefficiencies with this code, to make `docc process-archive index` command as fast as indexing during a `docc convert` command.
651+
//
652+
// First, it encodes the render node, which was read from a file, back to data; because that's what the overrides applier operates on
653+
let encodedRenderNode = try renderNode.encodeToJSON()
654+
// Second, the overrides applier will decode that data into an abstract JSON representation of arrays, dictionaries, string, numbers, etc.
655+
// After that the overrides applier loops over all the JSON patches and applies them to the abstract JSON representation.
656+
// With all the patches applies, the overrides applier encodes the abstract JSON representation into data again and returns it.
657+
let transformedData = try RenderNodeVariantOverridesApplier().applyVariantOverrides(in: encodedRenderNode, for: [objCVariantTrait])
658+
// Third, this code decodes the render node from the transformed data. If you count reading the render node from the documentation archive,
659+
// this is the fifth time that the same node is either encoded or decoded.
660+
let variantRenderNode = try RenderNode.decode(fromJSON: transformedData)
661+
// Finally, the decoded node is in a way flattened, so that it only contains its Objective-C content. That's why we pass `nil` instead of `[objCVariantTrait]` to this call.
662+
_ = try index(variantRenderNode, traits: nil)
663+
}
664+
665+
// If this render node was created by rendering a documentation node, we create a "view" into its Objective-C specific data and index that.
666+
let objVariantView = RenderNodeVariantView(wrapped: renderNode, traits: [objCVariantTrait])
667+
_ = try index(objVariantView, traits: [objCVariantTrait])
668+
}
669+
670+
// The private index implementation which indexes a given render node representation
671+
private func index(_ renderNode: any NavigatorIndexableRenderNodeRepresentation, traits: [RenderNode.Variant.Trait]?) throws -> InterfaceLanguage? {
622672
guard let navigatorIndex else {
623673
throw Error.navigatorIndexIsNil
624674
}
@@ -643,10 +693,10 @@ extension NavigatorIndex {
643693
.normalizedNavigatorIndexIdentifier(forLanguage: language.mask)
644694

645695
guard identifierToNode[normalizedIdentifier] == nil else {
646-
return // skip as item exists already.
696+
return nil // skip as item exists already.
647697
}
648698

649-
guard let title = (usePageTitle) ? renderNode.metadata.title : renderNode.navigatorTitle() else {
699+
guard let title = usePageTitle ? renderNode.metadata.title : renderNode.navigatorTitle() else {
650700
throw Error.missingTitle(description: "\(renderNode.identifier.absoluteString.singleQuoted) has an empty title and so can't have a usable entry in the index.")
651701
}
652702

@@ -724,13 +774,11 @@ extension NavigatorIndex {
724774
navigationItem.usrIdentifier = language.name + "-" + ExternalIdentifier.usr(usr).hash // We pair the hash and the language name
725775
}
726776

727-
let childrenRelationship = renderNode.childrenRelationship()
728-
729777
let navigatorNode = NavigatorTree.Node(item: navigationItem, bundleIdentifier: bundleIdentifier)
730778

731779
// Process the children
732780
var children = [Identifier]()
733-
for (index, child) in childrenRelationship.enumerated() {
781+
for (index, child) in renderNode.navigatorChildren(for: traits).enumerated() {
734782
let groupIdentifier: Identifier?
735783

736784
if let title = child.name {
@@ -807,30 +855,7 @@ extension NavigatorIndex {
807855
// Bump the nodes counter.
808856
counter += 1
809857

810-
// We only want to check for an objective-c variant
811-
// if we're currently indexing a swift variant.
812-
guard language == .swift else {
813-
return
814-
}
815-
816-
// Check if the render node has a variant for Objective-C
817-
//
818-
// Note that we need to check the `variants` property here, not the `variantsOverride`
819-
// property because `variantsOverride` is only populated when the RenderNode is encoded.
820-
let objCVariantTrait = renderNode.variants?.flatMap(\.traits).first { trait in
821-
switch trait {
822-
case .interfaceLanguage(let language):
823-
return InterfaceLanguage.from(string: language) == .objc
824-
}
825-
}
826-
827-
// In case we have a variant for Objective-C, apply the variant and re-index the render node.
828-
if let variantToApply = objCVariantTrait {
829-
let encodedRenderNode = try renderNode.encodeToJSON()
830-
let transformedData = try RenderNodeVariantOverridesApplier().applyVariantOverrides(in: encodedRenderNode, for: [variantToApply])
831-
let variantRenderNode = try RenderNode.decode(fromJSON: transformedData)
832-
try index(renderNode: variantRenderNode)
833-
}
858+
return language
834859
}
835860

836861
/// An internal struct to store data about a single navigator entry.

0 commit comments

Comments
 (0)