@@ -194,3 +194,151 @@ func TestRotatingAuth(t *testing.T) {
194194 mockSigner .AssertExpectations (t )
195195 })
196196}
197+
198+ // BenchmarkRotatingAuth_Headers_CachedPath benchmarks the fast path where headers are cached and within TTL.
199+ // This is the most common case in production.
200+ func BenchmarkRotatingAuth_Headers_CachedPath (b * testing.B ) {
201+
202+ pubKey , privKey , err := ed25519 .GenerateKey (nil )
203+ require .NoError (b , err )
204+
205+ mockSigner := & MockSigner {}
206+ dummySignature := ed25519 .Sign (privKey , []byte ("test data" ))
207+
208+ mockSigner .
209+ On ("Sign" , mock .Anything , mock .Anything , mock .Anything ).
210+ Return (dummySignature , nil ).
211+ Maybe ()
212+
213+ // Use a long TTL so headers don't expire during the benchmark
214+ ttl := 1 * time .Hour
215+ auth := beholder .NewRotatingAuth (pubKey , mockSigner , ttl , false )
216+
217+ // Prime the cache by calling Headers once
218+ ctx := b .Context ()
219+ _ , err = auth .Headers (ctx )
220+ require .NoError (b , err )
221+
222+ b .ReportAllocs ()
223+
224+ for b .Loop () {
225+ headers , err := auth .Headers (ctx )
226+ if err != nil {
227+ b .Fatal (err )
228+ }
229+ if len (headers ) == 0 {
230+ b .Fatal ("expected non-empty headers" )
231+ }
232+ }
233+ }
234+
235+ // BenchmarkRotatingAuth_Headers_ExpiredPath benchmarks the slow path where headers need to be regenerated.
236+ // This happens when TTL expires.
237+ func BenchmarkRotatingAuth_Headers_ExpiredPath (b * testing.B ) {
238+
239+ pubKey , privKey , err := ed25519 .GenerateKey (nil )
240+ require .NoError (b , err )
241+
242+ mockSigner := & MockSigner {}
243+ dummySignature := ed25519 .Sign (privKey , []byte ("test data" ))
244+
245+ mockSigner .
246+ On ("Sign" , mock .Anything , mock .Anything , mock .Anything ).
247+ Return (dummySignature , nil ).
248+ Maybe ()
249+
250+ // Use a TTL of 0 to force regeneration on every call
251+ ttl := 0 * time .Second
252+ auth := beholder .NewRotatingAuth (pubKey , mockSigner , ttl , false )
253+
254+ ctx := b .Context ()
255+
256+ b .ReportAllocs ()
257+
258+ for b .Loop () {
259+ headers , err := auth .Headers (ctx )
260+ if err != nil {
261+ b .Fatal (err )
262+ }
263+ if len (headers ) == 0 {
264+ b .Fatal ("expected non-empty headers" )
265+ }
266+ }
267+ }
268+
269+ // BenchmarkRotatingAuth_Headers_ParallelCached benchmarks concurrent access when headers are cached.
270+ // This simulates multiple goroutines making concurrent requests with valid cached headers.
271+ func BenchmarkRotatingAuth_Headers_ParallelCached (b * testing.B ) {
272+
273+ pubKey , privKey , err := ed25519 .GenerateKey (nil )
274+ require .NoError (b , err )
275+
276+ mockSigner := & MockSigner {}
277+ dummySignature := ed25519 .Sign (privKey , []byte ("test data" ))
278+
279+ mockSigner .
280+ On ("Sign" , mock .Anything , mock .Anything , mock .Anything ).
281+ Return (dummySignature , nil ).
282+ Maybe ()
283+
284+ // Use a long TTL so headers don't expire during the benchmark
285+ ttl := 1 * time .Hour
286+ auth := beholder .NewRotatingAuth (pubKey , mockSigner , ttl , false )
287+
288+ // Prime the cache
289+ ctx := b .Context ()
290+ _ , err = auth .Headers (ctx )
291+ require .NoError (b , err )
292+
293+ b .ResetTimer ()
294+ b .ReportAllocs ()
295+
296+ b .RunParallel (func (pb * testing.PB ) {
297+ for pb .Next () {
298+ headers , err := auth .Headers (ctx )
299+ if err != nil {
300+ b .Fatal (err )
301+ }
302+ if len (headers ) == 0 {
303+ b .Fatal ("expected non-empty headers" )
304+ }
305+ }
306+ })
307+ }
308+
309+ // BenchmarkRotatingAuth_Headers_ParallelExpired benchmarks concurrent access when headers expire.
310+ // This tests contention on the mutex when multiple goroutines race to regenerate headers.
311+ func BenchmarkRotatingAuth_Headers_ParallelExpired (b * testing.B ) {
312+
313+ pubKey , privKey , err := ed25519 .GenerateKey (nil )
314+ require .NoError (b , err )
315+
316+ mockSigner := & MockSigner {}
317+ dummySignature := ed25519 .Sign (privKey , []byte ("test data" ))
318+
319+ mockSigner .
320+ On ("Sign" , mock .Anything , mock .Anything , mock .Anything ).
321+ Return (dummySignature , nil ).
322+ Maybe ()
323+
324+ // Use a short TTL to cause periodic regeneration
325+ ttl := 10 * time .Millisecond
326+ auth := beholder .NewRotatingAuth (pubKey , mockSigner , ttl , false )
327+
328+ ctx := b .Context ()
329+
330+ b .ResetTimer ()
331+ b .ReportAllocs ()
332+
333+ b .RunParallel (func (pb * testing.PB ) {
334+ for pb .Next () {
335+ headers , err := auth .Headers (ctx )
336+ if err != nil {
337+ b .Fatal (err )
338+ }
339+ if len (headers ) == 0 {
340+ b .Fatal ("expected non-empty headers" )
341+ }
342+ }
343+ })
344+ }
0 commit comments