Skip to content

Commit 6640423

Browse files
committed
fix: harden aws perf runner for reliable ec2/ssm execution
1 parent 0da2200 commit 6640423

File tree

2 files changed

+157
-27
lines changed

2 files changed

+157
-27
lines changed

scripts/aws-perf/ec2-compare.sh

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
77
RUN_ID="wreq-perf-$(date -u +%Y%m%dT%H%M%SZ)"
88
INSTANCE_ID=""
99
SHOULD_CLEANUP=true
10+
PARAMS_FILE=""
1011

1112
REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-}}"
1213
INSTANCE_PROFILE_NAME="wreq-js-perf-ssm-profile"
@@ -86,11 +87,15 @@ base64_encode_file() {
8687
if base64 --help 2>/dev/null | grep -q -- "-w"; then
8788
base64 -w 0 "$path"
8889
else
89-
base64 "$path" | tr -d '\n'
90+
base64 <"$path" | tr -d '\n'
9091
fi
9192
}
9293

9394
cleanup() {
95+
if [[ -n "$PARAMS_FILE" ]]; then
96+
rm -f "$PARAMS_FILE" || true
97+
fi
98+
9499
if [[ -n "$INSTANCE_ID" && "$SHOULD_CLEANUP" == true ]]; then
95100
log "Terminating instance $INSTANCE_ID"
96101
aws ec2 terminate-instances --region "$REGION" --instance-ids "$INSTANCE_ID" >/dev/null || true
@@ -178,10 +183,21 @@ EXPIRY_EPOCH="$(( $(date +%s) + TTL_HOURS * 3600 ))"
178183

179184
launch_instance() {
180185
local market="$1"
181-
local market_args=()
182186

183187
if [[ "$market" == "spot" ]]; then
184-
market_args=(--instance-market-options "MarketType=spot,SpotOptions={SpotInstanceType=one-time,InstanceInterruptionBehavior=terminate}")
188+
aws ec2 run-instances \
189+
--region "$REGION" \
190+
--image-id "$AMI_ID" \
191+
--instance-type "$INSTANCE_TYPE" \
192+
--subnet-id "$SUBNET_ID" \
193+
--security-group-ids "$SECURITY_GROUP_ID" \
194+
--iam-instance-profile "Name=$INSTANCE_PROFILE_NAME" \
195+
--block-device-mappings 'DeviceName=/dev/xvda,Ebs={VolumeSize=25,VolumeType=gp3,DeleteOnTermination=true}' \
196+
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$RUN_ID},{Key=Project,Value=wreq-js},{Key=Purpose,Value=perf-benchmark},{Key=RunId,Value=$RUN_ID},{Key=ExpiresEpoch,Value=$EXPIRY_EPOCH}]" \
197+
--instance-market-options "MarketType=spot,SpotOptions={SpotInstanceType=one-time,InstanceInterruptionBehavior=terminate}" \
198+
--query 'Instances[0].InstanceId' \
199+
--output text
200+
return
185201
fi
186202

187203
aws ec2 run-instances \
@@ -193,7 +209,6 @@ launch_instance() {
193209
--iam-instance-profile "Name=$INSTANCE_PROFILE_NAME" \
194210
--block-device-mappings 'DeviceName=/dev/xvda,Ebs={VolumeSize=25,VolumeType=gp3,DeleteOnTermination=true}' \
195211
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$RUN_ID},{Key=Project,Value=wreq-js},{Key=Purpose,Value=perf-benchmark},{Key=RunId,Value=$RUN_ID},{Key=ExpiresEpoch,Value=$EXPIRY_EPOCH}]" \
196-
"${market_args[@]}" \
197212
--query 'Instances[0].InstanceId' \
198213
--output text
199214
}
@@ -250,13 +265,31 @@ CONCURRENCY=$(shell_quote "$CONCURRENCY") \
250265
THRESHOLD_PCT=$(shell_quote "$THRESHOLD_PCT") \
251266
bash /tmp/wreq-remote-compare.sh"
252267

268+
PARAMS_FILE="$(mktemp)"
269+
270+
node - "$PARAMS_FILE" "$REMOTE_SCRIPT_B64" "$run_command" <<'NODE'
271+
const fs = require("node:fs");
272+
const paramsPath = process.argv[2];
273+
const scriptB64 = process.argv[3];
274+
const runCommand = process.argv[4];
275+
const params = {
276+
commands: [
277+
"set -euo pipefail",
278+
`echo '${scriptB64}' | base64 -d >/tmp/wreq-remote-compare.sh`,
279+
"chmod +x /tmp/wreq-remote-compare.sh",
280+
runCommand,
281+
],
282+
};
283+
fs.writeFileSync(paramsPath, JSON.stringify(params));
284+
NODE
285+
253286
COMMAND_ID="$(aws ssm send-command \
254287
--region "$REGION" \
255288
--document-name "AWS-RunShellScript" \
256289
--instance-ids "$INSTANCE_ID" \
257290
--comment "wreq-js perf compare $RUN_ID" \
258291
--timeout-seconds 7200 \
259-
--parameters "commands=set -euo pipefail,echo $REMOTE_SCRIPT_B64 | base64 -d >/tmp/wreq-remote-compare.sh,chmod +x /tmp/wreq-remote-compare.sh,$run_command" \
292+
--parameters "file://$PARAMS_FILE" \
260293
--query 'Command.CommandId' \
261294
--output text)"
262295

scripts/aws-perf/remote-compare.sh

Lines changed: 119 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env bash
2-
set -euo pipefail
2+
set -eEuo pipefail
33

44
require_env() {
55
local key="$1"
@@ -29,27 +29,48 @@ log() {
2929
printf '[remote] %s\n' "$*"
3030
}
3131

32+
dump_failure_context() {
33+
local exit_code=$?
34+
log "Command failed with exit code ${exit_code}. Dumping available logs from ${RESULT_DIR}:"
35+
36+
local found=false
37+
for file in "$RESULT_DIR"/*.log; do
38+
if [[ -f "$file" ]]; then
39+
found=true
40+
echo "----- ${file} (tail) -----"
41+
tail -n 200 "$file" || true
42+
fi
43+
done
44+
45+
if [[ "$found" == false ]]; then
46+
log "No log files found."
47+
fi
48+
49+
exit "$exit_code"
50+
}
51+
trap dump_failure_context ERR
52+
3253
base64_encode_file() {
3354
local path="$1"
3455
if base64 --help 2>/dev/null | grep -q -- "-w"; then
3556
base64 -w 0 "$path"
3657
else
37-
base64 "$path" | tr -d '\n'
58+
base64 <"$path" | tr -d '\n'
3859
fi
3960
}
4061

4162
install_system_packages() {
4263
if command -v dnf >/dev/null 2>&1; then
4364
log "Installing packages via dnf"
44-
dnf install -y git curl tar xz gzip gcc gcc-c++ make perl-core which >/dev/null
65+
dnf install -y git tar xz gzip gcc gcc-c++ make perl-core which cmake clang clang-libs >/dev/null
4566
return
4667
fi
4768

4869
if command -v apt-get >/dev/null 2>&1; then
4970
log "Installing packages via apt-get"
5071
export DEBIAN_FRONTEND=noninteractive
5172
apt-get update -y >/dev/null
52-
apt-get install -y git curl ca-certificates build-essential xz-utils >/dev/null
73+
apt-get install -y git curl ca-certificates build-essential xz-utils cmake clang libclang-dev >/dev/null
5374
return
5475
fi
5576

@@ -106,13 +127,15 @@ ensure_rust() {
106127
}
107128

108129
source_rust_env() {
130+
export HOME="${HOME:-/root}"
131+
109132
if [[ -f "/root/.cargo/env" ]]; then
110133
# shellcheck disable=SC1091
111134
source /root/.cargo/env
112135
fi
113-
if [[ -f "$HOME/.cargo/env" ]]; then
136+
if [[ -f "${HOME}/.cargo/env" ]]; then
114137
# shellcheck disable=SC1090
115-
source "$HOME/.cargo/env"
138+
source "${HOME}/.cargo/env"
116139
fi
117140
}
118141

@@ -138,16 +161,24 @@ run_bench() {
138161
git -C "$repo_dir" reset --hard "$ref" >/dev/null
139162

140163
log "Installing npm dependencies for ${label}"
141-
(
164+
if ! (
142165
cd "$repo_dir"
143166
npm ci --no-audit --no-fund >"$RESULT_DIR/${label}-npm.log" 2>&1
144-
)
167+
); then
168+
echo "----- ${RESULT_DIR}/${label}-npm.log (tail) -----" >&2
169+
tail -n 200 "$RESULT_DIR/${label}-npm.log" >&2 || true
170+
return 1
171+
fi
145172

146173
log "Building native module for ${label}"
147-
(
174+
if ! (
148175
cd "$repo_dir"
149176
npm run build:rust >"$build_log" 2>&1
150-
)
177+
); then
178+
echo "----- ${build_log} (tail) -----" >&2
179+
tail -n 200 "$build_log" >&2 || true
180+
return 1
181+
fi
151182

152183
local -a bench_args
153184
bench_args=(
@@ -167,10 +198,14 @@ run_bench() {
167198
bench_args+=("--json" "$json_out")
168199

169200
log "Running benchmark ${label}"
170-
(
201+
if ! (
171202
cd "$repo_dir"
172203
npm run bench:run -- "${bench_args[@]}" >"$bench_log" 2>&1
173-
)
204+
); then
205+
echo "----- ${bench_log} (tail) -----" >&2
206+
tail -n 200 "$bench_log" >&2 || true
207+
return 1
208+
fi
174209
}
175210

176211
main() {
@@ -203,16 +238,78 @@ main() {
203238
run_bench "head" "$HEAD_REF" "$repo_dir"
204239

205240
log "Generating comparison report"
206-
(
207-
cd "$repo_dir"
208-
node scripts/aws-perf/compare-bench.mjs \
209-
--base "$RESULT_DIR/base.json" \
210-
--head "$RESULT_DIR/head.json" \
211-
--threshold-pct "$THRESHOLD_PCT" \
212-
--markdown "$RESULT_DIR/report.md" \
213-
--json "$RESULT_DIR/report.json" \
214-
--no-fail >"$RESULT_DIR/compare.log" 2>&1
215-
)
241+
node - "$RESULT_DIR/base.json" "$RESULT_DIR/head.json" "$THRESHOLD_PCT" "$RESULT_DIR/report.md" "$RESULT_DIR/report.json" >"$RESULT_DIR/compare.log" 2>&1 <<'NODE'
242+
const fs = require("node:fs");
243+
244+
const basePath = process.argv[2];
245+
const headPath = process.argv[3];
246+
const thresholdPct = Number(process.argv[4]);
247+
const markdownPath = process.argv[5];
248+
const jsonPath = process.argv[6];
249+
250+
const baseRun = JSON.parse(fs.readFileSync(basePath, "utf8"));
251+
const headRun = JSON.parse(fs.readFileSync(headPath, "utf8"));
252+
253+
const baseMap = new Map(baseRun.results.map((result) => [result.name, result]));
254+
const headMap = new Map(headRun.results.map((result) => [result.name, result]));
255+
256+
const names = [...baseMap.keys()].filter((name) => headMap.has(name)).sort();
257+
const scenarios = names.map((name) => {
258+
const base = baseMap.get(name);
259+
const head = headMap.get(name);
260+
const deltaPct = ((head.mean - base.mean) / base.mean) * 100;
261+
const regression = deltaPct <= -thresholdPct;
262+
const improvement = deltaPct >= thresholdPct;
263+
return {
264+
name,
265+
baseMean: base.mean,
266+
headMean: head.mean,
267+
deltaPct,
268+
baseCiPct: base.ci95.marginPct,
269+
headCiPct: head.ci95.marginPct,
270+
status: regression ? "REGRESSION" : improvement ? "IMPROVEMENT" : "OK",
271+
};
272+
});
273+
274+
const regressions = scenarios.filter((item) => item.status === "REGRESSION").map((item) => item.name);
275+
const report = {
276+
generatedAt: new Date().toISOString(),
277+
thresholdPct,
278+
baseCommit: baseRun.git?.commit,
279+
headCommit: headRun.git?.commit,
280+
regressions,
281+
pass: regressions.length === 0,
282+
scenarios,
283+
};
284+
285+
const num = (value) => value.toLocaleString("en-US", { maximumFractionDigits: 2 });
286+
const pct = (value) => `${value > 0 ? "+" : ""}${value.toFixed(2)}%`;
287+
288+
const lines = [];
289+
lines.push("# AWS Perf Compare");
290+
lines.push("");
291+
lines.push(`- Generated: ${report.generatedAt}`);
292+
lines.push(`- Threshold: ${report.thresholdPct}% throughput drop => regression`);
293+
if (report.baseCommit) lines.push(`- Base commit: ${report.baseCommit}`);
294+
if (report.headCommit) lines.push(`- Head commit: ${report.headCommit}`);
295+
lines.push("");
296+
lines.push("| Scenario | Base req/s | Head req/s | Delta | Status | Base CI | Head CI |");
297+
lines.push("|---|---:|---:|---:|---|---:|---:|");
298+
for (const row of scenarios) {
299+
lines.push(
300+
`| ${row.name} | ${num(row.baseMean)} | ${num(row.headMean)} | ${pct(row.deltaPct)} | ${row.status} | ±${row.baseCiPct.toFixed(2)}% | ±${row.headCiPct.toFixed(2)}% |`,
301+
);
302+
}
303+
lines.push("");
304+
lines.push(`## Gate: ${report.pass ? "PASS" : "FAIL"}`);
305+
if (!report.pass) {
306+
lines.push(`Regressions: ${report.regressions.join(", ")}`);
307+
}
308+
309+
const markdown = lines.join("\n");
310+
fs.writeFileSync(markdownPath, markdown, "utf8");
311+
fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), "utf8");
312+
NODE
216313

217314
cat "$RESULT_DIR/report.md"
218315
echo ""

0 commit comments

Comments
 (0)