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

speed up hot-path utf8 processing #52

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions Sources/Elementary/Core/AttributeStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public struct _MergedAttributes: Sequence, Sendable {
Iterator(storage)
}

public var isEmpty: Bool {
storage.isEmpty
}

public struct Iterator: IteratorProtocol {
enum State {
case empty
Expand Down
2 changes: 1 addition & 1 deletion Sources/Elementary/Core/Html+Elements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ extension HTMLComment: Sendable {}
extension HTMLRaw: Sendable {}

extension HTMLTagDefinition {
@usableFromInline
@usableFromInline @inline(__always)
static var renderingType: _HTMLRenderToken.RenderingType {
_rendersInline ? .inline : .block
}
Expand Down
98 changes: 68 additions & 30 deletions Sources/Elementary/Rendering/RenderingUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,61 +23,99 @@ extension _RenderingContext {
// I do not know why this function does not work in embedded, but currently it crashes the compiler
#if !hasFeature(Embedded)
extension [UInt8] {
@inline(__always)
mutating func appendToken(_ token: consuming _HTMLRenderToken) {
// avoid strings and append each component directly
switch token {
case let .startTag(tagName, attributes: attributes, isUnpaired: _, type: _):
append(60) // <
append(contentsOf: tagName.utf8)
for attribute in attributes {
append(32) // space
append(contentsOf: attribute.name.utf8)
if let value = attribute.value {
append(contentsOf: [61, 34]) // ="
appendEscapedAttributeValue(value)
append(34) // "
appendString(tagName)
if !attributes.isEmpty {
for attribute in attributes {
append(32) // space
appendString(attribute.name)
if let value = attribute.value {
append(contentsOf: [61, 34]) // ="
appendEscapedAttributeValue(value)
append(34) // "
}
}
}
append(62) // >
case let .endTag(tagName, _):
append(contentsOf: [60, 47]) // </
append(contentsOf: tagName.utf8)
appendString(tagName)
append(62) // >
case let .text(text):
appendEscapedText(text)
case let .raw(raw):
append(contentsOf: raw.utf8)
appendString(raw)
case let .comment(comment):
append(contentsOf: "<!--".utf8)
appendString("<!--")
appendEscapedText(comment)
append(contentsOf: "-->".utf8)
appendString("-->")
}
}

@inline(__always)
mutating func appendString(_ string: consuming String) {
string.withUTF8 { utf8 in
append(contentsOf: utf8)
}
}

@inline(__always)
mutating func appendEscapedAttributeValue(_ value: consuming String) {
for byte in value.utf8 {
switch byte {
case 38: // &
append(contentsOf: "&amp;".utf8)
case 34: // "
append(contentsOf: "&quot;".utf8)
default:
append(byte)
value.withUTF8 { utf8 in
var start = utf8.startIndex

for current in utf8.indices {
switch utf8[current] {
case 38: // &
append(contentsOf: utf8[start ..< current])
appendString("&amp;")
start = utf8.index(after: current)
case 34: // "
append(contentsOf: utf8[start ..< current])
appendString("&quot;")
start = utf8.index(after: current)
default:
()
}
}

if start < utf8.endIndex {
append(contentsOf: utf8[start ..< utf8.endIndex])
}
}
}

@inline(__always)
mutating func appendEscapedText(_ value: consuming String) {
for byte in value.utf8 {
switch byte {
case 38: // &
append(contentsOf: "&amp;".utf8)
case 60: // <
append(contentsOf: "&lt;".utf8)
case 62: // >
append(contentsOf: "&gt;".utf8)
default:
append(byte)
value.withUTF8 { utf8 in
var start = utf8.startIndex

for current in utf8.indices {
switch utf8[current] {
case 38: // &
append(contentsOf: utf8[start ..< current])
appendString("&amp;")
start = utf8.index(after: current)
case 60: // <
append(contentsOf: utf8[start ..< current])
appendString("&lt;")
start = utf8.index(after: current)
case 62: // >
append(contentsOf: utf8[start ..< current])
appendString("&gt;")
start = utf8.index(after: current)
default:
()
}
}

if start < utf8.endIndex {
append(contentsOf: utf8[start ..< utf8.endIndex])
}
}
}
Expand Down
3 changes: 0 additions & 3 deletions Tests/ElementaryTests/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,3 @@ func HTMLAssertEqualAsyncOnly(_ html: some HTML, _ expected: String, file: Stati
func HTMLFormattedAssertEqual(_ html: some HTML, _ expected: String, file: StaticString = #filePath, line: UInt = #line) {
XCTAssertEqual(expected, html.renderFormatted(), file: file, line: line)
}

@inline(never)
func blackHole<T>(_: T) {}