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

[6.1] Improve performance of JSONPointer encoding #1119

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 71 additions & 28 deletions Sources/SwiftDocC/Model/Rendering/Variants/JSONPointer.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 Apple Inc. and the Swift project authors
Copyright (c) 2021-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
Expand All @@ -20,7 +20,7 @@ public struct JSONPointer: Codable, CustomStringConvertible, Equatable {
public var pathComponents: [String]

public var description: String {
"/\(pathComponents.map(Self.escape).joined(separator: "/"))"
Self.escaped(pathComponents)
}

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

/// Escapes a path component of a JSON pointer.
static func escape(_ pointerPathComponents: String) -> String {
applyEscaping(pointerPathComponents, shouldUnescape: false)
}

/// Unescaped a path component of a JSON pointer.
static func unescape(_ pointerPathComponents: String) -> String {
applyEscaping(pointerPathComponents, shouldUnescape: true)
private static func escaped(_ pathComponents: [String]) -> String {
// This code is called quite frequently for mixed language content.
// Optimizing it has a measurable impact on the total documentation build time.

var string: [UTF8.CodeUnit] = []
string.reserveCapacity(
pathComponents.reduce(0) { acc, component in
acc + 1 /* the "/" separator */ + component.utf8.count
}
+ 16 // some extra capacity since the escaped replacements grow the string beyond its original length.
)

for component in pathComponents {
// The leading slash and component separator
string.append(forwardSlash)

// The escaped component
for char in component.utf8 {
switch char {
case tilde:
string.append(contentsOf: escapedTilde)
case forwardSlash:
string.append(contentsOf: escapedForwardSlash)
default:
string.append(char)
}
}
}

return String(decoding: string, as: UTF8.self)
}

/// Applies an escaping operation to the path component of a JSON pointer.
/// - Parameters:
/// - pointerPathComponent: The path component to escape.
/// - shouldUnescape: Whether this function should unescape or escape the path component.
/// - Returns: The escaped value if `shouldUnescape` is false, otherwise the escaped value.
private static func applyEscaping(_ pointerPathComponent: String, shouldUnescape: Bool) -> String {
EscapedCharacters.allCases
.reduce(pointerPathComponent) { partialResult, characterThatNeedsEscaping in
partialResult
.replacingOccurrences(
of: characterThatNeedsEscaping[
keyPath: shouldUnescape ? \EscapedCharacters.escaped : \EscapedCharacters.rawValue
],
with: characterThatNeedsEscaping[
keyPath: shouldUnescape ? \EscapedCharacters.rawValue : \EscapedCharacters.escaped
]
)
private static func unescaped(_ escapedRawString: String) -> [String] {
escapedRawString.removingLeadingSlash.components(separatedBy: "/").map {
// This code is called quite frequently for mixed language content.
// Optimizing it has a measurable impact on the total documentation build time.

var string: [UTF8.CodeUnit] = []
string.reserveCapacity($0.utf8.count)

var remaining = $0.utf8[...]
while let char = remaining.popFirst() {
guard char == tilde, let escapedCharacterIndicator = remaining.popFirst() else {
string.append(char)
continue
}

// Check the character
switch escapedCharacterIndicator {
case zero:
string.append(tilde)
case one:
string.append(forwardSlash)
default:
// This string isn't an escaped JSON Pointer. Return it as-is.
return $0
}
}

return String(decoding: string, as: UTF8.self)
}
}
}

// A few UInt8 raw values for various UTF-8 characters that this implementation frequently checks for

private let tilde = UTF8.CodeUnit(ascii: "~")
private let forwardSlash = UTF8.CodeUnit(ascii: "/")
private let zero = UTF8.CodeUnit(ascii: "0")
private let one = UTF8.CodeUnit(ascii: "1")

private let escapedTilde = [tilde, zero]
private let escapedForwardSlash = [tilde, one]