Created
April 17, 2026 17:39
-
-
Save hexsprite/4bd18da914eee1d763e2730457cf3729 to your computer and use it in GitHub Desktop.
Network repro matrix script for comparing intermittent curl timing stalls across Macs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| REQUESTS="${REQUESTS:-15}" | |
| STALL_SECS="${STALL_SECS:-0.5}" | |
| MAX_TIME="${MAX_TIME:-45}" | |
| CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-40}" | |
| OUTFILE="${OUTFILE:-network_repro_$(date +%Y%m%d_%H%M%S).csv}" | |
| usage() { | |
| cat <<'EOF' | |
| Usage: | |
| ./network_repro_matrix.sh [requests] | |
| Environment overrides: | |
| REQUESTS=20 Number of requests per target/mode | |
| STALL_SECS=0.5 Threshold used to mark a request as a stall | |
| MAX_TIME=45 curl --max-time | |
| CONNECT_TIMEOUT=40 curl --connect-timeout | |
| OUTFILE=results.csv CSV output path | |
| What it does: | |
| - tests several different sites | |
| - runs each site in normal mode and IPv4-only mode (-4) | |
| - records DNS, connect, TLS, first-byte, and total time | |
| - prints a short summary and writes raw rows to CSV | |
| EOF | |
| } | |
| if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then | |
| usage | |
| exit 0 | |
| fi | |
| if [[ $# -ge 1 ]]; then | |
| REQUESTS="$1" | |
| fi | |
| if ! command -v curl >/dev/null 2>&1; then | |
| echo "curl is required" >&2 | |
| exit 1 | |
| fi | |
| if ! command -v python3 >/dev/null 2>&1; then | |
| echo "python3 is required" >&2 | |
| exit 1 | |
| fi | |
| targets=( | |
| "nytimes|https://static01.nyt.com/images/2026/04/15/multimedia/15DC-SURVEILLANCE-vkgp/15DC-SURVEILLANCE-vkgp-threeByTwoMediumAt2X.jpg?format=pjpg&quality=75&auto=webp&disable=upscale" | |
| "google|https://www.google.com/generate_204" | |
| "cloudflare|https://www.cloudflare.com/cdn-cgi/trace" | |
| "apple|https://www.apple.com/library/test/success.html" | |
| "example|https://example.com/" | |
| ) | |
| tmpfile="$(mktemp)" | |
| trap 'rm -f "$tmpfile"' EXIT | |
| printf 'label,mode,run,http_code,remote_ip,exit_code,dns,connect,tls,starttransfer,total,stall\n' > "$OUTFILE" | |
| echo "Writing raw results to $OUTFILE" | |
| echo "Requests per target/mode: $REQUESTS" | |
| echo "Stall threshold: ${STALL_SECS}s" | |
| echo | |
| for entry in "${targets[@]}"; do | |
| label="${entry%%|*}" | |
| url="${entry#*|}" | |
| for mode in default ipv4; do | |
| curl_args=() | |
| if [[ "$mode" == "ipv4" ]]; then | |
| curl_args+=("-4") | |
| fi | |
| echo "== $label [$mode] ==" | |
| for run in $(seq 1 "$REQUESTS"); do | |
| fmt='http_code=%{http_code} remote_ip=%{remote_ip} dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} start=%{time_starttransfer} total=%{time_total}' | |
| set +e | |
| result="$( | |
| curl -sS -o /dev/null \ | |
| --max-time "$MAX_TIME" \ | |
| --connect-timeout "$CONNECT_TIMEOUT" \ | |
| "${curl_args[@]}" \ | |
| -w "$fmt" \ | |
| "$url" 2>&1 | |
| )" | |
| curl_exit=$? | |
| set -e | |
| python3 - "$label" "$mode" "$run" "$curl_exit" "$STALL_SECS" "$OUTFILE" "$result" <<'PY' | |
| import csv | |
| import re | |
| import sys | |
| label, mode, run, curl_exit, stall_secs, outfile, result = sys.argv[1:] | |
| fields = { | |
| "http_code": "000", | |
| "remote_ip": "", | |
| "dns": "", | |
| "connect": "", | |
| "tls": "", | |
| "start": "", | |
| "total": "", | |
| } | |
| for key in list(fields): | |
| m = re.search(rf"{key}=([^ \n]+)", result) | |
| if m: | |
| fields[key] = m.group(1) | |
| try: | |
| total = float(fields["total"]) | |
| except Exception: | |
| total = float("inf") | |
| stall = "yes" if total > float(stall_secs) else "no" | |
| with open(outfile, "a", newline="") as f: | |
| writer = csv.writer(f) | |
| writer.writerow([ | |
| label, | |
| mode, | |
| run, | |
| fields["http_code"], | |
| fields["remote_ip"], | |
| curl_exit, | |
| fields["dns"], | |
| fields["connect"], | |
| fields["tls"], | |
| fields["start"], | |
| fields["total"], | |
| stall, | |
| ]) | |
| summary = ( | |
| f"run{run} total={fields['total']}s " | |
| f"dns={fields['dns']} connect={fields['connect']} " | |
| f"tls={fields['tls']} start={fields['start']} " | |
| f"ip={fields['remote_ip'] or '-'} code={fields['http_code']} " | |
| f"exit={curl_exit}" | |
| ) | |
| if stall == "yes": | |
| summary += " STALL" | |
| print(summary) | |
| if curl_exit != "0": | |
| print(result.strip()) | |
| PY | |
| done | |
| echo | |
| done | |
| done | |
| python3 - "$OUTFILE" <<'PY' | |
| import csv | |
| import statistics | |
| import sys | |
| from collections import defaultdict | |
| rows = list(csv.DictReader(open(sys.argv[1], newline=""))) | |
| groups = defaultdict(list) | |
| for row in rows: | |
| groups[(row["label"], row["mode"])].append(row) | |
| print("Summary:") | |
| for key in sorted(groups): | |
| bucket = groups[key] | |
| totals = [] | |
| stalls = 0 | |
| connect_over_1 = 0 | |
| start_over_1 = 0 | |
| for row in bucket: | |
| try: | |
| total = float(row["total"]) | |
| totals.append(total) | |
| if total > 0.5: | |
| stalls += 1 | |
| except Exception: | |
| pass | |
| try: | |
| if float(row["connect"]) > 1.0: | |
| connect_over_1 += 1 | |
| except Exception: | |
| pass | |
| try: | |
| if float(row["starttransfer"]) > 1.0: | |
| start_over_1 += 1 | |
| except Exception: | |
| pass | |
| med = statistics.median(totals) if totals else float("nan") | |
| worst = max(totals) if totals else float("nan") | |
| print( | |
| f"- {key[0]:11s} {key[1]:7s} " | |
| f"stalls={stalls}/{len(bucket)} " | |
| f"median={med:.3f}s worst={worst:.3f}s " | |
| f"connect>1s={connect_over_1} start>1s={start_over_1}" | |
| ) | |
| PY | |
| echo | |
| echo "Done. Raw rows are in $OUTFILE" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment