Skip to content

Commit

Permalink
add benchmarks and performance improvements (#33)
Browse files Browse the repository at this point in the history
* added benchmark package

* added more benchmarks

* added @_transparent

* changed benchmark

* text rendering improvements

* less allocations for buffer
  • Loading branch information
sliemeobn authored Sep 17, 2024
1 parent c83b507 commit 41512d0
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 103 deletions.
9 changes: 9 additions & 0 deletions Benchmarks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.benchmarkBaselines
233 changes: 233 additions & 0 deletions Benchmarks/Benchmarks/ElementaryBenchmarks/Benchmarks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
@preconcurrency import Benchmark
import Elementary

let benchmarks = { @Sendable in
Benchmark.defaultConfiguration = .init(
metrics: [.wallClock, .mallocCountTotal, .instructions, .throughput],
scalingFactor: .kilo
)

Benchmark("initialize nested html tags") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(makeNestedTagsElement())
}
}

Benchmark("initialize single attribute elements") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(makeSingleAttributeElements())
}
}

Benchmark("initialize multi attribute elements") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(makeMultiAttributeElements())
}
}

Benchmark("render nested html tags") { benchmark in
let element = makeNestedTagsElement()
benchmark.startMeasurement()
for _ in benchmark.scaledIterations {
try blackHole(await element.renderAsync())
}
}

Benchmark("render nested custom elements") { benchmark in
let element = makeNestedCustomElement()
benchmark.startMeasurement()
for _ in benchmark.scaledIterations {
try blackHole(await element.renderAsync())
}
}

Benchmark("render single attribute elements") { benchmark in
let element = makeSingleAttributeElements()
benchmark.startMeasurement()
for _ in benchmark.scaledIterations {
try blackHole(await element.renderAsync())
}
}

Benchmark("render multi attribute elements") { benchmark in
let element = makeMultiAttributeElements()
benchmark.startMeasurement()
for _ in benchmark.scaledIterations {
try blackHole(await element.renderAsync())
}
}

Benchmark("render nested html tags (sync)") { benchmark in
let element = makeNestedTagsElement()
benchmark.startMeasurement()
for _ in benchmark.scaledIterations {
blackHole(element.render())
}
}

Benchmark("render merged attributes") { benchmark in
benchmark.startMeasurement()
for _ in benchmark.scaledIterations {
try blackHole(await p {
img(.class("1"), .style("1"), .id("old"))
.attributes(.class("2"), .style("2"))
.attributes(.id("new"))
}.renderAsync())
}
}

Benchmark("render tuples") { benchmark in
for _ in benchmark.scaledIterations {
try blackHole(await div {
img()
img()
img()
img()
div {
img()
img()
img()
img()
div {
img()
img()
}
}
}.renderAsync())
}
}

Benchmark("render array") { benchmark in
for _ in benchmark.scaledIterations {
try blackHole(await div {
for i in 0 ..< 1000 {
img(.id("\(i)"))
}
}.renderAsync())
}
}

Benchmark("render sequence") { benchmark in
for _ in benchmark.scaledIterations {
try blackHole(await div {
ForEach(0 ..< 1000) { i in
img(.id("\(i)"))
}
}.renderAsync())
}
}

Benchmark("render text") { benchmark in
for _ in benchmark.scaledIterations {
try blackHole(await div {
"Hello, World!"
"This is a paragraph."
"Some interpolation \("maybe")"
i { "Italic" }
b { "Bold" }
}.renderAsync())
}
}

Benchmark("render full document") { benchmark in
for _ in benchmark.scaledIterations {
try blackHole(
await 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")
}
}
}
}.renderAsync())
}
}
}

func makeNestedTagsElement() -> some HTML {
body {
main {
section {
div {
p {
span {
i {}
}
}
}
}
}
}
}

func makeNestedCustomElement() -> some HTML {
MyCustomElement {
MyCustomElement {
MyCustomElement {
MyCustomElement {}
}
}
}
}

func makeMultiAttributeElements() -> some HTML {
div(.id("1"), .class("2")) {
div(.id("3"), .class("4")) {
div(.id("5"), .class("6")) {
div(.id("7"), .class("8")) {}
}
}
}
}

func makeSingleAttributeElements() -> some HTML {
div(.id("1")) {
div(.id("3")) {
div(.id("5")) {
div(.id("7")) {}
}
}
}
}

struct MyCustomElement<H: HTML>: HTML {
var myContent: H

init(@HTMLBuilder content: () -> H) {
myContent = content()
}

var content: some HTML {
myContent
}
}

struct MyListItem: HTML {
let number: Int

var content: some HTML {
let isEven = number.isMultiple(of: 2)

li(.id("\(number)")) {
if isEven {
"Even Item \(number)"
} else {
"Item \(number)"
}
}.attributes(.class("even"), when: isEven)
}
}
25 changes: 25 additions & 0 deletions Benchmarks/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
name: "Benchmarks",
platforms: [.macOS(.v14)],
products: [],
dependencies: [
.package(url: "https://github.com/ordo-one/package-benchmark", from: "1.0.0"),
.package(path: "../"),
],
targets: [
.executableTarget(
name: "ElementaryBenchmarks",
dependencies: [
.product(name: "Benchmark", package: "package-benchmark"),
.product(name: "Elementary", package: "Elementary"),
],
path: "Benchmarks/ElementaryBenchmarks",
plugins: [
.plugin(name: "BenchmarkPlugin", package: "package-benchmark"),
]
),
]
)
5 changes: 2 additions & 3 deletions Sources/Elementary/Core/CoreModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ public protocol HTML<Tag> {
/// The HTML content of this component.
@HTMLBuilder var content: Content { get }

@_spi(Rendering)
static func _render<Renderer: _HTMLRendering>(_ html: consuming Self, into renderer: inout Renderer, with context: consuming _RenderingContext)

@_spi(Rendering)
static func _render<Renderer: _AsyncHTMLRendering>(_ html: consuming Self, into renderer: inout Renderer, with context: consuming _RenderingContext) async throws
}

Expand Down Expand Up @@ -81,10 +78,12 @@ public protocol _AsyncHTMLRendering {
}

public extension HTML {
@_transparent
static func _render<Renderer: _HTMLRendering>(_ html: consuming Self, into renderer: inout Renderer, with context: consuming _RenderingContext) {
Content._render(html.content, into: &renderer, with: context)
}

@_transparent
static func _render<Renderer: _AsyncHTMLRendering>(_ html: consuming Self, into renderer: inout Renderer, with context: consuming _RenderingContext) async throws {
try await Content._render(html.content, into: &renderer, with: context)
}
Expand Down
14 changes: 8 additions & 6 deletions Sources/Elementary/Rendering/HtmlAsyncRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ struct AsyncHTMLRenderer<Writer: HTMLStreamWriter>: _AsyncHTMLRendering {
init(writer: Writer, chunkSize: Int) {
self.writer = writer
self.chunkSize = chunkSize
buffer.reserveCapacity(chunkSize)
// add some extra space as we do not know or check the exact size of the tokens before adding to the chunk buffer
// ideally, this avoids unnecessary reallocations
buffer.reserveCapacity(chunkSize + 500)
}

mutating func appendToken(_ token: consuming _HTMLRenderToken) async throws {
let value = token.renderedValue.utf8
if value.count + buffer.count > buffer.capacity {
// let value = token.renderedValue.utf8
buffer.appendToken(token)
if buffer.count >= buffer.capacity {
try await flush()
buffer.replaceSubrange(0 ... buffer.count - 1, with: value)
} else {
buffer.append(contentsOf: value)
buffer.replaceSubrange(0 ... buffer.count - 1, with: [])
}
}

Expand All @@ -27,6 +28,7 @@ struct AsyncHTMLRenderer<Writer: HTMLStreamWriter>: _AsyncHTMLRendering {

final class BufferWriter: HTMLStreamWriter {
var result: [UInt8] = []

func write(_ bytes: ArraySlice<UInt8>) async throws {
result.append(contentsOf: bytes)
}
Expand Down
15 changes: 10 additions & 5 deletions Sources/Elementary/Rendering/HtmlTextRenderer.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
struct HTMLTextRenderer: _HTMLRendering {
private var result = ""
private var result: [UInt8] = []

init() {
// gotta start somewhere
result.reserveCapacity(1024)
}

mutating func appendToken(_ token: consuming _HTMLRenderToken) {
result.append(token.renderedValue)
result.appendToken(token)
}

consuming func collect() -> String {
result
String(decoding: result, as: UTF8.self)
}
}

Expand All @@ -15,7 +20,7 @@ struct HTMLStreamRenderer: _HTMLRendering {
let writer: (String) -> Void

mutating func appendToken(_ token: consuming _HTMLRenderToken) {
writer(token.renderedValue)
writer(token.renderedValue())
}
}

Expand Down Expand Up @@ -79,7 +84,7 @@ struct PrettyHTMLTextRenderer {

extension PrettyHTMLTextRenderer: _HTMLRendering {
mutating func appendToken(_ token: consuming _HTMLRenderToken) {
let renderedToken = token.renderedValue
let renderedToken = token.renderedValue()

if token.shouldInline(currentlyInlined: isInLineAfterBlockTagOpen || !currentInlineText.isEmpty) {
if !isInLineAfterBlockTagOpen {
Expand Down
Loading

0 comments on commit 41512d0

Please sign in to comment.