Evaluate internal WARM performance of LambdaSwiftSprinterNioPlugin with HelloWorld #38
Description
We want to provide some context on the internal performance when the Lambda is Warm
HelloWorld example:
import AsyncHTTPClient
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import LambdaSwiftSprinter
import LambdaSwiftSprinterNioPlugin
enum MyError: Error {
case unableToConvertString
}
class Lambda: LambdaHandler {
func commonHandler(event: Data, context: Context) -> LambdaResult {
guard let data = "Hello World!".data(using: .utf8) else {
return .failure(MyError.unableToConvertString)
}
return .success(data)
}
}
let lambda = Lambda()
public func log(_ object: Any, flush: Bool = false) {
fputs("\(object)\n", stderr)
if flush {
fflush(stderr)
}
}
do {
let sprinter = try SprinterNIO()
sprinter.register(handler: "helloWorld", lambda: lambda)
try sprinter.run()
} catch {
log(String(describing: error))
}
Note Data and Context are not used, but are being passed to the lambda function.
Using AWS Lambda with 256MB the warm performance is around ~1.19ms.
We want to evaluate where the execution time is spent
The main loop when the lambda is warm is contained in the following file:
https://github.com/swift-sprinter/aws-lambda-swift-sprinter-core/blob/master/Sources/LambdaSwiftSprinter/Sprinter.swift
public func run() throws {
while !cancel {
let (event, responseHeaders) = try apiClient.getNextInvocation()
counter += 1
if let lambdaRuntimeTraceId = responseHeaders.rhk(key: .runtimeTraceId) {
setenv(Context.AWSEnvironmentKey.xAmznTraceId.rawValue, lambdaRuntimeTraceId, 0)
}
guard let lambda = lambdas[handlerName] else {
try apiClient.postInitializationError(error: SprinterError.missingEnvironmentVariables(.handler))
return
}
let context = try Context(environment: environment, responseHeaders: responseHeaders)
let result = lambda.commonHandler(event: event, context: context)
switch result {
case .success(let outputData):
try apiClient.postInvocationResponse(for: context.awsRequestId, httpBody: outputData)
case .failure(let error):
try apiClient.postInvocationError(for: context.awsRequestId, error: error)
}
}
}
The getNextInvocation() is implemented in https://github.com/swift-sprinter/aws-lambda-swift-sprinter-nio-plugin/blob/master/Sources/LambdaSwiftSprinterNioPlugin/LambdaApiNIO.swift
public func getNextInvocation() throws -> (event: Data, responseHeaders: [AnyHashable: Any]) {
let result = try httpClient.execute(
request: _nextInvocationRequest,
deadline: nil
).wait()
let httpHeaders = result.headers
guard result.status.isValid() else {
throw SprinterNIOError.invalidResponse(result.status)
}
if let body = result.body,
let data = body.getData(at: 0,
length: body.readableBytes,
byteTransferStrategy: .noCopy) {
return (event: data, responseHeaders: httpHeaders.dictionary)
} else {
throw SprinterNIOError.invalidBuffer
}
}
Note that the Lambda remains in waiting for the next event while calling getNextInvocation() in the run loop until is killed or the timeout of 3600s is triggered.
We want understand if some optimisation can improve the performance
Reference Golang with a performance at warm of ~0.44 ms.
package main
import "github.com/aws/aws-lambda-go/lambda"
type Response struct {
StatusCode int `json:"statusCode"`
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded"`
}
func hello() (Response, error) {
return Response{
StatusCode: 200,
Body: "Hello world",
IsBase64Encoded: false,
}, nil
}
func main() {
lambda.Start(hello)
}