Skip to content

Commit a95bb3c

Browse files
committed
Create layout performance benchmarks and DummyBackend
1 parent d9cc075 commit a95bb3c

File tree

7 files changed

+969
-2
lines changed

7 files changed

+969
-2
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import SwiftCrossUI
2+
3+
struct Column: View {
4+
var body: some View {
5+
HStack {
6+
Color.orange.frame(width: 5)
7+
Text("Lorem ipsum dolor sit amet.")
8+
}
9+
}
10+
}
11+
12+
struct DoubleColumn: View {
13+
var body: some View {
14+
HStack {
15+
Column()
16+
Column()
17+
}
18+
}
19+
}
20+
21+
struct Row: View {
22+
var body: some View {
23+
HStack {
24+
HStack {
25+
DoubleColumn()
26+
DoubleColumn()
27+
DoubleColumn()
28+
DoubleColumn()
29+
}
30+
HStack {
31+
DoubleColumn()
32+
DoubleColumn()
33+
DoubleColumn()
34+
DoubleColumn()
35+
}
36+
}
37+
}
38+
}
39+
40+
struct DoubleRow: View {
41+
var body: some View {
42+
VStack {
43+
Row()
44+
Row()
45+
}
46+
}
47+
}
48+
49+
struct GridView: TestCaseView {
50+
var body: some View {
51+
VStack {
52+
VStack {
53+
DoubleRow()
54+
DoubleRow()
55+
DoubleRow()
56+
DoubleRow()
57+
}
58+
VStack {
59+
DoubleRow()
60+
DoubleRow()
61+
DoubleRow()
62+
DoubleRow()
63+
}
64+
}
65+
}
66+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Benchmark
2+
import SwiftCrossUI
3+
import DummyBackend
4+
import Foundation
5+
6+
protocol TestCaseView: View {
7+
init()
8+
}
9+
10+
#if BENCHMARK_VIZ
11+
import DefaultBackend
12+
13+
struct VizApp<V: TestCaseView>: App {
14+
var body: some Scene {
15+
WindowGroup("Benchmark visualisation") {
16+
V()
17+
}
18+
}
19+
}
20+
#endif
21+
22+
@main
23+
struct Benchmarks {
24+
@MainActor
25+
static func main() async {
26+
let backend = DummyBackend()
27+
let defaultEnvironment = EnvironmentValues(backend: backend)
28+
let environment = backend.computeRootEnvironment(defaultEnvironment: defaultEnvironment)
29+
.with(\.window, backend.createWindow(withDefaultSize: nil))
30+
31+
@MainActor
32+
func makeNode<V: View>(_ view: V) -> ViewGraphNode<V, DummyBackend> {
33+
ViewGraphNode(for: view, backend: backend, snapshot: nil, environment: environment)
34+
}
35+
36+
@MainActor
37+
func updateNode<V: View>(_ node: ViewGraphNode<V, DummyBackend>, _ size: SIMD2<Int>) {
38+
_ = node.update(proposedSize: size, environment: environment, dryRun: true)
39+
_ = node.update(proposedSize: size, environment: environment, dryRun: false)
40+
}
41+
42+
#if BENCHMARK_VIZ
43+
var benchmarkVisualizations: [(name: String, main: () -> Never)] = []
44+
#endif
45+
46+
@MainActor
47+
func benchmarkLayout<V: TestCaseView>(of viewType: V.Type, _ size: SIMD2<Int>, _ label: String) {
48+
#if BENCHMARK_VIZ
49+
benchmarkVisualizations.append((
50+
label,
51+
{
52+
VizApp<V>.main()
53+
exit(0)
54+
}
55+
))
56+
#else
57+
let node = makeNode(V())
58+
benchmark(label) { @MainActor in
59+
updateNode(node, size)
60+
}
61+
#endif
62+
}
63+
64+
// Register benchmarks
65+
benchmarkLayout(of: GridView.self, SIMD2(800, 800), "grid")
66+
benchmarkLayout(of: ScrollableMessageListView.self, SIMD2(800, 800), "message list")
67+
68+
#if BENCHMARK_VIZ
69+
let names = benchmarkVisualizations.map(\.name).joined(separator: " | ")
70+
print("Benchmark to viz (\(names)): ", terminator: "")
71+
guard let benchmarkName = readLine() else {
72+
print("Nothing entered")
73+
exit(1)
74+
}
75+
76+
guard
77+
let benchmark = benchmarkVisualizations.first(
78+
where: { $0.name == benchmarkName }
79+
)
80+
else {
81+
print("\(benchmarkName) doesn't match any benchmarks")
82+
exit(1)
83+
}
84+
85+
benchmark.main()
86+
#else
87+
await Benchmark.main()
88+
#endif
89+
}
90+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import SwiftCrossUI
2+
3+
struct MessageView: View {
4+
static let profileSize = 40
5+
6+
var message: ScrollableMessageListView.Message
7+
8+
var body: some View {
9+
HStack(alignment: .top) {
10+
message.author.profileColor
11+
.frame(width: Self.profileSize, height: Self.profileSize)
12+
.cornerRadius(Self.profileSize / 2)
13+
14+
VStack(alignment: .leading) {
15+
HStack {
16+
Text(message.author.name)
17+
.foregroundColor(message.author.handleColor)
18+
.emphasized()
19+
20+
Text(message.time)
21+
.foregroundColor(.gray)
22+
}
23+
24+
Text(message.content)
25+
}
26+
}
27+
}
28+
}
29+
30+
struct ScrollableMessageListView: TestCaseView {
31+
static let authors = [
32+
Author(name: "stackotter", handleColor: .blue, profileColor: .pink),
33+
Author(name: "gregc", handleColor: .yellow, profileColor: .yellow),
34+
Author(name: "bbrk24", handleColor: .purple, profileColor: .green)
35+
]
36+
37+
static let sentences = [
38+
"in the meantime you should be able to disable hot reloading support on linux at line 107 of Package.swift. hot reloading isn't actually supported on linux (it'll tell you that it's unsupported if you try to use hot reloading at runtime), i just left the code enabled to catch cross-platform regressions like this one hahah",
39+
"I assume stroke style gets overridden? if so we can make path store stroke style and then StyledShape can store another strokestyle if it wants, and when it produces the path it just overwrites its strokestyle before passing it on or something",
40+
"I am hesitant to completely mimic SwiftUI here because you can set stroke style on both Path and Shape, but stroke color only on Shape, so I have no idea what happens if you give them conflicting stroke styles",
41+
"I think stroke and fill modifiers on Shape would be a bit nicer. we could get them to return StyledShape or something and then implement the same two modifiers on StyledShape and get StyledShape to store fill and stroke",
42+
"Computer Software Is Hard"
43+
]
44+
45+
static let times = [
46+
"8:54am",
47+
"9:13am",
48+
"10:20pm",
49+
"12:01am",
50+
"1:00pm"
51+
]
52+
53+
static let messages = generateMessages(1000)
54+
55+
struct Author {
56+
var name: String
57+
var handleColor: Color
58+
var profileColor: Color
59+
60+
static let empty = Self(
61+
name: "<no name>",
62+
handleColor: .white,
63+
profileColor: .white
64+
)
65+
}
66+
67+
struct Message {
68+
var author: Author
69+
var content: String
70+
var time: String
71+
}
72+
73+
static func generateMessages(_ count: Int) -> [Message] {
74+
var messages: [Message] = []
75+
messages.reserveCapacity(count)
76+
77+
for i in 0..<count {
78+
let message = Message(
79+
author: authors[i % authors.count],
80+
content: sentences[i % sentences.count],
81+
time: times[i % times.count]
82+
)
83+
messages.append(message)
84+
}
85+
return messages
86+
}
87+
88+
var body: some View {
89+
ScrollView {
90+
VStack(alignment: .leading) {
91+
ForEach(Self.messages) { message in
92+
MessageView(message: message)
93+
}
94+
}
95+
.frame(maxWidth: .infinity)
96+
.padding(20)
97+
}
98+
}
99+
}

Package.resolved

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] {
5959
}
6060
}
6161

62+
// When SCUI_BENCHMARK_VIZ is present, we include the DefaultBackend to allow
63+
// viewing of each benchmark test case with an actual backend.
64+
let additionalLayoutPerformanceBenchmarkDependencies: [Target.Dependency]
65+
let layoutPerformanceSwiftSettings: [SwiftSetting]
66+
if ProcessInfo.processInfo.environment["SCUI_BENCHMARK_VIZ"] == "1" {
67+
additionalLayoutPerformanceBenchmarkDependencies = ["DefaultBackend"]
68+
layoutPerformanceSwiftSettings = [.define("BENCHMARK_VIZ")]
69+
} else {
70+
additionalLayoutPerformanceBenchmarkDependencies = []
71+
layoutPerformanceSwiftSettings = []
72+
}
73+
6274
let package = Package(
6375
name: "swift-cross-ui",
6476
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .macCatalyst(.v13), .visionOS(.v1)],
@@ -110,6 +122,10 @@ let package = Package(
110122
url: "https://github.com/stackotter/swift-winui",
111123
revision: "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86"
112124
),
125+
.package(
126+
url: "https://github.com/stackotter/swift-benchmark",
127+
.upToNextMinor(from: "0.2.0")
128+
),
113129
// .package(
114130
// url: "https://github.com/stackotter/TermKit",
115131
// revision: "163afa64f1257a0c026cc83ed8bc47a5f8fc9704"
@@ -252,6 +268,19 @@ let package = Package(
252268
name: "WinUIInterop",
253269
dependencies: []
254270
),
271+
.target(name: "DummyBackend", dependencies: ["SwiftCrossUI"]),
272+
273+
.executableTarget(
274+
name: "LayoutPerformanceBenchmark",
275+
dependencies: [
276+
.product(name: "Benchmark", package: "swift-benchmark"),
277+
"SwiftCrossUI",
278+
"DummyBackend",
279+
] + additionalLayoutPerformanceBenchmarkDependencies,
280+
path: "Benchmarks/LayoutPerformanceBenchmark",
281+
swiftSettings: layoutPerformanceSwiftSettings
282+
),
283+
255284
// .target(
256285
// name: "CursesBackend",
257286
// dependencies: ["SwiftCrossUI", "TermKit"]

0 commit comments

Comments
 (0)