Browse Source

Add Hummingbird 2 (#8995)

* Hummingbird2 benchmarks

* Update connection count
Adam Fowler 1 year ago
parent
commit
898495562b

+ 1 - 1
frameworks/Swift/.gitignore

@@ -5,4 +5,4 @@ xcuserdata/
 .swiftpm/
 DerivedData/
 Package.resolved
-.vscode/
+.vscode/

+ 44 - 0
frameworks/Swift/hummingbird2/README.md

@@ -0,0 +1,44 @@
+# Hummingbird 2 Benchmarking Test
+
+Hummingbird 2 is a lightweight, flexible HTTP server framework written in Swift. HUmmingbird 2 is a complete rewrite using Swift concurrency.
+
+### Test Type Implementation Source Code
+
+* [JSON](src/Sources/server/main.swift)
+* [PLAINTEXT](src/Sources/server/main.swift)
+* [DB](src-postgres/Sources/server/Controllers/WorldController.swift)
+* [Query](src-postgres/Sources/server/Controllers/WorldController.swift)
+* [Updates](src-postgres/Sources/server/Controllers/WorldController.swift)
+* [Fortunes](src-postgres/Sources/server/Controllers/FortunesController.swift)
+
+## Important Libraries
+This version of Hummingbird requires
+* [Swift 5.10](https://swift.org)  
+* [SwiftNIO 2.x](https://github.com/apple/swift-nio/)
+In these tests for database access it uses
+* [PostgresKit 2.21](https://github.com/vapor/postgres-nio/)
+
+## Test URLs
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### Query
+
+http://localhost:8080/queries?queries=
+
+### Updates
+
+http://localhost:8080/updates?queries=
+
+### Fortunes
+
+http://localhost:8080/fortunes

+ 47 - 0
frameworks/Swift/hummingbird2/benchmark_config.json

@@ -0,0 +1,47 @@
+{
+  "framework": "hummingbird2",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "None",
+        "framework": "Hummingbird2",
+        "language": "Swift",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Hummingbird 2",
+        "notes": "",
+        "versus": "swift-nio"
+      },
+      "postgres": {
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "postgres",
+        "framework": "Hummingbird2",
+        "language": "Swift",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Hummingbird 2",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 29 - 0
frameworks/Swift/hummingbird2/hummingbird2-postgres.dockerfile

@@ -0,0 +1,29 @@
+# ================================
+# Build image
+# ================================
+FROM swift:5.10 as build
+WORKDIR /build
+
+# Copy entire repo into container
+COPY ./src-postgres .
+
+# Compile with optimizations
+RUN swift build \
+	-c release \
+	-Xswiftc -enforce-exclusivity=unchecked
+
+# ================================
+# Run image
+# ================================
+FROM swift:5.10-slim
+WORKDIR /run
+
+# Copy build artifacts
+COPY --from=build /build/.build/release /run
+
+ENV SERVER_PORT=8080
+ENV SERVER_HOSTNAME=0.0.0.0
+
+EXPOSE 8080
+
+CMD ["./server"]

+ 29 - 0
frameworks/Swift/hummingbird2/hummingbird2.dockerfile

@@ -0,0 +1,29 @@
+# ================================
+# Build image
+# ================================
+FROM swift:5.10 as build
+WORKDIR /build
+
+# Copy entire repo into container
+COPY ./src .
+
+# Compile with optimizations
+RUN swift build \
+	-c release \
+	-Xswiftc -enforce-exclusivity=unchecked
+
+# ================================
+# Run image
+# ================================
+FROM swift:5.10-slim
+WORKDIR /run
+
+# Copy build artifacts
+COPY --from=build /build/.build/release /run
+
+ENV SERVER_PORT=8080
+ENV SERVER_HOSTNAME=0.0.0.0
+
+EXPOSE 8080
+
+CMD ["./server"]

+ 32 - 0
frameworks/Swift/hummingbird2/src-postgres/Package.swift

@@ -0,0 +1,32 @@
+// swift-tools-version:5.10
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+    name: "server",
+    platforms: [.macOS(.v14)],
+    products: [
+        .executable(name: "server", targets: ["server"])
+    ],
+    dependencies: [
+        .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0-beta.4"),
+        .package(url: "https://github.com/hummingbird-project/swift-mustache.git", from: "2.0.0-beta"),
+        .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.21.0"),
+    ],
+    targets: [
+        .executableTarget(name: "server",
+            dependencies: [
+                .product(name: "Hummingbird", package: "hummingbird"),
+                .product(name: "Mustache", package: "swift-mustache"),
+                .product(name: "PostgresNIO", package: "postgres-nio"),
+            ],
+            swiftSettings: [
+                // Enable better optimizations when building in Release configuration. Despite the use of
+                // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
+                // builds. See <https://github.com/swift-server/guides#building-for-production> for details.
+                .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
+            ]
+        ),
+    ]
+)

+ 49 - 0
frameworks/Swift/hummingbird2/src-postgres/Sources/server/Controllers/FortunesController.swift

@@ -0,0 +1,49 @@
+import Hummingbird
+import Mustache
+import PostgresNIO
+
+struct HTML: ResponseGenerator, Sendable {
+    let html: String
+    public func response(from request: Request, context: some BaseRequestContext) -> Response {
+        let buffer = context.allocator.buffer(string: html)
+        return Response(status: .ok, headers: [.contentType: "text/html; charset=utf-8"], body: .init(byteBuffer: buffer))
+    }
+}
+
+final class FortunesController: Sendable {
+    typealias Context = TechFrameworkRequestContext
+    let template: MustacheTemplate
+    let postgresClient: PostgresClient
+
+    init(postgresClient: PostgresClient) {
+        self.postgresClient = postgresClient
+        self.template = try! MustacheTemplate(string: """
+        <!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>{{#.}}<tr><td>{{id}}</td><td>{{message}}</td></tr>{{/.}}</table></body></html>
+        """)
+    }
+
+    var routes: RouteCollection<Context> {
+        RouteCollection(context: Context.self)
+            .get("fortunes", use: fortunes)
+    }
+
+    /// In this test, the framework's ORM is used to fetch all rows from a database
+    ///  table containing an unknown number of Unix fortune cookie messages (the 
+    /// table has 12 rows, but the code cannot have foreknowledge of the table's 
+    /// size). An additional fortune cookie message is inserted into the list at 
+    /// runtime and then the list is sorted by the message text. Finally, the list 
+    /// is delivered to the client using a server-side HTML template. The message 
+    /// text must be considered untrusted and properly escaped and the UTF-8 fortune messages must be rendered properly.
+    @Sendable func fortunes(request: Request, context: Context) async throws -> HTML {
+        let rows = try await self.postgresClient.query("SELECT id, message FROM Fortune")
+        var fortunes: [Fortune] = []
+        for try await (id, message) in rows.decode((Int32, String).self, context: .default) {
+            fortunes.append(.init(id: id, message: message))
+        }
+
+        fortunes.append(.init(id: 0, message: "Additional fortune added at request time."))
+        let sortedFortunes = fortunes.sorted { $0.message < $1.message }
+        return HTML(html: self.template.render(sortedFortunes) )
+        
+    }
+}

+ 83 - 0
frameworks/Swift/hummingbird2/src-postgres/Sources/server/Controllers/WorldController.swift

@@ -0,0 +1,83 @@
+import Hummingbird
+import PostgresNIO
+
+struct WorldController {
+    typealias Context = TechFrameworkRequestContext
+    let postgresClient: PostgresClient
+
+    var routes: RouteCollection<Context> {
+        RouteCollection(context: Context.self)
+            .get("db", use: single)
+            .get("queries", use: multiple)
+            .get("updates", use: updates)
+    }
+
+    /// In this test, each request is processed by fetching a single row from a 
+    /// simple database table. That row is then serialized as a JSON response.
+    @Sendable func single(request: Request, context: Context) async throws -> World {
+        let id = Int32.random(in: 1...10_000)
+        let rows = try await self.postgresClient.query("SELECT id, randomnumber FROM World WHERE id = \(id)")
+        for try await (id, randomNumber) in rows.decode((Int32, Int32).self, context: .default) {
+            return World(id: id, randomNumber: randomNumber)
+        }
+        throw HTTPError(.notFound)
+    }
+
+    /// In this test, each request is processed by fetching multiple rows from a 
+    /// simple database table and serializing these rows as a JSON response. The 
+    /// test is run multiple times: testing 1, 5, 10, 15, and 20 queries per request. 
+    /// All tests are run at 512 concurrency.
+    @Sendable func multiple(request: Request, context: Context) async throws -> [World] {
+        let queries = (request.uri.queryParameters.get("queries", as: Int.self) ?? 1).bound(1, 500)
+        return try await withThrowingTaskGroup(of: World.self) { group in
+            for _ in 0..<queries {
+                group.addTask {
+                    let id = Int32.random(in: 1...10_000)
+                    let rows = try await self.postgresClient.query("SELECT id, randomnumber FROM World WHERE id = \(id)")
+                    for try await (id, randomNumber) in rows.decode((Int32, Int32).self, context: .default) {
+                        return World(id: id, randomNumber: randomNumber)
+                    }
+                    throw HTTPError(.notFound)
+                }
+            }
+            var result: [World] = .init()
+            result.reserveCapacity(queries)
+            for try await world in group {
+                result.append(world)
+            }
+            return result
+        }
+    }
+
+    /// This test exercises database writes. Each request is processed by fetching 
+    /// multiple rows from a simple database table, converting the rows to in-memory 
+    /// objects, modifying one attribute of each object in memory, updating each 
+    /// associated row in the database individually, and then serializing the list 
+    /// of objects as a JSON response. The test is run multiple times: testing 1, 5, 
+    /// 10, 15, and 20 updates per request. Note that the number of statements per 
+    /// request is twice the number of updates since each update is paired with one 
+    /// query to fetch the object. All tests are run at 512 concurrency.
+    @Sendable func updates(request: Request, context: Context) async throws -> [World] {
+        let queries = (request.uri.queryParameters.get("queries", as: Int.self) ?? 1).bound(1, 500)
+        return try await withThrowingTaskGroup(of: World.self) { group in
+            for _ in 0..<queries {
+                group.addTask {
+                    let id = Int32.random(in: 1...10_000)
+                    let rows = try await self.postgresClient.query("SELECT id FROM World WHERE id = \(id)")
+                    for try await (id) in rows.decode((Int32).self, context: .default) {
+                        let randomNumber = Int32.random(in: 1...10_000)
+                        try await self.postgresClient.query("UPDATE World SET randomnumber = \(randomNumber) WHERE id = \(id)")
+                        return World(id: id, randomNumber: randomNumber)
+                    }
+                    throw HTTPError(.notFound)
+                }
+            }
+            var result: [World] = .init()
+            result.reserveCapacity(queries)
+            for try await world in group {
+                result.append(world)
+            }
+            return result
+        }
+    }
+}

+ 21 - 0
frameworks/Swift/hummingbird2/src-postgres/Sources/server/Models/Fortune.swift

@@ -0,0 +1,21 @@
+import Hummingbird
+import Mustache
+
+struct Fortune: ResponseEncodable, Sendable {
+    var id: Int32
+    var message: String
+}
+
+// avoid using Mirror as it is expensive
+extension Fortune: MustacheParent {
+    func child(named: String) -> Any? {
+        switch named {
+        case "id":
+            return id
+        case "message":
+            return message
+        default:
+            return nil
+        }
+    }
+}

+ 7 - 0
frameworks/Swift/hummingbird2/src-postgres/Sources/server/Models/World.swift

@@ -0,0 +1,7 @@
+import Hummingbird
+
+struct World: ResponseEncodable {
+    var id: Int32
+    var randomNumber: Int32
+}
+

+ 62 - 0
frameworks/Swift/hummingbird2/src-postgres/Sources/server/main.swift

@@ -0,0 +1,62 @@
+import Hummingbird
+import PostgresNIO
+
+// postgresql.conf specifies max_connections = 2000
+// https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Environment#citrine-self-hosted
+// https://github.com/TechEmpower/FrameworkBenchmarks/blob/master/toolset/databases/postgres/postgresql.conf#L64
+
+extension Int {
+    func bound(_ minValue: Int, _ maxValue: Int) -> Int {
+        return Swift.min(maxValue, Swift.max(minValue, self))
+    }
+}
+
+struct TechFrameworkRequestContext: RequestContext {
+    static let jsonEncoder = JSONEncoder()
+    static let jsonDecoder = JSONDecoder()
+
+    var coreContext: Hummingbird.CoreRequestContext
+
+    // Use a global JSON Encoder
+    var responseEncoder: JSONEncoder { Self.jsonEncoder }
+    // Use a global JSON Decoder
+    var requestDecoder: JSONDecoder { Self.jsonDecoder }
+
+    init(channel: any Channel, logger: Logger) {
+        self.coreContext = .init(allocator: channel.allocator, logger: logger)
+    }
+}
+
+func runApp() async throws {
+    let env = Environment()
+    let serverHostName = env.get("SERVER_HOSTNAME") ?? "127.0.0.1"
+    let serverPort = env.get("SERVER_PORT", as: Int.self) ?? 8080
+
+    var postgresConfiguration = PostgresClient.Configuration(
+        host: "tfb-database", 
+        username: "benchmarkdbuser", 
+        password: "benchmarkdbpass", 
+        database: "hello_world", 
+        tls: .disable
+    )
+    postgresConfiguration.options.maximumConnections = 1900
+    let postgresClient = PostgresClient(
+        configuration: postgresConfiguration, 
+        eventLoopGroup: MultiThreadedEventLoopGroup.singleton
+    )
+    let router = Router(context: TechFrameworkRequestContext.self)
+    router.addRoutes(WorldController(postgresClient: postgresClient).routes)
+    router.addRoutes(FortunesController(postgresClient: postgresClient).routes)
+    var app = Application(
+        router: router,
+        configuration: .init(
+            address: .hostname(serverHostName, port: serverPort),
+            serverName: "HB2",
+            backlog: 8192
+        )
+    )
+    app.addServices(postgresClient)
+    try await app.runService()
+}
+
+try await runApp()

+ 28 - 0
frameworks/Swift/hummingbird2/src/Package.swift

@@ -0,0 +1,28 @@
+// swift-tools-version:5.10
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+    name: "server",
+    platforms: [.macOS(.v14)],
+    products: [
+        .executable(name: "server", targets: ["server"])
+    ],
+    dependencies: [
+        .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0-beta"),
+    ],
+    targets: [
+        .target(name: "server",
+            dependencies: [
+                .product(name: "Hummingbird", package: "hummingbird"),
+            ],
+            swiftSettings: [
+                // Enable better optimizations when building in Release configuration. Despite the use of
+                // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
+                // builds. See <https://github.com/swift-server/guides#building-for-production> for details.
+                .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
+            ]
+        ),
+    ]
+)

+ 48 - 0
frameworks/Swift/hummingbird2/src/Sources/server/main.swift

@@ -0,0 +1,48 @@
+import Hummingbird
+import Logging
+import NIOCore
+
+struct Object: ResponseEncodable {
+    let message: String
+}
+
+struct TechFrameworkRequestContext: RequestContext {
+    static let jsonEncoder = JSONEncoder()
+    static let jsonDecoder = JSONDecoder()
+
+    var coreContext: Hummingbird.CoreRequestContext
+
+    // Use a global JSON Encoder
+    var responseEncoder: JSONEncoder { Self.jsonEncoder }
+    // Use a global JSON Decoder
+    var requestDecoder: JSONDecoder { Self.jsonDecoder }
+
+    init(channel: any Channel, logger: Logger) {
+        self.coreContext = .init(allocator: channel.allocator, logger: logger)
+    }
+}
+
+func runApp() async throws {
+    let env = Environment()
+    let serverHostName = env.get("SERVER_HOSTNAME") ?? "127.0.0.1"
+    let serverPort = env.get("SERVER_PORT", as: Int.self) ?? 8080
+
+    let router = Router(context: TechFrameworkRequestContext.self)
+    router.get("plaintext") { _,_ in
+        "Hello, world!"
+    }
+    router.get("json") { _,_ in
+        Object(message: "Hello, world!")
+    }
+    let app = Application(
+        router: router,
+        configuration: .init(
+            address: .hostname(serverHostName, port: serverPort),
+            serverName: "HB",
+            backlog: 8192
+        )
+    )
+    try await app.runService()
+}
+
+try await runApp()