Skip to content

Commit 2bed417

Browse files
tomerdGitHub Enterprise
authored and
GitHub Enterprise
committed
performance tests (#16)
motivation: benchmark for comparison of warm/cold runs changes: * refactor configuration * add mock server that can be used by perf tests * add simple perf test script * change redundant classes to structs, make remaining classes final * make offloading opt-in * safer locking * fix format
1 parent bd3cf9e commit 2bed417

24 files changed

+539
-200
lines changed

.swiftformat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
--patternlet inline
99
--stripunusedargs unnamed-only
1010
--comments ignore
11+
--ifdef no-indent
1112

1213
# rules

Package.swift

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,24 @@
22

33
import PackageDescription
44

5-
var targets: [PackageDescription.Target] = [
6-
.target(name: "SwiftAwsLambda", dependencies: ["Logging", "Backtrace", "NIOHTTP1"]),
7-
.target(name: "SwiftAwsLambdaSample", dependencies: ["SwiftAwsLambda"]),
8-
.target(name: "SwiftAwsLambdaStringSample", dependencies: ["SwiftAwsLambda"]),
9-
.target(name: "SwiftAwsLambdaCodableSample", dependencies: ["SwiftAwsLambda"]),
10-
.testTarget(name: "SwiftAwsLambdaTests", dependencies: ["SwiftAwsLambda"]),
11-
]
12-
135
let package = Package(
146
name: "swift-aws-lambda",
157
products: [
168
.library(name: "SwiftAwsLambda", targets: ["SwiftAwsLambda"]),
17-
.executable(name: "SwiftAwsLambdaSample", targets: ["SwiftAwsLambdaSample"]),
18-
.executable(name: "SwiftAwsLambdaStringSample", targets: ["SwiftAwsLambdaStringSample"]),
19-
.executable(name: "SwiftAwsLambdaCodableSample", targets: ["SwiftAwsLambdaCodableSample"]),
209
],
2110
dependencies: [
2211
.package(url: "https://github.com/apple/swift-nio.git", from: "2.8.0"),
2312
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
2413
.package(url: "https://github.com/ianpartridge/swift-backtrace.git", from: "1.1.0"),
2514
],
26-
targets: targets
15+
targets: [
16+
.target(name: "SwiftAwsLambda", dependencies: ["Logging", "Backtrace", "NIOHTTP1"]),
17+
.testTarget(name: "SwiftAwsLambdaTests", dependencies: ["SwiftAwsLambda"]),
18+
// samples
19+
.target(name: "SwiftAwsLambdaSample", dependencies: ["SwiftAwsLambda"]),
20+
.target(name: "SwiftAwsLambdaStringSample", dependencies: ["SwiftAwsLambda"]),
21+
.target(name: "SwiftAwsLambdaCodableSample", dependencies: ["SwiftAwsLambda"]),
22+
// perf tests
23+
.target(name: "MockServer", dependencies: ["Logging", "NIOHTTP1"]),
24+
]
2725
)

Sources/MockServer/main.swift

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAwsLambda open source project
4+
//
5+
// Copyright (c) 2017-2018 Apple Inc. and the SwiftAwsLambda project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAwsLambda project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import Logging
17+
import NIO
18+
import NIOHTTP1
19+
20+
internal struct MockServer {
21+
private let logger: Logger
22+
private let group: EventLoopGroup
23+
private let host: String
24+
private let port: Int
25+
private let mode: Mode
26+
private let keepAlive: Bool
27+
28+
public init() {
29+
var logger = Logger(label: "MockServer")
30+
logger.logLevel = env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info
31+
self.logger = logger
32+
self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
33+
self.host = env("HOST") ?? "127.0.0.1"
34+
self.port = env("PORT").flatMap(Int.init) ?? 7000
35+
self.mode = env("MODE").flatMap(Mode.init) ?? .string
36+
self.keepAlive = env("KEEP_ALIVE").flatMap(Bool.init) ?? true
37+
}
38+
39+
func start() throws {
40+
let bootstrap = ServerBootstrap(group: group)
41+
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
42+
.childChannelInitializer { channel in
43+
channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in
44+
channel.pipeline.addHandler(HTTPHandler(logger: self.logger,
45+
keepAlive: self.keepAlive,
46+
mode: self.mode))
47+
}
48+
}
49+
try bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture<Void> in
50+
guard let localAddress = channel.localAddress else {
51+
return channel.eventLoop.makeFailedFuture(ServerError.cantBind)
52+
}
53+
self.logger.info("\(self) started and listening on \(localAddress)")
54+
return channel.eventLoop.makeSucceededFuture(())
55+
}.wait()
56+
}
57+
}
58+
59+
internal final class HTTPHandler: ChannelInboundHandler {
60+
public typealias InboundIn = HTTPServerRequestPart
61+
public typealias OutboundOut = HTTPServerResponsePart
62+
63+
private let logger: Logger
64+
private let mode: Mode
65+
private let keepAlive: Bool
66+
67+
private var requestHead: HTTPRequestHead!
68+
private var requestBody: ByteBuffer?
69+
70+
public init(logger: Logger, keepAlive: Bool, mode: Mode) {
71+
self.logger = logger
72+
self.mode = mode
73+
self.keepAlive = keepAlive
74+
}
75+
76+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
77+
let requestPart = unwrapInboundIn(data)
78+
79+
switch requestPart {
80+
case .head(let head):
81+
self.requestHead = head
82+
self.requestBody?.clear()
83+
case .body(var buffer):
84+
if self.requestBody == nil {
85+
self.requestBody = buffer
86+
} else {
87+
self.requestBody!.writeBuffer(&buffer)
88+
}
89+
case .end:
90+
self.processRequest(context: context)
91+
}
92+
}
93+
94+
func processRequest(context: ChannelHandlerContext) {
95+
self.logger.debug("\(self) processing \(self.requestHead.uri)")
96+
97+
var responseStatus: HTTPResponseStatus
98+
var responseBody: String?
99+
var responseHeaders: [(String, String)]?
100+
101+
if self.requestHead.uri.hasSuffix("/next") {
102+
let requestId = UUID().uuidString
103+
responseStatus = .ok
104+
switch self.mode {
105+
case .string:
106+
responseBody = requestId
107+
case .json:
108+
responseBody = "{ \"body\": \"\(requestId)\" }"
109+
}
110+
responseHeaders = [(AmazonHeaders.requestID, requestId)]
111+
} else if self.requestHead.uri.hasSuffix("/response") {
112+
responseStatus = .accepted
113+
} else {
114+
responseStatus = .notFound
115+
}
116+
self.writeResponse(context: context, status: responseStatus, headers: responseHeaders, body: responseBody)
117+
}
118+
119+
func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: String? = nil) {
120+
var headers = HTTPHeaders(headers ?? [])
121+
headers.add(name: "Content-Length", value: "\(body?.utf8.count ?? 0)")
122+
headers.add(name: "Connection", value: self.keepAlive ? "keep-alive" : "close")
123+
let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: status, headers: headers)
124+
125+
context.write(wrapOutboundOut(.head(head))).whenFailure { error in
126+
self.logger.error("\(self) write error \(error)")
127+
}
128+
129+
if let b = body {
130+
var buffer = context.channel.allocator.buffer(capacity: b.utf8.count)
131+
buffer.writeString(b)
132+
context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in
133+
self.logger.error("\(self) write error \(error)")
134+
}
135+
}
136+
137+
context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in
138+
if case .failure(let error) = result {
139+
self.logger.error("\(self) write error \(error)")
140+
}
141+
if !self.self.keepAlive {
142+
context.close().whenFailure { error in
143+
self.logger.error("\(self) close error \(error)")
144+
}
145+
}
146+
}
147+
}
148+
}
149+
150+
internal enum ServerError: Error {
151+
case notReady
152+
case cantBind
153+
}
154+
155+
internal enum AmazonHeaders {
156+
static let requestID = "Lambda-Runtime-Aws-Request-Id"
157+
static let traceID = "Lambda-Runtime-Trace-Id"
158+
static let clientContext = "X-Amz-Client-Context"
159+
static let cognitoIdentity = "X-Amz-Cognito-Identity"
160+
static let deadline = "Lambda-Runtime-Deadline-Ms"
161+
static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn"
162+
}
163+
164+
internal enum Mode: String {
165+
case string
166+
case json
167+
}
168+
169+
func env(_ name: String) -> String? {
170+
guard let value = getenv(name) else {
171+
return nil
172+
}
173+
return String(utf8String: value)
174+
}
175+
176+
// main
177+
let server = MockServer()
178+
try! server.start()
179+
dispatchMain()

Sources/SwiftAwsLambda/HttpClient.swift

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,61 +12,56 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Foundation
1615
import NIO
1716
import NIOConcurrencyHelpers
1817
import NIOHTTP1
1918

2019
/// A barebone HTTP client to interact with AWS Runtime Engine which is an HTTP server.
2120
internal class HTTPClient {
2221
private let eventLoop: EventLoop
23-
private let config: Lambda.Config.RuntimeEngine
22+
private let configuration: Lambda.Configuration.RuntimeEngine
2423

25-
private var _state = State.disconnected
24+
private var state = State.disconnected
2625
private let lock = Lock()
2726

28-
init(eventLoop: EventLoop, config: Lambda.Config.RuntimeEngine) {
27+
init(eventLoop: EventLoop, configuration: Lambda.Configuration.RuntimeEngine) {
2928
self.eventLoop = eventLoop
30-
self.config = config
31-
}
32-
33-
private var state: State {
34-
get {
35-
return self.lock.withLock {
36-
self._state
37-
}
38-
}
39-
set {
40-
self.lock.withLockVoid {
41-
self._state = newValue
42-
}
43-
}
29+
self.configuration = configuration
4430
}
4531

4632
func get(url: String, timeout: TimeAmount? = nil) -> EventLoopFuture<Response> {
47-
return self.execute(Request(url: self.config.baseURL.appendingPathComponent(url), method: .GET, timeout: timeout ?? self.config.requestTimeout))
33+
return self.execute(Request(url: self.configuration.baseURL.appendingPathComponent(url),
34+
method: .GET,
35+
timeout: timeout ?? self.configuration.requestTimeout))
4836
}
4937

5038
func post(url: String, body: ByteBuffer, timeout: TimeAmount? = nil) -> EventLoopFuture<Response> {
51-
return self.execute(Request(url: self.config.baseURL.appendingPathComponent(url), method: .POST, body: body, timeout: timeout ?? self.config.requestTimeout))
39+
return self.execute(Request(url: self.configuration.baseURL.appendingPathComponent(url),
40+
method: .POST,
41+
body: body,
42+
timeout: timeout ?? self.configuration.requestTimeout))
5243
}
5344

5445
private func execute(_ request: Request) -> EventLoopFuture<Response> {
46+
self.lock.lock()
5547
switch self.state {
5648
case .connected(let channel):
5749
guard channel.isActive else {
5850
// attempt to reconnect
5951
self.state = .disconnected
52+
self.lock.unlock()
6053
return self.execute(request)
6154
}
55+
self.lock.unlock()
6256
let promise = channel.eventLoop.makePromise(of: Response.self)
6357
let wrapper = HTTPRequestWrapper(request: request, promise: promise)
6458
return channel.writeAndFlush(wrapper).flatMap {
6559
promise.futureResult
6660
}
6761
case .disconnected:
6862
return self.connect().flatMap {
69-
self.execute(request)
63+
self.lock.unlock()
64+
return self.execute(request)
7065
}
7166
default:
7267
preconditionFailure("invalid state \(self.state)")
@@ -81,11 +76,11 @@ internal class HTTPClient {
8176
let bootstrap = ClientBootstrap(group: eventLoop)
8277
.channelInitializer { channel in
8378
channel.pipeline.addHTTPClientHandlers().flatMap {
84-
channel.pipeline.addHandlers([HTTPHandler(keepAlive: self.config.keepAlive),
85-
UnaryHandler(keepAlive: self.config.keepAlive)])
79+
channel.pipeline.addHandlers([HTTPHandler(keepAlive: self.configuration.keepAlive),
80+
UnaryHandler(keepAlive: self.configuration.keepAlive)])
8681
}
8782
}
88-
return bootstrap.connect(host: self.config.baseURL.host, port: self.config.baseURL.port).flatMapThrowing { channel in
83+
return bootstrap.connect(host: self.configuration.baseURL.host, port: self.configuration.baseURL.port).flatMapThrowing { channel in
8984
self.state = .connected(channel)
9085
}
9186
}

Sources/SwiftAwsLambda/Lambda+Codable.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Foundation
15+
import Foundation // for JSON
1616

1717
/// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `Codable` payloads.
1818
/// This is the most common way to use this library in AWS Lambda, since its JSON based.
@@ -32,13 +32,13 @@ extension Lambda {
3232
}
3333

3434
// for testing
35-
internal static func run<In: Decodable, Out: Encodable>(maxTimes: Int = 0, closure: @escaping LambdaCodableClosure<In, Out>) -> LambdaLifecycleResult {
36-
return self.run(handler: LambdaClosureWrapper(closure), maxTimes: maxTimes)
35+
internal static func run<In: Decodable, Out: Encodable>(configuration: Configuration = .init(), closure: @escaping LambdaCodableClosure<In, Out>) -> LambdaLifecycleResult {
36+
return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration)
3737
}
3838

3939
// for testing
40-
internal static func run<Handler>(handler: Handler, maxTimes: Int = 0) -> LambdaLifecycleResult where Handler: LambdaCodableHandler {
41-
return self.run(handler: handler as LambdaHandler, maxTimes: maxTimes)
40+
internal static func run<Handler>(handler: Handler, configuration: Configuration = .init()) -> LambdaLifecycleResult where Handler: LambdaCodableHandler {
41+
return self.run(handler: handler as LambdaHandler, configuration: configuration)
4242
}
4343
}
4444

@@ -104,9 +104,10 @@ public extension LambdaCodableHandler {
104104
/// LambdaCodableJsonCodec is an implementation of `LambdaCodableCodec` which does `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding
105105
/// using JSONEncoder and JSONDecoder respectively.
106106
// This is a class as encoder amd decoder are a class, which means its cheaper to hold a reference to both in a class then a struct.
107-
private class LambdaCodableJsonCodec<In: Decodable, Out: Encodable>: LambdaCodableCodec<In, Out> {
107+
private final class LambdaCodableJsonCodec<In: Decodable, Out: Encodable>: LambdaCodableCodec<In, Out> {
108108
private let encoder = JSONEncoder()
109109
private let decoder = JSONDecoder()
110+
110111
public override func encode(_ value: Out) -> Result<[UInt8], Error> {
111112
do {
112113
return .success(try [UInt8](self.encoder.encode(value)))

Sources/SwiftAwsLambda/Lambda+String.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ extension Lambda {
2929
}
3030

3131
// for testing
32-
internal static func run(maxTimes: Int = 0, _ closure: @escaping LambdaStringClosure) -> LambdaLifecycleResult {
33-
return self.run(handler: LambdaClosureWrapper(closure), maxTimes: maxTimes)
32+
internal static func run(configuration: Configuration = .init(), _ closure: @escaping LambdaStringClosure) -> LambdaLifecycleResult {
33+
return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration)
3434
}
3535

3636
// for testing
37-
internal static func run(handler: LambdaStringHandler, maxTimes: Int = 0) -> LambdaLifecycleResult {
38-
return self.run(handler: handler as LambdaHandler, maxTimes: maxTimes)
37+
internal static func run(handler: LambdaStringHandler, configuration: Configuration = .init()) -> LambdaLifecycleResult {
38+
return self.run(handler: handler as LambdaHandler, configuration: configuration)
3939
}
4040
}
4141

0 commit comments

Comments
 (0)