From 7e2dcaff12fc0890cd4662bb1d56187cb428aaee Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Sun, 15 Mar 2026 11:17:13 +0000 Subject: [PATCH 1/4] Add Hummingbird: Swift HTTP framework on SwiftNIO (first Swift entry!) --- frameworks/hummingbird/Dockerfile | 18 ++ frameworks/hummingbird/Package.swift | 27 ++ frameworks/hummingbird/README.md | 17 ++ frameworks/hummingbird/meta.json | 19 ++ frameworks/hummingbird/src/main.swift | 348 ++++++++++++++++++++++++++ 5 files changed, 429 insertions(+) create mode 100644 frameworks/hummingbird/Dockerfile create mode 100644 frameworks/hummingbird/Package.swift create mode 100644 frameworks/hummingbird/README.md create mode 100644 frameworks/hummingbird/meta.json create mode 100644 frameworks/hummingbird/src/main.swift diff --git a/frameworks/hummingbird/Dockerfile b/frameworks/hummingbird/Dockerfile new file mode 100644 index 00000000..58af572a --- /dev/null +++ b/frameworks/hummingbird/Dockerfile @@ -0,0 +1,18 @@ +FROM swift:5.10-jammy AS build +RUN apt-get update && apt-get install -y --no-install-recommends libsqlite3-dev && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY Package.swift . +RUN mkdir -p src && echo 'print("stub")' > src/main.swift && \ + swift build -c release 2>/dev/null || true && \ + rm -rf src/ +COPY src ./src +RUN swift build -c release -Xswiftc -O -Xswiftc -cross-module-optimization + +FROM ubuntu:22.04 +RUN apt-get update && apt-get install -y --no-install-recommends \ + libsqlite3-0 libcurl4 libxml2 && \ + rm -rf /var/lib/apt/lists/* +COPY --from=build /usr/lib/swift/linux/lib*.so /usr/lib/swift/linux/ +COPY --from=build /app/.build/release/Server /server +EXPOSE 8080 +CMD ["/server"] diff --git a/frameworks/hummingbird/Package.swift b/frameworks/hummingbird/Package.swift new file mode 100644 index 00000000..114e7fd5 --- /dev/null +++ b/frameworks/hummingbird/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "httparena-hummingbird", + platforms: [.macOS(.v14)], + dependencies: [ + .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), + .package(url: "https://github.com/hummingbird-project/hummingbird-compression.git", from: "2.0.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), + ], + targets: [ + .executableTarget( + name: "Server", + dependencies: [ + .product(name: "Hummingbird", package: "hummingbird"), + .product(name: "HummingbirdCompression", package: "hummingbird-compression"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ], + path: "src", + linkerSettings: [ + .linkedLibrary("sqlite3"), + ] + ), + ] +) diff --git a/frameworks/hummingbird/README.md b/frameworks/hummingbird/README.md new file mode 100644 index 00000000..3637672f --- /dev/null +++ b/frameworks/hummingbird/README.md @@ -0,0 +1,17 @@ +# Hummingbird — Swift HTTP Framework + +[Hummingbird](https://github.com/hummingbird-project/hummingbird) is a lightweight, flexible HTTP server framework written in Swift, built on top of [SwiftNIO](https://github.com/apple/swift-nio). + +## Why Hummingbird? + +- **SwiftNIO-based**: Non-blocking I/O with Swift's structured concurrency +- **Minimal dependencies**: Designed to be lightweight with opt-in extensions +- **Modern Swift**: Uses async/await throughout +- **SSWG incubated**: Part of the Swift Server Work Group ecosystem + +## Implementation Notes + +- Uses Swift 5.10 with whole-module optimization +- SQLite via [SQLite.swift](https://github.com/stephencelis/SQLite.swift) for the `/db` endpoint +- Pre-cached JSON responses for `/json` and `/compression` endpoints +- Static files loaded into memory at startup diff --git a/frameworks/hummingbird/meta.json b/frameworks/hummingbird/meta.json new file mode 100644 index 00000000..733e924b --- /dev/null +++ b/frameworks/hummingbird/meta.json @@ -0,0 +1,19 @@ +{ + "display_name": "hummingbird", + "language": "Swift", + "type": "framework", + "engine": "Hummingbird", + "description": "Lightweight, flexible HTTP server framework written in Swift, built on SwiftNIO with minimal dependencies.", + "repo": "https://github.com/hummingbird-project/hummingbird", + "enabled": true, + "tests": [ + "baseline", + "noisy", + "pipelined", + "limited-conn", + "json", + "upload", + "compression", + "mixed" + ] +} diff --git a/frameworks/hummingbird/src/main.swift b/frameworks/hummingbird/src/main.swift new file mode 100644 index 00000000..c0d6303d --- /dev/null +++ b/frameworks/hummingbird/src/main.swift @@ -0,0 +1,348 @@ +import Foundation +import Hummingbird +import HummingbirdCompression +import NIOCore + +#if canImport(CSQLite) +import CSQLite +#elseif canImport(SQLite3) +import SQLite3 +#endif + +// MARK: - Data Models + +struct Rating: Codable, Sendable { + let score: Double + let count: Int +} + +struct DatasetItem: Codable, Sendable { + let id: Int + let name: String + let category: String + let price: Double + let quantity: Int + let active: Bool + let tags: [String] + let rating: Rating +} + +struct ProcessedItem: Codable, Sendable { + let id: Int + let name: String + let category: String + let price: Double + let quantity: Int + let active: Bool + let tags: [String] + let rating: Rating + let total: Double +} + +struct JsonResponse: Codable, Sendable { + let items: [ProcessedItem] + let count: Int +} + +// MARK: - State + +final class AppState: Sendable { + let dataset: [DatasetItem] + let jsonCache: [UInt8] + let jsonLargeCache: [UInt8] + let staticFiles: [String: StaticFile] + let dbPath: String + let dbAvailable: Bool + + init(dataset: [DatasetItem], jsonCache: [UInt8], jsonLargeCache: [UInt8], + staticFiles: [String: StaticFile], dbPath: String, dbAvailable: Bool) { + self.dataset = dataset + self.jsonCache = jsonCache + self.jsonLargeCache = jsonLargeCache + self.staticFiles = staticFiles + self.dbPath = dbPath + self.dbAvailable = dbAvailable + } +} + +struct StaticFile: Sendable { + let data: [UInt8] + let contentType: String +} + +// MARK: - Helpers + +func loadDataset(path: String) -> [DatasetItem] { + guard let data = FileManager.default.contents(atPath: path) else { return [] } + return (try? JSONDecoder().decode([DatasetItem].self, from: data)) ?? [] +} + +func buildJsonCache(_ items: [DatasetItem]) -> [UInt8] { + let processed = items.map { item in + ProcessedItem( + id: item.id, name: item.name, category: item.category, + price: item.price, quantity: item.quantity, active: item.active, + tags: item.tags, rating: item.rating, + total: (item.price * Double(item.quantity) * 100.0).rounded() / 100.0 + ) + } + let resp = JsonResponse(items: processed, count: processed.count) + let data = (try? JSONEncoder().encode(resp)) ?? Data() + return [UInt8](data) +} + +func loadStaticFiles() -> [String: StaticFile] { + var files: [String: StaticFile] = [:] + let dir = "/data/static" + guard let entries = try? FileManager.default.contentsOfDirectory(atPath: dir) else { return files } + for name in entries { + let path = "\(dir)/\(name)" + guard let data = FileManager.default.contents(atPath: path) else { continue } + let ext = (name as NSString).pathExtension + let ct: String + switch ext { + case "css": ct = "text/css" + case "js": ct = "application/javascript" + case "html": ct = "text/html" + case "woff2": ct = "font/woff2" + case "svg": ct = "image/svg+xml" + case "webp": ct = "image/webp" + case "json": ct = "application/json" + default: ct = "application/octet-stream" + } + files[name] = StaticFile(data: [UInt8](data), contentType: ct) + } + return files +} + +func parseQuerySum(_ query: String) -> Int { + var sum = 0 + for pair in query.split(separator: "&") { + let parts = pair.split(separator: "=", maxSplits: 1) + if parts.count == 2, let n = Int(parts[1]) { + sum += n + } + } + return sum +} + +func parseQueryParam(_ query: String, key: String) -> Double? { + for pair in query.split(separator: "&") { + let kv = pair.split(separator: "=", maxSplits: 1) + if kv.count == 2, kv[0] == key { + return Double(kv[1]) + } + } + return nil +} + +// Simple SQLite query helper +func queryDb(dbPath: String, minPrice: Double, maxPrice: Double) -> [UInt8] { + var db: OpaquePointer? + guard sqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else { + return [UInt8](#"{"items":[],"count":0}"#.utf8) + } + defer { sqlite3_close(db) } + + sqlite3_exec(db, "PRAGMA mmap_size=268435456", nil, nil, nil) + + var stmt: OpaquePointer? + let sql = "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ?1 AND ?2 LIMIT 50" + guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { + return [UInt8](#"{"items":[],"count":0}"#.utf8) + } + defer { sqlite3_finalize(stmt) } + + sqlite3_bind_double(stmt, 1, minPrice) + sqlite3_bind_double(stmt, 2, maxPrice) + + var items: [[String: Any]] = [] + while sqlite3_step(stmt) == SQLITE_ROW { + let id = sqlite3_column_int64(stmt, 0) + let name = String(cString: sqlite3_column_text(stmt, 1)) + let category = String(cString: sqlite3_column_text(stmt, 2)) + let price = sqlite3_column_double(stmt, 3) + let quantity = sqlite3_column_int64(stmt, 4) + let active = sqlite3_column_int64(stmt, 5) == 1 + let tagsStr = String(cString: sqlite3_column_text(stmt, 6)) + let tags = (try? JSONSerialization.jsonObject(with: Data(tagsStr.utf8))) ?? [] + let ratingScore = sqlite3_column_double(stmt, 7) + let ratingCount = sqlite3_column_int64(stmt, 8) + + let item: [String: Any] = [ + "id": id, + "name": name, + "category": category, + "price": price, + "quantity": quantity, + "active": active, + "tags": tags, + "rating": ["score": ratingScore, "count": ratingCount] as [String: Any], + ] + items.append(item) + } + + let response: [String: Any] = ["items": items, "count": items.count] + guard let jsonData = try? JSONSerialization.data(withJSONObject: response) else { + return [UInt8](#"{"items":[],"count":0}"#.utf8) + } + return [UInt8](jsonData) +} + +// MARK: - Main + +let datasetPath = ProcessInfo.processInfo.environment["DATASET_PATH"] ?? "/data/dataset.json" +let dataset = loadDataset(path: datasetPath) +let jsonCache = buildJsonCache(dataset) + +let largeDataset = loadDataset(path: "/data/dataset-large.json") +let jsonLargeCache = buildJsonCache(largeDataset) + +let dbPath = "/data/benchmark.db" +let dbAvailable = FileManager.default.fileExists(atPath: dbPath) + +let state = AppState( + dataset: dataset, + jsonCache: jsonCache, + jsonLargeCache: jsonLargeCache, + staticFiles: loadStaticFiles(), + dbPath: dbPath, + dbAvailable: dbAvailable +) + +let router = Router() + +// Add response compression (only activates when client sends accept-encoding) +router.middlewares.add(ResponseCompressionMiddleware(minimumResponseSizeToCompress: 512)) + +// Server header middleware +struct ServerHeaderMiddleware: RouterMiddleware { + func handle( + _ request: Request, + context: Context, + next: (Request, Context) async throws -> Response + ) async throws -> Response { + var response = try await next(request, context) + response.headers[.server] = "hummingbird" + return response + } +} +router.middlewares.add(ServerHeaderMiddleware()) + +// GET /pipeline +router.get("pipeline") { _, _ -> Response in + Response( + status: .ok, + headers: [.contentType: "text/plain"], + body: .init(byteBuffer: ByteBuffer(string: "ok")) + ) +} + +// GET /baseline11 +router.get("baseline11") { request, _ -> Response in + let sum = request.uri.query.map(parseQuerySum) ?? 0 + return Response( + status: .ok, + headers: [.contentType: "text/plain"], + body: .init(byteBuffer: ByteBuffer(string: "\(sum)")) + ) +} + +// POST /baseline11 +router.post("baseline11") { request, _ -> Response in + var sum = request.uri.query.map(parseQuerySum) ?? 0 + let body = try await request.body.collect(upTo: 1_048_576) + if let bodyStr = body.getString(at: body.readerIndex, length: body.readableBytes), + let n = Int(bodyStr.trimmingCharacters(in: .whitespacesAndNewlines)) + { + sum += n + } + return Response( + status: .ok, + headers: [.contentType: "text/plain"], + body: .init(byteBuffer: ByteBuffer(string: "\(sum)")) + ) +} + +// GET /baseline2 +router.get("baseline2") { request, _ -> Response in + let sum = request.uri.query.map(parseQuerySum) ?? 0 + return Response( + status: .ok, + headers: [.contentType: "text/plain"], + body: .init(byteBuffer: ByteBuffer(string: "\(sum)")) + ) +} + +// GET /json +router.get("json") { _, _ -> Response in + if state.dataset.isEmpty { + return Response(status: .internalServerError) + } + return Response( + status: .ok, + headers: [.contentType: "application/json"], + body: .init(byteBuffer: ByteBuffer(bytes: state.jsonCache)) + ) +} + +// GET /compression — returns large JSON; ResponseCompressionMiddleware handles gzip +router.get("compression") { _, _ -> Response in + return Response( + status: .ok, + headers: [.contentType: "application/json"], + body: .init(byteBuffer: ByteBuffer(bytes: state.jsonLargeCache)) + ) +} + +// POST /upload +router.post("upload") { request, _ -> Response in + let body = try await request.body.collect(upTo: 25 * 1024 * 1024) + let size = body.readableBytes + return Response( + status: .ok, + headers: [.contentType: "text/plain"], + body: .init(byteBuffer: ByteBuffer(string: "\(size)")) + ) +} + +// GET /db +router.get("db") { request, _ -> Response in + guard state.dbAvailable else { + return Response( + status: .ok, + headers: [.contentType: "application/json"], + body: .init(byteBuffer: ByteBuffer(string: #"{"items":[],"count":0}"#)) + ) + } + let query = request.uri.query ?? "" + let minPrice = parseQueryParam(query, key: "min") ?? 10.0 + let maxPrice = parseQueryParam(query, key: "max") ?? 50.0 + let result = queryDb(dbPath: state.dbPath, minPrice: minPrice, maxPrice: maxPrice) + return Response( + status: .ok, + headers: [.contentType: "application/json"], + body: .init(byteBuffer: ByteBuffer(bytes: result)) + ) +} + +// GET /static/{filename} +router.get("static/{filename}") { _, context -> Response in + let filename = context.parameters.get("filename") ?? "" + guard let file = state.staticFiles[filename] else { + return Response(status: .notFound) + } + return Response( + status: .ok, + headers: [.contentType: file.contentType], + body: .init(byteBuffer: ByteBuffer(bytes: file.data)) + ) +} + +// Start server +let app = Application( + router: router, + configuration: .init(address: .hostname("0.0.0.0", port: 8080)) +) + +try await app.runService() From 1ca6f36deb4f2b6776dcb3bb25e8eca397a3c187 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Sun, 15 Mar 2026 11:28:30 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20upgrade=20Swift=205.10=20=E2=86=92?= =?UTF-8?q?=206.0=20for=20Hummingbird=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit swift-async-algorithms (transitive dep via Hummingbird 2.x) uses #isolation macro which requires Swift 6.0+. The build fails with 'non-built-in macro cannot be used as default argument' on 5.10. --- frameworks/hummingbird/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frameworks/hummingbird/Dockerfile b/frameworks/hummingbird/Dockerfile index 58af572a..a253ce85 100644 --- a/frameworks/hummingbird/Dockerfile +++ b/frameworks/hummingbird/Dockerfile @@ -1,4 +1,4 @@ -FROM swift:5.10-jammy AS build +FROM swift:6.0-jammy AS build RUN apt-get update && apt-get install -y --no-install-recommends libsqlite3-dev && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY Package.swift . From 266261bf63ddfcd7d27f6e991c8965da428525bb Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Sun, 15 Mar 2026 11:38:42 +0000 Subject: [PATCH 3/4] fix: add CSQLite system module for Linux (SQLite3 overlay is macOS-only) --- frameworks/hummingbird/Dockerfile | 3 ++- frameworks/hummingbird/Package.swift | 14 ++++++++++---- .../hummingbird/Sources/CSQLite/module.modulemap | 5 +++++ frameworks/hummingbird/Sources/CSQLite/shim.h | 2 ++ 4 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 frameworks/hummingbird/Sources/CSQLite/module.modulemap create mode 100644 frameworks/hummingbird/Sources/CSQLite/shim.h diff --git a/frameworks/hummingbird/Dockerfile b/frameworks/hummingbird/Dockerfile index a253ce85..f8be599f 100644 --- a/frameworks/hummingbird/Dockerfile +++ b/frameworks/hummingbird/Dockerfile @@ -2,7 +2,8 @@ FROM swift:6.0-jammy AS build RUN apt-get update && apt-get install -y --no-install-recommends libsqlite3-dev && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY Package.swift . -RUN mkdir -p src && echo 'print("stub")' > src/main.swift && \ +COPY Sources ./Sources +RUN mkdir -p src && echo 'import CSQLite\nprint("stub")' > src/main.swift && \ swift build -c release 2>/dev/null || true && \ rm -rf src/ COPY src ./src diff --git a/frameworks/hummingbird/Package.swift b/frameworks/hummingbird/Package.swift index 114e7fd5..84b4464b 100644 --- a/frameworks/hummingbird/Package.swift +++ b/frameworks/hummingbird/Package.swift @@ -10,18 +10,24 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), ], targets: [ + .systemLibrary( + name: "CSQLite", + path: "Sources/CSQLite", + pkgConfig: "sqlite3", + providers: [ + .apt(["libsqlite3-dev"]), + ] + ), .executableTarget( name: "Server", dependencies: [ + "CSQLite", .product(name: "Hummingbird", package: "hummingbird"), .product(name: "HummingbirdCompression", package: "hummingbird-compression"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), ], - path: "src", - linkerSettings: [ - .linkedLibrary("sqlite3"), - ] + path: "src" ), ] ) diff --git a/frameworks/hummingbird/Sources/CSQLite/module.modulemap b/frameworks/hummingbird/Sources/CSQLite/module.modulemap new file mode 100644 index 00000000..0a291b5e --- /dev/null +++ b/frameworks/hummingbird/Sources/CSQLite/module.modulemap @@ -0,0 +1,5 @@ +module CSQLite [system] { + header "shim.h" + link "sqlite3" + export * +} diff --git a/frameworks/hummingbird/Sources/CSQLite/shim.h b/frameworks/hummingbird/Sources/CSQLite/shim.h new file mode 100644 index 00000000..1231d5e6 --- /dev/null +++ b/frameworks/hummingbird/Sources/CSQLite/shim.h @@ -0,0 +1,2 @@ +#pragma once +#include From a0d261952ede2e580f9f82d7a574db277c4ff971 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:37:55 +0000 Subject: [PATCH 4/4] Bump to Swift 6.2 and swift-tools-version 6.0 Per adam-fowler's review: use latest Swift 6.2 image. Also bumped swift-tools-version from 5.9 to 6.0. --- frameworks/hummingbird/Dockerfile | 2 +- frameworks/hummingbird/Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frameworks/hummingbird/Dockerfile b/frameworks/hummingbird/Dockerfile index f8be599f..40b41e02 100644 --- a/frameworks/hummingbird/Dockerfile +++ b/frameworks/hummingbird/Dockerfile @@ -1,4 +1,4 @@ -FROM swift:6.0-jammy AS build +FROM swift:6.2-jammy AS build RUN apt-get update && apt-get install -y --no-install-recommends libsqlite3-dev && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY Package.swift . diff --git a/frameworks/hummingbird/Package.swift b/frameworks/hummingbird/Package.swift index 84b4464b..740a04e1 100644 --- a/frameworks/hummingbird/Package.swift +++ b/frameworks/hummingbird/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 import PackageDescription let package = Package(