Skip to content

Commit d6ddd26

Browse files
authored
speed up hot-path utf8 processing (#52)
1 parent 4ab9270 commit d6ddd26

File tree

4 files changed

+73
-34
lines changed

4 files changed

+73
-34
lines changed

Sources/Elementary/Core/AttributeStorage.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ public struct _MergedAttributes: Sequence, Sendable {
100100
Iterator(storage)
101101
}
102102

103+
public var isEmpty: Bool {
104+
storage.isEmpty
105+
}
106+
103107
public struct Iterator: IteratorProtocol {
104108
enum State {
105109
case empty

Sources/Elementary/Core/Html+Elements.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ extension HTMLComment: Sendable {}
166166
extension HTMLRaw: Sendable {}
167167

168168
extension HTMLTagDefinition {
169-
@usableFromInline
169+
@usableFromInline @inline(__always)
170170
static var renderingType: _HTMLRenderToken.RenderingType {
171171
_rendersInline ? .inline : .block
172172
}

Sources/Elementary/Rendering/RenderingUtils.swift

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,61 +23,99 @@ extension _RenderingContext {
2323
// I do not know why this function does not work in embedded, but currently it crashes the compiler
2424
#if !hasFeature(Embedded)
2525
extension [UInt8] {
26+
@inline(__always)
2627
mutating func appendToken(_ token: consuming _HTMLRenderToken) {
2728
// avoid strings and append each component directly
2829
switch token {
2930
case let .startTag(tagName, attributes: attributes, isUnpaired: _, type: _):
3031
append(60) // <
31-
append(contentsOf: tagName.utf8)
32-
for attribute in attributes {
33-
append(32) // space
34-
append(contentsOf: attribute.name.utf8)
35-
if let value = attribute.value {
36-
append(contentsOf: [61, 34]) // ="
37-
appendEscapedAttributeValue(value)
38-
append(34) // "
32+
appendString(tagName)
33+
if !attributes.isEmpty {
34+
for attribute in attributes {
35+
append(32) // space
36+
appendString(attribute.name)
37+
if let value = attribute.value {
38+
append(contentsOf: [61, 34]) // ="
39+
appendEscapedAttributeValue(value)
40+
append(34) // "
41+
}
3942
}
4043
}
4144
append(62) // >
4245
case let .endTag(tagName, _):
4346
append(contentsOf: [60, 47]) // </
44-
append(contentsOf: tagName.utf8)
47+
appendString(tagName)
4548
append(62) // >
4649
case let .text(text):
4750
appendEscapedText(text)
4851
case let .raw(raw):
49-
append(contentsOf: raw.utf8)
52+
appendString(raw)
5053
case let .comment(comment):
51-
append(contentsOf: "<!--".utf8)
54+
appendString("<!--")
5255
appendEscapedText(comment)
53-
append(contentsOf: "-->".utf8)
56+
appendString("-->")
57+
}
58+
}
59+
60+
@inline(__always)
61+
mutating func appendString(_ string: consuming String) {
62+
string.withUTF8 { utf8 in
63+
append(contentsOf: utf8)
5464
}
5565
}
5666

67+
@inline(__always)
5768
mutating func appendEscapedAttributeValue(_ value: consuming String) {
58-
for byte in value.utf8 {
59-
switch byte {
60-
case 38: // &
61-
append(contentsOf: "&amp;".utf8)
62-
case 34: // "
63-
append(contentsOf: "&quot;".utf8)
64-
default:
65-
append(byte)
69+
value.withUTF8 { utf8 in
70+
var start = utf8.startIndex
71+
72+
for current in utf8.indices {
73+
switch utf8[current] {
74+
case 38: // &
75+
append(contentsOf: utf8[start ..< current])
76+
appendString("&amp;")
77+
start = utf8.index(after: current)
78+
case 34: // "
79+
append(contentsOf: utf8[start ..< current])
80+
appendString("&quot;")
81+
start = utf8.index(after: current)
82+
default:
83+
()
84+
}
85+
}
86+
87+
if start < utf8.endIndex {
88+
append(contentsOf: utf8[start ..< utf8.endIndex])
6689
}
6790
}
6891
}
6992

93+
@inline(__always)
7094
mutating func appendEscapedText(_ value: consuming String) {
71-
for byte in value.utf8 {
72-
switch byte {
73-
case 38: // &
74-
append(contentsOf: "&amp;".utf8)
75-
case 60: // <
76-
append(contentsOf: "&lt;".utf8)
77-
case 62: // >
78-
append(contentsOf: "&gt;".utf8)
79-
default:
80-
append(byte)
95+
value.withUTF8 { utf8 in
96+
var start = utf8.startIndex
97+
98+
for current in utf8.indices {
99+
switch utf8[current] {
100+
case 38: // &
101+
append(contentsOf: utf8[start ..< current])
102+
appendString("&amp;")
103+
start = utf8.index(after: current)
104+
case 60: // <
105+
append(contentsOf: utf8[start ..< current])
106+
appendString("&lt;")
107+
start = utf8.index(after: current)
108+
case 62: // >
109+
append(contentsOf: utf8[start ..< current])
110+
appendString("&gt;")
111+
start = utf8.index(after: current)
112+
default:
113+
()
114+
}
115+
}
116+
117+
if start < utf8.endIndex {
118+
append(contentsOf: utf8[start ..< utf8.endIndex])
81119
}
82120
}
83121
}

Tests/ElementaryTests/Utilities.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,3 @@ func HTMLAssertEqualAsyncOnly(_ html: some HTML, _ expected: String, file: Stati
1515
func HTMLFormattedAssertEqual(_ html: some HTML, _ expected: String, file: StaticString = #filePath, line: UInt = #line) {
1616
XCTAssertEqual(expected, html.renderFormatted(), file: file, line: line)
1717
}
18-
19-
@inline(never)
20-
func blackHole<T>(_: T) {}

0 commit comments

Comments
 (0)