Skip to content

Commit 2990493

Browse files
authored
Perf (#1)
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 2990493

24 files changed

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

Sources/SwiftAwsLambda/HttpClient.swift

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,61 +12,57 @@
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
29+
self.configuration = configuration
3130
}
3231

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-
}
44-
}
4532

4633
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))
34+
return self.execute(Request(url: self.configuration.baseURL.appendingPathComponent(url),
35+
method: .GET,
36+
timeout: timeout ?? self.configuration.requestTimeout))
4837
}
4938

5039
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))
40+
return self.execute(Request(url: self.configuration.baseURL.appendingPathComponent(url),
41+
method: .POST,
42+
body: body,
43+
timeout: timeout ?? self.configuration.requestTimeout))
5244
}
5345

5446
private func execute(_ request: Request) -> EventLoopFuture<Response> {
47+
self.lock.lock()
5548
switch self.state {
5649
case .connected(let channel):
5750
guard channel.isActive else {
5851
// attempt to reconnect
5952
self.state = .disconnected
53+
self.lock.unlock()
6054
return self.execute(request)
6155
}
56+
self.lock.unlock()
6257
let promise = channel.eventLoop.makePromise(of: Response.self)
6358
let wrapper = HTTPRequestWrapper(request: request, promise: promise)
6459
return channel.writeAndFlush(wrapper).flatMap {
6560
promise.futureResult
6661
}
6762
case .disconnected:
6863
return self.connect().flatMap {
69-
self.execute(request)
64+
self.lock.unlock()
65+
return self.execute(request)
7066
}
7167
default:
7268
preconditionFailure("invalid state \(self.state)")
@@ -81,11 +77,11 @@ internal class HTTPClient {
8177
let bootstrap = ClientBootstrap(group: eventLoop)
8278
.channelInitializer { channel in
8379
channel.pipeline.addHTTPClientHandlers().flatMap {
84-
channel.pipeline.addHandlers([HTTPHandler(keepAlive: self.config.keepAlive),
85-
UnaryHandler(keepAlive: self.config.keepAlive)])
80+
channel.pipeline.addHandlers([HTTPHandler(keepAlive: self.configuration.keepAlive),
81+
UnaryHandler(keepAlive: self.configuration.keepAlive)])
8682
}
8783
}
88-
return bootstrap.connect(host: self.config.baseURL.host, port: self.config.baseURL.port).flatMapThrowing { channel in
84+
return bootstrap.connect(host: self.configuration.baseURL.host, port: self.configuration.baseURL.port).flatMapThrowing { channel in
8985
self.state = .connected(channel)
9086
}
9187
}

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)