Skip to content

Commit 159f74f

Browse files
authored
Implement a better cache key for resolved references (#793)
1 parent 942c249 commit 159f74f

File tree

6 files changed

+52
-120
lines changed

6 files changed

+52
-120
lines changed

Sources/SwiftDocC/Model/Identifier.swift

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,14 @@ extension TopicReferenceResolutionErrorInfo {
141141
/// > its data.
142142
public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomStringConvertible {
143143
typealias ReferenceBundleIdentifier = String
144-
typealias ReferenceKey = String
144+
private struct ReferenceKey: Hashable {
145+
var path: String
146+
var fragment: String?
147+
var sourceLanguages: Set<SourceLanguage>
148+
}
145149

146150
/// A synchronized reference cache to store resolved references.
147-
static var sharedPool = Synchronized([ReferenceBundleIdentifier: [ReferenceKey: ResolvedTopicReference]]())
151+
private static var sharedPool = Synchronized([ReferenceBundleIdentifier: [ReferenceKey: ResolvedTopicReference]]())
148152

149153
/// Clears cached references belonging to the bundle with the given identifier.
150154
/// - Parameter bundleIdentifier: The identifier of the bundle to which the method should clear belonging references.
@@ -219,11 +223,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
219223
private init(bundleIdentifier: String, urlReadablePath: String, urlReadableFragment: String? = nil, sourceLanguages: Set<SourceLanguage>) {
220224
precondition(!sourceLanguages.isEmpty, "ResolvedTopicReference.sourceLanguages cannot be empty")
221225
// Check for a cached instance of the reference
222-
let key = Self.cacheKey(
223-
urlReadablePath: urlReadablePath,
224-
urlReadableFragment: urlReadableFragment,
225-
sourceLanguages: sourceLanguages
226-
)
226+
let key = ReferenceKey(path: urlReadablePath, fragment: urlReadableFragment, sourceLanguages: sourceLanguages)
227227
let cached = Self.sharedPool.sync { $0[bundleIdentifier]?[key] }
228228
if let resolved = cached {
229229
self = resolved
@@ -244,20 +244,6 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
244244
}
245245
}
246246

247-
private static func cacheKey(
248-
urlReadablePath path: String,
249-
urlReadableFragment fragment: String?,
250-
sourceLanguages: Set<SourceLanguage>
251-
) -> String {
252-
let sourceLanguagesString = sourceLanguages.map(\.id).sorted().joined(separator: "-")
253-
254-
if let fragment = fragment {
255-
return "\(path):\(fragment):\(sourceLanguagesString)"
256-
} else {
257-
return "\(path):\(sourceLanguagesString)"
258-
}
259-
}
260-
261247
/// The topic URL as you would write in a link.
262248
public var url: URL {
263249
return _storage.url
@@ -495,22 +481,10 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
495481
self.absoluteString = self.url.absoluteString
496482
}
497483
}
498-
}
499-
500-
typealias ResolvedTopicReferenceCacheKey = String
501-
502-
extension ResolvedTopicReference {
503-
/// Returns a unique cache ID for a pair of unresolved and parent references.
504-
static func cacheIdentifier(_ reference: UnresolvedTopicReference, fromSymbolLink: Bool, in parent: ResolvedTopicReference?) -> ResolvedTopicReferenceCacheKey {
505-
let isSymbolLink = fromSymbolLink ? ":symbol" : ""
506-
if let parent = parent {
507-
// Create a cache id in the parent context
508-
return "\(reference.topicURL.absoluteString):\(parent.bundleIdentifier):\(parent.path):\(parent.sourceLanguage.id)\(isSymbolLink)"
509-
} else {
510-
// A cache ID for an external reference
511-
assert(reference.topicURL.components.host != nil)
512-
return reference.topicURL.absoluteString.appending(isSymbolLink)
513-
}
484+
485+
// For testing the caching
486+
static func _numberOfCachedReferences(bundleID: ReferenceBundleIdentifier) -> Int? {
487+
return Self.sharedPool.sync { $0[bundleID]?.count }
514488
}
515489
}
516490

Tests/SwiftDocCTests/Converter/RenderNodeCodableTests.swift

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,21 +146,14 @@ class RenderNodeCodableTests: XCTestCase {
146146
subdirectory: "Test Resources"
147147
)!
148148

149-
let uniqueBundleIdentifier = #function
149+
let bundleID = #function
150150

151-
let renderNodeWithUniqueBundleID = try String(
152-
contentsOf: exampleRenderNodeJSON
153-
)
154-
.replacingOccurrences(
155-
of: "org.swift.docc.example",
156-
with: uniqueBundleIdentifier
157-
)
151+
let renderNodeWithUniqueBundleID = try String(contentsOf: exampleRenderNodeJSON)
152+
.replacingOccurrences(of: "org.swift.docc.example", with: bundleID)
158153

159154
_ = try JSONDecoder().decode(RenderNode.self, from: Data(renderNodeWithUniqueBundleID.utf8))
160155

161-
ResolvedTopicReference.sharedPool.sync { sharedPool in
162-
XCTAssertNil(sharedPool[uniqueBundleIdentifier])
163-
}
156+
XCTAssertNil(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID))
164157
}
165158

166159
func testDecodeRenderNodeWithoutTopicSectionStyle() throws {

Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,7 @@ Root
260260
atomically: true
261261
)
262262

263-
ResolvedTopicReference.sharedPool.sync { sharedPool in
264-
XCTAssertNil(sharedPool[uniqueTestBundleIdentifier])
265-
}
263+
XCTAssertNil(ResolvedTopicReference._numberOfCachedReferences(bundleID: uniqueTestBundleIdentifier))
266264
}
267265

268266

Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2672,41 +2672,35 @@ let expected = """
26722672
}
26732673

26742674
func testContextCachesReferences() throws {
2675+
let bundleID = #function
26752676
// Verify there is no pool bucket for the bundle we're about to test
2676-
XCTAssertNil(ResolvedTopicReference.sharedPool.sync({ $0[#function] }))
2677+
XCTAssertNil(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID))
26772678

26782679
let (_, _, _) = try testBundleAndContext(copying: "TestBundle", excludingPaths: [], codeListings: [:], configureBundle: { rootURL in
26792680
let infoPlistURL = rootURL.appendingPathComponent("Info.plist", isDirectory: false)
26802681
try! String(contentsOf: infoPlistURL)
2681-
.replacingOccurrences(of: "org.swift.docc.example", with: #function)
2682+
.replacingOccurrences(of: "org.swift.docc.example", with: bundleID)
26822683
.write(to: infoPlistURL, atomically: true, encoding: .utf8)
26832684
})
26842685

26852686
// Verify there is a pool bucket for the bundle we've loaded
2686-
XCTAssertNotNil(ResolvedTopicReference.sharedPool.sync({ $0[#function] }))
2687+
XCTAssertNotNil(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID))
26872688

2688-
guard let references = ResolvedTopicReference.sharedPool.sync({ $0[#function] }) else {
2689-
return
2690-
}
2691-
2692-
let beforeCount = references.count
2689+
let beforeCount = try XCTUnwrap(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID))
26932690

26942691
// Verify a given identifier exists in the pool by creating it and verifying it wasn't added to the pool
2695-
let identifier = ResolvedTopicReference(bundleIdentifier: #function, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)
2696-
_ = identifier
2692+
_ = ResolvedTopicReference(bundleIdentifier: bundleID, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)
26972693

26982694
// Verify create the reference above did not add to the cache
2699-
XCTAssertEqual(beforeCount, ResolvedTopicReference.sharedPool.sync({ $0[#function]!.count }))
2695+
XCTAssertEqual(beforeCount, ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID))
27002696

27012697
// Create a new reference for the same bundle that was not loaded with the context
2702-
let newIdentifier = ResolvedTopicReference(bundleIdentifier: #function, path: "/tutorials/Test-Bundle/TestTutorial/\(#function)", sourceLanguage: .swift)
2703-
_ = newIdentifier
2698+
_ = ResolvedTopicReference(bundleIdentifier: bundleID, path: "/tutorials/Test-Bundle/TestTutorial/\(#function)", sourceLanguage: .swift)
27042699

27052700
// Verify creating a new reference added to the ones loaded with the context
2706-
XCTAssertNotEqual(beforeCount, ResolvedTopicReference.sharedPool.sync({ $0[#function]!.count }))
2701+
XCTAssertNotEqual(beforeCount, ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID))
27072702

2708-
// Purge the pool
2709-
ResolvedTopicReference.purgePool(for: #function)
2703+
ResolvedTopicReference.purgePool(for: bundleID)
27102704
}
27112705

27122706
func testAbstractAfterMetadataDirective() throws {

Tests/SwiftDocCTests/Model/IdentifierTests.swift

Lines changed: 25 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -65,67 +65,42 @@ class IdentifierTests: XCTestCase {
6565
}
6666

6767
func testReusingReferences() {
68-
// Verify the bundle doesn't exist in the pool
69-
XCTAssertFalse(ResolvedTopicReference.sharedPool.sync({ $0.keys.contains(#function) }))
68+
let bundleID = #function
69+
XCTAssertNil(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), "Cache for this bundle shouldn't exist because caching is not enabled by default")
7070

71-
// Enable caching for our test bundle identifier
72-
ResolvedTopicReference.enableReferenceCaching(for: #function)
71+
// Add one reference
72+
ResolvedTopicReference.enableReferenceCaching(for: bundleID)
73+
XCTAssertEqual(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), 0, "Should have an empty cache after enabling reference caching for this bundle")
7374

74-
// Create a resolved reference
75-
let ref = ResolvedTopicReference(bundleIdentifier: #function, path: "/path/child", sourceLanguage: .swift)
76-
_ = ref // to suppress the warning above
75+
// Add the same reference repeatedly
76+
_ = ResolvedTopicReference(bundleIdentifier: bundleID, path: "/path/to/page", sourceLanguage: .swift)
77+
XCTAssertEqual(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), 1, "Should have an cached one reference because a reference with this bundle identifier was created")
7778

78-
// Verify the bundle was added to the pool
79-
guard let references = ResolvedTopicReference.sharedPool.sync({ $0[#function] }) else {
80-
XCTFail("Reference bundle was not added to reference cache")
81-
return
82-
}
79+
_ = ResolvedTopicReference(bundleIdentifier: bundleID, path: "/path/to/page", sourceLanguage: .swift)
80+
_ = ResolvedTopicReference(bundleIdentifier: bundleID, path: "/path/to/page", sourceLanguage: .swift)
81+
XCTAssertEqual(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), 1, "Should still only have one cached reference because the same reference was created repeatedly")
8382

84-
// Verify the child is now in the pool
85-
XCTAssertEqual(references.contains(where: { pair -> Bool in
86-
return pair.key.contains("/path/child")
87-
}), true)
83+
// Add another reference
84+
_ = ResolvedTopicReference(bundleIdentifier: bundleID, path: "/path/to/other-page", sourceLanguage: .swift)
85+
XCTAssertEqual(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), 2, "Should have cached another reference because two different references with this bundle identifier has been created")
8886

89-
// Clear the cache
90-
ResolvedTopicReference.purgePool(for: #function)
87+
// Purge and repeat
88+
ResolvedTopicReference.purgePool(for: bundleID)
89+
XCTAssertNil(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), "Cache for this bundle shouldn't have been deleted because the pool was purged")
9190

92-
// Verify there are no references in the pool for that bundle
93-
XCTAssertFalse(ResolvedTopicReference.sharedPool.sync({ $0.keys.contains(#function) }))
91+
ResolvedTopicReference.enableReferenceCaching(for: bundleID)
92+
XCTAssertEqual(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), 0, "Should have an empty cache after enabling reference caching for this bundle")
9493

95-
// Re-enable caching for our test bundle identifier
96-
ResolvedTopicReference.enableReferenceCaching(for: #function)
97-
98-
let ref1 = ResolvedTopicReference(bundleIdentifier: #function, path: "/path/child", sourceLanguage: .swift)
99-
_ = ref1
100-
101-
// Verify the bundle was added to the pool
102-
guard let references1 = ResolvedTopicReference.sharedPool.sync({ $0[#function] }) else {
103-
XCTFail("Reference bundle was not added to reference cache")
104-
return
105-
}
106-
107-
// Verify the pool bucket was re-created and the reference is in the pool
108-
XCTAssertEqual(references1.contains(where: { pair -> Bool in
109-
return pair.key.contains("/path/child")
110-
}), true)
94+
_ = ResolvedTopicReference(bundleIdentifier: bundleID, path: "/path/to/page", sourceLanguage: .swift)
95+
XCTAssertEqual(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), 1, "Should have an cached one reference because a reference with this bundle identifier was created")
11196
}
11297

11398
func testReferencesAreNotCachedByDefault() {
114-
// Verify the bundle doesn't exist in the pool
115-
XCTAssertFalse(ResolvedTopicReference.sharedPool.sync({ $0.keys.contains(#function) }))
116-
117-
// Create a resolved reference
118-
let reference = ResolvedTopicReference(
119-
bundleIdentifier: #function,
120-
path: "/path/child",
121-
sourceLanguage: .swift
122-
)
123-
124-
// Verify the bundle still doesn't exist in the pool
125-
XCTAssertFalse(ResolvedTopicReference.sharedPool.sync({ $0.keys.contains(#function) }))
99+
let bundleID = #function
100+
XCTAssertNil(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), "References for this bundle shouldn't exist because caching is not enabled by default")
126101

127-
// Add a use of 'reference' to suppress Swift's 'reference' was never used warning
128-
XCTAssertNotNil(reference)
102+
_ = ResolvedTopicReference(bundleIdentifier: bundleID, path: "/path/to/page", sourceLanguage: .swift)
103+
XCTAssertNil(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID), "After creating a reference in this bundle, references still shouldn't exist because caching is not enabled by default")
129104
}
130105

131106
func testReferenceInitialPathComponents() {

Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,9 +1769,7 @@ class ConvertActionTests: XCTestCase {
17691769

17701770
_ = try action.perform(logHandle: .none)
17711771

1772-
ResolvedTopicReference.sharedPool.sync { sharedPool in
1773-
XCTAssertEqual(sharedPool[#function]?.count, 8)
1774-
}
1772+
XCTAssertEqual(ResolvedTopicReference._numberOfCachedReferences(bundleID: #function), 8)
17751773
}
17761774

17771775
func testIgnoresAnalyzerHintsByDefault() throws {

0 commit comments

Comments
 (0)