Add URL-level concurrency (-c flag, Step 7)

Probe multiple URLs in parallel with a bounded worker pool; the N samples
of each URL remain sequential to preserve accurate min/avg/max statistics.
Default auto-concurrency is min(numURLs, 8); -c 1 restores serial mode.
Output is always buffered and printed in original input order. Verified
clean with go test -race.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 01:33:03 +02:00
parent 45583ac2be
commit 9a69ba946f
9 changed files with 394 additions and 22 deletions

View File

@@ -32,6 +32,7 @@ make help # list all targets with descriptions
| `go-run` | Build and run (pass flags via `ARGS=`) |
| `go-test` | `go test ./...` |
| `go-test-verbose` | `go test -v ./...` — per-case PASS/FAIL output |
| `go-test-race` | `go test -race ./...` — run tests with the data-race detector |
| `go-check` | `go-fmt` + `go-vet` + `go-test` |
| `go-cover` | Coverage report → `go/coverage.html` |
| `go-fmt` | `gofmt -w go/` |
@@ -54,6 +55,9 @@ make test
# Verbose output showing each test case
make go-test-verbose
# Run tests with the race detector (verify concurrency safety)
make go-test-race
# Pre-commit gate (format, vet, test all in one)
make check

View File

@@ -0,0 +1,76 @@
# Step 7 — Concurrency
`latprobe` now probes multiple URLs in parallel, cutting wall-clock time for
large runs without sacrificing measurement accuracy.
## How it works
| Axis | Behaviour |
|------|-----------|
| Across URLs | **Parallel** — up to `-c` workers run simultaneously |
| Samples of one URL (`-n`) | **Sequential** — always serial within a URL |
Keeping samples sequential ensures that min/avg/max statistics for a single URL
reflect genuine latency variability, not artificial load created by firing
multiple requests at the same server at once.
Output is always printed in **input order**, regardless of which URL finishes
first.
## Flag
```
-c, --concurrency int Max URLs probed in parallel (0 = auto, default)
```
| Value | Behaviour |
|-------|-----------|
| `0` (default) | Auto: `min(numURLs, 8)` — scales with workload, caps at 8 |
| `1` | Fully serial — identical to the old behaviour; best for precision |
| `N > 1` | Explicit worker cap |
## When to use serial mode
For the most accurate latency numbers — especially when comparing sites —
use `-c 1`. Concurrent probes share your local NIC, DNS resolver, and CPU,
which can inflate timings on slower machines or fast batch runs.
```sh
# High fidelity: one URL at a time
./go/latprobe -c 1 -n 10 https://www.google.com https://www.bbc.co.uk
# Speed: all four probed in parallel (auto default would do this anyway)
./go/latprobe -c 4 -n 10 https://www.google.com https://www.bbc.co.uk \
https://www.lemonde.fr https://www.abc.net.au
```
## Example: speedup in practice
Five sites, 5 samples each — sequential vs parallel:
```sh
# Serial (-c 1): ~wall time ≈ sum of all RTTs × 5
time ./go/latprobe -c 1 -n 5 \
https://www.google.com https://www.bbc.co.uk \
https://www.lemonde.fr https://www.spiegel.de https://www.abc.net.au
# real ~1m15s (5 sites × ~15s sequential)
# Parallel (default): ~wall time ≈ slowest single URL × 5
time ./go/latprobe -n 5 \
https://www.google.com https://www.bbc.co.uk \
https://www.lemonde.fr https://www.spiegel.de https://www.abc.net.au
# real ~18s (all 5 measured at once, gated by the slowest)
```
## Exit codes
Exit codes work exactly as before: when multiple failure types occur the
**highest** code wins. Concurrency does not change this — exit codes are
accumulated after all workers complete.
## Race safety
The implementation uses a semaphore channel (`chan struct{}`) and
`sync.WaitGroup` with each goroutine writing only its own indexed result slot,
so there is no shared mutable state. `http.DefaultClient` is safe for concurrent
use. Verified clean with `go test -race` (`make go-test-race`).