Skip to content

Commit d756f51

Browse files
vkuttypCopilot
andcommitted
Add Swift benchmark: CosmoSQLClient (NIO) vs SQLClient-Swift (FreeTDS)
cosmo-benchmark/ is a standalone SPM executable that measures: - Cold connect + query + close (both drivers) - Warm query on persistent connection (both drivers) - Warm single-row query (both drivers) - Warm decode<T>() / toJson() (NIO only) FreeTDS benchmarks skip gracefully if freetds is not installed. Run: swift run -c release (from cosmo-benchmark/) Env vars: BENCH_HOST BENCH_PORT BENCH_DB BENCH_USER BENCH_PASS BENCH_ITER Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5df4c2a commit d756f51

4 files changed

Lines changed: 367 additions & 0 deletions

File tree

cosmo-benchmark/Package.resolved

Lines changed: 87 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cosmo-benchmark/Package.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// swift-tools-version: 6.0
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "cosmo-benchmark",
6+
platforms: [.macOS(.v13)],
7+
dependencies: [
8+
// CosmoSQLClient (NIO-based — this repo)
9+
.package(path: ".."),
10+
// SQLClient-Swift (FreeTDS-based)
11+
.package(url: "https://github.com/vkuttyp/SQLClient-Swift.git", branch: "main"),
12+
],
13+
targets: [
14+
.executableTarget(
15+
name: "cosmo-benchmark",
16+
dependencies: [
17+
.product(name: "CosmoMSSQL", package: "sql-nio"),
18+
.product(name: "CosmoSQLCore", package: "sql-nio"),
19+
.product(name: "SQLClientSwift", package: "SQLClient-Swift"),
20+
]
21+
),
22+
]
23+
)

cosmo-benchmark/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# CosmoSQLClient Benchmarks
2+
3+
Compares **CosmoSQLClient (Swift NIO)** vs **SQLClient-Swift (FreeTDS)** for MSSQL Server.
4+
5+
## Prerequisites
6+
7+
FreeTDS must be installed for the FreeTDS driver to be active:
8+
9+
```bash
10+
# macOS
11+
brew install freetds
12+
13+
# Ubuntu/Debian
14+
sudo apt install freetds-dev
15+
```
16+
17+
## Run
18+
19+
```bash
20+
cd cosmo-benchmark
21+
22+
# defaults: hanan.iserveus.com:1433 MurshiDb sa aBCD111
23+
swift run -c release
24+
25+
# custom server
26+
BENCH_HOST=myserver BENCH_PORT=1433 BENCH_DB=MyDB \
27+
BENCH_USER=sa BENCH_PASS=mypass BENCH_ITER=50 \
28+
swift run -c release
29+
```
30+
31+
## Scenarios
32+
33+
| Scenario | CosmoSQL (NIO) | FreeTDS |
34+
|---|---|---|
35+
| Cold connect + query + close |||
36+
| Warm query only (persistent conn) |||
37+
| Warm single-row query |||
38+
| Warm query + `decode<T>()` / Codable |||
39+
| Warm query + `toJson()` |||
40+
41+
## Notes
42+
43+
- FreeTDS benchmarks are skipped if FreeTDS is not installed (graceful degradation)
44+
- Results include avg / min / max ms per iteration and a winner comparison table
45+
- Use `BENCH_ITER=50` or higher for more stable results
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import Foundation
2+
import CosmoMSSQL
3+
import CosmoSQLCore
4+
import SQLClientSwift
5+
6+
// ─────────────────────────────────────────────
7+
// MARK: - Configuration
8+
// ─────────────────────────────────────────────
9+
10+
let host = ProcessInfo.processInfo.environment["BENCH_HOST"] ?? "localhost"
11+
let port = UInt16(ProcessInfo.processInfo.environment["BENCH_PORT"] ?? "1433") ?? 1433
12+
let database = ProcessInfo.processInfo.environment["BENCH_DB"] ?? "MurshiDb"
13+
let user = ProcessInfo.processInfo.environment["BENCH_USER"] ?? "sa"
14+
let password = ProcessInfo.processInfo.environment["BENCH_PASS"] ?? "aBCD111"
15+
let query = ProcessInfo.processInfo.environment["BENCH_QUERY"] ?? "SELECT * FROM Accounts"
16+
let iterations = Int(ProcessInfo.processInfo.environment["BENCH_ITER"] ?? "20") ?? 20
17+
18+
// ─────────────────────────────────────────────
19+
// MARK: - Timing helpers
20+
// ─────────────────────────────────────────────
21+
22+
struct BenchResult {
23+
let label: String
24+
let iterations: Int
25+
let totalMs: Double
26+
var avgMs: Double { totalMs / Double(iterations) }
27+
var minMs: Double
28+
var maxMs: Double
29+
}
30+
31+
func measure(label: String, iterations: Int, block: () async throws -> Void) async -> BenchResult {
32+
var times: [Double] = []
33+
for _ in 0..<iterations {
34+
let t = Date()
35+
do { try await block() } catch { print(" ⚠️ \(label) error: \(error)") }
36+
times.append(Date().timeIntervalSince(t) * 1000)
37+
}
38+
let total = times.reduce(0, +)
39+
return BenchResult(
40+
label: label,
41+
iterations: iterations,
42+
totalMs: total,
43+
minMs: times.min() ?? 0,
44+
maxMs: times.max() ?? 0
45+
)
46+
}
47+
48+
func printResult(_ r: BenchResult) {
49+
print(String(format: " %-48s avg %7.2f ms min %7.2f ms max %7.2f ms (%d runs)",
50+
(r.label as NSString).utf8String!, r.avgMs, r.minMs, r.maxMs, r.iterations))
51+
}
52+
53+
func printHeader(_ title: String) {
54+
print("\n" + String(repeating: "", count: 90))
55+
print(" \(title)")
56+
print(String(repeating: "", count: 90))
57+
}
58+
59+
// ─────────────────────────────────────────────
60+
// MARK: - CosmoSQLClient (NIO) benchmarks
61+
// ─────────────────────────────────────────────
62+
63+
func benchCosmo() async {
64+
printHeader("🔵 CosmoSQLClient (NIO-based, pure Swift)")
65+
66+
let connStr = "Server=\(host),\(port);Database=\(database);User Id=\(user);Password=\(password);Encrypt=true;TrustServerCertificate=true"
67+
68+
// 1. Cold: connect + query + close (per iteration)
69+
let cold = await measure(label: "Cold connect + query + close", iterations: iterations) {
70+
let conn = try await MSSQLConnection.connect(
71+
configuration: .init(connectionString: connStr))
72+
defer { Task { try? await conn.close() } }
73+
_ = try await conn.query(query, [])
74+
}
75+
printResult(cold)
76+
77+
// 2. Warm: persistent connection, query only
78+
let conn = try? await MSSQLConnection.connect(
79+
configuration: .init(connectionString: connStr))
80+
if let conn {
81+
let warm = await measure(label: "Warm query only (persistent conn)", iterations: iterations) {
82+
_ = try await conn.query(query, [])
83+
}
84+
printResult(warm)
85+
86+
// 3. Warm: single row
87+
let warmSingle = await measure(label: "Warm single-row query", iterations: iterations) {
88+
_ = try await conn.query("SELECT TOP 1 * FROM Accounts", [])
89+
}
90+
printResult(warmSingle)
91+
92+
// 4. Warm: decode into typed list
93+
let warmDecode = await measure(label: "Warm query + decode<Account>()", iterations: iterations) {
94+
let rows = try await conn.query(query, [])
95+
_ = try rows.asDataTable().decode(as: Account.self)
96+
}
97+
printResult(warmDecode)
98+
99+
// 5. Warm: toJson
100+
let warmJson = await measure(label: "Warm query + toJson()", iterations: iterations) {
101+
let rows = try await conn.query(query, [])
102+
_ = rows.asDataTable().toJson()
103+
}
104+
printResult(warmJson)
105+
106+
try? await conn.close()
107+
} else {
108+
print(" ⚠️ Could not connect — skipping warm benchmarks")
109+
}
110+
}
111+
112+
// ─────────────────────────────────────────────
113+
// MARK: - SQLClient-Swift (FreeTDS) benchmarks
114+
// ─────────────────────────────────────────────
115+
116+
func benchFreeTDS() async {
117+
printHeader("🟠 SQLClient-Swift (FreeTDS-based)")
118+
119+
var options = SQLClientConnectionOptions(
120+
server: host,
121+
username: user,
122+
password: password,
123+
database: database
124+
)
125+
options.port = port
126+
127+
// 1. Cold: connect + query + disconnect
128+
let cold = await measure(label: "Cold connect + query + disconnect", iterations: iterations) {
129+
let client = SQLClient()
130+
try await client.connect(options: options)
131+
_ = try await client.execute(query)
132+
await client.disconnect()
133+
}
134+
printResult(cold)
135+
136+
// 2. Warm: persistent connection
137+
let client = SQLClient()
138+
let connected = (try? await client.connect(options: options)) != nil
139+
if connected {
140+
let warm = await measure(label: "Warm query only (persistent conn)", iterations: iterations) {
141+
_ = try await client.execute(query)
142+
}
143+
printResult(warm)
144+
145+
let warmSingle = await measure(label: "Warm single-row query", iterations: iterations) {
146+
_ = try await client.execute("SELECT TOP 1 * FROM Accounts")
147+
}
148+
printResult(warmSingle)
149+
150+
await client.disconnect()
151+
} else {
152+
print(" ⚠️ FreeTDS not available or could not connect — is freetds installed?")
153+
print(" brew install freetds (macOS)")
154+
print(" apt install freetds-dev (Linux)")
155+
}
156+
}
157+
158+
// ─────────────────────────────────────────────
159+
// MARK: - Decodable model
160+
// ─────────────────────────────────────────────
161+
162+
struct Account: Decodable {
163+
let AccountNo: String?
164+
let AccountName: String?
165+
}
166+
167+
// ─────────────────────────────────────────────
168+
// MARK: - Summary table
169+
// ─────────────────────────────────────────────
170+
171+
func printSummary(_ cosmo: [BenchResult], _ freetds: [BenchResult]) {
172+
printHeader("📊 Summary — avg ms per operation (lower is better)")
173+
print(String(format: " %-40s %12s %12s %10s",
174+
"Scenario", "CosmoSQL(NIO)", "FreeTDS", "Winner"))
175+
print(" " + String(repeating: "-", count: 80))
176+
177+
let pairs = zip(cosmo, freetds)
178+
for (c, f) in pairs {
179+
let winner = c.avgMs < f.avgMs ? "🔵 NIO" : "🟠 FreeTDS"
180+
let diff = abs(c.avgMs - f.avgMs)
181+
let pct = (diff / max(c.avgMs, f.avgMs)) * 100
182+
print(String(format: " %-40s %10.2f ms %10.2f ms %@ (%.0f%% faster)",
183+
c.label.truncated(to: 40), c.avgMs, f.avgMs, winner, pct))
184+
}
185+
}
186+
187+
extension String {
188+
func truncated(to length: Int) -> String {
189+
count > length ? String(prefix(length - 1)) + "" : self
190+
}
191+
}
192+
193+
// ─────────────────────────────────────────────
194+
// MARK: - Entry point
195+
// ─────────────────────────────────────────────
196+
197+
print("""
198+
╔══════════════════════════════════════════════════════════════════════════════╗
199+
║ CosmoSQLClient Benchmark — Swift NIO vs FreeTDS ║
200+
╠══════════════════════════════════════════════════════════════════════════════╣
201+
║ Host: \(host):\(port) DB: \(database)
202+
║ Query: \(query)
203+
║ Iterations: \(iterations) per scenario
204+
╚══════════════════════════════════════════════════════════════════════════════╝
205+
""")
206+
207+
await benchCosmo()
208+
await benchFreeTDS()
209+
210+
print("\n" + String(repeating: "", count: 90))
211+
print(" Benchmark complete.")
212+
print(String(repeating: "", count: 90) + "\n")

0 commit comments

Comments
 (0)