Skip to content

Commit f4a255b

Browse files
committed
powclient: add parallel benchmarks
1 parent a0115cd commit f4a255b

2 files changed

Lines changed: 228 additions & 0 deletions

File tree

.idea/.gitignore

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

benchmark_test.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package powclient
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"net/http/httptest"
10+
"sync"
11+
"sync/atomic"
12+
"testing"
13+
"time"
14+
)
15+
16+
const benchmarkLargeChallenge = "849387260465695603243"
17+
18+
func BenchmarkSolveSemiprime(b *testing.B) {
19+
benchmarks := []struct {
20+
name string
21+
challenge string
22+
parallel bool
23+
wantErr error
24+
}{
25+
{name: "small_semiprime_serial", challenge: "35"},
26+
{name: "small_semiprime_parallel", challenge: "35", parallel: true},
27+
{name: "large_semiprime_serial", challenge: benchmarkLargeChallenge},
28+
{name: "large_semiprime_parallel", challenge: benchmarkLargeChallenge, parallel: true},
29+
{name: "invalid_prime_parallel", challenge: "13", parallel: true, wantErr: ErrInvalidChallenge},
30+
}
31+
32+
for _, bm := range benchmarks {
33+
b.Run(bm.name, func(b *testing.B) {
34+
b.ReportAllocs()
35+
36+
call := func() error {
37+
_, err := solveSemiprime(context.Background(), bm.challenge)
38+
if bm.wantErr != nil {
39+
if !errors.Is(err, bm.wantErr) {
40+
return fmt.Errorf("solveSemiprime(%q) error = %v, want %v", bm.challenge, err, bm.wantErr)
41+
}
42+
return nil
43+
}
44+
if err != nil {
45+
return fmt.Errorf("solveSemiprime(%q): %w", bm.challenge, err)
46+
}
47+
return nil
48+
}
49+
50+
if bm.parallel {
51+
runParallelBenchmark(b, 0, call)
52+
return
53+
}
54+
55+
for i := 0; i < b.N; i++ {
56+
if err := call(); err != nil {
57+
b.Fatal(err)
58+
}
59+
}
60+
})
61+
}
62+
}
63+
64+
func BenchmarkRetToken(b *testing.B) {
65+
benchmarks := []struct {
66+
name string
67+
challenge string
68+
parallel bool
69+
isolated bool
70+
timeout time.Duration
71+
wantErr error
72+
hookDelay time.Duration
73+
requestDelay time.Duration
74+
}{
75+
{name: "reused_transport_serial", challenge: "35"},
76+
{name: "reused_transport_parallel", challenge: "35", parallel: true, requestDelay: 500 * time.Microsecond},
77+
{name: "isolated_transport_parallel", challenge: "35", parallel: true, isolated: true, requestDelay: 500 * time.Microsecond},
78+
{name: "timeout_path_parallel", challenge: benchmarkLargeChallenge, parallel: true, timeout: time.Millisecond, wantErr: context.DeadlineExceeded, hookDelay: 2 * time.Millisecond},
79+
}
80+
81+
for _, bm := range benchmarks {
82+
b.Run(bm.name, func(b *testing.B) {
83+
clearTransportCache()
84+
server, requests := newBenchmarkPowServer(bm.challenge, bm.requestDelay)
85+
b.Cleanup(func() {
86+
server.Close()
87+
clearTransportCache()
88+
})
89+
90+
if bm.hookDelay > 0 {
91+
originalHook := pollardRhoStepHook
92+
pollardRhoStepHook = func() {
93+
time.Sleep(bm.hookDelay)
94+
}
95+
b.Cleanup(func() {
96+
pollardRhoStepHook = originalHook
97+
})
98+
}
99+
100+
var serialCounter uint64
101+
call := func() error {
102+
params := newBenchmarkGetTokenParams(server.URL, bm.timeout)
103+
if bm.isolated {
104+
params.SNI = fmt.Sprintf("bench-%d", atomic.AddUint64(&serialCounter, 1))
105+
}
106+
107+
_, err := RetToken(params)
108+
if bm.isolated {
109+
key := transportCacheKey(params)
110+
if value, ok := transportCache.Load(key); ok {
111+
if transport, ok := value.(*http.Transport); ok {
112+
transport.CloseIdleConnections()
113+
}
114+
transportCache.Delete(key)
115+
}
116+
}
117+
if bm.wantErr != nil {
118+
if !errors.Is(err, bm.wantErr) {
119+
return fmt.Errorf("RetToken() error = %v, want %v", err, bm.wantErr)
120+
}
121+
return nil
122+
}
123+
if err != nil {
124+
return fmt.Errorf("RetToken(): %w", err)
125+
}
126+
return nil
127+
}
128+
129+
b.ReportAllocs()
130+
if bm.parallel {
131+
b.SetParallelism(4)
132+
runParallelBenchmark(b, 2, call)
133+
} else {
134+
for i := 0; i < b.N; i++ {
135+
if err := call(); err != nil {
136+
b.Fatal(err)
137+
}
138+
}
139+
}
140+
141+
if requests.Load() == 0 {
142+
b.Fatal("benchmark server did not receive any requests")
143+
}
144+
})
145+
}
146+
}
147+
148+
func newBenchmarkPowServer(challenge string, requestDelay time.Duration) (*httptest.Server, *atomic.Int64) {
149+
requests := &atomic.Int64{}
150+
151+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
152+
requests.Add(1)
153+
if requestDelay > 0 {
154+
time.Sleep(requestDelay)
155+
}
156+
157+
switch r.URL.Path {
158+
case "/request_challenge":
159+
_ = json.NewEncoder(w).Encode(RequestResponse{
160+
Challenge: Challenge{RequestID: "bench-req", Challenge: challenge},
161+
RequestTime: 123,
162+
})
163+
case "/submit_answer":
164+
_ = json.NewEncoder(w).Encode(SubmitResponse{Token: "token-123"})
165+
default:
166+
http.NotFound(w, r)
167+
}
168+
}))
169+
170+
return server, requests
171+
}
172+
173+
func newBenchmarkGetTokenParams(baseURL string, timeout time.Duration) *GetTokenParams {
174+
params := NewGetTokenParams()
175+
params.BaseUrl = baseURL
176+
params.TimeoutSec = timeout
177+
return params
178+
}
179+
180+
func runParallelBenchmark(b *testing.B, maxConcurrent int, fn func() error) {
181+
b.Helper()
182+
183+
var (
184+
once sync.Once
185+
firstErr error
186+
)
187+
188+
var limiter chan struct{}
189+
if maxConcurrent > 0 {
190+
limiter = make(chan struct{}, maxConcurrent)
191+
}
192+
193+
b.RunParallel(func(pb *testing.PB) {
194+
for pb.Next() {
195+
if limiter != nil {
196+
limiter <- struct{}{}
197+
}
198+
199+
err := fn()
200+
201+
if limiter != nil {
202+
<-limiter
203+
}
204+
if err != nil {
205+
once.Do(func() {
206+
firstErr = err
207+
})
208+
}
209+
}
210+
})
211+
212+
if firstErr != nil {
213+
b.Fatal(firstErr)
214+
}
215+
}
216+
217+
func clearTransportCache() {
218+
transportCache.Range(func(key, value any) bool {
219+
if transport, ok := value.(*http.Transport); ok {
220+
transport.CloseIdleConnections()
221+
}
222+
transportCache.Delete(key)
223+
return true
224+
})
225+
}

0 commit comments

Comments
 (0)