Skip to content

Commit

Permalink
improved performance (#49)
Browse files Browse the repository at this point in the history
* StaticString tagNames and tighter attribute render loop

* inlinable attribute constructors

* inlinable on all generic attribute initializers

* benchmark timeouts

* stick to strings for tags (staticstring performs better, but only by a few percent in realistic cases)

* a bit of cleanup

* adjusted formatted rendering
  • Loading branch information
sliemeobn authored Oct 12, 2024
1 parent 5994b0c commit 0a0ebb3
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 175 deletions.
31 changes: 30 additions & 1 deletion Benchmarks/Benchmarks/ElementaryBenchmarks/Benchmarks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import Elementary
let benchmarks = { @Sendable in
Benchmark.defaultConfiguration = .init(
metrics: [.wallClock, .mallocCountTotal, .instructions, .throughput],
scalingFactor: .kilo
scalingFactor: .kilo,
maxDuration: .seconds(5)
)

Benchmark("initialize nested html tags") { benchmark in
Expand Down Expand Up @@ -156,6 +157,34 @@ let benchmarks = { @Sendable in
}.renderAsync())
}
}

Benchmark("render full document (sync)") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(
html {
head {
title { "Hello, World!" }
meta(.name("viewport"), .content("width=device-width, initial-scale=1"))
link(.rel("stylesheet"), .href("styles.css"))
}
body {
h1(.class("hello")) { "Hello, World!" }
p { "This is a paragraph." }
a(.href("https://swift.org")) {
"Swift"
}
ul(.class("fancy-list")) {
ForEach(0 ..< 1000) { i in
MyCustomElement {
MyListItem(number: i)
}
HTMLComment("This is a comment")
}
}
}
}.render())
}
}
}

func makeNestedTagsElement() -> some HTML {
Expand Down
159 changes: 91 additions & 68 deletions Sources/Elementary/Core/AttributeStorage.swift
Original file line number Diff line number Diff line change
@@ -1,36 +1,55 @@
struct StoredAttribute: Equatable, Sendable {
enum MergeMode: Equatable {
public struct _StoredAttribute: Equatable, Sendable {
@usableFromInline
enum MergeMode: Equatable, Sendable {
case appendValue(_ separator: String = " ")
case replaceValue
case ignoreIfSet
}

var name: String
var value: String?
public var name: String
public var value: String?
@usableFromInline
var mergeMode: MergeMode = .replaceValue

mutating func appending(value: String?, separatedBy separator: String) {
self.value = switch (self.value, value) {
case (_, .none): self.value
case let (.none, .some(value)): value
case let (.some(existingValue), .some(otherValue)): "\(existingValue)\(separator)\(otherValue)"
@usableFromInline
init(name: String, value: String? = nil, mergeMode: MergeMode = .replaceValue) {
self.name = name
self.value = value
self.mergeMode = mergeMode
}

mutating func mergeWith(_ attribute: consuming _StoredAttribute) {
switch attribute.mergeMode {
case let .appendValue(separator):
value = switch (value, attribute.value) {
case (_, .none): value
case let (.none, .some(value)): value
case let (.some(existingValue), .some(otherValue)): "\(existingValue)\(separator)\(otherValue)"
}
case .replaceValue:
value = attribute.value
case .ignoreIfSet:
break
}
}
}

enum AttributeStorage: Sendable {
public enum _AttributeStorage: Sendable {
case none
case single(StoredAttribute)
case multiple([StoredAttribute])
case single(_StoredAttribute)
case multiple([_StoredAttribute])

@inlinable
init() {
self = .none
}

@inlinable
init(_ attribute: HTMLAttribute<some HTMLTagDefinition>) {
self = .single(attribute.htmlAttribute)
}

@inlinable
init(_ attributes: [HTMLAttribute<some HTMLTagDefinition>]) {
switch attributes.count {
case 0: self = .none
Expand All @@ -39,15 +58,15 @@ enum AttributeStorage: Sendable {
}
}

var isEmpty: Bool {
public var isEmpty: Bool {
switch self {
case .none: return true
case .single: return false
case let .multiple(attributes): return attributes.isEmpty // just to be sure...
}
}

mutating func append(_ attributes: consuming AttributeStorage) {
public mutating func append(_ attributes: consuming _AttributeStorage) {
// maybe this was a bad idea....
switch (self, attributes) {
case (_, .none):
Expand All @@ -68,89 +87,93 @@ enum AttributeStorage: Sendable {
}
}

consuming func flattened() -> FlattenedAttributeView {
public consuming func flattened() -> _MergedAttributes {
.init(storage: self)
}
}

extension AttributeStorage {
struct FlattenedAttributeView: Sequence, Sendable {
typealias Element = StoredAttribute
var storage: AttributeStorage
public struct _MergedAttributes: Sequence, Sendable {
public typealias Element = _StoredAttribute
var storage: _AttributeStorage

consuming func makeIterator() -> Iterator {
Iterator(storage)
}
public consuming func makeIterator() -> Iterator {
Iterator(storage)
}

struct Iterator: IteratorProtocol {
enum State {
case empty
case single(StoredAttribute)
case flattening([StoredAttribute], Int)
case _temporaryNothing
}
public struct Iterator: IteratorProtocol {
enum State {
case empty
case single(_StoredAttribute)
case flattening([_StoredAttribute], Int)
case _temporaryNothing
}

var state: State
var state: State

init(_ storage: consuming AttributeStorage) {
switch storage {
case .none: state = .empty
case let .single(attribute): state = .single(attribute)
case let .multiple(attributes): state = .flattening(attributes, 0)
}
init(_ storage: consuming _AttributeStorage) {
switch storage {
case .none: state = .empty
case let .single(attribute): state = .single(attribute)
case let .multiple(attributes): state = .flattening(attributes, 0)
}
}

mutating func next() -> StoredAttribute? {
switch state {
case .empty: return nil
case let .single(attribute):
public mutating func next() -> _StoredAttribute? {
switch state {
case .empty: return nil
case let .single(attribute):
state = .empty
return attribute
case .flattening(var list, let index):
state = ._temporaryNothing
let (attribute, newIndex) = nextflattenedAttribute(attributes: &list, from: index)

if let newIndex {
state = .flattening(list, newIndex)
} else {
state = .empty
return attribute
case .flattening(var list, let index):
state = ._temporaryNothing
let (attribute, newIndex) = nextflattenedAttribute(attributes: &list, from: index)

if let newIndex {
state = .flattening(list, newIndex)
} else {
state = .empty
}

return attribute
case ._temporaryNothing:
fatalError("unexpected _temporaryNothing state")
}

return attribute
case ._temporaryNothing:
fatalError("unexpected _temporaryNothing state")
}
}
}
}

private let blankedOut = StoredAttribute(name: "")
private func nextflattenedAttribute(attributes: inout [StoredAttribute], from index: Int) -> (StoredAttribute, Int?) {
var attribute = attributes[index]
attributes[index] = blankedOut
private func nextflattenedAttribute(attributes: inout [_StoredAttribute], from index: Int) -> (_StoredAttribute, Int?) {
var attribute: _StoredAttribute = .blankedOut
swap(&attribute, &attributes[index])

var nextIndex: Int?

for j in attributes.indices[(index + 1)...] {
// fast-skip blanked out attributes
guard !attributes[j].name.isEmpty else { continue }

guard attributes[j].name == attribute.name else {
guard attributes[j].name.utf8Equals(attribute.name) else {
if nextIndex == nil { nextIndex = j }
continue
}

switch attributes[j].mergeMode {
case let .appendValue(separator):
attribute.appending(value: attributes[j].value, separatedBy: separator)
case .replaceValue:
attribute.value = attributes[j].value
case .ignoreIfSet:
break
}
attributes[j] = blankedOut
var mergedAttribute: _StoredAttribute = .blankedOut
swap(&mergedAttribute, &attributes[j])

attribute.mergeWith(mergedAttribute)
}

return (attribute, nextIndex)
}

private extension _StoredAttribute {
static let blankedOut = _StoredAttribute(name: "")
}

private extension String {
@inline(__always)
func utf8Equals(_ other: borrowing String) -> Bool {
// for embedded support
utf8.elementsEqual(other.utf8)
}
}
6 changes: 2 additions & 4 deletions Sources/Elementary/Core/CoreModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ extension Never: HTMLTagDefinition {
}

public struct _RenderingContext {
var attributes: AttributeStorage
var attributes: _AttributeStorage

public static var emptyContext: Self { Self(attributes: .none) }
}
Expand All @@ -60,9 +60,7 @@ public enum _HTMLRenderToken {
case inline
}

case startTagOpen(String, type: RenderingType)
case attribute(name: String, value: String?)
case startTagClose(isUnpaired: Bool = false)
case startTag(String, attributes: _MergedAttributes, isUnpaired: Bool, type: RenderingType)
case endTag(String, type: RenderingType)
case text(String)
case raw(String)
Expand Down
26 changes: 22 additions & 4 deletions Sources/Elementary/Core/Html+Attributes.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// An HTML attribute that can be applied to an HTML element of the associated tag.
public struct HTMLAttribute<Tag: HTMLTagDefinition>: Sendable {
var htmlAttribute: StoredAttribute
@usableFromInline
var htmlAttribute: _StoredAttribute

/// The name of the attribute.
public var name: String { htmlAttribute.name }
Expand All @@ -11,7 +12,12 @@ public struct HTMLAttribute<Tag: HTMLTagDefinition>: Sendable {

/// The action to take when merging an attribute with the same name.
public struct HTMLAttributeMergeAction: Sendable {
var mergeMode: StoredAttribute.MergeMode
@usableFromInline
var mergeMode: _StoredAttribute.MergeMode

init(mergeMode: _StoredAttribute.MergeMode) {
self.mergeMode = mergeMode
}

/// Replaces the value of the existing attribute with the new value.
public static var replacing: Self { .init(mergeMode: .replaceValue) }
Expand All @@ -29,13 +35,15 @@ public extension HTMLAttribute {
/// - name: The name of the attribute.
/// - value: The value of the attribute.
/// - action: The merge action to use with a previously attached attribute with the same name.
@inlinable
init(name: String, value: String?, mergedBy action: HTMLAttributeMergeAction = .replacing) {
htmlAttribute = .init(name: name, value: value, mergeMode: action.mergeMode)
}

/// Changes the default merge action of this attribute.
/// - Parameter action: The new merge action to use.
/// - Returns: A modified attribute with the specified merge action.
@inlinable
consuming func mergedBy(_ action: HTMLAttributeMergeAction) -> HTMLAttribute {
.init(name: name, value: value, mergedBy: action)
}
Expand All @@ -44,7 +52,14 @@ public extension HTMLAttribute {
public struct _AttributedElement<Content: HTML>: HTML {
public var content: Content

var attributes: AttributeStorage
@usableFromInline
var attributes: _AttributeStorage

@usableFromInline
init(content: Content, attributes: _AttributeStorage) {
self.content = content
self.attributes = attributes
}

@_spi(Rendering)
public static func _render<Renderer: _HTMLRendering>(_ html: consuming Self, into renderer: inout Renderer, with context: consuming _RenderingContext) {
Expand All @@ -67,6 +82,7 @@ public extension HTML where Tag: HTMLTrait.Attributes.Global {
/// - attribute: The attribute to add to the element.
/// - condition: If set to false, the attribute will not be added.
/// - Returns: A new element with the specified attribute added.
@inlinable
func attributes(_ attribute: HTMLAttribute<Tag>, when condition: Bool = true) -> _AttributedElement<Self> {
if condition {
return _AttributedElement(content: self, attributes: .init(attribute))
Expand All @@ -80,6 +96,7 @@ public extension HTML where Tag: HTMLTrait.Attributes.Global {
/// - attributes: The attributes to add to the element.
/// - condition: If set to false, the attributes will not be added.
/// - Returns: A new element with the specified attributes added.
@inlinable
func attributes(_ attributes: HTMLAttribute<Tag>..., when condition: Bool = true) -> _AttributedElement<Self> {
_AttributedElement(content: self, attributes: .init(condition ? attributes : []))
}
Expand All @@ -89,13 +106,14 @@ public extension HTML where Tag: HTMLTrait.Attributes.Global {
/// - attributes: The attributes to add to the element as an array.
/// - condition: If set to false, the attributes will not be added.
/// - Returns: A new element with the specified attributes added.
@inlinable
func attributes(contentsOf attributes: [HTMLAttribute<Tag>], when condition: Bool = true) -> _AttributedElement<Self> {
_AttributedElement(content: self, attributes: .init(condition ? attributes : []))
}
}

private extension _RenderingContext {
mutating func prependAttributes(_ attributes: consuming AttributeStorage) {
mutating func prependAttributes(_ attributes: consuming _AttributeStorage) {
attributes.append(self.attributes)
self.attributes = attributes
}
Expand Down
Loading

0 comments on commit 0a0ebb3

Please sign in to comment.