Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ type personalization struct {
Density string `json:"density"`
FontScale string `json:"fontScale"`
} `json:"theme"`
Tiles []tile `json:"tiles"`
FeatureFlags map[string]bool `json:"featureFlags"`
Offers []offer `json:"offers"`
Notifications []notification `json:"notifications"`
Entitlements []string `json:"entitlements"`
Tiles []tile `json:"tiles"`
FeatureFlags map[string]bool `json:"featureFlags"`
Offers []offer `json:"offers"`
Notifications []notification `json:"notifications"`
Entitlements []string `json:"entitlements"`
Experiments map[string]string `json:"experiments"`
Padding string `json:"_padding"`
Padding string `json:"_padding"`
}

type tile struct {
Expand All @@ -121,10 +121,10 @@ type offer struct {
}

type notification struct {
ID string `json:"id"`
Kind string `json:"kind"`
Message string `json:"message"`
Unread bool `json:"unread"`
ID string `json:"id"`
Kind string `json:"kind"`
Message string `json:"message"`
Unread bool `json:"unread"`
}

// basePayload returns the fixed personalization content before padding.
Expand Down Expand Up @@ -216,6 +216,27 @@ func main() {
gzPool := sync.Pool{New: func() any { w, _ := gzip.NewWriterLevel(io.Discard, gzip.BestSpeed); return w }}

mux := http.NewServeMux()

// SureRoute test object used by Akamai to determine the best edge-to-origin path.
// This is a static dummy HTML response and is separate from the latency API.
mux.HandleFunc("/sureroute-test-object.html", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}

h := w.Header()
h.Set("Content-Type", "text/html; charset=utf-8")
h.Set("Cache-Control", "no-store")
h.Set("X-Served-By", "cache")
if cfg.hsts {
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}

http.ServeFile(w, r, "/sureroute-test-object.html")
})

mux.HandleFunc("/v1/personalization", func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
if r.Method != http.MethodGet {
Expand All @@ -228,7 +249,7 @@ func main() {
h.Set("Content-Type", "application/json; charset=utf-8")
h.Set("Cache-Control", "no-store")
h.Set("X-Tier", tierFromHost(r.Host, cfg.tier))
h.Set("X-Served-By", cfg.servedBy)
h.Set("X-Served-By", "cache")
if cfg.hsts {
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
Expand All @@ -253,7 +274,7 @@ func main() {

// Liveness/readiness for load balancers and health checks.
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Served-By", cfg.servedBy)
w.Header().Set("X-Served-By", "cache")
w.WriteHeader(http.StatusOK)
io.WriteString(w, "ok\n")
})
Expand Down
67 changes: 67 additions & 0 deletions bench/curl/waterfall-linode-insecure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# waterfall.sh — cold-path per-request latency waterfall to CSV.
#
# Each request is a fresh connection (new DNS/TCP/TLS), so this captures the
# full breakdown the spec asks for: dns / tcp / tls / ttfb / download / total,
# plus the app's Server-Timing and X-Tier/X-Served-By headers.
#
# Usage:
# waterfall.sh <host> <count> [pin_ip]
# <host> e.g. t1.aws.example.com (request is https://<host>/v1/personalization)
# <count> number of samples
# [pin_ip] optional: pin <host> to this IP (T0 = DNS excluded from the path)
#
# Writes CSV rows to stdout. Redirect into results/.
set -euo pipefail

HOST="${1:?usage: waterfall.sh <host> <count> [pin_ip]}"
COUNT="${2:?usage: waterfall.sh <host> <count> [pin_ip]}"
PIN_IP="${3:-}"

URL="https://${HOST}/v1/personalization"
RESOLVE_ARGS=()
PINNED="false"
if [[ -n "$PIN_IP" ]]; then
RESOLVE_ARGS=(--resolve "${HOST}:443:${PIN_IP}")
PINNED="true"
fi

# CSV header (suppress with PRINT_HEADER=0 when appending interleaved rows)
if [[ "${PRINT_HEADER:-1}" == "1" ]]; then
echo "ts_unix,host,pinned,http_code,size_bytes,dns_ms,tcp_ms,tls_ms,ttfb_ms,download_ms,total_ms,app_ms,x_tier,x_served_by"
fi

hdr="$(mktemp)"
trap 'rm -f "$hdr"' EXIT

# curl timing vars are seconds (float); we convert to ms. time_starttransfer is
# TTFB measured from start; we subtract appconnect to keep tcp/tls/ttfb additive.
# Trailing \n so `read` returns 0 (curl -w emits no newline → would trip set -e).
fmt='%{time_namelookup} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total} %{http_code} %{size_download}\n'

for ((i = 0; i < COUNT; i++)); do
read -r t_dns t_conn t_tls t_ttfb t_total code size < <(
curl -k -sS -o /dev/null -D "$hdr" \
--http2 -H 'Accept-Encoding: gzip' \
${RESOLVE_ARGS[@]+"${RESOLVE_ARGS[@]}"} \
-w "$fmt" "$URL" || echo "0 0 0 0 0 000 0"
)

app_ms="$(awk -F'dur=' '/[Ss]erver-[Tt]iming/{print $2+0}' "$hdr" | head -1)"
x_tier="$(awk -F': ' 'tolower($1)=="x-tier"{gsub(/\r/,"",$2);print $2}' "$hdr" | head -1)"
x_served="$(awk -F': ' 'tolower($1)=="x-served-by"{gsub(/\r/,"",$2);print $2}' "$hdr" | head -1)"

awk -v ts="$(date +%s)" -v host="$HOST" -v pinned="$PINNED" \
-v dns="$t_dns" -v conn="$t_conn" -v tls="$t_tls" -v ttfb="$t_ttfb" -v tot="$t_total" \
-v code="$code" -v size="$size" -v app="${app_ms:-0}" -v xt="${x_tier:-}" -v xs="${x_served:-}" '
BEGIN {
dns_ms = dns*1000;
tcp_ms = (conn-dns)*1000;
tls_ms = (tls>0 ? (tls-conn)*1000 : 0);
ttfb_ms = (ttfb - (tls>0?tls:conn))*1000;
dl_ms = (tot-ttfb)*1000;
total_ms = tot*1000;
printf "%s,%s,%s,%s,%s,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%s,%s\n",
ts, host, pinned, code, size, dns_ms, tcp_ms, tls_ms, ttfb_ms, dl_ms, total_ms, app, xt, xs;
}'
done
6 changes: 6 additions & 0 deletions bench/k6/personalization.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ export default function () {
'body ~4KB (uncompressed)': (r) => r.headers['Content-Encoding'] === 'gzip' || r.body.length === 4096,
});

if (res.status !== 200) {
console.log(
`[WAF-DIAG] status=${res.status} trace=${res.headers['x-amzn-trace-id'] || 'N/A'}`
);
}

const st = res.headers['Server-Timing'];
if (st) {
const m = /dur=([\d.]+)/.exec(st);
Expand Down
80 changes: 80 additions & 0 deletions bench/run-linode.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# run-linode.sh — full Linode measurement run for one vantage point.
#
# Produces, under results/<vantage>-<timestamp>/:
# meta.txt run metadata (versions, host, IP, protocol state)
# cold.csv interleaved cold-path waterfall across all tiers (curl)
# warm-t*.json k6 warm-path summaries per tier (p50/p90/p95/p99)
#
# Tiers are interleaved request-by-request so time-of-day effects hit all tiers
# equally. T0 is DNS-pinned to the origin IP; T1/T2 resolve normally.
#
# Env:
# DOMAIN required, apex domain for T0/T1 (t0/t1.<DOMAIN>)
# ORIGIN_IP required, Linode origin public IP (for T0 pin)
# VANTAGE label for this probe location (default: "local")
# SAMPLES samples per tier (default 1000)
# VUS k6 concurrency (default 10)
set -euo pipefail

DOMAIN="${DOMAIN:?set DOMAIN}"
ORIGIN_IP="${ORIGIN_IP:?set ORIGIN_IP}"
VANTAGE="${VANTAGE:-local}"
SAMPLES="${SAMPLES:-1000}"
VUS="${VUS:-10}"

HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
TS="$(date +%Y%m%d-%H%M%S)"
OUT="$ROOT/results/${VANTAGE}-${TS}"
mkdir -p "$OUT"

T0="t0.${DOMAIN}"
T1="t1.${DOMAIN}"
T2="linode-test.chase.com"

# --- metadata ---
{
echo "vantage=$VANTAGE"
echo "timestamp=$TS"
echo "domain=$DOMAIN"
echo "origin_ip=$ORIGIN_IP"
echo "samples_per_tier=$SAMPLES"
echo "vus=$VUS"
echo "curl=$(curl --version | head -1)"
echo "openssl=$(curl --version | tr ' ' '\n' | grep -i openssl || true)"
echo "tiers: t0(pinned)=$T0 t1=$T1 t2=$T2"
} > "$OUT/meta.txt"
echo "==> metadata -> $OUT/meta.txt"

# --- cold path: interleaved across tiers ---
COLD="$OUT/cold.csv"
PRINT_HEADER=1 "$HERE/curl/waterfall.sh" "$T0" 0 "$ORIGIN_IP" > "$COLD" # header only (count 0)
echo "==> cold path ($SAMPLES/tier, interleaved) -> $COLD"
for ((i = 1; i <= SAMPLES; i++)); do
PRINT_HEADER=0 "$HERE/curl/waterfall.sh" "$T0" 1 "$ORIGIN_IP" >> "$COLD"
PRINT_HEADER=0 "$HERE/curl/waterfall.sh" "$T1" 1 >> "$COLD"
PRINT_HEADER=0 "$HERE/curl/waterfall.sh" "$T2" 1 >> "$COLD"
if ((i % 100 == 0)); then echo " cold: $i/$SAMPLES per tier"; fi
done

# --- warm path: k6 per tier (local k6 or dockerized) ---
run_k6() {
local url="$1" pin="$2" out="$3"
if command -v k6 >/dev/null 2>&1; then
TARGET_URL="$url" PIN_IP="$pin" SAMPLES="$SAMPLES" VUS="$VUS" \
k6 run --summary-export "$out" "$HERE/k6/personalization.js"
else
docker run --rm -i -v "$HERE/k6":/k6 \
-e TARGET_URL="$url" -e PIN_IP="$pin" -e SAMPLES="$SAMPLES" -e VUS="$VUS" \
grafana/k6:latest run --summary-export /k6/summary.tmp.json /k6/personalization.js
cp "$HERE/k6/summary.tmp.json" "$out" && rm -f "$HERE/k6/summary.tmp.json"
fi
}

echo "==> warm path (k6)"
run_k6 "https://$T0/v1/personalization" "$ORIGIN_IP" "$OUT/warm-t0.json"
run_k6 "https://$T1/v1/personalization" "" "$OUT/warm-t1.json"
run_k6 "https://$T2/v1/personalization" "" "$OUT/warm-t2.json"

echo "==> done. results in $OUT"
Loading