Browse Source

Add HummingbirdCore plain text and JSON tests (#6837)

Adam Fowler 3 years ago
parent
commit
e5f38b2339

+ 7 - 0
frameworks/Swift/hummingbird-core/.gitignore

@@ -0,0 +1,7 @@
+.DS_Store
+.build/
+/*.xcodeproj
+xcuserdata/
+.swiftpm/
+DerivedData/
+Package.resolved

+ 22 - 0
frameworks/Swift/hummingbird-core/README.md

@@ -0,0 +1,22 @@
+# Hummingbird Core Benchmarking Test
+
+Hummingbird Core is the HTTP server for the Hummingbird framework.
+
+### Test Type Implementation Source Code
+
+* [JSON](src/Sources/server/main.swift)
+* [PLAINTEXT](src/Sources/server/main.swift)
+
+## Important Libraries
+This version of Hummingbird requires
+* [Swift 5.3](https://swift.org)  
+* [SwiftNIO 2.x](https://github.com/apple/swift-nio/)
+
+## Test URLs
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext

+ 24 - 0
frameworks/Swift/hummingbird-core/benchmark_config.json

@@ -0,0 +1,24 @@
+{
+  "framework": "hummingbird-core",
+  "tests": [{
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "None",
+        "framework": "HummingbirdCore",
+        "language": "Swift",
+        "flavor": "None",
+        "orm": "None",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "HummingbirdCore",
+        "notes": "",
+        "versus": "None"
+      }
+    }]
+}

+ 29 - 0
frameworks/Swift/hummingbird-core/hummingbird-core.dockerfile

@@ -0,0 +1,29 @@
+# ================================
+# Build image
+# ================================
+FROM swift:5.5 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.5-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"]

+ 27 - 0
frameworks/Swift/hummingbird-core/src/Package.swift

@@ -0,0 +1,27 @@
+// swift-tools-version:5.3
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+    name: "server",
+    products: [
+        .executable(name: "server", targets: ["server"])
+    ],
+    dependencies: [
+        .package(url: "https://github.com/hummingbird-project/hummingbird-core.git", .upToNextMinor(from: "0.12.1")),
+    ],
+    targets: [
+        .target(name: "server",
+            dependencies: [
+                .product(name: "HummingbirdCore", package: "hummingbird-core"),
+            ],
+            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))
+            ]
+        ),
+    ]
+)

+ 132 - 0
frameworks/Swift/hummingbird-core/src/Sources/server/DateCache.swift

@@ -0,0 +1,132 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Hummingbird server framework project
+//
+// Copyright (c) 2021-2021 the Hummingbird authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import NIOCore
+import NIOPosix
+
+/// Current date cache.
+///
+/// Getting the current date formatted is an expensive operation. This creates a scheduled task that will
+/// update a cached version of the date in the format as detailed in RFC1123 once every second. To
+/// avoid threading issues it is assumed that `currentDate` will only every be accessed on the same
+/// EventLoop that the update is running.
+public class HBDateCache {
+    /// Current formatted date
+    public var currentDate: String
+
+    /// return date cache for this thread. If one doesn't exist create one scheduled on EventLoop
+    public static func getDateCache(on eventLoop: EventLoop) -> HBDateCache {
+        guard let dateCache = thread.currentValue else {
+            self.thread.currentValue = .init(eventLoop: eventLoop)
+            return self.thread.currentValue!
+        }
+        return dateCache
+    }
+
+    static func shutdownDateCaches(eventLoopGroup: EventLoopGroup) -> EventLoopFuture<Void> {
+        var dateCacheShutdownFutures: [EventLoopFuture<Void>] = []
+        for eventLoop in eventLoopGroup.makeIterator() {
+            let future: EventLoopFuture<Void> = eventLoop.flatSubmit {
+                guard let dateCache = thread.currentValue else {
+                    return eventLoop.makeSucceededFuture(())
+                }
+                thread.currentValue = nil
+                return dateCache.shutdown(eventLoop: eventLoop)
+            }
+            dateCacheShutdownFutures.append(future)
+        }
+        return EventLoopFuture.andAllComplete(dateCacheShutdownFutures, on: eventLoopGroup.next())
+    }
+
+    /// Initialize DateCache to run on a specific `EventLoop`
+    private init(eventLoop: EventLoop) {
+        assert(eventLoop.inEventLoop)
+        var timeVal = timeval.init()
+        gettimeofday(&timeVal, nil)
+        self.currentDate = Self.formatRFC1123Date(timeVal.tv_sec)
+
+        let millisecondsSinceLastSecond = Double(timeVal.tv_usec) / 1000.0
+        let millisecondsUntilNextSecond = Int64(1000.0 - millisecondsSinceLastSecond)
+        self.task = eventLoop.scheduleRepeatedTask(initialDelay: .milliseconds(millisecondsUntilNextSecond), delay: .seconds(1)) { _ in
+            self.updateDate()
+        }
+    }
+
+    private func shutdown(eventLoop: EventLoop) -> EventLoopFuture<Void> {
+        let promise = eventLoop.makePromise(of: Void.self)
+        self.task.cancel(promise: promise)
+        return promise.futureResult
+    }
+
+    /// Render Epoch seconds as RFC1123 formatted date
+    /// - Parameter epochTime: epoch seconds to render
+    /// - Returns: Formatted date
+    public static func formatRFC1123Date(_ epochTime: Int) -> String {
+        var epochTime = epochTime
+        var timeStruct = tm.init()
+        gmtime_r(&epochTime, &timeStruct)
+        let year = Int(timeStruct.tm_year + 1900)
+        let day = self.dayNames[numericCast(timeStruct.tm_wday)]
+        let month = self.monthNames[numericCast(timeStruct.tm_mon)]
+        var formatted = day
+        formatted.reserveCapacity(30)
+        formatted += ", "
+        formatted += timeStruct.tm_mday.description
+        formatted += " "
+        formatted += month
+        formatted += " "
+        formatted += self.numberNames[year / 100]
+        formatted += self.numberNames[year % 100]
+        formatted += " "
+        formatted += self.numberNames[numericCast(timeStruct.tm_hour)]
+        formatted += ":"
+        formatted += self.numberNames[numericCast(timeStruct.tm_min)]
+        formatted += ":"
+        formatted += self.numberNames[numericCast(timeStruct.tm_sec)]
+        formatted += " GMT"
+
+        return formatted
+    }
+
+    private func updateDate() {
+        let epochTime = time(nil)
+        self.currentDate = Self.formatRFC1123Date(epochTime)
+    }
+
+    /// Thread-specific HBDateCache
+    private static let thread: ThreadSpecificVariable<HBDateCache> = .init()
+
+    private var task: RepeatedTask!
+
+    private static let dayNames = [
+        "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
+    ]
+
+    private static let monthNames = [
+        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+    ]
+
+    private static let numberNames = [
+        "00", "01", "02", "03", "04", "05", "06", "07", "08", "09",
+        "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
+        "20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
+        "30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
+        "40", "41", "42", "43", "44", "45", "46", "47", "48", "49",
+        "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
+        "60", "61", "62", "63", "64", "65", "66", "67", "68", "69",
+        "70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
+        "80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
+        "90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
+    ]
+}

+ 59 - 0
frameworks/Swift/hummingbird-core/src/Sources/server/main.swift

@@ -0,0 +1,59 @@
+import Foundation
+import HummingbirdCore
+import NIOCore
+import NIOHTTP1
+import NIOPosix
+
+struct TechEmpowerResponder: HBHTTPResponder {
+    let plainTextBody = "Hello, world!"
+
+    func respond(to request: HBHTTPRequest, context: ChannelHandlerContext, onComplete: @escaping (Result<HBHTTPResponse, Error>) -> Void) {
+        let body: ByteBuffer
+        let status: HTTPResponseStatus
+        let headers: HTTPHeaders
+        switch (request.head.uri, request.head.method) {
+        case ("/plaintext", .GET):
+            status = .ok
+            headers = HTTPHeaders([
+                ("content-type", "text/plain"),
+                ("date", HBDateCache.getDateCache(on: context.eventLoop).currentDate)
+            ])
+            body = context.channel.allocator.buffer(string: plainTextBody)
+
+        case ("/json", .GET):
+            status = .ok
+            headers = HTTPHeaders([
+                ("content-type", "application/json"),
+                ("date", HBDateCache.getDateCache(on: context.eventLoop).currentDate)
+            ])
+            let json = try! JSONEncoder().encode(["message": plainTextBody])
+            body = context.channel.allocator.buffer(bytes: json)
+
+        default:
+            onComplete(.failure(HBHTTPError(.badRequest)))
+            return
+        }
+        let responseHead = HTTPResponseHead(version: .init(major: 1, minor: 1), status: status, headers: headers)
+        let response = HBHTTPResponse(head: responseHead, body: .byteBuffer(body))
+        onComplete(.success(response))
+    }
+}
+
+func runApp() throws {
+    let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
+    defer { try? eventLoopGroup.syncShutdownGracefully() }
+
+    let serverHostName = ProcessInfo.processInfo.environment["SERVER_HOSTNAME"] ?? "127.0.0.1"
+    let serverPort = ProcessInfo.processInfo.environment["SERVER_PORT"].map { Int($0) ?? 8080 } ?? 8080
+    let configuration = HBHTTPServer.Configuration(
+        address: .hostname(serverHostName, port: serverPort),
+        serverName: "hb-core",
+        withPipeliningAssistance: false
+    )
+
+    let server = HBHTTPServer(group: eventLoopGroup, configuration: configuration)
+    try server.start(responder: TechEmpowerResponder()).wait()
+    try server.wait()
+}
+
+try runApp()