Skip to content

Commit 2817cef

Browse files
moved some code around; preparing to benchmark networking throughput and...
- tested some alternative dynamic implementations - merged `ErrorDiagnostic` and `SimpleDiagnosticMessage` in `HTMLKitMacros.swift`
1 parent a2d6dc6 commit 2817cef

File tree

6 files changed

+196
-69
lines changed

6 files changed

+196
-69
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// Networking.swift
3+
//
4+
//
5+
// Created by Evan Anderson on 10/10/24.
6+
//
7+
8+
import Benchmark
9+
import Utilities
10+
11+
import TestElementary
12+
import TestPlot
13+
import TestSwiftHTMLBB
14+
import TestSwiftHTMLKit
15+
import TestSwiftHTMLPF
16+
import TestSwim
17+
import TestToucan
18+
import TestVaporHTMLKit
19+
import TestVaux
20+
21+
import Hummingbird
22+
import Vapor
23+
24+
let benchmarks = {
25+
Benchmark.defaultConfiguration = .init(metrics: .all)
26+
27+
let libraries:[String:HTMLGenerator] = [
28+
"BinaryBirds" : SwiftHTMLBBTests(),
29+
"Elementary" : ElementaryTests(),
30+
"Plot" : PlotTests(),
31+
"Pointfreeco" : SwiftHTMLPFTests(),
32+
"SwiftHTMLKit" : SwiftHTMLKitTests(),
33+
"Swim" : SwimTests(),
34+
"VaporHTMLKit" : VaporHTMLKitTests(),
35+
"Vaux" : VauxTests()
36+
]
37+
/*
38+
// hummingbird
39+
let router:Hummingbird.Router = Hummingbird.Router()
40+
for (library, value) in libraries {
41+
router.get(RouterPath(library)) { request, _ -> String in
42+
return value.staticHTML()
43+
}
44+
router.get(RouterPath("d" + library)) { request, _ -> String in
45+
return value.dynamicHTML(HTMLContext())
46+
}
47+
}
48+
let app:Hummingbird.Application = Hummingbird.Application(router: router, configuration: .init(address: .hostname("127.0.0.1", port: 8080)))
49+
Task {
50+
try! await Task.sleep(for: .seconds(5))
51+
for (key, value) in libraries {
52+
let request_s:Hummingbird.Request = Request(head: HTTPRequest(method: .get, scheme: nil, authority: nil, path: "http://127.0.0.1:8080/" + key), body: RequestBody(buffer: ByteBuffer()))
53+
let request_d:Hummingbird.Request = Request(head: HTTPRequest(method: .get, scheme: nil, authority: nil, path: "http://127.0.0.1:8080/d" + key), body: RequestBody(buffer: ByteBuffer()))
54+
Benchmark(key) {
55+
for _ in $0.scaledIterations {
56+
//blackHole()
57+
}
58+
}
59+
Benchmark("d" + key) {
60+
for _ in $0.scaledIterations {
61+
//blackHole()
62+
}
63+
}
64+
}
65+
}
66+
try await app.runService()*/
67+
}

Benchmarks/Benchmarks/Run/main.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// main.swift
3+
//
4+
//
5+
// Created by Evan Anderson on 10/10/24.
6+
//
7+
8+
import Benchmark
9+
import Utilities
10+
11+
import TestElementary
12+
import TestPlot
13+
import TestSwiftHTMLBB
14+
import TestSwiftHTMLKit
15+
import TestSwiftHTMLPF
16+
import TestSwim
17+
import TestToucan
18+
import TestVaporHTMLKit
19+
import TestVaux

Benchmarks/Benchmarks/SwiftHTMLKit/SwiftHTMLKit.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,19 @@ package struct SwiftHTMLKitTests : HTMLGenerator {
2121
)
2222
)
2323
}
24+
// performance notes
25+
// - maping makes unneccessary copies and hurts throughput
26+
// - interpolation hurts performance, a lot less than maping but still noticeable
27+
// - adding strings (concatenation) is faster than interpolation
28+
// - calculating the size of the result than assigning the contents in a String is significantly worse than interpolation and concatenation
29+
// - calculating the size of the result than assigning the contents in a Data is about the same performance, if not faster, as SwiftHTMLKit native solution with interpolation/concatenation
2430
package func dynamicHTML(_ context: HTMLContext) -> String {
2531
var qualities:String = ""
2632
for quality in context.user.qualities {
2733
qualities += #li("\(quality)")
34+
//qualities += "<li>" + quality + "</li>"
2835
}
36+
//return "<!DOCTYPE html><html><body><h1>" + context.heading + "</h1><div id=\"" + context.desc_id + "\"><p>" + context.string + "</p></div><h2>" + context.user.details_heading + "</h2><h3>" + context.user.qualities_heading + "</h3><ul id=\"" + context.user.qualities_id + "\">" + qualities + "</ul></body></html>"
2937
return #html(
3038
#body(
3139
#h1("\(context.heading)"),

Benchmarks/Package.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,27 @@ let package = Package(
112112
],
113113
path: "Benchmarks/Vaux"
114114
),
115+
.executableTarget(
116+
name: "Run",
117+
dependencies: [
118+
"Utilities",
119+
120+
"TestElementary",
121+
"TestLeaf",
122+
"TestPlot",
123+
"TestSwiftHTMLBB",
124+
"TestSwiftHTMLKit",
125+
"TestSwiftHTMLPF",
126+
"TestSwim",
127+
"TestToucan",
128+
"TestVaporHTMLKit",
129+
"TestVaux",
130+
.product(name: "Benchmark", package: "package-benchmark"),
131+
.product(name: "Vapor", package: "vapor"),
132+
.product(name: "Hummingbird", package: "hummingbird")
133+
],
134+
path: "Benchmarks/Run"
135+
),
115136

116137
.testTarget(
117138
name: "UnitTests",
@@ -138,6 +159,7 @@ let package = Package(
138159
name: "Benchmarks",
139160
dependencies: [
140161
"Utilities",
162+
141163
"TestElementary",
142164
"TestLeaf",
143165
"TestPlot",
@@ -154,6 +176,30 @@ let package = Package(
154176
plugins: [
155177
.plugin(name: "BenchmarkPlugin", package: "package-benchmark")
156178
]
179+
),
180+
.executableTarget(
181+
name: "Networking",
182+
dependencies: [
183+
"Utilities",
184+
185+
"TestElementary",
186+
"TestLeaf",
187+
"TestPlot",
188+
"TestSwiftHTMLBB",
189+
"TestSwiftHTMLKit",
190+
"TestSwiftHTMLPF",
191+
"TestSwim",
192+
"TestToucan",
193+
"TestVaporHTMLKit",
194+
"TestVaux",
195+
.product(name: "Benchmark", package: "package-benchmark"),
196+
.product(name: "Vapor", package: "vapor"),
197+
.product(name: "Hummingbird", package: "hummingbird")
198+
],
199+
path: "Benchmarks/Networking",
200+
plugins: [
201+
.plugin(name: "BenchmarkPlugin", package: "package-benchmark")
202+
]
157203
)
158204
]
159205
)

Sources/HTMLKitMacros/HTMLElement.swift

Lines changed: 54 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,34 @@ struct HTMLElement : ExpressionMacro {
1717
}
1818

1919
private extension HTMLElement {
20+
static func parse_element_macro(context: some MacroExpansionContext, expression: MacroExpansionExprSyntax) -> String {
21+
guard let elementType:HTMLElementType = HTMLElementType(rawValue: expression.macroName.text) else { return "\(expression)" }
22+
let childs:SyntaxChildren = expression.arguments.children(viewMode: .all)
23+
if elementType == .escapeHTML {
24+
return childs.compactMap({
25+
guard let child:LabeledExprSyntax = $0.labeled else { return nil }
26+
return parse_inner_html(context: context, elementType: elementType, child: child)
27+
}).joined()
28+
}
29+
let tag:String, isVoid:Bool
30+
var children:Slice<SyntaxChildren>
31+
if elementType == .custom {
32+
tag = childs.first(where: { $0.labeled?.label?.text == "tag" })!.labeled!.expression.stringLiteral!.string
33+
isVoid = childs.first(where: { $0.labeled?.label?.text == "isVoid" })!.labeled!.expression.booleanLiteral!.literal.text == "true"
34+
children = childs.dropFirst() // tag
35+
children.removeFirst() // isVoid
36+
} else {
37+
tag = elementType.rawValue
38+
isVoid = elementType.isVoid
39+
children = childs.prefix(childs.count)
40+
}
41+
let data:ElementData = parse_arguments(context: context, elementType: elementType, children: children)
42+
var string:String = (elementType == .html ? "<!DOCTYPE html>" : "") + "<" + tag + data.attributes + ">" + data.innerHTML
43+
if !isVoid {
44+
string += "</" + tag + ">"
45+
}
46+
return string
47+
}
2048
static func parse_arguments(context: some MacroExpansionContext, elementType: HTMLElementType, children: Slice<SyntaxChildren>) -> ElementData {
2149
var attributes:[String] = [], innerHTML:[String] = []
2250
for element in children {
@@ -41,30 +69,6 @@ private extension HTMLElement {
4169
}
4270
return ElementData(attributes: attributes, innerHTML: innerHTML)
4371
}
44-
static func parse_inner_html(context: some MacroExpansionContext, elementType: HTMLElementType, child: LabeledExprSyntax) -> String? {
45-
if let macro:MacroExpansionExprSyntax = child.expression.macroExpansion {
46-
var string:String = parse_element_macro(context: context, expression: macro)
47-
if elementType == .escapeHTML {
48-
string.escapeHTML(attribute: false)
49-
}
50-
return string
51-
} else if var string:String = child.expression.stringLiteral?.string {
52-
string.escapeHTML(attribute: false)
53-
return string
54-
} else if let function:FunctionCallExprSyntax = child.expression.as(FunctionCallExprSyntax.self), function.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text == "StaticString" {
55-
return function.arguments.first!.expression.stringLiteral!.string.escapingHTML(attribute: false)
56-
} else {
57-
context.diagnose(Diagnostic(node: child, message: ErrorDiagnostic(id: "unallowedExpression", message: "Expression not allowed. String interpolation is required when encoding runtime values."), fixIts: [
58-
FixIt(message: SimpleDiagnosticMessage(id: "useStringInterpolation", message: "Use String Interpolation.", severity: .error), changes: [
59-
FixIt.Change.replace(
60-
oldNode: Syntax(child),
61-
newNode: Syntax(StringLiteralExprSyntax(content: "\\(\(child))"))
62-
)
63-
])
64-
]))
65-
return nil
66-
}
67-
}
6872
static func parse_global_attributes(context: some MacroExpansionContext, elementType: HTMLElementType, array: ArrayExprSyntax) -> [String] {
6973
var keys:Set<String> = [], attributes:[String] = []
7074
for element in array.elements {
@@ -94,10 +98,10 @@ private extension HTMLElement {
9498
break
9599
}
96100
if key.contains(" ") {
97-
context.diagnose(Diagnostic(node: key_element, message: ErrorDiagnostic(id: "spacesNotAllowedInAttributeDeclaration", message: "Spaces are not allowed in attribute declaration.")))
101+
context.diagnose(Diagnostic(node: key_element, message: DiagnosticMsg(id: "spacesNotAllowedInAttributeDeclaration", message: "Spaces are not allowed in attribute declaration.")))
98102
} else if let value:String = value {
99103
if keys.contains(key) {
100-
context.diagnose(Diagnostic(node: key_element, message: ErrorDiagnostic(id: "globalAttributeAlreadyDefined", message: "Global attribute is already defined.")))
104+
context.diagnose(Diagnostic(node: key_element, message: DiagnosticMsg(id: "globalAttributeAlreadyDefined", message: "Global attribute is already defined.")))
101105
} else {
102106
attributes.append(key + (value.isEmpty ? "" : "=\\\"" + value + "\\\""))
103107
keys.insert(key)
@@ -106,33 +110,29 @@ private extension HTMLElement {
106110
}
107111
return attributes
108112
}
109-
static func parse_element_macro(context: some MacroExpansionContext, expression: MacroExpansionExprSyntax) -> String {
110-
guard let elementType:HTMLElementType = HTMLElementType(rawValue: expression.macroName.text) else { return "\(expression)" }
111-
let childs:SyntaxChildren = expression.arguments.children(viewMode: .all)
112-
if elementType == .escapeHTML {
113-
return childs.compactMap({
114-
guard let child:LabeledExprSyntax = $0.labeled else { return nil }
115-
return parse_inner_html(context: context, elementType: elementType, child: child)
116-
}).joined()
117-
}
118-
let tag:String, isVoid:Bool
119-
var children:Slice<SyntaxChildren>
120-
if elementType == .custom {
121-
tag = childs.first(where: { $0.labeled?.label?.text == "tag" })!.labeled!.expression.stringLiteral!.string
122-
isVoid = childs.first(where: { $0.labeled?.label?.text == "isVoid" })!.labeled!.expression.booleanLiteral!.literal.text == "true"
123-
children = childs.dropFirst() // tag
124-
children.removeFirst() // isVoid
113+
static func parse_inner_html(context: some MacroExpansionContext, elementType: HTMLElementType, child: LabeledExprSyntax) -> String? {
114+
if let macro:MacroExpansionExprSyntax = child.expression.macroExpansion {
115+
var string:String = parse_element_macro(context: context, expression: macro)
116+
if elementType == .escapeHTML {
117+
string.escapeHTML(attribute: false)
118+
}
119+
return string
120+
} else if var string:String = child.expression.stringLiteral?.string {
121+
string.escapeHTML(attribute: false)
122+
return string
123+
} else if let function:FunctionCallExprSyntax = child.expression.as(FunctionCallExprSyntax.self), function.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text == "StaticString" {
124+
return function.arguments.first!.expression.stringLiteral!.string.escapingHTML(attribute: false)
125125
} else {
126-
tag = elementType.rawValue
127-
isVoid = elementType.isVoid
128-
children = childs.prefix(childs.count)
129-
}
130-
let data:ElementData = parse_arguments(context: context, elementType: elementType, children: children)
131-
var string:String = (elementType == .html ? "<!DOCTYPE html>" : "") + "<" + tag + data.attributes + ">" + data.innerHTML
132-
if !isVoid {
133-
string += "</" + tag + ">"
126+
context.diagnose(Diagnostic(node: child, message: DiagnosticMsg(id: "unallowedExpression", message: "Expression not allowed. String interpolation is required when encoding runtime values."), fixIts: [
127+
FixIt(message: DiagnosticMsg(id: "useStringInterpolation", message: "Use String Interpolation.", severity: .error), changes: [
128+
FixIt.Change.replace(
129+
oldNode: Syntax(child),
130+
newNode: Syntax(StringLiteralExprSyntax(content: "\\(\(child))"))
131+
)
132+
])
133+
]))
134+
return nil
134135
}
135-
return string
136136
}
137137

138138
struct ElementData {
@@ -228,7 +228,7 @@ private extension HTMLElement {
228228
let separator:String = get_separator(key: key)
229229
let string_return_logic:(ExprSyntax, String) -> String = {
230230
if $1.contains(separator) {
231-
context.diagnose(Diagnostic(node: $0, message: ErrorDiagnostic(id: "characterNotAllowedInDeclaration", message: "Character \"" + separator + "\" is not allowed when declaring values for \"" + key + "\".")))
231+
context.diagnose(Diagnostic(node: $0, message: DiagnosticMsg(id: "characterNotAllowedInDeclaration", message: "Character \"" + separator + "\" is not allowed when declaring values for \"" + key + "\".")))
232232
}
233233
return $1
234234
}
@@ -252,14 +252,14 @@ private extension HTMLElement {
252252
return nil
253253
}
254254
guard var (string, returnType):(String, LiteralReturnType) = return_string_or_interpolation() else {
255-
//context.diagnose(Diagnostic(node: expression, message: ErrorDiagnostic(id: "somethingWentWrong", message: "Something went wrong. (" + expression.debugDescription + ")", severity: .warning)))
255+
//context.diagnose(Diagnostic(node: expression, message: DiagnosticMsg(id: "somethingWentWrong", message: "Something went wrong. (" + expression.debugDescription + ")", severity: .warning)))
256256
return nil
257257
}
258258
if returnType == .interpolation {
259259
string = "\\(" + string + ")"
260260
}
261261
if string.contains("\\(") {
262-
context.diagnose(Diagnostic(node: expression, message: ErrorDiagnostic(id: "unsafeInterpolation", message: "Interpolation may introduce raw HTML elements.", severity: .warning)))
262+
context.diagnose(Diagnostic(node: expression, message: DiagnosticMsg(id: "unsafeInterpolation", message: "Interpolation may introduce raw HTML elements.", severity: .warning)))
263263
}
264264
return (string, returnType)
265265
}

Sources/HTMLKitMacros/HTMLKitMacros.swift

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import SwiftSyntaxMacros
1010
import SwiftDiagnostics
1111

1212
// MARK: ErrorDiagnostic
13-
struct ErrorDiagnostic : DiagnosticMessage {
13+
struct DiagnosticMsg : DiagnosticMessage {
1414
let message:String
1515
let diagnosticID:MessageID
1616
let severity:DiagnosticSeverity
@@ -21,20 +21,7 @@ struct ErrorDiagnostic : DiagnosticMessage {
2121
self.severity = severity
2222
}
2323
}
24-
25-
// MARK: SimpleDiagnosticMessage
26-
struct SimpleDiagnosticMessage : DiagnosticMessage, Error {
27-
let message:String
28-
let diagnosticID:MessageID
29-
let severity:DiagnosticSeverity
30-
31-
init(id: String, message: String, severity: DiagnosticSeverity) {
32-
self.message = message
33-
self.diagnosticID = MessageID(domain: "HTMLKitMacros", id: id)
34-
self.severity = severity
35-
}
36-
}
37-
extension SimpleDiagnosticMessage : FixItMessage {
24+
extension DiagnosticMsg : FixItMessage {
3825
var fixItID : MessageID { diagnosticID }
3926
}
4027

0 commit comments

Comments
 (0)