Skip to content

Commit 11bea7b

Browse files
authored
Performance Test the invocation loop (fix #377) (#542)
re-implement MAX_INVOCATIONS and fix shell script Fix #377 ### Motivation: In v1, there was a script measuring the performance of the invocation loop. Re-instate this script to allow users and developers to measure the performance impact of their changes. ### Modifications: I re-implemented MAX_INVOCATIONS, to avoid the client looping against the Mock Server. But this time, MAX_INVOCATIONS is handled on the server, not on the client. I slightly modified the script to work with v2 and the new MockServer. ### Result: The script works. This PR has a dependency on #465
1 parent bae9f27 commit 11bea7b

File tree

2 files changed

+77
-67
lines changed

2 files changed

+77
-67
lines changed

Sources/MockServer/MockHTTPServer.swift

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ struct HttpServer {
3535
private let eventLoopGroup: MultiThreadedEventLoopGroup
3636
/// the mode. Are we mocking a server for a Lambda function that expects a String or a JSON document? (default: string)
3737
private let mode: Mode
38-
/// the number of connections this server must accept before shutting down (default: 1)
38+
/// the number of invocations this server must accept before shutting down (default: 1)
3939
private let maxInvocations: Int
4040
/// the logger (control verbosity with LOG_LEVEL environment variable)
4141
private let logger: Logger
@@ -91,10 +91,6 @@ struct HttpServer {
9191
]
9292
)
9393

94-
// This counter is used to track the number of incoming connections.
95-
// This mock servers accepts n TCP connection then shutdowns
96-
let connectionCounter = SharedCounter(maxValue: self.maxInvocations)
97-
9894
// We are handling each incoming connection in a separate child task. It is important
9995
// to use a discarding task group here which automatically discards finished child tasks.
10096
// A normal task group retains all child tasks and their outputs in memory until they are
@@ -105,21 +101,15 @@ struct HttpServer {
105101
try await channel.executeThenClose { inbound in
106102
for try await connectionChannel in inbound {
107103

108-
let counter = connectionCounter.current()
109-
logger.trace("Handling new connection", metadata: ["connectionNumber": "\(counter)"])
110-
111104
group.addTask {
112-
await self.handleConnection(channel: connectionChannel)
113-
logger.trace("Done handling connection", metadata: ["connectionNumber": "\(counter)"])
105+
await self.handleConnection(channel: connectionChannel, maxInvocations: self.maxInvocations)
106+
logger.trace("Done handling connection")
114107
}
115108

116-
if connectionCounter.increment() {
117-
logger.info(
118-
"Maximum number of connections reached, shutting down after current connection",
119-
metadata: ["maxConnections": "\(self.maxInvocations)"]
120-
)
121-
break // this causes the server to shutdown after handling the connection
122-
}
109+
// This mock server only accepts one connection
110+
// the Lambda Function Runtime will send multiple requests on that single connection
111+
// This Mock Server closes the connection when MAX_INVOCATION is reached
112+
break
123113
}
124114
}
125115
}
@@ -131,17 +121,19 @@ struct HttpServer {
131121
/// It handles two requests: one for the next invocation and one for the response.
132122
/// when the maximum number of requests is reached, it closes the connection.
133123
private func handleConnection(
134-
channel: NIOAsyncChannel<HTTPServerRequestPart, HTTPServerResponsePart>
124+
channel: NIOAsyncChannel<HTTPServerRequestPart, HTTPServerResponsePart>,
125+
maxInvocations: Int
135126
) async {
136127

137128
var requestHead: HTTPRequestHead!
138129
var requestBody: ByteBuffer?
139130

140-
// each Lambda invocation results in TWO HTTP requests (next and response)
141-
let requestCount = SharedCounter(maxValue: 2)
131+
// each Lambda invocation results in TWO HTTP requests (GET /next and POST /response)
132+
let maxRequests = maxInvocations * 2
133+
let requestCount = SharedCounter(maxValue: maxRequests)
142134

143135
// Note that this method is non-throwing and we are catching any error.
144-
// We do this since we don't want to tear down the whole server when a single connection
136+
// We do this since we don't want to tear down the whole server when a single request
145137
// encounters an error.
146138
do {
147139
try await channel.executeThenClose { inbound, outbound in
@@ -178,7 +170,7 @@ struct HttpServer {
178170
if requestCount.increment() {
179171
logger.info(
180172
"Maximum number of requests reached, closing this connection",
181-
metadata: ["maxRequest": "2"]
173+
metadata: ["maxRequest": "\(maxRequests)"]
182174
)
183175
break // this finishes handiling request on this connection
184176
}

scripts/performance_test.sh

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
##
44
## This source file is part of the SwiftAWSLambdaRuntime open source project
55
##
6-
## Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
## Copyright (c) 2017-2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors
77
## Licensed under Apache License v2.0
88
##
99
## See LICENSE.txt for license information
@@ -13,67 +13,89 @@
1313
##
1414
##===----------------------------------------------------------------------===##
1515

16-
set -eu
16+
log() { printf -- "** %s\n" "$*" >&2; }
17+
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
18+
fatal() { error "$@"; exit 1; }
1719

1820
export HOST=127.0.0.1
19-
export PORT=3000
21+
export PORT=7000
2022
export AWS_LAMBDA_RUNTIME_API="$HOST:$PORT"
21-
export LOG_LEVEL=warning # important, otherwise log becomes a bottleneck
23+
export LOG_LEVEL=error # important, otherwise log becomes a bottleneck
24+
25+
DATE_CMD="date"
26+
# using gdate on darwin for nanoseconds
27+
# gdate is installed by coreutils on macOS
28+
if [[ $(uname -s) == "Darwin" ]]; then
29+
if ! command -v gdate &> /dev/null; then
30+
# shellcheck disable=SC2006 # we explicitly want to use backticks here
31+
fatal "gdate could not be found. Please \`brew install coreutils\` to proceed."
32+
fi
33+
DATE_CMD="gdate"
34+
fi
35+
echo "⏱️ using $DATE_CMD to count time"
2236

23-
# using gdate on mdarwin for nanoseconds
24-
if [[ $(uname -s) == "Linux" ]]; then
25-
shopt -s expand_aliases
26-
alias gdate="date"
37+
if ! command -v "$DATE_CMD" &> /dev/null; then
38+
fatal "$DATE_CMD could not be found. Please install $DATE_CMD to proceed."
2739
fi
2840

41+
echo "🏗️ Building library and test functions"
2942
swift build -c release -Xswiftc -g
3043
LAMBDA_USE_LOCAL_DEPS=../.. swift build --package-path Examples/HelloWorld -c release -Xswiftc -g
3144
LAMBDA_USE_LOCAL_DEPS=../.. swift build --package-path Examples/HelloJSON -c release -Xswiftc -g
3245

3346
cleanup() {
34-
kill -9 $server_pid # ignore-unacceptable-language
47+
pkill -9 MockServer && echo "killed previous mock server" # ignore-unacceptable-language
3548
}
3649

37-
trap "cleanup" ERR
50+
# start a mock server
51+
start_mockserver() {
52+
if [ $# -ne 2 ]; then
53+
fatal "Usage: $0 <mode> <invocations>"
54+
fi
55+
MODE=$1
56+
INVOCATIONS=$2
57+
pkill -9 MockServer && echo "killed previous mock server" && sleep 1 # ignore-unacceptable-language
58+
echo "👨‍🔧 starting server in $MODE mode for $INVOCATIONS invocations"
59+
(MAX_INVOCATIONS="$INVOCATIONS" MODE="$MODE" ./.build/release/MockServer) &
60+
server_pid=$!
61+
sleep 1
62+
kill -0 $server_pid # check server is alive # ignore-unacceptable-language
63+
}
3864

39-
cold_iterations=1000
40-
warm_iterations=10000
65+
cold_iterations=100
66+
warm_iterations=1000
4167
results=()
4268

4369
#------------------
4470
# string
4571
#------------------
4672

47-
export MODE=string
73+
MODE=string
4874

49-
# start (fork) mock server
50-
pkill -9 MockServer && echo "killed previous servers" && sleep 1 # ignore-unacceptable-language
51-
echo "starting server in $MODE mode"
52-
(./.build/release/MockServer) &
53-
server_pid=$!
54-
sleep 1
55-
kill -0 $server_pid # check server is alive # ignore-unacceptable-language
75+
# Start mock server
76+
start_mockserver "$MODE" "$cold_iterations"
5677

5778
# cold start
58-
echo "running $MODE mode cold test"
79+
echo "🚀❄️ running $MODE mode $cold_iterations cold test"
5980
cold=()
60-
export MAX_REQUESTS=1
6181
for (( i=0; i<cold_iterations; i++ )); do
62-
start=$(gdate +%s%N)
82+
start=$("$DATE_CMD" +%s%N)
6383
./Examples/HelloWorld/.build/release/MyLambda
64-
end=$(gdate +%s%N)
84+
end=$("$DATE_CMD" +%s%N)
6585
cold+=( $((end-start)) )
6686
done
6787
sum_cold=$(IFS=+; echo "$((${cold[*]}))")
6888
avg_cold=$((sum_cold/cold_iterations))
6989
results+=( "$MODE, cold: $avg_cold (ns)" )
7090

91+
# reset mock server
92+
start_mockserver "$MODE" "$warm_iterations"
93+
7194
# normal calls
72-
echo "running $MODE mode warm test"
73-
export MAX_REQUESTS=$warm_iterations
74-
start=$(gdate +%s%N)
95+
echo "🚀🌤️ running $MODE mode warm test"
96+
start=$("$DATE_CMD" +%s%N)
7597
./Examples/HelloWorld/.build/release/MyLambda
76-
end=$(gdate +%s%N)
98+
end=$("$DATE_CMD" +%s%N)
7799
sum_warm=$((end-start-avg_cold)) # substract by avg cold since the first call is cold
78100
avg_warm=$((sum_warm/(warm_iterations-1))) # substract since the first call is cold
79101
results+=( "$MODE, warm: $avg_warm (ns)" )
@@ -84,34 +106,30 @@ results+=( "$MODE, warm: $avg_warm (ns)" )
84106

85107
export MODE=json
86108

87-
# start (fork) mock server
88-
pkill -9 MockServer && echo "killed previous servers" && sleep 1 # ignore-unacceptable-language
89-
echo "starting server in $MODE mode"
90-
(./.build/release/MockServer) &
91-
server_pid=$!
92-
sleep 1
93-
kill -0 $server_pid # check server is alive # ignore-unacceptable-language
109+
# Start mock server
110+
start_mockserver "$MODE" "$cold_iterations"
94111

95112
# cold start
96-
echo "running $MODE mode cold test"
113+
echo "🚀❄️ running $MODE mode cold test"
97114
cold=()
98-
export MAX_REQUESTS=1
99115
for (( i=0; i<cold_iterations; i++ )); do
100-
start=$(gdate +%s%N)
101-
./Examples/HelloJSON/.build/release/MyLambda
102-
end=$(gdate +%s%N)
116+
start=$("$DATE_CMD" +%s%N)
117+
./Examples/HelloJSON/.build/release/HelloJSON
118+
end=$("$DATE_CMD" +%s%N)
103119
cold+=( $((end-start)) )
104120
done
105121
sum_cold=$(IFS=+; echo "$((${cold[*]}))")
106122
avg_cold=$((sum_cold/cold_iterations))
107123
results+=( "$MODE, cold: $avg_cold (ns)" )
108124

125+
# reset mock server
126+
start_mockserver "$MODE" "$warm_iterations"
127+
109128
# normal calls
110-
echo "running $MODE mode warm test"
111-
export MAX_REQUESTS=$warm_iterations
112-
start=$(gdate +%s%N)
113-
./Examples/HelloJSON/.build/release/MyLambda
114-
end=$(gdate +%s%N)
129+
echo "🚀🌤️ running $MODE mode warm test"
130+
start=$("$DATE_CMD" +%s%N)
131+
./Examples/HelloJSON/.build/release/HelloJSON
132+
end=$("$DATE_CMD" +%s%N)
115133
sum_warm=$((end-start-avg_cold)) # substract by avg cold since the first call is cold
116134
avg_warm=$((sum_warm/(warm_iterations-1))) # substract since the first call is cold
117135
results+=( "$MODE, warm: $avg_warm (ns)" )

0 commit comments

Comments
 (0)