Skip to content

Commit e0ce789

Browse files
authored
Improve performance of JSONPointer encoding (#1099)
* Improve performance of JSONPointer encoding & decoding * Reserve some extra capacity in temporary storage to avoid reallocations
1 parent 093049d commit e0ce789

File tree

1 file changed

+71
-28
lines changed

1 file changed

+71
-28
lines changed

Sources/SwiftDocC/Model/Rendering/Variants/JSONPointer.swift

Lines changed: 71 additions & 28 deletions
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
@@ -20,7 +20,7 @@ public struct JSONPointer: Codable, CustomStringConvertible, Equatable {
2020
public var pathComponents: [String]
2121

2222
public var description: String {
23-
"/\(pathComponents.map(Self.escape).joined(separator: "/"))"
23+
Self.escaped(pathComponents)
2424
}
2525

2626
/// Creates a JSON Pointer given its path components.
@@ -87,36 +87,79 @@ public struct JSONPointer: Codable, CustomStringConvertible, Equatable {
8787
public init(from decoder: Decoder) throws {
8888
let container = try decoder.singleValueContainer()
8989
let stringValue = try container.decode(String.self)
90-
self.pathComponents = stringValue.removingLeadingSlash.components(separatedBy: "/").map(Self.unescape)
90+
self.pathComponents = Self.unescaped(stringValue)
9191
}
9292

93-
/// Escapes a path component of a JSON pointer.
94-
static func escape(_ pointerPathComponents: String) -> String {
95-
applyEscaping(pointerPathComponents, shouldUnescape: false)
96-
}
97-
98-
/// Unescaped a path component of a JSON pointer.
99-
static func unescape(_ pointerPathComponents: String) -> String {
100-
applyEscaping(pointerPathComponents, shouldUnescape: true)
93+
private static func escaped(_ pathComponents: [String]) -> String {
94+
// This code is called quite frequently for mixed language content.
95+
// Optimizing it has a measurable impact on the total documentation build time.
96+
97+
var string: [UTF8.CodeUnit] = []
98+
string.reserveCapacity(
99+
pathComponents.reduce(0) { acc, component in
100+
acc + 1 /* the "/" separator */ + component.utf8.count
101+
}
102+
+ 16 // some extra capacity since the escaped replacements grow the string beyond its original length.
103+
)
104+
105+
for component in pathComponents {
106+
// The leading slash and component separator
107+
string.append(forwardSlash)
108+
109+
// The escaped component
110+
for char in component.utf8 {
111+
switch char {
112+
case tilde:
113+
string.append(contentsOf: escapedTilde)
114+
case forwardSlash:
115+
string.append(contentsOf: escapedForwardSlash)
116+
default:
117+
string.append(char)
118+
}
119+
}
120+
}
121+
122+
return String(decoding: string, as: UTF8.self)
101123
}
102124

103-
/// Applies an escaping operation to the path component of a JSON pointer.
104-
/// - Parameters:
105-
/// - pointerPathComponent: The path component to escape.
106-
/// - shouldUnescape: Whether this function should unescape or escape the path component.
107-
/// - Returns: The escaped value if `shouldUnescape` is false, otherwise the escaped value.
108-
private static func applyEscaping(_ pointerPathComponent: String, shouldUnescape: Bool) -> String {
109-
EscapedCharacters.allCases
110-
.reduce(pointerPathComponent) { partialResult, characterThatNeedsEscaping in
111-
partialResult
112-
.replacingOccurrences(
113-
of: characterThatNeedsEscaping[
114-
keyPath: shouldUnescape ? \EscapedCharacters.escaped : \EscapedCharacters.rawValue
115-
],
116-
with: characterThatNeedsEscaping[
117-
keyPath: shouldUnescape ? \EscapedCharacters.rawValue : \EscapedCharacters.escaped
118-
]
119-
)
125+
private static func unescaped(_ escapedRawString: String) -> [String] {
126+
escapedRawString.removingLeadingSlash.components(separatedBy: "/").map {
127+
// This code is called quite frequently for mixed language content.
128+
// Optimizing it has a measurable impact on the total documentation build time.
129+
130+
var string: [UTF8.CodeUnit] = []
131+
string.reserveCapacity($0.utf8.count)
132+
133+
var remaining = $0.utf8[...]
134+
while let char = remaining.popFirst() {
135+
guard char == tilde, let escapedCharacterIndicator = remaining.popFirst() else {
136+
string.append(char)
137+
continue
138+
}
139+
140+
// Check the character
141+
switch escapedCharacterIndicator {
142+
case zero:
143+
string.append(tilde)
144+
case one:
145+
string.append(forwardSlash)
146+
default:
147+
// This string isn't an escaped JSON Pointer. Return it as-is.
148+
return $0
149+
}
120150
}
151+
152+
return String(decoding: string, as: UTF8.self)
153+
}
121154
}
122155
}
156+
157+
// A few UInt8 raw values for various UTF-8 characters that this implementation frequently checks for
158+
159+
private let tilde = UTF8.CodeUnit(ascii: "~")
160+
private let forwardSlash = UTF8.CodeUnit(ascii: "/")
161+
private let zero = UTF8.CodeUnit(ascii: "0")
162+
private let one = UTF8.CodeUnit(ascii: "1")
163+
164+
private let escapedTilde = [tilde, zero]
165+
private let escapedForwardSlash = [tilde, one]

0 commit comments

Comments
 (0)