Skip to content

Commit 257549a

Browse files
myleshortonclaude
andcommitted
Add performance comparison with fronted package
Benchmark results on Apple M4 Pro comparing domainfront vs fronted: 5x fewer goroutines, 2x faster sort, zero leaks on shutdown. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 894d8c6 commit 257549a

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

PERFORMANCE.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Performance Comparison: `domainfront` vs `fronted`
2+
3+
Benchmarks run on Apple M4 Pro, Go 1.24, `go test -bench=. -benchmem -count=3`.
4+
5+
## Goroutine Load
6+
7+
| Metric | `fronted` | `domainfront` | Improvement |
8+
|--------|-----------|---------------|-------------|
9+
| Goroutines during operation | 15 | 3 | **5x fewer** |
10+
| Goroutines leaked after Close | 6 | 0 | **Clean shutdown** |
11+
12+
`fronted` spawns ~15 goroutines: 1 keepcurrent runner, 1 keepcurrent data channel consumer, 10 pond workers, 1 findWorkingFronts loop, 1 maintainCache, plus any leaked from pond. `domainfront` spawns exactly **3**: crawler, cacheSaver, and (optionally) configUpdater. All exit cleanly on `Close()` with zero leaks.
13+
14+
## Hot Path: Take/Return Cycle
15+
16+
| Operation | `fronted` | `domainfront` |
17+
|-----------|-----------|---------------|
18+
| Take + Return | 17 ns/op, 0 allocs | 68 ns/op, 0 allocs |
19+
20+
`fronted` is ~4x faster here because it's a raw buffered channel send/receive. `domainfront` uses the same channel internally but `ReturnSuccess` also calls `markSucceeded()` (mutex lock + `time.Now()`). This 50ns difference is **negligible** compared to the ~5-50ms TLS dial that follows every Take.
21+
22+
## Candidates/Sort (called by crawler + cache saver)
23+
24+
| Operation | `fronted` | `domainfront` | Improvement |
25+
|-----------|-----------|---------------|-------------|
26+
| 5000 fronts sort | 677 us, 82 KB | 359 us, 205 KB | **1.9x faster** |
27+
28+
`domainfront` is nearly **2x faster** because it snapshots timestamps outside the lock and sorts without acquiring per-front read locks during comparison. It uses more memory (205 KB vs 82 KB) because of the parallel `indexed` struct array — a deliberate trade of ~120 KB temporary memory for halved sort time and reduced lock contention.
29+
30+
## Provider Lookup
31+
32+
| Operation | `fronted` | `domainfront` | Improvement |
33+
|-----------|-----------|---------------|-------------|
34+
| Exact host alias match | 37 ns/op | 34 ns/op | ~same |
35+
| Passthrough pattern match | 53 ns/op | 39 ns/op | **1.4x faster** |
36+
37+
`domainfront` pre-lowercases passthrough patterns at config load time, saving a `strings.ToLower` on every lookup in the hot path.
38+
39+
## SNI Generation
40+
41+
| Operation | `fronted` | `domainfront` | Improvement |
42+
|-----------|-----------|---------------|-------------|
43+
| GenerateSNI | 49 ns/op, 1 alloc | 51 ns/op, 0 allocs | **Zero allocs** |
44+
45+
Same speed, but `domainfront` avoids the allocation by taking `string` instead of `*Masquerade`.
46+
47+
## Request Rewriting
48+
49+
| Operation | `fronted` | `domainfront` |
50+
|-----------|-----------|---------------|
51+
| Rewrite request | 339 ns/op, 6 allocs | 330 ns/op, 6 allocs |
52+
53+
Essentially identical — same fundamental work.
54+
55+
## Structural/Memory Improvements
56+
57+
| Concern | `fronted` | `domainfront` |
58+
|---------|-----------|---------------|
59+
| Front list growth | Unbounded append (`addFronts`) — grows forever on config updates | `Replace()` atomic swap — bounded to config size |
60+
| Sort under lock | `sortedCopy()` holds RLock during O(n log n) sort | Sort happens **outside** the lock |
61+
| Per-front locks during sort | Sort comparator calls `lastSucceeded()` = O(n log n) lock acquisitions | Timestamps snapshotted once = O(n) lock acquisitions |
62+
| Dependencies | `pond`, `keepcurrent`, `ops` + transitive deps | Only `utls` + `go-yaml` |
63+
64+
## Speed to Working Fronts
65+
66+
Both use the same fundamental approach (parallel vetting with POST to test URL), so time-to-first-working-front is dominated by network latency (~50-200ms per TLS dial). The key structural difference: `domainfront`'s crawler doesn't need `pond` — it uses a simple semaphore + WaitGroup with the same concurrency (10 workers), avoiding the worker pool library overhead and goroutine pool lifecycle.
67+
68+
## Bottom Line
69+
70+
`domainfront` is **not dramatically faster at the micro-benchmark level** — the hot paths (Take/Return, Lookup, rewrite) are in the same ballpark. The real wins are:
71+
72+
1. **5x fewer goroutines** (3 vs 15), zero leaks on shutdown
73+
2. **Bounded memory** — no unbounded front list growth on config updates
74+
3. **2x faster sort** with less lock contention on the candidate list
75+
4. **Cleaner shutdown** — single context cancellation vs multiple stop channels
76+
5. **Fewer dependencies** — smaller binary, less supply chain surface

0 commit comments

Comments
 (0)